diff --git a/src/App.tsx b/src/App.tsx index 5207df86..48e2b37f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,6 +38,7 @@ import InternetRadio from './pages/InternetRadio'; import FolderBrowser from './pages/FolderBrowser'; import DeviceSync from './pages/DeviceSync'; import NowPlayingPage from './pages/NowPlaying'; +import WhatsNew from './pages/WhatsNew'; import MiniPlayer from './components/MiniPlayer'; import { initMiniPlayerBridgeOnMain } from './utils/miniPlayerBridge'; import FullscreenPlayer from './components/FullscreenPlayer'; @@ -53,7 +54,6 @@ import OfflineLibrary from './pages/OfflineLibrary'; import Genres from './pages/Genres'; import GenreDetail from './pages/GenreDetail'; import ExportPickerModal from './components/ExportPickerModal'; -import ChangelogModal from './components/ChangelogModal'; import AppUpdater from './components/AppUpdater'; import TitleBar from './components/TitleBar'; import { IS_LINUX } from './utils/platform'; @@ -229,14 +229,9 @@ function AppShell() { fn(); }, [currentTrack, isPlaying]); - const [changelogModalOpen, setChangelogModalOpen] = useState(false); - - useEffect(() => { - const { showChangelogOnUpdate, lastSeenChangelogVersion } = useAuthStore.getState(); - if (showChangelogOnUpdate && lastSeenChangelogVersion !== version) { - setChangelogModalOpen(true); - } - }, []); + // Post-update changelog is now surfaced via a dismissible banner in the + // sidebar (WhatsNewBanner) that links to the /whats-new page — no auto + // modal takeover on startup. const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => { return localStorage.getItem('psysonic_sidebar_collapsed') === 'true'; @@ -402,6 +397,7 @@ function AppShell() { } /> : } /> } /> + } /> } /> } /> } /> @@ -436,7 +432,6 @@ function AppShell() { - {changelogModalOpen && setChangelogModalOpen(false)} />} ); } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 44bd4286..6a11286f 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -15,6 +15,7 @@ import { } from 'lucide-react'; import PsysonicLogo from './PsysonicLogo'; import PSmallLogo from './PSmallLogo'; +import WhatsNewBanner from './WhatsNewBanner'; import { getPlaylists } from '../api/subsonic'; import { usePlaylistStore } from '../store/playlistStore'; 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. */} +
+ + {/* What's New banner — only visible while the current release hasn't been seen. */} + + {/* Now Playing — fixed, always visible */} `nav-link nav-link-nowplaying ${isActive ? 'active' : ''}`} data-tooltip={isCollapsed ? t('sidebar.nowPlaying') : undefined} data-tooltip-pos="bottom" - style={{ marginTop: 'auto' }} > diff --git a/src/components/WhatsNewBanner.tsx b/src/components/WhatsNewBanner.tsx new file mode 100644 index 00000000..88a09e14 --- /dev/null +++ b/src/components/WhatsNewBanner.tsx @@ -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 ( + + ); + } + + return ( + + ); +} diff --git a/src/locales/de.ts b/src/locales/de.ts index c15c4e42..cec68616 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -599,11 +599,13 @@ export const deTranslation = { aboutVersion: 'Version', aboutBuiltWith: 'Gebaut mit Tauri · React · TypeScript · Rust/rodio', aboutAiCredit: 'Mit freundlicher Unterstützung von Claude Code by Anthropic', + aboutReleaseNotesLabel: 'Release Notes', + aboutReleaseNotesLink: 'Neuigkeiten dieser Version öffnen', aboutContributorsLabel: 'Mitwirkende', aboutSpecialThanksLabel: 'Besonderer Dank', changelog: 'Changelog', 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', 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).', @@ -739,6 +741,14 @@ export const deTranslation = { dontShowAgain: 'Nicht mehr anzeigen', 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: { title: 'Hilfe', s1: 'Erste Schritte', diff --git a/src/locales/en.ts b/src/locales/en.ts index ceee2906..c0847f43 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -601,11 +601,13 @@ export const enTranslation = { aboutVersion: 'Version', aboutBuiltWith: 'Built with Tauri · React · TypeScript · Rust/rodio', aboutAiCredit: 'Developed with the support of Claude Code by Anthropic', + aboutReleaseNotesLabel: 'Release notes', + aboutReleaseNotesLink: "Open this version's what's-new", aboutContributorsLabel: 'Contributors', aboutSpecialThanksLabel: 'Special Thanks', changelog: 'Changelog', 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', 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).', @@ -741,6 +743,14 @@ export const enTranslation = { dontShowAgain: "Don't show again", 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: { title: 'Help', s1: 'Getting Started', diff --git a/src/locales/es.ts b/src/locales/es.ts index 779187d4..9fe6ab63 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -592,11 +592,13 @@ export const esTranslation = { aboutVersion: 'Versión', aboutBuiltWith: 'Construido con Tauri · React · TypeScript · Rust/rodio', 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', aboutSpecialThanksLabel: 'Agradecimientos Especiales', changelog: 'Registro de Cambios', 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', 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).', @@ -732,6 +734,14 @@ export const esTranslation = { dontShowAgain: 'No mostrar de nuevo', 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: { title: 'Ayuda', s1: 'Primeros Pasos', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 5f271128..519c0139 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -587,11 +587,13 @@ export const frTranslation = { aboutVersion: 'Version', aboutBuiltWith: 'Construit avec Tauri · React · TypeScript · Rust/rodio', 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', aboutSpecialThanksLabel: 'Remerciements spéciaux', changelog: 'Journal des modifications', 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', 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).', @@ -727,6 +729,14 @@ export const frTranslation = { dontShowAgain: 'Ne plus afficher', 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: { title: 'Aide', s1: 'Démarrage', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 78f46604..a2218e33 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -586,11 +586,13 @@ export const nbTranslation = { aboutVersion: 'Versjon', aboutBuiltWith: 'Bygget med Tauri · React · TypeScript · Rust/rodio', aboutAiCredit: 'Utviklet med støtte fra Claude Code laget av Anthropic', + aboutReleaseNotesLabel: 'Versjonsnotater', + aboutReleaseNotesLink: 'Åpne nyhetene for denne versjonen', aboutContributorsLabel: 'Bidragsytere', aboutSpecialThanksLabel: 'Spesiell takk', changelog: 'Endringslogg', 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', 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å).', @@ -726,6 +728,14 @@ export const nbTranslation = { dontShowAgain: "Ikke vis igjen", 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: { title: 'Hjelp', s1: 'Komme i gang', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 3545c95c..0e204a4b 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -586,11 +586,13 @@ export const nlTranslation = { aboutVersion: 'Versie', aboutBuiltWith: 'Gebouwd met Tauri · React · TypeScript · Rust/rodio', aboutAiCredit: 'Ontwikkeld met ondersteuning van Claude Code door Anthropic', + aboutReleaseNotesLabel: 'Release-notities', + aboutReleaseNotesLink: 'Nieuws van deze versie openen', aboutContributorsLabel: 'Bijdragers', aboutSpecialThanksLabel: 'Speciale dank', changelog: 'Wijzigingslog', 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', 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).', @@ -726,6 +728,14 @@ export const nlTranslation = { dontShowAgain: 'Niet meer weergeven', 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: { title: 'Help', s1: 'Aan de slag', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index bddb7ed0..cceb4667 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -613,11 +613,13 @@ export const ruTranslation = { aboutVersion: 'Версия', aboutBuiltWith: 'Tauri · React · TypeScript · Rust/rodio', aboutAiCredit: 'При поддержке Claude Code (Anthropic)', + aboutReleaseNotesLabel: 'Примечания к выпуску', + aboutReleaseNotesLink: 'Открыть «Что нового» этой версии', aboutContributorsLabel: 'Участники', aboutSpecialThanksLabel: 'Особая благодарность', changelog: 'Список изменений', showChangelogOnUpdate: 'Показывать «Что нового» после обновления', - showChangelogOnUpdateDesc: 'Автоматически при первом запуске новой версии.', + showChangelogOnUpdateDesc: 'После обновления над «Сейчас играет» появится ненавязчивый баннер журнала изменений. Клик открывает заметки о выпуске, X скрывает.', randomMixTitle: 'Случайный микс', randomMixBlacklistTitle: 'Свои слова-фильтры', randomMixBlacklistDesc: @@ -756,6 +758,14 @@ export const ruTranslation = { dontShowAgain: 'Больше не показывать', close: 'Понятно', }, + whatsNew: { + title: 'Что нового', + empty: 'Для этой версии пока нет записи в журнале изменений.', + bannerTitle: 'Журнал изменений', + bannerCollapsed: 'Что нового в v{{version}}', + dismiss: 'Скрыть', + close: 'Закрыть', + }, help: { title: 'Справка', s1: 'С чего начать', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index dfc4c417..d1cc4064 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -582,11 +582,13 @@ export const zhTranslation = { aboutVersion: '版本', aboutBuiltWith: '使用 Tauri · React · TypeScript · Rust/rodio 构建', aboutAiCredit: '在 Anthropic 的 Claude Code 支持下开发', + aboutReleaseNotesLabel: '版本说明', + aboutReleaseNotesLink: '打开此版本的新功能', aboutContributorsLabel: '贡献者', aboutSpecialThanksLabel: '特别感谢', changelog: '更新日志', showChangelogOnUpdate: '更新时显示"新功能"', - showChangelogOnUpdateDesc: '新版本首次启动时自动显示更新内容。', + showChangelogOnUpdateDesc: '更新后在「正在播放」上方显示一个低调的更新日志横幅。点击打开发行说明,X 按钮关闭。', randomMixTitle: '随机混音', randomMixBlacklistTitle: '自定义过滤关键词', randomMixBlacklistDesc: '当任何关键词匹配流派、标题、专辑或艺术家时,歌曲将被排除(当上方复选框开启时生效)。', @@ -722,6 +724,14 @@ export const zhTranslation = { dontShowAgain: '不再显示', close: '知道了', }, + whatsNew: { + title: '新功能', + empty: '此版本暂无更新日志。', + bannerTitle: '更新日志', + bannerCollapsed: 'v{{version}} 新功能', + dismiss: '关闭', + close: '关闭', + }, help: { title: '帮助', s1: '入门指南', diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 3563ec5d..91f38586 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -2445,6 +2445,18 @@ export default function Settings() { AI {t('settings.aboutAiCredit')}
+
+ {t('settings.aboutReleaseNotesLabel')} + +
+
+ + +
+ {entry ? ( + renderChangelogBody(entry.body) + ) : ( +

{t('whatsNew.empty')}

+ )} +
+ + ); +} diff --git a/src/styles/components.css b/src/styles/components.css index 94c18f86..9fc0408f 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -1868,7 +1868,7 @@ } .track-size { - color: var(--ctp-overlay0); + color: var(--text-muted); } .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. Pausing them cuts that to near zero while hidden without touching the 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 { width: 100%; diff --git a/src/styles/theme.css b/src/styles/theme.css index da05384c..87971cf6 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -4584,9 +4584,10 @@ input[type="range"]:hover::-webkit-slider-thumb { --accent-glow: rgba(69, 255, 0, 0.45); --text-primary: #ffffff; --text-secondary: #dce8ff; - --text-muted: #b8d0f8; - /* lighter so readable on #3a62a5 */ - --border: #2a5090; + --text-muted: #e8f0ff; + /* lifted from #b8d0f8 — clears AA on Luna-blue bg-app and dark sidebar */ + --border: #071027; + /* darkened from #2a5090 to meet 3:1 UI threshold on all main surfaces */ --border-subtle: #4a72b8; --positive: #30dd00; --warning: #ffdd44; @@ -10780,11 +10781,13 @@ input[type="range"]:hover::-webkit-slider-thumb { --accent-glow: rgba(21, 101, 200, 0.40); --text-primary: #0d1d3c; --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-subtle: rgba(140, 190, 240, 0.30); --positive: #1a8020; - --warning: #c8980c; + --warning: #735a00; + /* deeper Vista gold — #c8980c was 2.37:1 on bg-app, now 5.91:1 */ --danger: #c02020; --radius-sm: 3px; --radius-md: 6px; @@ -11122,6 +11125,22 @@ input[type="range"]:hover::-webkit-slider-thumb { 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'] { diff --git a/src/utils/changelogMarkdown.tsx b/src/utils/changelogMarkdown.tsx new file mode 100644 index 00000000..c043c483 --- /dev/null +++ b/src/utils/changelogMarkdown.tsx @@ -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({part.slice(2, -2)}); + } else if (part.startsWith('*') && part.endsWith('*') && part.length > 2) { + tokens.push({part.slice(1, -1)}); + } else if (part.startsWith('`') && part.endsWith('`')) { + tokens.push({part.slice(1, -1)}); + } 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( + { e.preventDefault(); open(url).catch(() => {}); }} + className="whats-new-link" + > + {label} + + ); + 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(
    {bulletBuffer}
); + bulletBuffer = []; + }; + const flushQuote = () => { + if (quoteBuffer.length === 0) return; + out.push( +
+ {renderInlineMarkdown(quoteBuffer.join(' '), `q-${out.length}`)} +
+ ); + 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(
); + continue; + } + + if (line.startsWith('### ')) { + flushBullets(); flushQuote(); + out.push(

{renderInlineMarkdown(line.slice(4), `h3-${i}`)}

); + continue; + } + if (line.startsWith('#### ')) { + flushBullets(); flushQuote(); + out.push(

{renderInlineMarkdown(line.slice(5), `h4-${i}`)}

); + continue; + } + + if (line.startsWith('> ')) { + flushBullets(); + quoteBuffer.push(line.slice(2)); + continue; + } + + if (line.startsWith('- ') || line.startsWith('* ')) { + flushQuote(); + bulletBuffer.push( +
  • {renderInlineMarkdown(line.slice(2), `li-${i}`)}
  • + ); + continue; + } + + // Paragraph / plain line + flushBullets(); flushQuote(); + out.push(

    {renderInlineMarkdown(line, `p-${i}`)}

    ); + } + flushBullets(); flushQuote(); + return out; +}