diff --git a/.gitignore b/.gitignore index 346ac8c..c993fca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist-ssr *.local .idea .bin +.vscode yarn.lock yarn-error.log diff --git a/package.json b/package.json index d14f356..0384101 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "localforage": "^1.9.0", "m3u8-parser": "^4.7.0", "moment": "^2.29.1", + "nanoid": "^3.1.30", "prop-types": "^15.7.2", "qs": "^6.10.1", "re-resizable": "^6.9.1", diff --git a/src/main/browserView.ts b/src/main/browserView.ts index 525b3b6..642bb3d 100644 --- a/src/main/browserView.ts +++ b/src/main/browserView.ts @@ -3,6 +3,7 @@ import { BrowserView } from "electron"; import { log } from "main/utils"; import { SourceUrl } from "types/common"; import { webviewPartition, Windows } from "main/variables"; +import { nanoid } from "nanoid"; const createBrowserView = (session: Electron.Session): void => { const browserWindow = windowManager.get(Windows.BROWSER_WINDOW); @@ -46,6 +47,7 @@ const createBrowserView = (session: Electron.Session): void => { Windows.MAIN_WINDOW ); const value: SourceUrl = { + id: nanoid(), title: webContents.getTitle(), url: details.url, headers: details.requestHeaders, diff --git a/src/main/handleIpc.ts b/src/main/handleIpc.ts index a96cbc6..9e95422 100644 --- a/src/main/handleIpc.ts +++ b/src/main/handleIpc.ts @@ -1,7 +1,6 @@ -import { app, dialog, ipcMain, shell } from "electron"; +import { app, dialog, ipcMain, Menu, shell } from "electron"; import { failFn, successFn } from "./utils"; import windowManager from "./window/windowManager"; -import { M3u8DLArgs } from "types/common"; import executor from "main/executor"; import request from "main/request"; import { binDir, Windows } from "main/variables"; @@ -55,6 +54,41 @@ const handleIpc = (): void => { if (view) view.webContents.loadURL(url || "https://baidu.com"); }); + ipcMain.on("open-download-item-context-menu", (e, item: SourceItem) => { + const mainWin = windowManager.get(Windows.MAIN_WINDOW); + const menu = Menu.buildFromTemplate([ + { + label: "详情", + click: () => { + e.sender.send("download-context-menu-detail", item); + }, + }, + { type: "separator" }, + { + label: "下载", + click: () => { + e.sender.send("download-context-menu-download", item); + }, + }, + { + label: "删除", + click: () => { + e.sender.send("download-context-menu-delete", item); + }, + }, + { type: "separator" }, + { + label: "清空列表", + click: () => { + e.sender.send("download-context-menu-clear-all"); + }, + }, + ]); + menu.popup({ + window: mainWin, + }); + }); + ipcMain.handle("exec", async (event, exeFile: string, args: M3u8DLArgs) => { let resp; try { diff --git a/src/main/window/windowManager.ts b/src/main/window/windowManager.ts index bbc22ff..60e752d 100644 --- a/src/main/window/windowManager.ts +++ b/src/main/window/windowManager.ts @@ -1,7 +1,7 @@ import { BrowserWindow } from "electron"; import { IWindowListItem, IWindowManager } from "types/main"; import windowList from "./windowList"; -import { Windows } from "./variables"; +import { Windows } from "../variables"; class WindowManager implements IWindowManager { private windowMap: Map = new Map(); diff --git a/src/preload/index.ts b/src/preload/index.ts index c3715be..ed0a273 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -45,6 +45,8 @@ const api: ElectronApi = { browserViewReload: () => ipcRenderer.send("browser-view-reload"), browserViewLoadURL: (url) => ipcRenderer.send("browser-view-load-url", url), request: (options) => ipcRenderer.invoke("request", options), + itemContextMenu: (item) => + ipcRenderer.send("open-download-item-context-menu", item), }; contextBridge.exposeInMainWorld(apiKey, api); diff --git a/src/renderer/hooks/electron.ts b/src/renderer/hooks/electron.ts new file mode 100644 index 0000000..6771633 --- /dev/null +++ b/src/renderer/hooks/electron.ts @@ -0,0 +1,5 @@ +const useElectron = (): ElectronApi => { + return window.electron; +}; + +export default useElectron; diff --git a/src/renderer/nodes/browser/index.tsx b/src/renderer/nodes/browser/index.tsx index aa2b03e..4afa8c7 100644 --- a/src/renderer/nodes/browser/index.tsx +++ b/src/renderer/nodes/browser/index.tsx @@ -7,7 +7,7 @@ import { insertFav, isFavFunc, removeFav } from "renderer/utils/localforge"; import WindowToolBar from "renderer/components/WindowToolBar"; import SearchBar from "./elements/SearchBar"; import onEvent from "renderer/utils/td-utils"; -import electron from "renderer/utils/electron"; +import useElectron from "renderer/hooks/electron"; tdApp.init(); @@ -34,6 +34,11 @@ const BrowserWindow: FC = () => { const [isFav, setIsFav] = useState(false); const webviewRef = useRef(); const resizeObserver = useRef(); + const { + browserViewGoBack, + browserViewReload, + browserViewLoadURL, + } = useElectron(); useEffect(() => { initWebView(); @@ -77,16 +82,16 @@ const BrowserWindow: FC = () => { const onGoBack = () => { onEvent.browserPageGoBack(); - electron.browserViewGoBack(); + browserViewGoBack(); }; const onReload = () => { onEvent.browserPageReload(); - electron.browserViewReload(); + browserViewReload(); }; const onGoBackHome = () => { - electron.browserViewLoadURL(); + browserViewLoadURL(); }; const onUrlChange = (url: string) => { @@ -94,7 +99,7 @@ const BrowserWindow: FC = () => { }; const handleEnter = () => { - electron.browserViewLoadURL(url); + browserViewLoadURL(url); }; const handleClickFav = async () => { diff --git a/src/renderer/nodes/main/elements/DownloadList/index.scss b/src/renderer/nodes/main/elements/DownloadList/index.scss index 6268b9f..6adcde8 100644 --- a/src/renderer/nodes/main/elements/DownloadList/index.scss +++ b/src/renderer/nodes/main/elements/DownloadList/index.scss @@ -1,7 +1,7 @@ @import "../../../main"; .new-download-list { - .list-item { + .list-item-container { cursor: pointer; .list-item-inner { diff --git a/src/renderer/nodes/main/elements/DownloadList/index.tsx b/src/renderer/nodes/main/elements/DownloadList/index.tsx index 7207237..b2e062c 100644 --- a/src/renderer/nodes/main/elements/DownloadList/index.tsx +++ b/src/renderer/nodes/main/elements/DownloadList/index.tsx @@ -12,18 +12,12 @@ import AutoSizer from "react-virtualized-auto-sizer"; import { Resizable } from "re-resizable"; import "./index.scss"; import { Box } from "@chakra-ui/react"; -import { - Fav, - M3u8DLArgs, - MediaGoArgs, - SourceItem, - SourceItemForm, -} from "types/common"; import { SourceStatus, SourceType } from "renderer/types"; import classNames from "classnames"; import { Button, Dropdown, + Empty, Form, FormInstance, Input, @@ -50,6 +44,8 @@ import { insertFav, insertVideo, removeFav, + removeVideo, + removeVideos, updateVideoStatus, updateVideoTitle, updateVideoUrl, @@ -61,6 +57,8 @@ import { Settings } from "renderer/store/models/settings"; import { AppState } from "renderer/store/reducers"; import { FileDrop } from "react-file-drop"; import ProForm from "@ant-design/pro-form"; +import useElectron from "renderer/hooks/electron"; +import { nanoid } from "nanoid"; type ActionButton = { key: string; @@ -123,6 +121,11 @@ const DownloadList: React.FC = ({ const [maxWidth, setMaxWidth] = useState(winWidth); const [currentSourceItem, setCurrentSourceItem] = useState(); const settings = useSelector((state) => state.settings); + const { + itemContextMenu, + addEventListener, + removeEventListener, + } = useElectron(); const { exeFile } = settings; const [formRef] = Form.useForm(); const [detailForm] = Form.useForm(); @@ -136,12 +139,52 @@ const DownloadList: React.FC = ({ initData(); window.addEventListener("resize", calcMaxWidth); + addEventListener("download-context-menu-detail", contextMenuDetail); + addEventListener("download-context-menu-download", contextMenuDownload); + addEventListener("download-context-menu-delete", contextMenuDelete); + addEventListener("download-context-menu-clear-all", contextMenuClearAll); return () => { window.removeEventListener("resize", calcMaxWidth); + removeEventListener("context-menu-command", contextMenuDetail); + removeEventListener( + "download-context-menu-download", + contextMenuDownload + ); + removeEventListener("download-context-menu-delete", contextMenuDelete); + removeEventListener( + "download-context-menu-clear-all", + contextMenuClearAll + ); }; }, []); + const contextMenuDetail = ( + e: Electron.IpcRendererEvent, + item: SourceItem + ) => { + setCurrentSourceItem(item); + detailForm.setFieldsValue(preProcessFormData(item)); + calcMaxWidth(); + }; + const contextMenuDownload = ( + e: Electron.IpcRendererEvent, + item: SourceItem + ) => { + downloadFile(item); + }; + const contextMenuDelete = async ( + event: Electron.IpcRendererEvent, + item: SourceItem + ) => { + await removeVideo(item.url); + await updateTableData(); + }; + const contextMenuClearAll = () => { + const keys = tableData.map((item) => item.url); + removeVideos(keys); + }; + const initData = async () => { const favs = await getFavs(); setFavsList(favs); @@ -167,9 +210,6 @@ const DownloadList: React.FC = ({ return result; }; - // 表单数据后处理 - const postProcessFormData = () => {}; - // 渲染视频下载的状态 const renderStatus = (item: SourceItem) => { const status = item.status; @@ -185,12 +225,31 @@ const DownloadList: React.FC = ({ setIsModalVisible(false); }; + // 新建下载 + const newDownload = () => { + onEvent.mainPageNewSource(); + setIsModalVisible(true); + }; + + // 打开浏览器 + const openBrowser = () => { + onEvent.mainPageOpenBrowserPage(); + window.electron.openBrowserWindow(); + }; + + // 打开使用帮助 + const openHelp = () => { + onEvent.mainPageHelp(); + window.electron.openExternal(helpUrl); + }; + // 向列表中插入一条数据并且请求详情 const insertUpdateTableData = async ( item: SourceItemForm ): Promise => { const { workspace } = settings; const sourceItem: SourceItem = { + id: nanoid(), status: SourceStatus.Ready, type: SourceType.M3u8, directory: workspace, @@ -382,35 +441,20 @@ const DownloadList: React.FC = ({ return ( - { - onEvent.mainPageOpenBrowserPage(); - window.electron.openBrowserWindow(); - }} + onClick={openBrowser} overlay={browserMenu} > 打开浏览器 - @@ -545,124 +589,166 @@ const DownloadList: React.FC = ({ {renderToolBar()} - - 0 ? ( + - - {({ height, width }) => ( - - height={height} - itemSize={35} - width={width} - itemData={tableData} - itemCount={tableData.length} - itemKey={(index, data) => { - const item = data[index]; - return item.title; - }} - > - {({ index, style, data }) => { - const item = data[index]; - - return ( - - {renderStatus(item)} + + + {({ height, width }) => ( + + height={height} + itemSize={35} + width={width} + itemData={tableData} + itemCount={tableData.length} + itemKey={(index, data) => { + const item = data[index]; + return item.id || `${item.title}-${index}`; + }} + > + {({ index, style, data }) => { + const item = data[index]; + + return ( { - setCurrentSourceItem(item); - detailForm.setFieldsValue(preProcessFormData(item)); - calcMaxWidth(); + className={classNames("list-item-container")} + _hover={{ bg: "#EBEEF5" }} + style={style} + title={item.title} + display={"flex"} + flexDirection={"row"} + alignItems={"center"} + px={15} + onContextMenu={() => { + itemContextMenu(item); }} > - {item.title} + {renderStatus(item)} + { + setCurrentSourceItem(item); + detailForm.setFieldsValue( + preProcessFormData(item) + ); + calcMaxWidth(); + }} + > + {item.title} + + {renderActionButtons(item)} - {renderActionButtons(item)} - - ); - }} - - )} - - - - {currentSourceItem && ( - - { - const item = detailForm.getFieldsValue(); - await downloadFile(item); - }, - }} + ); + }} + + )} + + + + {currentSourceItem && ( + - - - - - - - - - - - - )} - + { + const item = detailForm.getFieldsValue(); + await downloadFile(item); + }, + }} + > + + + + + + + + + + + + )} + + ) : ( + + + 没有数据,请 + +
+ 或者 + +
+ 或者 + + + } + /> +
+ )} {/*新建下载窗口*/} { const dispatch = useDispatch(); const settings = useSelector((state) => state.settings); const { workspace } = settings; + const { + addEventListener, + removeEventListener, + closeMainWindow, + } = useElectron(); useEffect(() => { initData(); - electron.addEventListener("m3u8", handleWebViewMessage); + addEventListener("m3u8", handleWebViewMessage); return () => { - electron.removeEventListener("m3u8", handleWebViewMessage); + removeEventListener("m3u8", handleWebViewMessage); }; }, []); @@ -129,7 +133,7 @@ const MainPage: FC = () => { { - electron.closeMainWindow(); + closeMainWindow(); }} />
diff --git a/src/renderer/utils/electron.ts b/src/renderer/utils/electron.ts deleted file mode 100644 index 970317c..0000000 --- a/src/renderer/utils/electron.ts +++ /dev/null @@ -1,3 +0,0 @@ -const electron = window.electron; - -export default electron; diff --git a/src/renderer/utils/localforge.ts b/src/renderer/utils/localforge.ts index ed5dd2f..d3565dc 100644 --- a/src/renderer/utils/localforge.ts +++ b/src/renderer/utils/localforge.ts @@ -1,4 +1,3 @@ -import { Fav, SourceItem } from "types/common"; import * as localforage from "localforage"; import { SourceStatus } from "renderer/types"; @@ -57,7 +56,7 @@ const updateVideoUrl = async (source: SourceItem, url: string) => { } }; -const removeVideo = async (url: string) => { +export const removeVideo = async (url: string) => { let videos = await localforage.getItem(keys.videos); if (!Array.isArray(videos)) videos = []; const favIndex = videos.findIndex((item) => item.url === url); diff --git a/src/types/common.d.ts b/src/types/common.d.ts index 253c122..6a90704 100644 --- a/src/types/common.d.ts +++ b/src/types/common.d.ts @@ -1,13 +1,14 @@ -// 从主进程中想渲染进程发送的参数 -import { SourceStatus, SourceType } from "renderer/types"; - declare interface SourceUrl { + id: string; title: string; duration: number; url: string; headers?: Record; } +declare type SourceStatus = "ready" | "downloading" | "failed" | "success"; +declare type SourceType = "m3u8" | "m4s"; + declare type SourceItem = SourceUrl & { status: SourceStatus; type: SourceType; @@ -70,13 +71,3 @@ declare interface VideoDetail { segmentsLen: number; duration: number; } - -export { - SourceUrl, - Fav, - SourceItem, - SourceItemForm, - M3u8DLArgs, - MediaGoArgs, - VideoDetail, -}; diff --git a/src/types/renderer.d.ts b/src/types/renderer.d.ts index b3c32a0..49dc64b 100644 --- a/src/types/renderer.d.ts +++ b/src/types/renderer.d.ts @@ -102,6 +102,7 @@ interface ElectronApi { browserViewReload: () => void; browserViewLoadURL: (url?: string) => void; request: (options: RequestOptions) => Promise>; + itemContextMenu: (item: SourceItem) => void; } declare interface Window { diff --git a/tsconfig.json b/tsconfig.json index 93cbd36..74ec45f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -45,7 +45,8 @@ "./src/types/**/*" ], "files": [ - "./types/renderer.d.ts", - "./types/main.d.ts" + "./src/types/renderer.d.ts", + "./src/types/main.d.ts", + "./src/types/common.d.ts" ] }