gate VideoTogether behind settings

This commit is contained in:
kuekhaoyang
2026-04-02 22:29:27 +08:00
parent 37b6e160d7
commit e73416d76b
10 changed files with 207 additions and 48 deletions
+140
View File
@@ -0,0 +1,140 @@
'use client';
import { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';
import { settingsStore } from '@/lib/store/settings-store';
const HIDE_STYLE_ID = 'kvideo-videotogether-visibility';
const SCRIPT_ID = 'kvideo-videotogether-script';
declare global {
interface Window {
VideoTogetherLoading?: boolean;
VideoTogetherSettingEnabled?: boolean;
videoTogetherWebsiteSettingUrl?: string;
videoTogetherExtension?: unknown;
videoTogetherFlyPannel?: {
Minimize?: (isDefault?: boolean) => void;
} | null;
}
}
interface VideoTogetherControllerProps {
envEnabled: boolean;
scriptUrl: string;
settingUrl?: string;
}
function isSupportedRoute(pathname: string | null): boolean {
return pathname?.startsWith('/player') === true || pathname?.startsWith('/iptv') === true;
}
function syncMinimizedDefaults(forceCurrentPageMinimized: boolean) {
if (typeof window === 'undefined') {
return;
}
try {
localStorage.setItem('EnableMiniBar', JSON.stringify(true));
localStorage.setItem('MinimiseDefault', JSON.stringify(true));
if (forceCurrentPageMinimized) {
localStorage.setItem('VideoTogetherMinimizedHere', '1');
window.videoTogetherFlyPannel?.Minimize?.(true);
}
} catch {
// Ignore storage failures so player pages continue working.
}
}
function updateVisibility(hidden: boolean) {
if (typeof document === 'undefined') {
return;
}
let style = document.getElementById(HIDE_STYLE_ID) as HTMLStyleElement | null;
if (!style) {
style = document.createElement('style');
style.id = HIDE_STYLE_ID;
document.head.appendChild(style);
}
style.textContent = hidden
? `
#VideoTogetherWrapper,
#VideoTogetherfullscreenSWrapper,
#videoTogetherLoading,
#videoTogetherTxtMsgTouch {
display: none !important;
}
`
: `
#videoTogetherLoading {
display: none !important;
}
`;
}
export function VideoTogetherController({
envEnabled,
scriptUrl,
settingUrl,
}: VideoTogetherControllerProps) {
const pathname = usePathname();
const [videoTogetherEnabled, setVideoTogetherEnabled] = useState(false);
useEffect(() => {
const sync = () => {
setVideoTogetherEnabled(settingsStore.getSettings().videoTogetherEnabled);
};
sync();
return settingsStore.subscribe(sync);
}, []);
const supportedRoute = isSupportedRoute(pathname);
const shouldActivate = envEnabled && videoTogetherEnabled && supportedRoute;
useEffect(() => {
if (!envEnabled || !videoTogetherEnabled) {
updateVisibility(true);
return;
}
syncMinimizedDefaults(supportedRoute);
}, [envEnabled, videoTogetherEnabled, supportedRoute]);
useEffect(() => {
updateVisibility(!shouldActivate);
}, [shouldActivate]);
useEffect(() => {
if (!shouldActivate) {
return;
}
if (settingUrl) {
window.videoTogetherWebsiteSettingUrl = settingUrl;
}
if (
document.getElementById(SCRIPT_ID) ||
document.getElementById('videotogether-script') ||
window.VideoTogetherLoading ||
window.videoTogetherExtension ||
window.videoTogetherFlyPannel
) {
return;
}
const script = document.createElement('script');
script.id = SCRIPT_ID;
script.src = scriptUrl;
script.async = true;
document.body.appendChild(script);
}, [scriptUrl, settingUrl, shouldActivate]);
return null;
}
-30
View File
@@ -1,30 +0,0 @@
import Script from 'next/script';
const DEFAULT_VIDEOTOGETHER_SCRIPT_URL =
'https://fastly.jsdelivr.net/gh/VideoTogether/VideoTogether@latest/release/extension.website.user.js';
export function VideoTogetherScript() {
if (process.env.VIDEOTOGETHER_ENABLED === 'false') {
return null;
}
const scriptUrl = process.env.VIDEOTOGETHER_SCRIPT_URL?.trim() || DEFAULT_VIDEOTOGETHER_SCRIPT_URL;
const settingUrl = process.env.VIDEOTOGETHER_SETTING_URL?.trim();
return (
<>
{settingUrl ? (
<script
dangerouslySetInnerHTML={{
__html: `window.videoTogetherWebsiteSettingUrl = ${JSON.stringify(settingUrl)};`,
}}
/>
) : null}
<Script
id="videotogether-script"
src={scriptUrl}
strategy="afterInteractive"
/>
</>
);
}
+30
View File
@@ -7,6 +7,7 @@
import { Icons } from '@/components/ui/Icon';
import { useRuntimeFeatures } from '@/components/RuntimeFeaturesProvider';
import { Switch } from '@/components/ui/Switch';
import {
type ProxyMode,
DEFAULT_SEEK_STEP_SECONDS,
@@ -21,6 +22,8 @@ interface PlayerSettingsProps {
onProxyModeChange: (mode: ProxyMode) => void;
seekStepSeconds: number;
onSeekStepSecondsChange: (value: number) => void;
videoTogetherEnabled?: boolean;
onVideoTogetherEnabledChange?: (enabled: boolean) => void;
danmakuApiUrl: string;
onDanmakuApiUrlChange: (url: string) => void;
danmakuOpacity: number;
@@ -48,6 +51,8 @@ export function PlayerSettings({
onProxyModeChange,
seekStepSeconds,
onSeekStepSecondsChange,
videoTogetherEnabled,
onVideoTogetherEnabledChange,
danmakuApiUrl,
onDanmakuApiUrlChange,
danmakuOpacity,
@@ -209,6 +214,31 @@ export function PlayerSettings({
<div className="border-t border-[var(--glass-border)]" />
{typeof videoTogetherEnabled === 'boolean' && onVideoTogetherEnabledChange ? (
<>
<div>
<div className="flex items-center justify-between gap-4">
<div>
<h3 className="font-medium text-[var(--text-color)] mb-2 inline-flex items-center gap-2">
<Icons.Users size={18} className="text-[var(--accent-color)]" />
(VideoTogether)
</h3>
<p className="text-sm text-[var(--text-color-secondary)]">
IPTV
</p>
</div>
<Switch
checked={videoTogetherEnabled}
onChange={onVideoTogetherEnabledChange}
ariaLabel="一起看开关"
/>
</div>
</div>
<div className="border-t border-[var(--glass-border)]" />
</>
) : null}
{/* Danmaku Settings */}
<div>
<h3 className="font-medium text-[var(--text-color)] mb-2 inline-flex items-center gap-2">