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;