From 056fabb266888615ec8c49e9bc7e08e19d9ced64 Mon Sep 17 00:00:00 2001
From: Jip Fr <jipfrijlink@gmail.com>
Date: Sun, 10 Dec 2023 20:58:56 +0100
Subject: [PATCH 1/9] Add safe area to mobile overlays

Co-authored-by: mrjvs <mistrjvs@gmail.com>
---
 src/components/overlays/positions/OverlayMobilePosition.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/overlays/positions/OverlayMobilePosition.tsx b/src/components/overlays/positions/OverlayMobilePosition.tsx
index bebf3f03..53b21000 100644
--- a/src/components/overlays/positions/OverlayMobilePosition.tsx
+++ b/src/components/overlays/positions/OverlayMobilePosition.tsx
@@ -16,7 +16,7 @@ export function OverlayMobilePosition(props: MobilePositionProps) {
   return (
     <div
       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,
       ])}
     >

From 95c5e7399659dd35538e63c7abdc424e87c6c25f Mon Sep 17 00:00:00 2001
From: Jip Fr <jipfrijlink@gmail.com>
Date: Sun, 10 Dec 2023 22:10:37 +0100
Subject: [PATCH 2/9] Better error for unknown account

Co-authored-by: mrjvs <mistrjvs@gmail.com>
---
 src/assets/locales/en.json             |  2 +-
 src/assets/locales/pirate.json         |  2 +-
 src/pages/parts/auth/LoginFormPart.tsx | 20 ++++++++++++++------
 3 files changed, 16 insertions(+), 8 deletions(-)

diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json
index 71fb1d78..90d6e56b 100644
--- a/src/assets/locales/en.json
+++ b/src/assets/locales/en.json
@@ -17,7 +17,7 @@
     "login": {
       "title": "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",
       "submit": "Login",
       "passphraseLabel": "12-Word passphrase",
diff --git a/src/assets/locales/pirate.json b/src/assets/locales/pirate.json
index b0048e82..0abcc5d5 100644
--- a/src/assets/locales/pirate.json
+++ b/src/assets/locales/pirate.json
@@ -16,7 +16,7 @@
       "passphrasePlaceholder": "Passphrase",
       "submit": "Hoist Anchor",
       "title": "Hoist the Jolly Roger",
-      "validationError": "Arr, invalid or incomplete passphrase"
+      "validationError": "Arr, incorrect or incomplete passphrase"
     },
     "register": {
       "information": {
diff --git a/src/pages/parts/auth/LoginFormPart.tsx b/src/pages/parts/auth/LoginFormPart.tsx
index d87f6e14..1ff43422 100644
--- a/src/pages/parts/auth/LoginFormPart.tsx
+++ b/src/pages/parts/auth/LoginFormPart.tsx
@@ -1,6 +1,7 @@
 import { useState } from "react";
 import { Trans, useTranslation } from "react-i18next";
 import { useAsyncFn } from "react-use";
+import type { AsyncReturnType } from "type-fest";
 
 import { verifyValidMnemonic } from "@/backend/accounts/crypto";
 import { Button } from "@/components/buttons/Button";
@@ -37,12 +38,19 @@ export function LoginFormPart(props: LoginFormPartProps) {
       if (validatedDevice.length === 0)
         throw new Error(t("auth.login.deviceLengthError") ?? undefined);
 
-      const account = await login({
-        mnemonic: inputMnemonic,
-        userData: {
-          device: validatedDevice,
-        },
-      });
+      let account: AsyncReturnType<typeof login>;
+      try {
+        account = await login({
+          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);
 

From ca0ccb240e933eb1c3bc9476d418aaab6828fcac Mon Sep 17 00:00:00 2001
From: Jip Fr <jipfrijlink@gmail.com>
Date: Sun, 10 Dec 2023 22:10:57 +0100
Subject: [PATCH 3/9] Abortable scraping

Co-authored-by: mrjvs <mistrjvs@gmail.com>
---
 src/pages/parts/player/ScrapingPart.tsx | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/pages/parts/player/ScrapingPart.tsx b/src/pages/parts/player/ScrapingPart.tsx
index 54a7afbd..eee6edcb 100644
--- a/src/pages/parts/player/ScrapingPart.tsx
+++ b/src/pages/parts/player/ScrapingPart.tsx
@@ -1,6 +1,7 @@
 import { ProviderControls, ScrapeMedia } from "@movie-web/providers";
 import classNames from "classnames";
 import { useEffect, useRef } from "react";
+import { useMountedState } from "react-use";
 import type { AsyncReturnType } from "type-fest";
 
 import {
@@ -30,6 +31,7 @@ export interface ScrapingProps {
 export function ScrapingPart(props: ScrapingProps) {
   const { report } = useReportProviders();
   const { startScraping, sourceOrder, sources, currentSource } = useScrape();
+  const isMounted = useMountedState();
 
   const containerRef = useRef<HTMLDivElement | null>(null);
   const listRef = useRef<HTMLDivElement | null>(null);
@@ -57,6 +59,7 @@ export function ScrapingPart(props: ScrapingProps) {
     started.current = true;
     (async () => {
       const output = await startScraping(props.media);
+      if (!isMounted()) return;
       props.onResult?.(
         resultRef.current.sources,
         resultRef.current.sourceOrder
@@ -70,7 +73,7 @@ export function ScrapingPart(props: ScrapingProps) {
       );
       props.onGetStream?.(output);
     })();
-  }, [startScraping, props, report]);
+  }, [startScraping, props, report, isMounted]);
 
   const currentProvider = sourceOrder.find(
     (s) => sources[s.id].status === "pending"

From c4dcc42b9ddc12ccc9cd88c99408ce73c00c39f2 Mon Sep 17 00:00:00 2001
From: Jip Fr <jipfrijlink@gmail.com>
Date: Sun, 10 Dec 2023 22:11:54 +0100
Subject: [PATCH 4/9] Fix weird underflow on media card on Safari

---
 src/components/media/MediaCard.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx
index a98770f0..32708285 100644
--- a/src/components/media/MediaCard.tsx
+++ b/src/components/media/MediaCard.tsx
@@ -95,7 +95,7 @@ function MediaCardContent({
           {percentage !== undefined ? (
             <>
               <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" : ""
                 }`}
               />

From 3109da2154cff8f358e21ecfefce4e8376a1b344 Mon Sep 17 00:00:00 2001
From: Jip Fr <jipfrijlink@gmail.com>
Date: Sun, 10 Dec 2023 22:12:09 +0100
Subject: [PATCH 5/9] Some progress for thumbnail fix on iOS

---
 .../player/internals/ThumbnailScraper.tsx     | 23 ++++++++++++++++++-
 src/pages/developer/VideoTesterView.tsx       |  2 +-
 2 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/src/components/player/internals/ThumbnailScraper.tsx b/src/components/player/internals/ThumbnailScraper.tsx
index 4be50eac..82202f07 100644
--- a/src/components/player/internals/ThumbnailScraper.tsx
+++ b/src/components/player/internals/ThumbnailScraper.tsx
@@ -40,6 +40,10 @@ class ThumnbnailWorker {
 
   start(source: LoadableSource) {
     const el = document.createElement("video");
+    el.setAttribute("muted", "true");
+    // ! Remove before merging
+    el.setAttribute("controls", "true");
+    el.setAttribute("playsinline", "true");
     const canvas = document.createElement("canvas");
     this.hls = new Hls();
     if (source.type === "mp4") {
@@ -65,6 +69,8 @@ class ThumnbnailWorker {
 
   private async initVideo() {
     if (!this.videoEl || !this.canvasEl) return;
+    // ! Remove before merging
+    document.body.appendChild(this.videoEl);
     await new Promise((resolve, reject) => {
       this.videoEl?.addEventListener("loadedmetadata", resolve);
       this.videoEl?.addEventListener("error", reject);
@@ -76,9 +82,14 @@ class ThumnbnailWorker {
 
   private async takeSnapshot(at: number) {
     if (!this.videoEl || !this.canvasEl) return;
+    await this.videoEl.play(); // so that `seeked` actually triggers
     this.videoEl.currentTime = at;
     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;
     const ctx = this.canvasEl.getContext("2d");
@@ -91,6 +102,16 @@ class ThumnbnailWorker {
       this.canvasEl.height
     );
     const imgUrl = this.canvasEl.toDataURL();
+
+    // ! Remove before merging
+    const img = new Image();
+    img.src = imgUrl;
+    img.style.border = "1px solid yellow";
+    document.body.appendChild(img);
+    const p = document.createElement("p");
+    p.innerText = `${at}`;
+    document.body.appendChild(p);
+
     if (this.interrupted) return;
     if (imgUrl === "data:," || !imgUrl) return; // failed image rendering
 
diff --git a/src/pages/developer/VideoTesterView.tsx b/src/pages/developer/VideoTesterView.tsx
index 10e6a9c0..e58c4632 100644
--- a/src/pages/developer/VideoTesterView.tsx
+++ b/src/pages/developer/VideoTesterView.tsx
@@ -19,7 +19,7 @@ const testMeta: PlayerMeta = {
 
 const testStreams: Record<StreamType, string> = {
   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> = {

From 903185f5a398da0d4a8a6338ddfa237d7712dd0a Mon Sep 17 00:00:00 2001
From: Jip Fr <jipfrijlink@gmail.com>
Date: Mon, 11 Dec 2023 17:47:24 +0100
Subject: [PATCH 6/9] Add safe area inset bottom to next episode button

---
 src/components/player/atoms/NextEpisodeButton.tsx | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/components/player/atoms/NextEpisodeButton.tsx b/src/components/player/atoms/NextEpisodeButton.tsx
index b80df3a9..cef4717b 100644
--- a/src/components/player/atoms/NextEpisodeButton.tsx
+++ b/src/components/player/atoms/NextEpisodeButton.tsx
@@ -62,9 +62,11 @@ export function NextEpisodeButton(props: {
   if (isHidden || status !== "playing" || duration === 0) show = false;
 
   const animation = showingState === "hover" ? "slide-up" : "fade";
-  let bottom = "bottom-24";
+  let bottom = "bottom-[calc(6rem+env(safe-area-inset-bottom))]";
   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(
     (v) => v.number === (meta?.episode?.number ?? 0) + 1

From b5cb432241bcf2eb8419efaa139432f7feee33af Mon Sep 17 00:00:00 2001
From: Jip Fr <jipfrijlink@gmail.com>
Date: Wed, 13 Dec 2023 21:13:57 +0100
Subject: [PATCH 7/9] Add safe area to next episode button, clean up debugging
 stuff, disable thumbnails on Safari, fix lightbar on landscape

Co-authored-by: mrjvs <mistrjvs@gmail.com>
---
 .../player/atoms/NextEpisodeButton.tsx          |  2 +-
 .../player/internals/ThumbnailScraper.tsx       | 17 +++--------------
 src/components/utils/Lightbar.tsx               |  4 ++--
 3 files changed, 6 insertions(+), 17 deletions(-)

diff --git a/src/components/player/atoms/NextEpisodeButton.tsx b/src/components/player/atoms/NextEpisodeButton.tsx
index cef4717b..8699d311 100644
--- a/src/components/player/atoms/NextEpisodeButton.tsx
+++ b/src/components/player/atoms/NextEpisodeButton.tsx
@@ -88,7 +88,7 @@ export function NextEpisodeButton(props: {
     <Transition
       animation={animation}
       show={show}
-      className="absolute right-12 bottom-0"
+      className="absolute right-[calc(3rem+env(safe-area-inset-right))] bottom-0"
     >
       <div
         className={classNames([
diff --git a/src/components/player/internals/ThumbnailScraper.tsx b/src/components/player/internals/ThumbnailScraper.tsx
index 82202f07..a86d2043 100644
--- a/src/components/player/internals/ThumbnailScraper.tsx
+++ b/src/components/player/internals/ThumbnailScraper.tsx
@@ -5,6 +5,7 @@ import { playerStatus } from "@/stores/player/slices/source";
 import { ThumbnailImage } from "@/stores/player/slices/thumbnails";
 import { usePlayerStore } from "@/stores/player/store";
 import { LoadableSource, selectQuality } from "@/stores/player/utils/qualities";
+import { isSafari } from "@/utils/detectFeatures";
 
 function makeQueue(layers: number): number[] {
   const output = [0, 1];
@@ -39,11 +40,9 @@ class ThumnbnailWorker {
   }
 
   start(source: LoadableSource) {
+    if (isSafari) return false;
     const el = document.createElement("video");
     el.setAttribute("muted", "true");
-    // ! Remove before merging
-    el.setAttribute("controls", "true");
-    el.setAttribute("playsinline", "true");
     const canvas = document.createElement("canvas");
     this.hls = new Hls();
     if (source.type === "mp4") {
@@ -69,8 +68,6 @@ class ThumnbnailWorker {
 
   private async initVideo() {
     if (!this.videoEl || !this.canvasEl) return;
-    // ! Remove before merging
-    document.body.appendChild(this.videoEl);
     await new Promise((resolve, reject) => {
       this.videoEl?.addEventListener("loadedmetadata", resolve);
       this.videoEl?.addEventListener("error", reject);
@@ -103,15 +100,6 @@ class ThumnbnailWorker {
     );
     const imgUrl = this.canvasEl.toDataURL();
 
-    // ! Remove before merging
-    const img = new Image();
-    img.src = imgUrl;
-    img.style.border = "1px solid yellow";
-    document.body.appendChild(img);
-    const p = document.createElement("p");
-    p.innerText = `${at}`;
-    document.body.appendChild(p);
-
     if (this.interrupted) return;
     if (imgUrl === "data:," || !imgUrl) return; // failed image rendering
 
@@ -163,6 +151,7 @@ export function ThumbnailScraper() {
     workerRef.current = ins;
     ins.start(inputStream.stream);
   }, [source, addImage, resetImages, status]);
+
   const startRef = useRef(start);
   useEffect(() => {
     startRef.current = start;
diff --git a/src/components/utils/Lightbar.tsx b/src/components/utils/Lightbar.tsx
index 59d4cac8..0949dad3 100644
--- a/src/components/utils/Lightbar.tsx
+++ b/src/components/utils/Lightbar.tsx
@@ -161,8 +161,8 @@ function ParticlesCanvas() {
 
 export function Lightbar(props: { className?: string }) {
   return (
-    <div className="absolute inset-0 w-full h-screen overflow-hidden pointer-events-none -mt-64">
-      <div className="max-w-screen w-full h-screen relative pt-64">
+    <div className="absolute inset-0 w-full h-[300vh] overflow-hidden pointer-events-none -mt-64">
+      <div className="max-w-screen w-full h-[300vh] relative pt-64">
         <div className={props.className}>
           <div className="lightbar">
             <ParticlesCanvas />

From 87b6399d5d2a00aebd33e85a3a42809559cd80a5 Mon Sep 17 00:00:00 2001
From: Jip Fr <jipfrijlink@gmail.com>
Date: Wed, 13 Dec 2023 21:25:48 +0100
Subject: [PATCH 8/9] Fix tall boi page UwU

---
 src/components/utils/Lightbar.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/utils/Lightbar.tsx b/src/components/utils/Lightbar.tsx
index 0949dad3..6968c597 100644
--- a/src/components/utils/Lightbar.tsx
+++ b/src/components/utils/Lightbar.tsx
@@ -161,8 +161,8 @@ function ParticlesCanvas() {
 
 export function Lightbar(props: { className?: string }) {
   return (
-    <div className="absolute inset-0 w-full h-[300vh] overflow-hidden pointer-events-none -mt-64">
-      <div className="max-w-screen w-full h-[300vh] relative pt-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-[calc(100vh+16rem)] relative pt-64">
         <div className={props.className}>
           <div className="lightbar">
             <ParticlesCanvas />

From 1bcddb80aab7694c4dc905608d06644b78501150 Mon Sep 17 00:00:00 2001
From: Jip Fr <jipfrijlink@gmail.com>
Date: Wed, 13 Dec 2023 21:41:55 +0100
Subject: [PATCH 9/9] Remove weird gradient thingie

---
 src/pages/layouts/SubPageLayout.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/layouts/SubPageLayout.tsx b/src/pages/layouts/SubPageLayout.tsx
index a18cfa7b..3be3e97e 100644
--- a/src/pages/layouts/SubPageLayout.tsx
+++ b/src/pages/layouts/SubPageLayout.tsx
@@ -26,7 +26,7 @@ export function BlurEllipsis(props: { positionClass?: string }) {
 export function SubPageLayout(props: { children: React.ReactNode }) {
   return (
     <div
-      className="from-[#0D0D1A] to-background-main"
+      className="bg-background-main"
       style={{
         backgroundImage:
           "linear-gradient(to bottom, var(--tw-gradient-from), var(--tw-gradient-to) 800px)",