drop oauth plan (copy config)

This commit is contained in:
2026-08-08 07:10:59 -04:00
parent 13a31aabdc
commit 3ef19f80b8
20 changed files with 530 additions and 1592 deletions

View File

@@ -55,7 +55,7 @@ export function createAppStore() {
init();
const saveState = (next: AppState) => {
saveAppStateToFile(next).catch(() => {});
saveAppStateToFile(next);
};
const updateState = (next: AppState) => {

View File

@@ -5,122 +5,122 @@
import { createSignal } from "solid-js";
import {
loadAudioNavFromFile,
saveAudioNavToFile,
loadAudioNavFromFile,
saveAudioNavToFile,
} from "../utils/app-persistence";
/** Source type for audio navigation */
export enum AudioSource {
FEED = "feed",
MY_SHOWS = "my_shows",
SEARCH = "search",
FEED = "feed",
MY_SHOWS = "my_shows",
SEARCH = "search",
}
/** Audio navigation state */
export interface AudioNavState {
/** Current source type */
source: AudioSource;
/** Index of current episode in the ordered list */
currentIndex: number;
/** Podcast ID for My Shows source */
podcastId?: string;
/** Timestamp when navigation state was last saved */
lastUpdated: Date;
/** Current source type */
source: AudioSource;
/** Index of current episode in the ordered list */
currentIndex: number;
/** Podcast ID for My Shows source */
podcastId?: string;
/** Timestamp when navigation state was last saved */
lastUpdated: Date;
}
/** Default navigation state */
const defaultNavState: AudioNavState = {
source: AudioSource.FEED,
currentIndex: 0,
lastUpdated: new Date(),
source: AudioSource.FEED,
currentIndex: 0,
lastUpdated: new Date(),
};
/** Create audio navigation store */
export function createAudioNavStore() {
const [navState, setNavState] = createSignal<AudioNavState>(defaultNavState);
const [navState, setNavState] = createSignal<AudioNavState>(defaultNavState);
/** Persist current navigation state to file (fire-and-forget) */
function persist(): void {
saveAudioNavToFile(navState()).catch(() => {});
}
/** Persist current navigation state to file (fire-and-forget) */
function persist(): void {
saveAudioNavToFile(navState());
}
/** Load navigation state from file */
async function init(): Promise<void> {
const loaded = await loadAudioNavFromFile<AudioNavState>();
if (loaded) {
setNavState(loaded);
}
}
/** Load navigation state from file */
async function init(): Promise<void> {
const loaded = await loadAudioNavFromFile<AudioNavState>();
if (loaded) {
setNavState(loaded);
}
}
/** Fire-and-forget initialization */
init();
/** Fire-and-forget initialization */
init();
return {
/** Get current navigation state */
get state(): AudioNavState {
return navState();
},
return {
/** Get current navigation state */
get state(): AudioNavState {
return navState();
},
/** Update source type */
setSource: (source: AudioSource, podcastId?: string) => {
setNavState((prev) => ({
...prev,
source,
podcastId,
lastUpdated: new Date(),
}));
persist();
},
/** Update source type */
setSource: (source: AudioSource, podcastId?: string) => {
setNavState((prev) => ({
...prev,
source,
podcastId,
lastUpdated: new Date(),
}));
persist();
},
/** Move to next episode */
next: (currentIndex: number) => {
setNavState((prev) => ({
...prev,
currentIndex,
lastUpdated: new Date(),
}));
persist();
},
/** Move to next episode */
next: (currentIndex: number) => {
setNavState((prev) => ({
...prev,
currentIndex,
lastUpdated: new Date(),
}));
persist();
},
/** Move to previous episode */
prev: (currentIndex: number) => {
setNavState((prev) => ({
...prev,
currentIndex,
lastUpdated: new Date(),
}));
persist();
},
/** Move to previous episode */
prev: (currentIndex: number) => {
setNavState((prev) => ({
...prev,
currentIndex,
lastUpdated: new Date(),
}));
persist();
},
/** Reset to default state */
reset: () => {
setNavState(defaultNavState);
persist();
},
/** Reset to default state */
reset: () => {
setNavState(defaultNavState);
persist();
},
/** Get current index */
getCurrentIndex: (): number => {
return navState().currentIndex;
},
/** Get current index */
getCurrentIndex: (): number => {
return navState().currentIndex;
},
/** Get current source */
getSource: (): AudioSource => {
return navState().source;
},
/** Get current source */
getSource: (): AudioSource => {
return navState().source;
},
/** Get current podcast ID */
getPodcastId: (): string | undefined => {
return navState().podcastId;
},
};
/** Get current podcast ID */
getPodcastId: (): string | undefined => {
return navState().podcastId;
},
};
}
/** Singleton instance */
let audioNavInstance: ReturnType<typeof createAudioNavStore> | null = null;
export function useAudioNavStore() {
if (!audioNavInstance) {
audioNavInstance = createAudioNavStore();
}
return audioNavInstance;
if (!audioNavInstance) {
audioNavInstance = createAudioNavStore();
}
return audioNavInstance;
}

View File

@@ -1,244 +0,0 @@
/**
* Authentication store for PodTUI
* Uses Zustand for state management with localStorage persistence
* Authentication is DISABLED by default
*/
import { createSignal } from "solid-js"
import type {
User,
AuthState,
AuthError,
AuthErrorCode,
LoginCredentials,
AuthScreen,
} from "../types/auth"
import { AUTH_CONFIG, DEFAULT_AUTH_ENABLED } from "../config/auth"
/** Initial auth state */
const initialState: AuthState = {
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
}
/** Load auth state from localStorage */
function loadAuthState(): AuthState {
if (typeof localStorage === "undefined") {
return initialState
}
try {
const stored = localStorage.getItem(AUTH_CONFIG.storage.authState)
if (stored) {
const parsed = JSON.parse(stored)
// Convert date strings back to Date objects
if (parsed.user?.createdAt) {
parsed.user.createdAt = new Date(parsed.user.createdAt)
}
if (parsed.user?.lastLoginAt) {
parsed.user.lastLoginAt = new Date(parsed.user.lastLoginAt)
}
return parsed
}
} catch {
// Ignore parse errors, use initial state
}
return initialState
}
/** Save auth state to localStorage */
function saveAuthState(state: AuthState): void {
if (typeof localStorage === "undefined") {
return
}
try {
localStorage.setItem(AUTH_CONFIG.storage.authState, JSON.stringify(state))
} catch {
// Ignore storage errors
}
}
/** Create auth store using Solid signals */
export function createAuthStore() {
const [state, setState] = createSignal<AuthState>(loadAuthState())
const [authEnabled, setAuthEnabled] = createSignal(DEFAULT_AUTH_ENABLED)
const [currentScreen, setCurrentScreen] = createSignal<AuthScreen>("login")
/** Update state and persist */
const updateState = (updates: Partial<AuthState>) => {
setState((prev) => {
const next = { ...prev, ...updates }
saveAuthState(next)
return next
})
}
/** Login with email/password (placeholder - no real backend) */
const login = async (credentials: LoginCredentials): Promise<boolean> => {
updateState({ isLoading: true, error: null })
// Simulate network delay
await new Promise((r) => setTimeout(r, 500))
// Validate email format
if (!AUTH_CONFIG.email.pattern.test(credentials.email)) {
updateState({
isLoading: false,
error: {
code: "INVALID_CREDENTIALS" as AuthErrorCode,
message: "Invalid email format",
},
})
return false
}
// Validate password length
if (credentials.password.length < AUTH_CONFIG.password.minLength) {
updateState({
isLoading: false,
error: {
code: "INVALID_CREDENTIALS" as AuthErrorCode,
message: `Password must be at least ${AUTH_CONFIG.password.minLength} characters`,
},
})
return false
}
// Create mock user (in real app, this would validate against backend)
const user: User = {
id: crypto.randomUUID(),
email: credentials.email,
name: credentials.email.split("@")[0],
createdAt: new Date(),
lastLoginAt: new Date(),
syncEnabled: true,
}
updateState({
user,
isAuthenticated: true,
isLoading: false,
error: null,
})
return true
}
/** Logout and clear state */
const logout = () => {
updateState({
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
})
setCurrentScreen("login")
}
/** Validate 8-character code */
const validateCode = async (code: string): Promise<boolean> => {
updateState({ isLoading: true, error: null })
// Simulate network delay
await new Promise((r) => setTimeout(r, 500))
const normalizedCode = code.toUpperCase().replace(/[^A-Z0-9]/g, "")
// Check code length
if (normalizedCode.length !== AUTH_CONFIG.codeValidation.codeLength) {
updateState({
isLoading: false,
error: {
code: "INVALID_CODE" as AuthErrorCode,
message: `Code must be ${AUTH_CONFIG.codeValidation.codeLength} characters`,
},
})
return false
}
// Check code format
if (!AUTH_CONFIG.codeValidation.allowedChars.test(normalizedCode)) {
updateState({
isLoading: false,
error: {
code: "INVALID_CODE" as AuthErrorCode,
message: "Code must contain only letters and numbers",
},
})
return false
}
// Mock successful code validation
const user: User = {
id: crypto.randomUUID(),
email: `sync-${normalizedCode.toLowerCase()}@podtui.local`,
name: `Sync User (${normalizedCode.slice(0, 4)})`,
createdAt: new Date(),
lastLoginAt: new Date(),
syncEnabled: true,
}
updateState({
user,
isAuthenticated: true,
isLoading: false,
error: null,
})
return true
}
/** Clear error */
const clearError = () => {
updateState({ error: null })
}
/** Enable/disable auth */
const toggleAuthEnabled = () => {
setAuthEnabled((prev) => !prev)
}
return {
// State accessors (signals)
state,
authEnabled,
currentScreen,
// Actions
login,
logout,
validateCode,
clearError,
setCurrentScreen,
toggleAuthEnabled,
// Computed
get user() {
return state().user
},
get isAuthenticated() {
return state().isAuthenticated
},
get isLoading() {
return state().isLoading
},
get error() {
return state().error
},
}
}
/** Singleton auth store instance */
let authStoreInstance: ReturnType<typeof createAuthStore> | null = null
/** Get or create auth store */
export function useAuthStore() {
if (!authStoreInstance) {
authStoreInstance = createAuthStore()
}
return authStoreInstance
}

View File

@@ -12,7 +12,6 @@ import type { DownloadedEpisode } from "../types/episode";
import type { Episode } from "../types/episode";
import { downloadEpisode } from "../utils/episode-downloader";
import { ensureConfigDir, getConfigFilePath } from "../utils/config-dir";
import { backupConfigFile } from "../utils/config-backup";
const DOWNLOADS_FILE = "downloads.json";
const MAX_CONCURRENT = 2;
@@ -94,7 +93,6 @@ export function createDownloadStore() {
async function saveDownloads(): Promise<void> {
try {
await ensureConfigDir();
await backupConfigFile(DOWNLOADS_FILE);
const map = downloads();
const records: DownloadRecord[] = [];
for (const [, dl] of map) {

View File

@@ -19,7 +19,6 @@ import {
} from "../utils/feeds-persistence";
import { useDownloadStore } from "./download";
import { DownloadStatus } from "../types/episode";
import { useAuthStore } from "./auth";
/** Max episodes to load per page/chunk */
const MAX_EPISODES_REFRESH = 50;
@@ -35,12 +34,12 @@ const episodeLoadCount = new Map<string, number>();
/** Save feeds to file (async, fire-and-forget) */
function saveFeeds(feeds: Feed[]): void {
saveFeedsToFile(feeds).catch(() => {});
saveFeedsToFile(feeds);
}
/** Save sources to file (async, fire-and-forget) */
function saveSources(sources: PodcastSource[]): void {
saveSourcesToFile(sources).catch(() => {});
saveSourcesToFile(sources);
}
/** Create feed store */
@@ -62,18 +61,10 @@ export function createFeedStore() {
const getFilteredFeeds = (): Feed[] => {
let result = [...feeds()];
const f = filter();
const authStore = useAuthStore();
// Filter by visibility
if (f.visibility && f.visibility !== "all") {
result = result.filter((feed) => feed.visibility === f.visibility);
} else if (f.visibility === "all") {
// Only show private feeds if authenticated
result = result.filter(
(feed) =>
feed.visibility === FeedVisibility.PUBLIC ||
authStore.isAuthenticated,
);
}
// Filter by source

View File

@@ -8,8 +8,8 @@
import { createSignal } from "solid-js";
import type { Progress } from "../types/episode";
import {
loadProgressFromFile,
saveProgressToFile,
loadProgressFromFile,
saveProgressToFile,
} from "../utils/app-persistence";
/** Threshold (fraction 0-1) at which an episode is considered completed */
@@ -21,146 +21,146 @@ const MIN_POSITION_TO_SAVE = 5;
// --- Singleton store ---
const [progressMap, setProgressMap] = createSignal<Record<string, Progress>>(
{},
{},
);
/** Persist current progress map to file (fire-and-forget) */
function persist(): void {
saveProgressToFile(progressMap()).catch(() => {});
saveProgressToFile(progressMap());
}
/** Parse raw progress entries from file, reviving Date objects */
function parseProgressEntries(
raw: Record<string, unknown>,
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;
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 function initProgress(): Promise<void> {
const raw = await loadProgressFromFile();
const parsed = parseProgressEntries(raw as Record<string, unknown>);
setProgressMap(parsed);
const raw = await loadProgressFromFile();
const parsed = parseProgressEntries(raw as Record<string, unknown>);
setProgressMap(parsed);
}
// Fire-and-forget init
initProgress();
function createProgressStore() {
return {
/**
* Get progress for a specific episode.
*/
get(episodeId: string): Progress | undefined {
return progressMap()[episodeId];
},
return {
/**
* Get progress for a specific episode.
*/
get(episodeId: string): Progress | undefined {
return progressMap()[episodeId];
},
/**
* Get all progress entries.
*/
all(): Record<string, Progress> {
return progressMap();
},
/**
* Get all progress entries.
*/
all(): Record<string, Progress> {
return progressMap();
},
/**
* Update progress for an episode. Only persists if position is meaningful.
*/
update(
episodeId: string,
position: number,
duration: number,
playbackSpeed?: number,
): void {
if (position < MIN_POSITION_TO_SAVE && duration > 0) return;
/**
* Update progress for an episode. Only persists if position is meaningful.
*/
update(
episodeId: string,
position: number,
duration: number,
playbackSpeed?: number,
): void {
if (position < MIN_POSITION_TO_SAVE && duration > 0) return;
setProgressMap((prev) => ({
...prev,
[episodeId]: {
episodeId,
position,
duration,
timestamp: new Date(),
playbackSpeed,
},
}));
persist();
},
setProgressMap((prev) => ({
...prev,
[episodeId]: {
episodeId,
position,
duration,
timestamp: new Date(),
playbackSpeed,
},
}));
persist();
},
/**
* Check if an episode is completed.
*/
isCompleted(episodeId: string): boolean {
const p = progressMap()[episodeId];
if (!p || p.duration <= 0) return false;
return p.position / p.duration >= COMPLETION_THRESHOLD;
},
/**
* Check if an episode is completed.
*/
isCompleted(episodeId: string): boolean {
const p = progressMap()[episodeId];
if (!p || p.duration <= 0) return false;
return p.position / p.duration >= COMPLETION_THRESHOLD;
},
/**
* Get progress percentage (0-100) for an episode.
*/
getPercent(episodeId: string): number {
const p = progressMap()[episodeId];
if (!p || p.duration <= 0) return 0;
return Math.min(100, Math.round((p.position / p.duration) * 100));
},
/**
* Get progress percentage (0-100) for an episode.
*/
getPercent(episodeId: string): number {
const p = progressMap()[episodeId];
if (!p || p.duration <= 0) return 0;
return Math.min(100, Math.round((p.position / p.duration) * 100));
},
/**
* Mark an episode as completed (set position to duration).
*/
markCompleted(episodeId: string): void {
const p = progressMap()[episodeId];
const duration = p?.duration ?? 0;
setProgressMap((prev) => ({
...prev,
[episodeId]: {
episodeId,
position: duration,
duration,
timestamp: new Date(),
playbackSpeed: p?.playbackSpeed,
},
}));
persist();
},
/**
* Mark an episode as completed (set position to duration).
*/
markCompleted(episodeId: string): void {
const p = progressMap()[episodeId];
const duration = p?.duration ?? 0;
setProgressMap((prev) => ({
...prev,
[episodeId]: {
episodeId,
position: duration,
duration,
timestamp: new Date(),
playbackSpeed: p?.playbackSpeed,
},
}));
persist();
},
/**
* Remove progress for an episode (e.g. "mark as new").
*/
remove(episodeId: string): void {
setProgressMap((prev) => {
const next = { ...prev };
delete next[episodeId];
return next;
});
persist();
},
/**
* Remove progress for an episode (e.g. "mark as new").
*/
remove(episodeId: string): void {
setProgressMap((prev) => {
const next = { ...prev };
delete next[episodeId];
return next;
});
persist();
},
/**
* Clear all progress data.
*/
clear(): void {
setProgressMap({});
persist();
},
};
/**
* Clear all progress data.
*/
clear(): void {
setProgressMap({});
persist();
},
};
}
// Singleton instance
let instance: ReturnType<typeof createProgressStore> | null = null;
export function useProgressStore() {
if (!instance) {
instance = createProgressStore();
}
return instance;
if (!instance) {
instance = createProgressStore();
}
return instance;
}