Skip to content

Commit

Permalink
feat: progress
Browse files Browse the repository at this point in the history
  • Loading branch information
Oc1S committed Jun 25, 2024
1 parent 1b717a2 commit dfdc4c2
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 53 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ module.exports = {
rules: {
'simple-import-sort/imports': 'warn',
'simple-import-sort/exports': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'prefer-const': 'off',
'react/react-in-jsx-scope': 'off',
'import/no-unresolved': 'off',
'react-hooks/exhaustive-deps': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'off',
'react/self-closing-comp': 'warn',
},
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ A Chrome Ehentai Helper Plugin.
✅Total Page
Page Range
✅Sussess Progress
Error Retry
Auto Height
✅Error Retry


> Inspired by [EHentaiChromeExtension
Expand Down
7 changes: 4 additions & 3 deletions pages/options/src/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const Row = ({ label, content }: Record<'label' | 'content', React.ReactNode>) =
};

const TextInput = ({ className, ...rest }: React.InputHTMLAttributes<HTMLInputElement>) => (
<input type="text" className={`border-b border-primary bg-transparent text-primary ${className}`} {...rest} />
<input type="text" className={`border-primary text-primary border-b bg-transparent ${className}`} {...rest} />
);

const Options: FC = () => {
Expand Down Expand Up @@ -186,16 +186,17 @@ const Options: FC = () => {
}>
<Radio value="[index]">Index</Radio>
<Radio value="[name]">Name</Radio>
<Radio value="[index]_[total]">Index_Total</Radio>
</RadioGroup>
),
},
};

