final feature set
This commit is contained in:
@@ -3,8 +3,11 @@ import { DEFAULT_THEME, THEME_JSON } from "../constants/themes"
|
||||
import type { AppSettings, AppState, ThemeColors, ThemeName, ThemeMode, UserPreferences } from "../types/settings"
|
||||
import { resolveTheme } from "../utils/theme-resolver"
|
||||
import type { ThemeJson } from "../types/theme-schema"
|
||||
|
||||
const STORAGE_KEY = "podtui_app_state"
|
||||
import {
|
||||
loadAppStateFromFile,
|
||||
saveAppStateToFile,
|
||||
migrateAppStateFromLocalStorage,
|
||||
} from "../utils/app-persistence"
|
||||
|
||||
const defaultSettings: AppSettings = {
|
||||
theme: "system",
|
||||
@@ -24,33 +27,21 @@ const defaultState: AppState = {
|
||||
customTheme: DEFAULT_THEME,
|
||||
}
|
||||
|
||||
const loadState = (): AppState => {
|
||||
if (typeof localStorage === "undefined") return defaultState
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return defaultState
|
||||
const parsed = JSON.parse(raw) as Partial<AppState>
|
||||
return {
|
||||
settings: { ...defaultSettings, ...parsed.settings },
|
||||
preferences: { ...defaultPreferences, ...parsed.preferences },
|
||||
customTheme: { ...DEFAULT_THEME, ...parsed.customTheme },
|
||||
}
|
||||
} catch {
|
||||
return defaultState
|
||||
}
|
||||
}
|
||||
|
||||
const saveState = (state: AppState) => {
|
||||
if (typeof localStorage === "undefined") return
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
||||
} catch {
|
||||
// ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
export function createAppStore() {
|
||||
const [state, setState] = createSignal<AppState>(loadState())
|
||||
// Start with defaults; async load will update once ready
|
||||
const [state, setState] = createSignal<AppState>(defaultState)
|
||||
|
||||
// Fire-and-forget async initialisation
|
||||
const init = async () => {
|
||||
await migrateAppStateFromLocalStorage()
|
||||
const loaded = await loadAppStateFromFile()
|
||||
setState(loaded)
|
||||
}
|
||||
init()
|
||||
|
||||
const saveState = (next: AppState) => {
|
||||
saveAppStateToFile(next).catch(() => {})
|
||||
}
|
||||
|
||||
const updateState = (next: AppState) => {
|
||||
setState(next)
|
||||
|
||||
@@ -11,6 +11,14 @@ import type { Episode, EpisodeStatus } from "../types/episode"
|
||||
import type { PodcastSource, SourceType } from "../types/source"
|
||||
import { DEFAULT_SOURCES } from "../types/source"
|
||||
import { parseRSSFeed } from "../api/rss-parser"
|
||||
import {
|
||||
loadFeedsFromFile,
|
||||
saveFeedsToFile,
|
||||
loadSourcesFromFile,
|
||||
saveSourcesToFile,
|
||||
migrateFeedsFromLocalStorage,
|
||||
migrateSourcesFromLocalStorage,
|
||||
} from "../utils/feeds-persistence"
|
||||
|
||||
/** Max episodes to fetch on refresh */
|
||||
const MAX_EPISODES_REFRESH = 50
|
||||
@@ -18,85 +26,30 @@ const MAX_EPISODES_REFRESH = 50
|
||||
/** Max episodes to fetch on initial subscribe */
|
||||
const MAX_EPISODES_SUBSCRIBE = 20
|
||||
|
||||
/** Storage keys */
|
||||
const STORAGE_KEYS = {
|
||||
feeds: "podtui_feeds",
|
||||
sources: "podtui_sources",
|
||||
}
|
||||
|
||||
/** Load feeds from localStorage */
|
||||
function loadFeeds(): Feed[] {
|
||||
if (typeof localStorage === "undefined") {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.feeds)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
// Convert date strings
|
||||
return parsed.map((feed: Feed) => ({
|
||||
...feed,
|
||||
lastUpdated: new Date(feed.lastUpdated),
|
||||
podcast: {
|
||||
...feed.podcast,
|
||||
lastUpdated: new Date(feed.podcast.lastUpdated),
|
||||
},
|
||||
episodes: feed.episodes.map((ep: Episode) => ({
|
||||
...ep,
|
||||
pubDate: new Date(ep.pubDate),
|
||||
})),
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
/** Save feeds to localStorage */
|
||||
/** Save feeds to file (async, fire-and-forget) */
|
||||
function saveFeeds(feeds: Feed[]): void {
|
||||
if (typeof localStorage === "undefined") return
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEYS.feeds, JSON.stringify(feeds))
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
saveFeedsToFile(feeds).catch(() => {})
|
||||
}
|
||||
|
||||
/** Load sources from localStorage */
|
||||
function loadSources(): PodcastSource[] {
|
||||
if (typeof localStorage === "undefined") {
|
||||
return [...DEFAULT_SOURCES]
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.sources)
|
||||
if (stored) {
|
||||
return JSON.parse(stored)
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
return [...DEFAULT_SOURCES]
|
||||
}
|
||||
|
||||
/** Save sources to localStorage */
|
||||
/** Save sources to file (async, fire-and-forget) */
|
||||
function saveSources(sources: PodcastSource[]): void {
|
||||
if (typeof localStorage === "undefined") return
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEYS.sources, JSON.stringify(sources))
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
saveSourcesToFile(sources).catch(() => {})
|
||||
}
|
||||
|
||||
/** Create feed store */
|
||||
export function createFeedStore() {
|
||||
const [feeds, setFeeds] = createSignal<Feed[]>(loadFeeds())
|
||||
const [sources, setSources] = createSignal<PodcastSource[]>(loadSources())
|
||||
const [feeds, setFeeds] = createSignal<Feed[]>([])
|
||||
const [sources, setSources] = createSignal<PodcastSource[]>([...DEFAULT_SOURCES])
|
||||
|
||||
// Async initialization: migrate from localStorage, then load from file
|
||||
;(async () => {
|
||||
await migrateFeedsFromLocalStorage()
|
||||
await migrateSourcesFromLocalStorage()
|
||||
const loadedFeeds = await loadFeedsFromFile()
|
||||
if (loadedFeeds.length > 0) setFeeds(loadedFeeds)
|
||||
const loadedSources = await loadSourcesFromFile<PodcastSource>()
|
||||
if (loadedSources && loadedSources.length > 0) setSources(loadedSources)
|
||||
})()
|
||||
const [filter, setFilter] = createSignal<FeedFilter>({
|
||||
visibility: "all",
|
||||
sortBy: "updated" as FeedSortField,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
/**
|
||||
* Episode progress store for PodTUI
|
||||
*
|
||||
* Persists per-episode playback progress to localStorage.
|
||||
* Persists per-episode playback progress to a JSON file in XDG_CONFIG_HOME.
|
||||
* Tracks position, duration, completion, and last-played timestamp.
|
||||
*/
|
||||
|
||||
import { createSignal } from "solid-js"
|
||||
import type { Progress } from "../types/episode"
|
||||
|
||||
const STORAGE_KEY = "podtui_progress"
|
||||
import {
|
||||
loadProgressFromFile,
|
||||
saveProgressToFile,
|
||||
migrateProgressFromLocalStorage,
|
||||
} from "../utils/app-persistence"
|
||||
|
||||
/** Threshold (fraction 0-1) at which an episode is considered completed */
|
||||
const COMPLETION_THRESHOLD = 0.95
|
||||
@@ -16,48 +19,42 @@ const COMPLETION_THRESHOLD = 0.95
|
||||
/** Minimum seconds of progress before persisting */
|
||||
const MIN_POSITION_TO_SAVE = 5
|
||||
|
||||
// --- localStorage helpers ---
|
||||
|
||||
function loadProgress(): Record<string, Progress> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return {}
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>
|
||||
const result: Record<string, Progress> = {}
|
||||
for (const [key, value] of Object.entries(parsed)) {
|
||||
const p = value as Record<string, unknown>
|
||||
result[key] = {
|
||||
episodeId: p.episodeId as string,
|
||||
position: p.position as number,
|
||||
duration: p.duration as number,
|
||||
timestamp: new Date(p.timestamp as string),
|
||||
playbackSpeed: p.playbackSpeed as number | undefined,
|
||||
}
|
||||
}
|
||||
return result
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function saveProgress(data: Record<string, Progress>): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
|
||||
} catch {
|
||||
// Quota exceeded or unavailable — silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
// --- Singleton store ---
|
||||
|
||||
const [progressMap, setProgressMap] = createSignal<Record<string, Progress>>(
|
||||
loadProgress(),
|
||||
)
|
||||
const [progressMap, setProgressMap] = createSignal<Record<string, Progress>>({})
|
||||
|
||||
/** Persist current progress map to file (fire-and-forget) */
|
||||
function persist(): void {
|
||||
saveProgress(progressMap())
|
||||
saveProgressToFile(progressMap()).catch(() => {})
|
||||
}
|
||||
|
||||
/** Parse raw progress entries from file, reviving Date objects */
|
||||
function parseProgressEntries(raw: Record<string, unknown>): Record<string, Progress> {
|
||||
const result: Record<string, Progress> = {}
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
const p = value as Record<string, unknown>
|
||||
result[key] = {
|
||||
episodeId: p.episodeId as string,
|
||||
position: p.position as number,
|
||||
duration: p.duration as number,
|
||||
timestamp: new Date(p.timestamp as string),
|
||||
playbackSpeed: p.playbackSpeed as number | undefined,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Async initialisation — migrate from localStorage then load from file */
|
||||
async function initProgress(): Promise<void> {
|
||||
await migrateProgressFromLocalStorage()
|
||||
const raw = await loadProgressFromFile()
|
||||
const parsed = parseProgressEntries(raw as Record<string, unknown>)
|
||||
setProgressMap(parsed)
|
||||
}
|
||||
|
||||
// Fire-and-forget init
|
||||
initProgress()
|
||||
|
||||
function createProgressStore() {
|
||||
return {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user