From 6e3ca48d3fd5d9cc7730ddc6f31db7c9a82a6e44 Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Tue, 13 Jan 2026 23:28:06 +0700 Subject: [PATCH] v7.0.5 --- app.go | 52 +++ frontend/index.html | 33 +- frontend/package.json | 4 +- frontend/package.json.md5 | 2 +- frontend/pnpm-lock.yaml | 54 +-- frontend/src/App.tsx | 220 +++++------ frontend/src/components/SettingsPage.tsx | 454 +++++++++++------------ frontend/src/index.css | 35 +- frontend/src/lib/settings.ts | 99 ++++- 9 files changed, 552 insertions(+), 401 deletions(-) diff --git a/app.go b/app.go index 0e04fe6..bc738ca 100644 --- a/app.go +++ b/app.go @@ -1053,3 +1053,55 @@ func (a *App) SkipDownloadItem(itemID, filePath string) { func (a *App) GetPreviewURL(trackID string) (string, error) { return backend.GetPreviewURL(trackID) } + +func (a *App) GetConfigPath() (string, error) { + dir, err := backend.GetFFmpegDir() + if err != nil { + return "", err + } + return filepath.Join(dir, "config.json"), nil +} + +func (a *App) SaveSettings(settings map[string]interface{}) error { + configPath, err := a.GetConfigPath() + if err != nil { + return err + } + + dir := filepath.Dir(configPath) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + + data, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return err + } + + return os.WriteFile(configPath, data, 0644) +} + +func (a *App) LoadSettings() (map[string]interface{}, error) { + configPath, err := a.GetConfigPath() + if err != nil { + return nil, err + } + + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return nil, nil + } + + data, err := os.ReadFile(configPath) + if err != nil { + return nil, err + } + + var settings map[string]interface{} + if err := json.Unmarshal(data, &settings); err != nil { + return nil, err + } + + return settings, nil +} diff --git a/frontend/index.html b/frontend/index.html index a2bcfa8..8257e4a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,16 +1,21 @@ - - - - - - - - SpotiFLAC - - -
- - - + + + + + + + + + SpotiFLAC + + + +
+ + + + \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 694c143..ef63ef9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,7 +27,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", - "motion": "^12.26.1", + "motion": "^12.26.2", "next-themes": "^0.4.6", "react": "^19.2.3", "react-dom": "^19.2.3", @@ -37,7 +37,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.2", - "@types/node": "^25.0.7", + "@types/node": "^25.0.8", "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 6876de5..78dabe9 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -65caca63c4f7ac1740046770c7a945b0 \ No newline at end of file +68754ba75ba7fe058dd9ebf6593e2759 \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index be0ff33..d613ad9 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -43,7 +43,7 @@ importers: version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tailwindcss/vite': specifier: ^4.1.18 - version: 4.1.18(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 4.1.18(vite@7.3.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -54,8 +54,8 @@ importers: specifier: ^0.562.0 version: 0.562.0(react@19.2.3) motion: - specifier: ^12.26.1 - version: 12.26.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^12.26.2 + version: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -79,8 +79,8 @@ importers: specifier: ^9.39.2 version: 9.39.2 '@types/node': - specifier: ^25.0.7 - version: 25.0.7 + specifier: ^25.0.8 + version: 25.0.8 '@types/react': specifier: ^19.2.8 version: 19.2.8 @@ -89,7 +89,7 @@ importers: version: 19.2.3(@types/react@19.2.8) '@vitejs/plugin-react': specifier: ^5.1.2 - version: 5.1.2(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 5.1.2(vite@7.3.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2)) eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) @@ -116,7 +116,7 @@ importers: version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2) + version: 7.3.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2) packages: @@ -1259,8 +1259,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@25.0.7': - resolution: {integrity: sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w==} + '@types/node@25.0.8': + resolution: {integrity: sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} @@ -1540,8 +1540,8 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - framer-motion@12.26.1: - resolution: {integrity: sha512-Uzc8wGldU4FpmGotthjjcj0SZhigcODjqvKT7lzVZHsmYkzQMFfMIv0vHQoXCeoe/Ahxqp4by4A6QbzFA/lblw==} + framer-motion@12.26.2: + resolution: {integrity: sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -1751,14 +1751,14 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - motion-dom@12.24.11: - resolution: {integrity: sha512-DlWOmsXMJrV8lzZyd+LKjG2CXULUs++bkq8GZ2Sr0R0RRhs30K2wtY+LKiTjhmJU3W61HK+rB0GLz6XmPvTA1A==} + motion-dom@12.26.2: + resolution: {integrity: sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw==} motion-utils@12.24.10: resolution: {integrity: sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==} - motion@12.26.1: - resolution: {integrity: sha512-IVhzx9HOQTiJ9ykthMOlZPnLwrkXziN5Q/yebsqBYlFJb2rHP8yhmKc8O/YUT9byPJlxOeqkzfNYCrVKZx8vqg==} + motion@12.26.2: + resolution: {integrity: sha512-2Q6g0zK1gUJKhGT742DAe42LgietcdiJ3L3OcYAHCQaC1UkLnn6aC8S/obe4CxYTLAgid2asS1QdQ/blYfo5dw==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -3016,12 +3016,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2))': + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 4.1.18 tailwindcss: 4.1.18 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2) '@types/babel__core@7.20.5': dependencies: @@ -3048,7 +3048,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@25.0.7': + '@types/node@25.0.8': dependencies: undici-types: 7.16.0 @@ -3151,7 +3151,7 @@ snapshots: '@typescript-eslint/types': 8.53.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2))': + '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) @@ -3159,7 +3159,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.53 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2) transitivePeerDependencies: - supports-color @@ -3399,9 +3399,9 @@ snapshots: flatted@3.3.3: {} - framer-motion@12.26.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - motion-dom: 12.24.11 + motion-dom: 12.26.2 motion-utils: 12.24.10 tslib: 2.8.1 optionalDependencies: @@ -3554,15 +3554,15 @@ snapshots: dependencies: brace-expansion: 2.0.2 - motion-dom@12.24.11: + motion-dom@12.26.2: dependencies: motion-utils: 12.24.10 motion-utils@12.24.10: {} - motion@12.26.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - framer-motion: 12.26.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tslib: 2.8.1 optionalDependencies: react: 19.2.3 @@ -3808,7 +3808,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.8 - vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2): + vite@7.3.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -3817,7 +3817,7 @@ snapshots: rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 25.0.8 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 818a75b..c1175a6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,7 +5,7 @@ import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Search, X, ArrowUp } from "lucide-react"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { getSettings, getSettingsWithDefaults, saveSettings, applyThemeMode, applyFont } from "@/lib/settings"; +import { getSettings, getSettingsWithDefaults, loadSettings, saveSettings, applyThemeMode, applyFont } from "@/lib/settings"; import { applyTheme } from "@/lib/themes"; import { OpenFolder } from "../wailsjs/go/main/App"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; @@ -59,13 +59,13 @@ function App() { const downloadQueue = useDownloadQueueDialog(); useEffect(() => { const initSettings = async () => { - const settings = getSettings(); + const settings = await loadSettings(); applyThemeMode(settings.themeMode); applyTheme(settings.theme); applyFont(settings.fontFamily); if (!settings.downloadPath) { const settingsWithDefaults = await getSettingsWithDefaults(); - saveSettings(settingsWithDefaults); + await saveSettings(settingsWithDefaults); } }; initSettings(); @@ -339,79 +339,79 @@ function App() { return ; default: return (<> -
+
- - - -
- -
- Fetch Artist - - Set timeout for fetching metadata. Longer timeout is recommended for artists - with large discography. - - {metadata.pendingArtistName && (
-

{metadata.pendingArtistName}

-
)} -
-
- - metadata.setTimeoutValue(Number(e.target.value))}/> -

