diff --git a/components/player/hooks/desktop/useFullscreenControls.ts b/components/player/hooks/desktop/useFullscreenControls.ts
index a834a91..2a6acf7 100644
--- a/components/player/hooks/desktop/useFullscreenControls.ts
+++ b/components/player/hooks/desktop/useFullscreenControls.ts
@@ -9,6 +9,7 @@ interface UseFullscreenControlsProps {
isAirPlaySupported: boolean;
setIsPiPSupported: (supported: boolean) => void;
setIsAirPlaySupported: (supported: boolean) => void;
+ fullscreenType?: 'native' | 'window';
}
export function useFullscreenControls({
@@ -19,7 +20,8 @@ export function useFullscreenControls({
isPiPSupported,
isAirPlaySupported,
setIsPiPSupported,
- setIsAirPlaySupported
+ setIsAirPlaySupported,
+ fullscreenType = 'native'
}: UseFullscreenControlsProps) {
useEffect(() => {
if (typeof document !== 'undefined') {
@@ -34,6 +36,11 @@ export function useFullscreenControls({
if (!containerRef.current) return;
if (!isFullscreen) {
+ if (fullscreenType === 'window') {
+ setIsFullscreen(true);
+ return;
+ }
+
try {
if (containerRef.current.requestFullscreen) {
await containerRef.current.requestFullscreen();
@@ -44,11 +51,9 @@ export function useFullscreenControls({
} else if ((containerRef.current as any).msRequestFullscreen) {
await (containerRef.current as any).msRequestFullscreen();
} else if (videoRef.current && (videoRef.current as any).webkitEnterFullscreen) {
- // Fallback for browsers that only support fullscreen on video element (like some car browsers)
(videoRef.current as any).webkitEnterFullscreen();
}
- // Lock orientation to landscape on mobile devices if supported
if (window.screen && (window.screen as any).orientation && (window.screen as any).orientation.lock) {
try {
await (window.screen as any).orientation.lock('landscape');
@@ -58,7 +63,6 @@ export function useFullscreenControls({
}
} catch (error) {
console.warn('Fullscreen request failed, trying fallback:', error);
- // Last ditch effort: try native video fullscreen if container failed
if (videoRef.current && (videoRef.current as any).webkitEnterFullscreen) {
try {
(videoRef.current as any).webkitEnterFullscreen();
@@ -68,6 +72,11 @@ export function useFullscreenControls({
}
}
} else {
+ if (fullscreenType === 'window') {
+ setIsFullscreen(false);
+ return;
+ }
+
try {
if (document.exitFullscreen) {
await document.exitFullscreen();
@@ -79,7 +88,6 @@ export function useFullscreenControls({
await (document as any).msExitFullscreen();
}
- // Unlock orientation when exiting fullscreen
if (window.screen && (window.screen as any).orientation && (window.screen as any).orientation.unlock) {
try {
(window.screen as any).orientation.unlock();
@@ -91,7 +99,7 @@ export function useFullscreenControls({
console.error('Failed to exit fullscreen:', error);
}
}
- }, [containerRef, videoRef, isFullscreen]);
+ }, [containerRef, videoRef, isFullscreen, fullscreenType, setIsFullscreen]);
useEffect(() => {
const handleFullscreenChange = () => {
@@ -101,9 +109,12 @@ export function useFullscreenControls({
(document as any).mozFullScreenElement ||
(document as any).msFullscreenElement
);
- setIsFullscreen(isInFullscreen);
- // Double check orientation lock/unlock on change
+ // Only update if not in window mode, or if exiting native mode
+ if (fullscreenType === 'native' || !isInFullscreen) {
+ setIsFullscreen(isInFullscreen);
+ }
+
if (isInFullscreen) {
if (window.screen && (window.screen as any).orientation && (window.screen as any).orientation.lock) {
(window.screen as any).orientation.lock('landscape').catch(() => { });
@@ -128,7 +139,19 @@ export function useFullscreenControls({
document.removeEventListener('mozfullscreenchange', handleFullscreenChange);
document.removeEventListener('MSFullscreenChange', handleFullscreenChange);
};
- }, [setIsFullscreen]);
+ }, [setIsFullscreen, fullscreenType]);
+
+ useEffect(() => {
+ if (isFullscreen && fullscreenType === 'window') {
+ const handleEsc = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setIsFullscreen(false);
+ }
+ };
+ window.addEventListener('keydown', handleEsc);
+ return () => window.removeEventListener('keydown', handleEsc);
+ }
+ }, [isFullscreen, fullscreenType, setIsFullscreen]);
const togglePictureInPicture = useCallback(async () => {
if (!videoRef.current || !isPiPSupported) return;
diff --git a/components/player/hooks/useDesktopPlayerLogic.ts b/components/player/hooks/useDesktopPlayerLogic.ts
index e8c7bc9..1d7a47a 100644
--- a/components/player/hooks/useDesktopPlayerLogic.ts
+++ b/components/player/hooks/useDesktopPlayerLogic.ts
@@ -22,6 +22,7 @@ interface UseDesktopPlayerLogicProps {
refs: DesktopPlayerState['refs'];
data: DesktopPlayerState['data'];
actions: DesktopPlayerState['actions'];
+ fullscreenType?: 'native' | 'window';
}
export function useDesktopPlayerLogic({
@@ -32,7 +33,8 @@ export function useDesktopPlayerLogic({
onTimeUpdate,
refs,
data,
- actions
+ actions,
+ fullscreenType = 'native'
}: UseDesktopPlayerLogicProps) {
const {
videoRef, containerRef, progressBarRef, volumeBarRef,
@@ -117,7 +119,8 @@ export function useDesktopPlayerLogic({
const fullscreenControls = useFullscreenControls({
containerRef, videoRef, isFullscreen, setIsFullscreen,
- isPiPSupported, isAirPlaySupported, setIsPiPSupported, setIsAirPlaySupported
+ isPiPSupported, isAirPlaySupported, setIsPiPSupported, setIsAirPlaySupported,
+ fullscreenType
});
const controlsVisibility = useControlsVisibility({
diff --git a/components/player/hooks/usePlayerSettings.ts b/components/player/hooks/usePlayerSettings.ts
index 22208ac..58976db 100644
--- a/components/player/hooks/usePlayerSettings.ts
+++ b/components/player/hooks/usePlayerSettings.ts
@@ -20,6 +20,7 @@ export function usePlayerSettings() {
adFilter: stored.adFilter,
adFilterMode: stored.adFilterMode,
adKeywords: stored.adKeywords,
+ fullscreenType: stored.fullscreenType,
};
});
@@ -37,6 +38,7 @@ export function usePlayerSettings() {
adFilter: stored.adFilter,
adFilterMode: stored.adFilterMode,
adKeywords: stored.adKeywords,
+ fullscreenType: stored.fullscreenType,
});
});
return unsubscribe;
@@ -89,6 +91,10 @@ export function usePlayerSettings() {
updateSetting('adKeywords', value);
}, [updateSetting]);
+ const setFullscreenType = useCallback((value: 'native' | 'window') => {
+ updateSetting('fullscreenType', value);
+ }, [updateSetting]);
+
return {
...settings,
setAutoNextEpisode,
@@ -100,5 +106,6 @@ export function usePlayerSettings() {
setAdFilter,
setAdFilterMode,
setAdKeywords,
+ setFullscreenType,
};
}
diff --git a/components/player/web-fullscreen.css b/components/player/web-fullscreen.css
new file mode 100644
index 0000000..338e450
--- /dev/null
+++ b/components/player/web-fullscreen.css
@@ -0,0 +1,18 @@
+/* Web Fullscreen (Window Fullscreen) Styles */
+.kvideo-container.is-web-fullscreen {
+ position: fixed !important;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100vw !important;
+ height: 100vh !important;
+ z-index: 9999 !important;
+ background: black !important;
+ border-radius: 0 !important;
+}
+
+/* Ensure controls and video take up full space in web fullscreen */
+.kvideo-container.is-web-fullscreen video {
+ width: 100% !important;
+ height: 100% !important;
+ border-radius: 0 !important;
+}
diff --git a/components/ui/icons/utility-icons.tsx b/components/ui/icons/utility-icons.tsx
index d0d554d..98321a8 100644
--- a/components/ui/icons/utility-icons.tsx
+++ b/components/ui/icons/utility-icons.tsx
@@ -172,4 +172,10 @@ export const UtilityIcons = {
),
+ Settings: ({ className = "", size = 24 }: IconProps) => (
+
+ ),
};
diff --git a/lib/store/settings-store.ts b/lib/store/settings-store.ts
index d2af77c..8272613 100644
--- a/lib/store/settings-store.ts
+++ b/lib/store/settings-store.ts
@@ -43,6 +43,7 @@ export interface AppSettings {
realtimeLatency: boolean; // Enable real-time latency ping updates
searchDisplayMode: SearchDisplayMode; // 'normal' = individual cards, 'grouped' = group same-name videos
episodeReverseOrder: boolean; // Persist episode list reverse state
+ fullscreenType: 'native' | 'window'; // Fullscreen mode preference
}
import { exportSettings, importSettings, SEARCH_HISTORY_KEY, WATCH_HISTORY_KEY } from './settings-helpers';
@@ -114,6 +115,7 @@ function getDefaultAppSettings(): AppSettings {
realtimeLatency: false,
searchDisplayMode: 'normal',
episodeReverseOrder: false,
+ fullscreenType: 'native',
};
}
@@ -189,6 +191,7 @@ export const settingsStore = {
realtimeLatency: parsed.realtimeLatency !== undefined ? parsed.realtimeLatency : false,
searchDisplayMode: parsed.searchDisplayMode === 'grouped' ? 'grouped' : 'normal',
episodeReverseOrder: parsed.episodeReverseOrder !== undefined ? parsed.episodeReverseOrder : false,
+ fullscreenType: parsed.fullscreenType === 'window' ? 'window' : 'native',
};
} catch {
// Even if localStorage fails, we should return defaults + ENV subscriptions
diff --git a/package-lock.json b/package-lock.json
index 468d6dc..c14e680 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "kvideo",
- "version": "4.0.2",
+ "version": "4.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kvideo",
- "version": "4.0.2",
+ "version": "4.0.3",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
diff --git a/package.json b/package.json
index f4c0496..03daebe 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "kvideo",
- "version": "4.0.2",
+ "version": "4.0.3",
"private": true,
"scripts": {
"dev": "next dev",