mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
feat(changelog): replace auto-modal with sidebar banner + /whats-new page
Removes the giant startup changelog modal. After an update, a compact neutral-palette pill now sits above Now Playing in the sidebar saying "Changelog — vX.Y.Z". Clicking opens a proper /whats-new page in the main content area; X dismisses. Page renders the current version's CHANGELOG entry with inline Markdown (headings, lists, blockquotes, hr, bold/italic/code, and real [label](url) links that open via the Tauri shell plugin). Visual tokens are hardcoded slate/gray/blue so the banner and page look identical across every theme (dark, light, skeuomorphic, whatever) — requested for consistent contrast. Auto-mark-as-seen removed from the page; the banner only goes away on explicit dismiss, so users can re-read as often as they want. Settings → About gains a "Release notes" row with a link that resets the seen-version and opens the page, so the banner can be retriggered manually (also helpful for dev builds ahead of the current tag). The existing "Show changelog on update" toggle now gates the banner instead of the modal; description strings updated in all 8 locales. fix(themes): WCAG contrast audit — mocha + winmedplayer + wista Mocha (Catppuccin dark) .track-size used --ctp-overlay0 directly (3.36:1 on bg-app, 2.57 on bg-card). Swapped to --text-muted → 7.37 / 5.65. Fix also lifts macchiato/frappe/latte which shared the failure. WinMedPlayer (Luna) --text-muted #b8d0f8 (3.87:1 on bg-app) → #e8f0ff (5.28) --border #2a5090 (1.31 on bg-app) → #071027 (3.12, meets 3:1 UI) Wista (Vista Aero) Lyrics pane sits on bg-sidebar #0e1e3e but used --text-primary (#0d1d3c) for active lines — 1.01:1, literally invisible. Added component overrides: .lyrics-line / .lyrics-status / word-synced variants → #aac8f0 (9.60), .lyrics-line.active → #ffffff (16.48). Palette: --warning #c8980c → #735a00 (2.37 → 5.91 on bg-app), --text-muted #4870a8 → #3f6aa0 (4.23 → 4.65 on bg-card). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-10
@@ -38,6 +38,7 @@ import InternetRadio from './pages/InternetRadio';
|
|||||||
import FolderBrowser from './pages/FolderBrowser';
|
import FolderBrowser from './pages/FolderBrowser';
|
||||||
import DeviceSync from './pages/DeviceSync';
|
import DeviceSync from './pages/DeviceSync';
|
||||||
import NowPlayingPage from './pages/NowPlaying';
|
import NowPlayingPage from './pages/NowPlaying';
|
||||||
|
import WhatsNew from './pages/WhatsNew';
|
||||||
import MiniPlayer from './components/MiniPlayer';
|
import MiniPlayer from './components/MiniPlayer';
|
||||||
import { initMiniPlayerBridgeOnMain } from './utils/miniPlayerBridge';
|
import { initMiniPlayerBridgeOnMain } from './utils/miniPlayerBridge';
|
||||||
import FullscreenPlayer from './components/FullscreenPlayer';
|
import FullscreenPlayer from './components/FullscreenPlayer';
|
||||||
@@ -53,7 +54,6 @@ import OfflineLibrary from './pages/OfflineLibrary';
|
|||||||
import Genres from './pages/Genres';
|
import Genres from './pages/Genres';
|
||||||
import GenreDetail from './pages/GenreDetail';
|
import GenreDetail from './pages/GenreDetail';
|
||||||
import ExportPickerModal from './components/ExportPickerModal';
|
import ExportPickerModal from './components/ExportPickerModal';
|
||||||
import ChangelogModal from './components/ChangelogModal';
|
|
||||||
import AppUpdater from './components/AppUpdater';
|
import AppUpdater from './components/AppUpdater';
|
||||||
import TitleBar from './components/TitleBar';
|
import TitleBar from './components/TitleBar';
|
||||||
import { IS_LINUX } from './utils/platform';
|
import { IS_LINUX } from './utils/platform';
|
||||||
@@ -229,14 +229,9 @@ function AppShell() {
|
|||||||
fn();
|
fn();
|
||||||
}, [currentTrack, isPlaying]);
|
}, [currentTrack, isPlaying]);
|
||||||
|
|
||||||
const [changelogModalOpen, setChangelogModalOpen] = useState(false);
|
// Post-update changelog is now surfaced via a dismissible banner in the
|
||||||
|
// sidebar (WhatsNewBanner) that links to the /whats-new page — no auto
|
||||||
useEffect(() => {
|
// modal takeover on startup.
|
||||||
const { showChangelogOnUpdate, lastSeenChangelogVersion } = useAuthStore.getState();
|
|
||||||
if (showChangelogOnUpdate && lastSeenChangelogVersion !== version) {
|
|
||||||
setChangelogModalOpen(true);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
||||||
return localStorage.getItem('psysonic_sidebar_collapsed') === 'true';
|
return localStorage.getItem('psysonic_sidebar_collapsed') === 'true';
|
||||||
@@ -402,6 +397,7 @@ function AppShell() {
|
|||||||
<Route path="/most-played" element={<MostPlayed />} />
|
<Route path="/most-played" element={<MostPlayed />} />
|
||||||
<Route path="/now-playing" element={isMobile ? <MobilePlayerView /> : <NowPlayingPage />} />
|
<Route path="/now-playing" element={isMobile ? <MobilePlayerView /> : <NowPlayingPage />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
|
<Route path="/whats-new" element={<WhatsNew />} />
|
||||||
<Route path="/help" element={<Help />} />
|
<Route path="/help" element={<Help />} />
|
||||||
<Route path="/offline" element={<OfflineLibrary />} />
|
<Route path="/offline" element={<OfflineLibrary />} />
|
||||||
<Route path="/genres" element={<Genres />} />
|
<Route path="/genres" element={<Genres />} />
|
||||||
@@ -436,7 +432,6 @@ function AppShell() {
|
|||||||
<DownloadFolderModal />
|
<DownloadFolderModal />
|
||||||
<TooltipPortal />
|
<TooltipPortal />
|
||||||
<AppUpdater />
|
<AppUpdater />
|
||||||
{changelogModalOpen && <ChangelogModal onClose={() => setChangelogModalOpen(false)} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import PsysonicLogo from './PsysonicLogo';
|
import PsysonicLogo from './PsysonicLogo';
|
||||||
import PSmallLogo from './PSmallLogo';
|
import PSmallLogo from './PSmallLogo';
|
||||||
|
import WhatsNewBanner from './WhatsNewBanner';
|
||||||
import { getPlaylists } from '../api/subsonic';
|
import { getPlaylists } from '../api/subsonic';
|
||||||
import { usePlaylistStore } from '../store/playlistStore';
|
import { usePlaylistStore } from '../store/playlistStore';
|
||||||
import { ALL_NAV_ITEMS } from '../config/navItems';
|
import { ALL_NAV_ITEMS } from '../config/navItems';
|
||||||
@@ -294,13 +295,18 @@ export default function Sidebar({
|
|||||||
)
|
)
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Spacer: everything from here onward sticks to the bottom of the sidebar. */}
|
||||||
|
<div className="sidebar-bottom-spacer" />
|
||||||
|
|
||||||
|
{/* What's New banner — only visible while the current release hasn't been seen. */}
|
||||||
|
<WhatsNewBanner collapsed={isCollapsed} />
|
||||||
|
|
||||||
{/* Now Playing — fixed, always visible */}
|
{/* Now Playing — fixed, always visible */}
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/now-playing"
|
to="/now-playing"
|
||||||
className={({ isActive }) => `nav-link nav-link-nowplaying ${isActive ? 'active' : ''}`}
|
className={({ isActive }) => `nav-link nav-link-nowplaying ${isActive ? 'active' : ''}`}
|
||||||
data-tooltip={isCollapsed ? t('sidebar.nowPlaying') : undefined}
|
data-tooltip={isCollapsed ? t('sidebar.nowPlaying') : undefined}
|
||||||
data-tooltip-pos="bottom"
|
data-tooltip-pos="bottom"
|
||||||
style={{ marginTop: 'auto' }}
|
|
||||||
>
|
>
|
||||||
<span className="nav-np-icon-wrap">
|
<span className="nav-np-icon-wrap">
|
||||||
<AudioLines size={isCollapsed ? 22 : 18} />
|
<AudioLines size={isCollapsed ? 22 : 18} />
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Sparkles, X } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { version } from '../../package.json';
|
||||||
|
import { useAuthStore } from '../store/authStore';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
collapsed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sidebar pill shown above Now Playing while the current app version hasn't
|
||||||
|
* been opened yet. Clicking opens the What's New page; X dismisses.
|
||||||
|
*
|
||||||
|
* Uses a fixed neutral palette so it looks identical across every theme.
|
||||||
|
*/
|
||||||
|
export default function WhatsNewBanner({ collapsed }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const lastSeen = useAuthStore(s => s.lastSeenChangelogVersion);
|
||||||
|
const setLastSeen = useAuthStore(s => s.setLastSeenChangelogVersion);
|
||||||
|
const showOnUpdate = useAuthStore(s => s.showChangelogOnUpdate);
|
||||||
|
|
||||||
|
if (!showOnUpdate || lastSeen === version) return null;
|
||||||
|
|
||||||
|
const dismiss = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setLastSeen(version);
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = () => navigate('/whats-new');
|
||||||
|
|
||||||
|
if (collapsed) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="whats-new-banner whats-new-banner--collapsed"
|
||||||
|
onClick={open}
|
||||||
|
data-tooltip={t('whatsNew.bannerCollapsed', { version })}
|
||||||
|
data-tooltip-pos="bottom"
|
||||||
|
>
|
||||||
|
<Sparkles size={16} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button type="button" className="whats-new-banner" onClick={open}>
|
||||||
|
<Sparkles size={14} className="whats-new-banner__icon" />
|
||||||
|
<span className="whats-new-banner__text">
|
||||||
|
<span className="whats-new-banner__title">{t('whatsNew.bannerTitle')}</span>
|
||||||
|
<span className="whats-new-banner__version">v{version}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="whats-new-banner__dismiss"
|
||||||
|
role="button"
|
||||||
|
aria-label={t('whatsNew.dismiss')}
|
||||||
|
onClick={dismiss}
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
+11
-1
@@ -599,11 +599,13 @@ export const deTranslation = {
|
|||||||
aboutVersion: 'Version',
|
aboutVersion: 'Version',
|
||||||
aboutBuiltWith: 'Gebaut mit Tauri · React · TypeScript · Rust/rodio',
|
aboutBuiltWith: 'Gebaut mit Tauri · React · TypeScript · Rust/rodio',
|
||||||
aboutAiCredit: 'Mit freundlicher Unterstützung von Claude Code by Anthropic',
|
aboutAiCredit: 'Mit freundlicher Unterstützung von Claude Code by Anthropic',
|
||||||
|
aboutReleaseNotesLabel: 'Release Notes',
|
||||||
|
aboutReleaseNotesLink: 'Neuigkeiten dieser Version öffnen',
|
||||||
aboutContributorsLabel: 'Mitwirkende',
|
aboutContributorsLabel: 'Mitwirkende',
|
||||||
aboutSpecialThanksLabel: 'Besonderer Dank',
|
aboutSpecialThanksLabel: 'Besonderer Dank',
|
||||||
changelog: 'Changelog',
|
changelog: 'Changelog',
|
||||||
showChangelogOnUpdate: "'Was ist neu' bei Update anzeigen",
|
showChangelogOnUpdate: "'Was ist neu' bei Update anzeigen",
|
||||||
showChangelogOnUpdateDesc: 'Zeigt automatisch die Neuerungen an, wenn eine neue Version zum ersten Mal gestartet wird.',
|
showChangelogOnUpdateDesc: 'Blendet nach einem Update einen dezenten Changelog-Banner über Now Playing ein. Klick öffnet die Release Notes, X blendet ihn aus.',
|
||||||
randomMixTitle: 'Zufallsmix',
|
randomMixTitle: 'Zufallsmix',
|
||||||
randomMixBlacklistTitle: 'Eigene Filter-Keywords',
|
randomMixBlacklistTitle: 'Eigene Filter-Keywords',
|
||||||
randomMixBlacklistDesc: 'Songs werden ausgeschlossen, wenn ein Keyword auf Genre, Titel, Album oder Künstler zutrifft (aktiv wenn die Checkbox oben an ist).',
|
randomMixBlacklistDesc: 'Songs werden ausgeschlossen, wenn ein Keyword auf Genre, Titel, Album oder Künstler zutrifft (aktiv wenn die Checkbox oben an ist).',
|
||||||
@@ -739,6 +741,14 @@ export const deTranslation = {
|
|||||||
dontShowAgain: 'Nicht mehr anzeigen',
|
dontShowAgain: 'Nicht mehr anzeigen',
|
||||||
close: 'Verstanden',
|
close: 'Verstanden',
|
||||||
},
|
},
|
||||||
|
whatsNew: {
|
||||||
|
title: 'Was ist neu',
|
||||||
|
empty: 'Für diese Version liegt noch kein Changelog-Eintrag vor.',
|
||||||
|
bannerTitle: 'Changelog',
|
||||||
|
bannerCollapsed: 'Neu in v{{version}}',
|
||||||
|
dismiss: 'Ausblenden',
|
||||||
|
close: 'Schließen',
|
||||||
|
},
|
||||||
help: {
|
help: {
|
||||||
title: 'Hilfe',
|
title: 'Hilfe',
|
||||||
s1: 'Erste Schritte',
|
s1: 'Erste Schritte',
|
||||||
|
|||||||
+11
-1
@@ -601,11 +601,13 @@ export const enTranslation = {
|
|||||||
aboutVersion: 'Version',
|
aboutVersion: 'Version',
|
||||||
aboutBuiltWith: 'Built with Tauri · React · TypeScript · Rust/rodio',
|
aboutBuiltWith: 'Built with Tauri · React · TypeScript · Rust/rodio',
|
||||||
aboutAiCredit: 'Developed with the support of Claude Code by Anthropic',
|
aboutAiCredit: 'Developed with the support of Claude Code by Anthropic',
|
||||||
|
aboutReleaseNotesLabel: 'Release notes',
|
||||||
|
aboutReleaseNotesLink: "Open this version's what's-new",
|
||||||
aboutContributorsLabel: 'Contributors',
|
aboutContributorsLabel: 'Contributors',
|
||||||
aboutSpecialThanksLabel: 'Special Thanks',
|
aboutSpecialThanksLabel: 'Special Thanks',
|
||||||
changelog: 'Changelog',
|
changelog: 'Changelog',
|
||||||
showChangelogOnUpdate: "Show 'What's New' on update",
|
showChangelogOnUpdate: "Show 'What's New' on update",
|
||||||
showChangelogOnUpdateDesc: "Automatically show what's new when a new version is launched for the first time.",
|
showChangelogOnUpdateDesc: "Show a discreet changelog banner above Now Playing after an update. Click opens the release notes; X dismisses it.",
|
||||||
randomMixTitle: 'Random Mix',
|
randomMixTitle: 'Random Mix',
|
||||||
randomMixBlacklistTitle: 'Custom Filter Keywords',
|
randomMixBlacklistTitle: 'Custom Filter Keywords',
|
||||||
randomMixBlacklistDesc: 'Songs are excluded when any keyword matches their genre, title, album, or artist (active when the checkbox above is on).',
|
randomMixBlacklistDesc: 'Songs are excluded when any keyword matches their genre, title, album, or artist (active when the checkbox above is on).',
|
||||||
@@ -741,6 +743,14 @@ export const enTranslation = {
|
|||||||
dontShowAgain: "Don't show again",
|
dontShowAgain: "Don't show again",
|
||||||
close: 'Got it',
|
close: 'Got it',
|
||||||
},
|
},
|
||||||
|
whatsNew: {
|
||||||
|
title: "What's New",
|
||||||
|
empty: 'No changelog entry for this version yet.',
|
||||||
|
bannerTitle: 'Changelog',
|
||||||
|
bannerCollapsed: "What's new in v{{version}}",
|
||||||
|
dismiss: 'Dismiss',
|
||||||
|
close: 'Close',
|
||||||
|
},
|
||||||
help: {
|
help: {
|
||||||
title: 'Help',
|
title: 'Help',
|
||||||
s1: 'Getting Started',
|
s1: 'Getting Started',
|
||||||
|
|||||||
+11
-1
@@ -592,11 +592,13 @@ export const esTranslation = {
|
|||||||
aboutVersion: 'Versión',
|
aboutVersion: 'Versión',
|
||||||
aboutBuiltWith: 'Construido con Tauri · React · TypeScript · Rust/rodio',
|
aboutBuiltWith: 'Construido con Tauri · React · TypeScript · Rust/rodio',
|
||||||
aboutAiCredit: 'Desarrollado con el apoyo de Claude Code by Anthropic',
|
aboutAiCredit: 'Desarrollado con el apoyo de Claude Code by Anthropic',
|
||||||
|
aboutReleaseNotesLabel: 'Notas de la versión',
|
||||||
|
aboutReleaseNotesLink: 'Abrir las novedades de esta versión',
|
||||||
aboutContributorsLabel: 'Contribuidores',
|
aboutContributorsLabel: 'Contribuidores',
|
||||||
aboutSpecialThanksLabel: 'Agradecimientos Especiales',
|
aboutSpecialThanksLabel: 'Agradecimientos Especiales',
|
||||||
changelog: 'Registro de Cambios',
|
changelog: 'Registro de Cambios',
|
||||||
showChangelogOnUpdate: "Mostrar 'Novedades' al actualizar",
|
showChangelogOnUpdate: "Mostrar 'Novedades' al actualizar",
|
||||||
showChangelogOnUpdateDesc: "Muestra automáticamente las novedades cuando se lanza una nueva versión por primera vez.",
|
showChangelogOnUpdateDesc: 'Muestra un discreto banner de changelog encima de Now Playing tras una actualización. Clic abre las notas de la versión; X lo oculta.',
|
||||||
randomMixTitle: 'Mezcla Aleatoria',
|
randomMixTitle: 'Mezcla Aleatoria',
|
||||||
randomMixBlacklistTitle: 'Palabras Clave de Filtro Personalizadas',
|
randomMixBlacklistTitle: 'Palabras Clave de Filtro Personalizadas',
|
||||||
randomMixBlacklistDesc: 'Las canciones se excluyen cuando cualquier palabra clave coincide con su género, título, álbum o artista (activo cuando el checkbox de arriba está activado).',
|
randomMixBlacklistDesc: 'Las canciones se excluyen cuando cualquier palabra clave coincide con su género, título, álbum o artista (activo cuando el checkbox de arriba está activado).',
|
||||||
@@ -732,6 +734,14 @@ export const esTranslation = {
|
|||||||
dontShowAgain: 'No mostrar de nuevo',
|
dontShowAgain: 'No mostrar de nuevo',
|
||||||
close: 'Entendido',
|
close: 'Entendido',
|
||||||
},
|
},
|
||||||
|
whatsNew: {
|
||||||
|
title: 'Novedades',
|
||||||
|
empty: 'Aún no hay entrada de changelog para esta versión.',
|
||||||
|
bannerTitle: 'Changelog',
|
||||||
|
bannerCollapsed: 'Novedades en v{{version}}',
|
||||||
|
dismiss: 'Ocultar',
|
||||||
|
close: 'Cerrar',
|
||||||
|
},
|
||||||
help: {
|
help: {
|
||||||
title: 'Ayuda',
|
title: 'Ayuda',
|
||||||
s1: 'Primeros Pasos',
|
s1: 'Primeros Pasos',
|
||||||
|
|||||||
+11
-1
@@ -587,11 +587,13 @@ export const frTranslation = {
|
|||||||
aboutVersion: 'Version',
|
aboutVersion: 'Version',
|
||||||
aboutBuiltWith: 'Construit avec Tauri · React · TypeScript · Rust/rodio',
|
aboutBuiltWith: 'Construit avec Tauri · React · TypeScript · Rust/rodio',
|
||||||
aboutAiCredit: 'Développé avec le soutien de Claude Code par Anthropic',
|
aboutAiCredit: 'Développé avec le soutien de Claude Code par Anthropic',
|
||||||
|
aboutReleaseNotesLabel: 'Notes de version',
|
||||||
|
aboutReleaseNotesLink: 'Ouvrir les nouveautés de cette version',
|
||||||
aboutContributorsLabel: 'Contributeurs',
|
aboutContributorsLabel: 'Contributeurs',
|
||||||
aboutSpecialThanksLabel: 'Remerciements spéciaux',
|
aboutSpecialThanksLabel: 'Remerciements spéciaux',
|
||||||
changelog: 'Journal des modifications',
|
changelog: 'Journal des modifications',
|
||||||
showChangelogOnUpdate: "Afficher 'Quoi de neuf' lors des mises à jour",
|
showChangelogOnUpdate: "Afficher 'Quoi de neuf' lors des mises à jour",
|
||||||
showChangelogOnUpdateDesc: 'Affiche automatiquement les nouveautés au premier lancement d\'une nouvelle version.',
|
showChangelogOnUpdateDesc: "Affiche une bannière discrète du changelog au-dessus de Now Playing après une mise à jour. Un clic ouvre les notes de version, le X la masque.",
|
||||||
randomMixTitle: 'Mix aléatoire',
|
randomMixTitle: 'Mix aléatoire',
|
||||||
randomMixBlacklistTitle: 'Mots-clés de filtre personnalisés',
|
randomMixBlacklistTitle: 'Mots-clés de filtre personnalisés',
|
||||||
randomMixBlacklistDesc: 'Les morceaux sont exclus si un mot-clé correspond à leur genre, titre, album ou artiste (actif quand la case ci-dessus est cochée).',
|
randomMixBlacklistDesc: 'Les morceaux sont exclus si un mot-clé correspond à leur genre, titre, album ou artiste (actif quand la case ci-dessus est cochée).',
|
||||||
@@ -727,6 +729,14 @@ export const frTranslation = {
|
|||||||
dontShowAgain: 'Ne plus afficher',
|
dontShowAgain: 'Ne plus afficher',
|
||||||
close: 'Compris',
|
close: 'Compris',
|
||||||
},
|
},
|
||||||
|
whatsNew: {
|
||||||
|
title: 'Quoi de neuf',
|
||||||
|
empty: 'Aucune entrée de changelog pour cette version.',
|
||||||
|
bannerTitle: 'Changelog',
|
||||||
|
bannerCollapsed: 'Nouveau dans v{{version}}',
|
||||||
|
dismiss: 'Ignorer',
|
||||||
|
close: 'Fermer',
|
||||||
|
},
|
||||||
help: {
|
help: {
|
||||||
title: 'Aide',
|
title: 'Aide',
|
||||||
s1: 'Démarrage',
|
s1: 'Démarrage',
|
||||||
|
|||||||
+11
-1
@@ -586,11 +586,13 @@ export const nbTranslation = {
|
|||||||
aboutVersion: 'Versjon',
|
aboutVersion: 'Versjon',
|
||||||
aboutBuiltWith: 'Bygget med Tauri · React · TypeScript · Rust/rodio',
|
aboutBuiltWith: 'Bygget med Tauri · React · TypeScript · Rust/rodio',
|
||||||
aboutAiCredit: 'Utviklet med støtte fra Claude Code laget av Anthropic',
|
aboutAiCredit: 'Utviklet med støtte fra Claude Code laget av Anthropic',
|
||||||
|
aboutReleaseNotesLabel: 'Versjonsnotater',
|
||||||
|
aboutReleaseNotesLink: 'Åpne nyhetene for denne versjonen',
|
||||||
aboutContributorsLabel: 'Bidragsytere',
|
aboutContributorsLabel: 'Bidragsytere',
|
||||||
aboutSpecialThanksLabel: 'Spesiell takk',
|
aboutSpecialThanksLabel: 'Spesiell takk',
|
||||||
changelog: 'Endringslogg',
|
changelog: 'Endringslogg',
|
||||||
showChangelogOnUpdate: "Vis 'Hva er nytt' ved oppdatering til ny versjon",
|
showChangelogOnUpdate: "Vis 'Hva er nytt' ved oppdatering til ny versjon",
|
||||||
showChangelogOnUpdateDesc: "Vis automatisk hva som er nytt når en ny versjon lanseres for første gang.",
|
showChangelogOnUpdateDesc: "Viser et diskret changelog-banner over Now Playing etter en oppdatering. Klikk åpner versjonsnotatene; X skjuler det.",
|
||||||
randomMixTitle: 'Tilfeldig miks',
|
randomMixTitle: 'Tilfeldig miks',
|
||||||
randomMixBlacklistTitle: 'Egendefinerte filternøkkelord',
|
randomMixBlacklistTitle: 'Egendefinerte filternøkkelord',
|
||||||
randomMixBlacklistDesc: 'Sanger ekskluderes når et nøkkelord samsvarer med sjanger, tittel, album eller artist (aktiv når avkrysningsboksen ovenfor er på).',
|
randomMixBlacklistDesc: 'Sanger ekskluderes når et nøkkelord samsvarer med sjanger, tittel, album eller artist (aktiv når avkrysningsboksen ovenfor er på).',
|
||||||
@@ -726,6 +728,14 @@ export const nbTranslation = {
|
|||||||
dontShowAgain: "Ikke vis igjen",
|
dontShowAgain: "Ikke vis igjen",
|
||||||
close: 'Forstått',
|
close: 'Forstått',
|
||||||
},
|
},
|
||||||
|
whatsNew: {
|
||||||
|
title: 'Nyheter',
|
||||||
|
empty: 'Ingen endringslogg for denne versjonen ennå.',
|
||||||
|
bannerTitle: 'Changelog',
|
||||||
|
bannerCollapsed: 'Nyheter i v{{version}}',
|
||||||
|
dismiss: 'Skjul',
|
||||||
|
close: 'Lukk',
|
||||||
|
},
|
||||||
help: {
|
help: {
|
||||||
title: 'Hjelp',
|
title: 'Hjelp',
|
||||||
s1: 'Komme i gang',
|
s1: 'Komme i gang',
|
||||||
|
|||||||
+11
-1
@@ -586,11 +586,13 @@ export const nlTranslation = {
|
|||||||
aboutVersion: 'Versie',
|
aboutVersion: 'Versie',
|
||||||
aboutBuiltWith: 'Gebouwd met Tauri · React · TypeScript · Rust/rodio',
|
aboutBuiltWith: 'Gebouwd met Tauri · React · TypeScript · Rust/rodio',
|
||||||
aboutAiCredit: 'Ontwikkeld met ondersteuning van Claude Code door Anthropic',
|
aboutAiCredit: 'Ontwikkeld met ondersteuning van Claude Code door Anthropic',
|
||||||
|
aboutReleaseNotesLabel: 'Release-notities',
|
||||||
|
aboutReleaseNotesLink: 'Nieuws van deze versie openen',
|
||||||
aboutContributorsLabel: 'Bijdragers',
|
aboutContributorsLabel: 'Bijdragers',
|
||||||
aboutSpecialThanksLabel: 'Speciale dank',
|
aboutSpecialThanksLabel: 'Speciale dank',
|
||||||
changelog: 'Wijzigingslog',
|
changelog: 'Wijzigingslog',
|
||||||
showChangelogOnUpdate: "'Wat is nieuw' tonen bij update",
|
showChangelogOnUpdate: "'Wat is nieuw' tonen bij update",
|
||||||
showChangelogOnUpdateDesc: 'Toont automatisch de nieuwigheden bij de eerste start van een nieuwe versie.',
|
showChangelogOnUpdateDesc: 'Toont een discrete changelog-banner boven Now Playing na een update. Klik opent de release-notities; X verbergt hem.',
|
||||||
randomMixTitle: 'Willekeurige mix',
|
randomMixTitle: 'Willekeurige mix',
|
||||||
randomMixBlacklistTitle: 'Aangepaste filtertrefwoorden',
|
randomMixBlacklistTitle: 'Aangepaste filtertrefwoorden',
|
||||||
randomMixBlacklistDesc: 'Nummers worden uitgesloten als een trefwoord overeenkomt met hun genre, titel, album of artiest (actief wanneer het selectievakje hierboven is aangevinkt).',
|
randomMixBlacklistDesc: 'Nummers worden uitgesloten als een trefwoord overeenkomt met hun genre, titel, album of artiest (actief wanneer het selectievakje hierboven is aangevinkt).',
|
||||||
@@ -726,6 +728,14 @@ export const nlTranslation = {
|
|||||||
dontShowAgain: 'Niet meer weergeven',
|
dontShowAgain: 'Niet meer weergeven',
|
||||||
close: 'Begrepen',
|
close: 'Begrepen',
|
||||||
},
|
},
|
||||||
|
whatsNew: {
|
||||||
|
title: 'Wat is nieuw',
|
||||||
|
empty: 'Nog geen changelog-item voor deze versie.',
|
||||||
|
bannerTitle: 'Changelog',
|
||||||
|
bannerCollapsed: 'Nieuw in v{{version}}',
|
||||||
|
dismiss: 'Verbergen',
|
||||||
|
close: 'Sluiten',
|
||||||
|
},
|
||||||
help: {
|
help: {
|
||||||
title: 'Help',
|
title: 'Help',
|
||||||
s1: 'Aan de slag',
|
s1: 'Aan de slag',
|
||||||
|
|||||||
+11
-1
@@ -613,11 +613,13 @@ export const ruTranslation = {
|
|||||||
aboutVersion: 'Версия',
|
aboutVersion: 'Версия',
|
||||||
aboutBuiltWith: 'Tauri · React · TypeScript · Rust/rodio',
|
aboutBuiltWith: 'Tauri · React · TypeScript · Rust/rodio',
|
||||||
aboutAiCredit: 'При поддержке Claude Code (Anthropic)',
|
aboutAiCredit: 'При поддержке Claude Code (Anthropic)',
|
||||||
|
aboutReleaseNotesLabel: 'Примечания к выпуску',
|
||||||
|
aboutReleaseNotesLink: 'Открыть «Что нового» этой версии',
|
||||||
aboutContributorsLabel: 'Участники',
|
aboutContributorsLabel: 'Участники',
|
||||||
aboutSpecialThanksLabel: 'Особая благодарность',
|
aboutSpecialThanksLabel: 'Особая благодарность',
|
||||||
changelog: 'Список изменений',
|
changelog: 'Список изменений',
|
||||||
showChangelogOnUpdate: 'Показывать «Что нового» после обновления',
|
showChangelogOnUpdate: 'Показывать «Что нового» после обновления',
|
||||||
showChangelogOnUpdateDesc: 'Автоматически при первом запуске новой версии.',
|
showChangelogOnUpdateDesc: 'После обновления над «Сейчас играет» появится ненавязчивый баннер журнала изменений. Клик открывает заметки о выпуске, X скрывает.',
|
||||||
randomMixTitle: 'Случайный микс',
|
randomMixTitle: 'Случайный микс',
|
||||||
randomMixBlacklistTitle: 'Свои слова-фильтры',
|
randomMixBlacklistTitle: 'Свои слова-фильтры',
|
||||||
randomMixBlacklistDesc:
|
randomMixBlacklistDesc:
|
||||||
@@ -756,6 +758,14 @@ export const ruTranslation = {
|
|||||||
dontShowAgain: 'Больше не показывать',
|
dontShowAgain: 'Больше не показывать',
|
||||||
close: 'Понятно',
|
close: 'Понятно',
|
||||||
},
|
},
|
||||||
|
whatsNew: {
|
||||||
|
title: 'Что нового',
|
||||||
|
empty: 'Для этой версии пока нет записи в журнале изменений.',
|
||||||
|
bannerTitle: 'Журнал изменений',
|
||||||
|
bannerCollapsed: 'Что нового в v{{version}}',
|
||||||
|
dismiss: 'Скрыть',
|
||||||
|
close: 'Закрыть',
|
||||||
|
},
|
||||||
help: {
|
help: {
|
||||||
title: 'Справка',
|
title: 'Справка',
|
||||||
s1: 'С чего начать',
|
s1: 'С чего начать',
|
||||||
|
|||||||
+11
-1
@@ -582,11 +582,13 @@ export const zhTranslation = {
|
|||||||
aboutVersion: '版本',
|
aboutVersion: '版本',
|
||||||
aboutBuiltWith: '使用 Tauri · React · TypeScript · Rust/rodio 构建',
|
aboutBuiltWith: '使用 Tauri · React · TypeScript · Rust/rodio 构建',
|
||||||
aboutAiCredit: '在 Anthropic 的 Claude Code 支持下开发',
|
aboutAiCredit: '在 Anthropic 的 Claude Code 支持下开发',
|
||||||
|
aboutReleaseNotesLabel: '版本说明',
|
||||||
|
aboutReleaseNotesLink: '打开此版本的新功能',
|
||||||
aboutContributorsLabel: '贡献者',
|
aboutContributorsLabel: '贡献者',
|
||||||
aboutSpecialThanksLabel: '特别感谢',
|
aboutSpecialThanksLabel: '特别感谢',
|
||||||
changelog: '更新日志',
|
changelog: '更新日志',
|
||||||
showChangelogOnUpdate: '更新时显示"新功能"',
|
showChangelogOnUpdate: '更新时显示"新功能"',
|
||||||
showChangelogOnUpdateDesc: '新版本首次启动时自动显示更新内容。',
|
showChangelogOnUpdateDesc: '更新后在「正在播放」上方显示一个低调的更新日志横幅。点击打开发行说明,X 按钮关闭。',
|
||||||
randomMixTitle: '随机混音',
|
randomMixTitle: '随机混音',
|
||||||
randomMixBlacklistTitle: '自定义过滤关键词',
|
randomMixBlacklistTitle: '自定义过滤关键词',
|
||||||
randomMixBlacklistDesc: '当任何关键词匹配流派、标题、专辑或艺术家时,歌曲将被排除(当上方复选框开启时生效)。',
|
randomMixBlacklistDesc: '当任何关键词匹配流派、标题、专辑或艺术家时,歌曲将被排除(当上方复选框开启时生效)。',
|
||||||
@@ -722,6 +724,14 @@ export const zhTranslation = {
|
|||||||
dontShowAgain: '不再显示',
|
dontShowAgain: '不再显示',
|
||||||
close: '知道了',
|
close: '知道了',
|
||||||
},
|
},
|
||||||
|
whatsNew: {
|
||||||
|
title: '新功能',
|
||||||
|
empty: '此版本暂无更新日志。',
|
||||||
|
bannerTitle: '更新日志',
|
||||||
|
bannerCollapsed: 'v{{version}} 新功能',
|
||||||
|
dismiss: '关闭',
|
||||||
|
close: '关闭',
|
||||||
|
},
|
||||||
help: {
|
help: {
|
||||||
title: '帮助',
|
title: '帮助',
|
||||||
s1: '入门指南',
|
s1: '入门指南',
|
||||||
|
|||||||
@@ -2445,6 +2445,18 @@ export default function Settings() {
|
|||||||
<span style={{ color: 'var(--text-muted)', minWidth: 56 }}>AI</span>
|
<span style={{ color: 'var(--text-muted)', minWidth: 56 }}>AI</span>
|
||||||
<span style={{ color: 'var(--text-secondary)' }}>{t('settings.aboutAiCredit')}</span>
|
<span style={{ color: 'var(--text-secondary)' }}>{t('settings.aboutAiCredit')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<span style={{ color: 'var(--text-muted)', minWidth: 56 }}>{t('settings.aboutReleaseNotesLabel')}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
useAuthStore.getState().setLastSeenChangelogVersion('');
|
||||||
|
navigate('/whats-new');
|
||||||
|
}}
|
||||||
|
style={{ color: 'var(--accent)', background: 'none', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left' }}
|
||||||
|
>
|
||||||
|
{t('settings.aboutReleaseNotesLink')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
style={{ display: 'flex', width: '100%', alignItems: 'center', gap: '0.5rem', background: 'none', border: 'none', cursor: 'pointer', padding: 0, textAlign: 'left' }}
|
style={{ display: 'flex', width: '100%', alignItems: 'center', gap: '0.5rem', background: 'none', border: 'none', cursor: 'pointer', padding: 0, textAlign: 'left' }}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Sparkles, X } from 'lucide-react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { version } from '../../package.json';
|
||||||
|
import changelogRaw from '../../CHANGELOG.md?raw';
|
||||||
|
import { renderChangelogBody } from '../utils/changelogMarkdown';
|
||||||
|
|
||||||
|
export default function WhatsNew() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (window.history.length > 1) navigate(-1);
|
||||||
|
else navigate('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const entry = useMemo(() => {
|
||||||
|
const blocks = changelogRaw.split(/\n(?=## \[)/).filter((b: string) => b.startsWith('## ['));
|
||||||
|
const block = blocks.find((b: string) => b.startsWith(`## [${version}]`));
|
||||||
|
if (!block) return null;
|
||||||
|
const lines = block.split('\n');
|
||||||
|
const match = lines[0].match(/## \[([^\]]+)\](?:\s*-\s*(.+))?/);
|
||||||
|
const body = lines.slice(1).join('\n').trim();
|
||||||
|
return { version: match?.[1] ?? version, date: match?.[2] ?? '', body };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="whats-new">
|
||||||
|
<header className="whats-new__header">
|
||||||
|
<div className="whats-new__title-row">
|
||||||
|
<Sparkles size={20} className="whats-new__icon" />
|
||||||
|
<div>
|
||||||
|
<h1 className="whats-new__title">{t('whatsNew.title')}</h1>
|
||||||
|
<div className="whats-new__subtitle">
|
||||||
|
v{entry?.version ?? version}
|
||||||
|
{entry?.date && <span className="whats-new__date"> · {entry.date}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="whats-new__close"
|
||||||
|
onClick={close}
|
||||||
|
aria-label={t('whatsNew.close')}
|
||||||
|
data-tooltip={t('whatsNew.close')}
|
||||||
|
data-tooltip-pos="bottom"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="whats-new__body">
|
||||||
|
{entry ? (
|
||||||
|
renderChangelogBody(entry.body)
|
||||||
|
) : (
|
||||||
|
<p className="whats-new__empty">{t('whatsNew.empty')}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+224
-1
@@ -1868,7 +1868,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-size {
|
.track-size {
|
||||||
color: var(--ctp-overlay0);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-duration {
|
.track-duration {
|
||||||
@@ -8832,6 +8832,229 @@ html.no-compositing .fsr-lyric-line.fsrl-active .fsr-lyric-word.active {
|
|||||||
led-pulse, …) even when the app is minimized, causing constant GPU use.
|
led-pulse, …) even when the app is minimized, causing constant GPU use.
|
||||||
Pausing them cuts that to near zero while hidden without touching the
|
Pausing them cuts that to near zero while hidden without touching the
|
||||||
running Rust/audio threads. */
|
running Rust/audio threads. */
|
||||||
|
/* ─ What's New — banner + page ────────────────────────────────────────────
|
||||||
|
Fixed neutral palette so it reads identically on every theme (light and
|
||||||
|
dark). The banner sits in the sidebar just above Now Playing; the page
|
||||||
|
opens in the main content area. */
|
||||||
|
|
||||||
|
.sidebar-bottom-spacer {
|
||||||
|
margin-top: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
background: #1f2233;
|
||||||
|
color: #dfe3f6;
|
||||||
|
border: 1px solid #3a3f5a;
|
||||||
|
border-radius: 8px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-banner:hover {
|
||||||
|
background: #272b40;
|
||||||
|
border-color: #4a5072;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-banner__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #8ec8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-banner__text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-banner__title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f1f3fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-banner__version {
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: #9aa0bd;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-banner__dismiss {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #9aa0bd;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.whats-new-banner__dismiss:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #e4e7f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-banner--collapsed {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Page ─── */
|
||||||
|
.whats-new {
|
||||||
|
max-width: 880px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 32px 48px;
|
||||||
|
color: #e4e7f5;
|
||||||
|
background: #16182a;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 8px 24px rgba(0,0,0,0.25);
|
||||||
|
border: 1px solid #2a2d44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new__header {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #2a2d44;
|
||||||
|
}
|
||||||
|
.whats-new__title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new__close {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #3a3f5a;
|
||||||
|
background: transparent;
|
||||||
|
color: #9aa0bd;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.whats-new__close:hover {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
color: #e4e7f5;
|
||||||
|
border-color: #4a5072;
|
||||||
|
}
|
||||||
|
.whats-new__icon {
|
||||||
|
color: #8ec8ff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.whats-new__title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
color: #f1f3fb;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.whats-new__subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9aa0bd;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.whats-new__date {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new__body {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
.whats-new__empty {
|
||||||
|
color: #9aa0bd;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f1f3fb;
|
||||||
|
margin: 22px 0 10px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid #2a2d44;
|
||||||
|
}
|
||||||
|
.whats-new-h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #dfe3f6;
|
||||||
|
margin: 14px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-p {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-list {
|
||||||
|
list-style: disc;
|
||||||
|
padding-left: 22px;
|
||||||
|
margin: 6px 0 10px;
|
||||||
|
}
|
||||||
|
.whats-new-list li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
.whats-new-list li::marker {
|
||||||
|
color: #6b7192;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-quote {
|
||||||
|
border-left: 3px solid #3a3f5a;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin: 10px 0;
|
||||||
|
color: #b6bcd8;
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #2a2d44;
|
||||||
|
margin: 18px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-code {
|
||||||
|
background: #262a3e;
|
||||||
|
color: #d4d7f0;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: var(--font-mono, ui-monospace, monospace);
|
||||||
|
font-size: 12.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-new-link {
|
||||||
|
color: #8ec8ff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px dashed #4a5072;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.whats-new-link:hover {
|
||||||
|
color: #b6dbff;
|
||||||
|
border-bottom-color: #8ec8ff;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─ Mini Player window ───────────────────────────────────────────────────── */
|
/* ─ Mini Player window ───────────────────────────────────────────────────── */
|
||||||
.mini-player {
|
.mini-player {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
+24
-5
@@ -4584,9 +4584,10 @@ input[type="range"]:hover::-webkit-slider-thumb {
|
|||||||
--accent-glow: rgba(69, 255, 0, 0.45);
|
--accent-glow: rgba(69, 255, 0, 0.45);
|
||||||
--text-primary: #ffffff;
|
--text-primary: #ffffff;
|
||||||
--text-secondary: #dce8ff;
|
--text-secondary: #dce8ff;
|
||||||
--text-muted: #b8d0f8;
|
--text-muted: #e8f0ff;
|
||||||
/* lighter so readable on #3a62a5 */
|
/* lifted from #b8d0f8 — clears AA on Luna-blue bg-app and dark sidebar */
|
||||||
--border: #2a5090;
|
--border: #071027;
|
||||||
|
/* darkened from #2a5090 to meet 3:1 UI threshold on all main surfaces */
|
||||||
--border-subtle: #4a72b8;
|
--border-subtle: #4a72b8;
|
||||||
--positive: #30dd00;
|
--positive: #30dd00;
|
||||||
--warning: #ffdd44;
|
--warning: #ffdd44;
|
||||||
@@ -10780,11 +10781,13 @@ input[type="range"]:hover::-webkit-slider-thumb {
|
|||||||
--accent-glow: rgba(21, 101, 200, 0.40);
|
--accent-glow: rgba(21, 101, 200, 0.40);
|
||||||
--text-primary: #0d1d3c;
|
--text-primary: #0d1d3c;
|
||||||
--text-secondary: #2a4878;
|
--text-secondary: #2a4878;
|
||||||
--text-muted: #4870a8;
|
--text-muted: #3f6aa0;
|
||||||
|
/* slight lift from #4870a8 to clear AA on bg-card (4.23 → 4.65) */
|
||||||
--border: rgba(100, 160, 220, 0.45);
|
--border: rgba(100, 160, 220, 0.45);
|
||||||
--border-subtle: rgba(140, 190, 240, 0.30);
|
--border-subtle: rgba(140, 190, 240, 0.30);
|
||||||
--positive: #1a8020;
|
--positive: #1a8020;
|
||||||
--warning: #c8980c;
|
--warning: #735a00;
|
||||||
|
/* deeper Vista gold — #c8980c was 2.37:1 on bg-app, now 5.91:1 */
|
||||||
--danger: #c02020;
|
--danger: #c02020;
|
||||||
--radius-sm: 3px;
|
--radius-sm: 3px;
|
||||||
--radius-md: 6px;
|
--radius-md: 6px;
|
||||||
@@ -11122,6 +11125,22 @@ input[type="range"]:hover::-webkit-slider-thumb {
|
|||||||
color: #6090b8;
|
color: #6090b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Lyrics pane sits inside the queue panel (bg-sidebar = #0e1e3e deep navy).
|
||||||
|
Default --text-primary / --text-muted are near-navy and invisible there —
|
||||||
|
explicit light tokens get the pane back to AA+. */
|
||||||
|
[data-theme='wista'] .lyrics-line,
|
||||||
|
[data-theme='wista'] .lyrics-status,
|
||||||
|
[data-theme='wista'] .lyrics-word-synced .lyrics-line {
|
||||||
|
color: #aac8f0;
|
||||||
|
}
|
||||||
|
[data-theme='wista'] .lyrics-line.active,
|
||||||
|
[data-theme='wista'] .lyrics-word-synced .lyrics-line.active {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
[data-theme='wista'] .lyrics-line.completed {
|
||||||
|
color: #aac8f0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─────────────────────────────────────────── */
|
/* ─────────────────────────────────────────── */
|
||||||
|
|
||||||
[data-theme='aqua-quartz'] {
|
[data-theme='aqua-quartz'] {
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { open } from '@tauri-apps/plugin-shell';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render inline markdown segments: **bold**, *italic*, `code`, [text](url).
|
||||||
|
* External links open in the user's default browser via the Tauri shell plugin.
|
||||||
|
*/
|
||||||
|
export function renderInlineMarkdown(text: string, keyPrefix = 'i'): React.ReactNode[] {
|
||||||
|
// Tokenize — order matters: links first (no recursion), then emphasis/code.
|
||||||
|
const tokens: React.ReactNode[] = [];
|
||||||
|
const linkRe = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
const pushInline = (segment: string) => {
|
||||||
|
const parts = segment.split(/(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g);
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!part) continue;
|
||||||
|
if (part.startsWith('**') && part.endsWith('**')) {
|
||||||
|
tokens.push(<strong key={`${keyPrefix}-${i++}`}>{part.slice(2, -2)}</strong>);
|
||||||
|
} else if (part.startsWith('*') && part.endsWith('*') && part.length > 2) {
|
||||||
|
tokens.push(<em key={`${keyPrefix}-${i++}`}>{part.slice(1, -1)}</em>);
|
||||||
|
} else if (part.startsWith('`') && part.endsWith('`')) {
|
||||||
|
tokens.push(<code key={`${keyPrefix}-${i++}`} className="whats-new-code">{part.slice(1, -1)}</code>);
|
||||||
|
} else {
|
||||||
|
tokens.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
while ((match = linkRe.exec(text)) !== null) {
|
||||||
|
if (match.index > lastIndex) pushInline(text.slice(lastIndex, match.index));
|
||||||
|
const [full, label, url] = match;
|
||||||
|
tokens.push(
|
||||||
|
<a
|
||||||
|
key={`${keyPrefix}-link-${i++}`}
|
||||||
|
href={url}
|
||||||
|
onClick={(e) => { e.preventDefault(); open(url).catch(() => {}); }}
|
||||||
|
className="whats-new-link"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
lastIndex = match.index + full.length;
|
||||||
|
}
|
||||||
|
if (lastIndex < text.length) pushInline(text.slice(lastIndex));
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a subset of GitHub-flavored Markdown used by our CHANGELOG: headings
|
||||||
|
* (### / ####), bullets (- / *), blockquotes, horizontal rules, and inline
|
||||||
|
* formatting (bold/italic/code/links).
|
||||||
|
*/
|
||||||
|
export function renderChangelogBody(body: string): React.ReactNode[] {
|
||||||
|
const lines = body.split('\n');
|
||||||
|
const out: React.ReactNode[] = [];
|
||||||
|
let bulletBuffer: React.ReactNode[] = [];
|
||||||
|
let quoteBuffer: string[] = [];
|
||||||
|
|
||||||
|
const flushBullets = () => {
|
||||||
|
if (bulletBuffer.length === 0) return;
|
||||||
|
out.push(<ul key={`ul-${out.length}`} className="whats-new-list">{bulletBuffer}</ul>);
|
||||||
|
bulletBuffer = [];
|
||||||
|
};
|
||||||
|
const flushQuote = () => {
|
||||||
|
if (quoteBuffer.length === 0) return;
|
||||||
|
out.push(
|
||||||
|
<blockquote key={`q-${out.length}`} className="whats-new-quote">
|
||||||
|
{renderInlineMarkdown(quoteBuffer.join(' '), `q-${out.length}`)}
|
||||||
|
</blockquote>
|
||||||
|
);
|
||||||
|
quoteBuffer = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed === '') { flushBullets(); flushQuote(); continue; }
|
||||||
|
|
||||||
|
if (trimmed === '---') {
|
||||||
|
flushBullets(); flushQuote();
|
||||||
|
out.push(<hr key={`hr-${out.length}`} className="whats-new-hr" />);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('### ')) {
|
||||||
|
flushBullets(); flushQuote();
|
||||||
|
out.push(<h3 key={`h3-${out.length}`} className="whats-new-h3">{renderInlineMarkdown(line.slice(4), `h3-${i}`)}</h3>);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.startsWith('#### ')) {
|
||||||
|
flushBullets(); flushQuote();
|
||||||
|
out.push(<h4 key={`h4-${out.length}`} className="whats-new-h4">{renderInlineMarkdown(line.slice(5), `h4-${i}`)}</h4>);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('> ')) {
|
||||||
|
flushBullets();
|
||||||
|
quoteBuffer.push(line.slice(2));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('- ') || line.startsWith('* ')) {
|
||||||
|
flushQuote();
|
||||||
|
bulletBuffer.push(
|
||||||
|
<li key={`li-${i}`}>{renderInlineMarkdown(line.slice(2), `li-${i}`)}</li>
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paragraph / plain line
|
||||||
|
flushBullets(); flushQuote();
|
||||||
|
out.push(<p key={`p-${i}`} className="whats-new-p">{renderInlineMarkdown(line, `p-${i}`)}</p>);
|
||||||
|
}
|
||||||
|
flushBullets(); flushQuote();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user