mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-20 03:03:43 +08:00
新增M3U8启发式广告检测算法并支持动态关键词配置
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { usePlayerSettings } from '@/components/player/hooks/usePlayerSettings';
|
||||
|
||||
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;
|
||||
}
|
||||
}, [keywords, setAdKeywords]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import React from 'react';
|
||||
import { Icons } from '@/components/ui/Icon';
|
||||
import { usePlayerSettings } from '../hooks/usePlayerSettings';
|
||||
import { settingsStore, AdFilterMode } from '@/lib/store/settings-store';
|
||||
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
@@ -32,16 +33,28 @@ export function DesktopMoreMenu({
|
||||
autoSkipOutro,
|
||||
skipOutroSeconds,
|
||||
showModeIndicator,
|
||||
adFilter,
|
||||
setAutoNextEpisode,
|
||||
setAutoSkipIntro,
|
||||
setSkipIntroSeconds,
|
||||
setAutoSkipOutro,
|
||||
setSkipOutroSeconds,
|
||||
setShowModeIndicator,
|
||||
setAdFilter,
|
||||
adFilterMode,
|
||||
setAdFilterMode,
|
||||
} = usePlayerSettings();
|
||||
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0 });
|
||||
const [isAdFilterOpen, setAdFilterOpen] = React.useState(false);
|
||||
|
||||
const AD_FILTER_LABELS: Record<string, string> = {
|
||||
off: '关闭',
|
||||
keyword: '关键词',
|
||||
heuristic: '智能(Beta)',
|
||||
aggressive: '激进'
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showMoreMenu && buttonRef.current && containerRef.current) {
|
||||
@@ -130,6 +143,46 @@ export function DesktopMoreMenu({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Ad Filter Mode Selector */}
|
||||
<div className="px-4 py-2.5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 text-sm text-[var(--text-color)]">
|
||||
<Icons.ShieldAlert size={18} />
|
||||
<span>广告过滤</span>
|
||||
</div>
|
||||
{/* Custom Ad Filter Mode Selector */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setAdFilterOpen(!isAdFilterOpen)}
|
||||
className="flex items-center gap-1.5 bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] text-xs rounded-md px-2.5 py-1.5 outline-none hover:border-[var(--accent-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_5%,transparent)] transition-all cursor-pointer"
|
||||
>
|
||||
<span>{AD_FILTER_LABELS[adFilterMode] || '关闭'}</span>
|
||||
<Icons.ChevronDown size={14} className={`text-[var(--text-color-secondary)] transition-transform duration-200 ${isAdFilterOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{isAdFilterOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10 cursor-default" onClick={() => setAdFilterOpen(false)} />
|
||||
<div className="absolute right-0 top-full mt-1 w-28 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] rounded-lg shadow-xl overflow-hidden z-20 flex flex-col animate-in fade-in zoom-in-95 duration-150">
|
||||
{Object.entries(AD_FILTER_LABELS).map(([mode, label]) => (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => {
|
||||
setAdFilterMode(mode as AdFilterMode);
|
||||
setAdFilterOpen(false);
|
||||
}}
|
||||
className={`text-left text-xs px-3 py-2.5 hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)] transition-colors w-full flex items-center justify-between group ${adFilterMode === mode ? 'text-[var(--accent-color)] font-medium bg-[color-mix(in_srgb,var(--accent-color)_5%,transparent)]' : 'text-[var(--text-color)]'
|
||||
}`}
|
||||
>
|
||||
<span>{label}</span>
|
||||
{adFilterMode === mode && <Icons.Check size={12} className="text-[var(--accent-color)]" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto Next Episode Switch */}
|
||||
<div className="px-4 py-2.5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 text-sm text-[var(--text-color)]">
|
||||
@@ -225,7 +278,7 @@ export function DesktopMoreMenu({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import Hls from 'hls.js';
|
||||
import { usePlayerSettings } from './usePlayerSettings';
|
||||
import { filterM3u8Ad } from '@/lib/utils/m3u8-utils';
|
||||
|
||||
interface UseHlsPlayerProps {
|
||||
videoRef: React.RefObject<HTMLVideoElement | null>;
|
||||
@@ -17,6 +19,8 @@ export function useHlsPlayer({
|
||||
onError
|
||||
}: UseHlsPlayerProps) {
|
||||
const hlsRef = useRef<Hls | null>(null);
|
||||
const { adFilterMode, adKeywords } = usePlayerSettings();
|
||||
const isAdFilterEnabled = adFilterMode !== 'off';
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
@@ -29,45 +33,66 @@ export function useHlsPlayer({
|
||||
}
|
||||
|
||||
let hls: Hls | null = null;
|
||||
let extraBlobs: string[] = [];
|
||||
|
||||
// Check if HLS is supported natively (Safari, Mobile Chrome)
|
||||
// We prefer native playback if available as it's usually more battery efficient
|
||||
const isNativeHlsSupported = video.canPlayType('application/vnd.apple.mpegurl');
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
// Use hls.js for browsers without native support (Desktop Chrome, Firefox, Edge)
|
||||
// OR if we want to force hls.js for better control (optional, but sticking to native first is safer)
|
||||
|
||||
// Note: Some desktop browsers (like Safari) support native HLS.
|
||||
// We usually prefer native, BUT sometimes native implementation is buggy or lacks features.
|
||||
// For now, we follow the standard pattern: Native first, then HLS.js.
|
||||
// EXCEPT for Chrome on Desktop which reports canPlayType as '' (false).
|
||||
// Define custom loader class to intercept manifest loading
|
||||
// We use 'any' cast because default loader type might not be strictly exposed in all typings
|
||||
const DefaultLoader = (Hls as any).DefaultConfig.loader;
|
||||
|
||||
if (!isNativeHlsSupported) {
|
||||
hls = new Hls({
|
||||
class AdFilterLoader extends DefaultLoader {
|
||||
load(context: any, config: any, callbacks: any) {
|
||||
if (isAdFilterEnabled && (context.type === 'manifest' || context.type === 'level')) {
|
||||
const originalOnSuccess = callbacks.onSuccess;
|
||||
callbacks.onSuccess = (response: any, stats: any, context: any, networkDetails: any) => {
|
||||
if (typeof response.data === 'string') {
|
||||
try {
|
||||
// Filter the content
|
||||
response.data = filterM3u8Ad(response.data, context.url, adFilterMode, adKeywords);
|
||||
} catch (e) {
|
||||
console.warn('[HLS] Ad filter error:', e);
|
||||
}
|
||||
}
|
||||
originalOnSuccess(response, stats, context, networkDetails);
|
||||
};
|
||||
}
|
||||
super.load(context, config, callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNativeHlsSupported || isAdFilterEnabled) {
|
||||
// If ad filtering is on, we force Hls.js even on native-supported desktop browsers
|
||||
// Exceptions might exist for iOS where MSE is strictly not available, check Hls.isSupported() result carefully.
|
||||
// Hls.isSupported() is false on iOS Safari usually, so this block won't run there.
|
||||
|
||||
const config: any = {
|
||||
// Worker & Performance
|
||||
enableWorker: true,
|
||||
lowLatencyMode: false, // Disable low latency for more stable playback
|
||||
lowLatencyMode: false,
|
||||
|
||||
// Buffer Settings - More aggressive buffering for smoother playback
|
||||
maxBufferLength: 60, // Buffer up to 60 seconds ahead
|
||||
maxMaxBufferLength: 120, // Allow up to 2 minutes of buffer
|
||||
maxBufferSize: 60 * 1000 * 1000, // 60MB buffer size
|
||||
maxBufferHole: 0.5, // Allow small gaps in buffer
|
||||
// Buffer Settings
|
||||
maxBufferLength: 60,
|
||||
maxMaxBufferLength: 120,
|
||||
maxBufferSize: 60 * 1000 * 1000,
|
||||
maxBufferHole: 0.5,
|
||||
|
||||
// Start with more buffer before playing
|
||||
startFragPrefetch: true, // Enable prefetching next fragment
|
||||
// Start with more buffer
|
||||
startFragPrefetch: true,
|
||||
|
||||
// ABR (Adaptive Bitrate) Settings - Be more conservative
|
||||
abrEwmaDefaultEstimate: 500000, // Start with conservative bandwidth estimate (500kbps)
|
||||
abrEwmaFastLive: 3, // Fast adaptation for live
|
||||
abrEwmaSlowLive: 9, // Slow adaptation for live
|
||||
abrEwmaFastVoD: 3, // Fast adaptation for VoD
|
||||
abrEwmaSlowVoD: 9, // Slow adaptation for VoD
|
||||
abrBandWidthFactor: 0.8, // Use 80% of estimated bandwidth (conservative)
|
||||
abrBandWidthUpFactor: 0.7, // Even more conservative when switching up
|
||||
// ABR Settings
|
||||
abrEwmaDefaultEstimate: 500000,
|
||||
abrEwmaFastLive: 3,
|
||||
abrEwmaSlowLive: 9,
|
||||
abrEwmaFastVoD: 3,
|
||||
abrEwmaSlowVoD: 9,
|
||||
abrBandWidthFactor: 0.8,
|
||||
abrBandWidthUpFactor: 0.7,
|
||||
|
||||
// Loading Settings - More retries and longer timeouts
|
||||
// Loading Settings
|
||||
fragLoadingMaxRetry: 6,
|
||||
fragLoadingRetryDelay: 1000,
|
||||
fragLoadingMaxRetryTimeout: 64000,
|
||||
@@ -79,29 +104,35 @@ export function useHlsPlayer({
|
||||
levelLoadingMaxRetryTimeout: 64000,
|
||||
|
||||
// Timeouts
|
||||
fragLoadingTimeOut: 20000, // 20 seconds for fragment loading
|
||||
manifestLoadingTimeOut: 10000, // 10 seconds for manifest
|
||||
levelLoadingTimeOut: 10000, // 10 seconds for level
|
||||
fragLoadingTimeOut: 20000,
|
||||
manifestLoadingTimeOut: 10000,
|
||||
levelLoadingTimeOut: 10000,
|
||||
|
||||
// Backbuffer - Keep some played content for seeking back
|
||||
backBufferLength: 30, // Keep 30 seconds of played content
|
||||
});
|
||||
// Backbuffer
|
||||
backBufferLength: 30,
|
||||
};
|
||||
|
||||
// Use custom loader if ad filtering is enabled
|
||||
if (isAdFilterEnabled) {
|
||||
config.loader = AdFilterLoader;
|
||||
}
|
||||
|
||||
hls = new Hls(config);
|
||||
hlsRef.current = hls;
|
||||
|
||||
hls.loadSource(src);
|
||||
hls.attachMedia(video);
|
||||
|
||||
// Auto Play Handler
|
||||
hls.on(Hls.Events.FRAG_LOADED, (event, data) => {
|
||||
// Force play if we have the first segment and it's not playing yet
|
||||
// detailed: data.frag.sn is the sequence number
|
||||
if (autoPlay && video.paused && data.frag.start === 0) {
|
||||
video.play().catch(console.warn);
|
||||
}
|
||||
});
|
||||
|
||||
// Manifest Parsed Handler
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
|
||||
// Check for HEVC/H.265 codec (limited browser support)
|
||||
// Check for HEVC
|
||||
if (hls) {
|
||||
const levels = hls.levels;
|
||||
if (levels && levels.length > 0) {
|
||||
@@ -110,10 +141,7 @@ export function useHlsPlayer({
|
||||
level.videoCodec?.toLowerCase().includes('h265')
|
||||
);
|
||||
if (hasHEVC) {
|
||||
console.warn('[HLS] ⚠️ HEVC/H.265 codec detected - may not play in all browsers');
|
||||
console.warn('[HLS] Supported: Safari with hardware acceleration, some Edge versions');
|
||||
console.warn('[HLS] Not supported: Most Chrome/Firefox versions');
|
||||
// Notify parent about potential codec issues
|
||||
console.warn('[HLS] ⚠️ HEVC detected');
|
||||
onError?.('检测到 HEVC/H.265 编码,当前浏览器可能不支持');
|
||||
}
|
||||
}
|
||||
@@ -121,12 +149,13 @@ export function useHlsPlayer({
|
||||
|
||||
if (autoPlay) {
|
||||
video.play().catch((err) => {
|
||||
console.warn('[HLS] Autoplay prevented:', err);
|
||||
// console.warn('[HLS] Autoplay prevented:', err);
|
||||
onAutoPlayPrevented?.(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Error Handling
|
||||
let networkErrorRetries = 0;
|
||||
let mediaErrorRetries = 0;
|
||||
const MAX_RETRIES = 3;
|
||||
@@ -136,22 +165,18 @@ export function useHlsPlayer({
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
networkErrorRetries++;
|
||||
console.error(`[HLS] Network error (${networkErrorRetries}/${MAX_RETRIES}), trying to recover...`, data);
|
||||
if (networkErrorRetries <= MAX_RETRIES) {
|
||||
hls?.startLoad();
|
||||
} else {
|
||||
console.error('[HLS] Too many network errors, giving up');
|
||||
onError?.('网络错误:无法加载视频流');
|
||||
hls?.destroy();
|
||||
}
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
mediaErrorRetries++;
|
||||
console.error(`[HLS] Media error (${mediaErrorRetries}/${MAX_RETRIES}), trying to recover...`, data);
|
||||
if (mediaErrorRetries <= MAX_RETRIES) {
|
||||
hls?.recoverMediaError();
|
||||
} else {
|
||||
console.error('[HLS] Too many media errors, giving up');
|
||||
onError?.('媒体错误:视频格式不支持或已损坏');
|
||||
hls?.destroy();
|
||||
}
|
||||
@@ -162,20 +187,151 @@ export function useHlsPlayer({
|
||||
hls?.destroy();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Non-fatal errors
|
||||
console.warn('[HLS] Non-fatal error:', data.type, data.details);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Native HLS support
|
||||
// Native HLS (Desktop Safari, no Filter)
|
||||
video.src = src;
|
||||
}
|
||||
} else if (isNativeHlsSupported) {
|
||||
// Fallback for environments where Hls.js is not supported but native is (e.g. iOS without MSE?)
|
||||
video.src = src;
|
||||
// Native HLS (iOS, Mobile Safari)
|
||||
// Limitations: Native HLS cannot easily intercept sub-playlist requests.
|
||||
// We use fetch+blob for the master playlist as a best 'first-level' filter.
|
||||
// If the ad discontinuity is in the master playlist (rare for ads, common for periods), it works.
|
||||
// If it's in sub-playlists, it might fail unless we parse and blob those too (complex).
|
||||
|
||||
if (isAdFilterEnabled) {
|
||||
const processMasterPlaylist = async (masterSrc: string) => {
|
||||
// Move blob tracking outside try to ensure cleanup on error
|
||||
const createdBlobs: string[] = [];
|
||||
|
||||
// Safely resolve relative URLs to absolute (handles iOS Safari scenarios)
|
||||
let absoluteMasterSrc: string;
|
||||
try {
|
||||
absoluteMasterSrc = new URL(masterSrc, window.location.href).toString();
|
||||
} catch {
|
||||
absoluteMasterSrc = masterSrc; // Fallback if URL parsing fails
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(absoluteMasterSrc);
|
||||
const masterContent = await response.text();
|
||||
|
||||
// If it's a simple playlist (no variants), just filter and play
|
||||
if (!masterContent.includes('#EXT-X-STREAM-INF')) {
|
||||
const filtered = filterM3u8Ad(masterContent, absoluteMasterSrc, adFilterMode, adKeywords);
|
||||
const blob = new Blob([filtered], { type: 'application/vnd.apple.mpegurl' });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
createdBlobs.push(blobUrl);
|
||||
return { masterBlobUrl: blobUrl, allBlobs: createdBlobs };
|
||||
}
|
||||
|
||||
// It IS a master playlist. Use map + Promise.all for clean concurrent processing.
|
||||
const lines = masterContent.split(/\r?\n/);
|
||||
|
||||
// Helper to safely compare origins
|
||||
const isSameOrigin = (url: string): boolean => {
|
||||
try {
|
||||
return new URL(url).origin === new URL(absoluteMasterSrc).origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Process each line, looking back at previous line to determine context
|
||||
const lineProcessingPromises = lines.map(async (line, index) => {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Handle #EXT-X-MEDIA:URI="..."
|
||||
if (trimmedLine.startsWith('#EXT-X-MEDIA') && trimmedLine.includes('URI="')) {
|
||||
const uriMatch = trimmedLine.match(/URI="([^"]+)"/);
|
||||
const uri = uriMatch?.[1];
|
||||
if (uri) {
|
||||
// Process if relative URL or same-origin absolute URL
|
||||
const isRelative = !uri.startsWith('http');
|
||||
const sameOrigin = uri.startsWith('http') && isSameOrigin(uri);
|
||||
|
||||
if (isRelative || sameOrigin) {
|
||||
try {
|
||||
const absoluteUrl = isRelative ? new URL(uri, absoluteMasterSrc).toString() : uri;
|
||||
const subRes = await fetch(absoluteUrl);
|
||||
const subContent = await subRes.text();
|
||||
const filteredSub = filterM3u8Ad(subContent, absoluteUrl, adFilterMode, adKeywords);
|
||||
const subBlob = new Blob([filteredSub], { type: 'application/vnd.apple.mpegurl' });
|
||||
const subBlobUrl = URL.createObjectURL(subBlob);
|
||||
createdBlobs.push(subBlobUrl);
|
||||
return line.replace(`URI="${uri}"`, `URI="${subBlobUrl}"`);
|
||||
} catch (e) {
|
||||
console.warn('[HLS Native] Failed to process EXT-X-MEDIA URI:', e);
|
||||
return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle playlist URL (line after #EXT-X-STREAM-INF)
|
||||
const prevLine = index > 0 ? lines[index - 1].trim() : '';
|
||||
if (prevLine.startsWith('#EXT-X-STREAM-INF') && trimmedLine && !trimmedLine.startsWith('#')) {
|
||||
// Process if relative URL or same-origin absolute URL
|
||||
const isRelative = !trimmedLine.startsWith('http');
|
||||
const sameOrigin = trimmedLine.startsWith('http') && isSameOrigin(trimmedLine);
|
||||
|
||||
if (isRelative || sameOrigin) {
|
||||
try {
|
||||
const absoluteUrl = isRelative ? new URL(trimmedLine, absoluteMasterSrc).toString() : trimmedLine;
|
||||
const subRes = await fetch(absoluteUrl);
|
||||
const subContent = await subRes.text();
|
||||
const filteredSub = filterM3u8Ad(subContent, absoluteUrl, adFilterMode, adKeywords);
|
||||
const subBlob = new Blob([filteredSub], { type: 'application/vnd.apple.mpegurl' });
|
||||
const subBlobUrl = URL.createObjectURL(subBlob);
|
||||
createdBlobs.push(subBlobUrl);
|
||||
return subBlobUrl;
|
||||
} catch (e) {
|
||||
console.warn('[HLS Native] Failed to process variant playlist:', e);
|
||||
return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All other lines pass through unchanged
|
||||
return line;
|
||||
});
|
||||
|
||||
const processedLines = await Promise.all(lineProcessingPromises);
|
||||
|
||||
// Join back
|
||||
const finalMasterContent = processedLines.join('\n');
|
||||
const masterBlob = new Blob([finalMasterContent], { type: 'application/vnd.apple.mpegurl' });
|
||||
const masterBlobUrl = URL.createObjectURL(masterBlob);
|
||||
createdBlobs.push(masterBlobUrl);
|
||||
|
||||
return { masterBlobUrl, allBlobs: createdBlobs };
|
||||
} catch (e) {
|
||||
// Critical: Clean up any blobs created before the error
|
||||
for (const blobUrl of createdBlobs) {
|
||||
try {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
} catch { /* ignore cleanup errors */ }
|
||||
}
|
||||
console.error('[HLS Native] Recursive fetch failed', e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
processMasterPlaylist(src).then((result) => {
|
||||
video.src = result.masterBlobUrl;
|
||||
extraBlobs = result.allBlobs;
|
||||
}).catch((e) => {
|
||||
console.warn('[HLS Native] Ad filtering failed, falling back to original source.', e);
|
||||
onError?.('广告过滤失败,已回退到原始视频流');
|
||||
video.src = src;
|
||||
});
|
||||
|
||||
} else {
|
||||
video.src = src;
|
||||
}
|
||||
} else {
|
||||
console.error('[HLS] HLS not supported in this browser');
|
||||
console.error('[HLS] HLS not supported');
|
||||
onError?.('当前浏览器不支持 HLS 视频播放');
|
||||
}
|
||||
|
||||
@@ -183,6 +339,7 @@ export function useHlsPlayer({
|
||||
if (hls) {
|
||||
hls.destroy();
|
||||
}
|
||||
extraBlobs.forEach(url => URL.revokeObjectURL(url));
|
||||
};
|
||||
}, [src, videoRef, autoPlay, onAutoPlayPrevented, onError]);
|
||||
}, [src, videoRef, autoPlay, onAutoPlayPrevented, onError, isAdFilterEnabled, adFilterMode, adKeywords]);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
import { settingsStore, AdFilterMode } from '@/lib/store/settings-store';
|
||||
|
||||
/**
|
||||
* Hook to access and update player settings from the settings store
|
||||
@@ -17,6 +17,9 @@ export function usePlayerSettings() {
|
||||
autoSkipOutro: stored.autoSkipOutro,
|
||||
skipOutroSeconds: stored.skipOutroSeconds,
|
||||
showModeIndicator: stored.showModeIndicator,
|
||||
adFilter: stored.adFilter,
|
||||
adFilterMode: stored.adFilterMode,
|
||||
adKeywords: stored.adKeywords,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -31,6 +34,9 @@ export function usePlayerSettings() {
|
||||
autoSkipOutro: stored.autoSkipOutro,
|
||||
skipOutroSeconds: stored.skipOutroSeconds,
|
||||
showModeIndicator: stored.showModeIndicator,
|
||||
adFilter: stored.adFilter,
|
||||
adFilterMode: stored.adFilterMode,
|
||||
adKeywords: stored.adKeywords,
|
||||
});
|
||||
});
|
||||
return unsubscribe;
|
||||
@@ -71,6 +77,18 @@ export function usePlayerSettings() {
|
||||
updateSetting('showModeIndicator', value);
|
||||
}, [updateSetting]);
|
||||
|
||||
const setAdFilter = useCallback((value: boolean) => {
|
||||
updateSetting('adFilter', value);
|
||||
}, [updateSetting]);
|
||||
|
||||
const setAdFilterMode = useCallback((value: AdFilterMode) => {
|
||||
updateSetting('adFilterMode', value);
|
||||
}, [updateSetting]);
|
||||
|
||||
const setAdKeywords = useCallback((value: string[]) => {
|
||||
updateSetting('adKeywords', value);
|
||||
}, [updateSetting]);
|
||||
|
||||
return {
|
||||
...settings,
|
||||
setAutoNextEpisode,
|
||||
@@ -79,5 +97,8 @@ export function usePlayerSettings() {
|
||||
setAutoSkipOutro,
|
||||
setSkipOutroSeconds,
|
||||
setShowModeIndicator,
|
||||
setAdFilter,
|
||||
setAdFilterMode,
|
||||
setAdKeywords,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,14 @@ export const UtilityIcons = {
|
||||
</svg>
|
||||
),
|
||||
|
||||
ShieldAlert: ({ className = "", size = 24 }: IconProps) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
),
|
||||
|
||||
Sparkles: ({ className = "", size = 24 }: IconProps) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||
<path d="M12 3v18M5.2 8.2l13.6 7.6M18.8 8.2L5.2 15.8" />
|
||||
|
||||
Reference in New Issue
Block a user