This commit is contained in:
kuekhaoyang
2026-01-13 16:32:01 +08:00
parent a93f96c967
commit 04aa734ff8
4 changed files with 108 additions and 58 deletions
+42 -43
View File
@@ -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(() => {
-2
View File
@@ -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();
+33 -13
View File
@@ -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)
}
+33
View File
@@ -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) {