From 30ccaf51a8720ca101d8cdb15500d5fd120baa62 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Sun, 21 Jun 2026 00:48:26 +0200 Subject: [PATCH] fix(updater): rebuild the update modal on a reusable Modal (#1142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ui): add reusable Modal component Portal + dimmed backdrop (no backdrop-filter — on WebKitGTK the blur bleeds onto modal content on some GPU stacks), Escape/backdrop/X to close, header (icon/title/subtitle) + scrollable body + footer slot. First consumer is the update modal; the other hand-rolled modals migrate onto it over time. * fix(updater): rebuild the update modal on the reusable Modal Fixes a user report (Manjaro / AMD / X11): blurry modal text (the eq-popup backdrop-filter bled onto the content), the version arrow sitting between the two header rows, and the unclear Skip / Remind buttons. Now uses the no-blur Modal shell; the header icon aligns with the title; Skip is a clear button and Remind me later is the accent action when there is no in-app install. Drops the now-dead update-modal shell CSS. * docs(changelog): update notification popup fix (PR #1142) --- CHANGELOG.md | 6 + src/components/AppUpdater.tsx | 409 ++++++++++++++--------------- src/components/Modal.tsx | 78 ++++++ src/styles/components/index.css | 1 + src/styles/components/ui-modal.css | 98 +++++++ src/styles/layout/update-toast.css | 53 +--- 6 files changed, 382 insertions(+), 263 deletions(-) create mode 100644 src/components/Modal.tsx create mode 100644 src/styles/components/ui-modal.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f65b134..0c7188ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -146,6 +146,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Track-advance queue pushes no longer suspend idle auto-pull, so the connection LED does not flash yellow on every song change. Yellow sync still appears after a local queue edit while paused; it clears while audio is playing. +### Update notification — clearer popup on Linux + +**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1142](https://github.com/Psychotoxical/psysonic/pull/1142)**, reported by zunoz on Discord + +* The "new version available" popup no longer shows blurry, unfocused text on some Linux setups (the background blur could bleed onto the dialog). The version arrow now lines up with the heading, and the Skip / Remind me later buttons read clearly — Remind me later is the highlighted action when there's no in-app installer. + ## [1.48.1] - 2026-06-15 diff --git a/src/components/AppUpdater.tsx b/src/components/AppUpdater.tsx index 30383772..42e3ec5d 100644 --- a/src/components/AppUpdater.tsx +++ b/src/components/AppUpdater.tsx @@ -1,10 +1,11 @@ -import { createPortal } from 'react-dom'; +import { type ReactNode } from 'react'; import { open } from '@tauri-apps/plugin-shell'; -import { ArrowUpCircle, CheckCircle2, ChevronDown, Download, FolderOpen, RefreshCw, ShieldCheck, X } from 'lucide-react'; +import { ArrowUpCircle, CheckCircle2, ChevronDown, Download, FolderOpen, RefreshCw, ShieldCheck } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { version as currentVersion } from '../../package.json'; import { formatBytes } from '../utils/format/formatBytes'; import { useAppUpdater } from '../hooks/useAppUpdater'; +import Modal from './Modal'; import Changelog from './appUpdater/Changelog'; export default function AppUpdater() { @@ -18,222 +19,208 @@ export default function AppUpdater() { if (!release || dismissed) return null; - return createPortal( - <> -
setDismissed(true)} style={{ zIndex: 3000 }} /> -
e.stopPropagation()} - > - {/* Header */} -
- -
- {t('common.updaterModalTitle')} - - v{currentVersion} → v{release.version} - -
- +
+ + {showInstallBtn && ( + -
+ )} + + ); + } else if (dlState === 'done' && useTauriUpdater) { + footer = ( + <> +
+ + + ); + } else if (dlState === 'done') { + footer = ( + <> +
+ + + ); + } else if (dlState === 'error') { + footer = ( + <> + +
+ + + ); + } - {/* Scrollable body: changelog + download area — single overflow container */} -
- {/* Collapsible Changelog */} - {release.body && ( -
- - {changelogOpen && ( -
- -
- )} -
- )} - - {/* Download / AUR area */} -
- {showAurHint ? ( -
-
{t('common.updaterAurHint')}
- yay -S psysonic-bin - sudo pacman -Syu psysonic-bin -
- ) : useTauriUpdater ? ( - <> - {dlState === 'idle' && ( -
-
- {t('common.updaterMacReadyTitle', { defaultValue: 'Ready to install' })} -
-
- {t('common.updaterMacReady', { - defaultValue: 'The update downloads, verifies and applies in place — no DMG needed. The app restarts automatically when done.', - })} -
-
- - - {t('common.updaterTrustNotarized', { defaultValue: 'Notarized by Apple' })} - - - - {t('common.updaterTrustSignature', { defaultValue: 'Signature verified' })} - -
-
- )} - {dlState === 'downloading' && ( -
-
-
-
- {pct}% - - {formatBytes(dlProgress.bytes)} - {dlProgress.total > 0 && ` / ${formatBytes(dlProgress.total)}`} - -
- )} - {dlState === 'done' && ( -
- -
- {t('common.updaterMacDoneTitle', { defaultValue: 'Update installed' })} -
-
- {countdown !== null - ? t('common.updaterRestartingIn', { defaultValue: 'Restarting in {{n}}s…', n: countdown }) - : t('common.updaterRestarting', { defaultValue: 'Restarting…' })} -
-
- )} - {dlState === 'error' && ( -
{dlError || t('common.updaterErrorMsg')}
- )} - - ) : asset ? ( - <> - {dlState === 'idle' && ( -
- {asset.name} - {formatBytes(asset.size)} -
- )} - {dlState === 'downloading' && ( -
-
-
-
- {pct}% - - {formatBytes(dlProgress.bytes)} - {dlProgress.total > 0 && ` / ${formatBytes(dlProgress.total)}`} - -
- )} - {dlState === 'done' && ( -
-
{t('common.updaterDone')}
-
{t('common.updaterInstallHint')}
- -
- )} - {dlState === 'error' && ( -
{dlError || t('common.updaterErrorMsg')}
- )} - - ) : ( -
- + return ( + setDismissed(true)} + icon={} + title={t('common.updaterModalTitle')} + subtitle={<>v{currentVersion} → v{release.version}} + closeLabel={t('common.updaterRemindBtn')} + footer={footer} + > + {/* Collapsible changelog */} + {release.body && ( +
+ + {changelogOpen && ( +
+
)}
-
{/* end update-modal-body */} + )} - {/* Footer buttons — state-dependent to avoid redundant/jumping buttons */} -
- {dlState === 'idle' && ( - <> - -
- - {showInstallBtn && ( - - )} - - )} - {dlState === 'downloading' &&
} - {dlState === 'done' && useTauriUpdater && ( - <> -
- - - )} - {dlState === 'done' && !useTauriUpdater && ( - <> -
- - - )} - {dlState === 'error' && ( - <> -
- - - - )} -
+
+ )} + {dlState === 'error' && ( +
{dlError || t('common.updaterErrorMsg')}
+ )} + + ) : ( +
+ +
+ )}
- , - document.body + ); } diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 00000000..bf1a78fe --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,78 @@ +import { type ReactNode, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { X } from 'lucide-react'; + +interface ModalProps { + open: boolean; + /** Called on Escape, backdrop click, and the header close button. */ + onClose: () => void; + title: ReactNode; + /** Smaller line under the title (e.g. a version range). */ + subtitle?: ReactNode; + /** Leading icon, aligned with the title line. */ + icon?: ReactNode; + /** Action row pinned to the bottom, above the modal edge. */ + footer?: ReactNode; + size?: 'sm' | 'md' | 'lg'; + /** Hide the header close (X) button. */ + hideClose?: boolean; + /** Accessible label / tooltip for the close button. */ + closeLabel?: string; + children: ReactNode; +} + +/** + * Reusable modal shell: a portal to `document.body` with an opaque, centered + * card over a dimmed backdrop. Closes on Escape, backdrop click and the header + * X. Deliberately uses **no `backdrop-filter`** — on WebKitGTK (some GPU stacks) + * the blur bleeds onto the modal content, making text look unfocused. The other + * hand-rolled modals migrate onto this over time. + */ +export default function Modal({ + open, onClose, title, subtitle, icon, footer, size = 'md', hideClose, closeLabel, children, +}: ModalProps) { + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [open, onClose]); + + if (!open) return null; + + return createPortal( +
+
e.stopPropagation()} + > +
+ {icon && {icon}} +
+ {title} + {subtitle != null && {subtitle}} +
+ {!hideClose && ( + + )} +
+
{children}
+ {footer &&
{footer}
} +
+
, + document.body, + ); +} diff --git a/src/styles/components/index.css b/src/styles/components/index.css index 582c13aa..b29817e5 100644 --- a/src/styles/components/index.css +++ b/src/styles/components/index.css @@ -17,6 +17,7 @@ @import './share-queue-preview-modal.css'; @import './tracklist.css'; @import './modal.css'; +@import './ui-modal.css'; @import './playback-delay-sleep-delayed-start-modal.css'; @import './lucky-mix-pips-shared-by-the-inline-queue-lucky-cube-indicator.css'; @import './playback-buffering-overlay.css'; diff --git a/src/styles/components/ui-modal.css b/src/styles/components/ui-modal.css new file mode 100644 index 00000000..1f673ab9 --- /dev/null +++ b/src/styles/components/ui-modal.css @@ -0,0 +1,98 @@ +/* ─── Reusable Modal shell (src/components/Modal.tsx) ─── */ +@keyframes ui-modal-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +.ui-modal-backdrop { + position: fixed; + inset: 0; + z-index: 3000; + background: var(--modal-scrim, rgba(0, 0, 0, 0.5)); + display: flex; + align-items: center; + justify-content: center; + padding: 6vh 16px; + animation: fadeIn 150ms ease both; + /* No backdrop-filter: on WebKitGTK (some GPU stacks) the blur bleeds onto the + modal content and makes its text look unfocused. */ +} + +.ui-modal { + position: relative; + display: flex; + flex-direction: column; + width: min(520px, 94vw); + max-height: 88vh; + background: var(--bg-sidebar); + border: 1px solid var(--border); + border-radius: 14px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5); + animation: ui-modal-in 200ms ease both; +} +.ui-modal--sm { width: min(400px, 94vw); } +.ui-modal--lg { width: min(720px, 94vw); } + +.ui-modal-header { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 14px 16px 12px 18px; +} +.ui-modal-icon { + display: flex; + align-items: center; + margin-top: 1px; + color: var(--accent); + flex-shrink: 0; +} +.ui-modal-titles { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} +.ui-modal-title { + font-size: 15px; + font-weight: 700; + color: var(--text-primary); + line-height: 1.3; +} +.ui-modal-subtitle { + font-size: 12px; + color: var(--text-muted); +} +.ui-modal-close { + flex-shrink: 0; + display: flex; + align-items: center; + padding: 2px; + background: none; + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + color: var(--text-muted); + opacity: 0.7; + transition: opacity var(--transition-fast), color var(--transition-fast), background var(--transition-fast); +} +.ui-modal-close:hover { + opacity: 1; + color: var(--text-primary); + background: var(--bg-glass); +} + +.ui-modal-body { + flex: 1; + min-height: 0; + overflow-y: auto; +} + +.ui-modal-footer { + display: flex; + align-items: center; + gap: var(--space-2); + padding: 12px 18px; + border-top: 1px solid var(--border-subtle); + flex-shrink: 0; +} diff --git a/src/styles/layout/update-toast.css b/src/styles/layout/update-toast.css index e66403ac..a1a00e1f 100644 --- a/src/styles/layout/update-toast.css +++ b/src/styles/layout/update-toast.css @@ -34,18 +34,6 @@ text-transform: uppercase; flex: 1; } -.app-updater-dismiss { - background: none; - border: none; - cursor: pointer; - color: var(--text-muted); - padding: 0; - display: flex; - align-items: center; - opacity: 0.6; - transition: opacity var(--transition-fast); -} -.app-updater-dismiss:hover { opacity: 1; } .app-updater-version { font-size: 12px; color: var(--text-secondary); @@ -136,30 +124,7 @@ font-style: italic; } -/* ── Update Modal ────────────────────────────────────────────────────────── */ -.update-modal { - width: min(520px, 94vw); - gap: 0; - padding: 0; - max-height: 85vh; - display: flex; - flex-direction: column; -} -.update-modal-header { - padding: 14px 16px 14px 18px; -} -.update-modal-versions { - font-size: 12px; - color: var(--text-muted); - display: block; - margin-top: 2px; -} -/* Scrollable middle section (changelog + download area combined) */ -.update-modal-body { - flex: 1; - overflow-y: auto; - min-height: 0; -} +/* ── Update Modal — content styles (the shell is the reusable Modal) ── */ /* Changelog accordion */ .update-modal-changelog { border-top: 1px solid var(--border-subtle); @@ -318,22 +283,6 @@ .update-modal-asset-none { padding: 4px 0; } -/* Footer */ -.update-modal-footer { - display: flex; - align-items: center; - gap: var(--space-2); - padding: 12px 18px; - border-top: 1px solid var(--border-subtle); - flex-shrink: 0; -} -.update-modal-skip { - font-size: 12px; - color: var(--text-muted); - padding: var(--space-1) var(--space-2); -} -.update-modal-skip:hover { color: var(--text-secondary); } - .update-toast { margin: 0 var(--space-1) var(--space-2); padding: var(--space-3) var(--space-3);