From 04aa734ff8385dd62198ce90775a0eecfe364b11 Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Tue, 13 Jan 2026 16:32:01 +0800 Subject: [PATCH] Update --- components/PasswordGate.tsx | 85 ++++++++++++++++---------------- lib/hooks/useHomePage.ts | 2 - lib/hooks/useSubscriptionSync.ts | 46 ++++++++++++----- lib/hooks/useVideoPlayer.ts | 33 +++++++++++++ 4 files changed, 108 insertions(+), 58 deletions(-) diff --git a/components/PasswordGate.tsx b/components/PasswordGate.tsx index c7a5c51..7e8d28a 100644 --- a/components/PasswordGate.tsx +++ b/components/PasswordGate.tsx @@ -2,11 +2,15 @@ import { useState, useEffect } from 'react'; import { settingsStore } from '@/lib/store/settings-store'; +import { useSubscriptionSync } from '@/lib/hooks/useSubscriptionSync'; import { Lock } from 'lucide-react'; const SESSION_UNLOCKED_KEY = 'kvideo-unlocked'; export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword }: { children: React.ReactNode, hasEnvPassword: boolean }) { + // Enable background subscription syncing globally + useSubscriptionSync(); + const [isLocked, setIsLocked] = useState(true); const [password, setPassword] = useState(''); const [error, setError] = useState(false); @@ -15,57 +19,52 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword } const [isValidating, setIsValidating] = useState(false); useEffect(() => { - const settings = settingsStore.getSettings(); - const isUnlocked = sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true'; + let mounted = true; - // Determine initial lock state immediately - // Lock if (local password enabled OR env password exists) AND not already unlocked in session - const shouldBeLocked = (settings.passwordAccess || initialHasEnvPassword) && !isUnlocked; + const init = async () => { + const settings = settingsStore.getSettings(); + const isUnlocked = sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true'; - setIsLocked(shouldBeLocked); - setIsClient(true); + // 1. Initial local check (fast) + const localLocked = (settings.passwordAccess || initialHasEnvPassword) && !isUnlocked; + if (mounted) setIsLocked(localLocked); + if (mounted) setIsClient(true); - // Still run background checks to keep everything in sync - checkEnvPasswordStatus(); - checkLockStatus(); - }, [initialHasEnvPassword]); + // 2. Fetch remote config & sync + try { + const res = await fetch('/api/config'); + if (!res.ok) throw new Error('Failed to fetch config'); - const checkEnvPasswordStatus = async () => { - try { - const res = await fetch('/api/config'); - const data = await res.json(); - setHasEnvPassword(data.hasEnvPassword); + const data = await res.json(); - // Sync subscription sources if provided by environment - if (data.subscriptionSources) { - settingsStore.syncEnvSubscriptions(data.subscriptionSources); + if (mounted) { + setHasEnvPassword(data.hasEnvPassword); + + // CRITICAL: Sync subscriptions immediately + if (data.subscriptionSources) { + console.log('Syncing env subscriptions:', data.subscriptionSources); + settingsStore.syncEnvSubscriptions(data.subscriptionSources); + } + + // Re-evaluate lock status with confirmed server state + // We only care about envPassword if we are not unlocked. + // Access control logic: + // Locked IF: (Local setting ON OR Env Password Exists) AND (Not Unlocked) + const confirmLocked = (settings.passwordAccess || data.hasEnvPassword) && !isUnlocked; + setIsLocked(confirmLocked); + } + } catch (e) { + console.error("PasswordGate init failed:", e); + // Fallback: rely on initial/local state which was already set } - } catch { - // Silently fail - } - }; + }; - const checkLockStatus = async () => { - const settings = settingsStore.getSettings(); - const isUnlocked = sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true'; + init(); - try { - const res = await fetch('/api/config'); - const data = await res.json(); - // Note: envPasswordSet is not directly used for immediate locking if we want to rely on real-time settings - // But we should respect the server config. - // However, for the subscription fix, we mainly care about 'settings.passwordAccess' updating in real-time. - const envPasswordSet = data.hasEnvPassword; - - // Updated lock state check - const currentlyLocked = (settings.passwordAccess || envPasswordSet) && !isUnlocked; - setIsLocked(currentlyLocked); - } catch { - // Handle error by checking local settings - const currentlyLocked = settings.passwordAccess && !isUnlocked; - setIsLocked(currentlyLocked); - } - }; + return () => { + mounted = false; + }; + }, [initialHasEnvPassword]); // Subscribe to settings changes (real-time updates) useEffect(() => { diff --git a/lib/hooks/useHomePage.ts b/lib/hooks/useHomePage.ts index 7a639a3..b987a1a 100644 --- a/lib/hooks/useHomePage.ts +++ b/lib/hooks/useHomePage.ts @@ -2,11 +2,9 @@ import { useState, useRef, useEffect, useCallback } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useSearchCache } from '@/lib/hooks/useSearchCache'; import { useParallelSearch } from '@/lib/hooks/useParallelSearch'; -import { useSubscriptionSync } from '@/lib/hooks/useSubscriptionSync'; import { settingsStore, type SortOption } from '@/lib/store/settings-store'; export function useHomePage() { - useSubscriptionSync(); const router = useRouter(); const searchParams = useSearchParams(); const { loadFromCache, saveToCache } = useSearchCache(); diff --git a/lib/hooks/useSubscriptionSync.ts b/lib/hooks/useSubscriptionSync.ts index 3bfdfd5..abc7dbd 100644 --- a/lib/hooks/useSubscriptionSync.ts +++ b/lib/hooks/useSubscriptionSync.ts @@ -1,27 +1,45 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { settingsStore } from '@/lib/store/settings-store'; import { fetchSourcesFromUrl, mergeSources } from '@/lib/utils/source-import-utils'; export function useSubscriptionSync() { - const hasSyncedRef = useRef(false); + const [subscriptions, setSubscriptions] = useState(() => settingsStore.getSettings().subscriptions); + // Subscribe to settings changes to detect when subscriptions are updated (e.g. from PasswordGate env sync) useEffect(() => { - if (hasSyncedRef.current) return; - hasSyncedRef.current = true; + const unsubscribe = settingsStore.subscribe(() => { + const currentSubs = settingsStore.getSettings().subscriptions; + setSubscriptions(currentSubs); + }); + return () => unsubscribe(); + }, []); + // Effect to run the sync when subscriptions change + useEffect(() => { const sync = async () => { + const activeSubscriptions = subscriptions.filter((s: any) => s.autoRefresh !== false); + if (activeSubscriptions.length === 0) return; + + // We need to check if we actually need to sync. + // If we just synced, or if nothing changed, maybe skip? + // For now, let's rely on a simplified approach: + // If the subscription list length/content changes, we might want to re-sync. + // But be careful of infinite loops if we update sources inside this effect. + const settings = settingsStore.getSettings(); - const subscriptions = settings.subscriptions.filter(s => s.autoRefresh !== false); - - if (subscriptions.length === 0) return; - let anyChanged = false; let currentSources = [...settings.sources]; let currentPremiumSources = [...settings.premiumSources]; - let updatedSubscriptions = [...settings.subscriptions]; + // We use a local copy of subscriptions to avoid re-triggering this effect when we update 'lastUpdated' + let updatedSubscriptions = [...subscriptions]; + + for (let i = 0; i < activeSubscriptions.length; i++) { + const sub = activeSubscriptions[i]; + + // Optional: Check if we synced this recently (e.g. within 5 minutes) to avoid spamming on hot-reload/nav + // const now = Date.now(); + // if (sub.lastUpdated && now - sub.lastUpdated < 5 * 60 * 1000) continue; - for (let i = 0; i < subscriptions.length; i++) { - const sub = subscriptions[i]; try { const result = await fetchSourcesFromUrl(sub.url); @@ -58,6 +76,8 @@ export function useSubscriptionSync() { } }; - sync(); - }, []); + // Debounce slightly to avoid rapid-fire updates if multiple settings change + const timeoutId = setTimeout(sync, 1000); + return () => clearTimeout(timeoutId); + }, [subscriptions]); // Only re-run if subscriptions array reference changes (which happens on saveSettings) } diff --git a/lib/hooks/useVideoPlayer.ts b/lib/hooks/useVideoPlayer.ts index 6464007..648ac83 100644 --- a/lib/hooks/useVideoPlayer.ts +++ b/lib/hooks/useVideoPlayer.ts @@ -54,10 +54,16 @@ export function useVideoPlayer( isReversedRef.current = isReversed; }, [isReversed]); + + const fetchVideoDetails = useCallback(async () => { if (!videoId || !source) return; try { + // Don't clear error immediately if we are just retrying silently, + // but for manual retry or initial load we should. + // Let's clear it to show loading state if we want, or keep it. + // Standard behavior: clear error and show loading. setVideoError(''); setLoading(true); @@ -121,6 +127,33 @@ export function useVideoPlayer( } }, [videoId, source]); + // EFFECT: Retry logic when settings change (e.g., sources loaded from subscriptions) + useEffect(() => { + if (!videoId || !source || !videoError) return; + + const unsubscribe = settingsStore.subscribe(() => { + // If we are currently in an error state (likely "Invalid source configuration"), + // and settings updated (likely new sources arrived), try fetching again. + // We can be smarter: check if the source ID now exists in the store. + const settings = settingsStore.getSettings(); + const allSources = [ + ...settings.sources, + ...settings.premiumSources, + ...settings.subscriptions, // note: subscription items aren't usually video sources directly but let's check broadly + ]; + + // We really need to check if the specific source ID is now available + // But since 'subscriptions' in store expands into 'sources'/'premiumSources', + // we just check if any sources exist now. + if (allSources.length > 0) { + console.log("Settings updated, retrying video fetch..."); + fetchVideoDetails(); + } + }); + + return () => unsubscribe(); + }, [videoId, source, videoError, fetchVideoDetails]); + // Sync state from params if they change externally (e.g. back/forward navigation) useEffect(() => { if (videoData?.episodes && episodeParam !== null) {