drop oauth plan (copy config)
This commit is contained in:
@@ -55,7 +55,7 @@ export function createAppStore() {
|
||||
init();
|
||||
|
||||
const saveState = (next: AppState) => {
|
||||
saveAppStateToFile(next).catch(() => {});
|
||||
saveAppStateToFile(next);
|
||||
};
|
||||
|
||||
const updateState = (next: AppState) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user