- Default: 60 seconds. For large discographies, try 300-600 seconds (5-10 - minutes). -

-
-
- - - - -
-
- - - -
- -
- Fetch Album - - Do you want to fetch metadata for this album? - - {metadata.selectedAlbum && (
-

{metadata.selectedAlbum.name}

-
)} - - - + + Fetch Artist + + Set timeout for fetching metadata. Longer timeout is recommended for artists + with large discography. + + {metadata.pendingArtistName && (
+

{metadata.pendingArtistName}

+
)} +
+
+ + metadata.setTimeoutValue(Number(e.target.value))}/> +

+ Default: 60 seconds. For large discographies, try 300-600 seconds (5-10 + minutes). +

+
+
+ + + + +
+
+ + + + +
+ +
+ Fetch Album + + Do you want to fetch metadata for this album? + + {metadata.selectedAlbum && (
+

{metadata.selectedAlbum.name}

+
)} + + + - -
-
+ + Fetch Album + + + + - { + { setSpotifyUrl(url); const updatedUrl = await metadata.handleFetchMetadata(url); if (updatedUrl) { @@ -419,53 +419,53 @@ function App() { } }} history={fetchHistory} onHistorySelect={handleHistorySelect} onHistoryRemove={removeFromHistory} hasResult={!!metadata.metadata} searchMode={isSearchMode} onSearchModeChange={setIsSearchMode}/> - {!isSearchMode && metadata.metadata && renderMetadata()} - ); + {!isSearchMode && metadata.metadata && renderMetadata()} + ); } }; return ( -
- - +
+ + - -
-
- {renderPage()} -
+ +
+
+ {renderPage()} +
+
+ + + + + + + + + {showScrollTop && ()} + + + + + + Unsaved Changes + + You have unsaved changes in Settings. Are you sure you want to leave? Your changes will be lost. + + + + + + + +
- - - - - - - - - {showScrollTop && ()} - - - - - - Unsaved Changes - - You have unsaved changes in Settings. Are you sure you want to leave? Your changes will be lost. - - - - - - - - -
); } export default App; diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx index 02af7c0..8fba270 100644 --- a/frontend/src/components/SettingsPage.tsx +++ b/frontend/src/components/SettingsPage.tsx @@ -13,17 +13,17 @@ import { themes, applyTheme } from "@/lib/themes"; import { SelectFolder } from "../../wailsjs/go/main/App"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; const TidalIcon = () => ( - - - ); + + +); const QobuzIcon = () => ( - - - ); + + +); const AmazonIcon = () => ( - - - ); + + +); interface SettingsPageProps { onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void; onResetRequest?: (resetFn: () => void) => void; @@ -76,13 +76,13 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting const settingsWithDefaults = await getSettingsWithDefaults(); setSavedSettings(settingsWithDefaults); setTempSettings(settingsWithDefaults); - saveSettings(settingsWithDefaults); + await saveSettings(settingsWithDefaults); } }; loadDefaults(); }, []); - const handleSave = () => { - saveSettings(tempSettings); + const handleSave = async () => { + await saveSettings(tempSettings); setSavedSettings(tempSettings); toast.success("Settings saved"); onUnsavedChangesChange?.(false); @@ -110,165 +110,165 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting } }; return (
-

Settings

+

Settings

-
- -
- -
- -
- setTempSettings((prev) => ({ ...prev, downloadPath: e.target.value }))} placeholder="C:\Users\YourUsername\Music"/> - -
-
+
- -
- - -
+
- -
- - -
- - -
- - -
- - -
- - setTempSettings(prev => ({ ...prev, sfxEnabled: checked }))}/> +
+ +
+ setTempSettings((prev) => ({ ...prev, downloadPath: e.target.value }))} placeholder="C:\Users\YourUsername\Music"/> +
- -
- -
- -
- - - {tempSettings.downloader === "tidal" && ()} - - {tempSettings.downloader === "qobuz" && ()} - - {tempSettings.downloader === "amazon" && ()} -
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + setTempSettings(prev => ({ ...prev, sfxEnabled: checked }))}/> +
+
+ + +
+ +
+ +
+ + + {tempSettings.downloader === "tidal" && ()} + + {tempSettings.downloader === "qobuz" && ()} + + {tempSettings.downloader === "amazon" && ()}
+
- -
-
- - setTempSettings(prev => ({ ...prev, embedLyrics: checked }))}/> -
-
- - setTempSettings(prev => ({ ...prev, embedMaxQualityCover: checked }))}/> -
+ +
+
+ + setTempSettings(prev => ({ ...prev, embedLyrics: checked }))}/>
+
+ + setTempSettings(prev => ({ ...prev, embedMaxQualityCover: checked }))}/> +
+
-
+
- -
-
- - - - - - -

