diff --git a/components/PasswordGate.tsx b/components/PasswordGate.tsx index bbb2efe..c7a5c51 100644 --- a/components/PasswordGate.tsx +++ b/components/PasswordGate.tsx @@ -52,6 +52,9 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword } 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 @@ -64,6 +67,32 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword } } }; + // Subscribe to settings changes (real-time updates) + useEffect(() => { + // Function to handle updates from the store + const handleSettingsUpdate = () => { + const settings = settingsStore.getSettings(); + const isUnlocked = sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true'; + + // We can't easily check env password synchronously here, but we can check local settings. + // If local setting says lock, and we are not unlocked, we lock. + // If local setting says unlock (and no env password known yet), we unlock. + // To be safe, we might just re-run checkLockStatus() but that's async. + // For immediate UI feedback on "Enable/Disable Password" toggle in settings: + + // If password access is disabled in settings, and we assume no env password for a moment (or rely on previous state): + if (!settings.passwordAccess && !hasEnvPassword) { + setIsLocked(false); + } else if (settings.passwordAccess && !isUnlocked) { + setIsLocked(true); + } + }; + + const unsubscribe = settingsStore.subscribe(handleSettingsUpdate); + return () => unsubscribe(); + }, [hasEnvPassword]); + + const handleUnlock = async (e: React.FormEvent) => { e.preventDefault(); diff --git a/components/player/VideoPlayer.tsx b/components/player/VideoPlayer.tsx index 02f92fe..570d85c 100644 --- a/components/player/VideoPlayer.tsx +++ b/components/player/VideoPlayer.tsx @@ -43,10 +43,20 @@ export function VideoPlayer({ // Get showModeIndicator setting const [showModeIndicator, setShowModeIndicator] = useState(false); + useEffect(() => { + // Initial value setShowModeIndicator(settingsStore.getSettings().showModeIndicator); + + // Subscribe to changes + const unsubscribe = settingsStore.subscribe(() => { + setShowModeIndicator(settingsStore.getSettings().showModeIndicator); + }); + + return () => unsubscribe(); }, []); + // Use reactive hook to subscribe to history updates // This ensures the component re-renders when history is hydrated from localStorage const { viewingHistory, addToHistory } = useHistory(isPremium); diff --git a/lib/hooks/usePremiumContent.ts b/lib/hooks/usePremiumContent.ts index 640320b..c9bbf5d 100644 --- a/lib/hooks/usePremiumContent.ts +++ b/lib/hooks/usePremiumContent.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { useInfiniteScroll } from '@/lib/hooks/useInfiniteScroll'; import { settingsStore } from '@/lib/store/settings-store'; @@ -19,6 +19,9 @@ export function usePremiumContent(categoryValue: string) { const [hasMore, setHasMore] = useState(true); const [page, setPage] = useState(1); + // Track source count to detect meaningful updates + const sourceCountRef = useRef(0); + const loadVideos = useCallback(async (pageNum: number, append = false) => { if (loading) return; @@ -34,6 +37,11 @@ export function usePremiumContent(categoryValue: string) { ...settings.subscriptions.filter(s => (s as any).group === 'premium') ].filter(s => (s as any).enabled !== false); + if (premiumSources.length === 0) { + setLoading(false); + return; + } + // Should we include normal subscriptions too if categoryValue requests them? // The API handles filtering by categoryValue map. // If categoryValue is empty (Recommend), we use all enabled premium sources. @@ -64,13 +72,44 @@ export function usePremiumContent(categoryValue: string) { } }, [loading, categoryValue]); + // Initial load and category change useEffect(() => { setPage(1); setVideos([]); setHasMore(true); + + // Initial check for sources + const settings = settingsStore.getSettings(); + const sourcesCount = settings.premiumSources.length + settings.subscriptions.length; + sourceCountRef.current = sourcesCount; + loadVideos(1, false); }, [categoryValue]); // eslint-disable-line react-hooks/exhaustive-deps + // Subscribe to settings changes to handle async source loading + useEffect(() => { + const handleSettingsUpdate = () => { + const settings = settingsStore.getSettings(); + const premiumSources = [ + ...settings.premiumSources, + ...settings.subscriptions.filter(s => (s as any).group === 'premium') + ].filter(s => (s as any).enabled !== false); + + const currentSourceCount = premiumSources.length; + + // If we have 0 videos and suddenly gain sources, we should retry loading + // OR if the number of sources significantly changed (e.g. from 0 to N) + if (videos.length === 0 && currentSourceCount > 0 && !loading) { + // Determine if we should reload. + // Mostly needed when initial load failed due to no sources. + loadVideos(1, false); + } + }; + + const unsubscribe = settingsStore.subscribe(handleSettingsUpdate); + return () => unsubscribe(); + }, [loadVideos, videos.length, loading]); + const { prefetchRef, loadMoreRef } = useInfiniteScroll({ hasMore, loading, diff --git a/lib/hooks/usePremiumHomePage.ts b/lib/hooks/usePremiumHomePage.ts index 951e842..bbaa777 100644 --- a/lib/hooks/usePremiumHomePage.ts +++ b/lib/hooks/usePremiumHomePage.ts @@ -2,23 +2,24 @@ import { useState, useRef, useEffect, useMemo, 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 } from '@/lib/store/settings-store'; +import { VideoSource } from '@/lib/types'; export function usePremiumHomePage() { + useSubscriptionSync(); const router = useRouter(); const searchParams = useSearchParams(); const { loadFromCache, saveToCache } = useSearchCache(); const hasLoadedCache = useRef(false); + const hasSearchedWithSourcesRef = useRef(false); const [query, setQuery] = useState(''); const [hasSearched, setHasSearched] = useState(false); const [currentSortBy, setCurrentSortBy] = useState('default'); - // Get premium sources from settings store (supports user customization) - const enabledPremiumSources = useMemo(() => { - const settings = settingsStore.getSettings(); - return settings.premiumSources.filter(s => s.enabled); - }, []); + // Use state for sources to trigger re-renders when they update + const [enabledPremiumSources, setEnabledPremiumSources] = useState([]); const onUrlUpdate = useCallback((q: string) => { router.replace(`/premium?q=${encodeURIComponent(q)}`, { scroll: false }); @@ -40,6 +41,19 @@ export function usePremiumHomePage() { onUrlUpdate ); + // Core search execution function - extracted to eliminate duplication + const executeSearch = useCallback((searchQuery: string, sources: VideoSource[]) => { + if (!searchQuery.trim()) return false; + + if (sources.length === 0) { + return false; + } + + performSearch(searchQuery, sources, currentSortBy as any); + hasSearchedWithSourcesRef.current = true; + return true; + }, [performSearch, currentSortBy]); + // Re-sort results when sort preference changes useEffect(() => { if (hasSearched && results.length > 0) { @@ -47,34 +61,71 @@ export function usePremiumHomePage() { } }, [currentSortBy, applySorting, hasSearched, results.length]); + // Load sources and subscribe to changes + useEffect(() => { + const updateSettings = () => { + const settings = settingsStore.getSettings(); + + // Update sort preference if changed + // Note: settings.sortBy might be shared or we might want specific one, + // but for now we follow the store or keep local state if we want independence. + // The original hook had local state 'default'. + + const newPremiumSources = settings.premiumSources.filter(s => s.enabled); + setEnabledPremiumSources(newPremiumSources); + + // Check if we need to re-trigger search due to new sources being loaded + const hasSources = newPremiumSources.length > 0; + + // If we have a query, and we haven't searched with sources yet, + // and we suddenly have sources, trigger the search. + if (query && hasSources && !hasSearchedWithSourcesRef.current && !loading) { + if (executeSearch(query, newPremiumSources)) { + setHasSearched(true); + } + } + }; + + // Initial load + updateSettings(); + + // Subscribe to changes + const unsubscribe = settingsStore.subscribe(updateSettings); + return () => unsubscribe(); + }, [query, loading, executeSearch]); + // Load cached results on mount useEffect(() => { if (hasLoadedCache.current) return; hasLoadedCache.current = true; const urlQuery = searchParams.get('q'); - // Note: We might want to separate cache for premium mode, but for now sharing or not using cache might be safer. - // However, useSearchCache uses localStorage which is shared. - // If we want to avoid leaking premium searches to normal history, we might want to disable cache or use a different key. - // For simplicity and "hidden" nature, maybe we don't load cache from normal mode? - // But the user asked for "same as original page". if (urlQuery) { setQuery(urlQuery); - handleSearch(urlQuery); + // We need to wait for sources to be available, which is handled by the subscription effect + // But if sources are already available (e.g. navigation), execute immediately + const currentSettings = settingsStore.getSettings(); + const currentSources = currentSettings.premiumSources.filter(s => s.enabled); + + if (currentSources.length > 0) { + handleSearch(urlQuery); + } + // If no sources yet, the useEffect above will catch it when they load } }, [searchParams]); const handleSearch = (searchQuery: string) => { setQuery(searchQuery); setHasSearched(true); - // Use enabled premium sources from settings - performSearch(searchQuery, enabledPremiumSources, currentSortBy as any); + // Use current state of sources + executeSearch(searchQuery, enabledPremiumSources); }; const handleReset = () => { setHasSearched(false); setQuery(''); + hasSearchedWithSourcesRef.current = false; resetSearch(); router.replace('/premium', { scroll: false }); }; diff --git a/package-lock.json b/package-lock.json index b760efc..b155a3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kvideo", - "version": "3.9.3", + "version": "3.9.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kvideo", - "version": "3.9.3", + "version": "3.9.4", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/package.json b/package.json index 4f9be0b..a1189d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kvideo", - "version": "3.9.3", + "version": "3.9.4", "private": true, "scripts": { "dev": "next dev",