diff --git a/src/components/settings/AppearanceTab.tsx b/src/components/settings/AppearanceTab.tsx new file mode 100644 index 00000000..56997da8 --- /dev/null +++ b/src/components/settings/AppearanceTab.tsx @@ -0,0 +1,384 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { invoke } from '@tauri-apps/api/core'; +import { Clock, Maximize2, Palette, Sliders, Type, ZoomIn } from 'lucide-react'; +import { useAuthStore } from '../../store/authStore'; +import type { SeekbarStyle } from '../../store/authStoreTypes'; +import { useFontStore, FontId } from '../../store/fontStore'; +import { useThemeStore } from '../../store/themeStore'; +import { IS_LINUX, IS_WINDOWS } from '../../utils/platform'; +import CustomSelect from '../CustomSelect'; +import SettingsSubSection from '../SettingsSubSection'; +import ThemePicker, { THEME_GROUPS } from '../ThemePicker'; +import { SeekbarPreview } from '../WaveformSeek'; + +export function AppearanceTab() { + const { t, i18n } = useTranslation(); + const auth = useAuthStore(); + const theme = useThemeStore(); + const fontStore = useFontStore(); + const [isTilingWm, setIsTilingWm] = useState(false); + + useEffect(() => { + if (!IS_LINUX) return; + invoke('is_tiling_wm_cmd').then(setIsTilingWm).catch(() => {}); + }, []); + + return ( + <> + } + > +
+ {theme.enableThemeScheduler && ( +
+ {t('settings.themeSchedulerActiveHint')} +
+ )} + theme.setTheme(v as any)} /> +
+
+ + } + > +
+
+
+
{t('settings.themeSchedulerEnable')}
+
{t('settings.themeSchedulerEnableSub')}
+
+ +
+ {theme.enableThemeScheduler && (() => { + const themeOptions = THEME_GROUPS.flatMap(g => + g.themes.map(th => ({ + value: th.id, + label: th.family ? `${th.family} ${th.label}` : th.label, + group: g.group, + })) + ); + const use12h = i18n.language === 'en'; + const hourOptions = Array.from({ length: 24 }, (_, i) => { + const value = String(i).padStart(2, '0'); + const label = use12h + ? `${i % 12 === 0 ? 12 : i % 12} ${i < 12 ? 'AM' : 'PM'}` + : value; + return { value, label }; + }); + const minuteOptions = ['00', '05', '10', '15', '20', '25', '30', '35', '40', '45', '50', '55'].map(m => ({ value: m, label: m })); + const dayH = theme.timeDayStart.split(':')[0]; + const dayM = theme.timeDayStart.split(':')[1]; + const nightH = theme.timeNightStart.split(':')[0]; + const nightM = theme.timeNightStart.split(':')[1]; + return ( +
+
+ + +
+
+ +
+ theme.setTimeDayStart(`${v}:${dayM}`)} options={hourOptions} /> + : + theme.setTimeDayStart(`${dayH}:${v}`)} options={minuteOptions} /> +
+
+
+ + +
+
+ +
+ theme.setTimeNightStart(`${v}:${nightM}`)} options={hourOptions} /> + : + theme.setTimeNightStart(`${nightH}:${v}`)} options={minuteOptions} /> +
+
+
+ ); + })()} +
+
+ + } + > +
+
+
+
{t('settings.coverArtBackground')}
+
{t('settings.coverArtBackgroundSub')}
+
+ +
+
+
+
+
{t('settings.playlistCoverPhoto')}
+
{t('settings.playlistCoverPhotoSub')}
+
+ +
+
+
+
+
{t('settings.showBitrate')}
+
{t('settings.showBitrateSub')}
+
+ +
+
+
+
+
{t('settings.floatingPlayerBar')}
+
{t('settings.floatingPlayerBarSub')}
+
+ +
+
+
+
+
{t('settings.showArtistImages')}
+
{t('settings.showArtistImagesDesc')}
+
+ +
+
+
+
+
{t('settings.showOrbitTrigger')}
+
{t('settings.showOrbitTriggerDesc')}
+
+ +
+ {!IS_WINDOWS && ( + <> +
+
+
+
{t('settings.preloadMiniPlayer')}
+
{t('settings.preloadMiniPlayerDesc')}
+
+ +
+ + )} + {IS_LINUX && !isTilingWm && ( + <> +
+
+
+
{t('settings.useCustomTitlebar')}
+
{t('settings.useCustomTitlebarDesc')}
+
+ +
+ + )} +
+ + + } + > +
+
+
+ {t('settings.uiScaleLabel')} + + {Math.round(fontStore.uiScale * 100)}% + +
+ {(() => { + const presets = [80, 90, 100, 110, 125, 150]; + const currentPct = Math.round(fontStore.uiScale * 100); + let idx = presets.indexOf(currentPct); + if (idx < 0) { + // Snap legacy off-preset values to the closest preset. + idx = presets.reduce((best, p, i) => + Math.abs(p - currentPct) < Math.abs(presets[best] - currentPct) ? i : best, 0); + } + return ( + <> + fontStore.setUiScale(presets[parseInt(e.target.value, 10)] / 100)} + className="ui-scale-slider" + /> +
+ {presets.map(p => { + const active = currentPct === p; + return ( + + ); + })} +
+ + ); + })()} +
+
+
+ + } + > +
+
+ {( + [ + // Accessibility-first: OpenDyslexic at the top so dyslexic + // readers don't have to scroll past 14 sans-serifs to find it. + { id: 'opendyslexic', label: 'OpenDyslexic', stack: "'OpenDyslexic', sans-serif", hint: t('settings.fontHintOpenDyslexic') }, + { id: 'inter', label: 'Inter', stack: "'Inter Variable', sans-serif" }, + { id: 'outfit', label: 'Outfit', stack: "'Outfit Variable', sans-serif" }, + { id: 'dm-sans', label: 'DM Sans', stack: "'DM Sans Variable', sans-serif" }, + { id: 'nunito', label: 'Nunito', stack: "'Nunito Variable', sans-serif" }, + { id: 'rubik', label: 'Rubik', stack: "'Rubik Variable', sans-serif" }, + { id: 'space-grotesk', label: 'Space Grotesk', stack: "'Space Grotesk Variable', sans-serif" }, + { id: 'figtree', label: 'Figtree', stack: "'Figtree Variable', sans-serif" }, + { id: 'manrope', label: 'Manrope', stack: "'Manrope Variable', sans-serif" }, + { id: 'plus-jakarta-sans', label: 'Plus Jakarta Sans', stack: "'Plus Jakarta Sans Variable', sans-serif" }, + { id: 'lexend', label: 'Lexend', stack: "'Lexend Variable', sans-serif" }, + { id: 'geist', label: 'Geist', stack: "'Geist Variable', sans-serif" }, + { id: 'jetbrains-mono', label: 'JetBrains Mono', stack: "'JetBrains Mono Variable', monospace" }, + { id: 'golos-text', label: 'Golos Text', stack: "'Golos Text Variable', sans-serif" }, + { id: 'unbounded', label: 'Unbounded', stack: "'Unbounded Variable', sans-serif" }, + ] as { id: FontId; label: string; stack: string; hint?: string }[] + ).map(f => ( + + ))} +
+
+
+ + } + > +
+
+
+
{t('settings.fsShowArtistPortrait')}
+
{t('settings.fsShowArtistPortraitDesc')}
+
+ +
+ {auth.showFsArtistPortrait && ( +
+
+ {t('settings.fsPortraitDim')} + {auth.fsPortraitDim}% +
+ auth.setFsPortraitDim(parseInt(e.target.value, 10))} + className="ui-scale-slider" + /> +
+ )} +
+
+ + } + > +
+
+ {t('settings.seekbarStyleDesc')} +
+
+ {(['truewave', 'pseudowave', 'linedot', 'bar', 'thick', 'segmented', 'neon', 'pulsewave', 'particletrail', 'liquidfill', 'retrotape'] as SeekbarStyle[]).map(style => ( + auth.setSeekbarStyle(style)} + /> + ))} +
+
+
+ + ); +} diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 749f26bc..246af3f5 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -1,11 +1,11 @@ -import type { ServerProfile, SeekbarStyle, LoggingMode, LoudnessLufsPreset } from '../store/authStoreTypes'; +import type { ServerProfile, LoggingMode, LoudnessLufsPreset } from '../store/authStoreTypes'; import React, { useState, useMemo, useCallback, useEffect, useLayoutEffect, useRef } from 'react'; import { version as appVersion } from '../../package.json'; import { useNavigate, useLocation } from 'react-router-dom'; import { Wifi, WifiOff, Globe, Music2, Sliders, LogOut, CheckCircle2, FolderOpen, - Palette, Server, Plus, Trash2, Eye, EyeOff, Info, ExternalLink, X, Type, Keyboard, ChevronDown, - RotateCcw, LayoutGrid, AppWindow, HardDrive, Download, Clock, ZoomIn, Sparkles, AlertTriangle, Maximize2, AudioLines, User, Lock, + Palette, Server, Plus, Trash2, Eye, EyeOff, Info, ExternalLink, X, Keyboard, ChevronDown, + RotateCcw, LayoutGrid, AppWindow, HardDrive, Download, Sparkles, AlertTriangle, AudioLines, User, Lock, Users, Search, Scale } from 'lucide-react'; import i18n from '../i18n'; @@ -20,14 +20,11 @@ import CustomSelect from '../components/CustomSelect'; import SettingsSubSection from '../components/SettingsSubSection'; import LicensesPanel from '../components/LicensesPanel'; import { AboutPsysonicBrandHeader } from '../components/AboutPsysonicLol'; -import ThemePicker, { THEME_GROUPS } from '../components/ThemePicker'; import { useAuthStore } from '../store/authStore'; -import { SeekbarPreview } from '../components/WaveformSeek'; import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform'; -import { useThemeStore } from '../store/themeStore'; -import { useFontStore, FontId } from '../store/fontStore'; import { useDragDrop } from '../contexts/DragDropContext'; import { AddServerForm } from '../components/settings/AddServerForm'; +import { AppearanceTab } from '../components/settings/AppearanceTab'; import { AudioTab } from '../components/settings/AudioTab'; import { BackupSection } from '../components/settings/BackupSection'; import { InputTab } from '../components/settings/InputTab'; @@ -54,18 +51,10 @@ const AUDIOMUSE_NV_PLUGIN_URL = 'https://github.com/NeptuneHub/AudioMuse-AI-NV-p export default function Settings() { const auth = useAuthStore(); - const theme = useThemeStore(); - const fontStore = useFontStore(); const serverId = auth.activeServerId ?? ''; const clearAllOffline = useOfflineStore(s => s.clearAll); const clearHotCacheDisk = useHotCacheStore(s => s.clearAllDisk); const hotCacheEntries = useHotCacheStore(s => s.entries); - const [isTilingWm, setIsTilingWm] = useState(false); - - useEffect(() => { - if (!IS_LINUX) return; - invoke('is_tiling_wm_cmd').then(setIsTilingWm).catch(() => {}); - }, []); const hotCacheTrackCount = useMemo(() => { if (!serverId) return 0; @@ -923,364 +912,7 @@ export default function Settings() { )} {/* ── Appearance ───────────────────────────────────────────────────────── */} - {activeTab === 'appearance' && ( - <> - } - > -
- {theme.enableThemeScheduler && ( -
- {t('settings.themeSchedulerActiveHint')} -
- )} - theme.setTheme(v as any)} /> -
-
- - } - > -
-
-
-
{t('settings.themeSchedulerEnable')}
-
{t('settings.themeSchedulerEnableSub')}
-
- -
- {theme.enableThemeScheduler && (() => { - const themeOptions = THEME_GROUPS.flatMap(g => - g.themes.map(th => ({ - value: th.id, - label: th.family ? `${th.family} ${th.label}` : th.label, - group: g.group, - })) - ); - const use12h = i18n.language === 'en'; - const hourOptions = Array.from({ length: 24 }, (_, i) => { - const value = String(i).padStart(2, '0'); - const label = use12h - ? `${i % 12 === 0 ? 12 : i % 12} ${i < 12 ? 'AM' : 'PM'}` - : value; - return { value, label }; - }); - const minuteOptions = ['00', '05', '10', '15', '20', '25', '30', '35', '40', '45', '50', '55'].map(m => ({ value: m, label: m })); - const dayH = theme.timeDayStart.split(':')[0]; - const dayM = theme.timeDayStart.split(':')[1]; - const nightH = theme.timeNightStart.split(':')[0]; - const nightM = theme.timeNightStart.split(':')[1]; - return ( -
-
- - -
-
- -
- theme.setTimeDayStart(`${v}:${dayM}`)} options={hourOptions} /> - : - theme.setTimeDayStart(`${dayH}:${v}`)} options={minuteOptions} /> -
-
-
- - -
-
- -
- theme.setTimeNightStart(`${v}:${nightM}`)} options={hourOptions} /> - : - theme.setTimeNightStart(`${nightH}:${v}`)} options={minuteOptions} /> -
-
-
- ); - })()} -
-
- - } - > -
-
-
-
{t('settings.coverArtBackground')}
-
{t('settings.coverArtBackgroundSub')}
-
- -
-
-
-
-
{t('settings.playlistCoverPhoto')}
-
{t('settings.playlistCoverPhotoSub')}
-
- -
-
-
-
-
{t('settings.showBitrate')}
-
{t('settings.showBitrateSub')}
-
- -
-
-
-
-
{t('settings.floatingPlayerBar')}
-
{t('settings.floatingPlayerBarSub')}
-
- -
-
-
-
-
{t('settings.showArtistImages')}
-
{t('settings.showArtistImagesDesc')}
-
- -
-
-
-
-
{t('settings.showOrbitTrigger')}
-
{t('settings.showOrbitTriggerDesc')}
-
- -
- {!IS_WINDOWS && ( - <> -
-
-
-
{t('settings.preloadMiniPlayer')}
-
{t('settings.preloadMiniPlayerDesc')}
-
- -
- - )} - {IS_LINUX && !isTilingWm && ( - <> -
-
-
-
{t('settings.useCustomTitlebar')}
-
{t('settings.useCustomTitlebarDesc')}
-
- -
- - )} -
- - - } - > -
-
-
- {t('settings.uiScaleLabel')} - - {Math.round(fontStore.uiScale * 100)}% - -
- {(() => { - const presets = [80, 90, 100, 110, 125, 150]; - const currentPct = Math.round(fontStore.uiScale * 100); - let idx = presets.indexOf(currentPct); - if (idx < 0) { - // Snap legacy off-preset values to the closest preset. - idx = presets.reduce((best, p, i) => - Math.abs(p - currentPct) < Math.abs(presets[best] - currentPct) ? i : best, 0); - } - return ( - <> - fontStore.setUiScale(presets[parseInt(e.target.value, 10)] / 100)} - className="ui-scale-slider" - /> -
- {presets.map(p => { - const active = currentPct === p; - return ( - - ); - })} -
- - ); - })()} -
-
-
- - } - > -
-
- {( - [ - // Accessibility-first: OpenDyslexic at the top so dyslexic - // readers don't have to scroll past 14 sans-serifs to find it. - { id: 'opendyslexic', label: 'OpenDyslexic', stack: "'OpenDyslexic', sans-serif", hint: t('settings.fontHintOpenDyslexic') }, - { id: 'inter', label: 'Inter', stack: "'Inter Variable', sans-serif" }, - { id: 'outfit', label: 'Outfit', stack: "'Outfit Variable', sans-serif" }, - { id: 'dm-sans', label: 'DM Sans', stack: "'DM Sans Variable', sans-serif" }, - { id: 'nunito', label: 'Nunito', stack: "'Nunito Variable', sans-serif" }, - { id: 'rubik', label: 'Rubik', stack: "'Rubik Variable', sans-serif" }, - { id: 'space-grotesk', label: 'Space Grotesk', stack: "'Space Grotesk Variable', sans-serif" }, - { id: 'figtree', label: 'Figtree', stack: "'Figtree Variable', sans-serif" }, - { id: 'manrope', label: 'Manrope', stack: "'Manrope Variable', sans-serif" }, - { id: 'plus-jakarta-sans', label: 'Plus Jakarta Sans', stack: "'Plus Jakarta Sans Variable', sans-serif" }, - { id: 'lexend', label: 'Lexend', stack: "'Lexend Variable', sans-serif" }, - { id: 'geist', label: 'Geist', stack: "'Geist Variable', sans-serif" }, - { id: 'jetbrains-mono', label: 'JetBrains Mono', stack: "'JetBrains Mono Variable', monospace" }, - { id: 'golos-text', label: 'Golos Text', stack: "'Golos Text Variable', sans-serif" }, - { id: 'unbounded', label: 'Unbounded', stack: "'Unbounded Variable', sans-serif" }, - ] as { id: FontId; label: string; stack: string; hint?: string }[] - ).map(f => ( - - ))} -
-
-
- - } - > -
-
-
-
{t('settings.fsShowArtistPortrait')}
-
{t('settings.fsShowArtistPortraitDesc')}
-
- -
- {auth.showFsArtistPortrait && ( -
-
- {t('settings.fsPortraitDim')} - {auth.fsPortraitDim}% -
- auth.setFsPortraitDim(parseInt(e.target.value, 10))} - className="ui-scale-slider" - /> -
- )} -
-
- - } - > -
-
- {t('settings.seekbarStyleDesc')} -
-
- {(['truewave', 'pseudowave', 'linedot', 'bar', 'thick', 'segmented', 'neon', 'pulsewave', 'particletrail', 'liquidfill', 'retrotape'] as SeekbarStyle[]).map(style => ( - auth.setSeekbarStyle(style)} - /> - ))} -
-
-
- - - )} + {activeTab === 'appearance' && } {/* ── Input ────────────────────────────────────────────────────────────── */} {activeTab === 'input' && }