Skip to content

Commit

Permalink
feat: ✨ 为 PWA 增加下载按钮
Browse files Browse the repository at this point in the history
  • Loading branch information
hymbz committed Feb 4, 2024
1 parent 0209c36 commit 41235a5
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 87 deletions.
8 changes: 7 additions & 1 deletion src/components/Toast/Toaster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { ToastItem } from './ToastItem';

import classes from './index.module.css';

export const [ref, setRef] = createSignal<HTMLElement>();

export const Toaster: Component = () => {
const [visible, setVisible] = createSignal(
document.visibilityState === 'visible',
Expand All @@ -21,7 +23,11 @@ export const Toaster: Component = () => {
});

return (
<div class={classes.root} data-paused={visible() ? undefined : ''}>
<div
ref={setRef}
class={classes.root}
data-paused={visible() ? undefined : ''}
>
<For each={store.list}>{(id) => <ToastItem {...store.map[id]} />}</For>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Toast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { css as style } from './index.module.css';

export const ToastStyle = style;

export { Toaster } from './Toaster';
export { Toaster, ref } from './Toaster';
export { toast } from './toast';

export interface Toast {
Expand Down
78 changes: 78 additions & 0 deletions src/components/useComponents/DownloadButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import MdFileDownload from '@material-design-icons/svg/round/file_download.svg';

import { zipSync, type Zippable } from 'fflate';
import { createMemo, createSignal } from 'solid-js';
import { saveAs } from 'helper';
import { request } from 'helper/request';
import { t } from 'helper/i18n';
import { store } from '../Manga';
import { IconButton } from '../IconButton';
import { toast } from './Toast';

/** 下载按钮 */
export const DownloadButton = () => {
const [statu, setStatu] = createSignal('button.download');

const getFileExt = (url: string) => url.split('.').pop();

const handleDownload = async () => {
const fileData: Zippable = {};

const downImgList = store.imgList.map((img) =>
img.translationType === 'show'
? `${img.translationUrl!}#.${getFileExt(img.src)}`
: img.src,
);
const imgIndexNum = `${downImgList.length}`.length;

for (let i = 0; i < downImgList.length; i += 1) {
setStatu(`${i}/${downImgList.length}`);
const index = `${i}`.padStart(imgIndexNum, '0');

let data: ArrayBuffer;
let fileName: string;

const url = downImgList[i];
if (url.startsWith('blob:')) {
const res = await fetch(url);
const blob = await res.blob();
data = await blob.arrayBuffer();
const fileExt = blob.type.split('/')[1];
fileName = `${index}.${fileExt}`;
} else {
const fileExt = getFileExt(url) ?? 'jpg';
fileName = `${index}.${fileExt}`;
try {
const res = await request<ArrayBufferLike>(url, {
responseType: 'arraybuffer',
errorText: `${t('alert.download_failed')}: ${fileName}`,
});
data = res.response;
} catch (error) {
fileName = `${index} - ${t('alert.download_failed')}.${fileExt}`;
}
}

fileData[fileName] = new Uint8Array(data!);
}

setStatu('button.packaging');
const zipped = zipSync(fileData, {
level: 0,
comment: window.location.href,
});
saveAs(new Blob([zipped]), `${document.title}.zip`);
setStatu('button.download_completed');
toast.success(t('button.download_completed'));
};

const tip = createMemo(
() => t(statu()) || `${t('button.downloading')} - ${statu()}`,
);

return (
<IconButton tip={tip()} onClick={handleDownload}>
<MdFileDownload />
</IconButton>
);
};
72 changes: 7 additions & 65 deletions src/components/useComponents/Manga.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import MdFileDownload from '@material-design-icons/svg/round/file_download.svg';
import MdClose from '@material-design-icons/svg/round/close.svg';

import fflate from 'fflate';
import { createMemo, createSignal } from 'solid-js';

import { t } from 'helper/i18n';
import { createStore } from 'solid-js/store';
import { createEffectOn, createRootMemo } from 'helper/solidJs';
import { DownloadButton } from './DownloadButton';
import { IconButton, IconButtonStyle } from '../IconButton';
import type { MangaProps } from '../Manga';
import { buttonListDivider, MangaStyle, Manga, store } from '../Manga';
import { request } from '../../helper/request';
import { saveAs } from '../../helper';
import { mountComponents } from './helper';
import { toast } from './Toast';

export { store };
export { showPageList } from '../Manga/actions';
Expand Down Expand Up @@ -92,59 +86,11 @@ export const useManga = async (initProps?: Partial<UseMangaProps>) => {
}
});

/** 下载按钮 */
const DownloadButton = () => {
const [statu, setStatu] = createSignal('button.download');

const getFileExt = (url: string) => url.split('.').pop();

const handleDownload = async () => {
const fileData: fflate.Zippable = {};

const downImgList = store.imgList.map((img) =>
img.translationType === 'show'
? `${img.translationUrl!}#.${getFileExt(img.src)}`
: img.src,
);
const imgIndexNum = `${downImgList.length}`.length;

for (let i = 0; i < downImgList.length; i += 1) {
setStatu(`${i}/${downImgList.length}`);
const index = `${i}`.padStart(imgIndexNum, '0');
const fileExt = getFileExt(downImgList[i]) ?? 'jpg';
const fileName = `${index}.${fileExt}`;
try {
const res = await request<ArrayBuffer>(downImgList[i], {
responseType: 'arraybuffer',
});
fileData[fileName] = new Uint8Array(res.response);
} catch (error) {
toast.error(`${fileName} ${t('alert.download_failed')}`);
fileData[`${index} - ${t('alert.download_failed')}.${fileExt}`] =
new Uint8Array();
}
}

setStatu('button.packaging');
const zipped = fflate.zipSync(fileData, {
level: 0,
comment: window.location.href,
});
saveAs(new Blob([zipped]), `${document.title}.zip`);
setStatu('button.download_completed');
toast.success(t('button.download_completed'));
};

const tip = createMemo(
() => t(statu()) || `${t('button.downloading')} - ${statu()}`,
);

return (
<IconButton tip={tip()} onClick={handleDownload}>
<MdFileDownload />
</IconButton>
);
};
const ExitButton = () => (
<IconButton tip={t('button.exit')} onClick={() => props.onExit?.()}>
<MdClose />
</IconButton>
);

setProps({
onExit: () => setProps('show', false),
Expand All @@ -155,11 +101,7 @@ export const useManga = async (initProps?: Partial<UseMangaProps>) => {
...list,
// 再在最下面添加分隔栏和退出按钮
buttonListDivider,
() => (
<IconButton tip={t('button.exit')} onClick={() => props.onExit?.()}>
<MdClose />
</IconButton>
),
ExitButton,
];
},
});
Expand Down
4 changes: 2 additions & 2 deletions src/components/useComponents/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { mountComponents } from './helper';
import { ToastStyle, Toaster, toast as _toast } from '../Toast';
import { ToastStyle, Toaster, toast as _toast, ref } from '../Toast';

