This commit is contained in:
kuekhaoyang
2026-01-13 16:04:13 +08:00
parent 1cf4a6b509
commit b11e762350
6 changed files with 146 additions and 17 deletions
+29
View File
@@ -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();
+10
View File
@@ -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);
+40 -1
View File
@@ -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,
+64 -13
View File
@@ -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<VideoSource[]>([]);
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 });
};
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "kvideo",
"version": "3.9.3",
"version": "3.9.4",
"private": true,
"scripts": {
"dev": "next dev",