mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
@@ -1,22 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { usePlayerSettings } from '@/components/player/hooks/usePlayerSettings';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
|
||||
interface AdKeywordsInjectorProps {
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
export function AdKeywordsInjector({ keywords }: AdKeywordsInjectorProps) {
|
||||
const { setAdKeywords } = usePlayerSettings();
|
||||
const initialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized.current && keywords.length > 0) {
|
||||
setAdKeywords(keywords);
|
||||
initialized.current = true;
|
||||
const normalizedKeywords = [...new Set(
|
||||
keywords
|
||||
.map((keyword) => keyword.trim())
|
||||
.filter((keyword) => keyword.length > 0)
|
||||
)];
|
||||
const currentSettings = settingsStore.getSettings();
|
||||
|
||||
const isUnchanged =
|
||||
currentSettings.adKeywords.length === normalizedKeywords.length &&
|
||||
currentSettings.adKeywords.every((keyword, index) => keyword === normalizedKeywords[index]);
|
||||
|
||||
if (!isUnchanged) {
|
||||
settingsStore.saveSettings({
|
||||
...currentSettings,
|
||||
adKeywords: normalizedKeywords,
|
||||
});
|
||||
}
|
||||
}, [keywords, setAdKeywords]);
|
||||
}, [keywords]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export function DesktopVideoPlayer({
|
||||
onResolutionDetected,
|
||||
}: DesktopVideoPlayerProps) {
|
||||
const { refs, data, actions } = useDesktopPlayerState();
|
||||
const { fullscreenType: settingsFullscreenType } = usePlayerSettings();
|
||||
const { fullscreenType: settingsFullscreenType } = usePlayerSettings(isPremium);
|
||||
const isIOS = useIsIOS();
|
||||
const isMobile = useIsMobile();
|
||||
const [seekStepSeconds, setSeekStepSeconds] = React.useState(DEFAULT_SEEK_STEP_SECONDS);
|
||||
@@ -160,6 +160,7 @@ export function DesktopVideoPlayer({
|
||||
useHlsPlayer({
|
||||
videoRef: refs.videoRef,
|
||||
src,
|
||||
isPremium,
|
||||
autoPlay: shouldAutoPlay
|
||||
});
|
||||
|
||||
@@ -206,6 +207,7 @@ export function DesktopVideoPlayer({
|
||||
currentTime,
|
||||
duration,
|
||||
isPlaying,
|
||||
isPremium,
|
||||
totalEpisodes,
|
||||
currentEpisodeIndex,
|
||||
onNextEpisode,
|
||||
@@ -335,6 +337,7 @@ export function DesktopVideoPlayer({
|
||||
isTransitioningToNextEpisode={isTransitioningToNextEpisode}
|
||||
// More Menu Props
|
||||
showMoreMenu={data.showMoreMenu}
|
||||
isPremium={isPremium}
|
||||
isProxied={src.includes('/api/proxy')}
|
||||
onToggleMoreMenu={() => actions.setShowMoreMenu(!data.showMoreMenu)}
|
||||
onMoreMenuMouseEnter={() => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createPortal } from 'react-dom';
|
||||
|
||||
interface DesktopMoreMenuProps {
|
||||
showMoreMenu: boolean;
|
||||
isPremium?: boolean;
|
||||
isProxied?: boolean;
|
||||
onToggleMoreMenu: () => void;
|
||||
onMouseEnter: () => void;
|
||||
@@ -22,6 +23,7 @@ interface DesktopMoreMenuProps {
|
||||
|
||||
export function DesktopMoreMenu({
|
||||
showMoreMenu,
|
||||
isPremium = false,
|
||||
isProxied = false,
|
||||
onToggleMoreMenu,
|
||||
onMouseEnter,
|
||||
@@ -60,7 +62,7 @@ export function DesktopMoreMenu({
|
||||
setDanmakuFontSize,
|
||||
danmakuDisplayArea,
|
||||
setDanmakuDisplayArea,
|
||||
} = usePlayerSettings();
|
||||
} = usePlayerSettings(isPremium);
|
||||
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
const menuRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -23,6 +23,7 @@ interface DesktopOverlayProps {
|
||||
onSkipForward: () => void;
|
||||
onSkipBackward: () => void;
|
||||
showMoreMenu: boolean;
|
||||
isPremium?: boolean;
|
||||
isProxied: boolean;
|
||||
onToggleMoreMenu: () => void;
|
||||
onMoreMenuMouseEnter: () => void;
|
||||
@@ -62,6 +63,7 @@ export function DesktopOverlay({
|
||||
onSkipBackward,
|
||||
showControls,
|
||||
showMoreMenu,
|
||||
isPremium = false,
|
||||
isProxied,
|
||||
onToggleMoreMenu,
|
||||
onMoreMenuMouseEnter,
|
||||
@@ -89,6 +91,7 @@ export function DesktopOverlay({
|
||||
<div className={`absolute top-8 left-6 z-40 transition-opacity duration-300 ${showControls ? 'opacity-100' : 'opacity-0'}`} style={{ pointerEvents: showControls ? 'auto' : 'none' }}>
|
||||
<DesktopMoreMenu
|
||||
showMoreMenu={showMoreMenu}
|
||||
isPremium={isPremium}
|
||||
isProxied={isProxied}
|
||||
onToggleMoreMenu={onToggleMoreMenu}
|
||||
onMouseEnter={onMoreMenuMouseEnter}
|
||||
|
||||
@@ -13,6 +13,7 @@ interface DesktopOverlayWrapperProps {
|
||||
onSkipBackward: () => void;
|
||||
isTransitioningToNextEpisode?: boolean;
|
||||
showMoreMenu: boolean;
|
||||
isPremium?: boolean;
|
||||
isProxied: boolean;
|
||||
onToggleMoreMenu: () => void;
|
||||
onMoreMenuMouseEnter: () => void;
|
||||
@@ -43,6 +44,7 @@ export function DesktopOverlayWrapper({
|
||||
onSkipBackward,
|
||||
isTransitioningToNextEpisode = false,
|
||||
showMoreMenu,
|
||||
isPremium = false,
|
||||
isProxied,
|
||||
onToggleMoreMenu,
|
||||
onMoreMenuMouseEnter,
|
||||
@@ -93,6 +95,7 @@ export function DesktopOverlayWrapper({
|
||||
onSkipForward={onSkipForward}
|
||||
onSkipBackward={onSkipBackward}
|
||||
showMoreMenu={showMoreMenu}
|
||||
isPremium={isPremium}
|
||||
isProxied={isProxied}
|
||||
onToggleMoreMenu={onToggleMoreMenu}
|
||||
onMoreMenuMouseEnter={onMoreMenuMouseEnter}
|
||||
|
||||
@@ -9,6 +9,7 @@ interface UseAutoSkipProps {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
isPlaying: boolean;
|
||||
isPremium?: boolean;
|
||||
totalEpisodes?: number;
|
||||
currentEpisodeIndex?: number;
|
||||
onNextEpisode?: () => void;
|
||||
@@ -27,6 +28,7 @@ export function useAutoSkip({
|
||||
currentTime,
|
||||
duration,
|
||||
isPlaying,
|
||||
isPremium = false,
|
||||
totalEpisodes = 1,
|
||||
currentEpisodeIndex = 0,
|
||||
onNextEpisode,
|
||||
@@ -39,7 +41,7 @@ export function useAutoSkip({
|
||||
skipIntroSeconds,
|
||||
autoSkipOutro,
|
||||
skipOutroSeconds,
|
||||
} = usePlayerSettings();
|
||||
} = usePlayerSettings(isPremium);
|
||||
|
||||
// Track if we've already skipped intro for this video session
|
||||
const hasSkippedIntroRef = useRef(false);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { filterM3u8Ad } from '@/lib/utils/m3u8-utils';
|
||||
interface UseHlsPlayerProps {
|
||||
videoRef: React.RefObject<HTMLVideoElement | null>;
|
||||
src: string;
|
||||
isPremium?: boolean;
|
||||
autoPlay?: boolean;
|
||||
onAutoPlayPrevented?: (error: Error) => void;
|
||||
onError?: (message: string) => void;
|
||||
@@ -14,12 +15,13 @@ interface UseHlsPlayerProps {
|
||||
export function useHlsPlayer({
|
||||
videoRef,
|
||||
src,
|
||||
isPremium = false,
|
||||
autoPlay = false,
|
||||
onAutoPlayPrevented,
|
||||
onError
|
||||
}: UseHlsPlayerProps) {
|
||||
const hlsRef = useRef<Hls | null>(null);
|
||||
const { adFilterMode, adKeywords } = usePlayerSettings();
|
||||
const { adFilterMode, adKeywords } = usePlayerSettings(isPremium);
|
||||
const isAdFilterEnabled = adFilterMode !== 'off';
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,135 +1,172 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { settingsStore, AdFilterMode } from '@/lib/store/settings-store';
|
||||
import {
|
||||
settingsStore,
|
||||
type AppSettings,
|
||||
type AdFilterMode,
|
||||
} from '@/lib/store/settings-store';
|
||||
import {
|
||||
premiumModeSettingsStore,
|
||||
type ModeSettings,
|
||||
} from '@/lib/store/premium-mode-settings';
|
||||
|
||||
interface PlayerSettingsSnapshot {
|
||||
autoNextEpisode: boolean;
|
||||
autoSkipIntro: boolean;
|
||||
skipIntroSeconds: number;
|
||||
autoSkipOutro: boolean;
|
||||
skipOutroSeconds: number;
|
||||
showModeIndicator: boolean;
|
||||
adFilter: boolean;
|
||||
adFilterMode: AdFilterMode;
|
||||
adKeywords: string[];
|
||||
fullscreenType: 'auto' | 'native' | 'window';
|
||||
proxyMode: 'retry' | 'none' | 'always';
|
||||
danmakuEnabled: boolean;
|
||||
danmakuApiUrl: string;
|
||||
danmakuOpacity: number;
|
||||
danmakuFontSize: number;
|
||||
danmakuDisplayArea: number;
|
||||
}
|
||||
|
||||
function getPlayerSettingsSnapshot(isPremium: boolean): PlayerSettingsSnapshot {
|
||||
const globalSettings = settingsStore.getSettings();
|
||||
const modeSettings = isPremium ? premiumModeSettingsStore.getSettings() : globalSettings;
|
||||
|
||||
return {
|
||||
autoNextEpisode: modeSettings.autoNextEpisode,
|
||||
autoSkipIntro: modeSettings.autoSkipIntro,
|
||||
skipIntroSeconds: modeSettings.skipIntroSeconds,
|
||||
autoSkipOutro: modeSettings.autoSkipOutro,
|
||||
skipOutroSeconds: modeSettings.skipOutroSeconds,
|
||||
showModeIndicator: modeSettings.showModeIndicator,
|
||||
adFilter: globalSettings.adFilter,
|
||||
adFilterMode: modeSettings.adFilterMode,
|
||||
adKeywords: globalSettings.adKeywords,
|
||||
fullscreenType: modeSettings.fullscreenType,
|
||||
proxyMode: modeSettings.proxyMode,
|
||||
danmakuEnabled: modeSettings.danmakuEnabled,
|
||||
danmakuApiUrl: modeSettings.danmakuApiUrl,
|
||||
danmakuOpacity: modeSettings.danmakuOpacity,
|
||||
danmakuFontSize: modeSettings.danmakuFontSize,
|
||||
danmakuDisplayArea: modeSettings.danmakuDisplayArea,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access and update player settings from the settings store
|
||||
* Provides reactive updates when settings change
|
||||
*/
|
||||
export function usePlayerSettings() {
|
||||
const [settings, setSettings] = useState(() => {
|
||||
const stored = settingsStore.getSettings();
|
||||
return {
|
||||
autoNextEpisode: stored.autoNextEpisode,
|
||||
autoSkipIntro: stored.autoSkipIntro,
|
||||
skipIntroSeconds: stored.skipIntroSeconds,
|
||||
autoSkipOutro: stored.autoSkipOutro,
|
||||
skipOutroSeconds: stored.skipOutroSeconds,
|
||||
showModeIndicator: stored.showModeIndicator,
|
||||
adFilter: stored.adFilter,
|
||||
adFilterMode: stored.adFilterMode,
|
||||
adKeywords: stored.adKeywords,
|
||||
fullscreenType: stored.fullscreenType,
|
||||
proxyMode: stored.proxyMode,
|
||||
danmakuEnabled: stored.danmakuEnabled,
|
||||
danmakuApiUrl: stored.danmakuApiUrl,
|
||||
danmakuOpacity: stored.danmakuOpacity,
|
||||
danmakuFontSize: stored.danmakuFontSize,
|
||||
danmakuDisplayArea: stored.danmakuDisplayArea,
|
||||
};
|
||||
});
|
||||
export function usePlayerSettings(isPremium: boolean = false) {
|
||||
const [settings, setSettings] = useState(() => getPlayerSettingsSnapshot(isPremium));
|
||||
|
||||
// Subscribe to settings changes
|
||||
useEffect(() => {
|
||||
const unsubscribe = settingsStore.subscribe(() => {
|
||||
const stored = settingsStore.getSettings();
|
||||
setSettings({
|
||||
autoNextEpisode: stored.autoNextEpisode,
|
||||
autoSkipIntro: stored.autoSkipIntro,
|
||||
skipIntroSeconds: stored.skipIntroSeconds,
|
||||
autoSkipOutro: stored.autoSkipOutro,
|
||||
skipOutroSeconds: stored.skipOutroSeconds,
|
||||
showModeIndicator: stored.showModeIndicator,
|
||||
adFilter: stored.adFilter,
|
||||
adFilterMode: stored.adFilterMode,
|
||||
adKeywords: stored.adKeywords,
|
||||
fullscreenType: stored.fullscreenType,
|
||||
proxyMode: stored.proxyMode,
|
||||
danmakuEnabled: stored.danmakuEnabled,
|
||||
danmakuApiUrl: stored.danmakuApiUrl,
|
||||
danmakuOpacity: stored.danmakuOpacity,
|
||||
danmakuFontSize: stored.danmakuFontSize,
|
||||
danmakuDisplayArea: stored.danmakuDisplayArea,
|
||||
const syncSettings = () => {
|
||||
setSettings(getPlayerSettingsSnapshot(isPremium));
|
||||
};
|
||||
|
||||
const modeStore = isPremium ? premiumModeSettingsStore : settingsStore;
|
||||
const unsubscribeModeStore = modeStore.subscribe(syncSettings);
|
||||
const unsubscribeGlobalStore = isPremium ? settingsStore.subscribe(syncSettings) : null;
|
||||
|
||||
syncSettings();
|
||||
|
||||
return () => {
|
||||
unsubscribeModeStore();
|
||||
unsubscribeGlobalStore?.();
|
||||
};
|
||||
}, [isPremium]);
|
||||
|
||||
const updateModeSettings = useCallback((partial: Partial<ModeSettings>) => {
|
||||
if (isPremium) {
|
||||
const currentSettings = premiumModeSettingsStore.getSettings();
|
||||
premiumModeSettingsStore.saveSettings({
|
||||
...currentSettings,
|
||||
...partial,
|
||||
});
|
||||
});
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateSetting = useCallback(<K extends keyof typeof settings>(
|
||||
key: K,
|
||||
value: typeof settings[K]
|
||||
) => {
|
||||
const currentSettings = settingsStore.getSettings();
|
||||
settingsStore.saveSettings({
|
||||
...currentSettings,
|
||||
[key]: value,
|
||||
...partial,
|
||||
});
|
||||
}, [isPremium]);
|
||||
|
||||
const updateGlobalSettings = useCallback((partial: Partial<AppSettings>) => {
|
||||
const currentSettings = settingsStore.getSettings();
|
||||
settingsStore.saveSettings({
|
||||
...currentSettings,
|
||||
...partial,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setAutoNextEpisode = useCallback((value: boolean) => {
|
||||
updateSetting('autoNextEpisode', value);
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ autoNextEpisode: value });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setAutoSkipIntro = useCallback((value: boolean) => {
|
||||
updateSetting('autoSkipIntro', value);
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ autoSkipIntro: value });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setSkipIntroSeconds = useCallback((value: number) => {
|
||||
updateSetting('skipIntroSeconds', Math.max(0, value));
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ skipIntroSeconds: Math.max(0, value) });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setAutoSkipOutro = useCallback((value: boolean) => {
|
||||
updateSetting('autoSkipOutro', value);
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ autoSkipOutro: value });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setSkipOutroSeconds = useCallback((value: number) => {
|
||||
updateSetting('skipOutroSeconds', Math.max(0, value));
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ skipOutroSeconds: Math.max(0, value) });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setShowModeIndicator = useCallback((value: boolean) => {
|
||||
updateSetting('showModeIndicator', value);
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ showModeIndicator: value });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setAdFilter = useCallback((value: boolean) => {
|
||||
updateSetting('adFilter', value);
|
||||
}, [updateSetting]);
|
||||
updateGlobalSettings({ adFilter: value });
|
||||
}, [updateGlobalSettings]);
|
||||
|
||||
const setAdFilterMode = useCallback((value: AdFilterMode) => {
|
||||
updateSetting('adFilterMode', value);
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ adFilterMode: value });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setAdKeywords = useCallback((value: string[]) => {
|
||||
updateSetting('adKeywords', value);
|
||||
}, [updateSetting]);
|
||||
updateGlobalSettings({ adKeywords: value });
|
||||
}, [updateGlobalSettings]);
|
||||
|
||||
const setFullscreenType = useCallback((value: 'auto' | 'native' | 'window') => {
|
||||
updateSetting('fullscreenType', value);
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ fullscreenType: value });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setProxyMode = useCallback((value: 'retry' | 'none' | 'always') => {
|
||||
updateSetting('proxyMode', value);
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ proxyMode: value });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setDanmakuEnabled = useCallback((value: boolean) => {
|
||||
updateSetting('danmakuEnabled', value);
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ danmakuEnabled: value });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setDanmakuApiUrl = useCallback((value: string) => {
|
||||
updateSetting('danmakuApiUrl', value);
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ danmakuApiUrl: value });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setDanmakuOpacity = useCallback((value: number) => {
|
||||
updateSetting('danmakuOpacity', Math.max(0.1, Math.min(1, value)));
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ danmakuOpacity: Math.max(0.1, Math.min(1, value)) });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setDanmakuFontSize = useCallback((value: number) => {
|
||||
updateSetting('danmakuFontSize', value);
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ danmakuFontSize: value });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
const setDanmakuDisplayArea = useCallback((value: number) => {
|
||||
updateSetting('danmakuDisplayArea', value);
|
||||
}, [updateSetting]);
|
||||
updateModeSettings({ danmakuDisplayArea: value });
|
||||
}, [updateModeSettings]);
|
||||
|
||||
return {
|
||||
...settings,
|
||||
|
||||
Reference in New Issue
Block a user