remove migration code
This commit is contained in:
@@ -2,19 +2,20 @@
|
||||
* App state persistence via JSON file in XDG_CONFIG_HOME
|
||||
*
|
||||
* Reads and writes app settings, preferences, and custom theme to a JSON file
|
||||
* instead of localStorage. Provides migration from localStorage on first run.
|
||||
*/
|
||||
|
||||
import { ensureConfigDir, getConfigFilePath } from "./config-dir"
|
||||
import { backupConfigFile } from "./config-backup"
|
||||
import type { AppState, AppSettings, UserPreferences, ThemeColors, VisualizerSettings } from "../types/settings"
|
||||
import { DEFAULT_THEME } from "../constants/themes"
|
||||
import { ensureConfigDir, getConfigFilePath } from "./config-dir";
|
||||
import { backupConfigFile } from "./config-backup";
|
||||
import type {
|
||||
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 LEGACY_APP_STATE_KEY = "podtui_app_state"
|
||||
const LEGACY_PROGRESS_KEY = "podtui_progress"
|
||||
const APP_STATE_FILE = "app-state.json";
|
||||
const PROGRESS_FILE = "progress.json";
|
||||
|
||||
// --- Defaults ---
|
||||
|
||||
@@ -24,7 +25,7 @@ const defaultVisualizerSettings: VisualizerSettings = {
|
||||
noiseReduction: 0.77,
|
||||
lowCutOff: 50,
|
||||
highCutOff: 10000,
|
||||
}
|
||||
};
|
||||
|
||||
const defaultSettings: AppSettings = {
|
||||
theme: "system",
|
||||
@@ -32,141 +33,89 @@ const defaultSettings: AppSettings = {
|
||||
playbackSpeed: 1,
|
||||
downloadPath: "",
|
||||
visualizer: defaultVisualizerSettings,
|
||||
}
|
||||
};
|
||||
|
||||
const defaultPreferences: UserPreferences = {
|
||||
showExplicit: false,
|
||||
autoDownload: false,
|
||||
}
|
||||
};
|
||||
|
||||
const defaultState: AppState = {
|
||||
settings: defaultSettings,
|
||||
preferences: defaultPreferences,
|
||||
customTheme: DEFAULT_THEME,
|
||||
}
|
||||
};
|
||||
|
||||
// --- App State ---
|
||||
|
||||
/** Load app state from JSON file */
|
||||
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 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 raw = await file.json();
|
||||
if (!raw || typeof raw !== "object") return defaultState;
|
||||
|
||||
const parsed = raw as Partial<AppState>
|
||||
const parsed = raw as Partial<AppState>;
|
||||
return {
|
||||
settings: { ...defaultSettings, ...parsed.settings },
|
||||
preferences: { ...defaultPreferences, ...parsed.preferences },
|
||||
customTheme: { ...DEFAULT_THEME, ...parsed.customTheme },
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
return defaultState
|
||||
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))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate app state from localStorage to file.
|
||||
* Only runs once — if the state file already exists, it's a no-op.
|
||||
*/
|
||||
export async function migrateAppStateFromLocalStorage(): Promise<boolean> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(APP_STATE_FILE)
|
||||
const file = Bun.file(filePath)
|
||||
if (await file.exists()) return false
|
||||
|
||||
if (typeof localStorage === "undefined") return false
|
||||
|
||||
const raw = localStorage.getItem(LEGACY_APP_STATE_KEY)
|
||||
if (!raw) return false
|
||||
|
||||
const parsed = JSON.parse(raw) as Partial<AppState>
|
||||
const state: AppState = {
|
||||
settings: { ...defaultSettings, ...parsed.settings },
|
||||
preferences: { ...defaultPreferences, ...parsed.preferences },
|
||||
customTheme: { ...DEFAULT_THEME, ...parsed.customTheme },
|
||||
}
|
||||
|
||||
await saveAppStateToFile(state)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// --- Progress ---
|
||||
|
||||
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>> {
|
||||
export async function loadProgressFromFile(): Promise<
|
||||
Record<string, ProgressEntry>
|
||||
> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(PROGRESS_FILE)
|
||||
const file = Bun.file(filePath)
|
||||
if (!(await file.exists())) return {}
|
||||
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>
|
||||
const raw = await file.json();
|
||||
if (!raw || typeof raw !== "object") return {};
|
||||
return raw as Record<string, ProgressEntry>;
|
||||
} catch {
|
||||
return {}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Save progress map to JSON file */
|
||||
export async function saveProgressToFile(data: Record<string, unknown>): Promise<void> {
|
||||
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))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate progress from localStorage to file.
|
||||
* Only runs once — if the progress file already exists, it's a no-op.
|
||||
*/
|
||||
export async function migrateProgressFromLocalStorage(): Promise<boolean> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(PROGRESS_FILE)
|
||||
const file = Bun.file(filePath)
|
||||
if (await file.exists()) return false
|
||||
|
||||
if (typeof localStorage === "undefined") return false
|
||||
|
||||
const raw = localStorage.getItem(LEGACY_PROGRESS_KEY)
|
||||
if (!raw) return false
|
||||
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!parsed || typeof parsed !== "object") return false
|
||||
|
||||
await saveProgressToFile(parsed as Record<string, unknown>)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +1,116 @@
|
||||
/**
|
||||
* Config file validation and migration for PodTUI
|
||||
*
|
||||
* Validates JSON structure of config files, handles corrupted files
|
||||
* gracefully (falling back to defaults), and provides a single
|
||||
* entry-point to migrate all localStorage data to XDG config files.
|
||||
*/
|
||||
|
||||
import { getConfigFilePath } from "./config-dir"
|
||||
import {
|
||||
migrateAppStateFromLocalStorage,
|
||||
migrateProgressFromLocalStorage,
|
||||
} from "./app-persistence"
|
||||
import {
|
||||
migrateFeedsFromLocalStorage,
|
||||
migrateSourcesFromLocalStorage,
|
||||
} from "./feeds-persistence"
|
||||
|
||||
import { getConfigFilePath } from "./config-dir";
|
||||
// --- Validation helpers ---
|
||||
|
||||
/** Check that a value is a non-null object */
|
||||
function isObject(v: unknown): v is Record<string, unknown> {
|
||||
return v !== null && typeof v === "object" && !Array.isArray(v)
|
||||
return v !== null && typeof v === "object" && !Array.isArray(v);
|
||||
}
|
||||
|
||||
/** Validate AppState JSON structure */
|
||||
export function validateAppState(data: unknown): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = []
|
||||
export function validateAppState(data: unknown): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
if (!isObject(data)) {
|
||||
return { valid: false, errors: ["app-state.json is not an object"] }
|
||||
return { valid: false, errors: ["app-state.json is not an object"] };
|
||||
}
|
||||
|
||||
// settings
|
||||
if (data.settings !== undefined) {
|
||||
if (!isObject(data.settings)) {
|
||||
errors.push("settings must be an object")
|
||||
errors.push("settings must be an object");
|
||||
} else {
|
||||
const s = data.settings as Record<string, unknown>
|
||||
if (s.theme !== undefined && typeof s.theme !== "string") errors.push("settings.theme must be a string")
|
||||
if (s.fontSize !== undefined && typeof s.fontSize !== "number") errors.push("settings.fontSize must be a number")
|
||||
if (s.playbackSpeed !== undefined && typeof s.playbackSpeed !== "number") errors.push("settings.playbackSpeed must be a number")
|
||||
if (s.downloadPath !== undefined && typeof s.downloadPath !== "string") errors.push("settings.downloadPath must be a string")
|
||||
const s = data.settings as Record<string, unknown>;
|
||||
if (s.theme !== undefined && typeof s.theme !== "string")
|
||||
errors.push("settings.theme must be a string");
|
||||
if (s.fontSize !== undefined && typeof s.fontSize !== "number")
|
||||
errors.push("settings.fontSize must be a number");
|
||||
if (s.playbackSpeed !== undefined && typeof s.playbackSpeed !== "number")
|
||||
errors.push("settings.playbackSpeed must be a number");
|
||||
if (s.downloadPath !== undefined && typeof s.downloadPath !== "string")
|
||||
errors.push("settings.downloadPath must be a string");
|
||||
}
|
||||
}
|
||||
|
||||
// preferences
|
||||
if (data.preferences !== undefined) {
|
||||
if (!isObject(data.preferences)) {
|
||||
errors.push("preferences must be an object")
|
||||
errors.push("preferences must be an object");
|
||||
} else {
|
||||
const p = data.preferences as Record<string, unknown>
|
||||
if (p.showExplicit !== undefined && typeof p.showExplicit !== "boolean") errors.push("preferences.showExplicit must be a boolean")
|
||||
if (p.autoDownload !== undefined && typeof p.autoDownload !== "boolean") errors.push("preferences.autoDownload must be a boolean")
|
||||
const p = data.preferences as Record<string, unknown>;
|
||||
if (p.showExplicit !== undefined && typeof p.showExplicit !== "boolean")
|
||||
errors.push("preferences.showExplicit must be a boolean");
|
||||
if (p.autoDownload !== undefined && typeof p.autoDownload !== "boolean")
|
||||
errors.push("preferences.autoDownload must be a boolean");
|
||||
}
|
||||
}
|
||||
|
||||
// customTheme
|
||||
if (data.customTheme !== undefined && !isObject(data.customTheme)) {
|
||||
errors.push("customTheme must be an object")
|
||||
errors.push("customTheme must be an object");
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors }
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
/** Validate feeds JSON structure */
|
||||
export function validateFeeds(data: unknown): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = []
|
||||
export function validateFeeds(data: unknown): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
if (!Array.isArray(data)) {
|
||||
return { valid: false, errors: ["feeds.json is not an array"] }
|
||||
return { valid: false, errors: ["feeds.json is not an array"] };
|
||||
}
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const feed = data[i]
|
||||
const feed = data[i];
|
||||
if (!isObject(feed)) {
|
||||
errors.push(`feeds[${i}] is not an object`)
|
||||
continue
|
||||
errors.push(`feeds[${i}] is not an object`);
|
||||
continue;
|
||||
}
|
||||
if (typeof feed.id !== "string") errors.push(`feeds[${i}].id must be a string`)
|
||||
if (!isObject(feed.podcast)) errors.push(`feeds[${i}].podcast must be an object`)
|
||||
if (!Array.isArray(feed.episodes)) errors.push(`feeds[${i}].episodes must be an array`)
|
||||
if (typeof feed.id !== "string")
|
||||
errors.push(`feeds[${i}].id must be a string`);
|
||||
if (!isObject(feed.podcast))
|
||||
errors.push(`feeds[${i}].podcast must be an object`);
|
||||
if (!Array.isArray(feed.episodes))
|
||||
errors.push(`feeds[${i}].episodes must be an array`);
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors }
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
/** Validate progress JSON structure */
|
||||
export function validateProgress(data: unknown): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = []
|
||||
export function validateProgress(data: unknown): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
if (!isObject(data)) {
|
||||
return { valid: false, errors: ["progress.json is not an object"] }
|
||||
return { valid: false, errors: ["progress.json is not an object"] };
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (!isObject(value)) {
|
||||
errors.push(`progress["${key}"] is not an object`)
|
||||
continue
|
||||
errors.push(`progress["${key}"] is not an object`);
|
||||
continue;
|
||||
}
|
||||
const p = value as Record<string, unknown>
|
||||
if (typeof p.episodeId !== "string") errors.push(`progress["${key}"].episodeId must be a string`)
|
||||
if (typeof p.position !== "number") errors.push(`progress["${key}"].position must be a number`)
|
||||
if (typeof p.duration !== "number") errors.push(`progress["${key}"].duration must be a number`)
|
||||
const p = value as Record<string, unknown>;
|
||||
if (typeof p.episodeId !== "string")
|
||||
errors.push(`progress["${key}"].episodeId must be a string`);
|
||||
if (typeof p.position !== "number")
|
||||
errors.push(`progress["${key}"].position must be a number`);
|
||||
if (typeof p.duration !== "number")
|
||||
errors.push(`progress["${key}"].duration must be a number`);
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors }
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
// --- Safe config file reading ---
|
||||
@@ -115,52 +124,27 @@ export async function safeReadConfigFile<T>(
|
||||
validator: (data: unknown) => { valid: boolean; errors: string[] },
|
||||
): Promise<{ data: T | null; errors: string[] }> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(filename)
|
||||
const file = Bun.file(filePath)
|
||||
const filePath = getConfigFilePath(filename);
|
||||
const file = Bun.file(filePath);
|
||||
if (!(await file.exists())) {
|
||||
return { data: null, errors: [] }
|
||||
return { data: null, errors: [] };
|
||||
}
|
||||
|
||||
const text = await file.text()
|
||||
let parsed: unknown
|
||||
const text = await file.text();
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(text)
|
||||
parsed = JSON.parse(text);
|
||||
} catch {
|
||||
return { data: null, errors: [`${filename}: invalid JSON`] }
|
||||
return { data: null, errors: [`${filename}: invalid JSON`] };
|
||||
}
|
||||
|
||||
const result = validator(parsed)
|
||||
const result = validator(parsed);
|
||||
if (!result.valid) {
|
||||
return { data: null, errors: result.errors }
|
||||
return { data: null, errors: result.errors };
|
||||
}
|
||||
|
||||
return { data: parsed as T, errors: [] }
|
||||
return { data: parsed as T, errors: [] };
|
||||
} catch (err) {
|
||||
return { data: null, errors: [`${filename}: ${String(err)}`] }
|
||||
return { data: null, errors: [`${filename}: ${String(err)}`] };
|
||||
}
|
||||
}
|
||||
|
||||
// --- Unified migration ---
|
||||
|
||||
/**
|
||||
* Run all localStorage -> file migrations.
|
||||
* Safe to call multiple times; each migration is a no-op if the target
|
||||
* file already exists.
|
||||
*
|
||||
* Returns a summary of what was migrated.
|
||||
*/
|
||||
export async function migrateAllFromLocalStorage(): Promise<{
|
||||
appState: boolean
|
||||
progress: boolean
|
||||
feeds: boolean
|
||||
sources: boolean
|
||||
}> {
|
||||
const [appState, progress, feeds, sources] = await Promise.all([
|
||||
migrateAppStateFromLocalStorage(),
|
||||
migrateProgressFromLocalStorage(),
|
||||
migrateFeedsFromLocalStorage(),
|
||||
migrateSourcesFromLocalStorage(),
|
||||
])
|
||||
|
||||
return { appState, progress, feeds, sources }
|
||||
}
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
* Feeds persistence via JSON file in XDG_CONFIG_HOME
|
||||
*
|
||||
* Reads and writes feeds to a JSON file instead of localStorage.
|
||||
* Provides migration from localStorage on first run.
|
||||
*/
|
||||
|
||||
import { ensureConfigDir, getConfigFilePath } from "./config-dir"
|
||||
import { backupConfigFile } from "./config-backup"
|
||||
import type { Feed } from "../types/feed"
|
||||
import { ensureConfigDir, getConfigFilePath } from "./config-dir";
|
||||
import { backupConfigFile } from "./config-backup";
|
||||
import type { Feed } from "../types/feed";
|
||||
|
||||
const FEEDS_FILE = "feeds.json"
|
||||
const SOURCES_FILE = "sources.json"
|
||||
const FEEDS_FILE = "feeds.json";
|
||||
const SOURCES_FILE = "sources.json";
|
||||
|
||||
/** Deserialize date strings back to Date objects in feed data */
|
||||
function reviveDates(feed: Feed): Feed {
|
||||
@@ -25,31 +24,31 @@ function reviveDates(feed: Feed): Feed {
|
||||
...ep,
|
||||
pubDate: new Date(ep.pubDate),
|
||||
})),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Load feeds from JSON file */
|
||||
export async function loadFeedsFromFile(): Promise<Feed[]> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(FEEDS_FILE)
|
||||
const file = Bun.file(filePath)
|
||||
if (!(await file.exists())) return []
|
||||
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)
|
||||
const raw = await file.json();
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.map(reviveDates);
|
||||
} catch {
|
||||
return []
|
||||
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))
|
||||
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
|
||||
}
|
||||
@@ -58,75 +57,26 @@ export async function saveFeedsToFile(feeds: Feed[]): Promise<void> {
|
||||
/** Load sources from JSON file */
|
||||
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 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[]
|
||||
const raw = await file.json();
|
||||
if (!Array.isArray(raw)) return null;
|
||||
return raw as T[];
|
||||
} catch {
|
||||
return null
|
||||
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))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate feeds from localStorage to file.
|
||||
* Only runs once — if the feeds file already exists, it's a no-op.
|
||||
*/
|
||||
export async function migrateFeedsFromLocalStorage(): Promise<boolean> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(FEEDS_FILE)
|
||||
const file = Bun.file(filePath)
|
||||
if (await file.exists()) return false // Already migrated
|
||||
|
||||
if (typeof localStorage === "undefined") return false
|
||||
|
||||
const raw = localStorage.getItem("podtui_feeds")
|
||||
if (!raw) return false
|
||||
|
||||
const feeds = JSON.parse(raw) as Feed[]
|
||||
if (!Array.isArray(feeds) || feeds.length === 0) return false
|
||||
|
||||
await saveFeedsToFile(feeds)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate sources from localStorage to file.
|
||||
*/
|
||||
export async function migrateSourcesFromLocalStorage(): Promise<boolean> {
|
||||
try {
|
||||
const filePath = getConfigFilePath(SOURCES_FILE)
|
||||
const file = Bun.file(filePath)
|
||||
if (await file.exists()) return false
|
||||
|
||||
if (typeof localStorage === "undefined") return false
|
||||
|
||||
const raw = localStorage.getItem("podtui_sources")
|
||||
if (!raw) return false
|
||||
|
||||
const sources = JSON.parse(raw)
|
||||
if (!Array.isArray(sources) || sources.length === 0) return false
|
||||
|
||||
await saveSourcesToFile(sources)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { ansiToRgba } from "./ansi-to-rgba"
|
||||
import { resolveTheme } from "./theme-resolver"
|
||||
import type { ThemeJson } from "../types/theme-schema"
|
||||
|
||||
describe("theme utils", () => {
|
||||
it("converts ansi codes", () => {
|
||||
const color = ansiToRgba(1)
|
||||
expect(color).toBeTruthy()
|
||||
})
|
||||
|
||||
it("resolves simple theme", () => {
|
||||
const json: ThemeJson = {
|
||||
theme: {
|
||||
primary: "#ffffff",
|
||||
secondary: "#000000",
|
||||
accent: "#000000",
|
||||
error: "#000000",
|
||||
warning: "#000000",
|
||||
success: "#000000",
|
||||
info: "#000000",
|
||||
text: "#000000",
|
||||
textMuted: "#000000",
|
||||
background: "#000000",
|
||||
backgroundPanel: "#000000",
|
||||
backgroundElement: "#000000",
|
||||
border: "#000000",
|
||||
borderActive: "#000000",
|
||||
borderSubtle: "#000000",
|
||||
diffAdded: "#000000",
|
||||
diffRemoved: "#000000",
|
||||
diffContext: "#000000",
|
||||
diffHunkHeader: "#000000",
|
||||
diffHighlightAdded: "#000000",
|
||||
diffHighlightRemoved: "#000000",
|
||||
diffAddedBg: "#000000",
|
||||
diffRemovedBg: "#000000",
|
||||
diffContextBg: "#000000",
|
||||
diffLineNumber: "#000000",
|
||||
diffAddedLineNumberBg: "#000000",
|
||||
diffRemovedLineNumberBg: "#000000",
|
||||
markdownText: "#000000",
|
||||
markdownHeading: "#000000",
|
||||
markdownLink: "#000000",
|
||||
markdownLinkText: "#000000",
|
||||
markdownCode: "#000000",
|
||||
markdownBlockQuote: "#000000",
|
||||
markdownEmph: "#000000",
|
||||
markdownStrong: "#000000",
|
||||
markdownHorizontalRule: "#000000",
|
||||
markdownListItem: "#000000",
|
||||
markdownListEnumeration: "#000000",
|
||||
markdownImage: "#000000",
|
||||
markdownImageText: "#000000",
|
||||
markdownCodeBlock: "#000000",
|
||||
syntaxComment: "#000000",
|
||||
syntaxKeyword: "#000000",
|
||||
syntaxFunction: "#000000",
|
||||
syntaxVariable: "#000000",
|
||||
syntaxString: "#000000",
|
||||
syntaxNumber: "#000000",
|
||||
syntaxType: "#000000",
|
||||
syntaxOperator: "#000000",
|
||||
syntaxPunctuation: "#000000",
|
||||
},
|
||||
}
|
||||
|
||||
const resolved = resolveTheme(json, "dark") as unknown as { primary: unknown }
|
||||
expect(resolved.primary).toBeTruthy()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user