mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 20:11:52 +01:00
commit
6a9eb11884
@ -17,7 +17,7 @@
|
|||||||
"login": {
|
"login": {
|
||||||
"title": "Login to your account",
|
"title": "Login to your account",
|
||||||
"description": "Please enter your passphrase to login to your account",
|
"description": "Please enter your passphrase to login to your account",
|
||||||
"validationError": "Invalid or incomplete passphrase",
|
"validationError": "Incorrect or incomplete passphrase",
|
||||||
"deviceLengthError": "Please enter a device name",
|
"deviceLengthError": "Please enter a device name",
|
||||||
"submit": "Login",
|
"submit": "Login",
|
||||||
"passphraseLabel": "12-Word passphrase",
|
"passphraseLabel": "12-Word passphrase",
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"passphrasePlaceholder": "Passphrase",
|
"passphrasePlaceholder": "Passphrase",
|
||||||
"submit": "Hoist Anchor",
|
"submit": "Hoist Anchor",
|
||||||
"title": "Hoist the Jolly Roger",
|
"title": "Hoist the Jolly Roger",
|
||||||
"validationError": "Arr, invalid or incomplete passphrase"
|
"validationError": "Arr, incorrect or incomplete passphrase"
|
||||||
},
|
},
|
||||||
"register": {
|
"register": {
|
||||||
"information": {
|
"information": {
|
||||||
|
@ -95,7 +95,7 @@ function MediaCardContent({
|
|||||||
{percentage !== undefined ? (
|
{percentage !== undefined ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
|
className={`absolute inset-x-0 -bottom-px pb-1 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
|
||||||
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
|
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
@ -16,7 +16,7 @@ export function OverlayMobilePosition(props: MobilePositionProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames([
|
className={classNames([
|
||||||
"pointer-events-auto px-4 pb-6 z-10 bottom-0 origin-top-left inset-x-0 absolute overflow-hidden max-h-[calc(100vh-1.5rem)] grid grid-rows-[minmax(0,1fr),auto]",
|
"pointer-events-auto px-4 pb-6 z-10 ml-[env(safe-area-inset-left)] mr-[env(safe-area-inset-right)] bottom-0 origin-top-left inset-x-0 absolute overflow-hidden max-h-[calc(100vh-1.5rem)] grid grid-rows-[minmax(0,1fr),auto]",
|
||||||
props.className,
|
props.className,
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
|
@ -62,9 +62,11 @@ export function NextEpisodeButton(props: {
|
|||||||
if (isHidden || status !== "playing" || duration === 0) show = false;
|
if (isHidden || status !== "playing" || duration === 0) show = false;
|
||||||
|
|
||||||
const animation = showingState === "hover" ? "slide-up" : "fade";
|
const animation = showingState === "hover" ? "slide-up" : "fade";
|
||||||
let bottom = "bottom-24";
|
let bottom = "bottom-[calc(6rem+env(safe-area-inset-bottom))]";
|
||||||
if (showingState === "always")
|
if (showingState === "always")
|
||||||
bottom = props.controlsShowing ? "bottom-24" : "bottom-12";
|
bottom = props.controlsShowing
|
||||||
|
? 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
|
||||||
@ -86,7 +88,7 @@ export function NextEpisodeButton(props: {
|
|||||||
<Transition
|
<Transition
|
||||||
animation={animation}
|
animation={animation}
|
||||||
show={show}
|
show={show}
|
||||||
className="absolute right-12 bottom-0"
|
className="absolute right-[calc(3rem+env(safe-area-inset-right))] bottom-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames([
|
className={classNames([
|
||||||
|
@ -5,6 +5,7 @@ import { playerStatus } from "@/stores/player/slices/source";
|
|||||||
import { ThumbnailImage } from "@/stores/player/slices/thumbnails";
|
import { ThumbnailImage } from "@/stores/player/slices/thumbnails";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { LoadableSource, selectQuality } from "@/stores/player/utils/qualities";
|
import { LoadableSource, selectQuality } from "@/stores/player/utils/qualities";
|
||||||
|
import { isSafari } from "@/utils/detectFeatures";
|
||||||
|
|
||||||
function makeQueue(layers: number): number[] {
|
function makeQueue(layers: number): number[] {
|
||||||
const output = [0, 1];
|
const output = [0, 1];
|
||||||
@ -39,7 +40,9 @@ class ThumnbnailWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start(source: LoadableSource) {
|
start(source: LoadableSource) {
|
||||||
|
if (isSafari) return false;
|
||||||
const el = document.createElement("video");
|
const el = document.createElement("video");
|
||||||
|
el.setAttribute("muted", "true");
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
this.hls = new Hls();
|
this.hls = new Hls();
|
||||||
if (source.type === "mp4") {
|
if (source.type === "mp4") {
|
||||||
@ -76,9 +79,14 @@ class ThumnbnailWorker {
|
|||||||
|
|
||||||
private async takeSnapshot(at: number) {
|
private async takeSnapshot(at: number) {
|
||||||
if (!this.videoEl || !this.canvasEl) return;
|
if (!this.videoEl || !this.canvasEl) return;
|
||||||
|
await this.videoEl.play(); // so that `seeked` actually triggers
|
||||||
this.videoEl.currentTime = at;
|
this.videoEl.currentTime = at;
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
this.videoEl?.addEventListener("seeked", resolve);
|
const onSeeked = () => {
|
||||||
|
this.videoEl?.removeEventListener("seeked", onSeeked);
|
||||||
|
resolve(null);
|
||||||
|
};
|
||||||
|
this.videoEl?.addEventListener("seeked", onSeeked);
|
||||||
});
|
});
|
||||||
if (!this.videoEl || !this.canvasEl) return;
|
if (!this.videoEl || !this.canvasEl) return;
|
||||||
const ctx = this.canvasEl.getContext("2d");
|
const ctx = this.canvasEl.getContext("2d");
|
||||||
@ -91,6 +99,7 @@ class ThumnbnailWorker {
|
|||||||
this.canvasEl.height
|
this.canvasEl.height
|
||||||
);
|
);
|
||||||
const imgUrl = this.canvasEl.toDataURL();
|
const imgUrl = this.canvasEl.toDataURL();
|
||||||
|
|
||||||
if (this.interrupted) return;
|
if (this.interrupted) return;
|
||||||
if (imgUrl === "data:," || !imgUrl) return; // failed image rendering
|
if (imgUrl === "data:," || !imgUrl) return; // failed image rendering
|
||||||
|
|
||||||
@ -142,6 +151,7 @@ export function ThumbnailScraper() {
|
|||||||
workerRef.current = ins;
|
workerRef.current = ins;
|
||||||
ins.start(inputStream.stream);
|
ins.start(inputStream.stream);
|
||||||
}, [source, addImage, resetImages, status]);
|
}, [source, addImage, resetImages, status]);
|
||||||
|
|
||||||
const startRef = useRef(start);
|
const startRef = useRef(start);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
startRef.current = start;
|
startRef.current = start;
|
||||||
|
@ -161,8 +161,8 @@ function ParticlesCanvas() {
|
|||||||
|
|
||||||
export function Lightbar(props: { className?: string }) {
|
export function Lightbar(props: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 w-full h-screen overflow-hidden pointer-events-none -mt-64">
|
<div className="absolute inset-0 w-full h-[calc(100vh+16rem)] overflow-hidden pointer-events-none -mt-64">
|
||||||
<div className="max-w-screen w-full h-screen relative pt-64">
|
<div className="max-w-screen w-full h-[calc(100vh+16rem)] relative pt-64">
|
||||||
<div className={props.className}>
|
<div className={props.className}>
|
||||||
<div className="lightbar">
|
<div className="lightbar">
|
||||||
<ParticlesCanvas />
|
<ParticlesCanvas />
|
||||||
|
@ -19,7 +19,7 @@ const testMeta: PlayerMeta = {
|
|||||||
|
|
||||||
const testStreams: Record<StreamType, string> = {
|
const testStreams: Record<StreamType, string> = {
|
||||||
hls: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8",
|
hls: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8",
|
||||||
mp4: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
|
mp4: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
|
||||||
};
|
};
|
||||||
|
|
||||||
const streamTypes: Record<StreamType, string> = {
|
const streamTypes: Record<StreamType, string> = {
|
||||||
|
@ -26,7 +26,7 @@ export function BlurEllipsis(props: { positionClass?: string }) {
|
|||||||
export function SubPageLayout(props: { children: React.ReactNode }) {
|
export function SubPageLayout(props: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="from-[#0D0D1A] to-background-main"
|
className="bg-background-main"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
"linear-gradient(to bottom, var(--tw-gradient-from), var(--tw-gradient-to) 800px)",
|
"linear-gradient(to bottom, var(--tw-gradient-from), var(--tw-gradient-to) 800px)",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
import type { AsyncReturnType } from "type-fest";
|
||||||
|
|
||||||
import { verifyValidMnemonic } from "@/backend/accounts/crypto";
|
import { verifyValidMnemonic } from "@/backend/accounts/crypto";
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
@ -37,12 +38,19 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
|||||||
if (validatedDevice.length === 0)
|
if (validatedDevice.length === 0)
|
||||||
throw new Error(t("auth.login.deviceLengthError") ?? undefined);
|
throw new Error(t("auth.login.deviceLengthError") ?? undefined);
|
||||||
|
|
||||||
const account = await login({
|
let account: AsyncReturnType<typeof login>;
|
||||||
mnemonic: inputMnemonic,
|
try {
|
||||||
userData: {
|
account = await login({
|
||||||
device: validatedDevice,
|
mnemonic: inputMnemonic,
|
||||||
},
|
userData: {
|
||||||
});
|
device: validatedDevice,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as any).status === 401)
|
||||||
|
throw new Error(t("auth.login.validationError") ?? undefined);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
await importData(account, progressItems, bookmarkItems);
|
await importData(account, progressItems, bookmarkItems);
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ProviderControls, ScrapeMedia } from "@movie-web/providers";
|
import { ProviderControls, ScrapeMedia } from "@movie-web/providers";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useMountedState } from "react-use";
|
||||||
import type { AsyncReturnType } from "type-fest";
|
import type { AsyncReturnType } from "type-fest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -30,6 +31,7 @@ export interface ScrapingProps {
|
|||||||
export function ScrapingPart(props: ScrapingProps) {
|
export function ScrapingPart(props: ScrapingProps) {
|
||||||
const { report } = useReportProviders();
|
const { report } = useReportProviders();
|
||||||
const { startScraping, sourceOrder, sources, currentSource } = useScrape();
|
const { startScraping, sourceOrder, sources, currentSource } = useScrape();
|
||||||
|
const isMounted = useMountedState();
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const listRef = useRef<HTMLDivElement | null>(null);
|
const listRef = useRef<HTMLDivElement | null>(null);
|
||||||
@ -57,6 +59,7 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
started.current = true;
|
started.current = true;
|
||||||
(async () => {
|
(async () => {
|
||||||
const output = await startScraping(props.media);
|
const output = await startScraping(props.media);
|
||||||
|
if (!isMounted()) return;
|
||||||
props.onResult?.(
|
props.onResult?.(
|
||||||
resultRef.current.sources,
|
resultRef.current.sources,
|
||||||
resultRef.current.sourceOrder
|
resultRef.current.sourceOrder
|
||||||
@ -70,7 +73,7 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
);
|
);
|
||||||
props.onGetStream?.(output);
|
props.onGetStream?.(output);
|
||||||
})();
|
})();
|
||||||
}, [startScraping, props, report]);
|
}, [startScraping, props, report, isMounted]);
|
||||||
|
|
||||||
const currentProvider = sourceOrder.find(
|
const currentProvider = sourceOrder.find(
|
||||||
(s) => sources[s.id].status === "pending"
|
(s) => sources[s.id].status === "pending"
|
||||||
|
Loading…
Reference in New Issue
Block a user