first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
import {FullscreenAdapter} from '@common/player/state/fullscreen/fullscreen-adapter';
import {IS_IPHONE} from '@common/utils/platform';
export function createIphoneFullscreenAdapter(
host: HTMLVideoElement,
onChange: () => void
): FullscreenAdapter {
return {
/**
* @link https://developer.apple.com/documentation/webkitjs/htmlvideoelement/1631913-webkitpresentationmode
*/
isFullscreen: () => {
return host.webkitPresentationMode === 'fullscreen';
},
/**
* @link https://developer.apple.com/documentation/webkitjs/htmlvideoelement/1628805-webkitsupportsfullscreen
*/
canFullScreen: () => {
return (
IS_IPHONE &&
typeof host.webkitSetPresentationMode === 'function' &&
(host.webkitSupportsFullscreen ?? false)
);
},
enter: () => {
return host.webkitSetPresentationMode?.('fullscreen');
},
exit: () => {
return host.webkitSetPresentationMode?.('inline');
},
bindEvents: () => {
host.removeEventListener('webkitpresentationmodechanged', onChange);
},
unbindEvents: () => {
host.addEventListener('webkitpresentationmodechanged', onChange);
},
};
}

View File

@@ -0,0 +1,40 @@
import {FullscreenAdapter} from '@common/player/state/fullscreen/fullscreen-adapter';
import fscreen from 'fscreen';
export function createNativeFullscreenAdapter(
host: HTMLElement,
onChange: () => void
): FullscreenAdapter {
host = host.closest('.fullscreen-host') ?? host;
return {
isFullscreen: () => {
if (fscreen.fullscreenElement === host) return true;
try {
// Throws in iOS Safari...
return host.matches(
// @ts-expect-error - `fullscreenPseudoClass` is missing from `@types/fscreen`.
fscreen.fullscreenPseudoClass
);
} catch (error) {
return false;
}
},
canFullScreen: () => {
return fscreen.fullscreenEnabled;
},
enter: () => {
return fscreen.requestFullscreen(host);
},
exit: () => {
return fscreen.exitFullscreen();
},
bindEvents: () => {
fscreen.addEventListener('fullscreenchange', onChange);
fscreen.addEventListener('fullscreenerror', onChange);
},
unbindEvents: () => {
fscreen.removeEventListener('fullscreenchange', onChange);
fscreen.removeEventListener('fullscreenerror', onChange);
},
};
}

View File

@@ -0,0 +1,8 @@
export interface FullscreenAdapter {
isFullscreen: () => boolean;
canFullScreen: () => boolean;
enter: () => void;
exit: () => void;
bindEvents: () => void;
unbindEvents: () => void;
}

View File

@@ -0,0 +1,112 @@
import {StateCreator} from 'zustand';
import {
PlayerState,
ProviderListeners,
} from '@common/player/state/player-state';
import {ScreenOrientation} from '@common/player/state/fullscreen/screen-orientation';
import {IS_IPHONE} from '@common/utils/platform';
import {FullscreenAdapter} from '@common/player/state/fullscreen/fullscreen-adapter';
import {createNativeFullscreenAdapter} from '@common/player/state/fullscreen/create-native-fullscreen-adapter';
import {createIphoneFullscreenAdapter} from '@common/player/state/fullscreen/create-iphone-fullscreen-adapter';
import {PipSlice} from '@common/player/state/pip/pip-slice';
export interface FullscreenSlice {
isFullscreen: boolean;
canFullscreen: boolean;
enterFullscreen: () => void;
exitFullscreen: () => void;
toggleFullscreen: () => void;
initFullscreen: () => void;
destroyFullscreen: () => void;
}
type BaseSliceCreator = StateCreator<
FullscreenSlice & PlayerState & PipSlice,
[['zustand/immer', unknown]],
[],
FullscreenSlice
>;
type StoreLice = BaseSliceCreator extends (...a: infer U) => infer R
? (...a: [...U, Set<Partial<ProviderListeners>>]) => R
: never;
const iPhoneProviderBlacklist = ['youtube'];
export const createFullscreenSlice: StoreLice = (set, get) => {
let subscription: () => void | undefined;
const orientation = new ScreenOrientation();
let adapter: FullscreenAdapter | undefined;
const onFullscreenChange = async () => {
const isFullscreen = adapter?.isFullscreen();
if (isFullscreen) {
// lock orientation to landscape
orientation.lock();
} else {
orientation.unlock();
}
set({isFullscreen});
};
const isSupported = (): boolean => {
// iPhone only allows putting video element in fullscreen, and
// there's no way to get access to it with YouTube iframe api
if (IS_IPHONE && iPhoneProviderBlacklist.includes(get().providerName!)) {
return false;
}
return adapter?.canFullScreen() ?? false;
};
return {
isFullscreen: false,
canFullscreen: false,
enterFullscreen: () => {
if (!isSupported() || adapter?.isFullscreen()) return;
// exit pip if it's active
if (get().isPip) {
get().exitPip();
}
return adapter?.enter();
},
exitFullscreen: () => {
if (!adapter?.isFullscreen()) return;
return adapter.exit();
},
toggleFullscreen: () => {
if (get().isFullscreen) {
get().exitFullscreen();
} else {
get().enterFullscreen();
}
},
initFullscreen: () => {
subscription = get().subscribe({
providerReady: ({el}) => {
// when changing adapters, remove previous adapter events and exit fullscreen
adapter?.unbindEvents();
if (get().isFullscreen) {
adapter?.exit();
}
// create new adapter, and if fullscreen is supported, bind events
adapter = IS_IPHONE
? createIphoneFullscreenAdapter(
el as HTMLVideoElement,
onFullscreenChange
)
: createNativeFullscreenAdapter(el, onFullscreenChange);
const canFullscreen = isSupported();
set({canFullscreen});
if (canFullscreen) {
adapter.bindEvents();
}
},
});
},
destroyFullscreen: () => {
get().exitFullscreen();
subscription?.();
},
};
};

