From 66e3f0e572333b46f68e8a41edc1998b5a1f91c0 Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Mon, 13 Apr 2026 22:18:08 +0700 Subject: [PATCH] .improved recent fetches --- app.go | 28 ++++++++++++ backend/recent_fetches.go | 91 +++++++++++++++++++++++++++++++++++++++ frontend/src/App.tsx | 65 +++++++++++++++++++--------- 3 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 backend/recent_fetches.go diff --git a/app.go b/app.go index 835faf4..f019414 100644 --- a/app.go +++ b/app.go @@ -935,6 +935,34 @@ func (a *App) ClearFetchHistoryByType(itemType string) error { return backend.ClearFetchHistoryByType(itemType, "SpotiFLAC") } +func (a *App) GetRecentFetches() (string, error) { + items, err := backend.LoadRecentFetches() + if err != nil { + return "", err + } + + data, err := json.Marshal(items) + if err != nil { + return "", err + } + + return string(data), nil +} + +func (a *App) SaveRecentFetches(payload string) error { + payload = strings.TrimSpace(payload) + if payload == "" { + payload = "[]" + } + + var items []backend.RecentFetchItem + if err := json.Unmarshal([]byte(payload), &items); err != nil { + return err + } + + return backend.SaveRecentFetches(items) +} + func (a *App) SaveSpectrumImage(audioFilePath string, base64Data string) (string, error) { if audioFilePath == "" || base64Data == "" { return "", fmt.Errorf("file path and image data are required") diff --git a/backend/recent_fetches.go b/backend/recent_fetches.go new file mode 100644 index 0000000..34d30fd --- /dev/null +++ b/backend/recent_fetches.go @@ -0,0 +1,91 @@ +package backend + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "sync" +) + +const recentFetchesFileName = "recent_fetches.json" + +type RecentFetchItem struct { + ID string `json:"id"` + URL string `json:"url"` + Type string `json:"type"` + Name string `json:"name"` + Artist string `json:"artist"` + Image string `json:"image"` + Timestamp int64 `json:"timestamp"` +} + +var ( + recentFetchesMu sync.Mutex + recentFetchesDirResolver = GetFFmpegDir +) + +func recentFetchesFilePath() (string, error) { + baseDir, err := recentFetchesDirResolver() + if err != nil { + return "", err + } + if err := os.MkdirAll(baseDir, 0o755); err != nil { + return "", err + } + return filepath.Join(baseDir, recentFetchesFileName), nil +} + +func LoadRecentFetches() ([]RecentFetchItem, error) { + recentFetchesMu.Lock() + defer recentFetchesMu.Unlock() + + filePath, err := recentFetchesFilePath() + if err != nil { + return nil, err + } + + data, err := os.ReadFile(filePath) + if err != nil { + if os.IsNotExist(err) { + return []RecentFetchItem{}, nil + } + return nil, err + } + + if strings.TrimSpace(string(data)) == "" { + return []RecentFetchItem{}, nil + } + + var items []RecentFetchItem + if err := json.Unmarshal(data, &items); err != nil { + return nil, err + } + + if items == nil { + return []RecentFetchItem{}, nil + } + + return items, nil +} + +func SaveRecentFetches(items []RecentFetchItem) error { + recentFetchesMu.Lock() + defer recentFetchesMu.Unlock() + + filePath, err := recentFetchesFilePath() + if err != nil { + return err + } + + if items == nil { + items = []RecentFetchItem{} + } + + data, err := json.MarshalIndent(items, "", " ") + if err != nil { + return err + } + + return os.WriteFile(filePath, data, 0o644) +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2d5410f..0e9608d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,7 +5,7 @@ import { Search, X, ArrowUp } from "lucide-react"; import { TooltipProvider } from "@/components/ui/tooltip"; import { getSettings, getSettingsWithDefaults, loadSettings, saveSettings, applyThemeMode, applyFont, updateSettings } from "@/lib/settings"; import { applyTheme } from "@/lib/themes"; -import { OpenFolder, CheckFFmpegInstalled, DownloadFFmpeg, GetBrewPath, InstallFFmpegWithBrew } from "../wailsjs/go/main/App"; +import { OpenFolder, CheckFFmpegInstalled, DownloadFFmpeg, GetBrewPath, GetRecentFetches, InstallFFmpegWithBrew, SaveRecentFetches } from "../wailsjs/go/main/App"; import { EventsOn, EventsOff, Quit } from "../wailsjs/runtime/runtime"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; import { TitleBar } from "@/components/TitleBar"; @@ -104,6 +104,25 @@ function dedupeHistoryItems(items: HistoryItem[]): HistoryItem[] { } return deduped; } +function sortHistoryItems(items: HistoryItem[]): HistoryItem[] { + return [...items].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); +} +function normalizeHistoryItems(items: HistoryItem[]): HistoryItem[] { + return dedupeHistoryItems(sortHistoryItems(items)).slice(0, MAX_HISTORY); +} +function parseStoredHistory(value: string | null): HistoryItem[] { + if (!value) { + return []; + } + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } + catch (err) { + console.error("Failed to parse stored history:", err); + return []; + } +} function App() { const [currentPage, setCurrentPage] = useState("main"); const [spotifyUrl, setSpotifyUrl] = useState(""); @@ -182,7 +201,7 @@ function App() { mediaQuery.addEventListener("change", handleChange); checkForUpdates(); ensureApiStatusCheckStarted(); - loadHistory(); + void loadHistory(); const handleScroll = () => { setShowScrollTop(window.scrollY > 300); }; @@ -232,19 +251,29 @@ function App() { console.error("Failed to check for updates:", err); } }; - const loadHistory = () => { + const persistRecentHistory = useCallback(async (history: HistoryItem[]) => { try { - const saved = localStorage.getItem(HISTORY_KEY); - if (saved) { - const deduped = dedupeHistoryItems(JSON.parse(saved)); - setFetchHistory(deduped); - localStorage.setItem(HISTORY_KEY, JSON.stringify(deduped)); - } + await SaveRecentFetches(JSON.stringify(history)); + } + catch (err) { + console.error("Failed to save recent fetches:", err); + } + }, []); + const loadHistory = useCallback(async () => { + try { + const saved = parseStoredHistory(localStorage.getItem(HISTORY_KEY)); + const persisted = parseStoredHistory(await GetRecentFetches()); + const normalized = normalizeHistoryItems([...persisted, ...saved]); + setFetchHistory(normalized); + await persistRecentHistory(normalized); } catch (err) { console.error("Failed to load history:", err); } - }; + finally { + localStorage.removeItem(HISTORY_KEY); + } + }, [persistRecentHistory]); const handleInstallFFmpeg = async (useBrew: boolean = false) => { setIsInstallingFFmpeg(true); setFfmpegInstallProgress(0); @@ -283,14 +312,6 @@ function App() { setFfmpegInstallStatus(""); } }; - const saveHistory = (history: HistoryItem[]) => { - try { - localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); - } - catch (err) { - console.error("Failed to save history:", err); - } - }; const addToHistory = (item: Omit) => { setFetchHistory((prev) => { const normalizedUrl = normalizeHistoryURL(item.url); @@ -302,15 +323,17 @@ function App() { id: crypto.randomUUID(), timestamp: Date.now(), }; - const updated = [newItem, ...filtered].slice(0, MAX_HISTORY); - saveHistory(updated); + const updated = normalizeHistoryItems([newItem, ...filtered]); + void persistRecentHistory(updated); return updated; }); }; const removeFromHistory = (id: string) => { setFetchHistory((prev) => { + if (!prev.some((h) => h.id === id)) + return prev; const updated = prev.filter((h) => h.id !== id); - saveHistory(updated); + void persistRecentHistory(updated); return updated; }); };