Feat: display download progress (#315)
* cache tedtalk download url * improve tedtalk download ux * display progress bar * fix locale * update UI * display progress when downloading audbile
This commit is contained in:
@@ -448,5 +448,7 @@
|
||||
"translationFailed": "Translation failed",
|
||||
"allRecordingsSynced": "All recordings synced",
|
||||
"syncingRecordings": "Syncing {{count}} recordings",
|
||||
"failedToSyncRecordings": "Syncing recordings failed"
|
||||
"failedToSyncRecordings": "Syncing recordings failed",
|
||||
"downloadUrlNotResolved": "Download URL not resolved",
|
||||
"resolvingDownloadUrl": "Resolving download URL"
|
||||
}
|
||||
|
||||
@@ -447,5 +447,7 @@
|
||||
"translationFailed": "翻译失败",
|
||||
"allRecordingsSynced": "所有录音已同步",
|
||||
"syncingRecordings": "{{count}} 条录音正在同步",
|
||||
"failedToSyncRecordings": "同步录音失败"
|
||||
"failedToSyncRecordings": "同步录音失败",
|
||||
"downloadUrlNotResolved": "无法解析下载地址",
|
||||
"resolvingDownloadUrl": "正在解析下载地址"
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ export class AudibleProvider {
|
||||
const view = new BrowserView();
|
||||
view.webContents.loadURL(this.baseURL + path);
|
||||
view.webContents.on("did-finish-load", () => {
|
||||
logger.debug(`Scraped ${this.baseURL + path}`);
|
||||
view.webContents
|
||||
.executeJavaScript(`document.documentElement.innerHTML`)
|
||||
.then((html) => resolve(html as string));
|
||||
});
|
||||
view.webContents.on("did-fail-load", () => {
|
||||
logger.error(`Failed to scrape ${this.baseURL + path}`);
|
||||
reject();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Progress,
|
||||
} from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
import { MediaPlayer, MediaProvider } from "@vidstack/react";
|
||||
@@ -27,10 +28,12 @@ export const AudibleBooksSegment = () => {
|
||||
null
|
||||
);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const downloadSample = () => {
|
||||
if (!selectedBook.sample) return;
|
||||
|
||||
setProgress(0);
|
||||
setDownloading(true);
|
||||
EnjoyApp.audios
|
||||
.create(selectedBook.sample, {
|
||||
@@ -73,6 +76,22 @@ export const AudibleBooksSegment = () => {
|
||||
fetchAudibleBooks();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedBook) return;
|
||||
|
||||
EnjoyApp.download.onState((_, downloadState) => {
|
||||
console.log(downloadState);
|
||||
const { state, received, total } = downloadState;
|
||||
if (state === "progressing") {
|
||||
setProgress(Math.floor((received / total) * 100));
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
EnjoyApp.download.removeAllListeners();
|
||||
};
|
||||
}, [selectedBook]);
|
||||
|
||||
if (!books?.length) return null;
|
||||
|
||||
return (
|
||||
@@ -158,6 +177,8 @@ export const AudibleBooksSegment = () => {
|
||||
{t("downloadSample")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
{downloading && progress > 0 && <Progress value={progress} />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/** @format */
|
||||
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import {
|
||||
@@ -11,8 +9,9 @@ import {
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Progress,
|
||||
toast,
|
||||
} from "@renderer/components/ui";
|
||||
import { LoaderSpin } from "@renderer/components";
|
||||
import { t } from "i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LoaderIcon } from "lucide-react";
|
||||
@@ -33,6 +32,8 @@ export const TedTalksSegment = () => {
|
||||
audio: string;
|
||||
video: string;
|
||||
}>();
|
||||
const [resolving, setResolving] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const addToLibrary = (type: DownloadType) => {
|
||||
if (!downloadUrl) return;
|
||||
@@ -42,6 +43,7 @@ export const TedTalksSegment = () => {
|
||||
if (type === DownloadType.video) url = downloadUrl.video;
|
||||
setSubmitting(true);
|
||||
setSubmittingType(type);
|
||||
setProgress(0);
|
||||
|
||||
EnjoyApp.videos
|
||||
.create(url, {
|
||||
@@ -49,6 +51,11 @@ export const TedTalksSegment = () => {
|
||||
coverUrl: selectedTalk?.primaryImageSet[0].url,
|
||||
})
|
||||
.then((record) => {
|
||||
if (!record) {
|
||||
toast.error(t("failedToDownload"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "video") {
|
||||
navigate(`/videos/${record.id}`);
|
||||
} else {
|
||||
@@ -61,16 +68,38 @@ export const TedTalksSegment = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const downloadTalk = () => {
|
||||
const resolveDowloadUrl = async () => {
|
||||
if (!selectedTalk?.canonicalUrl) return;
|
||||
if (resolving) return;
|
||||
|
||||
setResolving(true);
|
||||
setDownloadUrl(null);
|
||||
EnjoyApp.providers.ted
|
||||
.downloadTalk(selectedTalk?.canonicalUrl)
|
||||
.then((downloadUrl) => {
|
||||
if (!downloadUrl) return;
|
||||
setDownloadUrl(downloadUrl);
|
||||
});
|
||||
|
||||
const cachedUrl: {
|
||||
audio: string;
|
||||
video: string;
|
||||
} = await EnjoyApp.cacheObjects.get(
|
||||
`tedtalk-download-url-${selectedTalk?.canonicalUrl}`
|
||||
);
|
||||
if (cachedUrl) {
|
||||
setDownloadUrl(cachedUrl);
|
||||
setResolving(false);
|
||||
} else {
|
||||
EnjoyApp.providers.ted
|
||||
.downloadTalk(selectedTalk?.canonicalUrl)
|
||||
.then((url) => {
|
||||
if (!url) return;
|
||||
EnjoyApp.cacheObjects.set(
|
||||
`tedtalk-download-url-${selectedTalk?.canonicalUrl}`,
|
||||
url,
|
||||
60 * 60 * 24 * 7
|
||||
);
|
||||
setDownloadUrl(url);
|
||||
})
|
||||
.finally(() => {
|
||||
setResolving(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTalks = async () => {
|
||||
@@ -98,9 +127,25 @@ export const TedTalksSegment = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
downloadTalk();
|
||||
resolveDowloadUrl();
|
||||
}, [selectedTalk]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!downloadUrl) return;
|
||||
|
||||
EnjoyApp.download.onState((_, downloadState) => {
|
||||
console.log(downloadState);
|
||||
const { state, received, total } = downloadState;
|
||||
if (state === "progressing") {
|
||||
setProgress(Math.floor((received / total) * 100));
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
EnjoyApp.download.removeAllListeners();
|
||||
};
|
||||
}, [downloadUrl]);
|
||||
|
||||
if (!talks?.length) return null;
|
||||
|
||||
return (
|
||||
@@ -162,47 +207,68 @@ export const TedTalksSegment = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{downloadUrl ? (
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
EnjoyApp.shell.openExternal(selectedTalk?.canonicalUrl)
|
||||
}
|
||||
className="mr-auto"
|
||||
>
|
||||
{t("open")}
|
||||
</Button>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
EnjoyApp.shell.openExternal(selectedTalk?.canonicalUrl)
|
||||
}
|
||||
className="mr-auto"
|
||||
>
|
||||
{t("open")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={() => setSelectedTalk(null)} variant="secondary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
{downloadUrl.audio && (
|
||||
{downloadUrl ? (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => addToLibrary(DownloadType.audio)}
|
||||
disabled={submitting}
|
||||
onClick={() => setSelectedTalk(null)}
|
||||
variant="secondary"
|
||||
>
|
||||
{submittingType === DownloadType.audio && (
|
||||
<LoaderIcon className="w-4 h-4 animate-spin mr-2" />
|
||||
)}
|
||||
{t("downloadAudio")}
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
)}
|
||||
{downloadUrl.video && (
|
||||
<Button
|
||||
onClick={() => addToLibrary(DownloadType.video)}
|
||||
disabled={submitting}
|
||||
{downloadUrl.audio && (
|
||||
<Button
|
||||
onClick={() => addToLibrary(DownloadType.audio)}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submittingType === DownloadType.audio && (
|
||||
<LoaderIcon className="w-4 h-4 animate-spin mr-2" />
|
||||
)}
|
||||
{t("downloadAudio")}
|
||||
</Button>
|
||||
)}
|
||||
{downloadUrl.video && (
|
||||
<Button
|
||||
onClick={() => addToLibrary(DownloadType.video)}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submittingType === DownloadType.video && (
|
||||
<LoaderIcon className="w-4 h-4 animate-spin mr-2" />
|
||||
)}
|
||||
{t("downloadVideo")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : resolving ? (
|
||||
<div className="text-sm flex items-center justify-center">
|
||||
<LoaderIcon className="animate-spin" />
|
||||
<span className="ml-2">{t("resolvingDownloadUrl")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground text-center">
|
||||
{t("downloadUrlNotResolved")}
|
||||
{". "}
|
||||
<span
|
||||
className="underline cursor-pointer"
|
||||
onClick={resolveDowloadUrl}
|
||||
>
|
||||
{submittingType === DownloadType.video && (
|
||||
<LoaderIcon className="w-4 h-4 animate-spin mr-2" />
|
||||
)}
|
||||
{t("downloadVideo")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
) : (
|
||||
<LoaderSpin />
|
||||
)}
|
||||
{t("retry")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
||||
{submitting && progress > 0 && <Progress value={progress} />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user