mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-13 08:15:06 +01:00
Upgrade packages, bundling, performance
This commit is contained in:
parent
afe2b24c96
commit
48b708d569
136
package.json
136
package.json
@ -26,96 +26,98 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/auto-animate": "^0.7.0",
|
"@formkit/auto-animate": "^0.8.1",
|
||||||
"@headlessui/react": "^1.5.0",
|
"@headlessui/react": "^1.7.17",
|
||||||
"@movie-web/providers": "^1.1.5",
|
"@movie-web/providers": "^1.1.5",
|
||||||
"@noble/hashes": "^1.3.2",
|
"@noble/hashes": "^1.3.3",
|
||||||
"@react-spring/web": "^9.7.1",
|
"@react-spring/web": "^9.7.3",
|
||||||
"@scure/bip39": "^1.2.1",
|
"@scure/bip39": "^1.2.2",
|
||||||
"@sozialhelden/ietf-language-tags": "^5.4.2",
|
"@sozialhelden/ietf-language-tags": "^5.4.2",
|
||||||
"@types/node-forge": "^1.3.8",
|
"@types/node-forge": "^1.3.10",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"core-js": "^3.29.1",
|
"core-js": "^3.34.0",
|
||||||
"dompurify": "^3.0.1",
|
"dompurify": "^3.0.6",
|
||||||
"flag-icons": "^6.11.1",
|
"flag-icons": "^7.1.0",
|
||||||
"focus-trap-react": "^10.2.3",
|
"focus-trap-react": "^10.2.3",
|
||||||
"fscreen": "^1.2.0",
|
"fscreen": "^1.2.0",
|
||||||
"fuse.js": "^6.4.6",
|
"fuse.js": "^7.0.0",
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.4.14",
|
||||||
"i18next": "^22.4.5",
|
"i18next": "^23.7.11",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.3",
|
||||||
"iso-639-1": "^3.1.0",
|
"iso-639-1": "^3.1.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"million": "^2.6.4",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"ofetch": "^1.0.0",
|
"ofetch": "^1.3.3",
|
||||||
"react": "^17.0.2",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.2.0",
|
||||||
"react-ga4": "^2.0.0",
|
"react-ga4": "^2.1.0",
|
||||||
"react-google-recaptcha-v3": "^1.10.1",
|
"react-google-recaptcha-v3": "^1.10.1",
|
||||||
"react-helmet-async": "^1.3.0",
|
"react-helmet-async": "^2.0.4",
|
||||||
"react-i18next": "^12.1.1",
|
"react-i18next": "^14.0.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-lazy-with-preload": "^2.2.1",
|
||||||
|
"react-router-dom": "^6.21.1",
|
||||||
"react-sticky-el": "^2.1.0",
|
"react-sticky-el": "^2.1.0",
|
||||||
"react-turnstile": "^1.1.2",
|
"react-turnstile": "^1.1.2",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.2",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"subsrt-ts": "^2.1.1",
|
"subsrt-ts": "^2.1.2",
|
||||||
"zustand": "^4.3.9"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.21.3",
|
"@babel/core": "^7.23.6",
|
||||||
"@babel/preset-env": "^7.20.2",
|
"@babel/preset-env": "^7.23.6",
|
||||||
"@babel/preset-typescript": "^7.21.0",
|
"@babel/preset-typescript": "^7.23.3",
|
||||||
"@types/chromecast-caf-sender": "^1.0.5",
|
"@types/chromecast-caf-sender": "^1.0.8",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.2.1",
|
||||||
"@types/dompurify": "^2.4.0",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/fscreen": "^1.0.1",
|
"@types/fscreen": "^1.0.4",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/lodash.throttle": "^4.1.7",
|
"@types/lodash.throttle": "^4.1.9",
|
||||||
"@types/node": "^17.0.15",
|
"@types/node": "^20.10.5",
|
||||||
"@types/pako": "^2.0.0",
|
"@types/pako": "^2.0.3",
|
||||||
"@types/react": "^17.0.39",
|
"@types/react": "^18.2.45",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/react-helmet": "^6.1.6",
|
"@types/react-helmet": "^6.1.11",
|
||||||
"@types/react-router": "^5.1.20",
|
"@types/react-router": "^5.1.20",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-stickynode": "^4.0.0",
|
"@types/react-stickynode": "^4.0.3",
|
||||||
"@types/react-transition-group": "^4.4.5",
|
"@types/react-transition-group": "^4.4.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
||||||
"@typescript-eslint/parser": "^5.13.0",
|
"@typescript-eslint/parser": "^6.15.0",
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.16",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-airbnb": "19.0.4",
|
"eslint-config-airbnb": "19.0.4",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-import-resolver-typescript": "^2.5.0",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^5.1.1",
|
||||||
"eslint-plugin-react": "7.29.4",
|
"eslint-plugin-react": "7.33.2",
|
||||||
"eslint-plugin-react-hooks": "4.3.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"glob": "^10.3.3",
|
"glob": "^10.3.10",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.8",
|
||||||
"jsdom": "^21.1.0",
|
"jsdom": "^23.0.1",
|
||||||
"postcss": "^8.4.20",
|
"postcss": "^8.4.32",
|
||||||
"postcss-rtl": "^2.0.0",
|
"postcss-rtl": "^2.0.0",
|
||||||
"postcss-rtlcss": "^4.0.9",
|
"postcss-rtlcss": "^4.0.9",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^3.1.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.7",
|
"prettier-plugin-tailwindcss": "^0.5.9",
|
||||||
"tailwind-scrollbar": "^2.0.1",
|
"tailwind-scrollbar": "^3.0.5",
|
||||||
"tailwindcss": "^3.2.4",
|
"tailwindcss": "^3.4.0",
|
||||||
"tailwindcss-themer": "^3.1.0",
|
"tailwindcss-themer": "^4.0.0",
|
||||||
"type-fest": "^4.3.3",
|
"type-fest": "^4.8.3",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^4.4.12",
|
"vite": "^5.0.10",
|
||||||
"vite-plugin-checker": "^0.5.6",
|
"vite-plugin-checker": "^0.6.2",
|
||||||
"vite-plugin-package-version": "^1.0.2",
|
"vite-plugin-package-version": "^1.1.0",
|
||||||
"vite-plugin-pwa": "^0.16.5",
|
"vite-plugin-pwa": "^0.17.4",
|
||||||
"vite-plugin-static-copy": "^0.16.0",
|
"vite-plugin-static-copy": "^1.0.0",
|
||||||
"vitest": "^0.28.5"
|
"vitest": "^1.1.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
3593
pnpm-lock.yaml
3593
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,7 @@ export function getAuthHeaders(token: string): Record<string, string> {
|
|||||||
export async function accountLogin(
|
export async function accountLogin(
|
||||||
url: string,
|
url: string,
|
||||||
id: string,
|
id: string,
|
||||||
deviceName: string
|
deviceName: string,
|
||||||
): Promise<LoginResponse> {
|
): Promise<LoginResponse> {
|
||||||
return ofetch<LoginResponse>("/auth/login", {
|
return ofetch<LoginResponse>("/auth/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -19,7 +19,7 @@ export interface BookmarkInput {
|
|||||||
|
|
||||||
export function bookmarkMediaToInput(
|
export function bookmarkMediaToInput(
|
||||||
tmdbId: string,
|
tmdbId: string,
|
||||||
item: BookmarkMediaItem
|
item: BookmarkMediaItem,
|
||||||
): BookmarkInput {
|
): BookmarkInput {
|
||||||
return {
|
return {
|
||||||
meta: {
|
meta: {
|
||||||
@ -35,7 +35,7 @@ export function bookmarkMediaToInput(
|
|||||||
export async function addBookmark(
|
export async function addBookmark(
|
||||||
url: string,
|
url: string,
|
||||||
account: AccountWithToken,
|
account: AccountWithToken,
|
||||||
input: BookmarkInput
|
input: BookmarkInput,
|
||||||
) {
|
) {
|
||||||
return ofetch<BookmarkResponse>(
|
return ofetch<BookmarkResponse>(
|
||||||
`/users/${account.userId}/bookmarks/${input.tmdbId}`,
|
`/users/${account.userId}/bookmarks/${input.tmdbId}`,
|
||||||
@ -44,14 +44,14 @@ export async function addBookmark(
|
|||||||
headers: getAuthHeaders(account.token),
|
headers: getAuthHeaders(account.token),
|
||||||
baseURL: url,
|
baseURL: url,
|
||||||
body: input,
|
body: input,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeBookmark(
|
export async function removeBookmark(
|
||||||
url: string,
|
url: string,
|
||||||
account: AccountWithToken,
|
account: AccountWithToken,
|
||||||
id: string
|
id: string,
|
||||||
) {
|
) {
|
||||||
return ofetch<{ tmdbId: string }>(
|
return ofetch<{ tmdbId: string }>(
|
||||||
`/users/${account.userId}/bookmarks/${id}`,
|
`/users/${account.userId}/bookmarks/${id}`,
|
||||||
@ -59,6 +59,6 @@ export async function removeBookmark(
|
|||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: getAuthHeaders(account.token),
|
headers: getAuthHeaders(account.token),
|
||||||
baseURL: url,
|
baseURL: url,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ export function genMnemonic(): string {
|
|||||||
|
|
||||||
export async function signCode(
|
export async function signCode(
|
||||||
code: string,
|
code: string,
|
||||||
privateKey: Uint8Array
|
privateKey: Uint8Array,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
return forge.pki.ed25519.sign({
|
return forge.pki.ed25519.sign({
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
@ -91,7 +91,7 @@ export async function encryptData(data: string, secret: Uint8Array) {
|
|||||||
|
|
||||||
const cipher = forge.cipher.createCipher(
|
const cipher = forge.cipher.createCipher(
|
||||||
"AES-GCM",
|
"AES-GCM",
|
||||||
forge.util.createBuffer(secret)
|
forge.util.createBuffer(secret),
|
||||||
);
|
);
|
||||||
cipher.start({
|
cipher.start({
|
||||||
iv,
|
iv,
|
||||||
@ -104,7 +104,7 @@ export async function encryptData(data: string, secret: Uint8Array) {
|
|||||||
const tag = cipher.mode.tag;
|
const tag = cipher.mode.tag;
|
||||||
|
|
||||||
return `${forge.util.encode64(iv)}.${stringBufferToBase64(
|
return `${forge.util.encode64(iv)}.${stringBufferToBase64(
|
||||||
encryptedData
|
encryptedData,
|
||||||
)}.${stringBufferToBase64(tag)}` as const;
|
)}.${stringBufferToBase64(tag)}` as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ export function decryptData(data: string, secret: Uint8Array) {
|
|||||||
|
|
||||||
const decipher = forge.cipher.createDecipher(
|
const decipher = forge.cipher.createDecipher(
|
||||||
"AES-GCM",
|
"AES-GCM",
|
||||||
forge.util.createBuffer(secret)
|
forge.util.createBuffer(secret),
|
||||||
);
|
);
|
||||||
decipher.start({
|
decipher.start({
|
||||||
iv: base64ToStringBuffer(iv),
|
iv: base64ToStringBuffer(iv),
|
||||||
|
@ -9,7 +9,7 @@ import { ProgressInput } from "./progress";
|
|||||||
export function importProgress(
|
export function importProgress(
|
||||||
url: string,
|
url: string,
|
||||||
account: AccountWithToken,
|
account: AccountWithToken,
|
||||||
progressItems: ProgressInput[]
|
progressItems: ProgressInput[],
|
||||||
) {
|
) {
|
||||||
return ofetch<void>(`/users/${account.userId}/progress/import`, {
|
return ofetch<void>(`/users/${account.userId}/progress/import`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@ -22,7 +22,7 @@ export function importProgress(
|
|||||||
export function importBookmarks(
|
export function importBookmarks(
|
||||||
url: string,
|
url: string,
|
||||||
account: AccountWithToken,
|
account: AccountWithToken,
|
||||||
bookmarks: BookmarkInput[]
|
bookmarks: BookmarkInput[],
|
||||||
) {
|
) {
|
||||||
return ofetch<void>(`/users/${account.userId}/bookmarks`, {
|
return ofetch<void>(`/users/${account.userId}/bookmarks`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
@ -8,7 +8,7 @@ export interface ChallengeTokenResponse {
|
|||||||
|
|
||||||
export async function getLoginChallengeToken(
|
export async function getLoginChallengeToken(
|
||||||
url: string,
|
url: string,
|
||||||
publicKey: string
|
publicKey: string,
|
||||||
): Promise<ChallengeTokenResponse> {
|
): Promise<ChallengeTokenResponse> {
|
||||||
return ofetch<ChallengeTokenResponse>("/auth/login/start", {
|
return ofetch<ChallengeTokenResponse>("/auth/login/start", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -35,7 +35,7 @@ export interface LoginInput {
|
|||||||
|
|
||||||
export async function loginAccount(
|
export async function loginAccount(
|
||||||
url: string,
|
url: string,
|
||||||
data: LoginInput
|
data: LoginInput,
|
||||||
): Promise<LoginResponse> {
|
): Promise<LoginResponse> {
|
||||||
return ofetch<LoginResponse>("/auth/login/complete", {
|
return ofetch<LoginResponse>("/auth/login/complete", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -23,7 +23,7 @@ export interface ProgressInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function progressUpdateItemToInput(
|
export function progressUpdateItemToInput(
|
||||||
item: ProgressUpdateItem
|
item: ProgressUpdateItem,
|
||||||
): ProgressInput {
|
): ProgressInput {
|
||||||
return {
|
return {
|
||||||
duration: item.progress?.duration ?? 0,
|
duration: item.progress?.duration ?? 0,
|
||||||
@ -44,7 +44,7 @@ export function progressUpdateItemToInput(
|
|||||||
|
|
||||||
export function progressMediaItemToInputs(
|
export function progressMediaItemToInputs(
|
||||||
tmdbId: string,
|
tmdbId: string,
|
||||||
item: ProgressMediaItem
|
item: ProgressMediaItem,
|
||||||
): ProgressInput[] {
|
): ProgressInput[] {
|
||||||
if (item.type === "show") {
|
if (item.type === "show") {
|
||||||
return Object.entries(item.episodes).flatMap(([_, episode]) => ({
|
return Object.entries(item.episodes).flatMap(([_, episode]) => ({
|
||||||
@ -83,7 +83,7 @@ export function progressMediaItemToInputs(
|
|||||||
export async function setProgress(
|
export async function setProgress(
|
||||||
url: string,
|
url: string,
|
||||||
account: AccountWithToken,
|
account: AccountWithToken,
|
||||||
input: ProgressInput
|
input: ProgressInput,
|
||||||
) {
|
) {
|
||||||
return ofetch<ProgressResponse>(
|
return ofetch<ProgressResponse>(
|
||||||
`/users/${account.userId}/progress/${input.tmdbId}`,
|
`/users/${account.userId}/progress/${input.tmdbId}`,
|
||||||
@ -92,7 +92,7 @@ export async function setProgress(
|
|||||||
headers: getAuthHeaders(account.token),
|
headers: getAuthHeaders(account.token),
|
||||||
baseURL: url,
|
baseURL: url,
|
||||||
body: input,
|
body: input,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ export async function removeProgress(
|
|||||||
account: AccountWithToken,
|
account: AccountWithToken,
|
||||||
id: string,
|
id: string,
|
||||||
episodeId?: string,
|
episodeId?: string,
|
||||||
seasonId?: string
|
seasonId?: string,
|
||||||
) {
|
) {
|
||||||
await ofetch(`/users/${account.userId}/progress/${id}`, {
|
await ofetch(`/users/${account.userId}/progress/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
@ -9,7 +9,7 @@ export interface ChallengeTokenResponse {
|
|||||||
|
|
||||||
export async function getRegisterChallengeToken(
|
export async function getRegisterChallengeToken(
|
||||||
url: string,
|
url: string,
|
||||||
captchaToken?: string
|
captchaToken?: string,
|
||||||
): Promise<ChallengeTokenResponse> {
|
): Promise<ChallengeTokenResponse> {
|
||||||
return ofetch<ChallengeTokenResponse>("/auth/register/start", {
|
return ofetch<ChallengeTokenResponse>("/auth/register/start", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -42,7 +42,7 @@ export interface RegisterInput {
|
|||||||
|
|
||||||
export async function registerAccount(
|
export async function registerAccount(
|
||||||
url: string,
|
url: string,
|
||||||
data: RegisterInput
|
data: RegisterInput,
|
||||||
): Promise<RegisterResponse> {
|
): Promise<RegisterResponse> {
|
||||||
return ofetch<RegisterResponse>("/auth/register/complete", {
|
return ofetch<RegisterResponse>("/auth/register/complete", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -26,7 +26,7 @@ export async function getSessions(url: string, account: AccountWithToken) {
|
|||||||
export async function updateSession(
|
export async function updateSession(
|
||||||
url: string,
|
url: string,
|
||||||
account: AccountWithToken,
|
account: AccountWithToken,
|
||||||
update: SessionUpdate
|
update: SessionUpdate,
|
||||||
) {
|
) {
|
||||||
return ofetch<SessionResponse[]>(`/sessions/${account.sessionId}`, {
|
return ofetch<SessionResponse[]>(`/sessions/${account.sessionId}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
@ -39,7 +39,7 @@ export async function updateSession(
|
|||||||
export async function removeSession(
|
export async function removeSession(
|
||||||
url: string,
|
url: string,
|
||||||
token: string,
|
token: string,
|
||||||
sessionId: string
|
sessionId: string,
|
||||||
) {
|
) {
|
||||||
return ofetch<SessionResponse[]>(`/sessions/${sessionId}`, {
|
return ofetch<SessionResponse[]>(`/sessions/${sessionId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
@ -18,7 +18,7 @@ export interface SettingsResponse {
|
|||||||
export function updateSettings(
|
export function updateSettings(
|
||||||
url: string,
|
url: string,
|
||||||
account: AccountWithToken,
|
account: AccountWithToken,
|
||||||
settings: SettingsInput
|
settings: SettingsInput,
|
||||||
) {
|
) {
|
||||||
return ofetch<SettingsResponse>(`/users/${account.userId}/settings`, {
|
return ofetch<SettingsResponse>(`/users/${account.userId}/settings`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
@ -119,21 +119,21 @@ export function progressResponsesToEntries(responses: ProgressResponse[]) {
|
|||||||
|
|
||||||
export async function getUser(
|
export async function getUser(
|
||||||
url: string,
|
url: string,
|
||||||
token: string
|
token: string,
|
||||||
): Promise<{ user: UserResponse; session: SessionResponse }> {
|
): Promise<{ user: UserResponse; session: SessionResponse }> {
|
||||||
return ofetch<{ user: UserResponse; session: SessionResponse }>(
|
return ofetch<{ user: UserResponse; session: SessionResponse }>(
|
||||||
"/users/@me",
|
"/users/@me",
|
||||||
{
|
{
|
||||||
headers: getAuthHeaders(token),
|
headers: getAuthHeaders(token),
|
||||||
baseURL: url,
|
baseURL: url,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function editUser(
|
export async function editUser(
|
||||||
url: string,
|
url: string,
|
||||||
account: AccountWithToken,
|
account: AccountWithToken,
|
||||||
object: UserEdit
|
object: UserEdit,
|
||||||
): Promise<{ user: UserResponse; session: SessionResponse }> {
|
): Promise<{ user: UserResponse; session: SessionResponse }> {
|
||||||
return ofetch<{ user: UserResponse; session: SessionResponse }>(
|
return ofetch<{ user: UserResponse; session: SessionResponse }>(
|
||||||
`/users/${account.userId}`,
|
`/users/${account.userId}`,
|
||||||
@ -142,13 +142,13 @@ export async function editUser(
|
|||||||
headers: getAuthHeaders(account.token),
|
headers: getAuthHeaders(account.token),
|
||||||
body: object,
|
body: object,
|
||||||
baseURL: url,
|
baseURL: url,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteUser(
|
export async function deleteUser(
|
||||||
url: string,
|
url: string,
|
||||||
account: AccountWithToken
|
account: AccountWithToken,
|
||||||
): Promise<UserResponse> {
|
): Promise<UserResponse> {
|
||||||
return ofetch<UserResponse>(`/users/${account.userId}`, {
|
return ofetch<UserResponse>(`/users/${account.userId}`, {
|
||||||
headers: getAuthHeaders(account.token),
|
headers: getAuthHeaders(account.token),
|
||||||
|
@ -25,7 +25,7 @@ export function mwFetch<T>(url: string, ops: P<T>[1] = {}): R<T> {
|
|||||||
export async function singularProxiedFetch<T>(
|
export async function singularProxiedFetch<T>(
|
||||||
proxyUrl: string,
|
proxyUrl: string,
|
||||||
url: string,
|
url: string,
|
||||||
ops: P<T>[1] = {}
|
ops: P<T>[1] = {},
|
||||||
): R<T> {
|
): R<T> {
|
||||||
let combinedUrl = ops?.baseURL ?? "";
|
let combinedUrl = ops?.baseURL ?? "";
|
||||||
if (
|
if (
|
||||||
|
@ -96,7 +96,7 @@ export async function getApiToken(): Promise<string | null> {
|
|||||||
|
|
||||||
export async function connectServerSideEvents<T>(
|
export async function connectServerSideEvents<T>(
|
||||||
url: string,
|
url: string,
|
||||||
endEvents: string[]
|
endEvents: string[],
|
||||||
) {
|
) {
|
||||||
const apiToken = await getApiToken();
|
const apiToken = await getApiToken();
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ export function scrapeSourceOutputToProviderMetric(
|
|||||||
providerId: string,
|
providerId: string,
|
||||||
embedId: string | null,
|
embedId: string | null,
|
||||||
status: ProviderMetric["status"],
|
status: ProviderMetric["status"],
|
||||||
err: unknown | null
|
err: unknown | null,
|
||||||
): ProviderMetric {
|
): ProviderMetric {
|
||||||
const episodeId = media.episode?.tmdbId;
|
const episodeId = media.episode?.tmdbId;
|
||||||
const seasonId = media.season?.tmdbId;
|
const seasonId = media.season?.tmdbId;
|
||||||
@ -82,7 +82,7 @@ export function scrapeSourceOutputToProviderMetric(
|
|||||||
export function scrapeSegmentToProviderMetric(
|
export function scrapeSegmentToProviderMetric(
|
||||||
media: ScrapeMedia,
|
media: ScrapeMedia,
|
||||||
providerId: string,
|
providerId: string,
|
||||||
segment: ScrapingSegment
|
segment: ScrapingSegment,
|
||||||
): ProviderMetric | null {
|
): ProviderMetric | null {
|
||||||
const status = segmentStatusMap[segment.status];
|
const status = segmentStatusMap[segment.status];
|
||||||
if (!status) return null;
|
if (!status) return null;
|
||||||
@ -112,7 +112,7 @@ export function scrapeSegmentToProviderMetric(
|
|||||||
export function scrapePartsToProviderMetric(
|
export function scrapePartsToProviderMetric(
|
||||||
media: ScrapeMedia,
|
media: ScrapeMedia,
|
||||||
order: ScrapingItems[],
|
order: ScrapingItems[],
|
||||||
sources: Record<string, ScrapingSegment>
|
sources: Record<string, ScrapingSegment>,
|
||||||
): ProviderMetric[] {
|
): ProviderMetric[] {
|
||||||
const output: ProviderMetric[] = [];
|
const output: ProviderMetric[] = [];
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ const expirySeconds = 24 * 60 * 60;
|
|||||||
* Always returns SRT
|
* Always returns SRT
|
||||||
*/
|
*/
|
||||||
export async function downloadCaption(
|
export async function downloadCaption(
|
||||||
caption: CaptionListItem
|
caption: CaptionListItem,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const cached = downloadCache.get(caption.url);
|
const cached = downloadCache.get(caption.url);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
@ -34,7 +34,7 @@ export interface DetailedMeta {
|
|||||||
|
|
||||||
export function formatTMDBMetaResult(
|
export function formatTMDBMetaResult(
|
||||||
details: TMDBShowData | TMDBMovieData,
|
details: TMDBShowData | TMDBMovieData,
|
||||||
type: MWMediaType
|
type: MWMediaType,
|
||||||
): TMDBMediaResult {
|
): TMDBMediaResult {
|
||||||
if (type === MWMediaType.MOVIE) {
|
if (type === MWMediaType.MOVIE) {
|
||||||
const movie = details as TMDBMovieData;
|
const movie = details as TMDBMovieData;
|
||||||
@ -68,7 +68,7 @@ export function formatTMDBMetaResult(
|
|||||||
export async function getMetaFromId(
|
export async function getMetaFromId(
|
||||||
type: MWMediaType,
|
type: MWMediaType,
|
||||||
id: string,
|
id: string,
|
||||||
seasonId?: string
|
seasonId?: string,
|
||||||
): Promise<DetailedMeta | null> {
|
): Promise<DetailedMeta | null> {
|
||||||
const details = await getMediaDetails(id, mediaTypeToTMDB(type));
|
const details = await getMediaDetails(id, mediaTypeToTMDB(type));
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ export async function getMetaFromId(
|
|||||||
if (selectedSeason) {
|
if (selectedSeason) {
|
||||||
const episodes = await getEpisodes(
|
const episodes = await getEpisodes(
|
||||||
details.id.toString(),
|
details.id.toString(),
|
||||||
selectedSeason.season_number
|
selectedSeason.season_number,
|
||||||
);
|
);
|
||||||
|
|
||||||
seasonData = {
|
seasonData = {
|
||||||
@ -116,7 +116,7 @@ export async function getMetaFromId(
|
|||||||
export async function getLegacyMetaFromId(
|
export async function getLegacyMetaFromId(
|
||||||
type: MWMediaType,
|
type: MWMediaType,
|
||||||
id: string,
|
id: string,
|
||||||
seasonId?: string
|
seasonId?: string,
|
||||||
): Promise<DetailedMeta | null> {
|
): Promise<DetailedMeta | null> {
|
||||||
const queryType = mediaTypeToJW(type);
|
const queryType = mediaTypeToJW(type);
|
||||||
|
|
||||||
@ -135,15 +135,13 @@ export async function getLegacyMetaFromId(
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
let imdbId = data.external_ids.find(
|
let imdbId = data.external_ids.find((v) => v.provider === "imdb_latest")
|
||||||
(v) => v.provider === "imdb_latest"
|
?.external_id;
|
||||||
)?.external_id;
|
|
||||||
if (!imdbId)
|
if (!imdbId)
|
||||||
imdbId = data.external_ids.find((v) => v.provider === "imdb")?.external_id;
|
imdbId = data.external_ids.find((v) => v.provider === "imdb")?.external_id;
|
||||||
|
|
||||||
let tmdbId = data.external_ids.find(
|
let tmdbId = data.external_ids.find((v) => v.provider === "tmdb_latest")
|
||||||
(v) => v.provider === "tmdb_latest"
|
?.external_id;
|
||||||
)?.external_id;
|
|
||||||
if (!tmdbId)
|
if (!tmdbId)
|
||||||
tmdbId = data.external_ids.find((v) => v.provider === "tmdb")?.external_id;
|
tmdbId = data.external_ids.find((v) => v.provider === "tmdb")?.external_id;
|
||||||
|
|
||||||
@ -175,7 +173,7 @@ export function isLegacyMediaType(url: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function convertLegacyUrl(
|
export async function convertLegacyUrl(
|
||||||
url: string
|
url: string,
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
if (!isLegacyUrl(url)) return undefined;
|
if (!isLegacyUrl(url)) return undefined;
|
||||||
|
|
||||||
@ -191,7 +189,7 @@ export async function convertLegacyUrl(
|
|||||||
return `/media/${TMDBIdToUrlId(
|
return `/media/${TMDBIdToUrlId(
|
||||||
MWMediaType.SERIES,
|
MWMediaType.SERIES,
|
||||||
details.id.toString(),
|
details.id.toString(),
|
||||||
details.name
|
details.name,
|
||||||
)}${suffix}`;
|
)}${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ export function JWMediaToMediaType(type: string): MWMediaType {
|
|||||||
|
|
||||||
export function formatJWMeta(
|
export function formatJWMeta(
|
||||||
media: JWMediaResult,
|
media: JWMediaResult,
|
||||||
season?: JWSeasonMetaResult
|
season?: JWSeasonMetaResult,
|
||||||
): MWMediaMeta {
|
): MWMediaMeta {
|
||||||
const type = JWMediaToMediaType(media.object_type);
|
const type = JWMediaToMediaType(media.object_type);
|
||||||
let seasons: undefined | MWSeasonMeta[];
|
let seasons: undefined | MWSeasonMeta[];
|
||||||
@ -32,7 +32,7 @@ export function formatJWMeta(
|
|||||||
id: v.id.toString(),
|
id: v.id.toString(),
|
||||||
number: v.season_number,
|
number: v.season_number,
|
||||||
title: v.title,
|
title: v.title,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ export function JWMediaToId(media: MWMediaMeta): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function decodeJWId(
|
export function decodeJWId(
|
||||||
paramId: string
|
paramId: string,
|
||||||
): { id: string; type: MWMediaType } | null {
|
): { id: string; type: MWMediaType } | null {
|
||||||
const [prefix, type, id] = paramId.split("-", 3);
|
const [prefix, type, id] = paramId.split("-", 3);
|
||||||
if (prefix !== "JW") return null;
|
if (prefix !== "JW") return null;
|
||||||
|
@ -38,7 +38,7 @@ export function TMDBMediaToMediaType(type: TMDBContentTypes): MWMediaType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TMDBMediaToMediaItemType(
|
export function TMDBMediaToMediaItemType(
|
||||||
type: TMDBContentTypes
|
type: TMDBContentTypes,
|
||||||
): MediaItem["type"] {
|
): MediaItem["type"] {
|
||||||
if (type === TMDBContentTypes.MOVIE) return "movie";
|
if (type === TMDBContentTypes.MOVIE) return "movie";
|
||||||
if (type === TMDBContentTypes.TV) return "show";
|
if (type === TMDBContentTypes.TV) return "show";
|
||||||
@ -47,7 +47,7 @@ export function TMDBMediaToMediaItemType(
|
|||||||
|
|
||||||
export function formatTMDBMeta(
|
export function formatTMDBMeta(
|
||||||
media: TMDBMediaResult,
|
media: TMDBMediaResult,
|
||||||
season?: TMDBSeasonMetaResult
|
season?: TMDBSeasonMetaResult,
|
||||||
): MWMediaMeta {
|
): MWMediaMeta {
|
||||||
const type = TMDBMediaToMediaType(media.object_type);
|
const type = TMDBMediaToMediaType(media.object_type);
|
||||||
let seasons: undefined | MWSeasonMeta[];
|
let seasons: undefined | MWSeasonMeta[];
|
||||||
@ -59,7 +59,7 @@ export function formatTMDBMeta(
|
|||||||
title: v.title,
|
title: v.title,
|
||||||
id: v.id.toString(),
|
id: v.id.toString(),
|
||||||
number: v.season_number,
|
number: v.season_number,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ export function formatTMDBMetaToMediaItem(media: TMDBMediaResult): MediaItem {
|
|||||||
export function TMDBIdToUrlId(
|
export function TMDBIdToUrlId(
|
||||||
type: MWMediaType,
|
type: MWMediaType,
|
||||||
tmdbId: string,
|
tmdbId: string,
|
||||||
title: string
|
title: string,
|
||||||
) {
|
) {
|
||||||
return [
|
return [
|
||||||
"tmdb",
|
"tmdb",
|
||||||
@ -120,12 +120,12 @@ export function mediaItemToId(media: MediaItem): string {
|
|||||||
return TMDBIdToUrlId(
|
return TMDBIdToUrlId(
|
||||||
mediaItemTypeToMediaType(media.type),
|
mediaItemTypeToMediaType(media.type),
|
||||||
media.id,
|
media.id,
|
||||||
media.title
|
media.title,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeTMDBId(
|
export function decodeTMDBId(
|
||||||
paramId: string
|
paramId: string,
|
||||||
): { id: string; type: MWMediaType } | null {
|
): { id: string; type: MWMediaType } | null {
|
||||||
const [prefix, type, id] = paramId.split("-", 3);
|
const [prefix, type, id] = paramId.split("-", 3);
|
||||||
if (prefix !== "tmdb") return null;
|
if (prefix !== "tmdb") return null;
|
||||||
@ -160,7 +160,7 @@ async function get<T>(url: string, params?: object): Promise<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function multiSearch(
|
export async function multiSearch(
|
||||||
query: string
|
query: string,
|
||||||
): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> {
|
): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> {
|
||||||
const data = await get<TMDBSearchResult>("search/multi", {
|
const data = await get<TMDBSearchResult>("search/multi", {
|
||||||
query,
|
query,
|
||||||
@ -172,13 +172,13 @@ export async function multiSearch(
|
|||||||
const results = data.results.filter(
|
const results = data.results.filter(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.media_type === TMDBContentTypes.MOVIE ||
|
r.media_type === TMDBContentTypes.MOVIE ||
|
||||||
r.media_type === TMDBContentTypes.TV
|
r.media_type === TMDBContentTypes.TV,
|
||||||
);
|
);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateQuickSearchMediaUrl(
|
export async function generateQuickSearchMediaUrl(
|
||||||
query: string
|
query: string,
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const data = await multiSearch(query);
|
const data = await multiSearch(query);
|
||||||
if (data.length === 0) return undefined;
|
if (data.length === 0) return undefined;
|
||||||
@ -189,7 +189,7 @@ export async function generateQuickSearchMediaUrl(
|
|||||||
return `/media/${TMDBIdToUrlId(
|
return `/media/${TMDBIdToUrlId(
|
||||||
TMDBMediaToMediaType(result.media_type),
|
TMDBMediaToMediaType(result.media_type),
|
||||||
result.id.toString(),
|
result.id.toString(),
|
||||||
title
|
title,
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,12 +198,12 @@ type MediaDetailReturn<T extends TMDBContentTypes> =
|
|||||||
T extends TMDBContentTypes.MOVIE
|
T extends TMDBContentTypes.MOVIE
|
||||||
? TMDBMovieData
|
? TMDBMovieData
|
||||||
: T extends TMDBContentTypes.TV
|
: T extends TMDBContentTypes.TV
|
||||||
? TMDBShowData
|
? TMDBShowData
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
export function getMediaDetails<
|
export function getMediaDetails<
|
||||||
T extends TMDBContentTypes,
|
T extends TMDBContentTypes,
|
||||||
TReturn = MediaDetailReturn<T>
|
TReturn = MediaDetailReturn<T>,
|
||||||
>(id: string, type: T): Promise<TReturn> {
|
>(id: string, type: T): Promise<TReturn> {
|
||||||
if (type === TMDBContentTypes.MOVIE) {
|
if (type === TMDBContentTypes.MOVIE) {
|
||||||
return get<TReturn>(`/movie/${id}`, { append_to_response: "external_ids" });
|
return get<TReturn>(`/movie/${id}`, { append_to_response: "external_ids" });
|
||||||
@ -220,7 +220,7 @@ export function getMediaPoster(posterPath: string | null): string | undefined {
|
|||||||
|
|
||||||
export async function getEpisodes(
|
export async function getEpisodes(
|
||||||
id: string,
|
id: string,
|
||||||
season: number
|
season: number,
|
||||||
): Promise<TMDBEpisodeShort[]> {
|
): Promise<TMDBEpisodeShort[]> {
|
||||||
const data = await get<TMDBSeason>(`/tv/${id}/season/${season}`);
|
const data = await get<TMDBSeason>(`/tv/${id}/season/${season}`);
|
||||||
return data.episodes.map((e) => ({
|
return data.episodes.map((e) => ({
|
||||||
@ -231,7 +231,7 @@ export async function getEpisodes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getMovieFromExternalId(
|
export async function getMovieFromExternalId(
|
||||||
imdbId: string
|
imdbId: string,
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const data = await get<ExternalIdMovieSearchResult>(`/find/${imdbId}`, {
|
const data = await get<ExternalIdMovieSearchResult>(`/find/${imdbId}`, {
|
||||||
external_source: "imdb_id",
|
external_source: "imdb_id",
|
||||||
@ -245,7 +245,7 @@ export async function getMovieFromExternalId(
|
|||||||
|
|
||||||
export function formatTMDBSearchResult(
|
export function formatTMDBSearchResult(
|
||||||
result: TMDBMovieSearchResult | TMDBShowSearchResult,
|
result: TMDBMovieSearchResult | TMDBShowSearchResult,
|
||||||
mediatype: TMDBContentTypes
|
mediatype: TMDBContentTypes,
|
||||||
): TMDBMediaResult {
|
): TMDBMediaResult {
|
||||||
const type = TMDBMediaToMediaType(mediatype);
|
const type = TMDBMediaToMediaType(mediatype);
|
||||||
if (type === MWMediaType.SERIES) {
|
if (type === MWMediaType.SERIES) {
|
||||||
|
@ -20,7 +20,7 @@ export function Avatar(props: AvatarProps) {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
props.sizeClass,
|
props.sizeClass,
|
||||||
"rounded-full overflow-hidden flex items-center justify-center text-white"
|
"rounded-full overflow-hidden flex items-center justify-center text-white",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(to bottom right, ${props.profile.colorA}, ${props.profile.colorB})`,
|
background: `linear-gradient(to bottom right, ${props.profile.colorA}, ${props.profile.colorB})`,
|
||||||
@ -53,7 +53,7 @@ export function UserAvatar(props: {
|
|||||||
auth.account && auth.account.seed
|
auth.account && auth.account.seed
|
||||||
? base64ToBuffer(auth.account.seed)
|
? base64ToBuffer(auth.account.seed)
|
||||||
: null,
|
: null,
|
||||||
[auth]
|
[auth],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!auth.account || auth.account === null) return null;
|
if (!auth.account || auth.account === null) return null;
|
||||||
|
@ -51,7 +51,7 @@ export function FlagIcon(props: FlagIconProps) {
|
|||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"!w-8 h-6 rounded overflow-hidden bg-video-context-flagBg bg-cover bg-center block fi",
|
"!w-8 h-6 rounded overflow-hidden bg-video-context-flagBg bg-cover bg-center block fi",
|
||||||
props.countryCode ? `fi-${countryCode}` : undefined
|
props.countryCode ? `fi-${countryCode}` : undefined,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
|
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
|
||||||
import { UserAvatar } from "@/components/Avatar";
|
import { UserAvatar } from "@/components/Avatar";
|
||||||
@ -21,11 +21,11 @@ function GoToLink(props: {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const goTo = (href: string) => {
|
const goTo = (href: string) => {
|
||||||
if (href.startsWith("http")) window.open(href, "_blank");
|
if (href.startsWith("http")) window.open(href, "_blank");
|
||||||
else history.push(href);
|
else navigate(href);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,7 +61,7 @@ function DropdownLink(props: {
|
|||||||
props.highlight
|
props.highlight
|
||||||
? "text-dropdown-highlight hover:text-dropdown-highlightHover"
|
? "text-dropdown-highlight hover:text-dropdown-highlightHover"
|
||||||
: "text-dropdown-text hover:text-white",
|
: "text-dropdown-text hover:text-white",
|
||||||
props.className
|
props.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.icon ? <Icon icon={props.icon} className="text-xl" /> : null}
|
{props.icon ? <Icon icon={props.icon} className="text-xl" /> : null}
|
||||||
@ -88,7 +88,7 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
|
|||||||
const seed = useAuthStore((s) => s.account?.seed);
|
const seed = useAuthStore((s) => s.account?.seed);
|
||||||
const bufferSeed = useMemo(
|
const bufferSeed = useMemo(
|
||||||
() => (seed ? base64ToBuffer(seed) : null),
|
() => (seed ? base64ToBuffer(seed) : null),
|
||||||
[seed]
|
[seed],
|
||||||
);
|
);
|
||||||
const { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
|
|||||||
<Icon
|
<Icon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-xl transition-transform duration-100",
|
"text-xl transition-transform duration-100",
|
||||||
open ? "rotate-180" : ""
|
open ? "rotate-180" : "",
|
||||||
)}
|
)}
|
||||||
icon={Icons.CHEVRON_DOWN}
|
icon={Icons.CHEVRON_DOWN}
|
||||||
/>
|
/>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ReactNode, useCallback } from "react";
|
import { ReactNode, useCallback } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Spinner } from "@/components/layout/Spinner";
|
import { Spinner } from "@/components/layout/Spinner";
|
||||||
@ -19,13 +19,13 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Button(props: Props) {
|
export function Button(props: Props) {
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
const { onClick, href, loading } = props;
|
const { onClick, href, loading } = props;
|
||||||
const cb = useCallback(() => {
|
const cb = useCallback(() => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
if (href) history.push(href);
|
if (href) navigate(href);
|
||||||
else onClick?.();
|
else onClick?.();
|
||||||
}, [onClick, href, history, loading]);
|
}, [onClick, href, navigate, loading]);
|
||||||
|
|
||||||
let colorClasses = "bg-white hover:bg-gray-200 text-black";
|
let colorClasses = "bg-white hover:bg-gray-200 text-black";
|
||||||
if (props.theme === "purple")
|
if (props.theme === "purple")
|
||||||
@ -41,7 +41,7 @@ export function Button(props: Props) {
|
|||||||
props.padding ?? "px-4 py-3",
|
props.padding ?? "px-4 py-3",
|
||||||
props.className,
|
props.className,
|
||||||
colorClasses,
|
colorClasses,
|
||||||
props.disabled ? "cursor-not-allowed bg-opacity-60 text-opacity-60" : null
|
props.disabled ? "cursor-not-allowed bg-opacity-60 text-opacity-60" : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (props.disabled)
|
if (props.disabled)
|
||||||
@ -49,7 +49,7 @@ export function Button(props: Props) {
|
|||||||
.split(" ")
|
.split(" ")
|
||||||
.filter(
|
.filter(
|
||||||
(className) =>
|
(className) =>
|
||||||
!className.startsWith("hover:") && !className.startsWith("active:")
|
!className.startsWith("hover:") && !className.startsWith("active:"),
|
||||||
)
|
)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ export function ButtonPlain(props: ButtonPlainProps) {
|
|||||||
"cursor-pointer inline-flex items-center justify-center rounded-lg font-medium transition-[transform,background-color] duration-100 active:scale-105 md:px-8",
|
"cursor-pointer inline-flex items-center justify-center rounded-lg font-medium transition-[transform,background-color] duration-100 active:scale-105 md:px-8",
|
||||||
"px-4 py-3",
|
"px-4 py-3",
|
||||||
props.className,
|
props.className,
|
||||||
colorClasses
|
colorClasses,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -7,14 +7,14 @@ export function Toggle(props: { onClick: () => void; enabled?: boolean }) {
|
|||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"w-11 h-6 p-1 rounded-full grid transition-colors duration-100 group/toggle tabbable",
|
"w-11 h-6 p-1 rounded-full grid transition-colors duration-100 group/toggle tabbable",
|
||||||
props.enabled ? "bg-buttons-toggle" : "bg-buttons-toggleDisabled"
|
props.enabled ? "bg-buttons-toggle" : "bg-buttons-toggleDisabled",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative w-full h-full">
|
<div className="relative w-full h-full">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"scale-90 group-hover/toggle:scale-100 h-full aspect-square rounded-full bg-white absolute transition-all duration-100",
|
"scale-90 group-hover/toggle:scale-100 h-full aspect-square rounded-full bg-white absolute transition-all duration-100",
|
||||||
props.enabled ? "left-full transform -translate-x-full" : "left-0"
|
props.enabled ? "left-full transform -translate-x-full" : "left-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,7 +24,7 @@ export function ColorPicker(props: {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full h-10 rounded flex justify-center items-center text-white pointer border-2 border-opacity-10 cursor-pointer",
|
"w-full h-10 rounded flex justify-center items-center text-white pointer border-2 border-opacity-10 cursor-pointer",
|
||||||
props.value === color ? "border-white" : "border-transparent"
|
props.value === color ? "border-white" : "border-transparent",
|
||||||
)}
|
)}
|
||||||
onClick={() => props.onInput(color)}
|
onClick={() => props.onInput(color)}
|
||||||
style={{ backgroundColor: color }}
|
style={{ backgroundColor: color }}
|
||||||
|
@ -32,7 +32,7 @@ export function IconPicker(props: {
|
|||||||
"w-full h-10 rounded flex justify-center items-center text-white pointer border-2 border-opacity-10 cursor-pointer",
|
"w-full h-10 rounded flex justify-center items-center text-white pointer border-2 border-opacity-10 cursor-pointer",
|
||||||
props.value === icon
|
props.value === icon
|
||||||
? "bg-buttons-purple border-white"
|
? "bg-buttons-purple border-white"
|
||||||
: "bg-authentication-inputBg border-transparent"
|
: "bg-authentication-inputBg border-transparent",
|
||||||
)}
|
)}
|
||||||
onClick={() => props.onInput(icon)}
|
onClick={() => props.onInput(icon)}
|
||||||
key={icon}
|
key={icon}
|
||||||
|
@ -60,5 +60,5 @@ export const SearchBarInput = forwardRef<HTMLInputElement, SearchBarProps>(
|
|||||||
</Flare.Child>
|
</Flare.Child>
|
||||||
</Flare.Base>
|
</Flare.Base>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
@ -17,7 +17,7 @@ export function BrandPill(props: {
|
|||||||
props.backgroundClass ?? "bg-pill-background bg-opacity-50",
|
props.backgroundClass ?? "bg-pill-background bg-opacity-50",
|
||||||
props.clickable
|
props.clickable
|
||||||
? "transition-[transform,background-color] hover:scale-105 hover:bg-pill-backgroundHover hover:text-type-logo active:scale-95"
|
? "transition-[transform,background-color] hover:scale-105 hover:bg-pill-backgroundHover hover:text-type-logo active:scale-95"
|
||||||
: ""
|
: "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="text-xl" icon={Icons.MOVIE_WEB} />
|
<Icon className="text-xl" icon={Icons.MOVIE_WEB} />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import type { RequireExactlyOne } from "type-fest";
|
import type { RequireExactlyOne } from "type-fest";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
@ -21,13 +21,13 @@ type FooterLinkProps = RequireExactlyOne<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
function FooterLink(props: FooterLinkProps) {
|
function FooterLink(props: FooterLinkProps) {
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const navigateTo = useCallback(() => {
|
const navigateTo = useCallback(() => {
|
||||||
if (!props.to) return;
|
if (!props.to) return;
|
||||||
|
|
||||||
history.push(props.to);
|
navigate(props.to);
|
||||||
}, [history, props.to]);
|
}, [navigate, props.to]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
@ -99,7 +99,7 @@ export function FooterView(props: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={["flex min-h-screen flex-col", props.className || ""].join(
|
className={["flex min-h-screen flex-col", props.className || ""].join(
|
||||||
" "
|
" ",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div style={{ flex: "1 0 auto" }}>{props.children}</div>
|
<div style={{ flex: "1 0 auto" }}>{props.children}</div>
|
||||||
|
@ -51,7 +51,7 @@ export function Navigation(props: NavigationProps) {
|
|||||||
"fixed left-0 right-0 h-20 flex items-center",
|
"fixed left-0 right-0 h-20 flex items-center",
|
||||||
props.doBackground
|
props.doBackground
|
||||||
? "bg-background-main border-b border-utils-divider border-opacity-50"
|
? "bg-background-main border-b border-utils-divider border-opacity-50"
|
||||||
: null
|
: null,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.doBackground ? (
|
{props.doBackground ? (
|
||||||
|
@ -10,7 +10,7 @@ export function SettingsCard(props: {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full rounded-lg bg-settings-card-background bg-opacity-[0.15] border border-settings-card-border",
|
"w-full rounded-lg bg-settings-card-background bg-opacity-[0.15] border border-settings-card-border",
|
||||||
props.paddingClass ?? "px-8 py-6",
|
props.paddingClass ?? "px-8 py-6",
|
||||||
props.className
|
props.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
@ -28,7 +28,7 @@ export function SolidSettingsCard(props: {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full rounded-lg bg-settings-card-altBackground bg-opacity-50",
|
"w-full rounded-lg bg-settings-card-altBackground bg-opacity-50",
|
||||||
props.paddingClass ?? "px-8 py-6",
|
props.paddingClass ?? "px-8 py-6",
|
||||||
props.className
|
props.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -31,13 +31,13 @@ export function SidebarLink(props: {
|
|||||||
"tabbable w-full px-3 py-2 flex items-center space-x-3 cursor-pointer rounded my-2",
|
"tabbable w-full px-3 py-2 flex items-center space-x-3 cursor-pointer rounded my-2",
|
||||||
props.active
|
props.active
|
||||||
? "bg-settings-sidebar-activeLink text-settings-sidebar-type-activated"
|
? "bg-settings-sidebar-activeLink text-settings-sidebar-type-activated"
|
||||||
: null
|
: null,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-2xl text-settings-sidebar-type-icon",
|
"text-2xl text-settings-sidebar-type-icon",
|
||||||
props.active ? "text-settings-sidebar-type-iconActivated" : null
|
props.active ? "text-settings-sidebar-type-iconActivated" : null,
|
||||||
)}
|
)}
|
||||||
icon={props.icon}
|
icon={props.icon}
|
||||||
/>
|
/>
|
||||||
|
@ -66,7 +66,7 @@ function MediaCardContent({
|
|||||||
"relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-100",
|
"relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-100",
|
||||||
{
|
{
|
||||||
"group-hover:rounded-lg": !closable,
|
"group-hover:rounded-lg": !closable,
|
||||||
}
|
},
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: media.poster ? `url(${media.poster})` : undefined,
|
backgroundImage: media.poster ? `url(${media.poster})` : undefined,
|
||||||
@ -152,7 +152,7 @@ export function MediaCard(props: MediaCardProps) {
|
|||||||
link += `/${encodeURIComponent(props.series.seasonId)}`;
|
link += `/${encodeURIComponent(props.series.seasonId)}`;
|
||||||
} else {
|
} else {
|
||||||
link += `/${encodeURIComponent(
|
link += `/${encodeURIComponent(
|
||||||
props.series.seasonId
|
props.series.seasonId,
|
||||||
)}/${encodeURIComponent(props.series.episodeId)}`;
|
)}/${encodeURIComponent(props.series.episodeId)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -164,7 +164,7 @@ export function MediaCard(props: MediaCardProps) {
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"tabbable",
|
"tabbable",
|
||||||
props.closable ? "hover:cursor-default" : ""
|
props.closable ? "hover:cursor-default" : "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
@ -14,5 +14,5 @@ export const MediaGrid = forwardRef<HTMLDivElement, MediaGridProps>(
|
|||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
@ -32,7 +32,7 @@ export function WatchedMediaCard(props: WatchedMediaCardProps) {
|
|||||||
}, [progressItems, props.media]);
|
}, [progressItems, props.media]);
|
||||||
const itemToDisplay = useMemo(
|
const itemToDisplay = useMemo(
|
||||||
() => (item ? shouldShowProgress(item) : null),
|
() => (item ? shouldShowProgress(item) : null),
|
||||||
[item]
|
[item],
|
||||||
);
|
);
|
||||||
const percentage = itemToDisplay?.show
|
const percentage = itemToDisplay?.show
|
||||||
? (itemToDisplay.progress.watched / itemToDisplay.progress.duration) * 100
|
? (itemToDisplay.progress.watched / itemToDisplay.progress.duration) * 100
|
||||||
|
@ -80,7 +80,7 @@ export function OverlayPortal(props: {
|
|||||||
</div>
|
</div>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
</Transition>,
|
</Transition>,
|
||||||
portalElement
|
portalElement,
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,7 +21,7 @@ function RouterBase(props: { id: string; children: ReactNode }) {
|
|||||||
const router = useInternalOverlayRouter(props.id);
|
const router = useInternalOverlayRouter(props.id);
|
||||||
const routeMeta = useMemo(
|
const routeMeta = useMemo(
|
||||||
() => routes[router.currentRoute ?? ""],
|
() => routes[router.currentRoute ?? ""],
|
||||||
[routes, router]
|
[routes, router],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [dimensions, api] = useSpring(
|
const [dimensions, api] = useSpring(
|
||||||
@ -34,7 +34,7 @@ function RouterBase(props: { id: string; children: ReactNode }) {
|
|||||||
easing: easings.linear,
|
easing: easings.linear,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentState = useRef<null | string>(null);
|
const currentState = useRef<null | string>(null);
|
||||||
|
@ -25,11 +25,11 @@ function useCalculatePositions() {
|
|||||||
setLeft(
|
setLeft(
|
||||||
Math.min(
|
Math.min(
|
||||||
buttonCenter - card.width / 2,
|
buttonCenter - card.width / 2,
|
||||||
window.innerWidth - card.width - 30
|
window.innerWidth - card.width - 30,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -18,7 +18,7 @@ export function Chromecast(props: ChromecastProps) {
|
|||||||
const isVisible = (tag.getAttribute("style") ?? "").includes("inline");
|
const isVisible = (tag.getAttribute("style") ?? "").includes("inline");
|
||||||
setHidden(!isVisible);
|
setHidden(!isVisible);
|
||||||
},
|
},
|
||||||
[setHidden]
|
[setHidden],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -54,7 +54,7 @@ function SeasonsView({
|
|||||||
const meta = usePlayerStore((s) => s.meta);
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
const [loadingState, seasons] = useSeasonData(
|
const [loadingState, seasons] = useSeasonData(
|
||||||
meta?.tmdbId ?? "",
|
meta?.tmdbId ?? "",
|
||||||
selectedSeason
|
selectedSeason,
|
||||||
);
|
);
|
||||||
|
|
||||||
let content: ReactNode = null;
|
let content: ReactNode = null;
|
||||||
@ -120,7 +120,7 @@ function EpisodesView({
|
|||||||
// player already switches route after meta change
|
// player already switches route after meta change
|
||||||
router.close(true);
|
router.close(true);
|
||||||
},
|
},
|
||||||
[setPlayerMeta, loadingState, router, onChange]
|
[setPlayerMeta, loadingState, router, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!meta?.tmdbId) return null;
|
if (!meta?.tmdbId) return null;
|
||||||
@ -175,7 +175,7 @@ function EpisodesView({
|
|||||||
"p-0.5 px-2 rounded inline bg-video-context-hoverColor",
|
"p-0.5 px-2 rounded inline bg-video-context-hoverColor",
|
||||||
ep.id === meta?.episode?.tmdbId
|
ep.id === meta?.episode?.tmdbId
|
||||||
? "text-white bg-opacity-100"
|
? "text-white bg-opacity-100"
|
||||||
: "bg-opacity-50"
|
: "bg-opacity-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{t("player.menus.episodes.episodeBadge", {
|
{t("player.menus.episodes.episodeBadge", {
|
||||||
@ -226,7 +226,7 @@ function EpisodesOverlay({
|
|||||||
setSelectedSeason(seasonId);
|
setSelectedSeason(seasonId);
|
||||||
router.navigate("/episodes");
|
router.navigate("/episodes");
|
||||||
},
|
},
|
||||||
[router]
|
[router],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -10,7 +10,7 @@ import { usePlayerStore } from "@/stores/player/store";
|
|||||||
|
|
||||||
function shouldShowNextEpisodeButton(
|
function shouldShowNextEpisodeButton(
|
||||||
time: number,
|
time: number,
|
||||||
duration: number
|
duration: number,
|
||||||
): "always" | "hover" | "none" {
|
): "always" | "hover" | "none" {
|
||||||
const percentage = time / duration;
|
const percentage = time / duration;
|
||||||
const secondsFromEnd = duration - time;
|
const secondsFromEnd = duration - time;
|
||||||
@ -28,7 +28,7 @@ function Button(props: {
|
|||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"font-bold rounded h-10 w-40 scale-95 hover:scale-100 transition-all duration-200",
|
"font-bold rounded h-10 w-40 scale-95 hover:scale-100 transition-all duration-200",
|
||||||
props.className
|
props.className,
|
||||||
)}
|
)}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
@ -53,7 +53,7 @@ export function NextEpisodeButton(props: {
|
|||||||
const showingState = shouldShowNextEpisodeButton(time, duration);
|
const showingState = shouldShowNextEpisodeButton(time, duration);
|
||||||
const status = usePlayerStore((s) => s.status);
|
const status = usePlayerStore((s) => s.status);
|
||||||
const setShouldStartFromBeginning = usePlayerStore(
|
const setShouldStartFromBeginning = usePlayerStore(
|
||||||
(s) => s.setShouldStartFromBeginning
|
(s) => s.setShouldStartFromBeginning,
|
||||||
);
|
);
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
@ -69,7 +69,7 @@ export function NextEpisodeButton(props: {
|
|||||||
: "bottom-[calc(3rem+env(safe-area-inset-bottom))]";
|
: "bottom-[calc(3rem+env(safe-area-inset-bottom))]";
|
||||||
|
|
||||||
const nextEp = meta?.episodes?.find(
|
const nextEp = meta?.episodes?.find(
|
||||||
(v) => v.number === (meta?.episode?.number ?? 0) + 1
|
(v) => v.number === (meta?.episode?.number ?? 0) + 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadNextEpisode = useCallback(() => {
|
const loadNextEpisode = useCallback(() => {
|
||||||
|
@ -58,7 +58,7 @@ function ThumbnailDisplay(props: { at: number; show: boolean }) {
|
|||||||
<p className="text-center mt-1">
|
<p className="text-center mt-1">
|
||||||
{formatSeconds(
|
{formatSeconds(
|
||||||
Math.max(props.at, 0),
|
Math.max(props.at, 0),
|
||||||
durationExceedsHour(props.at)
|
durationExceedsHour(props.at),
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -79,7 +79,7 @@ function useMouseHoverPosition(barRef: RefObject<HTMLDivElement>) {
|
|||||||
const pos = (e.pageX - rect.left) / barRef.current.offsetWidth;
|
const pos = (e.pageX - rect.left) / barRef.current.offsetWidth;
|
||||||
setMousePos(pos * 100);
|
setMousePos(pos * 100);
|
||||||
},
|
},
|
||||||
[setMousePos, barRef]
|
[setMousePos, barRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mouseLeave = useCallback(() => {
|
const mouseLeave = useCallback(() => {
|
||||||
@ -97,10 +97,10 @@ export function ProgressBar() {
|
|||||||
const { isSeeking } = usePlayerStore((s) => s.interface);
|
const { isSeeking } = usePlayerStore((s) => s.interface);
|
||||||
|
|
||||||
const commitTime = useCallback(
|
const commitTime = useCallback(
|
||||||
(percentage) => {
|
(percentage: number) => {
|
||||||
display?.setTime(percentage * duration);
|
display?.setTime(percentage * duration);
|
||||||
},
|
},
|
||||||
[duration, display]
|
[duration, display],
|
||||||
);
|
);
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@ -108,7 +108,7 @@ export function ProgressBar() {
|
|||||||
|
|
||||||
const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
|
const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
|
||||||
ref,
|
ref,
|
||||||
commitTime
|
commitTime,
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSeeking(dragging);
|
setSeeking(dragging);
|
||||||
@ -165,8 +165,8 @@ export function ProgressBar() {
|
|||||||
0,
|
0,
|
||||||
Math.min(
|
Math.min(
|
||||||
1,
|
1,
|
||||||
dragging ? dragPercentage / 100 : time / duration
|
dragging ? dragPercentage / 100 : time / duration,
|
||||||
)
|
),
|
||||||
) * 100
|
) * 100
|
||||||
}%`,
|
}%`,
|
||||||
}}
|
}}
|
||||||
|
@ -22,19 +22,19 @@ export function Time(props: { short?: boolean }) {
|
|||||||
setTimeFormat(
|
setTimeFormat(
|
||||||
timeFormat === VideoPlayerTimeFormat.REGULAR
|
timeFormat === VideoPlayerTimeFormat.REGULAR
|
||||||
? VideoPlayerTimeFormat.REMAINING
|
? VideoPlayerTimeFormat.REMAINING
|
||||||
: VideoPlayerTimeFormat.REGULAR
|
: VideoPlayerTimeFormat.REGULAR,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTime = Math.min(
|
const currentTime = Math.min(
|
||||||
Math.max(isSeeking ? draggingTime : time, 0),
|
Math.max(isSeeking ? draggingTime : time, 0),
|
||||||
timeDuration
|
timeDuration,
|
||||||
);
|
);
|
||||||
const secondsRemaining = Math.abs(currentTime - timeDuration);
|
const secondsRemaining = Math.abs(currentTime - timeDuration);
|
||||||
|
|
||||||
const timeLeft = formatSeconds(
|
const timeLeft = formatSeconds(
|
||||||
secondsRemaining,
|
secondsRemaining,
|
||||||
durationExceedsHour(secondsRemaining)
|
durationExceedsHour(secondsRemaining),
|
||||||
);
|
);
|
||||||
const timeWatched = formatSeconds(currentTime, hasHours);
|
const timeWatched = formatSeconds(currentTime, hasHours);
|
||||||
const timeFinished = new Date(Date.now() + secondsRemaining * 1e3);
|
const timeFinished = new Date(Date.now() + secondsRemaining * 1e3);
|
||||||
|
@ -23,16 +23,16 @@ export function Volume(props: Props) {
|
|||||||
const { setVolume, toggleMute } = useVolume();
|
const { setVolume, toggleMute } = useVolume();
|
||||||
|
|
||||||
const commitVolume = useCallback(
|
const commitVolume = useCallback(
|
||||||
(percentage) => {
|
(percentage: number) => {
|
||||||
setVolume(percentage);
|
setVolume(percentage);
|
||||||
},
|
},
|
||||||
[setVolume]
|
[setVolume],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
|
const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
|
||||||
ref,
|
ref,
|
||||||
commitVolume,
|
commitVolume,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
|
@ -19,7 +19,7 @@ export function ColorOption(props: {
|
|||||||
type="button"
|
type="button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"tabbable p-1.5 bg-video-context-buttonFocus rounded transition-colors duration-100",
|
"tabbable p-1.5 bg-video-context-buttonFocus rounded transition-colors duration-100",
|
||||||
props.active ? "bg-opacity-100" : "bg-opacity-0 cursor-pointer"
|
props.active ? "bg-opacity-100" : "bg-opacity-0 cursor-pointer",
|
||||||
)}
|
)}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
@ -50,18 +50,18 @@ export function CaptionSetting(props: {
|
|||||||
|
|
||||||
const currentPercentage = (props.value - props.min) / (props.max - props.min);
|
const currentPercentage = (props.value - props.min) / (props.max - props.min);
|
||||||
const commit = useCallback(
|
const commit = useCallback(
|
||||||
(percentage) => {
|
(percentage: number) => {
|
||||||
const range = props.max - props.min;
|
const range = props.max - props.min;
|
||||||
const newPercentage = Math.min(Math.max(percentage, 0), 1);
|
const newPercentage = Math.min(Math.max(percentage, 0), 1);
|
||||||
props.onChange?.(props.min + range * newPercentage);
|
props.onChange?.(props.min + range * newPercentage);
|
||||||
},
|
},
|
||||||
[props]
|
[props],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
|
const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
|
||||||
ref,
|
ref,
|
||||||
commit,
|
commit,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
@ -112,8 +112,8 @@ export function CaptionSetting(props: {
|
|||||||
0,
|
0,
|
||||||
Math.min(
|
Math.min(
|
||||||
1,
|
1,
|
||||||
dragging ? dragPercentage / 100 : currentPercentage
|
dragging ? dragPercentage / 100 : currentPercentage,
|
||||||
)
|
),
|
||||||
) * 100
|
) * 100
|
||||||
}%`,
|
}%`,
|
||||||
}}
|
}}
|
||||||
@ -141,7 +141,7 @@ export function CaptionSetting(props: {
|
|||||||
const num = Number((e.target as HTMLInputElement).value);
|
const num = Number((e.target as HTMLInputElement).value);
|
||||||
if (!Number.isNaN(num))
|
if (!Number.isNaN(num))
|
||||||
props.onChange?.(
|
props.onChange?.(
|
||||||
(props.decimalsAllowed ?? 0) === 0 ? Math.round(num) : num
|
(props.decimalsAllowed ?? 0) === 0 ? Math.round(num) : num,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@ -163,13 +163,13 @@ export function CaptionSetting(props: {
|
|||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
inputClasses,
|
inputClasses,
|
||||||
props.controlButtons ? "relative" : undefined
|
props.controlButtons ? "relative" : undefined,
|
||||||
)}
|
)}
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
{textTransformer(
|
{textTransformer(
|
||||||
props.value.toFixed(props.decimalsAllowed ?? 0)
|
props.value.toFixed(props.decimalsAllowed ?? 0),
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{props.controlButtons ? (
|
{props.controlButtons ? (
|
||||||
@ -180,7 +180,8 @@ export function CaptionSetting(props: {
|
|||||||
onClick={
|
onClick={
|
||||||
() =>
|
() =>
|
||||||
props.onChange?.(
|
props.onChange?.(
|
||||||
props.value - 1 / 10 ** (props.decimalsAllowed ?? 0)
|
props.value -
|
||||||
|
1 / 10 ** (props.decimalsAllowed ?? 0),
|
||||||
) // Remove depending on the decimalsAllowed. If there's 1 decimal allowed, add 0.1. For 2, add 0.01, etc.
|
) // Remove depending on the decimalsAllowed. If there's 1 decimal allowed, add 0.1. For 2, add 0.01, etc.
|
||||||
}
|
}
|
||||||
className={arrowButtonClasses}
|
className={arrowButtonClasses}
|
||||||
@ -194,7 +195,8 @@ export function CaptionSetting(props: {
|
|||||||
onClick={
|
onClick={
|
||||||
() =>
|
() =>
|
||||||
props.onChange?.(
|
props.onChange?.(
|
||||||
props.value + 1 / 10 ** (props.decimalsAllowed ?? 0)
|
props.value +
|
||||||
|
1 / 10 ** (props.decimalsAllowed ?? 0),
|
||||||
) // Add depending on the decimalsAllowed. If there's 1 decimal allowed, add 0.1. For 2, add 0.01, etc.
|
) // Add depending on the decimalsAllowed. If there's 1 decimal allowed, add 0.1. For 2, add 0.01, etc.
|
||||||
}
|
}
|
||||||
className={arrowButtonClasses}
|
className={arrowButtonClasses}
|
||||||
|
@ -127,7 +127,7 @@ export function CaptionsView({ id }: { id: string }) {
|
|||||||
setCurrentlyDownloading(language);
|
setCurrentlyDownloading(language);
|
||||||
return selectLanguage(language);
|
return selectLanguage(language);
|
||||||
},
|
},
|
||||||
[selectLanguage, setCurrentlyDownloading]
|
[selectLanguage, setCurrentlyDownloading],
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = subtitleList.map((v, i) => {
|
const content = subtitleList.map((v, i) => {
|
||||||
@ -141,7 +141,7 @@ export function CaptionsView({ id }: { id: string }) {
|
|||||||
loading={v.language === currentlyDownloading && downloadReq.loading}
|
loading={v.language === currentlyDownloading && downloadReq.loading}
|
||||||
error={
|
error={
|
||||||
v.language === currentlyDownloading && downloadReq.error
|
v.language === currentlyDownloading && downloadReq.error
|
||||||
? downloadReq.error
|
? downloadReq.error.toString()
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onClick={() => startDownload(v.language)}
|
onClick={() => startDownload(v.language)}
|
||||||
@ -182,3 +182,5 @@ export function CaptionsView({ id }: { id: string }) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default CaptionsView;
|
||||||
|
@ -48,7 +48,7 @@ export function DownloadView({ id }: { id: string }) {
|
|||||||
selectedCaption
|
selectedCaption
|
||||||
? convertSubtitlesToSrtDataurl(selectedCaption?.srtData)
|
? convertSubtitlesToSrtDataurl(selectedCaption?.srtData)
|
||||||
: null,
|
: null,
|
||||||
[selectedCaption]
|
[selectedCaption],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!downloadUrl) return null;
|
if (!downloadUrl) return null;
|
||||||
|
@ -21,7 +21,7 @@ function ButtonList(props: {
|
|||||||
"w-full px-2 py-1 rounded-md tabbable",
|
"w-full px-2 py-1 rounded-md tabbable",
|
||||||
props.selected === option
|
props.selected === option
|
||||||
? "bg-video-context-buttons-active text-white"
|
? "bg-video-context-buttons-active text-white"
|
||||||
: null
|
: null,
|
||||||
)}
|
)}
|
||||||
onClick={() => props.onClick(option)}
|
onClick={() => props.onClick(option)}
|
||||||
key={option}
|
key={option}
|
||||||
@ -44,7 +44,7 @@ export function PlaybackSettingsView({ id }: { id: string }) {
|
|||||||
(v: number) => {
|
(v: number) => {
|
||||||
display?.setPlaybackRate(v);
|
display?.setPlaybackRate(v);
|
||||||
},
|
},
|
||||||
[display]
|
[display],
|
||||||
);
|
);
|
||||||
|
|
||||||
const options = [0.25, 0.5, 1, 1.5, 2];
|
const options = [0.25, 0.5, 1, 1.5, 2];
|
||||||
|
@ -43,7 +43,7 @@ export function QualityView({ id }: { id: string }) {
|
|||||||
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
||||||
const switchQuality = usePlayerStore((s) => s.switchQuality);
|
const switchQuality = usePlayerStore((s) => s.switchQuality);
|
||||||
const enableAutomaticQuality = usePlayerStore(
|
const enableAutomaticQuality = usePlayerStore(
|
||||||
(s) => s.enableAutomaticQuality
|
(s) => s.enableAutomaticQuality,
|
||||||
);
|
);
|
||||||
const setAutomaticQuality = useQualityStore((s) => s.setAutomaticQuality);
|
const setAutomaticQuality = useQualityStore((s) => s.setAutomaticQuality);
|
||||||
const setLastChosenQuality = useQualityStore((s) => s.setLastChosenQuality);
|
const setLastChosenQuality = useQualityStore((s) => s.setLastChosenQuality);
|
||||||
@ -56,7 +56,7 @@ export function QualityView({ id }: { id: string }) {
|
|||||||
switchQuality(q);
|
switchQuality(q);
|
||||||
router.close();
|
router.close();
|
||||||
},
|
},
|
||||||
[router, switchQuality, setLastChosenQuality, setAutomaticQuality]
|
[router, switchQuality, setLastChosenQuality, setAutomaticQuality],
|
||||||
);
|
);
|
||||||
|
|
||||||
const changeAutomatic = useCallback(() => {
|
const changeAutomatic = useCallback(() => {
|
||||||
|
@ -17,14 +17,14 @@ export function SettingsMenu({ id }: { id: string }) {
|
|||||||
const router = useOverlayRouter(id);
|
const router = useOverlayRouter(id);
|
||||||
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
||||||
const selectedCaptionLanguage = usePlayerStore(
|
const selectedCaptionLanguage = usePlayerStore(
|
||||||
(s) => s.caption.selected?.language
|
(s) => s.caption.selected?.language,
|
||||||
);
|
);
|
||||||
const subtitlesEnabled = useSubtitleStore((s) => s.enabled);
|
const subtitlesEnabled = useSubtitleStore((s) => s.enabled);
|
||||||
const currentSourceId = usePlayerStore((s) => s.sourceId);
|
const currentSourceId = usePlayerStore((s) => s.sourceId);
|
||||||
const sourceName = useMemo(() => {
|
const sourceName = useMemo(() => {
|
||||||
if (!currentSourceId) return "...";
|
if (!currentSourceId) return "...";
|
||||||
const source = getCachedMetadata().find(
|
const source = getCachedMetadata().find(
|
||||||
(src) => src.id === currentSourceId
|
(src) => src.id === currentSourceId,
|
||||||
);
|
);
|
||||||
return source?.name ?? "...";
|
return source?.name ?? "...";
|
||||||
}, [currentSourceId]);
|
}, [currentSourceId]);
|
||||||
@ -59,7 +59,7 @@ export function SettingsMenu({ id }: { id: string }) {
|
|||||||
clickable
|
clickable
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.navigate(
|
router.navigate(
|
||||||
source?.type === "file" ? "/download" : "/download/unable"
|
source?.type === "file" ? "/download" : "/download/unable",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
rightSide={<Icon className="text-xl" icon={Icons.DOWNLOAD} />}
|
rightSide={<Icon className="text-xl" icon={Icons.DOWNLOAD} />}
|
||||||
|
@ -41,7 +41,7 @@ export function EmbedOption(props: {
|
|||||||
props.routerId,
|
props.routerId,
|
||||||
props.sourceId,
|
props.sourceId,
|
||||||
props.url,
|
props.url,
|
||||||
props.embedId
|
props.embedId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
export function BackLink(props: { url: string }) {
|
export function BackLink(props: { url: string }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => history.push(props.url)}
|
onClick={() => navigate(props.url)}
|
||||||
className="py-1 -my-1 px-2 -mx-2 tabbable rounded-lg flex items-center cursor-pointer text-type-secondary hover:text-white transition-colors duration-200 font-medium"
|
className="py-1 -my-1 px-2 -mx-2 tabbable rounded-lg flex items-center cursor-pointer text-type-secondary hover:text-white transition-colors duration-200 font-medium"
|
||||||
>
|
>
|
||||||
<Icon className="mr-2" icon={Icons.ARROW_LEFT} />
|
<Icon className="mr-2" icon={Icons.ARROW_LEFT} />
|
||||||
|
@ -8,7 +8,7 @@ export function BottomControls(props: {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const setHoveringAnyControls = usePlayerStore(
|
const setHoveringAnyControls = usePlayerStore(
|
||||||
(s) => s.setHoveringAnyControls
|
(s) => s.setHoveringAnyControls,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -21,7 +21,7 @@ export interface PlayerProps {
|
|||||||
function useHovering(containerEl: RefObject<HTMLDivElement>) {
|
function useHovering(containerEl: RefObject<HTMLDivElement>) {
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const updateInterfaceHovering = usePlayerStore(
|
const updateInterfaceHovering = usePlayerStore(
|
||||||
(s) => s.updateInterfaceHovering
|
(s) => s.updateInterfaceHovering,
|
||||||
);
|
);
|
||||||
const hovering = usePlayerStore((s) => s.interface.hovering);
|
const hovering = usePlayerStore((s) => s.interface.hovering);
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ export function LeftSideControls(props: {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const setHoveringLeftControls = usePlayerStore(
|
const setHoveringLeftControls = usePlayerStore(
|
||||||
(s) => s.setHoveringLeftControls
|
(s) => s.setHoveringLeftControls,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mouseLeave = useCallback(() => {
|
const mouseLeave = useCallback(() => {
|
||||||
|
@ -79,15 +79,15 @@ export function SubtitleRenderer() {
|
|||||||
|
|
||||||
const parsedCaptions = useMemo(
|
const parsedCaptions = useMemo(
|
||||||
() => (srtData ? parseSubtitles(srtData, language) : []),
|
() => (srtData ? parseSubtitles(srtData, language) : []),
|
||||||
[srtData, language]
|
[srtData, language],
|
||||||
);
|
);
|
||||||
|
|
||||||
const visibileCaptions = useMemo(
|
const visibileCaptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
parsedCaptions.filter(({ start, end }) =>
|
parsedCaptions.filter(({ start, end }) =>
|
||||||
captionIsVisible(start, end, delay, videoTime)
|
captionIsVisible(start, end, delay, videoTime),
|
||||||
),
|
),
|
||||||
[parsedCaptions, videoTime, delay]
|
[parsedCaptions, videoTime, delay],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -11,7 +11,7 @@ export function TopControls(props: {
|
|||||||
}) {
|
}) {
|
||||||
const bannerSize = useBannerSize("player");
|
const bannerSize = useBannerSize("player");
|
||||||
const setHoveringAnyControls = usePlayerStore(
|
const setHoveringAnyControls = usePlayerStore(
|
||||||
(s) => s.setHoveringAnyControls
|
(s) => s.setHoveringAnyControls,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -36,7 +36,7 @@ function hlsLevelToQuality(level: Level): SourceQuality | null {
|
|||||||
|
|
||||||
function qualityToHlsLevel(quality: SourceQuality): number | null {
|
function qualityToHlsLevel(quality: SourceQuality): number | null {
|
||||||
const found = Object.entries(levelConversionMap).find(
|
const found = Object.entries(levelConversionMap).find(
|
||||||
(entry) => entry[1] === quality
|
(entry) => entry[1] === quality,
|
||||||
);
|
);
|
||||||
return found ? +found[0] : null;
|
return found ? +found[0] : null;
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
});
|
});
|
||||||
if (availableQuality) {
|
if (availableQuality) {
|
||||||
const levelIndex = hls.levels.findIndex(
|
const levelIndex = hls.levels.findIndex(
|
||||||
(v) => v.height === qualityToHlsLevel(availableQuality)
|
(v) => v.height === qualityToHlsLevel(availableQuality),
|
||||||
);
|
);
|
||||||
if (levelIndex !== -1) {
|
if (levelIndex !== -1) {
|
||||||
hls.currentLevel = levelIndex;
|
hls.currentLevel = levelIndex;
|
||||||
@ -182,10 +182,10 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
videoElement.addEventListener("canplay", () => emit("loading", false));
|
videoElement.addEventListener("canplay", () => emit("loading", false));
|
||||||
videoElement.addEventListener("waiting", () => emit("loading", true));
|
videoElement.addEventListener("waiting", () => emit("loading", true));
|
||||||
videoElement.addEventListener("volumechange", () =>
|
videoElement.addEventListener("volumechange", () =>
|
||||||
emit("volumechange", videoElement?.muted ? 0 : videoElement?.volume ?? 0)
|
emit("volumechange", videoElement?.muted ? 0 : videoElement?.volume ?? 0),
|
||||||
);
|
);
|
||||||
videoElement.addEventListener("timeupdate", () =>
|
videoElement.addEventListener("timeupdate", () =>
|
||||||
emit("time", videoElement?.currentTime ?? 0)
|
emit("time", videoElement?.currentTime ?? 0),
|
||||||
);
|
);
|
||||||
videoElement.addEventListener("loadedmetadata", () => {
|
videoElement.addEventListener("loadedmetadata", () => {
|
||||||
if (
|
if (
|
||||||
@ -202,7 +202,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
if (videoElement)
|
if (videoElement)
|
||||||
emit(
|
emit(
|
||||||
"buffered",
|
"buffered",
|
||||||
handleBuffered(videoElement.currentTime, videoElement.buffered)
|
handleBuffered(videoElement.currentTime, videoElement.buffered),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
videoElement.addEventListener("webkitendfullscreen", () => {
|
videoElement.addEventListener("webkitendfullscreen", () => {
|
||||||
@ -216,7 +216,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
if (e.availability === "available") {
|
if (e.availability === "available") {
|
||||||
emit("canairplay", true);
|
emit("canairplay", true);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
videoElement.addEventListener("ratechange", () => {
|
videoElement.addEventListener("ratechange", () => {
|
||||||
if (videoElement) emit("playbackrate", videoElement.playbackRate);
|
if (videoElement) emit("playbackrate", videoElement.playbackRate);
|
||||||
@ -368,7 +368,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
webkitPlayer.webkitSetPresentationMode(
|
webkitPlayer.webkitSetPresentationMode(
|
||||||
webkitPlayer.webkitPresentationMode === "picture-in-picture"
|
webkitPlayer.webkitPresentationMode === "picture-in-picture"
|
||||||
? "inline"
|
? "inline"
|
||||||
: "picture-in-picture"
|
: "picture-in-picture",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (canPictureInPicture()) {
|
if (canPictureInPicture()) {
|
||||||
|
@ -28,7 +28,7 @@ export interface ChromeCastDisplayInterfaceOptions {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export function makeChromecastDisplayInterface(
|
export function makeChromecastDisplayInterface(
|
||||||
ops: ChromeCastDisplayInterfaceOptions
|
ops: ChromeCastDisplayInterfaceOptions,
|
||||||
): DisplayInterface {
|
): DisplayInterface {
|
||||||
const { emit, on, off } = makeEmitter<DisplayInterfaceEvents>();
|
const { emit, on, off } = makeEmitter<DisplayInterfaceEvents>();
|
||||||
let isPaused = false;
|
let isPaused = false;
|
||||||
@ -89,12 +89,12 @@ export function makeChromecastDisplayInterface(
|
|||||||
};
|
};
|
||||||
ops.controller?.addEventListener(
|
ops.controller?.addEventListener(
|
||||||
cast.framework.RemotePlayerEventType.ANY_CHANGE,
|
cast.framework.RemotePlayerEventType.ANY_CHANGE,
|
||||||
listen
|
listen,
|
||||||
);
|
);
|
||||||
return () => {
|
return () => {
|
||||||
ops.controller?.removeEventListener(
|
ops.controller?.removeEventListener(
|
||||||
cast.framework.RemotePlayerEventType.ANY_CHANGE,
|
cast.framework.RemotePlayerEventType.ANY_CHANGE,
|
||||||
listen
|
listen,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
|||||||
load(ops: qualityChangeOptions): void;
|
load(ops: qualityChangeOptions): void;
|
||||||
changeQuality(
|
changeQuality(
|
||||||
automaticQuality: boolean,
|
automaticQuality: boolean,
|
||||||
preferredQuality: SourceQuality | null
|
preferredQuality: SourceQuality | null,
|
||||||
): void;
|
): void;
|
||||||
processVideoElement(video: HTMLVideoElement): void;
|
processVideoElement(video: HTMLVideoElement): void;
|
||||||
processContainerElement(container: HTMLElement): void;
|
processContainerElement(container: HTMLElement): void;
|
||||||
|
@ -8,7 +8,7 @@ export function useCaptions() {
|
|||||||
const setLanguage = useSubtitleStore((s) => s.setLanguage);
|
const setLanguage = useSubtitleStore((s) => s.setLanguage);
|
||||||
const enabled = useSubtitleStore((s) => s.enabled);
|
const enabled = useSubtitleStore((s) => s.enabled);
|
||||||
const resetSubtitleSpecificSettings = useSubtitleStore(
|
const resetSubtitleSpecificSettings = useSubtitleStore(
|
||||||
(s) => s.resetSubtitleSpecificSettings
|
(s) => s.resetSubtitleSpecificSettings,
|
||||||
);
|
);
|
||||||
const setCaption = usePlayerStore((s) => s.setCaption);
|
const setCaption = usePlayerStore((s) => s.setCaption);
|
||||||
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
|
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
|
||||||
@ -27,7 +27,7 @@ export function useCaptions() {
|
|||||||
resetSubtitleSpecificSettings();
|
resetSubtitleSpecificSettings();
|
||||||
setLanguage(language);
|
setLanguage(language);
|
||||||
},
|
},
|
||||||
[setLanguage, captionList, setCaption, resetSubtitleSpecificSettings]
|
[setLanguage, captionList, setCaption, resetSubtitleSpecificSettings],
|
||||||
);
|
);
|
||||||
|
|
||||||
const disable = useCallback(async () => {
|
const disable = useCallback(async () => {
|
||||||
|
@ -22,7 +22,7 @@ export function useInitializeSource() {
|
|||||||
const source = usePlayerStore((s) => s.source);
|
const source = usePlayerStore((s) => s.source);
|
||||||
const sourceIdentifier = useMemo(
|
const sourceIdentifier = useMemo(
|
||||||
() => (source ? JSON.stringify(source) : null),
|
() => (source ? JSON.stringify(source) : null),
|
||||||
[source]
|
[source],
|
||||||
);
|
);
|
||||||
const { selectLastUsedLanguageIfEnabled } = useCaptions();
|
const { selectLastUsedLanguageIfEnabled } = useCaptions();
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ export interface Source {
|
|||||||
|
|
||||||
function getProgress(
|
function getProgress(
|
||||||
items: Record<string, ProgressMediaItem>,
|
items: Record<string, ProgressMediaItem>,
|
||||||
meta: PlayerMeta | null
|
meta: PlayerMeta | null,
|
||||||
): number {
|
): number {
|
||||||
const item = items[meta?.tmdbId ?? ""];
|
const item = items[meta?.tmdbId ?? ""];
|
||||||
if (!item || !meta) return 0;
|
if (!item || !meta) return 0;
|
||||||
@ -38,10 +38,10 @@ export function usePlayer() {
|
|||||||
const setSourceId = usePlayerStore((s) => s.setSourceId);
|
const setSourceId = usePlayerStore((s) => s.setSourceId);
|
||||||
const status = usePlayerStore((s) => s.status);
|
const status = usePlayerStore((s) => s.status);
|
||||||
const shouldStartFromBeginning = usePlayerStore(
|
const shouldStartFromBeginning = usePlayerStore(
|
||||||
(s) => s.interface.shouldStartFromBeginning
|
(s) => s.interface.shouldStartFromBeginning,
|
||||||
);
|
);
|
||||||
const setShouldStartFromBeginning = usePlayerStore(
|
const setShouldStartFromBeginning = usePlayerStore(
|
||||||
(s) => s.setShouldStartFromBeginning
|
(s) => s.setShouldStartFromBeginning,
|
||||||
);
|
);
|
||||||
const reset = usePlayerStore((s) => s.reset);
|
const reset = usePlayerStore((s) => s.reset);
|
||||||
const meta = usePlayerStore((s) => s.meta);
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
@ -61,7 +61,7 @@ export function usePlayer() {
|
|||||||
source: SourceSliceSource,
|
source: SourceSliceSource,
|
||||||
captions: CaptionListItem[],
|
captions: CaptionListItem[],
|
||||||
sourceId: string | null,
|
sourceId: string | null,
|
||||||
startAtOverride?: number
|
startAtOverride?: number,
|
||||||
) {
|
) {
|
||||||
const start = startAtOverride ?? getProgress(progressStore.items, meta);
|
const start = startAtOverride ?? getProgress(progressStore.items, meta);
|
||||||
setCaption(null);
|
setCaption(null);
|
||||||
|
@ -13,14 +13,14 @@ export function usePlayerMeta() {
|
|||||||
const { meta, setMeta } = usePlayer();
|
const { meta, setMeta } = usePlayer();
|
||||||
const scrapeMedia = useMemo(
|
const scrapeMedia = useMemo(
|
||||||
() => (meta ? metaToScrapeMedia(meta) : null),
|
() => (meta ? metaToScrapeMedia(meta) : null),
|
||||||
[meta]
|
[meta],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setDirectMeta = useCallback(
|
const setDirectMeta = useCallback(
|
||||||
(m: PlayerMeta) => {
|
(m: PlayerMeta) => {
|
||||||
setMeta(m, playerStatus.SCRAPING);
|
setMeta(m, playerStatus.SCRAPING);
|
||||||
},
|
},
|
||||||
[setMeta]
|
[setMeta],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setPlayerMeta = useCallback(
|
const setPlayerMeta = useCallback(
|
||||||
@ -65,7 +65,7 @@ export function usePlayerMeta() {
|
|||||||
setDirectMeta(playerMeta);
|
setDirectMeta(playerMeta);
|
||||||
return playerMeta;
|
return playerMeta;
|
||||||
},
|
},
|
||||||
[setDirectMeta]
|
[setDirectMeta],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -4,12 +4,12 @@ import { usePlayerStore } from "@/stores/player/store";
|
|||||||
export function useShouldShowControls() {
|
export function useShouldShowControls() {
|
||||||
const hovering = usePlayerStore((s) => s.interface.hovering);
|
const hovering = usePlayerStore((s) => s.interface.hovering);
|
||||||
const lastHoveringState = usePlayerStore(
|
const lastHoveringState = usePlayerStore(
|
||||||
(s) => s.interface.lastHoveringState
|
(s) => s.interface.lastHoveringState,
|
||||||
);
|
);
|
||||||
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
||||||
const hasOpenOverlay = usePlayerStore((s) => s.interface.hasOpenOverlay);
|
const hasOpenOverlay = usePlayerStore((s) => s.interface.hasOpenOverlay);
|
||||||
const isHoveringControls = usePlayerStore(
|
const isHoveringControls = usePlayerStore(
|
||||||
(s) => s.interface.isHoveringControls
|
(s) => s.interface.isHoveringControls,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isUsingTouch = lastHoveringState === PlayerHoverState.MOBILE_TAPPED;
|
const isUsingTouch = lastHoveringState === PlayerHoverState.MOBILE_TAPPED;
|
||||||
|
@ -24,7 +24,7 @@ export function useEmbedScraping(
|
|||||||
routerId: string,
|
routerId: string,
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
url: string,
|
url: string,
|
||||||
embedId: string
|
embedId: string,
|
||||||
) {
|
) {
|
||||||
const setSource = usePlayerStore((s) => s.setSource);
|
const setSource = usePlayerStore((s) => s.setSource);
|
||||||
const setCaption = usePlayerStore((s) => s.setCaption);
|
const setCaption = usePlayerStore((s) => s.setCaption);
|
||||||
@ -43,7 +43,7 @@ export function useEmbedScraping(
|
|||||||
const baseUrlMaker = makeProviderUrl(providerApiUrl);
|
const baseUrlMaker = makeProviderUrl(providerApiUrl);
|
||||||
const conn = await connectServerSideEvents<EmbedOutput>(
|
const conn = await connectServerSideEvents<EmbedOutput>(
|
||||||
baseUrlMaker.scrapeEmbed(embedId, url),
|
baseUrlMaker.scrapeEmbed(embedId, url),
|
||||||
["completed", "noOutput"]
|
["completed", "noOutput"],
|
||||||
);
|
);
|
||||||
result = await conn.promise();
|
result = await conn.promise();
|
||||||
} else {
|
} else {
|
||||||
@ -62,7 +62,7 @@ export function useEmbedScraping(
|
|||||||
sourceId,
|
sourceId,
|
||||||
embedId,
|
embedId,
|
||||||
status,
|
status,
|
||||||
err
|
err,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
throw err;
|
throw err;
|
||||||
@ -75,7 +75,7 @@ export function useEmbedScraping(
|
|||||||
setSource(
|
setSource(
|
||||||
convertRunoutputToSource({ stream: result.stream }),
|
convertRunoutputToSource({ stream: result.stream }),
|
||||||
convertProviderCaption(result.stream.captions),
|
convertProviderCaption(result.stream.captions),
|
||||||
progress
|
progress,
|
||||||
);
|
);
|
||||||
router.close();
|
router.close();
|
||||||
}, [embedId, sourceId, meta, router, report, setCaption]);
|
}, [embedId, sourceId, meta, router, report, setCaption]);
|
||||||
@ -107,7 +107,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
|
|||||||
const baseUrlMaker = makeProviderUrl(providerApiUrl);
|
const baseUrlMaker = makeProviderUrl(providerApiUrl);
|
||||||
const conn = await connectServerSideEvents<SourcererOutput>(
|
const conn = await connectServerSideEvents<SourcererOutput>(
|
||||||
baseUrlMaker.scrapeSource(sourceId, scrapeMedia),
|
baseUrlMaker.scrapeSource(sourceId, scrapeMedia),
|
||||||
["completed", "noOutput"]
|
["completed", "noOutput"],
|
||||||
);
|
);
|
||||||
result = await conn.promise();
|
result = await conn.promise();
|
||||||
} else {
|
} else {
|
||||||
@ -134,7 +134,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
|
|||||||
setSource(
|
setSource(
|
||||||
convertRunoutputToSource({ stream: result.stream }),
|
convertRunoutputToSource({ stream: result.stream }),
|
||||||
convertProviderCaption(result.stream.captions),
|
convertProviderCaption(result.stream.captions),
|
||||||
progress
|
progress,
|
||||||
);
|
);
|
||||||
setSourceId(sourceId);
|
setSourceId(sourceId);
|
||||||
router.close();
|
router.close();
|
||||||
@ -149,9 +149,9 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
|
|||||||
const conn = await connectServerSideEvents<EmbedOutput>(
|
const conn = await connectServerSideEvents<EmbedOutput>(
|
||||||
baseUrlMaker.scrapeEmbed(
|
baseUrlMaker.scrapeEmbed(
|
||||||
result.embeds[0].embedId,
|
result.embeds[0].embedId,
|
||||||
result.embeds[0].url
|
result.embeds[0].url,
|
||||||
),
|
),
|
||||||
["completed", "noOutput"]
|
["completed", "noOutput"],
|
||||||
);
|
);
|
||||||
embedResult = await conn.promise();
|
embedResult = await conn.promise();
|
||||||
} else {
|
} else {
|
||||||
@ -170,7 +170,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
|
|||||||
sourceId,
|
sourceId,
|
||||||
result.embeds[0].embedId,
|
result.embeds[0].embedId,
|
||||||
status,
|
status,
|
||||||
err
|
err,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
throw err;
|
throw err;
|
||||||
@ -181,7 +181,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
|
|||||||
sourceId,
|
sourceId,
|
||||||
result.embeds[0].embedId,
|
result.embeds[0].embedId,
|
||||||
"success",
|
"success",
|
||||||
null
|
null,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
setSourceId(sourceId);
|
setSourceId(sourceId);
|
||||||
@ -189,7 +189,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
|
|||||||
setSource(
|
setSource(
|
||||||
convertRunoutputToSource({ stream: embedResult.stream }),
|
convertRunoutputToSource({ stream: embedResult.stream }),
|
||||||
convertProviderCaption(embedResult.stream.captions),
|
convertProviderCaption(embedResult.stream.captions),
|
||||||
progress
|
progress,
|
||||||
);
|
);
|
||||||
router.close();
|
router.close();
|
||||||
}
|
}
|
||||||
|
@ -102,13 +102,13 @@ export function CastingInternal() {
|
|||||||
}
|
}
|
||||||
newControlller.addEventListener(
|
newControlller.addEventListener(
|
||||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
||||||
connectionChanged
|
connectionChanged,
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
newControlller.removeEventListener(
|
newControlller.removeEventListener(
|
||||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
||||||
connectionChanged
|
connectionChanged,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, [available, setPlayer, setController, setInstance, setIsCasting]);
|
}, [available, setPlayer, setController, setInstance, setIsCasting]);
|
||||||
|
@ -9,7 +9,7 @@ export function SectionTitle(props: {
|
|||||||
<h3
|
<h3
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"uppercase font-bold text-video-context-type-secondary text-xs pt-8 pl-1 pb-2.5 border-b border-video-context-border",
|
"uppercase font-bold text-video-context-type-secondary text-xs pt-8 pl-1 pb-2.5 border-b border-video-context-border",
|
||||||
props.className
|
props.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
@ -47,7 +47,7 @@ export function ScrollToActiveSection(props: {
|
|||||||
|
|
||||||
scrollingContainer.current?.scrollTo(
|
scrollingContainer.current?.scrollTo(
|
||||||
0,
|
0,
|
||||||
activeYPos - boxRect.height / 2 + activeLinkRect.height / 2
|
activeYPos - boxRect.height / 2 + activeLinkRect.height / 2,
|
||||||
);
|
);
|
||||||
}, [props.loaded]);
|
}, [props.loaded]);
|
||||||
|
|
||||||
|
@ -75,11 +75,11 @@ export function KeyboardEvents() {
|
|||||||
}
|
}
|
||||||
if (k === "ArrowUp")
|
if (k === "ArrowUp")
|
||||||
dataRef.current.setVolume(
|
dataRef.current.setVolume(
|
||||||
(dataRef.current.mediaPlaying?.volume || 0) + 0.15
|
(dataRef.current.mediaPlaying?.volume || 0) + 0.15,
|
||||||
);
|
);
|
||||||
if (k === "ArrowDown")
|
if (k === "ArrowDown")
|
||||||
dataRef.current.setVolume(
|
dataRef.current.setVolume(
|
||||||
(dataRef.current.mediaPlaying?.volume || 0) - 0.15
|
(dataRef.current.mediaPlaying?.volume || 0) - 0.15,
|
||||||
);
|
);
|
||||||
if (k === "m") dataRef.current.toggleMute();
|
if (k === "m") dataRef.current.toggleMute();
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ export interface StatusCircleLoading extends StatusCircle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function statusIsLoading(
|
function statusIsLoading(
|
||||||
props: StatusCircle | StatusCircleLoading
|
props: StatusCircle | StatusCircleLoading,
|
||||||
): props is StatusCircleLoading {
|
): props is StatusCircleLoading {
|
||||||
return props.type === "loading";
|
return props.type === "loading";
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) {
|
|||||||
() => ({
|
() => ({
|
||||||
percentage: statusIsLoading(props) ? props.percentage : 0,
|
percentage: statusIsLoading(props) ? props.percentage : 0,
|
||||||
}),
|
}),
|
||||||
[props]
|
[props],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -95,7 +95,7 @@ class ThumnbnailWorker {
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
this.canvasEl.width,
|
this.canvasEl.width,
|
||||||
this.canvasEl.height
|
this.canvasEl.height,
|
||||||
);
|
);
|
||||||
const imgUrl = this.canvasEl.toDataURL();
|
const imgUrl = this.canvasEl.toDataURL();
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ export function VideoClickTarget(props: { showingControls: boolean }) {
|
|||||||
const display = usePlayerStore((s) => s.display);
|
const display = usePlayerStore((s) => s.display);
|
||||||
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
||||||
const updateInterfaceHovering = usePlayerStore(
|
const updateInterfaceHovering = usePlayerStore(
|
||||||
(s) => s.updateInterfaceHovering
|
(s) => s.updateInterfaceHovering,
|
||||||
);
|
);
|
||||||
const hovering = usePlayerStore((s) => s.interface.hovering);
|
const hovering = usePlayerStore((s) => s.interface.hovering);
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ export function VideoClickTarget(props: { showingControls: boolean }) {
|
|||||||
updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED);
|
updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED);
|
||||||
else updateInterfaceHovering(PlayerHoverState.NOT_HOVERING);
|
else updateInterfaceHovering(PlayerHoverState.NOT_HOVERING);
|
||||||
},
|
},
|
||||||
[display, isPaused, hovering, updateInterfaceHovering]
|
[display, isPaused, hovering, updateInterfaceHovering],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
|
@ -70,7 +70,7 @@ function VideoElement() {
|
|||||||
const language = usePlayerStore((s) => s.caption.selected?.language);
|
const language = usePlayerStore((s) => s.caption.selected?.language);
|
||||||
const trackObjectUrl = useObjectUrl(
|
const trackObjectUrl = useObjectUrl(
|
||||||
() => (srtData ? convertSubtitlesToObjectUrl(srtData) : null),
|
() => (srtData ? convertSubtitlesToObjectUrl(srtData) : null),
|
||||||
[srtData]
|
[srtData],
|
||||||
);
|
);
|
||||||
|
|
||||||
// report video element to display interface
|
// report video element to display interface
|
||||||
|
@ -12,7 +12,7 @@ export function captionIsVisible(
|
|||||||
start: number,
|
start: number,
|
||||||
end: number,
|
end: number,
|
||||||
delay: number,
|
delay: number,
|
||||||
currentTime: number
|
currentTime: number,
|
||||||
) {
|
) {
|
||||||
const delayedStart = start / 1000 + delay;
|
const delayedStart = start / 1000 + delay;
|
||||||
const delayedEnd = end / 1000 + delay;
|
const delayedEnd = end / 1000 + delay;
|
||||||
@ -52,7 +52,7 @@ export function convertSubtitlesToSrt(text: string): string {
|
|||||||
|
|
||||||
export function parseSubtitles(
|
export function parseSubtitles(
|
||||||
text: string,
|
text: string,
|
||||||
_language?: string
|
_language?: string,
|
||||||
): CaptionCueType[] {
|
): CaptionCueType[] {
|
||||||
const vtt = convertSubtitlesToVtt(text);
|
const vtt = convertSubtitlesToVtt(text);
|
||||||
return parse(vtt).filter((cue) => cue.type === "caption") as CaptionCueType[];
|
return parse(vtt).filter((cue) => cue.type === "caption") as CaptionCueType[];
|
||||||
@ -64,7 +64,7 @@ function stringToBase64(input: string): string {
|
|||||||
|
|
||||||
export function convertSubtitlesToSrtDataurl(text: string): string {
|
export function convertSubtitlesToSrtDataurl(text: string): string {
|
||||||
return `data:application/x-subrip;base64,${stringToBase64(
|
return `data:application/x-subrip;base64,${stringToBase64(
|
||||||
convertSubtitlesToSrt(text)
|
convertSubtitlesToSrt(text),
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,12 +72,12 @@ export function convertSubtitlesToObjectUrl(text: string): string {
|
|||||||
return URL.createObjectURL(
|
return URL.createObjectURL(
|
||||||
new Blob([convertSubtitlesToVtt(text)], {
|
new Blob([convertSubtitlesToVtt(text)], {
|
||||||
type: "text/vtt",
|
type: "text/vtt",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertProviderCaption(
|
export function convertProviderCaption(
|
||||||
captions: RunOutput["stream"]["captions"]
|
captions: RunOutput["stream"]["captions"],
|
||||||
): CaptionListItem[] {
|
): CaptionListItem[] {
|
||||||
return captions.map((v) => ({
|
return captions.map((v) => ({
|
||||||
language: v.language,
|
language: v.language,
|
||||||
|
@ -18,7 +18,7 @@ const mediaErrorMap: Record<number, { name: string; key: string }> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function getMediaErrorDetails(
|
export function getMediaErrorDetails(
|
||||||
err: MediaError | null
|
err: MediaError | null,
|
||||||
): (typeof mediaErrorMap)[number] {
|
): (typeof mediaErrorMap)[number] {
|
||||||
const item = mediaErrorMap[err?.code ?? -1];
|
const item = mediaErrorMap[err?.code ?? -1];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
|
@ -36,7 +36,7 @@ export const TextInputControl = forwardRef<
|
|||||||
onFocus,
|
onFocus,
|
||||||
passwordToggleable,
|
passwordToggleable,
|
||||||
},
|
},
|
||||||
ref
|
ref,
|
||||||
) => {
|
) => {
|
||||||
let inputType = "text";
|
let inputType = "text";
|
||||||
const [showPassword, setShowPassword] = useState(true);
|
const [showPassword, setShowPassword] = useState(true);
|
||||||
@ -81,5 +81,5 @@ export const TextInputControl = forwardRef<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return input;
|
return input;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,7 @@ export function Paragraph(props: {
|
|||||||
<p
|
<p
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-errors-type-secondary",
|
"text-errors-type-secondary",
|
||||||
props.marginClass ?? "mt-6"
|
props.marginClass ?? "mt-6",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -8,7 +8,7 @@ export function Title(props: {
|
|||||||
<h2
|
<h2
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-white text-3xl font-bold text-opacity-100 mt-6",
|
"text-white text-3xl font-bold text-opacity-100 mt-6",
|
||||||
props.className
|
props.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -5,7 +5,7 @@ export function Divider(props: { marginClass?: string }) {
|
|||||||
<hr
|
<hr
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full h-px border-0 bg-utils-divider bg-opacity-50",
|
"w-full h-px border-0 bg-utils-divider bg-opacity-50",
|
||||||
props.marginClass ?? "my-8"
|
props.marginClass ?? "my-8",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -46,11 +46,11 @@ function Light(props: FlareProps) {
|
|||||||
const halfSize = size / 2;
|
const halfSize = size / 2;
|
||||||
outerRef.current.style.setProperty(
|
outerRef.current.style.setProperty(
|
||||||
"--bg-x",
|
"--bg-x",
|
||||||
`${(e.clientX - rect.left - halfSize).toFixed(0)}px`
|
`${(e.clientX - rect.left - halfSize).toFixed(0)}px`,
|
||||||
);
|
);
|
||||||
outerRef.current.style.setProperty(
|
outerRef.current.style.setProperty(
|
||||||
"--bg-y",
|
"--bg-y",
|
||||||
`${(e.clientY - rect.top - halfSize).toFixed(0)}px`
|
`${(e.clientY - rect.top - halfSize).toFixed(0)}px`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
document.addEventListener("mousemove", mouseMove);
|
document.addEventListener("mousemove", mouseMove);
|
||||||
@ -66,7 +66,7 @@ function Light(props: FlareProps) {
|
|||||||
props.className,
|
props.className,
|
||||||
{
|
{
|
||||||
"!opacity-100": props.enabled ?? false,
|
"!opacity-100": props.enabled ?? false,
|
||||||
}
|
},
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `radial-gradient(circle at center, rgba(var(${cssVar}), 1), rgba(var(${cssVar}), 0) 70%)`,
|
backgroundImage: `radial-gradient(circle at center, rgba(var(${cssVar}), 1), rgba(var(${cssVar}), 0) 70%)`,
|
||||||
@ -79,7 +79,7 @@ function Light(props: FlareProps) {
|
|||||||
className={c(
|
className={c(
|
||||||
"absolute inset-[1px] overflow-hidden",
|
"absolute inset-[1px] overflow-hidden",
|
||||||
props.className,
|
props.className,
|
||||||
props.backgroundClass
|
props.backgroundClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -33,7 +33,7 @@ class Particle {
|
|||||||
options: LightbarOptions = {
|
options: LightbarOptions = {
|
||||||
horizontalMotion: false,
|
horizontalMotion: false,
|
||||||
sizeRange: [10, 10],
|
sizeRange: [10, 10],
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
if (options.imgSrc) {
|
if (options.imgSrc) {
|
||||||
this.image = new Image();
|
this.image = new Image();
|
||||||
@ -117,7 +117,7 @@ class Particle {
|
|||||||
this.radius * 1.5,
|
this.radius * 1.5,
|
||||||
this.direction,
|
this.direction,
|
||||||
0,
|
0,
|
||||||
Math.PI * 2
|
Math.PI * 2,
|
||||||
);
|
);
|
||||||
ctx.fillStyle = "white";
|
ctx.fillStyle = "white";
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
@ -8,7 +8,7 @@ export function Ol(props: { items: React.ReactNode[] }) {
|
|||||||
<li
|
<li
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"grid grid-cols-[auto,1fr] gap-6",
|
"grid grid-cols-[auto,1fr] gap-6",
|
||||||
i !== props.items.length - 1 ? "pb-12" : undefined
|
i !== props.items.length - 1 ? "pb-12" : undefined,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative z-0">
|
<div className="relative z-0">
|
||||||
|
@ -24,7 +24,7 @@ interface Props {
|
|||||||
|
|
||||||
function getClasses(
|
function getClasses(
|
||||||
animation: TransitionAnimations,
|
animation: TransitionAnimations,
|
||||||
duration: string
|
duration: string,
|
||||||
): TransitionClasses {
|
): TransitionClasses {
|
||||||
if (animation === "slide-down") {
|
if (animation === "slide-down") {
|
||||||
return {
|
return {
|
||||||
|
@ -67,7 +67,7 @@ export function useAuth() {
|
|||||||
const publicKeyBase64Url = bytesToBase64Url(keys.publicKey);
|
const publicKeyBase64Url = bytesToBase64Url(keys.publicKey);
|
||||||
const { challenge } = await getLoginChallengeToken(
|
const { challenge } = await getLoginChallengeToken(
|
||||||
backendUrl,
|
backendUrl,
|
||||||
publicKeyBase64Url
|
publicKeyBase64Url,
|
||||||
);
|
);
|
||||||
const signature = await signChallenge(keys, challenge);
|
const signature = await signChallenge(keys, challenge);
|
||||||
const loginResult = await loginAccount(backendUrl, {
|
const loginResult = await loginAccount(backendUrl, {
|
||||||
@ -83,7 +83,7 @@ export function useAuth() {
|
|||||||
const seedBase64 = bytesToBase64(keys.seed);
|
const seedBase64 = bytesToBase64(keys.seed);
|
||||||
return userDataLogin(loginResult, user.user, user.session, seedBase64);
|
return userDataLogin(loginResult, user.user, user.session, seedBase64);
|
||||||
},
|
},
|
||||||
[userDataLogin, backendUrl]
|
[userDataLogin, backendUrl],
|
||||||
);
|
);
|
||||||
|
|
||||||
const logout = useCallback(async () => {
|
const logout = useCallback(async () => {
|
||||||
@ -92,7 +92,7 @@ export function useAuth() {
|
|||||||
await removeSession(
|
await removeSession(
|
||||||
backendUrl,
|
backendUrl,
|
||||||
currentAccount.token,
|
currentAccount.token,
|
||||||
currentAccount.sessionId
|
currentAccount.sessionId,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// we dont care about failing to delete session
|
// we dont care about failing to delete session
|
||||||
@ -104,7 +104,7 @@ export function useAuth() {
|
|||||||
async (registerData: RegistrationData) => {
|
async (registerData: RegistrationData) => {
|
||||||
const { challenge } = await getRegisterChallengeToken(
|
const { challenge } = await getRegisterChallengeToken(
|
||||||
backendUrl,
|
backendUrl,
|
||||||
registerData.recaptchaToken
|
registerData.recaptchaToken,
|
||||||
);
|
);
|
||||||
const keys = await keysFromMnemonic(registerData.mnemonic);
|
const keys = await keysFromMnemonic(registerData.mnemonic);
|
||||||
const signature = await signChallenge(keys, challenge);
|
const signature = await signChallenge(keys, challenge);
|
||||||
@ -122,17 +122,17 @@ export function useAuth() {
|
|||||||
registerResult,
|
registerResult,
|
||||||
registerResult.user,
|
registerResult.user,
|
||||||
registerResult.session,
|
registerResult.session,
|
||||||
bytesToBase64(keys.seed)
|
bytesToBase64(keys.seed),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[backendUrl, userDataLogin]
|
[backendUrl, userDataLogin],
|
||||||
);
|
);
|
||||||
|
|
||||||
const importData = useCallback(
|
const importData = useCallback(
|
||||||
async (
|
async (
|
||||||
account: AccountWithToken,
|
account: AccountWithToken,
|
||||||
progressItems: Record<string, ProgressMediaItem>,
|
progressItems: Record<string, ProgressMediaItem>,
|
||||||
bookmarks: Record<string, BookmarkMediaItem>
|
bookmarks: Record<string, BookmarkMediaItem>,
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
Object.keys(progressItems).length === 0 &&
|
Object.keys(progressItems).length === 0 &&
|
||||||
@ -142,17 +142,17 @@ export function useAuth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const progressInputs = Object.entries(progressItems).flatMap(
|
const progressInputs = Object.entries(progressItems).flatMap(
|
||||||
([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item)
|
([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bookmarkInputs = Object.entries(bookmarks).map(([tmdbId, item]) =>
|
const bookmarkInputs = Object.entries(bookmarks).map(([tmdbId, item]) =>
|
||||||
bookmarkMediaToInput(tmdbId, item)
|
bookmarkMediaToInput(tmdbId, item),
|
||||||
);
|
);
|
||||||
|
|
||||||
await importProgress(backendUrl, account, progressInputs);
|
await importProgress(backendUrl, account, progressInputs);
|
||||||
await importBookmarks(backendUrl, account, bookmarkInputs);
|
await importBookmarks(backendUrl, account, bookmarkInputs);
|
||||||
},
|
},
|
||||||
[backendUrl]
|
[backendUrl],
|
||||||
);
|
);
|
||||||
|
|
||||||
const restore = useCallback(
|
const restore = useCallback(
|
||||||
@ -180,7 +180,7 @@ export function useAuth() {
|
|||||||
|
|
||||||
syncData(user.user, user.session, progress, bookmarks, settings);
|
syncData(user.user, user.session, progress, bookmarks, settings);
|
||||||
},
|
},
|
||||||
[backendUrl, syncData, logout]
|
[backendUrl, syncData, logout],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -25,7 +25,7 @@ export function useAuthData() {
|
|||||||
const setTheme = useThemeStore((s) => s.setTheme);
|
const setTheme = useThemeStore((s) => s.setTheme);
|
||||||
const setAppLanguage = useLanguageStore((s) => s.setLanguage);
|
const setAppLanguage = useLanguageStore((s) => s.setLanguage);
|
||||||
const importSubtitleLanguage = useSubtitleStore(
|
const importSubtitleLanguage = useSubtitleStore(
|
||||||
(s) => s.importSubtitleLanguage
|
(s) => s.importSubtitleLanguage,
|
||||||
);
|
);
|
||||||
|
|
||||||
const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks);
|
const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks);
|
||||||
@ -36,7 +36,7 @@ export function useAuthData() {
|
|||||||
loginResponse: LoginResponse,
|
loginResponse: LoginResponse,
|
||||||
user: UserResponse,
|
user: UserResponse,
|
||||||
session: SessionResponse,
|
session: SessionResponse,
|
||||||
seed: string
|
seed: string,
|
||||||
) => {
|
) => {
|
||||||
const account = {
|
const account = {
|
||||||
token: loginResponse.token,
|
token: loginResponse.token,
|
||||||
@ -49,7 +49,7 @@ export function useAuthData() {
|
|||||||
setAccount(account);
|
setAccount(account);
|
||||||
return account;
|
return account;
|
||||||
},
|
},
|
||||||
[setAccount]
|
[setAccount],
|
||||||
);
|
);
|
||||||
|
|
||||||
const logout = useCallback(async () => {
|
const logout = useCallback(async () => {
|
||||||
@ -64,7 +64,7 @@ export function useAuthData() {
|
|||||||
_session: SessionResponse,
|
_session: SessionResponse,
|
||||||
progress: ProgressResponse[],
|
progress: ProgressResponse[],
|
||||||
bookmarks: BookmarkResponse[],
|
bookmarks: BookmarkResponse[],
|
||||||
settings: SettingsResponse
|
settings: SettingsResponse,
|
||||||
) => {
|
) => {
|
||||||
replaceBookmarks(bookmarkResponsesToEntries(bookmarks));
|
replaceBookmarks(bookmarkResponsesToEntries(bookmarks));
|
||||||
replaceItems(progressResponsesToEntries(progress));
|
replaceItems(progressResponsesToEntries(progress));
|
||||||
@ -87,7 +87,7 @@ export function useAuthData() {
|
|||||||
setAppLanguage,
|
setAppLanguage,
|
||||||
importSubtitleLanguage,
|
importSubtitleLanguage,
|
||||||
setTheme,
|
setTheme,
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -17,7 +17,7 @@ export function useRouterAnchorUpdate(id: string) {
|
|||||||
const setAnchorPoint = useOverlayStore((s) => s.setAnchorPoint);
|
const setAnchorPoint = useOverlayStore((s) => s.setAnchorPoint);
|
||||||
const routerActive = useMemo(
|
const routerActive = useMemo(
|
||||||
() => !!route && route.startsWith(`/${id}`),
|
() => !!route && route.startsWith(`/${id}`),
|
||||||
[route, id]
|
[route, id],
|
||||||
);
|
);
|
||||||
|
|
||||||
const update = useCallback(() => {
|
const update = useCallback(() => {
|
||||||
@ -96,7 +96,7 @@ export function useInternalOverlayRouter(id: string) {
|
|||||||
if (route && !preventRouteClear) setRoute(null);
|
if (route && !preventRouteClear) setRoute(null);
|
||||||
setTransition(null);
|
setTransition(null);
|
||||||
},
|
},
|
||||||
[setRoute, route, setTransition]
|
[setRoute, route, setTransition],
|
||||||
);
|
);
|
||||||
|
|
||||||
const open = useCallback(
|
const open = useCallback(
|
||||||
@ -104,7 +104,7 @@ export function useInternalOverlayRouter(id: string) {
|
|||||||
setTransition(null);
|
setTransition(null);
|
||||||
setRoute(joinPath(splitPath(defaultRoute, id)));
|
setRoute(joinPath(splitPath(defaultRoute, id)));
|
||||||
},
|
},
|
||||||
[id, setRoute, setTransition]
|
[id, setRoute, setTransition],
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeRoute = routerActive
|
const activeRoute = routerActive
|
||||||
|
@ -13,7 +13,7 @@ export function makePercentage(num: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isClickEvent(
|
function isClickEvent(
|
||||||
evt: ActivityEvent
|
evt: ActivityEvent,
|
||||||
): evt is React.MouseEvent<HTMLElement> | MouseEvent {
|
): evt is React.MouseEvent<HTMLElement> | MouseEvent {
|
||||||
return (
|
return (
|
||||||
evt.type === "mousedown" ||
|
evt.type === "mousedown" ||
|
||||||
@ -29,7 +29,7 @@ const getEventX = (evt: ActivityEvent) => {
|
|||||||
export function useProgressBar(
|
export function useProgressBar(
|
||||||
barRef: RefObject<HTMLElement>,
|
barRef: RefObject<HTMLElement>,
|
||||||
commit: (percentage: number) => void,
|
commit: (percentage: number) => void,
|
||||||
commitImmediately = false
|
commitImmediately = false,
|
||||||
) {
|
) {
|
||||||
const [mouseDown, setMouseDown] = useState<boolean>(false);
|
const [mouseDown, setMouseDown] = useState<boolean>(false);
|
||||||
const [progress, setProgress] = useState<number>(0);
|
const [progress, setProgress] = useState<number>(0);
|
||||||
@ -78,7 +78,7 @@ export function useProgressBar(
|
|||||||
((getEventX(ev) - rect.left) / barRef.current.offsetWidth) * 100;
|
((getEventX(ev) - rect.left) / barRef.current.offsetWidth) * 100;
|
||||||
setProgress(pos);
|
setProgress(pos);
|
||||||
},
|
},
|
||||||
[setProgress, barRef]
|
[setProgress, barRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -54,7 +54,7 @@ function useBaseScrape() {
|
|||||||
.reduce<Record<string, ScrapingSegment>>((a, v) => {
|
.reduce<Record<string, ScrapingSegment>>((a, v) => {
|
||||||
a[v.id] = v;
|
a[v.id] = v;
|
||||||
return a;
|
return a;
|
||||||
}, {})
|
}, {}),
|
||||||
);
|
);
|
||||||
setSourceOrder(evt.sourceIds.map((v) => ({ id: v, children: [] })));
|
setSourceOrder(evt.sourceIds.map((v) => ({ id: v, children: [] })));
|
||||||
}, []);
|
}, []);
|
||||||
@ -85,7 +85,7 @@ function useBaseScrape() {
|
|||||||
setSources((s) => {
|
setSources((s) => {
|
||||||
evt.embeds.forEach((v) => {
|
evt.embeds.forEach((v) => {
|
||||||
const source = getCachedMetadata().find(
|
const source = getCachedMetadata().find(
|
||||||
(src) => src.id === v.embedScraperId
|
(src) => src.id === v.embedScraperId,
|
||||||
);
|
);
|
||||||
if (!source) throw new Error("invalid source id");
|
if (!source) throw new Error("invalid source id");
|
||||||
const out: ScrapingSegment = {
|
const out: ScrapingSegment = {
|
||||||
@ -106,7 +106,7 @@ function useBaseScrape() {
|
|||||||
return [...s];
|
return [...s];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const startScrape = useCallback(() => {
|
const startScrape = useCallback(() => {
|
||||||
@ -158,7 +158,7 @@ export function useScrape() {
|
|||||||
const baseUrlMaker = makeProviderUrl(providerApiUrl);
|
const baseUrlMaker = makeProviderUrl(providerApiUrl);
|
||||||
const conn = await connectServerSideEvents<RunOutput | "">(
|
const conn = await connectServerSideEvents<RunOutput | "">(
|
||||||
baseUrlMaker.scrapeAll(media),
|
baseUrlMaker.scrapeAll(media),
|
||||||
["completed", "noOutput"]
|
["completed", "noOutput"],
|
||||||
);
|
);
|
||||||
conn.on("init", initEvent);
|
conn.on("init", initEvent);
|
||||||
conn.on("start", startEvent);
|
conn.on("start", startEvent);
|
||||||
@ -189,7 +189,7 @@ export function useScrape() {
|
|||||||
discoverEmbedsEvent,
|
discoverEmbedsEvent,
|
||||||
getResult,
|
getResult,
|
||||||
startScrape,
|
startScrape,
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -204,7 +204,7 @@ export function useListCenter(
|
|||||||
containerRef: RefObject<HTMLDivElement | null>,
|
containerRef: RefObject<HTMLDivElement | null>,
|
||||||
listRef: RefObject<HTMLDivElement | null>,
|
listRef: RefObject<HTMLDivElement | null>,
|
||||||
sourceOrder: ScrapingItems[],
|
sourceOrder: ScrapingItems[],
|
||||||
currentSource: string | undefined
|
currentSource: string | undefined,
|
||||||
) {
|
) {
|
||||||
const [renderedOnce, setRenderedOnce] = useState(false);
|
const [renderedOnce, setRenderedOnce] = useState(false);
|
||||||
|
|
||||||
@ -217,7 +217,7 @@ export function useListCenter(
|
|||||||
] as HTMLDivElement[];
|
] as HTMLDivElement[];
|
||||||
|
|
||||||
const currentIndex = elements.findIndex(
|
const currentIndex = elements.findIndex(
|
||||||
(e) => e.getAttribute("data-source-id") === currentSource
|
(e) => e.getAttribute("data-source-id") === currentSource,
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentElement = elements[currentIndex];
|
const currentElement = elements[currentIndex];
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function useQueryParams() {
|
export function useQueryParams() {
|
||||||
const loc = useLocation();
|
const loc = useLocation();
|
||||||
|
|
||||||
const queryParams = useMemo(() => {
|
const queryParams = useMemo(() => {
|
||||||
const obj: Record<string, string> = Object.fromEntries(
|
const obj: Record<string, string> = Object.fromEntries(
|
||||||
new URLSearchParams(loc.search).entries()
|
new URLSearchParams(loc.search).entries(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
@ -16,11 +16,11 @@ export function useQueryParams() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useQueryParam(
|
export function useQueryParam(
|
||||||
param: string
|
param: string,
|
||||||
): [string | null, (a: string | null) => void] {
|
): [string | null, (a: string | null) => void] {
|
||||||
const params = useQueryParams();
|
const params = useQueryParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const router = useHistory();
|
const navigate = useNavigate();
|
||||||
const currentValue = params[param] ?? null;
|
const currentValue = params[param] ?? null;
|
||||||
|
|
||||||
const set = useCallback(
|
const set = useCallback(
|
||||||
@ -28,11 +28,11 @@ export function useQueryParam(
|
|||||||
const parsed = new URLSearchParams(location.search);
|
const parsed = new URLSearchParams(location.search);
|
||||||
if (value) parsed.set(param, value);
|
if (value) parsed.set(param, value);
|
||||||
else parsed.delete(param);
|
else parsed.delete(param);
|
||||||
router.push({
|
navigate({
|
||||||
search: parsed.toString(),
|
search: parsed.toString(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[param, location.search, router]
|
[param, location.search, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [currentValue, set];
|
return [currentValue, set];
|
||||||
|
@ -22,7 +22,7 @@ export function useRandomTranslation() {
|
|||||||
|
|
||||||
return typeof keys === "string" ? keys : defaultTitle;
|
return typeof keys === "string" ? keys : defaultTitle;
|
||||||
},
|
},
|
||||||
[t, seed, shouldJoke]
|
[t, seed, shouldJoke],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { t: getRandomTranslation };
|
return { t: getRandomTranslation };
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { generatePath, useHistory, useParams } from "react-router-dom";
|
import { generatePath, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
function decode(query: string | null | undefined) {
|
function decode(query: string | null | undefined) {
|
||||||
return query ? decodeURIComponent(query) : "";
|
return query ? decodeURIComponent(query) : "";
|
||||||
@ -8,9 +8,9 @@ function decode(query: string | null | undefined) {
|
|||||||
export function useSearchQuery(): [
|
export function useSearchQuery(): [
|
||||||
string,
|
string,
|
||||||
(inp: string, force?: boolean) => void,
|
(inp: string, force?: boolean) => void,
|
||||||
() => void
|
() => void,
|
||||||
] {
|
] {
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
const params = useParams<{ query: string }>();
|
const params = useParams<{ query: string }>();
|
||||||
const [search, setSearch] = useState(decode(params.query));
|
const [search, setSearch] = useState(decode(params.query));
|
||||||
|
|
||||||
@ -22,13 +22,14 @@ export function useSearchQuery(): [
|
|||||||
setSearch(inp);
|
setSearch(inp);
|
||||||
if (!commitToUrl) return;
|
if (!commitToUrl) return;
|
||||||
if (inp.length === 0) {
|
if (inp.length === 0) {
|
||||||
history.replace("/");
|
navigate("/", { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
history.replace(
|
navigate(
|
||||||
generatePath("/browse/:query", {
|
generatePath("/browse/:query", {
|
||||||
query: inp,
|
query: inp,
|
||||||
})
|
}),
|
||||||
|
{ replace: true },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
import { SubtitleStyling } from "@/stores/subtitles";
|
import { SubtitleStyling } from "@/stores/subtitles";
|
||||||
|
|
||||||
export function useDerived<T>(
|
export function useDerived<T>(
|
||||||
initial: T
|
initial: T,
|
||||||
): [T, Dispatch<SetStateAction<T>>, () => void, boolean] {
|
): [T, Dispatch<SetStateAction<T>>, () => void, boolean] {
|
||||||
const [overwrite, setOverwrite] = useState<T | undefined>(undefined);
|
const [overwrite, setOverwrite] = useState<T | undefined>(undefined);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -19,14 +19,14 @@ export function useDerived<T>(
|
|||||||
}, [initial]);
|
}, [initial]);
|
||||||
const changed = useMemo(
|
const changed = useMemo(
|
||||||
() => !isEqual(overwrite, initial) && overwrite !== undefined,
|
() => !isEqual(overwrite, initial) && overwrite !== undefined,
|
||||||
[overwrite, initial]
|
[overwrite, initial],
|
||||||
);
|
);
|
||||||
const setter = useCallback<Dispatch<SetStateAction<T>>>(
|
const setter = useCallback<Dispatch<SetStateAction<T>>>(
|
||||||
(inp) => {
|
(inp) => {
|
||||||
if (!(inp instanceof Function)) setOverwrite(inp);
|
if (!(inp instanceof Function)) setOverwrite(inp);
|
||||||
else setOverwrite((s) => inp(s !== undefined ? s : initial));
|
else setOverwrite((s) => inp(s !== undefined ? s : initial));
|
||||||
},
|
},
|
||||||
[initial, setOverwrite]
|
[initial, setOverwrite],
|
||||||
);
|
);
|
||||||
const data = overwrite === undefined ? initial : overwrite;
|
const data = overwrite === undefined ? initial : overwrite;
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ export function useSettingsState(
|
|||||||
colorB: string;
|
colorB: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
| undefined
|
| undefined,
|
||||||
) {
|
) {
|
||||||
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
||||||
useDerived(proxyUrls);
|
useDerived(proxyUrls);
|
||||||
|
@ -6,7 +6,7 @@ import "@/assets/css/index.css";
|
|||||||
|
|
||||||
import React, { Suspense, useCallback } from "react";
|
import React, { Suspense, useCallback } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import { createRoot } from "react-dom/client";
|
||||||
import { HelmetProvider } from "react-helmet-async";
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { BrowserRouter, HashRouter } from "react-router-dom";
|
import { BrowserRouter, HashRouter } from "react-router-dom";
|
||||||
@ -114,7 +114,7 @@ function AuthWrapper() {
|
|||||||
{t(
|
{t(
|
||||||
isCustomUrl
|
isCustomUrl
|
||||||
? "screens.loadingUserError.textWithReset"
|
? "screens.loadingUserError.textWithReset"
|
||||||
: "screens.loadingUserError.text"
|
: "screens.loadingUserError.text",
|
||||||
)}
|
)}
|
||||||
</ErrorScreen>
|
</ErrorScreen>
|
||||||
);
|
);
|
||||||
@ -141,23 +141,23 @@ function TheRouter(props: { children: ReactNode }) {
|
|||||||
return <HashRouter>{props.children}</HashRouter>;
|
return <HashRouter>{props.children}</HashRouter>;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
const container = document.getElementById("root");
|
||||||
<React.StrictMode>
|
const root = createRoot(container!);
|
||||||
<ErrorBoundary>
|
|
||||||
<TurnstileProvider />
|
root.render(
|
||||||
<HelmetProvider>
|
<ErrorBoundary>
|
||||||
<Suspense fallback={<LoadingScreen type="lazy" />}>
|
<TurnstileProvider />
|
||||||
<ThemeProvider applyGlobal>
|
<HelmetProvider>
|
||||||
<ProgressSyncer />
|
<Suspense fallback={<LoadingScreen type="lazy" />}>
|
||||||
<BookmarkSyncer />
|
<ThemeProvider applyGlobal>
|
||||||
<SettingsSyncer />
|
<ProgressSyncer />
|
||||||
<TheRouter>
|
<BookmarkSyncer />
|
||||||
<MigrationRunner />
|
<SettingsSyncer />
|
||||||
</TheRouter>
|
<TheRouter>
|
||||||
</ThemeProvider>
|
<MigrationRunner />
|
||||||
</Suspense>
|
</TheRouter>
|
||||||
</HelmetProvider>
|
</ThemeProvider>
|
||||||
</ErrorBoundary>
|
</Suspense>
|
||||||
</React.StrictMode>,
|
</HelmetProvider>
|
||||||
document.getElementById("root")
|
</ErrorBoundary>,
|
||||||
);
|
);
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { useHistory } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
||||||
import { LoginFormPart } from "@/pages/parts/auth/LoginFormPart";
|
import { LoginFormPart } from "@/pages/parts/auth/LoginFormPart";
|
||||||
import { PageTitle } from "@/pages/parts/util/PageTitle";
|
import { PageTitle } from "@/pages/parts/util/PageTitle";
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubPageLayout>
|
<SubPageLayout>
|
||||||
<PageTitle subpage k="global.pages.login" />
|
<PageTitle subpage k="global.pages.login" />
|
||||||
<LoginFormPart
|
<LoginFormPart
|
||||||
onLogin={() => {
|
onLogin={() => {
|
||||||
history.push("/");
|
navigate("/");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SubPageLayout>
|
</SubPageLayout>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { RunOutput } from "@movie-web/providers";
|
import { RunOutput } from "@movie-web/providers";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||||
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
||||||
@ -18,7 +18,7 @@ import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
|||||||
import { parseTimestamp } from "@/utils/timestamp";
|
import { parseTimestamp } from "@/utils/timestamp";
|
||||||
|
|
||||||
export function PlayerView() {
|
export function PlayerView() {
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
const params = useParams<{
|
const params = useParams<{
|
||||||
media: string;
|
media: string;
|
||||||
episode?: string;
|
episode?: string;
|
||||||
@ -52,12 +52,12 @@ export function PlayerView() {
|
|||||||
const metaChange = useCallback(
|
const metaChange = useCallback(
|
||||||
(meta: PlayerMeta) => {
|
(meta: PlayerMeta) => {
|
||||||
if (meta?.type === "show")
|
if (meta?.type === "show")
|
||||||
history.push(
|
navigate(
|
||||||
`/media/${params.media}/${meta.season?.tmdbId}/${meta.episode?.tmdbId}`
|
`/media/${params.media}/${meta.season?.tmdbId}/${meta.episode?.tmdbId}`,
|
||||||
);
|
);
|
||||||
else history.push(`/media/${params.media}`);
|
else navigate(`/media/${params.media}`);
|
||||||
},
|
},
|
||||||
[history, params]
|
[navigate, params],
|
||||||
);
|
);
|
||||||
|
|
||||||
const playAfterScrape = useCallback(
|
const playAfterScrape = useCallback(
|
||||||
@ -71,7 +71,7 @@ export function PlayerView() {
|
|||||||
convertRunoutputToSource(out),
|
convertRunoutputToSource(out),
|
||||||
convertProviderCaption(out.stream.captions),
|
convertProviderCaption(out.stream.captions),
|
||||||
out.sourceId,
|
out.sourceId,
|
||||||
shouldStartFromBeginning ? 0 : startAt
|
shouldStartFromBeginning ? 0 : startAt,
|
||||||
);
|
);
|
||||||
setShouldStartFromBeginning(false);
|
setShouldStartFromBeginning(false);
|
||||||
},
|
},
|
||||||
@ -80,7 +80,7 @@ export function PlayerView() {
|
|||||||
startAtParam,
|
startAtParam,
|
||||||
shouldStartFromBeginning,
|
shouldStartFromBeginning,
|
||||||
setShouldStartFromBeginning,
|
setShouldStartFromBeginning,
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -108,3 +108,5 @@ export function PlayerView() {
|
|||||||
</PlayerPart>
|
</PlayerPart>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default PlayerView;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
|
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { MetaResponse } from "@/backend/accounts/meta";
|
import { MetaResponse } from "@/backend/accounts/meta";
|
||||||
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
||||||
@ -26,7 +26,7 @@ function CaptchaProvider(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RegisterPage() {
|
export function RegisterPage() {
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
const [mnemonic, setMnemonic] = useState<null | string>(null);
|
const [mnemonic, setMnemonic] = useState<null | string>(null);
|
||||||
const [account, setAccount] = useState<null | AccountProfile>(null);
|
const [account, setAccount] = useState<null | AccountProfile>(null);
|
||||||
@ -42,7 +42,7 @@ export function RegisterPage() {
|
|||||||
setSiteKey(
|
setSiteKey(
|
||||||
meta.hasCaptcha && meta.captchaClientKey
|
meta.hasCaptcha && meta.captchaClientKey
|
||||||
? meta.captchaClientKey
|
? meta.captchaClientKey
|
||||||
: null
|
: null,
|
||||||
);
|
);
|
||||||
setStep(1);
|
setStep(1);
|
||||||
}}
|
}}
|
||||||
@ -70,7 +70,7 @@ export function RegisterPage() {
|
|||||||
mnemonic={mnemonic}
|
mnemonic={mnemonic}
|
||||||
userData={account}
|
userData={account}
|
||||||
onNext={() => {
|
onNext={() => {
|
||||||
history.push("/");
|
navigate("/");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -45,7 +45,7 @@ function SettingsLayout(props: { children: React.ReactNode }) {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"grid gap-12",
|
"grid gap-12",
|
||||||
isMobile ? "grid-cols-1" : "lg:grid-cols-[280px,1fr]"
|
isMobile ? "grid-cols-1" : "lg:grid-cols-[280px,1fr]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SidebarPart />
|
<SidebarPart />
|
||||||
@ -135,7 +135,7 @@ export function SettingsPage() {
|
|||||||
decryptedName,
|
decryptedName,
|
||||||
proxySet,
|
proxySet,
|
||||||
backendUrlSetting,
|
backendUrlSetting,
|
||||||
account?.profile
|
account?.profile,
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveChanges = useCallback(async () => {
|
const saveChanges = useCallback(async () => {
|
||||||
@ -149,7 +149,7 @@ export function SettingsPage() {
|
|||||||
if (state.deviceName.changed) {
|
if (state.deviceName.changed) {
|
||||||
const newDeviceName = await encryptData(
|
const newDeviceName = await encryptData(
|
||||||
state.deviceName.state,
|
state.deviceName.state,
|
||||||
base64ToBuffer(account.seed)
|
base64ToBuffer(account.seed),
|
||||||
);
|
);
|
||||||
await updateSession(backendUrl, account, {
|
await updateSession(backendUrl, account, {
|
||||||
deviceName: newDeviceName,
|
deviceName: newDeviceName,
|
||||||
@ -270,3 +270,5 @@ export function SettingsPage() {
|
|||||||
</SubPageLayout>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default SettingsPage;
|
||||||
|
@ -54,7 +54,7 @@ export default function VideoTesterView() {
|
|||||||
setMeta(testMeta);
|
setMeta(testMeta);
|
||||||
playMedia(source, [], null);
|
playMedia(source, [], null);
|
||||||
},
|
},
|
||||||
[playMedia, setMeta]
|
[playMedia, setMeta],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -9,7 +9,7 @@ export function ErrorContainer(props: {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full p-6 text-center flex flex-col items-center",
|
"w-full p-6 text-center flex flex-col items-center",
|
||||||
props.maxWidth ?? "max-w-[28rem]"
|
props.maxWidth ?? "max-w-[28rem]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -10,13 +10,13 @@ export function BlurEllipsis(props: { positionClass?: string }) {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
props.positionClass ?? "fixed",
|
props.positionClass ?? "fixed",
|
||||||
"top-0 -right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentA blur-[100px] pointer-events-none opacity-25"
|
"top-0 -right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentA blur-[100px] pointer-events-none opacity-25",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
props.positionClass ?? "fixed",
|
props.positionClass ?? "fixed",
|
||||||
"top-0 right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentB blur-[100px] pointer-events-none opacity-25"
|
"top-0 right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentB blur-[100px] pointer-events-none opacity-25",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user