mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
feat: Implement scroll position management with a new setting, add a back-to-top button, and extend search cache duration.
This commit is contained in:
@@ -8,6 +8,8 @@ import { ServiceWorkerRegister } from "@/components/ServiceWorkerRegister";
|
||||
import { PasswordGate } from "@/components/PasswordGate";
|
||||
import { siteConfig } from "@/lib/config/site-config";
|
||||
import { AdKeywordsInjector } from "@/components/AdKeywordsInjector";
|
||||
import { BackToTop } from "@/components/ui/BackToTop";
|
||||
import { ScrollPositionManager } from "@/components/ScrollPositionManager";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
@@ -83,6 +85,8 @@ export default function RootLayout({
|
||||
<PasswordGate hasEnvPassword={!!process.env.ACCESS_PASSWORD}>
|
||||
<AdKeywordsWrapper />
|
||||
{children}
|
||||
<BackToTop />
|
||||
<ScrollPositionManager />
|
||||
</PasswordGate>
|
||||
<Analytics />
|
||||
<ServiceWorkerRegister />
|
||||
|
||||
+14
-1
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { Suspense, useMemo } from 'react';
|
||||
import { SearchForm } from '@/components/search/SearchForm';
|
||||
import { NoResults } from '@/components/search/NoResults';
|
||||
import { PopularFeatures } from '@/components/home/PopularFeatures';
|
||||
@@ -9,6 +9,7 @@ import { FavoritesSidebar } from '@/components/favorites/FavoritesSidebar';
|
||||
import { Navbar } from '@/components/layout/Navbar';
|
||||
import { SearchResults } from '@/components/home/SearchResults';
|
||||
import { useHomePage } from '@/lib/hooks/useHomePage';
|
||||
import { useLatencyPing } from '@/lib/hooks/useLatencyPing';
|
||||
|
||||
function HomePage() {
|
||||
const {
|
||||
@@ -23,6 +24,17 @@ function HomePage() {
|
||||
handleReset,
|
||||
} = useHomePage();
|
||||
|
||||
// Real-time latency pinging
|
||||
const sourceUrls = useMemo(() =>
|
||||
availableSources.map(s => ({ id: s.id, baseUrl: s.id })), // Using id as baseUrl if not available elsewhere
|
||||
[availableSources]
|
||||
);
|
||||
|
||||
const { latencies } = useLatencyPing({
|
||||
sourceUrls,
|
||||
enabled: hasSearched && results.length > 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Glass Navbar */}
|
||||
@@ -52,6 +64,7 @@ function HomePage() {
|
||||
results={results}
|
||||
availableSources={availableSources}
|
||||
loading={loading}
|
||||
latencies={latencies}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export function useSettingsPage() {
|
||||
const [searchDisplayMode, setSearchDisplayMode] = useState<SearchDisplayMode>('normal');
|
||||
const [fullscreenType, setFullscreenType] = useState<'native' | 'window'>('native');
|
||||
const [proxyMode, setProxyMode] = useState<ProxyMode>('retry');
|
||||
const [rememberScrollPosition, setRememberScrollPosition] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const settings = settingsStore.getSettings();
|
||||
@@ -40,6 +41,7 @@ export function useSettingsPage() {
|
||||
setSearchDisplayMode(settings.searchDisplayMode);
|
||||
setFullscreenType(settings.fullscreenType);
|
||||
setProxyMode(settings.proxyMode);
|
||||
setRememberScrollPosition(settings.rememberScrollPosition);
|
||||
|
||||
// Fetch env password status
|
||||
fetch('/api/config')
|
||||
@@ -301,6 +303,15 @@ export function useSettingsPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleRememberScrollPositionChange = (enabled: boolean) => {
|
||||
setRememberScrollPosition(enabled);
|
||||
const currentSettings = settingsStore.getSettings();
|
||||
settingsStore.saveSettings({
|
||||
...currentSettings,
|
||||
rememberScrollPosition: enabled,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRestoreDefaults = () => {
|
||||
const defaults = getDefaultSources();
|
||||
handleSourcesChange(defaults);
|
||||
@@ -355,5 +366,7 @@ export function useSettingsPage() {
|
||||
handleFullscreenTypeChange,
|
||||
proxyMode,
|
||||
handleProxyModeChange,
|
||||
rememberScrollPosition,
|
||||
handleRememberScrollPositionChange,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ export default function SettingsPage() {
|
||||
handleFullscreenTypeChange,
|
||||
proxyMode,
|
||||
handleProxyModeChange,
|
||||
rememberScrollPosition,
|
||||
handleRememberScrollPositionChange,
|
||||
} = useSettingsPage();
|
||||
|
||||
return (
|
||||
@@ -87,8 +89,10 @@ export default function SettingsPage() {
|
||||
<DisplaySettings
|
||||
realtimeLatency={realtimeLatency}
|
||||
searchDisplayMode={searchDisplayMode}
|
||||
rememberScrollPosition={rememberScrollPosition}
|
||||
onRealtimeLatencyChange={handleRealtimeLatencyChange}
|
||||
onSearchDisplayModeChange={handleSearchDisplayModeChange}
|
||||
onRememberScrollPositionChange={handleRememberScrollPositionChange}
|
||||
/>
|
||||
|
||||
{/* Source Management */}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
|
||||
/**
|
||||
* ScrollPositionManager - Maintains scroll position across navigation and refreshes
|
||||
* Uses sessionStorage to persist scroll state per URL
|
||||
*/
|
||||
export function ScrollPositionManager() {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Create a unique key for the current page including search params
|
||||
const getPageKey = useCallback(() => {
|
||||
const params = searchParams.toString();
|
||||
return `scroll-pos:${pathname}${params ? '?' + params : ''}`;
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
// Restoration logic
|
||||
useEffect(() => {
|
||||
const settings = settingsStore.getSettings();
|
||||
if (!settings.rememberScrollPosition) return;
|
||||
|
||||
const key = getPageKey();
|
||||
const savedPos = sessionStorage.getItem(key);
|
||||
|
||||
if (savedPos) {
|
||||
const position = parseInt(savedPos, 10);
|
||||
if (!isNaN(position) && position > 0) {
|
||||
// We use multiple attempts to restore scroll because content might be loading dynamically
|
||||
// (e.g., search results, movie grids)
|
||||
|
||||
// Keep trying until we actually scroll there or a timeout occurs
|
||||
let attempts = 0;
|
||||
const maxAttempts = 10;
|
||||
|
||||
const tryScroll = () => {
|
||||
const currentScroll = window.scrollY;
|
||||
window.scrollTo(0, position);
|
||||
attempts++;
|
||||
|
||||
// Verify if we actually reached the target position (with some wiggle room)
|
||||
const reached = Math.abs(window.scrollY - position) < 10;
|
||||
|
||||
if (!reached && attempts < maxAttempts) {
|
||||
// If we didn't reach it, it's likely because the page height hasn't caught up yet
|
||||
setTimeout(tryScroll, 200);
|
||||
}
|
||||
};
|
||||
|
||||
const timerId = setTimeout(tryScroll, 100);
|
||||
return () => clearTimeout(timerId);
|
||||
}
|
||||
}
|
||||
}, [getPageKey, pathname, searchParams]); // Run on navigation
|
||||
|
||||
// Saving logic
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const handleScroll = () => {
|
||||
const settings = settingsStore.getSettings();
|
||||
if (!settings.rememberScrollPosition) return;
|
||||
|
||||
// Debounce saving to avoid excessive writes to sessionStorage
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
const key = getPageKey();
|
||||
// Only save if we have scrolled
|
||||
if (window.scrollY > 0) {
|
||||
sessionStorage.setItem(key, window.scrollY.toString());
|
||||
} else {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [getPageKey]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -12,9 +12,16 @@ interface SearchResultsProps {
|
||||
availableSources: SourceBadge[];
|
||||
loading: boolean;
|
||||
isPremium?: boolean;
|
||||
latencies?: Record<string, number>;
|
||||
}
|
||||
|
||||
export function SearchResults({ results, availableSources, loading, isPremium = false }: SearchResultsProps) {
|
||||
export function SearchResults({
|
||||
results,
|
||||
availableSources,
|
||||
loading,
|
||||
isPremium = false,
|
||||
latencies = {}
|
||||
}: SearchResultsProps) {
|
||||
// Source badges hook - filters by video source
|
||||
const {
|
||||
selectedSources,
|
||||
@@ -62,7 +69,11 @@ export function SearchResults({ results, availableSources, loading, isPremium =
|
||||
)}
|
||||
|
||||
{/* Display filtered videos (both source and type filters applied) */}
|
||||
<VideoGrid videos={finalFilteredVideos} isPremium={isPremium} />
|
||||
<VideoGrid
|
||||
videos={finalFilteredVideos}
|
||||
isPremium={isPremium}
|
||||
latencies={latencies}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ interface VideoCardProps {
|
||||
isActive: boolean;
|
||||
onCardClick: (e: React.MouseEvent, cardId: string, videoUrl: string) => void;
|
||||
isPremium?: boolean;
|
||||
latencies?: Record<string, number>;
|
||||
}
|
||||
|
||||
export const VideoCard = memo<VideoCardProps>(({
|
||||
@@ -27,8 +28,10 @@ export const VideoCard = memo<VideoCardProps>(({
|
||||
cardId,
|
||||
isActive,
|
||||
onCardClick,
|
||||
isPremium = false
|
||||
isPremium = false,
|
||||
latencies = {}
|
||||
}) => {
|
||||
const displayLatency = latencies[video.source] ?? video.latency;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -91,8 +94,8 @@ export const VideoCard = memo<VideoCardProps>(({
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{video.latency !== undefined && (
|
||||
<LatencyBadge latency={video.latency} className="flex-shrink-0" />
|
||||
{displayLatency !== undefined && (
|
||||
<LatencyBadge latency={displayLatency} className="flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useCallback, useMemo, memo, useEffect } from 'react';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { VideoCard } from './VideoCard';
|
||||
import { VideoGroupCard, GroupedVideo } from './VideoGroupCard';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
@@ -10,27 +11,63 @@ interface VideoGridProps {
|
||||
videos: Video[];
|
||||
className?: string;
|
||||
isPremium?: boolean;
|
||||
latencies?: Record<string, number>;
|
||||
}
|
||||
|
||||
export const VideoGrid = memo(function VideoGrid({ videos, className = '', isPremium = false }: VideoGridProps) {
|
||||
export const VideoGrid = memo(function VideoGrid({
|
||||
videos,
|
||||
className = '',
|
||||
isPremium = false,
|
||||
latencies = {}
|
||||
}: VideoGridProps) {
|
||||
const [activeCardId, setActiveCardId] = useState<string | null>(null);
|
||||
const [visibleCount, setVisibleCount] = useState(24);
|
||||
const [displayMode, setDisplayMode] = useState<'normal' | 'grouped'>('normal');
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Load display mode from settings
|
||||
useEffect(() => {
|
||||
const settings = settingsStore.getSettings();
|
||||
setDisplayMode(settings.searchDisplayMode);
|
||||
|
||||
// Initial load: Check for saved scroll position to ensure we render enough items
|
||||
const params = searchParams.toString();
|
||||
const scrollKey = `scroll-pos:${pathname}${params ? '?' + params : ''}`;
|
||||
const savedPos = sessionStorage.getItem(scrollKey);
|
||||
|
||||
if (savedPos && settings.rememberScrollPosition) {
|
||||
const position = parseInt(savedPos, 10);
|
||||
if (!isNaN(position) && position > 500) {
|
||||
// Approximate visible count needed:
|
||||
// 500 is roughly where the second/third row starts.
|
||||
// Each row is ~300-400px high on most screens.
|
||||
// 24 items is 4-6 rows.
|
||||
// If scroll is deep, we force a larger initial visible count.
|
||||
// 24, 48, 72, 96...
|
||||
const estimatedRowsNeeded = Math.ceil(position / 300) + 2;
|
||||
// Match CSS breakpoints: sm: 3, md: 4, lg: 5, xl: 6
|
||||
const itemsPerRow = window.innerWidth >= 1280 ? 6 :
|
||||
(window.innerWidth >= 1024 ? 5 :
|
||||
(window.innerWidth >= 768 ? 4 :
|
||||
(window.innerWidth >= 640 ? 3 : 2)));
|
||||
const neededCount = Math.min(videos.length, estimatedRowsNeeded * itemsPerRow);
|
||||
|
||||
if (neededCount > 24) {
|
||||
setVisibleCount(Math.ceil(neededCount / 24) * 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = settingsStore.subscribe(() => {
|
||||
const newSettings = settingsStore.getSettings();
|
||||
setDisplayMode(newSettings.searchDisplayMode);
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
}, [pathname, searchParams, videos.length]);
|
||||
|
||||
if (videos.length === 0) {
|
||||
return null;
|
||||
@@ -150,6 +187,7 @@ export const VideoGrid = memo(function VideoGrid({ videos, className = '', isPre
|
||||
isActive={isActive}
|
||||
onCardClick={handleCardClick}
|
||||
isPremium={isPremium}
|
||||
latencies={latencies}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -166,6 +204,7 @@ export const VideoGrid = memo(function VideoGrid({ videos, className = '', isPre
|
||||
isActive={isActive}
|
||||
onCardClick={handleCardClick}
|
||||
isPremium={isPremium}
|
||||
latencies={latencies}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@ interface VideoGroupCardProps {
|
||||
isActive: boolean;
|
||||
onCardClick: (e: React.MouseEvent, cardId: string, videoUrl: string) => void;
|
||||
isPremium?: boolean;
|
||||
latencies?: Record<string, number>;
|
||||
}
|
||||
|
||||
export const VideoGroupCard = memo<VideoGroupCardProps>(({
|
||||
@@ -38,15 +39,16 @@ export const VideoGroupCard = memo<VideoGroupCardProps>(({
|
||||
cardId,
|
||||
isActive,
|
||||
onCardClick,
|
||||
isPremium = false
|
||||
isPremium = false,
|
||||
latencies = {}
|
||||
}) => {
|
||||
const { representative, videos, name } = group;
|
||||
|
||||
// Best latency from the group
|
||||
// Best latency from the group, preferring real-time updates
|
||||
const bestLatency = useMemo(() => {
|
||||
const latencies = videos.filter(v => v.latency !== undefined).map(v => v.latency!);
|
||||
return latencies.length > 0 ? Math.min(...latencies) : undefined;
|
||||
}, [videos]);
|
||||
const currentLatencies = videos.map(v => latencies[v.source] ?? v.latency).filter(l => l !== undefined) as number[];
|
||||
return currentLatencies.length > 0 ? Math.min(...currentLatencies) : undefined;
|
||||
}, [videos, latencies]);
|
||||
|
||||
// Generate URL with grouped sources data
|
||||
const videoUrl = useMemo(() => {
|
||||
|
||||
@@ -11,20 +11,41 @@ import { Switch } from '@/components/ui/Switch';
|
||||
interface DisplaySettingsProps {
|
||||
realtimeLatency: boolean;
|
||||
searchDisplayMode: SearchDisplayMode;
|
||||
rememberScrollPosition: boolean;
|
||||
onRealtimeLatencyChange: (enabled: boolean) => void;
|
||||
onSearchDisplayModeChange: (mode: SearchDisplayMode) => void;
|
||||
onRememberScrollPositionChange: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export function DisplaySettings({
|
||||
realtimeLatency,
|
||||
searchDisplayMode,
|
||||
rememberScrollPosition,
|
||||
onRealtimeLatencyChange,
|
||||
onSearchDisplayModeChange,
|
||||
onRememberScrollPositionChange,
|
||||
}: DisplaySettingsProps) {
|
||||
return (
|
||||
<div className="bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-sm)] p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-color)] mb-4">显示设置</h2>
|
||||
|
||||
{/* Remember Scroll Position Toggle */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium text-[var(--text-color)]">记住滚动位置</h3>
|
||||
<p className="text-sm text-[var(--text-color-secondary)] mt-1">
|
||||
退出或刷新页面后,自动恢复到之前的滚动位置
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={rememberScrollPosition}
|
||||
onChange={onRememberScrollPositionChange}
|
||||
ariaLabel="记住滚动位置开关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Real-time Latency Toggle */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ChevronUp } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* BackToTop - Floating button to scroll back to top of page
|
||||
* Follows Liquid Glass design system
|
||||
*/
|
||||
export function BackToTop() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const toggleVisibility = () => {
|
||||
// Show button after scrolling down 300px
|
||||
if (window.scrollY > 300) {
|
||||
setIsVisible(true);
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', toggleVisibility, { passive: true });
|
||||
|
||||
// Initial check in case page is already scrolled (e.g. on refresh)
|
||||
toggleVisibility();
|
||||
|
||||
return () => window.removeEventListener('scroll', toggleVisibility);
|
||||
}, []);
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={scrollToTop}
|
||||
className={`fixed bottom-8 right-8 z-[9999] p-3 rounded-full
|
||||
bg-[var(--glass-bg)] border border-[var(--glass-border)]
|
||||
shadow-[var(--shadow-md)] backdrop-blur-xl
|
||||
text-[var(--text-color)] transition-all duration-300 ease-out
|
||||
hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)]
|
||||
hover:scale-110 active:scale-95
|
||||
${isVisible
|
||||
? 'opacity-100 translate-y-0 scale-100'
|
||||
: 'opacity-0 translate-y-10 scale-50 pointer-events-none'
|
||||
}`}
|
||||
aria-label="返回顶部"
|
||||
title="返回顶部"
|
||||
>
|
||||
<ChevronUp size={24} strokeWidth={2.5} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export function useHomePage() {
|
||||
const { loadFromCache, saveToCache } = useSearchCache();
|
||||
const hasLoadedCache = useRef(false);
|
||||
const hasSearchedWithSourcesRef = useRef(false);
|
||||
const isInitialCacheLoad = useRef(false);
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
@@ -55,7 +56,9 @@ export function useHomePage() {
|
||||
|
||||
// Re-sort results when sort preference changes
|
||||
useEffect(() => {
|
||||
if (hasSearched && results.length > 0) {
|
||||
// Skip re-sorting if this is a load from cache, to preserve the "remembered" position
|
||||
// Only re-sort if the user explicitly changes the sortBy option later
|
||||
if (hasSearched && results.length > 0 && !isInitialCacheLoad.current) {
|
||||
applySorting(currentSortBy);
|
||||
}
|
||||
}, [currentSortBy, applySorting, hasSearched, results.length]);
|
||||
@@ -95,6 +98,14 @@ export function useHomePage() {
|
||||
|
||||
const handleSearch = useCallback((searchQuery: string) => {
|
||||
if (!searchQuery.trim()) return;
|
||||
|
||||
// Clear scroll position for this search query to ensure we start at the top on a fresh search
|
||||
const scrollKey = `scroll-pos:/?q=${encodeURIComponent(searchQuery)}`;
|
||||
sessionStorage.removeItem(scrollKey);
|
||||
|
||||
// Reset cache load flag for new search
|
||||
isInitialCacheLoad.current = false;
|
||||
|
||||
setQuery(searchQuery);
|
||||
setHasSearched(true);
|
||||
executeSearch(searchQuery);
|
||||
@@ -111,6 +122,7 @@ export function useHomePage() {
|
||||
if (urlQuery) {
|
||||
setQuery(urlQuery);
|
||||
if (cached && cached.query === urlQuery && cached.results.length > 0) {
|
||||
isInitialCacheLoad.current = true;
|
||||
setHasSearched(true);
|
||||
loadCachedResults(cached.results, cached.availableSources);
|
||||
hasSearchedWithSourcesRef.current = true;
|
||||
|
||||
@@ -8,7 +8,7 @@ interface SearchCache {
|
||||
}
|
||||
|
||||
const CACHE_KEY = 'kvideo_search_cache';
|
||||
const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes
|
||||
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
const MAX_CACHED_RESULTS = 300;
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface AppSettings {
|
||||
episodeReverseOrder: boolean; // Persist episode list reverse state
|
||||
fullscreenType: 'native' | 'window'; // Fullscreen mode preference
|
||||
proxyMode: ProxyMode; // Proxy behavior: 'retry' | 'none' | 'always'
|
||||
rememberScrollPosition: boolean; // Remember scroll position when navigating back or refreshing
|
||||
}
|
||||
|
||||
import { exportSettings, importSettings, SEARCH_HISTORY_KEY, WATCH_HISTORY_KEY } from './settings-helpers';
|
||||
@@ -119,6 +120,7 @@ function getDefaultAppSettings(): AppSettings {
|
||||
episodeReverseOrder: false,
|
||||
fullscreenType: 'native',
|
||||
proxyMode: 'retry',
|
||||
rememberScrollPosition: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,6 +198,7 @@ export const settingsStore = {
|
||||
episodeReverseOrder: parsed.episodeReverseOrder !== undefined ? parsed.episodeReverseOrder : false,
|
||||
fullscreenType: parsed.fullscreenType === 'window' ? 'window' : 'native',
|
||||
proxyMode: (parsed.proxyMode === 'retry' || parsed.proxyMode === 'none' || parsed.proxyMode === 'always') ? parsed.proxyMode : 'retry',
|
||||
rememberScrollPosition: parsed.rememberScrollPosition !== undefined ? parsed.rememberScrollPosition : true,
|
||||
};
|
||||
} catch {
|
||||
// Even if localStorage fails, we should return defaults + ENV subscriptions
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "kvideo",
|
||||
"version": "4.0.4",
|
||||
"version": "4.0.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kvideo",
|
||||
"version": "4.0.4",
|
||||
"version": "4.0.5",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kvideo",
|
||||
"version": "4.0.4",
|
||||
"version": "4.0.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user