checkpoint
This commit is contained in:
57
src/utils/cache.ts
Normal file
57
src/utils/cache.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
type CacheEntry<T> = {
|
||||
value: T
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const CACHE_KEY = "podtui_cache"
|
||||
const DEFAULT_TTL = 1000 * 60 * 60
|
||||
|
||||
const loadCache = (): Record<string, CacheEntry<unknown>> => {
|
||||
if (typeof localStorage === "undefined") return {}
|
||||
try {
|
||||
const raw = localStorage.getItem(CACHE_KEY)
|
||||
return raw ? (JSON.parse(raw) as Record<string, CacheEntry<unknown>>) : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const saveCache = (cache: Record<string, CacheEntry<unknown>>) => {
|
||||
if (typeof localStorage === "undefined") return
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(cache))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const cache = loadCache()
|
||||
|
||||
export const cacheValue = <T,>(key: string, value: T) => {
|
||||
cache[key] = { value, timestamp: Date.now() }
|
||||
saveCache(cache)
|
||||
}
|
||||
|
||||
export const getCachedValue = <T,>(key: string, ttl = DEFAULT_TTL): T | null => {
|
||||
const entry = cache[key] as CacheEntry<T> | undefined
|
||||
if (!entry) return null
|
||||
if (Date.now() - entry.timestamp > ttl) {
|
||||
delete cache[key]
|
||||
saveCache(cache)
|
||||
return null
|
||||
}
|
||||
return entry.value
|
||||
}
|
||||
|
||||
export const invalidateCache = (prefix?: string) => {
|
||||
if (!prefix) {
|
||||
Object.keys(cache).forEach((key) => delete cache[key])
|
||||
saveCache(cache)
|
||||
return
|
||||
}
|
||||
|
||||
Object.keys(cache)
|
||||
.filter((key) => key.startsWith(prefix))
|
||||
.forEach((key) => delete cache[key])
|
||||
saveCache(cache)
|
||||
}
|
||||
57
src/utils/data-fetcher.ts
Normal file
57
src/utils/data-fetcher.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { FeedVisibility } from "../types/feed"
|
||||
import type { Feed } from "../types/feed"
|
||||
import type { Episode } from "../types/episode"
|
||||
import type { Podcast } from "../types/podcast"
|
||||
import { cacheValue, getCachedValue } from "./cache"
|
||||
import { fetchEpisodes } from "@/api/client"
|
||||
|
||||
const feedKey = (feedUrl: string) => `feed:${feedUrl}`
|
||||
const episodesKey = (feedUrl: string) => `episodes:${feedUrl}`
|
||||
const searchKey = (query: string) => `search:${query.toLowerCase()}`
|
||||
|
||||
export const fetchFeedWithCache = async (feedUrl: string): Promise<Feed | null> => {
|
||||
const cached = getCachedValue<Feed>(feedKey(feedUrl))
|
||||
if (cached) return cached
|
||||
try {
|
||||
const episodes = await fetchEpisodes(feedUrl)
|
||||
const feed: Feed = {
|
||||
id: feedUrl,
|
||||
podcast: {
|
||||
id: feedUrl,
|
||||
title: feedUrl,
|
||||
description: "",
|
||||
feedUrl,
|
||||
lastUpdated: new Date(),
|
||||
isSubscribed: true,
|
||||
},
|
||||
episodes,
|
||||
visibility: FeedVisibility.PUBLIC,
|
||||
sourceId: "rss",
|
||||
lastUpdated: new Date(),
|
||||
isPinned: false,
|
||||
}
|
||||
cacheValue(feedKey(feedUrl), feed)
|
||||
return feed
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchEpisodesWithCache = async (feedUrl: string): Promise<Episode[]> => {
|
||||
const cached = getCachedValue<Episode[]>(episodesKey(feedUrl))
|
||||
if (cached) return cached
|
||||
const episodes = await fetchEpisodes(feedUrl)
|
||||
cacheValue(episodesKey(feedUrl), episodes)
|
||||
return episodes
|
||||
}
|
||||
|
||||
export const searchWithCache = async (
|
||||
query: string,
|
||||
fetcher: () => Promise<Podcast[]>
|
||||
): Promise<Podcast[]> => {
|
||||
const cached = getCachedValue<Podcast[]>(searchKey(query))
|
||||
if (cached) return cached
|
||||
const results = await fetcher()
|
||||
cacheValue(searchKey(query), results)
|
||||
return results
|
||||
}
|
||||
77
src/utils/persistence.ts
Normal file
77
src/utils/persistence.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { AppSettings, UserPreferences } from "../types/settings"
|
||||
import type { Feed } from "../types/feed"
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
settings: "podtui_settings",
|
||||
preferences: "podtui_preferences",
|
||||
feeds: "podtui_feeds",
|
||||
}
|
||||
|
||||
export const savePreference = (key: keyof UserPreferences, value: boolean) => {
|
||||
const current = loadPreferences()
|
||||
const next = { ...current, [key]: value }
|
||||
savePreferences(next)
|
||||
}
|
||||
|
||||
export const loadPreference = (key: keyof UserPreferences) => {
|
||||
return loadPreferences()[key]
|
||||
}
|
||||
|
||||
export const saveSettings = (settings: AppSettings) => {
|
||||
if (typeof localStorage === "undefined") return
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEYS.settings, JSON.stringify(settings))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export const loadSettings = (): AppSettings | null => {
|
||||
if (typeof localStorage === "undefined") return null
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEYS.settings)
|
||||
return raw ? (JSON.parse(raw) as AppSettings) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const savePreferences = (preferences: UserPreferences) => {
|
||||
if (typeof localStorage === "undefined") return
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEYS.preferences, JSON.stringify(preferences))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export const loadPreferences = (): UserPreferences => {
|
||||
if (typeof localStorage === "undefined") {
|
||||
return { showExplicit: false, autoDownload: false }
|
||||
}
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEYS.preferences)
|
||||
return raw ? (JSON.parse(raw) as UserPreferences) : { showExplicit: false, autoDownload: false }
|
||||
} catch {
|
||||
return { showExplicit: false, autoDownload: false }
|
||||
}
|
||||
}
|
||||
|
||||
export const saveFeeds = (feeds: Feed[]) => {
|
||||
if (typeof localStorage === "undefined") return
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEYS.feeds, JSON.stringify(feeds))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export const loadFeeds = (): Feed[] => {
|
||||
if (typeof localStorage === "undefined") return []
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEYS.feeds)
|
||||
return raw ? (JSON.parse(raw) as Feed[]) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
8
src/utils/waveform.ts
Normal file
8
src/utils/waveform.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const createWaveform = (width: number): number[] => {
|
||||
const data: number[] = []
|
||||
for (let i = 0; i < width; i += 1) {
|
||||
const value = 0.2 + Math.abs(Math.sin(i / 3)) * 0.8
|
||||
data.push(Number(value.toFixed(2)))
|
||||
}
|
||||
return data
|
||||
}
|
||||
Reference in New Issue
Block a user