diff --git a/packages/main/src/controller/DownloadController.ts b/packages/main/src/controller/DownloadController.ts index e31a9b1a..5655ac5c 100644 --- a/packages/main/src/controller/DownloadController.ts +++ b/packages/main/src/controller/DownloadController.ts @@ -28,6 +28,11 @@ export default class DownloadController implements Controller { private readonly mainWindow: MainWindow, ) {} + @handle("show-download-dialog") + async showDownloadDialog(e: IpcMainEvent, data: DownloadItem) { + this.mainWindow.window?.webContents.send("show-download-dialog", data); + } + @handle("add-download-item") async addDownloadItem(e: IpcMainEvent, video: DownloadItem) { const item = await this.videoRepository.addVideo(video); diff --git a/packages/main/src/preload.ts b/packages/main/src/preload.ts index eee889f9..da5e4170 100644 --- a/packages/main/src/preload.ts +++ b/packages/main/src/preload.ts @@ -74,7 +74,7 @@ const electronApi = { webviewShow: (): Promise => ipcRenderer.invoke("webview-show"), webviewUrlContextMenu: (): Promise => ipcRenderer.invoke("webview-url-contextmenu"), - downloadNow: (video: DownloadItem): Promise => + downloadNow: (video: Omit): Promise => ipcRenderer.invoke("download-now", video), combineToHomePage: (store: BrowserStore): Promise => ipcRenderer.invoke("combine-to-home-page", store), @@ -87,10 +87,12 @@ const electronApi = { ipcRenderer.invoke("set-shared-state", state), setUserAgent: (isMobile: boolean): Promise => ipcRenderer.invoke("webview-change-user-agent", isMobile), - downloadItem: (data: { name: string; url: string; type: string }) => + downloadItem: (data: Omit) => ipcRenderer.invoke("add-download-item", data), getDownloadLog: (id: number): Promise => ipcRenderer.invoke("get-download-log", id), + showDownloadDialog: (data: Omit) => + ipcRenderer.invoke("show-download-dialog", data), }; contextBridge.exposeInMainWorld(apiKey, electronApi); diff --git a/packages/main/src/services/WebviewService.ts b/packages/main/src/services/WebviewService.ts index 84b88649..1a57edaa 100644 --- a/packages/main/src/services/WebviewService.ts +++ b/packages/main/src/services/WebviewService.ts @@ -128,11 +128,6 @@ export default class WebviewService { } setBounds(bounds: Electron.Rectangle): void { - // if (isWin) { - // bounds.y = bounds.y + 30; - // } else { - // bounds.y = bounds.y - 0; - // } this.view.setBounds(bounds); } @@ -140,6 +135,7 @@ export default class WebviewService { const canGoBack = this.webContents.canGoBack(); try { + this.webContents.stop(); await this.webContents.loadURL(url || ""); } catch (err: unknown) { this.logger.error("加载 url 时出现错误: ", err); @@ -165,6 +161,7 @@ export default class WebviewService { } async goHome() { + this.webContents.stop(); this.webContents.clearHistory(); } diff --git a/packages/main/src/windows/MainWindow.ts b/packages/main/src/windows/MainWindow.ts index af987584..a0db401a 100644 --- a/packages/main/src/windows/MainWindow.ts +++ b/packages/main/src/windows/MainWindow.ts @@ -141,10 +141,13 @@ export default class MainWindow extends Window { this.send("download-stop", id); }; - receiveMessage = async (id: number, message: any) => { + receiveMessage = async (id: number, message: string) => { // 将日志写入数据库中 await this.videoRepository.appendDownloadLog(id, message); - this.send("download-message", id, message); + const showTerminal = this.store.get("showTerminal"); + if (showTerminal) { + this.send("download-message", id, message); + } }; send(channel: string, ...args: any[]) { diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 4f6c4406..479b2258 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -13,7 +13,6 @@ "dependencies": { "jquery": "^3.7.1", "lit": "^3.1.2", - "mitt": "^3.0.1", "nanoid": "^5.0.4" }, "devDependencies": { diff --git a/packages/plugin/src/components/OneButton.ts b/packages/plugin/src/components/BilibiliButton.ts similarity index 85% rename from packages/plugin/src/components/OneButton.ts rename to packages/plugin/src/components/BilibiliButton.ts index ae3c9c0b..560df19c 100644 --- a/packages/plugin/src/components/OneButton.ts +++ b/packages/plugin/src/components/BilibiliButton.ts @@ -1,10 +1,10 @@ import { LitElement, html, css } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { BILIBILI_DOWNLOAD_BUTTON, downloadItem } from "../helper"; +import { BILIBILI_DOWNLOAD_BUTTON, showDownloadDialog } from "../helper"; import $ from "jquery"; @customElement("one-button") -export class OneButton extends LitElement { +export class BilibiliButton extends LitElement { @property({ type: Number }) index = 0; @@ -39,7 +39,7 @@ export class OneButton extends LitElement { const videoImage = $(BILIBILI_DOWNLOAD_BUTTON).eq(this.index); const url = videoImage.attr("href") || ""; const name = videoImage.parent().find(".bili-video-card__info--tit").text(); - downloadItem({ name, url, type: "bilibili" }); + showDownloadDialog({ name, url, type: "bilibili" }); } render() { diff --git a/packages/plugin/src/components/FloatButton.ts b/packages/plugin/src/components/FloatButton.ts index 40c4fa8c..7ac44bbe 100644 --- a/packages/plugin/src/components/FloatButton.ts +++ b/packages/plugin/src/components/FloatButton.ts @@ -1,7 +1,7 @@ import { LitElement, html, css } from "lit"; import { customElement, property } from "lit/decorators.js"; import logo from "../assets/logo.png"; -import { addIpcListener, downloadItem } from "../helper"; +import { addIpcListener, showDownloadDialog } from "../helper"; @customElement("float-button") export class FloatButton extends LitElement { @@ -52,7 +52,7 @@ export class FloatButton extends LitElement { e.preventDefault(); e.stopPropagation(); - downloadItem({ + showDownloadDialog({ name: this.data.name, url: this.data.url, type: this.data.type, diff --git a/packages/plugin/src/components/index.ts b/packages/plugin/src/components/index.ts index 4dd8d899..a511b76b 100644 --- a/packages/plugin/src/components/index.ts +++ b/packages/plugin/src/components/index.ts @@ -1,2 +1,2 @@ -export { OneButton } from "./OneButton"; +export { BilibiliButton } from "./BilibiliButton"; export { FloatButton } from "./FloatButton"; diff --git a/packages/plugin/src/helper/events.ts b/packages/plugin/src/helper/events.ts deleted file mode 100644 index f5f72a31..00000000 --- a/packages/plugin/src/helper/events.ts +++ /dev/null @@ -1,3 +0,0 @@ -import mitt from "mitt"; - -export const emitter = mitt(); diff --git a/packages/plugin/src/helper/index.ts b/packages/plugin/src/helper/index.ts index d8234afe..5267f5fa 100644 --- a/packages/plugin/src/helper/index.ts +++ b/packages/plugin/src/helper/index.ts @@ -1,7 +1,5 @@ import { nanoid } from "nanoid"; -export { emitter } from "./events"; - const eventMap = new Map(); const getIpcId = (func: any) => { @@ -28,11 +26,15 @@ export function removeIpcListener(eventName: string, func: any) { export interface Item { name: string; url: string; - type: string; + type: any; } export function downloadItem(item: Item) { window.electron.downloadItem(item); } +export function showDownloadDialog(item: Item) { + window.electron.showDownloadDialog(item); +} + export const BILIBILI_DOWNLOAD_BUTTON = ".bili-video-card__image--link"; diff --git a/packages/plugin/src/main.ts b/packages/plugin/src/main.ts index e3cd0ef5..bd6203a6 100644 --- a/packages/plugin/src/main.ts +++ b/packages/plugin/src/main.ts @@ -15,7 +15,7 @@ function bilibili() { const isAd = $(card).find(".bili-video-card__info--ad"); if (isAd.length) return; - const downloadButton = document.createElement("one-button"); + const downloadButton = document.createElement("bilibili-button"); downloadButton.index = index; card.appendChild(downloadButton); }); diff --git a/packages/plugin/src/types.ts b/packages/plugin/src/types.ts index 659fff20..5c691b24 100644 --- a/packages/plugin/src/types.ts +++ b/packages/plugin/src/types.ts @@ -1,4 +1,4 @@ -import { OneButton } from "./components"; +import { BilibiliButton } from "./components"; import { ElectronApi } from "../../main/types/preload"; export enum DownloadType { @@ -15,7 +15,7 @@ export interface WebSource { declare global { interface HTMLElementTagNameMap { - "one-button": OneButton; + "bilibili-button": BilibiliButton; } interface Window { electron: ElectronApi; diff --git a/packages/renderer/package.json b/packages/renderer/package.json index 2c784f30..4b19b34b 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -22,6 +22,7 @@ "dayjs": "^1.11.10", "i18next": "^23.8.2", "i18next-browser-languagedetector": "^7.2.0", + "localforage": "^1.10.0", "match-sorter": "^6.3.3", "nanoid": "^5.0.4", "react": "^18.2.0", diff --git a/packages/renderer/src/i18n/index.ts b/packages/renderer/src/i18n/index.ts index 117b6fbe..554e9616 100644 --- a/packages/renderer/src/i18n/index.ts +++ b/packages/renderer/src/i18n/index.ts @@ -70,7 +70,7 @@ i18n light: "Light", pleaseSelectTheme: "Please select theme", displayLanguage: "Display Language", - chinese: "Chinese", + chinese: "中文", english: "English", pleaseSelectLanguage: "Please select language", downloadPrompt: "Download Prompt", @@ -126,6 +126,13 @@ Referer: http://www.example.com`, exportLog: "Export Log", showTerminal: "Show Console", consoleOutput: "Console", + downloadNow: "Download Now", + addToDownloadList: "Add to Download List", + videoType: "Video Type", + pleaseSelectVideoType: "Please select video type", + numberOfEpisodes: "Number of episodes", + showNumberOfEpisodes: "Show number of episodes", + canUseMouseWheelToAdjust: "Can use mouse wheel to adjust", }, }, zh: { @@ -182,7 +189,7 @@ Referer: http://www.example.com`, pleaseSelectDownloadDir: "请选择视频下载目录", downloaderTheme: "下载器主题", followSystem: "跟随系统", - english: "英文", + english: "English", pleaseSelectLanguage: "请选择显示语言", downloadPrompt: "下载完成提示", browserSetting: "浏览器设置", @@ -239,6 +246,13 @@ Referer: http://www.example.com`, exportLog: "导出日志", showTerminal: "显示控制台", consoleOutput: "控制台", + downloadNow: "立即下载", + addToDownloadList: "添加到下载列表", + videoType: "视频类型", + pleaseSelectVideoType: "请选择视频类型", + numberOfEpisodes: "集数", + showNumberOfEpisodes: "显示集数", + canUseMouseWheelToAdjust: "可以使用鼠标滚轮调整", }, }, }, diff --git a/packages/renderer/src/nodes/SourceExtract/index.tsx b/packages/renderer/src/nodes/SourceExtract/index.tsx index 17653d97..47f9c59c 100644 --- a/packages/renderer/src/nodes/SourceExtract/index.tsx +++ b/packages/renderer/src/nodes/SourceExtract/index.tsx @@ -24,13 +24,19 @@ import { Space, Spin, } from "antd"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import PageContainer from "../../components/PageContainer"; import useElectron from "../../hooks/electron"; import { generateUrl, getFavIcon } from "../../utils"; import "./index.scss"; -import { ModalForm, ProFormText } from "@ant-design/pro-components"; +import { + ModalForm, + ProFormDigit, + ProFormSelect, + ProFormSwitch, + ProFormText, +} from "@ant-design/pro-components"; import { BrowserStatus, PageMode, @@ -41,11 +47,29 @@ import { import WebView from "../../components/WebView"; import { selectAppStore } from "../../store"; import { useTranslation } from "react-i18next"; +import { nanoid } from "nanoid"; +import localforage from "localforage"; +import { DownloadType } from "../../types"; interface SourceExtractProps { page?: boolean; } +// 下载表单 +interface DownloadForm { + teleplay: boolean; + type: DownloadType; + name: string; + url: string; + numberOfEpisodes: number; +} + +// 集数 +interface NumberOfEpisodes { + teleplay: boolean; + numberOfEpisodes: number; +} + const SourceExtract: React.FC = ({ page = false }) => { const { getFavorites, @@ -62,6 +86,8 @@ const SourceExtract: React.FC = ({ page = false }) => { setUserAgent, webviewUrlContextMenu, getAppStore: ipcGetAppStore, + downloadNow, + addDownloadItem, } = useElectron(); const { t } = useTranslation(); const dispatch = useDispatch(); @@ -71,6 +97,10 @@ const SourceExtract: React.FC = ({ page = false }) => { const [hoverId, setHoverId] = useState(-1); const store = useSelector(selectBrowserStore); const appStore = useSelector(selectAppStore); + const [form] = Form.useForm(); + const [modalShow, setModalShow] = useState(false); + const [modalReadyShow, setModalReadyShow] = useState(false); + const sessionId = useRef(""); const curIsFavorite = favoriteList.find((item) => item.url === store.url); @@ -80,27 +110,34 @@ const SourceExtract: React.FC = ({ page = false }) => { }, []); const loadUrl = async (url: string) => { + const id = nanoid(); + sessionId.current = id; try { dispatch( setBrowserStore({ + url: "", mode: PageMode.Browser, status: BrowserStatus.Loading, }), ); await webviewLoadURL(url); - dispatch( - setBrowserStore({ - url: url, - status: BrowserStatus.Loaded, - }), - ); + if (sessionId.current === id) { + dispatch( + setBrowserStore({ + url: url, + status: BrowserStatus.Loaded, + }), + ); + } } catch (err) { - dispatch( - setBrowserStore({ - status: BrowserStatus.Failed, - errMsg: (err as any).message, - }), - ); + if (sessionId.current === id) { + dispatch( + setBrowserStore({ + status: BrowserStatus.Failed, + errMsg: (err as any).message, + }), + ); + } } }; @@ -211,15 +248,29 @@ const SourceExtract: React.FC = ({ page = false }) => { dispatch(setBrowserStore(state)); }, []); + const onShowDownloadDialog = async (e: any, data: DownloadForm) => { + const noe = await localforage.getItem("numberOfEpisodes"); + form.setFieldsValue({ + type: data.type, + url: data.url, + name: data.name, + teleplay: noe?.teleplay || false, + numberOfEpisodes: noe?.numberOfEpisodes || 1, + }); + setModalShow(true); + }; + useEffect(() => { const prevTitle = document.title; addIpcListener("webview-dom-ready", onDomReady); addIpcListener("favorite-item-event", onFavoriteEvent); + addIpcListener("show-download-dialog", onShowDownloadDialog); return () => { document.title = prevTitle; removeIpcListener("webview-dom-ready", onDomReady); removeIpcListener("favorite-item-event", onFavoriteEvent); + removeIpcListener("show-download-dialog", onShowDownloadDialog); }; }, []); @@ -268,7 +319,8 @@ const SourceExtract: React.FC = ({ page = false }) => { > - {store.mode === PageMode.Browser && store.status === BrowserStatus.Loading ? ( + {store.mode === PageMode.Browser && + store.status === BrowserStatus.Loading ? ( @@ -330,6 +382,8 @@ const SourceExtract: React.FC = ({ page = false }) => { let content =
; if (store.status === BrowserStatus.Loading) { content = ; + } else if (modalReadyShow || modalShow) { + content = <>; } else if (store.status === BrowserStatus.Failed) { content = ( @@ -480,6 +534,132 @@ const SourceExtract: React.FC = ({ page = false }) => { ); }; + const confirmDownload = async (now?: boolean) => { + try { + const data = form.getFieldsValue(); + const { numberOfEpisodes, teleplay, ...item } = data; + + if (teleplay) { + await localforage.setItem("numberOfEpisodes", { + numberOfEpisodes, + teleplay, + }); + } + + if (teleplay) { + item.name = `${item.name} - 第${numberOfEpisodes}集`; + } + + if (now) { + await downloadNow(item); + } else { + await addDownloadItem(item); + } + + // 提交成功后关闭弹窗 + setModalShow(false); + } catch (e) { + message.error((e as any).message); + } + }; + + // 渲染表单 + const renderModalForm = () => { + return ( + + open={modalShow} + title={t("newDownload")} + onOpenChange={setModalShow} + form={form} + modalProps={{ + destroyOnClose: true, + afterOpenChange: setModalReadyShow, + okText: t("downloadNow"), + styles: { + body: { + paddingTop: 5, + }, + }, + }} + submitter={{ + render: (props, [, okButton]) => { + return [ + , + okButton, + ]; + }, + }} + submitTimeout={2000} + onFinish={async () => { + await confirmDownload(true); + return true; + }} + labelCol={{ style: { width: "120px" } }} + layout="horizontal" + width={550} + labelAlign={"left"} + colon={false} + > + + + + {(form) => { + if (form.getFieldValue("teleplay")) { + return ( + + ); + } + }} + + + + + + ); + }; + return ( = ({ page = false }) => { ? renderBrowserPanel() : renderFavoriteList()} + {renderModalForm()} ); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac277c0e..ad5d6e35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -319,9 +319,6 @@ importers: lit: specifier: ^3.1.2 version: 3.1.2 - mitt: - specifier: ^3.0.1 - version: 3.0.1 nanoid: specifier: ^5.0.4 version: 5.0.4 @@ -407,6 +404,9 @@ importers: i18next-browser-languagedetector: specifier: ^7.2.0 version: 7.2.0 + localforage: + specifier: ^1.10.0 + version: 1.10.0 match-sorter: specifier: ^6.3.3 version: 6.3.3 @@ -7806,6 +7806,12 @@ packages: type-check: 0.4.0 dev: true + /lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + dependencies: + immediate: 3.0.6 + dev: false + /lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} dependencies: @@ -7886,6 +7892,12 @@ packages: pkg-types: 1.0.3 dev: true + /localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + dependencies: + lie: 3.1.1 + dev: false + /locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -8323,10 +8335,6 @@ packages: yallist: 4.0.0 dev: true - /mitt@3.0.1: - resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - dev: false - /mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}