diff --git a/src/components/AppRoot.tsx b/src/components/AppRoot.tsx
index dbee96a..696c46d 100644
--- a/src/components/AppRoot.tsx
+++ b/src/components/AppRoot.tsx
@@ -15,6 +15,7 @@ import { Bounce, ToastContainer } from 'react-toastify';
import { SettingsHome } from '~/features/settings/SettingsHome';
import { FAQ_PAGES } from '~/faq/FAQPages';
import { FaqHome } from '~/faq/FaqHome';
+import { DownloadAll } from '~/components/DownloadAll.tsx';
// Private to this file only.
const store = setupStore();
@@ -71,6 +72,7 @@ export function AppRoot() {
transition={Bounce}
/>
+
diff --git a/src/components/DownloadAll.tsx b/src/components/DownloadAll.tsx
new file mode 100644
index 0000000..5975033
--- /dev/null
+++ b/src/components/DownloadAll.tsx
@@ -0,0 +1,65 @@
+import { DecryptedAudioFile, selectFiles } from '~/features/file-listing/fileListingSlice';
+import { FaDownload } from 'react-icons/fa';
+import { useAppSelector } from '~/hooks';
+import { toast } from 'react-toastify';
+
+export function DownloadAll() {
+ const files = useAppSelector(selectFiles);
+ const filesLength = Object.keys(files).length;
+ const onClickDownloadAll = async () => {
+ let dir: FileSystemDirectoryHandle | undefined;
+ let success = 0;
+ try {
+ dir = await window.showDirectoryPicker();
+ } catch (e) {
+ console.error(e);
+ if (e instanceof Error && e.name === 'AbortError') {
+ return;
+ }
+ }
+ for (const [_, file] of Object.entries(files)) {
+ try {
+ if (dir) {
+ await DownloadNew(dir, file);
+ } else {
+ await DownloadOld(file);
+ }
+ success++;
+ } catch (e) {
+ console.error(`下载失败: ${file.fileName}`, e);
+ toast.error(`出现错误: ${e}`);
+ }
+ }
+ if (success === filesLength) {
+ toast.success(`成功下载: ${success}/${filesLength}首`);
+ } else {
+ toast.error(`成功下载: ${success}/${filesLength}首`);
+ }
+ };
+
+ return (
+
+ );
+}
+
+async function DownloadNew(dir: FileSystemDirectoryHandle, file: DecryptedAudioFile) {
+ const fileHandle = await dir.getFileHandle(file.cleanName + '.' + file.ext, { create: true });
+ const writable = await fileHandle.createWritable();
+ await fetch(file.decrypted).then((res) => res.body?.pipeTo(writable));
+}
+
+async function DownloadOld(file: DecryptedAudioFile) {
+ const a = document.createElement('a');
+ a.href = file.decrypted;
+ a.download = file.cleanName + '.' + file.ext;
+ document.body.append(a);
+ a.click();
+ a.remove();
+}