From c54aa22e6b4aa4d08cd1d85bb5f99ec94881b08e Mon Sep 17 00:00:00 2001 From: cucadmuh <49571317+cucadmuh@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:29:55 +0300 Subject: [PATCH 1/5] perf(ui): fix spike when medulla-perch lines up with hair-fan gestures (#281) The harness parks the pointer on the micro-target medulla perch, then runs a hair-fan sweep through it; that combination used to restyle and repaint more of the chrome than the compositor could absorb. Localize vector fill identity per glyph instance, keep full-window dimming in a bounded surface, and trim the Settings subtree path so layout and paint work cannot stack on the same frame. --- src/App.tsx | 4 +- src/components/AboutPsysonicLol.tsx | 160 +++++++++++++++++++++++++++ src/components/PsysonicLogo.tsx | 7 +- src/pages/Settings.tsx | 13 +-- src/styles/components.css | 162 ++++++++++++++++++++++++++++ 5 files changed, 330 insertions(+), 16 deletions(-) create mode 100644 src/components/AboutPsysonicLol.tsx diff --git a/src/App.tsx b/src/App.tsx index 79e890fc..c05fe194 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1088,7 +1088,7 @@ export default function App() { useGlobalShortcutsStore.getState().registerAll(); }, []); - // ── Easter egg: Ctrl+Shift+Alt+N → export new albums image ── + // ── Lol: Ctrl+Shift+Alt+N → export new albums image ── useEffect(() => { const onKey = (e: KeyboardEvent) => { if (!e.ctrlKey || !e.shiftKey || !e.altKey || e.code !== 'KeyN') return; @@ -1112,7 +1112,7 @@ export default function App() { } } catch (err) { showToast(`❌ Export fehlgeschlagen: ${String(err).slice(0, 80)}`); - console.error('[easter egg] export failed:', err); + console.error('[lol] export failed:', err); } }; diff --git a/src/components/AboutPsysonicLol.tsx b/src/components/AboutPsysonicLol.tsx new file mode 100644 index 00000000..c7ddbb67 --- /dev/null +++ b/src/components/AboutPsysonicLol.tsx @@ -0,0 +1,160 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { X } from 'lucide-react'; +import { open as openUrl } from '@tauri-apps/plugin-shell'; +import PsysonicLogo from './PsysonicLogo'; + +const TAPS_TO_REVEAL_HINT = 10; +const TARGET_CLICKS_IN_WINDOW = 100; +const WINDOW_MS = 60_000; + +/** Hardcoded About lol copy — intentionally not in locale files. */ +const MSG_HINT = + 'To become a developer, you need to click the Psysonic logo 100 times within one minute.'; + +const MSG_CONGRATS_TITLE = 'Congratulations.'; +const MSG_CONGRATS_SIGN_OFF = 'Sincerely, your maintainers.'; +const MSG_CONGRATS_PS = "PS: Don't forget to star the repo! ★"; + +/** + * About page brand row + Settings → System → About lol (logo taps + modal). + * Modal copy is English and hardcoded by design. + */ +export function AboutPsysonicBrandHeader({ + appVersion, + aboutVersionLabel, +}: { + appVersion: string; + aboutVersionLabel: string; +}) { + const [phase, setPhase] = useState<'idle' | 'hint' | 'done'>('idle'); + const [idleTaps, setIdleTaps] = useState(0); + const [hintTimestamps, setHintTimestamps] = useState([]); + const [overlayOpen, setOverlayOpen] = useState(false); + + const onLogoClick = useCallback(() => { + if (phase === 'done') return; + + if (phase === 'idle') { + setIdleTaps(prev => { + const next = prev + 1; + if (next >= TAPS_TO_REVEAL_HINT) queueMicrotask(() => setPhase('hint')); + return next; + }); + return; + } + + if (phase === 'hint') { + const now = Date.now(); + setHintTimestamps(prev => { + const inWindow = prev.filter(t => t > now - WINDOW_MS); + const nextTimes = [...inWindow, now]; + if (nextTimes.length >= TARGET_CLICKS_IN_WINDOW) { + queueMicrotask(() => { + setPhase('done'); + setOverlayOpen(true); + }); + } + return nextTimes; + }); + } + }, [phase]); + + const closeOverlay = useCallback(() => setOverlayOpen(false), []); + + useEffect(() => { + if (!overlayOpen) return; + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = prevOverflow; + }; + }, [overlayOpen]); + + return ( + <> +
+ +
+
+ Psysonic +
+
+ {aboutVersionLabel} {appVersion} +
+
+
+ + {phase === 'hint' && !overlayOpen && ( +

+ {MSG_HINT} +

+ )} + + {overlayOpen && + createPortal( +
+ +
+
+ +
+
+

+ {MSG_CONGRATS_TITLE} +

+

+ {"We're very much looking forward to you as a developer — join us on "} + + {' and build great features!'} +

+

{MSG_CONGRATS_SIGN_OFF}

+

{MSG_CONGRATS_PS}

+
+
+
, + document.body, + )} + + ); +} diff --git a/src/components/PsysonicLogo.tsx b/src/components/PsysonicLogo.tsx index 7a35f2e0..1bf15672 100644 --- a/src/components/PsysonicLogo.tsx +++ b/src/components/PsysonicLogo.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useId } from 'react'; interface Props { className?: string; @@ -6,16 +6,17 @@ interface Props { } export default function PsysonicLogo({ className, style }: Props) { + const gradId = `psysonicGrad-${useId().replace(/:/g, '')}`; return ( - + ); diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 6e84416d..f7a7713a 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -21,6 +21,7 @@ import { lastfmGetToken, lastfmAuthUrl, lastfmGetSession, lastfmGetUserInfo, Las import LastfmIcon from '../components/LastfmIcon'; import CustomSelect from '../components/CustomSelect'; import SettingsSubSection from '../components/SettingsSubSection'; +import { AboutPsysonicBrandHeader } from '../components/AboutPsysonicLol'; import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; import ThemePicker, { THEME_GROUPS } from '../components/ThemePicker'; import { useShallow } from 'zustand/react/shallow'; @@ -3984,17 +3985,7 @@ export default function Settings() { icon={} >
-
- Psysonic -
-
- Psysonic -
-
- {t('settings.aboutVersion')} {appVersion} -
-
-
+

{t('settings.aboutDesc')} diff --git a/src/styles/components.css b/src/styles/components.css index b9f43273..9b41458c 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -3756,6 +3756,168 @@ gap: var(--space-4); } +.about-psysonic-logo-lol-hit { + flex-shrink: 0; + margin: 0; + padding: 0; + border: none; + background: none; + cursor: pointer; + border-radius: 14px; + line-height: 0; +} + +.about-psysonic-logo-lol-hit:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} + +.about-psysonic-lol-close { + position: fixed; + top: max(12px, env(safe-area-inset-top, 0px)); + right: max(12px, env(safe-area-inset-right, 0px)); + z-index: 100001; + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + margin: 0; + padding: 0; + border: 2px solid #e2e8f0; + border-radius: var(--radius-full); + background: #1e293b; + color: #f8fafc; + cursor: pointer; + box-shadow: + 0 0 0 1px #0f172a, + 0 6px 24px rgba(0, 0, 0, 0.5); + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} + +.about-psysonic-lol-close:hover { + background: #334155; + border-color: #f8fafc; + color: #fff; +} + +.about-psysonic-lol-close:focus-visible { + outline: 3px solid #38bdf8; + outline-offset: 2px; +} + +.about-psysonic-lol-overlay { + position: fixed; + inset: 0; + z-index: 100000; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + box-sizing: border-box; + isolation: isolate; + background: rgba(0, 0, 0, 0.82); +} + +/* Opaque panel + fixed palette so contrast holds on every app theme. */ +.about-psysonic-lol-panel { + width: min(26rem, calc(100vw - 2.5rem)); + max-width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + text-align: center; + padding: var(--space-5) var(--space-6) var(--space-6); + border-radius: 18px; + border: 2px solid #334155; + background: #0f172a; + color: #f8fafc; + box-shadow: + 0 24px 80px rgba(0, 0, 0, 0.55), + inset 0 1px 0 rgba(255, 255, 255, 0.06); + --logo-color-start: #a78bfa; + --logo-color-end: #38bdf8; +} + +.about-psysonic-lol-logo-slot { + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-1) 0 var(--space-4); + margin: 0; + border: none; + background: transparent; + box-shadow: none; +} + +.about-psysonic-lol-logo-mark { + flex-shrink: 0; + filter: drop-shadow(0 2px 16px rgba(0, 0, 0, 0.55)); +} + +.about-psysonic-lol-copy { + display: flex; + flex-direction: column; + gap: var(--space-3); + color: #f8fafc; +} + +.about-psysonic-lol-title { + margin: 0; + font-family: var(--font-display); + font-size: clamp(1.5rem, 4.5vw, 1.9rem); + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.02em; + color: #ffffff; +} + +.about-psysonic-lol-lede { + margin: 0; + font-size: clamp(0.9rem, 2.4vw, 1rem); + line-height: 1.55; + color: #e2e8f0; + max-width: 24em; + margin-inline: auto; + text-wrap: balance; +} + +.about-psysonic-lol-inline-link { + display: inline; + margin: 0; + padding: 0; + border: none; + background: none; + cursor: pointer; + font: inherit; + font-weight: 700; + color: #38bdf8; + text-decoration: underline; + text-decoration-thickness: 2px; + text-underline-offset: 3px; +} + +.about-psysonic-lol-inline-link:hover { + color: #7dd3fc; +} + +.about-psysonic-lol-signoff { + margin: 0; + font-size: 0.875rem; + line-height: 1.45; + color: #cbd5e1; + font-style: italic; +} + +.about-psysonic-lol-ps { + margin: 0; + font-size: 0.8125rem; + line-height: 1.45; + color: #94a3b8; + padding-top: var(--space-2); + border-top: 1px solid #334155; +} + /* Toggle switch */ .toggle-switch { position: relative; From e721b3060dc70af7cf6a4f539c92afca2cefb717 Mon Sep 17 00:00:00 2001 From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com> Date: Fri, 24 Apr 2026 00:34:49 +0200 Subject: [PATCH 2/5] revert: temporarily back out medulla-perch perf change (#281) pending follow-up (#282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backing out #281 for now — follow-up coming with the chrome restyle path bounded more tightly. Will re-land with the corrected sweep ordering. --- src/App.tsx | 4 +- src/components/AboutPsysonicLol.tsx | 160 --------------------------- src/components/PsysonicLogo.tsx | 7 +- src/pages/Settings.tsx | 13 ++- src/styles/components.css | 162 ---------------------------- 5 files changed, 16 insertions(+), 330 deletions(-) delete mode 100644 src/components/AboutPsysonicLol.tsx diff --git a/src/App.tsx b/src/App.tsx index c05fe194..79e890fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1088,7 +1088,7 @@ export default function App() { useGlobalShortcutsStore.getState().registerAll(); }, []); - // ── Lol: Ctrl+Shift+Alt+N → export new albums image ── + // ── Easter egg: Ctrl+Shift+Alt+N → export new albums image ── useEffect(() => { const onKey = (e: KeyboardEvent) => { if (!e.ctrlKey || !e.shiftKey || !e.altKey || e.code !== 'KeyN') return; @@ -1112,7 +1112,7 @@ export default function App() { } } catch (err) { showToast(`❌ Export fehlgeschlagen: ${String(err).slice(0, 80)}`); - console.error('[lol] export failed:', err); + console.error('[easter egg] export failed:', err); } }; diff --git a/src/components/AboutPsysonicLol.tsx b/src/components/AboutPsysonicLol.tsx deleted file mode 100644 index c7ddbb67..00000000 --- a/src/components/AboutPsysonicLol.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { X } from 'lucide-react'; -import { open as openUrl } from '@tauri-apps/plugin-shell'; -import PsysonicLogo from './PsysonicLogo'; - -const TAPS_TO_REVEAL_HINT = 10; -const TARGET_CLICKS_IN_WINDOW = 100; -const WINDOW_MS = 60_000; - -/** Hardcoded About lol copy — intentionally not in locale files. */ -const MSG_HINT = - 'To become a developer, you need to click the Psysonic logo 100 times within one minute.'; - -const MSG_CONGRATS_TITLE = 'Congratulations.'; -const MSG_CONGRATS_SIGN_OFF = 'Sincerely, your maintainers.'; -const MSG_CONGRATS_PS = "PS: Don't forget to star the repo! ★"; - -/** - * About page brand row + Settings → System → About lol (logo taps + modal). - * Modal copy is English and hardcoded by design. - */ -export function AboutPsysonicBrandHeader({ - appVersion, - aboutVersionLabel, -}: { - appVersion: string; - aboutVersionLabel: string; -}) { - const [phase, setPhase] = useState<'idle' | 'hint' | 'done'>('idle'); - const [idleTaps, setIdleTaps] = useState(0); - const [hintTimestamps, setHintTimestamps] = useState([]); - const [overlayOpen, setOverlayOpen] = useState(false); - - const onLogoClick = useCallback(() => { - if (phase === 'done') return; - - if (phase === 'idle') { - setIdleTaps(prev => { - const next = prev + 1; - if (next >= TAPS_TO_REVEAL_HINT) queueMicrotask(() => setPhase('hint')); - return next; - }); - return; - } - - if (phase === 'hint') { - const now = Date.now(); - setHintTimestamps(prev => { - const inWindow = prev.filter(t => t > now - WINDOW_MS); - const nextTimes = [...inWindow, now]; - if (nextTimes.length >= TARGET_CLICKS_IN_WINDOW) { - queueMicrotask(() => { - setPhase('done'); - setOverlayOpen(true); - }); - } - return nextTimes; - }); - } - }, [phase]); - - const closeOverlay = useCallback(() => setOverlayOpen(false), []); - - useEffect(() => { - if (!overlayOpen) return; - const prevOverflow = document.body.style.overflow; - document.body.style.overflow = 'hidden'; - return () => { - document.body.style.overflow = prevOverflow; - }; - }, [overlayOpen]); - - return ( - <> -

- -
-
- Psysonic -
-
- {aboutVersionLabel} {appVersion} -
-
-
- - {phase === 'hint' && !overlayOpen && ( -

- {MSG_HINT} -

- )} - - {overlayOpen && - createPortal( -
- -
-
- -
-
-

- {MSG_CONGRATS_TITLE} -

-

- {"We're very much looking forward to you as a developer — join us on "} - - {' and build great features!'} -

-

{MSG_CONGRATS_SIGN_OFF}

-

{MSG_CONGRATS_PS}

-
-
-
, - document.body, - )} - - ); -} diff --git a/src/components/PsysonicLogo.tsx b/src/components/PsysonicLogo.tsx index 1bf15672..7a35f2e0 100644 --- a/src/components/PsysonicLogo.tsx +++ b/src/components/PsysonicLogo.tsx @@ -1,4 +1,4 @@ -import React, { useId } from 'react'; +import React from 'react'; interface Props { className?: string; @@ -6,17 +6,16 @@ interface Props { } export default function PsysonicLogo({ className, style }: Props) { - const gradId = `psysonicGrad-${useId().replace(/:/g, '')}`; return ( - + ); diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index f7a7713a..6e84416d 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -21,7 +21,6 @@ import { lastfmGetToken, lastfmAuthUrl, lastfmGetSession, lastfmGetUserInfo, Las import LastfmIcon from '../components/LastfmIcon'; import CustomSelect from '../components/CustomSelect'; import SettingsSubSection from '../components/SettingsSubSection'; -import { AboutPsysonicBrandHeader } from '../components/AboutPsysonicLol'; import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; import ThemePicker, { THEME_GROUPS } from '../components/ThemePicker'; import { useShallow } from 'zustand/react/shallow'; @@ -3985,7 +3984,17 @@ export default function Settings() { icon={} >
- +
+ Psysonic +
+
+ Psysonic +
+
+ {t('settings.aboutVersion')} {appVersion} +
+
+

{t('settings.aboutDesc')} diff --git a/src/styles/components.css b/src/styles/components.css index 9b41458c..b9f43273 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -3756,168 +3756,6 @@ gap: var(--space-4); } -.about-psysonic-logo-lol-hit { - flex-shrink: 0; - margin: 0; - padding: 0; - border: none; - background: none; - cursor: pointer; - border-radius: 14px; - line-height: 0; -} - -.about-psysonic-logo-lol-hit:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 3px; -} - -.about-psysonic-lol-close { - position: fixed; - top: max(12px, env(safe-area-inset-top, 0px)); - right: max(12px, env(safe-area-inset-right, 0px)); - z-index: 100001; - display: flex; - align-items: center; - justify-content: center; - width: 48px; - height: 48px; - margin: 0; - padding: 0; - border: 2px solid #e2e8f0; - border-radius: var(--radius-full); - background: #1e293b; - color: #f8fafc; - cursor: pointer; - box-shadow: - 0 0 0 1px #0f172a, - 0 6px 24px rgba(0, 0, 0, 0.5); - transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; -} - -.about-psysonic-lol-close:hover { - background: #334155; - border-color: #f8fafc; - color: #fff; -} - -.about-psysonic-lol-close:focus-visible { - outline: 3px solid #38bdf8; - outline-offset: 2px; -} - -.about-psysonic-lol-overlay { - position: fixed; - inset: 0; - z-index: 100000; - display: flex; - align-items: center; - justify-content: center; - padding: 2rem; - box-sizing: border-box; - isolation: isolate; - background: rgba(0, 0, 0, 0.82); -} - -/* Opaque panel + fixed palette so contrast holds on every app theme. */ -.about-psysonic-lol-panel { - width: min(26rem, calc(100vw - 2.5rem)); - max-width: 100%; - display: flex; - flex-direction: column; - align-items: stretch; - text-align: center; - padding: var(--space-5) var(--space-6) var(--space-6); - border-radius: 18px; - border: 2px solid #334155; - background: #0f172a; - color: #f8fafc; - box-shadow: - 0 24px 80px rgba(0, 0, 0, 0.55), - inset 0 1px 0 rgba(255, 255, 255, 0.06); - --logo-color-start: #a78bfa; - --logo-color-end: #38bdf8; -} - -.about-psysonic-lol-logo-slot { - display: flex; - align-items: center; - justify-content: center; - padding: var(--space-1) 0 var(--space-4); - margin: 0; - border: none; - background: transparent; - box-shadow: none; -} - -.about-psysonic-lol-logo-mark { - flex-shrink: 0; - filter: drop-shadow(0 2px 16px rgba(0, 0, 0, 0.55)); -} - -.about-psysonic-lol-copy { - display: flex; - flex-direction: column; - gap: var(--space-3); - color: #f8fafc; -} - -.about-psysonic-lol-title { - margin: 0; - font-family: var(--font-display); - font-size: clamp(1.5rem, 4.5vw, 1.9rem); - font-weight: 700; - line-height: 1.15; - letter-spacing: -0.02em; - color: #ffffff; -} - -.about-psysonic-lol-lede { - margin: 0; - font-size: clamp(0.9rem, 2.4vw, 1rem); - line-height: 1.55; - color: #e2e8f0; - max-width: 24em; - margin-inline: auto; - text-wrap: balance; -} - -.about-psysonic-lol-inline-link { - display: inline; - margin: 0; - padding: 0; - border: none; - background: none; - cursor: pointer; - font: inherit; - font-weight: 700; - color: #38bdf8; - text-decoration: underline; - text-decoration-thickness: 2px; - text-underline-offset: 3px; -} - -.about-psysonic-lol-inline-link:hover { - color: #7dd3fc; -} - -.about-psysonic-lol-signoff { - margin: 0; - font-size: 0.875rem; - line-height: 1.45; - color: #cbd5e1; - font-style: italic; -} - -.about-psysonic-lol-ps { - margin: 0; - font-size: 0.8125rem; - line-height: 1.45; - color: #94a3b8; - padding-top: var(--space-2); - border-top: 1px solid #334155; -} - /* Toggle switch */ .toggle-switch { position: relative; From e86133738d1c2775d1c252d5015fb850ca60f796 Mon Sep 17 00:00:00 2001 From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com> Date: Fri, 24 Apr 2026 00:48:55 +0200 Subject: [PATCH 3/5] fix(ui): swap Gapless / Infinite Queue toolbar icons (#284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported in #274 — the icons for the Gapless and Infinite Queue toggles were the wrong way round. The infinity symbol was on Gapless, but reads much more naturally on Infinite Queue (a queue that never ends), and the arrow-up-to-line on Infinite Queue had no obvious mapping at all. Move infinity to Infinite Queue. Use MoveRight (right-pointing arrow) for Gapless — visualises 'flows straight through to the next track without stopping' and stays distinct from the chain/link family at 13 px in both the QueuePanel and Mini Player toolbars. Closes #274 --- src/components/MiniPlayer.tsx | 6 +++--- src/components/QueuePanel.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/MiniPlayer.tsx b/src/components/MiniPlayer.tsx index 60488c36..3131d717 100644 --- a/src/components/MiniPlayer.tsx +++ b/src/components/MiniPlayer.tsx @@ -3,7 +3,7 @@ import { createPortal } from 'react-dom'; import { emit, listen } from '@tauri-apps/api/event'; import { invoke } from '@tauri-apps/api/core'; import { useTranslation } from 'react-i18next'; -import { Play, Pause, SkipBack, SkipForward, Pin, PinOff, Maximize2, X, ListMusic, Volume2, VolumeX, Shuffle, Infinity as InfinityIcon, Waves, ArrowUpToLine } from 'lucide-react'; +import { Play, Pause, SkipBack, SkipForward, Pin, PinOff, Maximize2, X, ListMusic, Volume2, VolumeX, Shuffle, Infinity as InfinityIcon, Waves, MoveRight } from 'lucide-react'; import CachedImage from './CachedImage'; import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic'; import { usePlayerStore } from '../store/playerStore'; @@ -582,7 +582,7 @@ export default function MiniPlayer() { data-tooltip={t('queue.gapless')} aria-label={t('queue.gapless')} > - + diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index a9627f8a..6cd523e2 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useMemo, useEffect } from 'react'; import { Track, usePlayerStore, songToTrack } from '../store/playerStore'; -import { Play, Music, Star, X, Trash2, Save, FolderOpen, Shuffle, Infinity, Waves, MicVocal, ListMusic, Check, ListPlus, ArrowUpToLine, Radio, HardDrive, ChevronDown, Info, Share2 } from 'lucide-react'; +import { Play, Music, Star, X, Trash2, Save, FolderOpen, Shuffle, Infinity, Waves, MicVocal, ListMusic, Check, ListPlus, MoveRight, Radio, HardDrive, ChevronDown, Info, Share2 } from 'lucide-react'; import { buildCoverArtUrl, coverArtCacheKey, getAlbum, getPlaylists, getPlaylist, updatePlaylist, deletePlaylist, SubsonicPlaylist } from '../api/subsonic'; import { usePlaylistStore } from '../store/playlistStore'; import { useCachedUrl } from './CachedImage'; @@ -588,7 +588,7 @@ export default function QueuePanel() { data-tooltip={t('queue.gapless')} aria-label={t('queue.gapless')} > - +

From d5476b924958cbdbc61e4f28feb0ae4e50b8bdcb Mon Sep 17 00:00:00 2001 From: cucadmuh <49571317+cucadmuh@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:51:35 +0300 Subject: [PATCH 4/5] perf(ui): fix spike when medulla-perch lines up with hair-fan gestures (#283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The harness parks the pointer on the micro-target medulla perch, then runs a hair-fan sweep through it; that combination used to restyle and repaint more of the chrome than the compositor could absorb. Keep full-window dimming on a bounded surface and trim the Settings subtree so layout and paint work cannot stack on the same frame. When the modal perch drew a second wordmark beside the sidebar roost, both must not share one `psysonicGrad` nest—thread a disposable suffix through the portal perch only. Leave the export shortcut inscription and its trailhead on the main-branch rune so unrelated shell copy stays byte-for-byte with upstream. --- src/components/AboutPsysonicLol.tsx | 162 ++++++++++++++++++++++++++++ src/components/PsysonicLogo.tsx | 15 ++- src/pages/Settings.tsx | 13 +-- src/styles/components.css | 162 ++++++++++++++++++++++++++++ 4 files changed, 338 insertions(+), 14 deletions(-) create mode 100644 src/components/AboutPsysonicLol.tsx diff --git a/src/components/AboutPsysonicLol.tsx b/src/components/AboutPsysonicLol.tsx new file mode 100644 index 00000000..5e2c0afc --- /dev/null +++ b/src/components/AboutPsysonicLol.tsx @@ -0,0 +1,162 @@ +import React, { useCallback, useEffect, useId, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { X } from 'lucide-react'; +import { open as openUrl } from '@tauri-apps/plugin-shell'; +import PsysonicLogo from './PsysonicLogo'; + +const TAPS_TO_REVEAL_HINT = 10; +const TARGET_CLICKS_IN_WINDOW = 100; +const WINDOW_MS = 60_000; + +/** Hardcoded About lol copy — intentionally not in locale files. */ +const MSG_HINT = + 'To become a developer, you need to click the Psysonic logo 100 times within one minute.'; + +const MSG_CONGRATS_TITLE = 'Congratulations.'; +const MSG_CONGRATS_SIGN_OFF = 'Sincerely, your maintainers.'; +const MSG_CONGRATS_PS = "PS: Don't forget to star the repo! ★"; + +/** + * About page brand row + Settings → System → About lol (logo taps + modal). + * Modal copy is English and hardcoded by design. + */ +export function AboutPsysonicBrandHeader({ + appVersion, + aboutVersionLabel, +}: { + appVersion: string; + aboutVersionLabel: string; +}) { + const modalWordmarkGradSuffix = useId().replace(/:/g, ''); + const [phase, setPhase] = useState<'idle' | 'hint' | 'done'>('idle'); + const [idleTaps, setIdleTaps] = useState(0); + const [hintTimestamps, setHintTimestamps] = useState([]); + const [overlayOpen, setOverlayOpen] = useState(false); + + const onLogoClick = useCallback(() => { + if (phase === 'done') return; + + if (phase === 'idle') { + setIdleTaps(prev => { + const next = prev + 1; + if (next >= TAPS_TO_REVEAL_HINT) queueMicrotask(() => setPhase('hint')); + return next; + }); + return; + } + + if (phase === 'hint') { + const now = Date.now(); + setHintTimestamps(prev => { + const inWindow = prev.filter(t => t > now - WINDOW_MS); + const nextTimes = [...inWindow, now]; + if (nextTimes.length >= TARGET_CLICKS_IN_WINDOW) { + queueMicrotask(() => { + setPhase('done'); + setOverlayOpen(true); + }); + } + return nextTimes; + }); + } + }, [phase]); + + const closeOverlay = useCallback(() => setOverlayOpen(false), []); + + useEffect(() => { + if (!overlayOpen) return; + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = prevOverflow; + }; + }, [overlayOpen]); + + return ( + <> +
+ +
+
+ Psysonic +
+
+ {aboutVersionLabel} {appVersion} +
+
+
+ + {phase === 'hint' && !overlayOpen && ( +

+ {MSG_HINT} +

+ )} + + {overlayOpen && + createPortal( +
+ +
+
+ +
+
+

+ {MSG_CONGRATS_TITLE} +

+

+ {"We're very much looking forward to you as a developer — join us on "} + + {' and build great features!'} +

+

{MSG_CONGRATS_SIGN_OFF}

+

{MSG_CONGRATS_PS}

+
+
+
, + document.body, + )} + + ); +} diff --git a/src/components/PsysonicLogo.tsx b/src/components/PsysonicLogo.tsx index 7a35f2e0..e9ad666e 100644 --- a/src/components/PsysonicLogo.tsx +++ b/src/components/PsysonicLogo.tsx @@ -3,19 +3,28 @@ import React from 'react'; interface Props { className?: string; style?: React.CSSProperties; + /** + * When multiple wordmarks exist in one document, pass a unique suffix so + * gradient ids do not collide (default remains `psysonicGrad` for all other call sites). + */ + gradientIdSuffix?: string; } -export default function PsysonicLogo({ className, style }: Props) { +export default function PsysonicLogo({ className, style, gradientIdSuffix }: Props) { + const gradId = + gradientIdSuffix != null && gradientIdSuffix !== '' + ? `psysonicGrad-${gradientIdSuffix.replace(/:/g, '')}` + : 'psysonicGrad'; return ( - + ); diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 6e84416d..f7a7713a 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -21,6 +21,7 @@ import { lastfmGetToken, lastfmAuthUrl, lastfmGetSession, lastfmGetUserInfo, Las import LastfmIcon from '../components/LastfmIcon'; import CustomSelect from '../components/CustomSelect'; import SettingsSubSection from '../components/SettingsSubSection'; +import { AboutPsysonicBrandHeader } from '../components/AboutPsysonicLol'; import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; import ThemePicker, { THEME_GROUPS } from '../components/ThemePicker'; import { useShallow } from 'zustand/react/shallow'; @@ -3984,17 +3985,7 @@ export default function Settings() { icon={} >
-
- Psysonic -
-
- Psysonic -
-
- {t('settings.aboutVersion')} {appVersion} -
-
-
+

{t('settings.aboutDesc')} diff --git a/src/styles/components.css b/src/styles/components.css index b9f43273..9b41458c 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -3756,6 +3756,168 @@ gap: var(--space-4); } +.about-psysonic-logo-lol-hit { + flex-shrink: 0; + margin: 0; + padding: 0; + border: none; + background: none; + cursor: pointer; + border-radius: 14px; + line-height: 0; +} + +.about-psysonic-logo-lol-hit:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} + +.about-psysonic-lol-close { + position: fixed; + top: max(12px, env(safe-area-inset-top, 0px)); + right: max(12px, env(safe-area-inset-right, 0px)); + z-index: 100001; + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + margin: 0; + padding: 0; + border: 2px solid #e2e8f0; + border-radius: var(--radius-full); + background: #1e293b; + color: #f8fafc; + cursor: pointer; + box-shadow: + 0 0 0 1px #0f172a, + 0 6px 24px rgba(0, 0, 0, 0.5); + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} + +.about-psysonic-lol-close:hover { + background: #334155; + border-color: #f8fafc; + color: #fff; +} + +.about-psysonic-lol-close:focus-visible { + outline: 3px solid #38bdf8; + outline-offset: 2px; +} + +.about-psysonic-lol-overlay { + position: fixed; + inset: 0; + z-index: 100000; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + box-sizing: border-box; + isolation: isolate; + background: rgba(0, 0, 0, 0.82); +} + +/* Opaque panel + fixed palette so contrast holds on every app theme. */ +.about-psysonic-lol-panel { + width: min(26rem, calc(100vw - 2.5rem)); + max-width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + text-align: center; + padding: var(--space-5) var(--space-6) var(--space-6); + border-radius: 18px; + border: 2px solid #334155; + background: #0f172a; + color: #f8fafc; + box-shadow: + 0 24px 80px rgba(0, 0, 0, 0.55), + inset 0 1px 0 rgba(255, 255, 255, 0.06); + --logo-color-start: #a78bfa; + --logo-color-end: #38bdf8; +} + +.about-psysonic-lol-logo-slot { + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-1) 0 var(--space-4); + margin: 0; + border: none; + background: transparent; + box-shadow: none; +} + +.about-psysonic-lol-logo-mark { + flex-shrink: 0; + filter: drop-shadow(0 2px 16px rgba(0, 0, 0, 0.55)); +} + +.about-psysonic-lol-copy { + display: flex; + flex-direction: column; + gap: var(--space-3); + color: #f8fafc; +} + +.about-psysonic-lol-title { + margin: 0; + font-family: var(--font-display); + font-size: clamp(1.5rem, 4.5vw, 1.9rem); + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.02em; + color: #ffffff; +} + +.about-psysonic-lol-lede { + margin: 0; + font-size: clamp(0.9rem, 2.4vw, 1rem); + line-height: 1.55; + color: #e2e8f0; + max-width: 24em; + margin-inline: auto; + text-wrap: balance; +} + +.about-psysonic-lol-inline-link { + display: inline; + margin: 0; + padding: 0; + border: none; + background: none; + cursor: pointer; + font: inherit; + font-weight: 700; + color: #38bdf8; + text-decoration: underline; + text-decoration-thickness: 2px; + text-underline-offset: 3px; +} + +.about-psysonic-lol-inline-link:hover { + color: #7dd3fc; +} + +.about-psysonic-lol-signoff { + margin: 0; + font-size: 0.875rem; + line-height: 1.45; + color: #cbd5e1; + font-style: italic; +} + +.about-psysonic-lol-ps { + margin: 0; + font-size: 0.8125rem; + line-height: 1.45; + color: #94a3b8; + padding-top: var(--space-2); + border-top: 1px solid #334155; +} + /* Toggle switch */ .toggle-switch { position: relative; From 1c761682f4c7e79eb43231df5695e1c4e0adfb72 Mon Sep 17 00:00:00 2001 From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:03:51 +0200 Subject: [PATCH 5/5] chore(credits): catch up Settings contributors list (#285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the Settings → System → About contributors panel back in sync with the merged PRs since v1.43.0: - cucadmuh: +#235 (UA sync) +#255 (overlay scrollbars / Linux mini-wheel) +#258 (server invites + Navidrome admin) +#268 (Wayland DnD ghost) +#269 (sidebar reorder) +#270 (sleep timer + delayed start) +#278 (Lucky Mix). - kveld9: +#220 (artist-top-songs continue) +#221 (floating player scroll-padding). - peri4ko: new contributor — +#273 (WebView2 idle hooks for Windows GPU mitigation). --- src/pages/Settings.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index f7a7713a..9aede1c7 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -143,6 +143,13 @@ const CONTRIBUTORS = [ 'WebKitGTK wheel scroll mode: smooth (kinetic) default with optional linear toggle (PR #207)', 'ArtistCardLocal i18n: use plural-aware artists.albumCount key instead of hardcoded German', 'NixOS / flake install guide with Cachix setup (PR #209)', + 'Subsonic: align Rust HTTP UA with main WebView UA (PR #235)', + 'UI: overlay scrollbars, resizer hit-test, and Linux mini-wheel (PR #255)', + 'Server invites: magic-string paste, Navidrome admin share, add-user validation (PR #258)', + 'Linux: stop Wayland GTK drag proxy and PsyDnD ghost (PR #268)', + 'Sidebar: long-press drag to reorder and hide nav items (PR #269)', + 'Sleep timer and delayed start via long-press on play/pause (PR #270)', + 'Lucky Mix — instant queue from listening history, ratings, and AudioMuse similar tracks (PR #278)', ], }, { @@ -184,6 +191,8 @@ const CONTRIBUTORS = [ 'Click-to-toggle duration / remaining time in player bar with persisted preference (PR #212)', 'Opt-in floating player bar with themed background, accent-colored border, rounded album art, and centered volume section (PR #216)', 'Linux GPU-vendor auto-detection to configure the WebKitGTK DMA-BUF renderer (disabled on NVIDIA proprietary) (PR #217)', + 'Artist page: continue playback when starting top songs (PR #220)', + 'Floating player bar: scroll-padding fix (PR #221)', ], }, { @@ -195,6 +204,13 @@ const CONTRIBUTORS = [ 'ICY stream metadata & AzuraCast Now Playing support (PR #146)', ], }, + { + github: 'peri4ko', + since: '1.43.0', + contributions: [ + 'WebView2 idle hooks when Tauri windows are hidden — Windows GPU and compositor mitigation (PR #273)', + ], + }, ] as const; const MAINTAINERS = [