feature: 更换交互方式

for/master
caorushizi 3 years ago
parent a2d82dd511
commit fa8149f477

1
.gitignore vendored

@ -5,6 +5,7 @@ dist-ssr
*.local
.idea
.bin
.vscode
yarn.lock
yarn-error.log

@ -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",

@ -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,

@ -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 {

@ -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<Windows | string, BrowserWindow> = new Map();

@ -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);

@ -0,0 +1,5 @@
const useElectron = (): ElectronApi => {
return window.electron;
};
export default useElectron;

@ -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<boolean>(false);
const webviewRef = useRef<HTMLDivElement>();
const resizeObserver = useRef<ResizeObserver>();
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 () => {

@ -1,7 +1,7 @@
@import "../../../main";
.new-download-list {
.list-item {
.list-item-container {
cursor: pointer;
.list-item-inner {

@ -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<Props> = ({
const [maxWidth, setMaxWidth] = useState<number>(winWidth);
const [currentSourceItem, setCurrentSourceItem] = useState<SourceItem>();
const settings = useSelector<AppState, Settings>((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<Props> = ({
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<Props> = ({
return result;
};
// 表单数据后处理
const postProcessFormData = () => {};
// 渲染视频下载的状态
const renderStatus = (item: SourceItem) => {
const status = item.status;
@ -185,12 +225,31 @@ const DownloadList: React.FC<Props> = ({
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<SourceItem> => {
const { workspace } = settings;
const sourceItem: SourceItem = {
id: nanoid(),
status: SourceStatus.Ready,
type: SourceType.M3u8,
directory: workspace,
@ -382,35 +441,20 @@ const DownloadList: React.FC<Props> = ({
return (
<Box p={10} borderBottom={"1px solid #EBEEF5"}>
<Space>
<Button
key={"1"}
onClick={() => {
onEvent.mainPageNewSource();
setIsModalVisible(true);
}}
>
<Button key={"1"} onClick={newDownload}>
<AppstoreAddOutlined />
</Button>
<Dropdown.Button
key={"2"}
trigger={["click"]}
onClick={() => {
onEvent.mainPageOpenBrowserPage();
window.electron.openBrowserWindow();
}}
onClick={openBrowser}
overlay={browserMenu}
>
<BlockOutlined />
</Dropdown.Button>
<Button
key={"4"}
onClick={async () => {
onEvent.mainPageHelp();
window.electron.openExternal(helpUrl);
}}
>
<Button key={"4"} onClick={openHelp}>
<QuestionCircleOutlined />
使
</Button>
@ -545,124 +589,166 @@ const DownloadList: React.FC<Props> = ({
<FileDrop onDrop={onDrop}>
<Box h={"100%"} w={"100%"} display={"flex"} flexDirection={"column"}>
{renderToolBar()}
<Box
flex={1}
display={"flex"}
overflow={"hidden"}
flexDirection={"row"}
>
<Resizable
as={Box}
enable={{ right: true }}
minHeight={"100%"}
minWidth={currentSourceItem ? "350px" : "100%"}
maxWidth={currentSourceItem ? maxWidth : "100%"}
{tableData.length > 0 ? (
<Box
flex={1}
display={"flex"}
overflow={"hidden"}
flexDirection={"row"}
>
<AutoSizer className={"new-download-list"}>
{({ height, width }) => (
<List<SourceItem[]>
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 (
<Box
className={classNames("list-item")}
style={style}
title={item.title}
display={"flex"}
flexDirection={"row"}
alignItems={"center"}
px={15}
>
{renderStatus(item)}
<Resizable
as={Box}
enable={{ right: true }}
minHeight={"100%"}
minWidth={currentSourceItem ? "350px" : "100%"}
maxWidth={currentSourceItem ? maxWidth : "100%"}
>
<AutoSizer className={"new-download-list"}>
{({ height, width }) => (
<List<SourceItem[]>
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 (
<Box
flex={1}
className={"list-item-inner"}
onClick={() => {
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)}
<Box
flex={1}
className={"list-item-inner"}
onClick={() => {
setCurrentSourceItem(item);
detailForm.setFieldsValue(
preProcessFormData(item)
);
calcMaxWidth();
}}
>
{item.title}
</Box>
{renderActionButtons(item)}
</Box>
{renderActionButtons(item)}
</Box>
);
}}
</List>
)}
</AutoSizer>
</Resizable>
{currentSourceItem && (
<Box
p={15}
height={"100%"}
flex={1}
overflowY={"auto"}
minW={"300px"}
>
<ProForm
form={detailForm}
size={"small"}
layout={"horizontal"}
submitter={{
searchConfig: {
resetText: "重置",
submitText: "下载",
},
resetButtonProps: {
style: {
// 隐藏重置按钮
display: "none",
},
},
onSubmit: async () => {
const item = detailForm.getFieldsValue();
await downloadFile(item);
},
}}
);
}}
</List>
)}
</AutoSizer>
</Resizable>
{currentSourceItem && (
<Box
p={15}
height={"100%"}
flex={1}
overflowY={"auto"}
minW={"300px"}
>
<ProForm.Group>
<ProFormText
name={"title"}
label="视频名称"
placeholder="请输入视频名称"
/>
<ProFormText
name={"url"}
label="请求地址"
placeholder="请输入请求地址"
/>
</ProForm.Group>
<ProForm.Group>
<ProFormText
name={"workspace"}
label="本地路径"
placeholder="请输入本地路径"
/>
<ProFormText
name={"exeFile"}
label="执行程序"
placeholder="请选择可执行程序"
/>
</ProForm.Group>
<ProFormTextArea label={"请求标头"} name={"headers"} />
</ProForm>
</Box>
)}
</Box>
<ProForm
form={detailForm}
size={"small"}
layout={"horizontal"}
submitter={{
searchConfig: {
resetText: "重置",
submitText: "下载",
},
resetButtonProps: {
style: {
// 隐藏重置按钮
display: "none",
},
},
onSubmit: async () => {
const item = detailForm.getFieldsValue();
await downloadFile(item);
},
}}
>
<ProForm.Group>
<ProFormText
name={"title"}
label="视频名称"
placeholder="请输入视频名称"
/>
<ProFormText
name={"url"}
label="请求地址"
placeholder="请输入请求地址"
/>
</ProForm.Group>
<ProForm.Group>
<ProFormText
name={"workspace"}
label="本地路径"
placeholder="请输入本地路径"
/>
<ProFormText
name={"exeFile"}
label="执行程序"
placeholder="请选择可执行程序"
/>
</ProForm.Group>
<ProFormTextArea label={"请求标头"} name={"headers"} />
</ProForm>
</Box>
)}
</Box>
) : (
<Box
flex={1}
display={"flex"}
overflow={"hidden"}
flexDirection={"row"}
alignItems={"center"}
justifyContent={"center"}
>
<Empty
image="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg"
imageStyle={{
height: 120,
}}
description={
<span>
<Button type={"link"} onClick={newDownload}>
</Button>
<br />
<Button type={"link"} onClick={openBrowser}>
</Button>
<br />
<Button type={"link"} onClick={openHelp}>
</Button>
</span>
}
/>
</Box>
)}
{/*新建下载窗口*/}
<Modal

@ -4,7 +4,6 @@ import { Badge, Button, Drawer, message, Tabs } from "antd";
import WindowToolBar from "renderer/components/WindowToolBar";
import Setting from "renderer/nodes/main/elements/Setting";
import Comment from "renderer/components/Comment";
import { SourceItem, SourceUrl } from "types/common";
import { SourceStatus, SourceType } from "renderer/types";
import {
getVideos,
@ -17,7 +16,7 @@ import { useDispatch, useSelector } from "react-redux";
import { updateSettings } from "renderer/store/actions/settings.actions";
import { AppState } from "renderer/store/reducers";
import { Settings } from "renderer/store/models/settings";
import electron from "renderer/utils/electron";
import useElectron from "renderer/hooks/electron";
import NewDownloadList from "renderer/nodes/main/elements/DownloadList";
const audio = new Audio(audioSrc);
@ -37,13 +36,18 @@ const MainPage: FC = () => {
const dispatch = useDispatch();
const settings = useSelector<AppState, Settings>((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 = () => {
<WindowToolBar
color="#4090F7"
onClose={() => {
electron.closeMainWindow();
closeMainWindow();
}}
/>
<div className="main-window">

@ -1,3 +0,0 @@
const electron = window.electron;
export default electron;

@ -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<SourceItem[]>(keys.videos);
if (!Array.isArray(videos)) videos = [];
const favIndex = videos.findIndex((item) => item.url === url);

@ -1,13 +1,14 @@
// 从主进程中想渲染进程发送的参数
import { SourceStatus, SourceType } from "renderer/types";
declare interface SourceUrl {
id: string;
title: string;
duration: number;
url: string;
headers?: Record<string, string>;
}
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,
};

@ -102,6 +102,7 @@ interface ElectronApi {
browserViewReload: () => void;
browserViewLoadURL: (url?: string) => void;
request: <T>(options: RequestOptions) => Promise<RequestResponse<T>>;
itemContextMenu: (item: SourceItem) => void;
}
declare interface Window {

@ -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"
]
}

Loading…
Cancel
Save