From f3d141ec6d6a06fbb104a76a5043b1393bc0f1c4 Mon Sep 17 00:00:00 2001 From: Will Kelly Date: Thu, 10 Oct 2024 11:38:12 -0500 Subject: [PATCH] fix ui around downloading. Treat all cached playlist calls as stale (but maintain cache). --- .github/workflows/android.yml | 3 + .github/workflows/ios.yml | 3 + ios/App/Podfile.lock | 16 +- package.json | 4 +- src/components/Player.tsx | 341 ++++++++++--------- src/components/PlaylistInfo.tsx | 4 +- src/components/Settings.tsx | 5 +- src/components/Settings/BulkListing.tsx | 9 +- src/components/Settings/DownloadBookItem.tsx | 4 + src/lib/Ui.tsx | 42 ++- src/lib/storage.ts | 4 +- src/pages/Playlist.tsx | 32 +- 12 files changed, 256 insertions(+), 211 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 24d79ce..9bcead9 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -5,6 +5,9 @@ jobs: build: runs-on: ubuntu-latest name: Build the project + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 9516cff..603f966 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -8,6 +8,9 @@ jobs: build: runs-on: macos-latest name: Build iOS app + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true steps: - name: Checkout source uses: actions/checkout@v4 diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 6fc9086..c243d9f 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -1,21 +1,21 @@ PODS: - - Capacitor (6.0.0): + - Capacitor (6.1.0): - CapacitorCordova - CapacitorApp (6.0.0): - Capacitor - CapacitorBlobWriter (0.0.1): - Capacitor - GCDWebServer (~> 3.0) - - CapacitorCordova (6.0.0) + - CapacitorCordova (6.1.0) - CapacitorDevice (6.0.0): - Capacitor - CapacitorFilesystem (6.0.0): - Capacitor - CapacitorHaptics (6.0.0): - Capacitor - - CapacitorKeyboard (6.0.0): + - CapacitorKeyboard (6.0.1): - Capacitor - - CapacitorPreferences (6.0.0): + - CapacitorPreferences (6.0.1): - Capacitor - CapacitorStatusBar (6.0.0): - Capacitor @@ -62,15 +62,15 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/status-bar" SPEC CHECKSUMS: - Capacitor: 559d073c4ca6c27f8e7002c807eea94c3ba435a9 + Capacitor: 187bd7847b6f71467015a20200a1a071be3e5f14 CapacitorApp: 9d53aec7101f7b030a950c5bdc4df8612576b279 CapacitorBlobWriter: 110eeaf80611f19bf01a8a05ff3672149ed0baad - CapacitorCordova: 8c4bfdf69368512e85b1d8b724dd7546abeb30af + CapacitorCordova: be703980ca797f847c3356f78fa175d21c8330c2 CapacitorDevice: f8fd88f9edd1261c55a109f32015b09bbbfdc4a0 CapacitorFilesystem: 60e59ba274c234a979e7a3be2552feaadcee4263 CapacitorHaptics: 9ebc9363f0e9b8eb4295088a0b474530acf1859b - CapacitorKeyboard: deacbd09d8d1029c3681197fb05d206b721d5f73 - CapacitorPreferences: c9b839a80baa72e4ed4bdb117618e1950d3046b5 + CapacitorKeyboard: 5f32a712adf41e07a61caafb82cf29fb6d8ba123 + CapacitorPreferences: 72909b165bc7807103778ddbb86d5d8ce06abf71 CapacitorStatusBar: 2e4369f99166125435641b1908d05f561eaba6f6 GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4 diff --git a/package.json b/package.json index 4a58fca..b4aa92b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "preview": "vite preview", "test.e2e": "playwright test", "test.unit": "vitest", - "lint": "biome check --apply --verbose src/**/* src/", + "lint": "biome check --write --verbose src/**/* src/", "build-ios": "ionic capacitor build ios --release --prod --no-open", "build-android": "ionic capacitor build android --release --prod --no-open", "ios": "npm run build && npm run sync && npx cap run ios", @@ -67,7 +67,7 @@ "vitest": "^1.6.0" }, "lint-staged": { - "*": "biome check --apply --verbose src/**/* src/" + "*": "biome check --write --verbose src/**/* src/" }, "description": "WA Sign language bibles app" } diff --git a/src/components/Player.tsx b/src/components/Player.tsx index 7b0ddd7..b8125e2 100644 --- a/src/components/Player.tsx +++ b/src/components/Player.tsx @@ -1,189 +1,190 @@ -import {type Dispatch, type SetStateAction, useEffect, useRef} from "react"; -import type {VideoJsPlayer} from "video.js"; -import type {IVidWithCustom, chapterMarkers} from "../customTypes/types"; -import {handleVideoJsTaps, playerCustomHotKeys} from "../lib/Ui"; -import {getSavedAppPreferences} from "../lib/storage"; +import { type Dispatch, type SetStateAction, useEffect, useRef } from "react"; +import type { VideoJsPlayer } from "video.js"; +import type { IVidWithCustom, chapterMarkers } from "../customTypes/types"; +import { handleVideoJsTaps, playerCustomHotKeys } from "../lib/Ui"; +import { getSavedAppPreferences } from "../lib/storage"; type Iplayer = { - setPlayer: Dispatch>; - existingPlayer: VideoJsPlayer | undefined; - playlistData: Record; - currentVid: IVidWithCustom; - setJumpingForwardAmount: Dispatch>; - setJumpingBackAmount: Dispatch>; - handleChapters( - vid: IVidWithCustom, - vidJsPlayer: VideoJsPlayer | undefined - ): Promise; + setPlayer: Dispatch>; + existingPlayer: VideoJsPlayer | undefined; + playlistData: Record; + currentVid: IVidWithCustom; + setJumpingForwardAmount: Dispatch>; + setJumpingBackAmount: Dispatch>; + handleChapters( + vid: IVidWithCustom, + vidJsPlayer: VideoJsPlayer | undefined, + ): Promise; }; export function VidJsPlayer({ - setPlayer, - existingPlayer, - playlistData, - currentVid, - setJumpingBackAmount, - setJumpingForwardAmount, - handleChapters, + setPlayer, + existingPlayer, + playlistData, + currentVid, + setJumpingBackAmount, + setJumpingForwardAmount, + handleChapters, }: Iplayer) { - const vidPlayerRef = useRef(null); - const vidJsPlayerContainerRef = useRef(null); + const vidPlayerRef = useRef(null); + const vidJsPlayerContainerRef = useRef(null); - async function bootPlayer() { - if (playlistData) { - const curAppState = await getSavedAppPreferences(); - // SPEED - const preferredSpeed = curAppState?.preferredSpeed || 1; - const jumpAmount = 5; //seconds to jump on double taps; - const firstBook = currentVid; - // Resume src, offline or stream - const firstVidSrces = firstBook.savedSources?.video - ? { - src: `${firstBook.savedSources?.video}`, - type: "video/mp4", - } - : firstBook.sources; + async function bootPlayer() { + if (playlistData) { + const curAppState = await getSavedAppPreferences(); + // SPEED + const preferredSpeed = curAppState?.preferredSpeed || 1; + const jumpAmount = 5; //seconds to jump on double taps; + const firstBook = currentVid; + // Resume src, offline or stream + const firstVidSrces = firstBook.savedSources?.video + ? { + src: `${firstBook.savedSources?.video}`, + type: "video/mp4", + } + : firstBook.sources; - const firstPoster = firstBook.savedSources?.poster - ? firstBook.savedSources?.poster - : firstBook.poster; + const firstPoster = firstBook.savedSources?.poster + ? firstBook.savedSources?.poster + : firstBook.poster; - // instantiate player - // window.bc is from /bc/willPlayer. This is a brightcove player that has been manually downloaded and included to avoid the network request for a 200kb + video js player. This allows us to bundle it for offline usage in mobile app more easily too. We could just use video js, but the bundled / minified player includes brightcoves built in analytics. If we are offline, they won't send, but that's a noop at that point. The priority is availability. - // SEe https://videojs.com/guides/options - const player = window.bc(vidPlayerRef.current, { - responsive: true, - fluid: true, - controls: true, - playbackRates: [0.5, 1, 1.5, 2, 2.5], - preload: "auto", - autoplay: false, - fullscreen: { - navigationUI: "show", - }, - enableDocumentPictureInPicture: true, - sources: firstVidSrces, - poster: firstPoster, - nativeControlsForTouch: true, - }); + // instantiate player + // window.bc is from /bc/willPlayer. This is a brightcove player that has been manually downloaded and included to avoid the network request for a 200kb + video js player. This allows us to bundle it for offline usage in mobile app more easily too. We could just use video js, but the bundled / minified player includes brightcoves built in analytics. If we are offline, they won't send, but that's a noop at that point. The priority is availability. + // SEe https://videojs.com/guides/options - player.playbackRate(preferredSpeed); - player.on("loadstart", () => maintainPlayerSpeed(player)); - player.language(navigator.languages[0]); - player.playsinline(true); //ios + const player = window.bc(vidPlayerRef.current, { + responsive: true, + fluid: true, + controls: true, + playbackRates: [0.5, 1, 1.5, 2, 2.5], + preload: "auto", + autoplay: false, + fullscreen: { + navigationUI: "show", + }, + enableDocumentPictureInPicture: true, + sources: firstVidSrces, + poster: firstPoster, + nativeControlsForTouch: true, + }); - // Get initial chapters if present - player.one("loadedmetadata", () => { - handleChapters(currentVid, player); - }); + player.playbackRate(preferredSpeed); + player.on("loadstart", () => maintainPlayerSpeed(player)); + player.language(navigator.languages[0]); + player.playsinline(true); //ios - const videoJsDomEl = player.el(); - // Handle mobile taps - handleVideoJsTaps({ - el: videoJsDomEl, - rightDoubleFxn(number) { - const curTime = player?.currentTime(); - if (!curTime) return; - // the extra minus jumpAmount on end of next line is to account for fact that min tap amount is 2 to diff btw double and single taps, so we still want to allow the smallest measure of jump back; - const newTime = number * jumpAmount + curTime - jumpAmount; - player?.currentTime(newTime); - setJumpingForwardAmount(null); - videoJsDomEl.classList.remove("vjs-user-active"); - }, - leftDoubleFxn(number) { - const curTime = player?.currentTime(); - if (!curTime) return; + // Get initial chapters if present + player.one("loadedmetadata", () => { + handleChapters(currentVid, player); + }); - const newTime = curTime - number * jumpAmount - jumpAmount; - player?.currentTime(newTime); - setJumpingBackAmount(null); - videoJsDomEl.classList.remove("vjs-user-active"); - }, - singleTapFxn() { - if (!player) return; - if (player.paused()) { - player.play(); - } else { - player.pause(); - } - }, - doubleTapUiClue(dir, tapsCount) { - if (dir === "LEFT") { - setJumpingBackAmount(tapsCount * jumpAmount - 5); - setJumpingForwardAmount(null); - } else if (dir === "RIGHT") { - setJumpingBackAmount(null); - setJumpingForwardAmount(tapsCount * jumpAmount - 5); - } - }, - }); + const videoJsDomEl = player.el(); + // Handle mobile taps + handleVideoJsTaps({ + el: videoJsDomEl, + rightDoubleFxn(number) { + const curTime = player?.currentTime(); + if (!curTime) return; + // the extra minus jumpAmount on end of next line is to account for fact that min tap amount is 2 to diff btw double and single taps, so we still want to allow the smallest measure of jump back; + const newTime = number * jumpAmount + curTime - jumpAmount; + player?.currentTime(newTime); + setJumpingForwardAmount(null); + videoJsDomEl.classList.remove("vjs-user-active"); + }, + leftDoubleFxn(number) { + const curTime = player?.currentTime(); + if (!curTime) return; - // @MANAGE KEYS TO SKIP - player.on("keydown", (e: KeyboardEvent) => - playerCustomHotKeys({ - e, - vjsPlayer: player, - increment: jumpAmount, - setJumpingBackAmount, - setJumpingForwardAmount, - }) - ); + const newTime = curTime - number * jumpAmount - jumpAmount; + player?.currentTime(newTime); + setJumpingBackAmount(null); + videoJsDomEl.classList.remove("vjs-user-active"); + }, + singleTapFxn() { + if (!player) return; + if (player.paused()) { + player.play(); + } else { + player.pause(); + } + }, + doubleTapUiClue(dir, tapsCount) { + if (dir === "LEFT") { + setJumpingBackAmount(tapsCount * jumpAmount - 5); + setJumpingForwardAmount(null); + } else if (dir === "RIGHT") { + setJumpingBackAmount(null); + setJumpingForwardAmount(tapsCount * jumpAmount - 5); + } + }, + }); - // Finally set state - setPlayer(player); - } - } - async function maintainPlayerSpeed(player: VideoJsPlayer) { - if (vidJsPlayerContainerRef.current) { - const curAppState = await getSavedAppPreferences(); - const preferredSpeed = curAppState?.preferredSpeed || 1; - player.playbackRate(preferredSpeed); - } - } - function handleSpeedChangesInApp() { - if (vidJsPlayerContainerRef.current && existingPlayer) { - vidJsPlayerContainerRef.current.addEventListener( - "adjustPlayerSpeed", - (event: Event) => { - const customEvent = event as CustomEvent; + // @MANAGE KEYS TO SKIP + player.on("keydown", (e: KeyboardEvent) => + playerCustomHotKeys({ + e, + vjsPlayer: player, + increment: jumpAmount, + setJumpingBackAmount, + setJumpingForwardAmount, + }), + ); - if ( - customEvent.detail && - typeof customEvent.detail.speed === "number" - ) { - const speed = customEvent.detail.speed; - existingPlayer.playbackRate(speed); - } - } - ); - } - } + // Finally set state + setPlayer(player); + } + } + async function maintainPlayerSpeed(player: VideoJsPlayer) { + if (vidJsPlayerContainerRef.current) { + const curAppState = await getSavedAppPreferences(); + const preferredSpeed = curAppState?.preferredSpeed || 1; + player.playbackRate(preferredSpeed); + } + } + function handleSpeedChangesInApp() { + if (vidJsPlayerContainerRef.current && existingPlayer) { + vidJsPlayerContainerRef.current.addEventListener( + "adjustPlayerSpeed", + (event: Event) => { + const customEvent = event as CustomEvent; - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - bootPlayer(); - }, []); + if ( + customEvent.detail && + typeof customEvent.detail.speed === "number" + ) { + const speed = customEvent.detail.speed; + existingPlayer.playbackRate(speed); + } + }, + ); + } + } - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - // a custom event dispatch listener - handleSpeedChangesInApp(); - }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + bootPlayer(); + }, []); - return ( -
- {playlistData && ( - // biome-ignore lint/a11y/useMediaCaption: captions provided through chap segments -
- ); + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + // a custom event dispatch listener + handleSpeedChangesInApp(); + }, []); + + return ( +
+ {playlistData && ( + // biome-ignore lint/a11y/useMediaCaption: captions provided through chap segments +
+ ); } diff --git a/src/components/PlaylistInfo.tsx b/src/components/PlaylistInfo.tsx index 6df698a..0d9e0cf 100644 --- a/src/components/PlaylistInfo.tsx +++ b/src/components/PlaylistInfo.tsx @@ -4,7 +4,7 @@ import { IconMaterialSymbolsCheckCircle } from "./Icons"; type IPlaylistInfo = { currentVid: IVidWithCustom; playlist: validPlaylistSlugs; - isSavingSingle: boolean; + isSavingSingle: string[]; }; export function PlaylistInfo({ currentVid, @@ -26,7 +26,7 @@ export function PlaylistInfo({ )} - {isSavingSingle && ( + {currentVid.id && isSavingSingle.includes(currentVid.id) && (
diff --git a/src/components/Settings/BulkListing.tsx b/src/components/Settings/BulkListing.tsx index 20cb13d..6d9709c 100644 --- a/src/components/Settings/BulkListing.tsx +++ b/src/components/Settings/BulkListing.tsx @@ -38,6 +38,7 @@ type IDownloadListing = { setCurrentVid: Dispatch>; downloadProgress: downloadProgressInfo | undefined; setShapedPlaylist: Dispatch>; + setIsSavingSingle: Dispatch>; }; export function BulkListing({ playlistData, @@ -49,6 +50,7 @@ export function BulkListing({ currentVid, downloadProgress, setShapedPlaylist, + setIsSavingSingle, }: IDownloadListing) { const { t } = useTranslation(); const [booksSelected, setBooksSelected] = useState>(); @@ -138,6 +140,10 @@ export function BulkListing({ for await (const vidChapter of flattenedBooks) { if (skipVidDownload(vidChapter)) continue; + setIsSavingSingle((prev) => { + // biome-ignore lint/style/noNonNullAssertion: + return [...prev, ...flattenedBooks.map((vid) => vid.id!)]; + }); if (!vidChapter.savedSources?.poster || !vidChapter.savedSources?.video) { setDownloadProgress({ amount: 0, @@ -214,7 +220,7 @@ export function BulkListing({ size="small" fill="outline" color="primary" - disabled={!booksSelected?.length} + disabled={!booksSelected?.length && !downloadProgress?.started} className="text-surface" onClick={() => { if (downloadProgress?.started) { @@ -246,6 +252,7 @@ export function BulkListing({ booksToCancel={booksToCancel} setBooksToCancel={setBooksToCancel} clearBookFromFs={clearBookFromFs} + setIsSavingSingle={setIsSavingSingle} /> ))} diff --git a/src/components/Settings/DownloadBookItem.tsx b/src/components/Settings/DownloadBookItem.tsx index 790861b..ccad69c 100644 --- a/src/components/Settings/DownloadBookItem.tsx +++ b/src/components/Settings/DownloadBookItem.tsx @@ -25,6 +25,7 @@ type BookToDownloadProps = { booksToCancel: (string | undefined)[]; setBooksToCancel: Dispatch>; clearBookFromFs(videos: IVidWithCustom[]): void; + setIsSavingSingle: Dispatch>; }; function bookIsFullyDownloaded(book: IVidWithCustom[]) { return book.every((vid) => !!vid.savedSources?.video); @@ -153,6 +154,9 @@ export function BookToDownload(props: BookToDownloadProps) { const vidsWithIds = book.value .filter((v) => !!v.id) .map((v) => v.id) as string[]; + props.setIsSavingSingle((prev) => + prev.filter((id) => !vidsWithIds.includes(id)), + ); if ( window.dotAppBooksToCancel && Array.isArray(window.dotAppBooksToCancel) && diff --git a/src/lib/Ui.tsx b/src/lib/Ui.tsx index 396caa4..518d6ce 100644 --- a/src/lib/Ui.tsx +++ b/src/lib/Ui.tsx @@ -354,7 +354,7 @@ type IupdatePrefsInBackground = { existingPlaylistData?: formattedPlaylist; playlist: string; }; -export async function updatePrefsInBackground({ +export async function mergeInPreviouslySavedVids({ existingPlaylistData, playlist, }: IupdatePrefsInBackground) { @@ -394,13 +394,16 @@ export async function updatePrefsInBackground({ const { videos, formattedVideos, ...restPlaylistData } = data; mutateTimeStampBcResponse(restPlaylistData); - await cacheBcPlaylistJson({ + // no need to await this write to fs here + cacheBcPlaylistJson({ data: JSON.stringify({ formattedVideos: groupedBy, ...restPlaylistData, }), playlistSlug: playlist, }); + // return api response after it's been mutated with any savedSources from FS + return data; } type cacheBcPlaylistJsonArgs = { data: string | Blob; @@ -438,6 +441,7 @@ async function fetchBcApiEndpoint(playlist: string) { console.warn(error); } } + export async function fetchBcData(playlist: validPlaylistSlugs) { try { let savedData: IPlaylistResponse | null = null; @@ -456,23 +460,27 @@ export async function fetchBcData(playlist: validPlaylistSlugs) { console.error(error); } + // Always refresh savedData with freshest from api right now, but do merge in anythign previously saved. We can look at doing the caching stuff again later, but for now, prioritize freshness if (savedData) { - if (savedData.refreshBy > Date.now()) { - return savedData; - } - if ( - savedData.refreshBy < Date.now() && - savedData.expiresBy > Date.now() - ) { - // This is a background update. If it fails while offline or whatever, it - updatePrefsInBackground({ - existingPlaylistData: savedData.formattedVideos, - playlist, - }); - return savedData; - } + // NOTE: THERE WAS SOME BANDWIDTH SAVING HERE BY ONLY REFRESHING AFTER SO LONG, BUT WE'RE JUST GONNA EAT THE BANDWIDTH FOR A PLAYLIST FETCH HERE AND NOT HAVE ANYTHING STALE + // if (savedData.refreshBy > Date.now()) { + // return savedData; + // } + // if ( + // savedData.refreshBy < Date.now() && + // savedData.expiresBy > Date.now() + // ) { + // This is a background update. If it fails while offline or whatever, it + mergeInPreviouslySavedVids({ + existingPlaylistData: savedData.formattedVideos, + playlist, + }); + return savedData; + // } + // } + // } } - + // if we are fetching fresh, we have nothing cached and don't need to worry about mergeing in previously saved vids const data = await fetchBcApiEndpoint(playlist); if (!data) throw new Error("fetch failed"); mutateTimeStampBcResponse(data); diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 82db8ef..7324403 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -22,8 +22,8 @@ export function makeVidSaver( vid: IVidWithCustom, ) { // NOTE: We could change this, but 10 seems like a reasonsable amount for avoiding timeouts. - const increment = 1024 * 1000 * 10; //1 kilobyte * 1000 (= 1mb) * 10 = (10mb) - // const increment = 10 * 10 * 1024; //10mb + // const increment = 1024 * 1000 * 10; //1 kilobyte * 1000 (= 1mb) * 10 = (10mb) + const increment = 1024 * 300; //1kb * 250 = 300kb const mp4FileName = `${vid.id}_mp4`; function getAggregatedBlobPath() { diff --git a/src/pages/Playlist.tsx b/src/pages/Playlist.tsx index 6aa8989..7e96467 100644 --- a/src/pages/Playlist.tsx +++ b/src/pages/Playlist.tsx @@ -67,7 +67,7 @@ function Playlist() { const [jumpingForwardAmount, setJumpingForwardAmount] = useState(null); const [jumpingBackAmount, setJumpingBackAmount] = useState(null); - const [isSavingSingle, setIsSavingSingle] = useState(false); + const [isSavingSingle, setIsSavingSingle] = useState([]); const alertRef: any = useRef(null); /*// #=============== PAGE FUNCTIONS ============= */ @@ -229,9 +229,23 @@ function Playlist() { // Using one each time a vid ends to try to avoid a stale closures issue that has cropped up some with react and videos js vidJsPlayer.off("ended", autoPlayToNextBook); vidJsPlayer.one("ended", autoPlayToNextBook); + // MAYBE: I HAD ERROR HANDLING FOR EXPIRED SRC errors on media not supported, but now fetchAndSetup no already fetches the latest sources, but leaving here in case need to troubleshoot more later + vidJsPlayer.on("error", handleVidJsError); } }, [currentVid, vidJsPlayer]); + async function handleVidJsError() { + if (!vidJsPlayer) return; + const err = vidJsPlayer.error(); + if (!err) return; + console.error({ err }); + if (err.code === 4) { + // 4 is mdia src not supported. + // await fetchAndSetup(); + } + // console.error(event); + } + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { async function refreshPlayer() { @@ -284,7 +298,7 @@ function Playlist() { let firstBookShow: IVidWithCustom[] = bucketToUse[keys[0]]; //default if (parsedState?.currentBookName) { const key = parsedState?.currentBookName; - if (key) { + if (key && bucketToUse[key]) { firstBookShow = bucketToUse[key]; } } @@ -337,7 +351,7 @@ function Playlist() { const commonAction = (action: "DOWNLOAD" | "DELETE") => { const settingsEl = document.querySelector("#settingsRef"); if (settingsEl) { - const adjustPlayerSpeedEvent = new CustomEvent( + const manageSingleVideoStorage = new CustomEvent( "manageSingleVideoStorage", { detail: { @@ -346,9 +360,10 @@ function Playlist() { }, }, ); - settingsEl.dispatchEvent(adjustPlayerSpeedEvent); - if (action === "DOWNLOAD") { - setIsSavingSingle(true); + settingsEl.dispatchEvent(manageSingleVideoStorage); + const curId = currentVid.id; + if (action === "DOWNLOAD" && curId) { + setIsSavingSingle((prev) => [...prev, curId]); } } }; @@ -382,9 +397,9 @@ function Playlist() { useEffect(() => { if (currentVid.savedSources?.video) { - setIsSavingSingle(false); + setIsSavingSingle((prev) => prev.filter((id) => id !== currentVid.id)); } - }, [currentVid.savedSources?.video]); + }, [currentVid.savedSources?.video, currentVid.id]); /*//# =============== MARKUP ============= */ return ( @@ -410,6 +425,7 @@ function Playlist() { playlistSlug={playlistInfo.playlist} setShapedPlaylist={setShapedPlaylist} setCurrentBook={setCurrentBook} + setIsSavingSingle={setIsSavingSingle} /> )}