Variables: {TEMPLATE_VARIABLES.map(v => v.key).join(", ")}

-
-
-
-
- { const preset = FOLDER_PRESETS[value]; setTempSettings(prev => ({ ...prev, @@ -276,37 +276,37 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting folderTemplate: value === "custom" ? (prev.folderTemplate || preset.template) : preset.template })); }}> - - - - - {Object.entries(FOLDER_PRESETS).map(([key, { label }]) => ({label}))} - - - {tempSettings.folderPreset === "custom" && ( setTempSettings(prev => ({ ...prev, folderTemplate: e.target.value }))} placeholder="{artist}/{album}" className="h-9 text-sm flex-1"/>)} -
- {tempSettings.folderTemplate && (

- Preview: {tempSettings.folderTemplate.replace(/\{artist\}/g, "Kendrick Lamar, SZA").replace(/\{album\}/g, "Black Panther").replace(/\{album_artist\}/g, "Kendrick Lamar").replace(/\{year\}/g, "2018")}/ -

)} + + + + + {Object.entries(FOLDER_PRESETS).map(([key, { label }]) => ({label}))} + + + {tempSettings.folderPreset === "custom" && ( setTempSettings(prev => ({ ...prev, folderTemplate: e.target.value }))} placeholder="{artist}/{album}" className="h-9 text-sm flex-1"/>)}
+ {tempSettings.folderTemplate && (

+ Preview: {tempSettings.folderTemplate.replace(/\{artist\}/g, "Kendrick Lamar, SZA").replace(/\{album\}/g, "Black Panther").replace(/\{album_artist\}/g, "Kendrick Lamar").replace(/\{year\}/g, "2018")}/ +

)} +
-
+
- -
-
- - - - - - -