return (
<div className="relative flex flex-col gap-4 items-center">
<div className="relative flex flex-col items-center gap-4">
<Toast visible={!!status}>{status}</Toast>
{/* table */}
<div className="flex flex-col bg-content1 rounded-lg p-4 gap-4">
<div className="bg-content1 flex flex-col gap-4 rounded-lg p-4">
{Object.keys(formItemMap).map(key => (
<Row key={key} label={formItemMap[key].label} content={formItemMap[key].content} />
))}
Expand Down
108 changes: 73 additions & 35 deletions pages/popup/src/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import {
withErrorBoundary,
withSuspense,
} from '@ehentai-helper/shared';
import { Button, Card, CardBody, Link, Spinner, Tab, Tabs } from '@nextui-org/react';
import { Button, Card, CardBody, Link, Progress, Spinner, Tab, Tabs } from '@nextui-org/react';
import axios from 'axios';
import clsx from 'clsx';
import { useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';

import { PageSelector } from './components';
import DownloadTable from './components/Table';
import { DownloadContext } from './Context';
import { useDownload } from './hooks';
Expand All @@ -36,8 +37,8 @@ enum StatusEnum {
Loading,
OtherPage,
EHentaiOther,
BeforeDownload,
Fail,
BeforeDownload,
Downloading,
DownLoadSuccess,
}
Expand All @@ -57,6 +58,7 @@ const Popup = () => {
numPages: 0,
totalImages: 0,
});

const { totalImages } = galleryPageInfo;
const finishedList = downloadList.filter(item => item.state === 'complete');

Expand All @@ -76,24 +78,46 @@ const Popup = () => {
return urls;
};

const [range, setRange] = useState<[number, number]>([1, galleryPageInfo.totalImages]);
useEffect(() => {
setRange([1, galleryPageInfo.totalImages]);
}, [galleryInfo]);

const [startIndex, endIndex] = range;
const [start, end] = useMemo(() => {
const start = {
indexOfPage: (startIndex % galleryPageInfo.imagesPerPage) - 1,
page: ~~(startIndex / galleryPageInfo.imagesPerPage),
};
const end = {
indexOfPage: (endIndex % galleryPageInfo.imagesPerPage) - 1,
page: ~~(endIndex / galleryPageInfo.imagesPerPage),
};
return [start, end];
}, [galleryPageInfo.imagesPerPage, startIndex, endIndex]);

/* 获取单张图片 */
const processImagePage = async (url: string, pageIndex: number, imageIndex: number) => {
const currentIndex = pageIndex * galleryPageInfo.imagesPerPage + imageIndex + 1;
if (currentIndex < startIndex || currentIndex > endIndex) return;
const res = await axios.get(url);
const responseText = res.data;
const doc = htmlStr2DOM(responseText);
let imageUrl = (doc.getElementById('img') as HTMLImageElement).src;
if (configRef.current.saveOriginalImages) {
const originalImage = (doc.getElementById('i7')?.childNodes[3] as HTMLAnchorElement).href;
const originalImage = (doc.getElementById('i7')?.childNodes?.[3] as HTMLAnchorElement)?.href;
imageUrl = originalImage ?? imageUrl;
}
chrome.downloads.download({ url: imageUrl }, id => {
imageIdMap.set(id, pageIndex * galleryPageInfo.imagesPerPage + imageIndex + 1);
imageIdMap.set(id, currentIndex);
});
};

/* 获取gallery整页所有图片 */
const processGalleryPage = async (url: string, pageIndex: number) => {
const { data: responseText } = await axios.get(url);
const processGalleryPage = async (pageIndex: number) => {
if (pageIndex < start.page || pageIndex > end.page) return;
const pageUrl = `${galleryFrontPageUrl.current}?p=${pageIndex}`;
const { data: responseText } = await axios.get(pageUrl);
const imagePageUrls = extractImagePageUrls(responseText);
processImagePage(imagePageUrls[0], pageIndex, 0); // Start immediately.
let imageIndex = 1;
Expand All @@ -110,25 +134,25 @@ const Popup = () => {

/** 开启下载图片 */
const downloadAllImages = () => {
processGalleryPage(galleryFrontPageUrl.current, 0); // Start immediately.
let pageIndex = 1;
// 下载该页图片后,继续下载下一页
let pageIndex = start.page;
processGalleryPage(pageIndex); // Start immediately.
pageIndex++;
const pageInterval = setInterval(() => {
if (pageIndex === galleryPageInfo.numPages) {
clearInterval(pageInterval);
return;
}
processGalleryPage(galleryFrontPageUrl.current + '?p=' + pageIndex, pageIndex);
processGalleryPage(pageIndex);
pageIndex++;
}, configRef.current.downloadInterval * galleryPageInfo.imagesPerPage);
};

/**
* 获取页码信息
*/
const extractNumGalleryPages = (html: string) => {
const extractGalleryPageInfo = (html: string) => {
const doc = htmlStr2DOM(html);
const pageInfoStr = doc.querySelector('gpc')?.innerHTML || '';
const pageInfoStr = doc.querySelector('.gpc')?.innerHTML || '';
const res = /Showing 1 - (\d+) of (\d*,*\d+) images/.exec(pageInfoStr);
if (!res) return;
const pageInfo = {
Expand Down Expand Up @@ -162,6 +186,12 @@ const Popup = () => {
setDownloadList(prev => prev.map(item => (item.id === id ? { ...item, ...newVal } : item)));
};

useEffect(() => {
if (status === StatusEnum.Downloading && galleryPageInfoRef.current.totalImages === finishedList.length) {
setStatus(StatusEnum.DownLoadSuccess);
}
}, [status, downloadList]);

// Save to the corresponding folder and rename files.
const handleDeterminingFilename: Parameters<typeof chrome.downloads.onDeterminingFilename.addListener>[0] = (
downloadItem,
Expand All @@ -170,23 +200,24 @@ const Popup = () => {
if (downloadItem.byExtensionName !== EXTENSION_NAME) return;
const { id } = downloadItem;
let { filename } = downloadItem;
const { intermediateDownloadPath, fileNameRule, filenameConflictAction: conflictAction } = configRef.current;
const name = filename.substring(0, filename.lastIndexOf('.') + 1);
const fileType = filename.substring(filename.lastIndexOf('.') + 1);
// Metadata.
if (fileType === 'txt') {
const { url } = downloadItem;
// 'name' is the first key of info file.
const isInfoFile = url.substring(url.indexOf(',') + 1).startsWith('name');
filename = configRef.current.intermediateDownloadPath + '/' + isInfoFile ? 'info.txt' : 'tags.txt';
filename = `${intermediateDownloadPath}/${isInfoFile ? 'info.txt' : 'tags.txt'}`;
} else {
filename = `${configRef.current.intermediateDownloadPath}/${configRef.current.fileNameRule
filename = `${intermediateDownloadPath}/${fileNameRule
.replace('[index]', String(imageIdMap.get(id)))
.replace('[name]', name)
.replace('[total]', `${galleryPageInfoRef.current.totalImages}`)}.${fileType}`;
}
suggest({
filename: `${filename}`,
conflictAction: configRef.current.filenameConflictAction,
filename,
conflictAction,
});
};

Expand Down Expand Up @@ -222,27 +253,30 @@ const Popup = () => {
case StatusEnum.Downloading:
return <>🤗Please do NOT close the extension popup page before ALL download tasks start.</>;
case StatusEnum.DownLoadSuccess:
return 'Download success';
return <div>Congrats! Download completed.</div>;
case StatusEnum.Fail:
return 'Failed to fetch data from the server, please retry later.';
}
};
const progress = (
<>
<div className="flex">
{finishedList.length > 0 && (
<div className="flex flex-col items-center justify-center">
<div>Finished /</div>
<div>{finishedList.length} /</div>
<div className="flex flex-col items-center gap-1">
{status >= StatusEnum.BeforeDownload && (
<div className="flex items-center">
<div>Total Page:</div>
<div>{totalImages}</div>
</div>
)}
<div className="flex flex-col items-center justify-center">
<div>Total Page</div>
<div>{totalImages}</div>
</div>
{finishedList.length > 0 && (
<Progress
aria-label="Loading..."
value={finishedList.length}
minValue={0}
maxValue={endIndex - startIndex + 1}
className="w-[200px]"
/>
)}
</div>

{finishedList.length > 0 && finishedList.length === totalImages && <div>Congrats! Download completed.</div>}
</>
);

Expand All @@ -254,16 +288,16 @@ const Popup = () => {
chrome.storage.sync.get(defaultConfig, async items => {
configRef.current = items as typeof configRef.current;
galleryFrontPageUrl.current = url.substring(0, url.lastIndexOf('/') + 1);
const { data: responseText } = await axios.get(galleryFrontPageUrl.current).catch(() => ({
const { data: htmlStr } = await axios.get(galleryFrontPageUrl.current).catch(() => ({
data: '',
}));
if (!responseText) {
if (!htmlStr) {
setStatus(StatusEnum.Fail);
return;
}
extractNumGalleryPages(responseText);
galleryInfo = extractGalleryInfo(responseText);
galleryTags = extractGalleryTags(responseText);
extractGalleryPageInfo(htmlStr);
galleryInfo = extractGalleryInfo(htmlStr);
galleryTags = extractGalleryTags(htmlStr);
configRef.current.intermediateDownloadPath += removeInvalidCharFromFilename(galleryInfo.name);
setStatus(StatusEnum.BeforeDownload);
setIsBtnVisible(true);
Expand Down Expand Up @@ -295,8 +329,12 @@ const Popup = () => {
<div className="flex flex-col items-center justify-center gap-4 p-2">
<div>{renderStatus()}</div>
{progress}

{[StatusEnum.BeforeDownload].includes(status) && (
<div className="flex flex-col items-center">
<div className="fixed bottom-40 flex flex-col items-center">
{range[1] > 0 && (
<PageSelector range={range} maxValue={totalImages} onChangeEnd={setRange as any} />
)}
<Button
color="primary"
hidden={isBtnVisible}
Expand Down
23 changes: 11 additions & 12 deletions pages/popup/src/components/PageSelector/index.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import { Slider } from '@nextui-org/react';
import { Slider, SliderProps } from '@nextui-org/react';
import clsx from 'clsx';
import { FC } from 'react';

type PageSelectorProps = {
range: [number, number];
};
} & SliderProps;

export const PageSelector: FC<PageSelectorProps> = ({ range }) => {
export const PageSelector: FC<PageSelectorProps> = ({ range, ...rest }) => {
return (
<Slider
size="lg"
label="Page Range"
maxValue={range[1]}
minValue={1}
step={1}
defaultValue={range}
classNames={{
base: 'max-w-md gap-3',
filler: 'bg-gradient-to-r from-pink-300 to-cyan-300 dark:from-pink-600 dark:to-cyan-800',
base: 'w-60 gap-3',
filler: 'bg-gradient-to-r from-secondary to-primary h-4',
track: 'border-x-4 h-4',
}}
renderThumb={({ index, ...props }) => (
<div
{...props}
className="bg-background border-small border-default-200 dark:border-default-400/50 shadow-medium group top-1/2 cursor-grab rounded-full p-1 data-[dragging=true]:cursor-grabbing">
className="bg-background border-small border-default-400/50 shadow-medium group top-1/2 cursor-grab rounded-full p-0.5 data-[dragging=true]:cursor-grabbing">
<span
className={clsx(
'shadow-small group-data-[dragging=true]:scale-80 block h-5 w-5 rounded-full bg-gradient-to-br transition-transform',
index === 0
? 'from-pink-200 to-pink-500 dark:from-pink-400 dark:to-pink-600'
: 'from-cyan-200 to-cyan-600 dark:from-cyan-600 dark:to-cyan-800'
'shadow-small bg-primary block h-4 w-4 rounded-full transition-transform group-data-[dragging=true]:scale-95',
index === 0 && 'bg-secondary'
)}
/>
</div>
)}
{...rest}
/>
);
};
1 change: 1 addition & 0 deletions pages/popup/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './PageSelector';
export * from './Table';

0 comments on commit dfdc4c2

Please sign in to comment.