mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
feat: Add initial support for Android TV and Apple TV platforms, introduce a dedicated premium mode settings store, and implement personalized video recommendations.
This commit is contained in:
@@ -25,7 +25,8 @@ interface HistoryActions {
|
||||
playbackPosition: number,
|
||||
duration: number,
|
||||
poster?: string,
|
||||
episodes?: Episode[]
|
||||
episodes?: Episode[],
|
||||
metadata?: { vod_actor?: string; type_name?: string; vod_area?: string }
|
||||
) => void;
|
||||
|
||||
removeFromHistory: (videoId: string | number, source: string) => void;
|
||||
@@ -61,7 +62,8 @@ const createHistoryStore = (name: string) =>
|
||||
playbackPosition,
|
||||
duration,
|
||||
poster,
|
||||
episodes = []
|
||||
episodes = [],
|
||||
metadata
|
||||
) => {
|
||||
const showIdentifier = generateShowIdentifier(title, source, videoId);
|
||||
const timestamp = Date.now();
|
||||
@@ -84,6 +86,9 @@ const createHistoryStore = (name: string) =>
|
||||
duration,
|
||||
timestamp,
|
||||
episodes: episodes.length > 0 ? episodes : state.viewingHistory[existingIndex].episodes,
|
||||
vod_actor: metadata?.vod_actor ?? state.viewingHistory[existingIndex].vod_actor,
|
||||
type_name: metadata?.type_name ?? state.viewingHistory[existingIndex].type_name,
|
||||
vod_area: metadata?.vod_area ?? state.viewingHistory[existingIndex].vod_area,
|
||||
};
|
||||
|
||||
newHistory = [
|
||||
@@ -104,6 +109,9 @@ const createHistoryStore = (name: string) =>
|
||||
poster,
|
||||
episodes,
|
||||
showIdentifier,
|
||||
vod_actor: metadata?.vod_actor,
|
||||
type_name: metadata?.type_name,
|
||||
vod_area: metadata?.vod_area,
|
||||
};
|
||||
|
||||
newHistory = [newItem, ...state.viewingHistory];
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Premium Mode Settings Store
|
||||
* Stores player/display settings separately for premium mode.
|
||||
* Mirrors the relevant subset of AppSettings but uses its own localStorage key.
|
||||
*/
|
||||
|
||||
import type { SortOption, SearchDisplayMode, ProxyMode, AdFilterMode } from './settings-store';
|
||||
|
||||
const PREMIUM_MODE_SETTINGS_KEY = 'kvideo-premium-mode-settings';
|
||||
|
||||
export interface ModeSettings {
|
||||
sortBy: SortOption;
|
||||
autoNextEpisode: boolean;
|
||||
autoSkipIntro: boolean;
|
||||
skipIntroSeconds: number;
|
||||
autoSkipOutro: boolean;
|
||||
skipOutroSeconds: number;
|
||||
showModeIndicator: boolean;
|
||||
adFilterMode: AdFilterMode;
|
||||
fullscreenType: 'auto' | 'native' | 'window';
|
||||
proxyMode: ProxyMode;
|
||||
realtimeLatency: boolean;
|
||||
searchDisplayMode: SearchDisplayMode;
|
||||
episodeReverseOrder: boolean;
|
||||
rememberScrollPosition: boolean;
|
||||
personalizedRecommendations: boolean;
|
||||
danmakuEnabled: boolean;
|
||||
danmakuApiUrl: string;
|
||||
danmakuOpacity: number;
|
||||
danmakuFontSize: number;
|
||||
}
|
||||
|
||||
function getDefaultModeSettings(): ModeSettings {
|
||||
return {
|
||||
sortBy: 'default',
|
||||
autoNextEpisode: true,
|
||||
autoSkipIntro: false,
|
||||
skipIntroSeconds: 0,
|
||||
autoSkipOutro: false,
|
||||
skipOutroSeconds: 0,
|
||||
showModeIndicator: false,
|
||||
adFilterMode: 'heuristic',
|
||||
fullscreenType: 'auto',
|
||||
proxyMode: 'retry',
|
||||
realtimeLatency: false,
|
||||
searchDisplayMode: 'normal',
|
||||
episodeReverseOrder: false,
|
||||
rememberScrollPosition: true,
|
||||
personalizedRecommendations: true,
|
||||
danmakuEnabled: false,
|
||||
danmakuApiUrl: process.env.NEXT_PUBLIC_DANMAKU_API_URL || '',
|
||||
danmakuOpacity: 0.7,
|
||||
danmakuFontSize: 20,
|
||||
};
|
||||
}
|
||||
|
||||
export const premiumModeSettingsStore = {
|
||||
getSettings(): ModeSettings {
|
||||
if (typeof window === 'undefined') {
|
||||
return getDefaultModeSettings();
|
||||
}
|
||||
|
||||
const stored = localStorage.getItem(PREMIUM_MODE_SETTINGS_KEY);
|
||||
if (!stored) {
|
||||
return getDefaultModeSettings();
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
return {
|
||||
sortBy: parsed.sortBy || 'default',
|
||||
autoNextEpisode: parsed.autoNextEpisode !== undefined ? parsed.autoNextEpisode : true,
|
||||
autoSkipIntro: parsed.autoSkipIntro !== undefined ? parsed.autoSkipIntro : false,
|
||||
skipIntroSeconds: typeof parsed.skipIntroSeconds === 'number' ? parsed.skipIntroSeconds : 0,
|
||||
autoSkipOutro: parsed.autoSkipOutro !== undefined ? parsed.autoSkipOutro : false,
|
||||
skipOutroSeconds: typeof parsed.skipOutroSeconds === 'number' ? parsed.skipOutroSeconds : 0,
|
||||
showModeIndicator: parsed.showModeIndicator !== undefined ? parsed.showModeIndicator : false,
|
||||
adFilterMode: parsed.adFilterMode || 'heuristic',
|
||||
fullscreenType: (parsed.fullscreenType === 'window' || parsed.fullscreenType === 'native' || parsed.fullscreenType === 'auto') ? parsed.fullscreenType : 'auto',
|
||||
proxyMode: (parsed.proxyMode === 'retry' || parsed.proxyMode === 'none' || parsed.proxyMode === 'always') ? parsed.proxyMode : 'retry',
|
||||
realtimeLatency: parsed.realtimeLatency !== undefined ? parsed.realtimeLatency : false,
|
||||
searchDisplayMode: parsed.searchDisplayMode === 'grouped' ? 'grouped' : 'normal',
|
||||
episodeReverseOrder: parsed.episodeReverseOrder !== undefined ? parsed.episodeReverseOrder : false,
|
||||
rememberScrollPosition: parsed.rememberScrollPosition !== undefined ? parsed.rememberScrollPosition : true,
|
||||
personalizedRecommendations: parsed.personalizedRecommendations !== undefined ? parsed.personalizedRecommendations : true,
|
||||
danmakuEnabled: parsed.danmakuEnabled !== undefined ? parsed.danmakuEnabled : false,
|
||||
danmakuApiUrl: typeof parsed.danmakuApiUrl === 'string' ? (parsed.danmakuApiUrl || process.env.NEXT_PUBLIC_DANMAKU_API_URL || '') : (process.env.NEXT_PUBLIC_DANMAKU_API_URL || ''),
|
||||
danmakuOpacity: typeof parsed.danmakuOpacity === 'number' ? parsed.danmakuOpacity : 0.7,
|
||||
danmakuFontSize: typeof parsed.danmakuFontSize === 'number' ? parsed.danmakuFontSize : 20,
|
||||
};
|
||||
} catch {
|
||||
return getDefaultModeSettings();
|
||||
}
|
||||
},
|
||||
|
||||
listeners: new Set<() => void>(),
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
},
|
||||
|
||||
notifyListeners(): void {
|
||||
this.listeners.forEach((listener) => listener());
|
||||
},
|
||||
|
||||
saveSettings(settings: ModeSettings): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(PREMIUM_MODE_SETTINGS_KEY, JSON.stringify(settings));
|
||||
this.notifyListeners();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to get mode-specific settings from the correct store.
|
||||
* Normal mode reads from settingsStore, premium mode reads from premiumModeSettingsStore.
|
||||
*/
|
||||
export function getModeSettings(isPremium: boolean): ModeSettings {
|
||||
if (isPremium) {
|
||||
return premiumModeSettingsStore.getSettings();
|
||||
}
|
||||
// For normal mode, extract ModeSettings-shaped data from the main settingsStore
|
||||
// Import dynamically to avoid circular dependencies
|
||||
const { settingsStore } = require('./settings-store');
|
||||
const s = settingsStore.getSettings();
|
||||
return {
|
||||
sortBy: s.sortBy,
|
||||
autoNextEpisode: s.autoNextEpisode,
|
||||
autoSkipIntro: s.autoSkipIntro,
|
||||
skipIntroSeconds: s.skipIntroSeconds,
|
||||
autoSkipOutro: s.autoSkipOutro,
|
||||
skipOutroSeconds: s.skipOutroSeconds,
|
||||
showModeIndicator: s.showModeIndicator,
|
||||
adFilterMode: s.adFilterMode,
|
||||
fullscreenType: s.fullscreenType,
|
||||
proxyMode: s.proxyMode,
|
||||
realtimeLatency: s.realtimeLatency,
|
||||
searchDisplayMode: s.searchDisplayMode,
|
||||
episodeReverseOrder: s.episodeReverseOrder,
|
||||
rememberScrollPosition: s.rememberScrollPosition,
|
||||
personalizedRecommendations: s.personalizedRecommendations,
|
||||
danmakuEnabled: s.danmakuEnabled,
|
||||
danmakuApiUrl: s.danmakuApiUrl,
|
||||
danmakuOpacity: s.danmakuOpacity,
|
||||
danmakuFontSize: s.danmakuFontSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the settings store for a given mode.
|
||||
*/
|
||||
export function getModeSettingsStore(isPremium: boolean) {
|
||||
if (isPremium) {
|
||||
return premiumModeSettingsStore;
|
||||
}
|
||||
// Return a wrapper around the main settingsStore that conforms to the same interface
|
||||
const { settingsStore } = require('./settings-store');
|
||||
return {
|
||||
getSettings: () => getModeSettings(false),
|
||||
subscribe: (listener: () => void) => settingsStore.subscribe(listener),
|
||||
saveSettings: (modeSettings: ModeSettings) => {
|
||||
const current = settingsStore.getSettings();
|
||||
settingsStore.saveSettings({
|
||||
...current,
|
||||
...modeSettings,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -45,6 +45,7 @@ export interface AppSettings {
|
||||
fullscreenType: 'auto' | 'native' | 'window'; // Fullscreen mode preference: 'auto' (native on desktop, window on mobile) | 'native' | 'window'
|
||||
proxyMode: ProxyMode; // Proxy behavior: 'retry' | 'none' | 'always'
|
||||
rememberScrollPosition: boolean; // Remember scroll position when navigating back or refreshing
|
||||
personalizedRecommendations: boolean; // Show personalized recommendations based on watch history
|
||||
// Danmaku settings
|
||||
danmakuEnabled: boolean; // Show danmaku overlay on video
|
||||
danmakuApiUrl: string; // Self-hosted danmaku API endpoint
|
||||
@@ -122,6 +123,7 @@ function getDefaultAppSettings(): AppSettings {
|
||||
fullscreenType: 'auto',
|
||||
proxyMode: 'retry',
|
||||
rememberScrollPosition: true,
|
||||
personalizedRecommendations: true,
|
||||
danmakuEnabled: false,
|
||||
danmakuApiUrl: process.env.NEXT_PUBLIC_DANMAKU_API_URL || '',
|
||||
danmakuOpacity: 0.7,
|
||||
@@ -202,6 +204,7 @@ export const settingsStore = {
|
||||
fullscreenType: (parsed.fullscreenType === 'window' || parsed.fullscreenType === 'native' || parsed.fullscreenType === 'auto') ? parsed.fullscreenType : 'auto',
|
||||
proxyMode: (parsed.proxyMode === 'retry' || parsed.proxyMode === 'none' || parsed.proxyMode === 'always') ? parsed.proxyMode : 'retry',
|
||||
rememberScrollPosition: parsed.rememberScrollPosition !== undefined ? parsed.rememberScrollPosition : true,
|
||||
personalizedRecommendations: parsed.personalizedRecommendations !== undefined ? parsed.personalizedRecommendations : true,
|
||||
danmakuEnabled: parsed.danmakuEnabled !== undefined ? parsed.danmakuEnabled : false,
|
||||
danmakuApiUrl: typeof parsed.danmakuApiUrl === 'string' ? (parsed.danmakuApiUrl || process.env.NEXT_PUBLIC_DANMAKU_API_URL || '') : (process.env.NEXT_PUBLIC_DANMAKU_API_URL || ''),
|
||||
danmakuOpacity: typeof parsed.danmakuOpacity === 'number' ? parsed.danmakuOpacity : 0.7,
|
||||
|
||||
Reference in New Issue
Block a user