drop oauth plan (copy config)
This commit is contained in:
@@ -1,158 +1,151 @@
|
||||
/**
|
||||
* App state persistence via JSON file in XDG_CONFIG_HOME
|
||||
* App state persistence — settings, preferences, and custom theme are stored
|
||||
* in the centralized `config.json` (see utils/config.ts). Playback progress
|
||||
* and audio-nav state stay in separate files (they change on every seek and
|
||||
* would thrash config.json).
|
||||
*
|
||||
* Reads and writes app settings, preferences, and custom theme to a JSON file
|
||||
* No backups — writes always overwrite.
|
||||
*/
|
||||
|
||||
import { ensureConfigDir, getConfigFilePath } from "./config-dir";
|
||||
import { backupConfigFile } from "./config-backup";
|
||||
import { loadConfig, updateConfig } from "./config";
|
||||
import type {
|
||||
AppState,
|
||||
AppSettings,
|
||||
UserPreferences,
|
||||
VisualizerSettings,
|
||||
AppState,
|
||||
AppSettings,
|
||||
UserPreferences,
|
||||
VisualizerSettings,
|
||||
} from "../types/settings";
|
||||
import { DEFAULT_THEME } from "../constants/themes";
|
||||
|
||||
const APP_STATE_FILE = "app-state.json";
|
||||
const PROGRESS_FILE = "progress.json";
|
||||
const AUDIO_NAV_FILE = "audio-nav.json";
|
||||
|
||||
// --- Defaults ---
|
||||
|
||||
const defaultVisualizerSettings: VisualizerSettings = {
|
||||
bars: 32,
|
||||
sensitivity: 1,
|
||||
noiseReduction: 0.77,
|
||||
lowCutOff: 50,
|
||||
highCutOff: 10000,
|
||||
bars: 32,
|
||||
sensitivity: 1,
|
||||
noiseReduction: 0.77,
|
||||
lowCutOff: 50,
|
||||
highCutOff: 10000,
|
||||
};
|
||||
|
||||
const defaultSettings: AppSettings = {
|
||||
theme: "system",
|
||||
fontSize: 14,
|
||||
playbackSpeed: 1,
|
||||
downloadPath: "",
|
||||
visualizer: defaultVisualizerSettings,
|
||||
theme: "system",
|
||||
fontSize: 14,
|
||||
playbackSpeed: 1,
|
||||
downloadPath: "",
|
||||
visualizer: defaultVisualizerSettings,
|
||||
};
|
||||
|
||||
const defaultPreferences: UserPreferences = {
|
||||
showExplicit: false,
|
||||
autoDownload: false,
|
||||
showExplicit: false,
|
||||
autoDownload: false,
|
||||
};
|
||||
|
||||
const defaultState: AppState = {
|
||||
settings: defaultSettings,
|
||||
preferences: defaultPreferences,
|
||||
customTheme: DEFAULT_THEME,
|
||||
settings: defaultSettings,
|
||||
preferences: defaultPreferences,
|
||||
customTheme: DEFAULT_THEME,
|
||||
};
|
||||
|
||||
// --- App State ---
|
||||
// ── App State (config.json) ─────────────────────────────────────────────────
|
||||
|
||||
/** Load app state from JSON file */
|
||||
/** Load app state from config.json */
|
||||
export async function loadAppStateFromFile(): Promise<AppState> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(APP_STATE_FILE);
|
||||
const file = Bun.file(filePath);
|
||||
if (!(await file.exists())) return defaultState;
|
||||
|
||||
const raw = await file.json();
|
||||
if (!raw || typeof raw !== "object") return defaultState;
|
||||
|
||||
const parsed = raw as Partial<AppState>;
|
||||
return {
|
||||
settings: { ...defaultSettings, ...parsed.settings },
|
||||
preferences: { ...defaultPreferences, ...parsed.preferences },
|
||||
customTheme: { ...DEFAULT_THEME, ...parsed.customTheme },
|
||||
};
|
||||
} catch {
|
||||
return defaultState;
|
||||
}
|
||||
try {
|
||||
const cfg = await loadConfig();
|
||||
if (!cfg || typeof cfg !== "object") return defaultState;
|
||||
return {
|
||||
settings: { ...defaultSettings, ...cfg.settings },
|
||||
preferences: { ...defaultPreferences, ...cfg.preferences },
|
||||
customTheme: { ...DEFAULT_THEME, ...cfg.customTheme },
|
||||
};
|
||||
} catch {
|
||||
return defaultState;
|
||||
}
|
||||
}
|
||||
|
||||
/** Save app state to JSON file */
|
||||
export async function saveAppStateToFile(state: AppState): Promise<void> {
|
||||
try {
|
||||
await ensureConfigDir();
|
||||
await backupConfigFile(APP_STATE_FILE);
|
||||
const filePath = getConfigFilePath(APP_STATE_FILE);
|
||||
await Bun.write(filePath, JSON.stringify(state, null, 2));
|
||||
} catch {
|
||||
// Silently ignore write errors
|
||||
}
|
||||
/** Save app state to config.json */
|
||||
export function saveAppStateToFile(state: AppState): void {
|
||||
updateConfig({
|
||||
settings: state.settings,
|
||||
preferences: state.preferences,
|
||||
customTheme: state.customTheme,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Playback Progress (separate file — changes on every seek) ───────────────
|
||||
|
||||
const PROGRESS_FILE = "progress.json";
|
||||
|
||||
interface ProgressEntry {
|
||||
episodeId: string;
|
||||
position: number;
|
||||
duration: number;
|
||||
timestamp: string | Date;
|
||||
playbackSpeed?: number;
|
||||
episodeId: string;
|
||||
position: number;
|
||||
duration: number;
|
||||
timestamp: string | Date;
|
||||
playbackSpeed?: number;
|
||||
}
|
||||
|
||||
/** Load progress map from JSON file */
|
||||
export async function loadProgressFromFile(): Promise<
|
||||
Record<string, ProgressEntry>
|
||||
Record<string, ProgressEntry>
|
||||
> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(PROGRESS_FILE);
|
||||
const file = Bun.file(filePath);
|
||||
if (!(await file.exists())) return {};
|
||||
try {
|
||||
const filePath = getConfigFilePath(PROGRESS_FILE);
|
||||
const file = Bun.file(filePath);
|
||||
if (!(await file.exists())) return {};
|
||||
|
||||
const raw = await file.json();
|
||||
if (!raw || typeof raw !== "object") return {};
|
||||
return raw as Record<string, ProgressEntry>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
const raw = await file.json();
|
||||
if (!raw || typeof raw !== "object") return {};
|
||||
return raw as Record<string, ProgressEntry>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Save progress map to JSON file */
|
||||
export async function saveProgressToFile(
|
||||
data: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await ensureConfigDir();
|
||||
await backupConfigFile(PROGRESS_FILE);
|
||||
const filePath = getConfigFilePath(PROGRESS_FILE);
|
||||
await Bun.write(filePath, JSON.stringify(data, null, 2));
|
||||
} catch {
|
||||
// Silently ignore write errors
|
||||
}
|
||||
/** Save progress map to JSON file (overwrite, no backup) */
|
||||
export function saveProgressToFile(data: Record<string, unknown>): void {
|
||||
(async () => {
|
||||
try {
|
||||
await ensureConfigDir();
|
||||
await Bun.write(
|
||||
getConfigFilePath(PROGRESS_FILE),
|
||||
JSON.stringify(data, null, 2),
|
||||
);
|
||||
} catch {
|
||||
// Silently ignore write errors
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
interface AudioNavEntry {
|
||||
source: string;
|
||||
currentIndex: number;
|
||||
podcastId?: string;
|
||||
lastUpdated: string;
|
||||
}
|
||||
// ── Audio Nav State (separate file — changes on every track change) ──────────
|
||||
|
||||
const AUDIO_NAV_FILE = "audio-nav.json";
|
||||
|
||||
/** Load audio navigation state from JSON file */
|
||||
export async function loadAudioNavFromFile<T>(): Promise<T | null> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(AUDIO_NAV_FILE);
|
||||
const file = Bun.file(filePath);
|
||||
if (!(await file.exists())) return null;
|
||||
try {
|
||||
const file = Bun.file(getConfigFilePath(AUDIO_NAV_FILE));
|
||||
if (!(await file.exists())) return null;
|
||||
|
||||
const raw = await file.json();
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
const raw = await file.json();
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
|
||||
return raw as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return raw as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Save audio navigation state to JSON file */
|
||||
export async function saveAudioNavToFile<T>(
|
||||
data: T,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await ensureConfigDir();
|
||||
const filePath = getConfigFilePath(AUDIO_NAV_FILE);
|
||||
await Bun.write(filePath, JSON.stringify(data, null, 2));
|
||||
} catch {
|
||||
// Silently ignore write errors
|
||||
}
|
||||
/** Save audio navigation state to JSON file (overwrite, no backup) */
|
||||
export function saveAudioNavToFile<T>(data: T): void {
|
||||
(async () => {
|
||||
try {
|
||||
await ensureConfigDir();
|
||||
await Bun.write(
|
||||
getConfigFilePath(AUDIO_NAV_FILE),
|
||||
JSON.stringify(data, null, 2),
|
||||
);
|
||||
} catch {
|
||||
// Silently ignore write errors
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* Config file backup utility for PodTUI
|
||||
*
|
||||
* Creates timestamped backups of config files before updates.
|
||||
* Keeps the most recent N backups and cleans up older ones.
|
||||
*/
|
||||
|
||||
import { readdir, unlink } from "fs/promises"
|
||||
import path from "path"
|
||||
import { getConfigDir, ensureConfigDir } from "./config-dir"
|
||||
|
||||
/** Maximum number of backup files to keep per config file */
|
||||
const MAX_BACKUPS = 5
|
||||
|
||||
/**
|
||||
* Generate a timestamped backup filename.
|
||||
* Example: feeds.json -> feeds.json.2026-02-05T120000.backup
|
||||
*/
|
||||
function backupFilename(originalName: string): string {
|
||||
const ts = new Date().toISOString().replace(/[:.]/g, "").slice(0, 15)
|
||||
return `${originalName}.${ts}.backup`
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backup of a config file before overwriting it.
|
||||
* No-op if the source file does not exist.
|
||||
*/
|
||||
export async function backupConfigFile(filename: string): Promise<boolean> {
|
||||
try {
|
||||
await ensureConfigDir()
|
||||
const dir = getConfigDir()
|
||||
const srcPath = path.join(dir, filename)
|
||||
const srcFile = Bun.file(srcPath)
|
||||
|
||||
if (!(await srcFile.exists())) return false
|
||||
|
||||
const content = await srcFile.text()
|
||||
if (!content || content.trim().length === 0) return false
|
||||
|
||||
const backupName = backupFilename(filename)
|
||||
const backupPath = path.join(dir, backupName)
|
||||
await Bun.write(backupPath, content)
|
||||
|
||||
// Clean up old backups
|
||||
await pruneBackups(filename)
|
||||
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep only the most recent MAX_BACKUPS backup files for a given config file.
|
||||
*/
|
||||
async function pruneBackups(filename: string): Promise<void> {
|
||||
try {
|
||||
const dir = getConfigDir()
|
||||
const entries = await readdir(dir)
|
||||
|
||||
// Match pattern: filename.*.backup
|
||||
const prefix = `${filename}.`
|
||||
const suffix = ".backup"
|
||||
const backups = entries
|
||||
.filter((e) => e.startsWith(prefix) && e.endsWith(suffix))
|
||||
.sort() // Lexicographic sort works because timestamps are ISO-like
|
||||
|
||||
if (backups.length <= MAX_BACKUPS) return
|
||||
|
||||
const toRemove = backups.slice(0, backups.length - MAX_BACKUPS)
|
||||
for (const name of toRemove) {
|
||||
await unlink(path.join(dir, name)).catch(() => {})
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List existing backup files for a given config file, newest first.
|
||||
*/
|
||||
export async function listBackups(filename: string): Promise<string[]> {
|
||||
try {
|
||||
const dir = getConfigDir()
|
||||
const entries = await readdir(dir)
|
||||
|
||||
const prefix = `${filename}.`
|
||||
const suffix = ".backup"
|
||||
return entries
|
||||
.filter((e) => e.startsWith(prefix) && e.endsWith(suffix))
|
||||
.sort()
|
||||
.reverse()
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
187
src/utils/config.ts
Normal file
187
src/utils/config.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Centralized PodTui configuration — a single `config.json` holding every
|
||||
* user-facing bit needed to migrate to a new machine by copying one file.
|
||||
*
|
||||
* Contains: settings, preferences, custom theme, feeds (subscriptions), and
|
||||
* sources (podcast search/RSS sources).
|
||||
*
|
||||
* Runtime state that changes on every playback action (progress, downloads,
|
||||
* audio-nav) stays in separate files to avoid rewriting this file on every
|
||||
* seek. Keybinds remain in `keybinds.jsonc` (user-editable JSONC).
|
||||
*
|
||||
* Writes are serialized to avoid concurrent read-modify-write races, and
|
||||
* always overwrite — no backup files are created.
|
||||
*/
|
||||
|
||||
import { ensureConfigDir, getConfigDir, getConfigFilePath } from "./config-dir";
|
||||
import type {
|
||||
AppSettings,
|
||||
UserPreferences,
|
||||
ThemeColors,
|
||||
} from "../types/settings";
|
||||
import type { Feed } from "../types/feed";
|
||||
import type { PodcastSource } from "../types/source";
|
||||
|
||||
/** Everything a user needs to migrate, in one file. */
|
||||
export interface PodTuiConfig {
|
||||
settings?: AppSettings;
|
||||
preferences?: UserPreferences;
|
||||
customTheme?: ThemeColors;
|
||||
feeds?: Feed[];
|
||||
sources?: PodcastSource[];
|
||||
}
|
||||
|
||||
const CONFIG_FILE = "config.json";
|
||||
|
||||
/** Legacy per-section files, migrated into config.json on first load. */
|
||||
const LEGACY_FILES = ["app-state.json", "feeds.json", "sources.json"] as const;
|
||||
|
||||
/** Load the full config from disk. Returns {} if missing or corrupt.
|
||||
* Runs one-time legacy migration on first call. */
|
||||
export async function loadConfig(): Promise<PodTuiConfig> {
|
||||
await migrateOnce();
|
||||
try {
|
||||
const file = Bun.file(getConfigFilePath(CONFIG_FILE));
|
||||
if (!(await file.exists())) return {};
|
||||
const raw = await file.json();
|
||||
if (!raw || typeof raw !== "object") return {};
|
||||
return raw as PodTuiConfig;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Write serialization ────────────────────────────────────────────────────
|
||||
// A simple promise chain ensures reads-modify-writes execute sequentially so
|
||||
// two concurrent saves can't clobber each other's sections.
|
||||
let writeChain: Promise<void> = Promise.resolve();
|
||||
|
||||
/** Update sections of config.json (read-modify-write, serialized, overwrite). */
|
||||
export function updateConfig(patch: Partial<PodTuiConfig>): void {
|
||||
writeChain = writeChain.then(async () => {
|
||||
try {
|
||||
await ensureConfigDir();
|
||||
const current = await loadConfig();
|
||||
const next = { ...current, ...patch };
|
||||
await Bun.write(
|
||||
getConfigFilePath(CONFIG_FILE),
|
||||
JSON.stringify(next, null, 2),
|
||||
);
|
||||
} catch {
|
||||
// Fire-and-forget persistence — silently ignore write errors.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Await all pending config writes (used by sync/export flows). */
|
||||
export async function flushConfig(): Promise<void> {
|
||||
await writeChain;
|
||||
}
|
||||
|
||||
/** Guards so migration runs exactly once per process. */
|
||||
let migrationDone = false;
|
||||
let migrationPromise: Promise<void> | null = null;
|
||||
|
||||
/** Run legacy migration + backup cleanup once, before the first config read. */
|
||||
async function migrateOnce(): Promise<void> {
|
||||
if (migrationDone) return;
|
||||
if (!migrationPromise) migrationPromise = migrateLegacyConfig();
|
||||
await migrationPromise;
|
||||
migrationDone = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* One-time migration: if config.json doesn't exist but legacy per-section
|
||||
* files do, merge them into a single config.json. Also cleans up any stale
|
||||
* backup files (`.backup` suffix) left by the old config-backup module.
|
||||
*
|
||||
* Safe to call on every startup — no-op once config.json exists (except for
|
||||
* backup cleanup, which runs unconditionally since those files are now dead).
|
||||
*/
|
||||
export async function migrateLegacyConfig(): Promise<void> {
|
||||
try {
|
||||
await ensureConfigDir();
|
||||
const dir = getConfigDir();
|
||||
const configExists = await Bun.file(
|
||||
getConfigFilePath(CONFIG_FILE),
|
||||
).exists();
|
||||
|
||||
if (!configExists) {
|
||||
const merged: PodTuiConfig = {};
|
||||
|
||||
// app-state.json → settings, preferences, customTheme
|
||||
const appStateFile = Bun.file(getConfigFilePath("app-state.json"));
|
||||
if (await appStateFile.exists()) {
|
||||
try {
|
||||
const raw = await appStateFile.json();
|
||||
if (raw && typeof raw === "object") {
|
||||
merged.settings = raw.settings;
|
||||
merged.preferences = raw.preferences;
|
||||
merged.customTheme = raw.customTheme;
|
||||
}
|
||||
} catch {
|
||||
// ignore corrupt legacy file
|
||||
}
|
||||
}
|
||||
|
||||
// feeds.json → feeds
|
||||
const feedsFile = Bun.file(getConfigFilePath("feeds.json"));
|
||||
if (await feedsFile.exists()) {
|
||||
try {
|
||||
const raw = await feedsFile.json();
|
||||
if (Array.isArray(raw)) merged.feeds = raw;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// sources.json → sources
|
||||
const sourcesFile = Bun.file(getConfigFilePath("sources.json"));
|
||||
if (await sourcesFile.exists()) {
|
||||
try {
|
||||
const raw = await sourcesFile.json();
|
||||
if (Array.isArray(raw)) merged.sources = raw;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(merged).length > 0) {
|
||||
await Bun.write(
|
||||
getConfigFilePath(CONFIG_FILE),
|
||||
JSON.stringify(merged, null, 2),
|
||||
);
|
||||
// Remove migrated legacy files
|
||||
for (const name of LEGACY_FILES) {
|
||||
await Bun.file(getConfigFilePath(name))
|
||||
.exists()
|
||||
.then(async (exists) => {
|
||||
if (exists)
|
||||
await import("fs/promises").then((fs) =>
|
||||
fs.unlink(getConfigFilePath(name)).catch(() => {}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up stale backup files (no longer created, remove old ones)
|
||||
await cleanBackups(dir);
|
||||
} catch {
|
||||
// Migration is best-effort — never block startup.
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove all `.backup` files from the config directory. */
|
||||
async function cleanBackups(dir: string): Promise<void> {
|
||||
try {
|
||||
const { readdir, unlink } = await import("fs/promises");
|
||||
const entries = await readdir(dir);
|
||||
const backups = entries.filter((e) => e.endsWith(".backup"));
|
||||
for (const name of backups) {
|
||||
await unlink(`${dir}/${name}`).catch(() => {});
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
@@ -105,8 +105,6 @@ export type AppEvents = {
|
||||
"player.play": { episodeId: string };
|
||||
"player.pause": { episodeId: string };
|
||||
"player.stop": {};
|
||||
"auth.login": { userId: string };
|
||||
"auth.logout": {};
|
||||
"toast.show": {
|
||||
message: string;
|
||||
variant: "info" | "success" | "warning" | "error";
|
||||
|
||||
@@ -1,82 +1,56 @@
|
||||
/**
|
||||
* Feeds persistence via JSON file in XDG_CONFIG_HOME
|
||||
*
|
||||
* Reads and writes feeds to a JSON file instead of localStorage.
|
||||
* Feeds & sources persistence — stored in the centralized `config.json`
|
||||
* (see utils/config.ts). No backups; writes always overwrite.
|
||||
*/
|
||||
|
||||
import { ensureConfigDir, getConfigFilePath } from "./config-dir";
|
||||
import { backupConfigFile } from "./config-backup";
|
||||
import { loadConfig, updateConfig } from "./config";
|
||||
import type { Feed } from "../types/feed";
|
||||
|
||||
const FEEDS_FILE = "feeds.json";
|
||||
const SOURCES_FILE = "sources.json";
|
||||
import type { PodcastSource } from "../types/source";
|
||||
|
||||
/** Deserialize date strings back to Date objects in feed data */
|
||||
function reviveDates(feed: Feed): Feed {
|
||||
return {
|
||||
...feed,
|
||||
lastUpdated: new Date(feed.lastUpdated),
|
||||
podcast: {
|
||||
...feed.podcast,
|
||||
lastUpdated: new Date(feed.podcast.lastUpdated),
|
||||
},
|
||||
episodes: feed.episodes.map((ep) => ({
|
||||
...ep,
|
||||
pubDate: new Date(ep.pubDate),
|
||||
})),
|
||||
};
|
||||
return {
|
||||
...feed,
|
||||
lastUpdated: new Date(feed.lastUpdated),
|
||||
podcast: {
|
||||
...feed.podcast,
|
||||
lastUpdated: new Date(feed.podcast.lastUpdated),
|
||||
},
|
||||
episodes: feed.episodes.map((ep) => ({
|
||||
...ep,
|
||||
pubDate: new Date(ep.pubDate),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/** Load feeds from JSON file */
|
||||
/** Load feeds from config.json */
|
||||
export async function loadFeedsFromFile(): Promise<Feed[]> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(FEEDS_FILE);
|
||||
const file = Bun.file(filePath);
|
||||
if (!(await file.exists())) return [];
|
||||
|
||||
const raw = await file.json();
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.map(reviveDates);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const cfg = await loadConfig();
|
||||
if (!Array.isArray(cfg.feeds)) return [];
|
||||
return cfg.feeds.map(reviveDates);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Save feeds to JSON file */
|
||||
export async function saveFeedsToFile(feeds: Feed[]): Promise<void> {
|
||||
try {
|
||||
await ensureConfigDir();
|
||||
await backupConfigFile(FEEDS_FILE);
|
||||
const filePath = getConfigFilePath(FEEDS_FILE);
|
||||
await Bun.write(filePath, JSON.stringify(feeds, null, 2));
|
||||
} catch {
|
||||
// Silently ignore write errors
|
||||
}
|
||||
/** Save feeds to config.json */
|
||||
export function saveFeedsToFile(feeds: Feed[]): void {
|
||||
updateConfig({ feeds });
|
||||
}
|
||||
|
||||
/** Load sources from JSON file */
|
||||
/** Load sources from config.json */
|
||||
export async function loadSourcesFromFile<T>(): Promise<T[] | null> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(SOURCES_FILE);
|
||||
const file = Bun.file(filePath);
|
||||
if (!(await file.exists())) return null;
|
||||
|
||||
const raw = await file.json();
|
||||
if (!Array.isArray(raw)) return null;
|
||||
return raw as T[];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const cfg = await loadConfig();
|
||||
if (!Array.isArray(cfg.sources)) return null;
|
||||
return cfg.sources as T[];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Save sources to JSON file */
|
||||
export async function saveSourcesToFile<T>(sources: T[]): Promise<void> {
|
||||
try {
|
||||
await ensureConfigDir();
|
||||
await backupConfigFile(SOURCES_FILE);
|
||||
const filePath = getConfigFilePath(SOURCES_FILE);
|
||||
await Bun.write(filePath, JSON.stringify(sources, null, 2));
|
||||
} catch {
|
||||
// Silently ignore write errors
|
||||
}
|
||||
/** Save sources to config.json */
|
||||
export function saveSourcesToFile<T>(sources: T[]): void {
|
||||
updateConfig({ sources: sources as unknown as PodcastSource[] });
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
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 []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user