Variables: {TEMPLATE_VARIABLES.map(v => v.key).join(", ")}

-
-
-
-
- { const preset = FILENAME_PRESETS[value]; setTempSettings(prev => ({ ...prev, @@ -314,48 +314,48 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting filenameTemplate: value === "custom" ? (prev.filenameTemplate || preset.template) : preset.template })); }}> - - - - - {Object.entries(FILENAME_PRESETS).map(([key, { label }]) => ({label}))} - - - {tempSettings.filenamePreset === "custom" && ( setTempSettings(prev => ({ ...prev, filenameTemplate: e.target.value }))} placeholder="{track}. {title}" className="h-9 text-sm flex-1"/>)} -
- {tempSettings.filenameTemplate && (

- Preview: {tempSettings.filenameTemplate.replace(/\{artist\}/g, "Kendrick Lamar, SZA").replace(/\{album_artist\}/g, "Kendrick Lamar").replace(/\{title\}/g, "All The Stars").replace(/\{track\}/g, "01").replace(/\{disc\}/g, "1").replace(/\{year\}/g, "2018")}.flac -

)} + + + + + {Object.entries(FILENAME_PRESETS).map(([key, { label }]) => ({label}))} + + + {tempSettings.filenamePreset === "custom" && ( setTempSettings(prev => ({ ...prev, filenameTemplate: e.target.value }))} placeholder="{track}. {title}" className="h-9 text-sm flex-1"/>)}
+ {tempSettings.filenameTemplate && (

+ Preview: {tempSettings.filenameTemplate.replace(/\{artist\}/g, "Kendrick Lamar, SZA").replace(/\{album_artist\}/g, "Kendrick Lamar").replace(/\{title\}/g, "All The Stars").replace(/\{track\}/g, "01").replace(/\{disc\}/g, "1").replace(/\{year\}/g, "2018")}.flac +

)}
+
- -
- - -
- - - - - Reset to Default? - - This will reset all settings to their default values. Your custom configurations will be lost. - - - - - - - - -
); +
+ + +
+ + + + + + Reset to Default? + + This will reset all settings to their default values. Your custom configurations will be lost. + + + + + + + + +
); } diff --git a/frontend/src/index.css b/frontend/src/index.css index 286a861..4698284 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -26,6 +26,7 @@ --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); + --font-sans: "Bricolage Grotesque", "Google Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } :root { @@ -75,11 +76,15 @@ * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground; - font-family: "Google Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: var(--font-sans); } - code, pre, .font-mono { + + code, + pre, + .font-mono { font-family: "Google Sans Code", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; } } @@ -134,43 +139,51 @@ /* Specific color for each toast type - match icon color */ [data-sonner-toast][data-type="success"] [data-description], [data-sonner-toast][data-type="success"] [data-description] * { - color: rgb(22 163 74) !important; /* green-600 - same as icon */ + color: rgb(22 163 74) !important; + /* green-600 - same as icon */ } [data-sonner-toast][data-type="error"] [data-description], [data-sonner-toast][data-type="error"] [data-description] * { - color: rgb(220 38 38) !important; /* red-600 - same as icon */ + color: rgb(220 38 38) !important; + /* red-600 - same as icon */ } [data-sonner-toast][data-type="warning"] [data-description], [data-sonner-toast][data-type="warning"] [data-description] * { - color: rgb(202 138 4) !important; /* yellow-600 - same as icon */ + color: rgb(202 138 4) !important; + /* yellow-600 - same as icon */ } [data-sonner-toast][data-type="info"] [data-description], [data-sonner-toast][data-type="info"] [data-description] * { - color: rgb(37 99 235) !important; /* blue-600 - same as icon */ + color: rgb(37 99 235) !important; + /* blue-600 - same as icon */ } /* Dark mode - use same icon colors */ .dark [data-sonner-toast][data-type="success"] [data-description], .dark [data-sonner-toast][data-type="success"] [data-description] * { - color: rgb(22 163 74) !important; /* green-600 */ + color: rgb(22 163 74) !important; + /* green-600 */ } .dark [data-sonner-toast][data-type="error"] [data-description], .dark [data-sonner-toast][data-type="error"] [data-description] * { - color: rgb(220 38 38) !important; /* red-600 */ + color: rgb(220 38 38) !important; + /* red-600 */ } .dark [data-sonner-toast][data-type="warning"] [data-description], .dark [data-sonner-toast][data-type="warning"] [data-description] * { - color: rgb(202 138 4) !important; /* yellow-600 */ + color: rgb(202 138 4) !important; + /* yellow-600 */ } .dark [data-sonner-toast][data-type="info"] [data-description], .dark [data-sonner-toast][data-type="info"] [data-description] * { - color: rgb(37 99 235) !important; /* blue-600 */ + color: rgb(37 99 235) !important; + /* blue-600 */ } /* Dark mode toast styling */ @@ -252,4 +265,4 @@ .custom-scrollbar::-webkit-scrollbar-thumb:hover { filter: brightness(1.2); -} +} \ No newline at end of file diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts index f1e69df..ffe0a89 100644 --- a/frontend/src/lib/settings.ts +++ b/frontend/src/lib/settings.ts @@ -1,5 +1,5 @@ -import { GetDefaults } from "../../wailsjs/go/main/App"; -export type FontFamily = "google-sans" | "inter" | "poppins" | "roboto" | "dm-sans" | "plus-jakarta-sans" | "manrope" | "space-grotesk" | "noto-sans" | "nunito-sans" | "figtree" | "raleway" | "public-sans" | "outfit" | "jetbrains-mono" | "geist-sans"; +import { GetDefaults, LoadSettings, SaveSettings as SaveToBackend } from "../../wailsjs/go/main/App"; +export type FontFamily = "google-sans" | "inter" | "poppins" | "roboto" | "dm-sans" | "plus-jakarta-sans" | "manrope" | "space-grotesk" | "noto-sans" | "nunito-sans" | "figtree" | "raleway" | "public-sans" | "outfit" | "jetbrains-mono" | "geist-sans" | "bricolage-grotesque"; export type FolderPreset = "none" | "artist" | "album" | "year-album" | "year-artist-album" | "artist-album" | "artist-year-album" | "artist-year-nested-album" | "album-artist" | "album-artist-album" | "album-artist-year-album" | "album-artist-year-nested-album" | "year" | "year-artist" | "custom"; export type FilenamePreset = "title" | "title-artist" | "artist-title" | "track-title" | "track-title-artist" | "track-artist-title" | "title-album-artist" | "track-title-album-artist" | "artist-album-title" | "track-dash-title" | "disc-track-title" | "disc-track-title-artist" | "custom"; export interface Settings { @@ -102,6 +102,7 @@ export const FONT_OPTIONS: { label: string; fontFamily: string; }[] = [ + { value: "bricolage-grotesque", label: "Bricolage Grotesque", fontFamily: '"Bricolage Grotesque", system-ui, sans-serif' }, { value: "dm-sans", label: "DM Sans", fontFamily: '"DM Sans", system-ui, sans-serif' }, { value: "figtree", label: "Figtree", fontFamily: '"Figtree", system-ui, sans-serif' }, { value: "geist-sans", label: "Geist Sans", fontFamily: '"Geist", system-ui, sans-serif' }, @@ -137,7 +138,8 @@ async function fetchDefaultPath(): Promise { } } const SETTINGS_KEY = "spotiflac-settings"; -export function getSettings(): Settings { +let cachedSettings: Settings | null = null; +function getSettingsFromLocalStorage(): Settings { try { const stored = localStorage.getItem(SETTINGS_KEY); if (stored) { @@ -195,10 +197,86 @@ export function getSettings(): Settings { } } catch (error) { - console.error("Failed to load settings:", error); + console.error("Failed to load settings from local storage:", error); } return DEFAULT_SETTINGS; } +export function getSettings(): Settings { + if (cachedSettings) + return cachedSettings; + return getSettingsFromLocalStorage(); +} +export async function loadSettings(): Promise { + try { + const backendSettings = await LoadSettings(); + if (backendSettings) { + const parsed = backendSettings as any; + if ('darkMode' in parsed && !('themeMode' in parsed)) { + parsed.themeMode = parsed.darkMode ? 'dark' : 'light'; + delete parsed.darkMode; + } + if (!('folderPreset' in parsed) && ('artistSubfolder' in parsed || 'albumSubfolder' in parsed)) { + const hasArtist = parsed.artistSubfolder; + const hasAlbum = parsed.albumSubfolder; + if (hasArtist && hasAlbum) { + parsed.folderPreset = "artist-album"; + parsed.folderTemplate = "{artist}/{album}"; + } + else if (hasArtist) { + parsed.folderPreset = "artist"; + parsed.folderTemplate = "{artist}"; + } + else if (hasAlbum) { + parsed.folderPreset = "album"; + parsed.folderTemplate = "{album}"; + } + else { + parsed.folderPreset = "none"; + parsed.folderTemplate = ""; + } + } + if (!('filenamePreset' in parsed) && 'filenameFormat' in parsed) { + const format = parsed.filenameFormat; + if (format === "title-artist") { + parsed.filenamePreset = "artist-title"; + parsed.filenameTemplate = "{artist} - {title}"; + } + else if (format === "artist-title") { + parsed.filenamePreset = "artist-title"; + parsed.filenameTemplate = "{artist} - {title}"; + } + else { + parsed.filenamePreset = "title"; + parsed.filenameTemplate = "{title}"; + } + } + parsed.operatingSystem = detectOS(); + if (!('tidalQuality' in parsed)) { + parsed.tidalQuality = "LOSSLESS"; + } + if (!('qobuzQuality' in parsed)) { + parsed.qobuzQuality = "6"; + } + if (!('amazonQuality' in parsed)) { + parsed.amazonQuality = "HI_RES"; + } + cachedSettings = { ...DEFAULT_SETTINGS, ...parsed }; + return cachedSettings!; + } + } + catch (error) { + console.error("Failed to load settings from backend:", error); + } + const local = getSettingsFromLocalStorage(); + try { + await SaveToBackend(local as any); + cachedSettings = local; + } + catch (error) { + console.error("Failed to migrate settings to backend:", error); + } + return local; +} export interface TemplateData { artist?: string; album?: string; @@ -224,30 +302,33 @@ export function parseTemplate(template: string, data: TemplateData): string { return result; } export async function getSettingsWithDefaults(): Promise { - const settings = getSettings(); + const settings = await loadSettings(); if (!settings.downloadPath) { settings.downloadPath = await fetchDefaultPath(); + await saveSettings(settings); } return settings; } -export function saveSettings(settings: Settings): void { +export async function saveSettings(settings: Settings): Promise { try { + cachedSettings = settings; localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); + await SaveToBackend(settings as any); } catch (error) { console.error("Failed to save settings:", error); } } -export function updateSettings(partial: Partial): Settings { +export async function updateSettings(partial: Partial): Promise { const current = getSettings(); const updated = { ...current, ...partial }; - saveSettings(updated); + await saveSettings(updated); return updated; } export async function resetToDefaultSettings(): Promise { const defaultPath = await fetchDefaultPath(); const defaultSettings = { ...DEFAULT_SETTINGS, downloadPath: defaultPath }; - saveSettings(defaultSettings); + await saveSettings(defaultSettings); return defaultSettings; } export function applyThemeMode(mode: "auto" | "light" | "dark"): void {