View File

@@ -0,0 +1,86 @@
export class ScreenOrientation {
protected currentLock: OrientationLockType | undefined;
async lock(lockType: OrientationLockType = 'landscape') {
if (!this.canOrientScreen() || this.currentLock) return;
try {
await (screen.orientation as any).lock(lockType);
this.currentLock = lockType;
} catch (e) {}
}
async unlock() {
if (!this.canOrientScreen() || !this.currentLock) return;
await screen.orientation.unlock();
}
canOrientScreen(): boolean {
return (
screen.orientation != null &&
!!(screen.orientation as any).lock &&
!!screen.orientation.unlock
);
}
}
export type OrientationLockType =
/**
* Any is an orientation that means the screen can be locked to any one of portrait-primary,
* portrait-secondary, landscape-primary and landscape-secondary.
*/
| 'any'
/**
* Landscape is an orientation where the screen width is greater than the screen height and
* depending on platform convention locking the screen to landscape can represent
* landscape-primary, landscape-secondary or both.
*/
| 'landscape'
/**
* Landscape-primary is an orientation where the screen width is greater than the screen height.
* If the device's natural orientation is landscape, then it is in landscape-primary when held
* in that position. If the device's natural orientation is portrait, the user agent sets
* landscape-primary from the two options as shown in the screen orientation values table.
*/
| 'landscape-primary'
/**
* Landscape-secondary is an orientation where the screen width is greater than the screen
* height. If the device's natural orientation is landscape, it is in landscape-secondary when
* rotated 180º from its natural orientation. If the device's natural orientation is portrait,
* the user agent sets landscape-secondary from the two options as shown in the screen
* orientation values table.
*/
| 'landscape-secondary'
/**
* Natural is an orientation that refers to either portrait-primary or landscape-primary
* depending on the device's usual orientation. This orientation is usually provided by the
* underlying operating system.
*/
| 'natural'
/**
* Portrait is an orientation where the screen width is less than or equal to the screen height
* and depending on platform convention locking the screen to portrait can represent
* portrait-primary, portrait-secondary or both.
*/
| 'portrait'
/**
* Portrait-primary is an orientation where the screen width is less than or equal to the screen
* height. If the device's natural orientation is portrait, then it is in portrait-primary when
* held in that position. If the device's natural orientation is landscape, the user agent sets
* portrait-primary from the two options as shown in the screen orientation values table.
*/
| 'portrait-primary'
/**
* Portrait-secondary is an orientation where the screen width is less than or equal to the
* screen height. If the device's natural orientation is portrait, then it is in
* portrait-secondary when rotated 180º from its natural position. If the device's natural
* orientation is landscape, the user agent sets portrait-secondary from the two options as
* shown in the screen orientation values table.
*/
| 'portrait-secondary';