mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
feat: Implement user-defined video sources and danmaku API management with new settings pages and a dedicated store.
This commit is contained in:
@@ -4,6 +4,7 @@ import { useSearchCache } from '@/lib/hooks/useSearchCache';
|
||||
import { useParallelSearch } from '@/lib/hooks/useParallelSearch';
|
||||
import { useSubscriptionSync } from '@/lib/hooks/useSubscriptionSync';
|
||||
import { settingsStore, type SortOption } from '@/lib/store/settings-store';
|
||||
import { userSourcesStore } from '@/lib/store/user-sources-store';
|
||||
|
||||
export function useHomePage() {
|
||||
useSubscriptionSync();
|
||||
@@ -48,11 +49,20 @@ export function useHomePage() {
|
||||
const settings = settingsStore.getSettings();
|
||||
const enabledSources = settings.sources.filter(s => s.enabled);
|
||||
|
||||
if (enabledSources.length === 0) {
|
||||
// Merge user personal sources
|
||||
const userSources = userSourcesStore.getSources().filter(s => s.enabled !== false);
|
||||
const allSources = [...enabledSources];
|
||||
for (const us of userSources) {
|
||||
if (!allSources.find(s => s.id === us.id)) {
|
||||
allSources.push(us);
|
||||
}
|
||||
}
|
||||
|
||||
if (allSources.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
performSearch(searchQuery, enabledSources, settings.sortBy);
|
||||
performSearch(searchQuery, allSources, settings.sortBy);
|
||||
hasSearchedWithSourcesRef.current = true;
|
||||
return true;
|
||||
}, [performSearch]);
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface AuthSession {
|
||||
profileId: string;
|
||||
name: string;
|
||||
role: Role;
|
||||
customPermissions?: Permission[];
|
||||
}
|
||||
|
||||
const SESSION_KEY = 'kvideo-session';
|
||||
@@ -76,7 +77,9 @@ export function isAdmin(): boolean {
|
||||
export function hasPermission(permission: Permission): boolean {
|
||||
const session = getSession();
|
||||
if (!session) return true; // No auth configured = full access
|
||||
return ROLE_PERMISSIONS[session.role]?.includes(permission) ?? false;
|
||||
if (ROLE_PERMISSIONS[session.role]?.includes(permission)) return true;
|
||||
if (session.customPermissions?.includes(permission)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function hasRole(minimumRole: Role): boolean {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* User Sources Store - Per-user video sources and danmaku APIs
|
||||
* Uses localStorage keyed by profileId for isolation between users
|
||||
*/
|
||||
|
||||
import { getProfileId } from './auth-store';
|
||||
import type { VideoSource } from '@/lib/types';
|
||||
|
||||
export interface DanmakuApiEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface UserSourcesState {
|
||||
sources: VideoSource[];
|
||||
danmakuApis: DanmakuApiEntry[];
|
||||
activeDanmakuApiId: string | null;
|
||||
}
|
||||
|
||||
function getStorageKey(): string {
|
||||
const profileId = getProfileId();
|
||||
return `kvideo-user-sources-${profileId}`;
|
||||
}
|
||||
|
||||
function getState(): UserSourcesState {
|
||||
if (typeof window === 'undefined') {
|
||||
return { sources: [], danmakuApis: [], activeDanmakuApiId: null };
|
||||
}
|
||||
try {
|
||||
const raw = localStorage.getItem(getStorageKey());
|
||||
if (!raw) return { sources: [], danmakuApis: [], activeDanmakuApiId: null };
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
sources: Array.isArray(parsed.sources) ? parsed.sources : [],
|
||||
danmakuApis: Array.isArray(parsed.danmakuApis) ? parsed.danmakuApis : [],
|
||||
activeDanmakuApiId: parsed.activeDanmakuApiId ?? null,
|
||||
};
|
||||
} catch {
|
||||
return { sources: [], danmakuApis: [], activeDanmakuApiId: null };
|
||||
}
|
||||
}
|
||||
|
||||
function saveState(state: UserSourcesState): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(getStorageKey(), JSON.stringify(state));
|
||||
userSourcesStore.notifyListeners();
|
||||
}
|
||||
|
||||
export const userSourcesStore = {
|
||||
listeners: new Set<() => void>(),
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => { this.listeners.delete(listener); };
|
||||
},
|
||||
|
||||
notifyListeners(): void {
|
||||
this.listeners.forEach(l => l());
|
||||
},
|
||||
|
||||
getState,
|
||||
|
||||
getSources(): VideoSource[] {
|
||||
return getState().sources;
|
||||
},
|
||||
|
||||
getDanmakuApis(): DanmakuApiEntry[] {
|
||||
return getState().danmakuApis;
|
||||
},
|
||||
|
||||
getActiveDanmakuApi(): DanmakuApiEntry | null {
|
||||
const state = getState();
|
||||
if (!state.activeDanmakuApiId) return null;
|
||||
return state.danmakuApis.find(a => a.id === state.activeDanmakuApiId) ?? null;
|
||||
},
|
||||
|
||||
addSource(source: VideoSource): void {
|
||||
const state = getState();
|
||||
if (state.sources.find(s => s.id === source.id)) return;
|
||||
saveState({ ...state, sources: [...state.sources, { ...source, enabled: true }] });
|
||||
},
|
||||
|
||||
removeSource(id: string): void {
|
||||
const state = getState();
|
||||
saveState({ ...state, sources: state.sources.filter(s => s.id !== id) });
|
||||
},
|
||||
|
||||
toggleSource(id: string): void {
|
||||
const state = getState();
|
||||
saveState({
|
||||
...state,
|
||||
sources: state.sources.map(s =>
|
||||
s.id === id ? { ...s, enabled: !s.enabled } : s
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
addDanmakuApi(entry: DanmakuApiEntry): void {
|
||||
const state = getState();
|
||||
if (state.danmakuApis.find(a => a.id === entry.id)) return;
|
||||
saveState({ ...state, danmakuApis: [...state.danmakuApis, entry] });
|
||||
},
|
||||
|
||||
removeDanmakuApi(id: string): void {
|
||||
const state = getState();
|
||||
const newApis = state.danmakuApis.filter(a => a.id !== id);
|
||||
const newActiveId = state.activeDanmakuApiId === id ? null : state.activeDanmakuApiId;
|
||||
saveState({ ...state, danmakuApis: newApis, activeDanmakuApiId: newActiveId });
|
||||
},
|
||||
|
||||
setActiveDanmakuApi(id: string | null): void {
|
||||
const state = getState();
|
||||
saveState({ ...state, activeDanmakuApiId: id });
|
||||
},
|
||||
};
|
||||
@@ -51,6 +51,7 @@ export interface SourceBadge {
|
||||
id: string;
|
||||
name: string;
|
||||
count: number;
|
||||
typeName?: string;
|
||||
}
|
||||
|
||||
export interface TypeBadge {
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface M3UChannel {
|
||||
routes?: string[];
|
||||
sourceId?: string;
|
||||
sourceName?: string;
|
||||
httpUserAgent?: string;
|
||||
httpReferrer?: string;
|
||||
}
|
||||
|
||||
export interface M3UPlaylist {
|
||||
@@ -40,6 +42,8 @@ export function parseM3U(content: string): M3UPlaylist {
|
||||
const tvgLogoMatch = line.match(/tvg-logo="([^"]*)"/i);
|
||||
const groupTitleMatch = line.match(/group-title="([^"]*)"/i);
|
||||
const tvgIdMatch = line.match(/tvg-id="([^"]*)"/i);
|
||||
const httpUserAgentMatch = line.match(/http-user-agent="([^"]*)"/i);
|
||||
const httpReferrerMatch = line.match(/http-referrer="([^"]*)"/i);
|
||||
|
||||
if (tvgNameMatch) channel.tvgName = tvgNameMatch[1];
|
||||
if (tvgLogoMatch) channel.logo = tvgLogoMatch[1];
|
||||
@@ -48,6 +52,8 @@ export function parseM3U(content: string): M3UPlaylist {
|
||||
if (channel.group) groupSet.add(channel.group);
|
||||
}
|
||||
if (tvgIdMatch) channel.tvgId = tvgIdMatch[1];
|
||||
if (httpUserAgentMatch) channel.httpUserAgent = httpUserAgentMatch[1];
|
||||
if (httpReferrerMatch) channel.httpReferrer = httpReferrerMatch[1];
|
||||
|
||||
// Extract channel name (after last comma)
|
||||
const commaIndex = line.lastIndexOf(',');
|
||||
|
||||
Reference in New Issue
Block a user