let dom: HTMLDivElement;

const init = () => {
if (dom) return;
if (dom || ref()) return;

// 提前挂载漫画节点,防止 toast 没法显示在漫画上层
if (!document.getElementById('comicRead')) {
Expand Down
25 changes: 25 additions & 0 deletions src/pwa/src/handleButtonList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import MdClose from '@material-design-icons/svg/round/close.svg';

import { t } from 'helper/i18n';
import { IconButton } from '../../components/IconButton';
import type { MangaProps } from '../../components/Manga';
import { buttonListDivider } from '../../components/Manga';
import { DownloadButton } from '../../components/useComponents/DownloadButton';
import { handleExit } from './store';

const ExitButton = () => (
<IconButton tip={t('button.exit')} onClick={handleExit}>
<MdClose />
</IconButton>
);

export const editButtonList: MangaProps['editButtonList'] = (list) => {
// 在设置按钮上方放置下载按钮
list.splice(-1, 0, DownloadButton);
return [
...list,
// 再在最下面添加分隔栏和退出按钮
buttonListDivider,
ExitButton,
];
};
22 changes: 4 additions & 18 deletions src/pwa/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
/* eslint-disable solid/no-innerhtml */
import MdClose from '@material-design-icons/svg/round/close.svg';

import { Show, type Component } from 'solid-js';
import { pwaInstallHandler } from 'pwa-install-handler';
import { directoryOpen, fileOpen } from 'browser-fs-access';
import { parse as parseMd } from 'marked';

import { setInitLang, t } from 'helper/i18n';
import type { MangaProps } from '../../components/Manga';
import { Manga, buttonListDivider } from '../../components/Manga';
import { IconButton } from '../../components/IconButton';
import { Manga } from '../../components/Manga';
import { Toaster, toast } from '../../components/Toast';

import { store, handleExit, loadNewImglist, _setState } from './store';
import { FileSystemToFile, imgExtension } from './helper';
import { handleDrag } from './handleDrag';
import { editButtonList } from './handleButtonList';
import { DownloadButton, loadUrl } from './DownloadButton';

import classes from './index.module.css';
Expand Down Expand Up @@ -53,17 +48,6 @@ window.launchQueue?.setConsumer(async (launchParams) => {
loadNewImglist(await FileSystemToFile(launchParams.files));
});

// 增加退出按钮
const editButtonList: MangaProps['editButtonList'] = (list) => [
...list,
buttonListDivider,
() => (
<IconButton tip={t('button.exit')} onClick={handleExit}>
<MdClose />
</IconButton>
),
];

// 支持粘贴 url
window.addEventListener('paste', (event) =>
loadUrl(event.clipboardData?.getData('text/plain')),
Expand All @@ -84,6 +68,7 @@ export const Root: Component = () => (
<div ref={(e) => handleDrag(e)} class={classes.root}>
<div class={classes.main} data-drag={store.dragging}>
<div class={classes.body}>
{/* eslint-disable-next-line solid/no-innerhtml */}
<div innerHTML={parseMd(t('pwa.tip_md')) as string} />

<span style={{ 'margin-top': '1em' }}>
Expand All @@ -105,6 +90,7 @@ export const Root: Component = () => (
class={classes.installTip}
classList={{ [classes.hide]: !!store.hiddenInstallTip }}
>
{/* eslint-disable-next-line solid/no-innerhtml */}
<div innerHTML={parseMd(t('pwa.install_md')) as string} />
<div style={{ 'text-align': 'center' }}>
<button type="button" on:click={pwaInstallHandler.install}>
Expand Down

0 comments on commit 41235a5

Please sign in to comment.