Compare commits

...

5 Commits

Author SHA1 Message Date
caorushizi 71739a3ffb feature: 修复一些问题 2022-01-08 17:19:45 +08:00
caorushizi db3becb071 feature: 修复一些问题 2022-01-08 13:28:37 +08:00
caorushizi a458f69990 feature: 清理代码。 2022-01-01 02:13:23 +08:00
caorushizi 82cf627488 feature: 自动升级。 2021-12-31 22:58:00 +08:00
caorushizi e00bd5dab0 feature: 修复一些问题 2021-12-31 01:41:50 +08:00
34 changed files with 432 additions and 405 deletions
-2
View File
@@ -10,7 +10,5 @@
<body>
<div id="root"></div>
<script type="module" src="/src/renderer/main.tsx"></script>
<script src="/lib/Valine.min.js"></script>
<script src="/lib/av-min.js"></script>
</body>
</html>
+11 -2
View File
@@ -1,6 +1,6 @@
{
"name": "media-downloader",
"version": "1.1.0",
"version": "1.1.1",
"description": "在线视频下载",
"author": "caorushizi",
"main": "dist/main/index.js",
@@ -11,7 +11,9 @@
"watch": "cross-env NODE_ENV=development node script/watch.js",
"electron": "cross-env NODE_ENV=production electron .",
"pack": "electron-builder --dir",
"build": "electron-builder"
"build": "electron-builder",
"publish": "npm run dist && npm run build",
"build-debug": "npm run dist && npm run pack && ./build/win-unpacked/在线视频下载.exe"
},
"dependencies": {
"@ant-design/icons": "^4.6.2",
@@ -20,9 +22,13 @@
"@chakra-ui/react": "^1.7.2",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@sentry/electron": "^2.5.4",
"@sentry/react": "^6.16.1",
"@sentry/tracing": "^6.16.1",
"antd": "^4.17.2",
"classnames": "^2.3.1",
"connected-react-router": "^6.9.1",
"electron-devtools-installer": "^3.2.0",
"electron-log": "^4.3.2",
"electron-squirrel-startup": "^1.0.0",
"electron-store": "^7.0.2",
@@ -70,6 +76,7 @@
"dotenv": "^8.2.0",
"electron": "16.0.4",
"electron-builder": "^22.14.2",
"electron-updater": "^4.6.1",
"esbuild": "^0.9.5",
"eslint": "^7.5.0",
"eslint-config-prettier": "^8.1.0",
@@ -91,6 +98,7 @@
"productName": "在线视频下载",
"appId": "mediago.ziying.site",
"copyright": "caorushizi",
"artifactName": "media-downloader-setup-${version}.${ext}",
"directories": {
"output": "build"
},
@@ -103,6 +111,7 @@
],
"win": {
"icon": "assets/icon.ico",
"requestedExecutionLevel": "requireAdministrator",
"target": [
{
"target": "nsis"
-26
View File
File diff suppressed because one or more lines are too long
-21
View File
File diff suppressed because one or more lines are too long
+10 -2
View File
@@ -4,10 +4,15 @@ const rimraf = require("rimraf");
rimraf.sync(resolve(__dirname, "../dist"));
require("dotenv").config({
path: resolve(__dirname, `.env.${process.env.NODE_ENV}`),
const { parsed } = require("dotenv").config({
path: resolve(__dirname, `../.env.${process.env.NODE_ENV}.local`),
});
const mainDefined = Object.keys(parsed).reduce((prev, cur) => {
prev[`process.env.${[cur]}`] = JSON.stringify(parsed[cur]);
return prev;
}, {});
function buildMain() {
return require("esbuild").build({
entryPoints: [
@@ -21,6 +26,9 @@ function buildMain() {
external: ["electron"],
outdir: resolve(__dirname, "../dist"),
loader: { ".png": "file" },
define: {
...mainDefined,
},
});
}
+4 -13
View File
@@ -5,13 +5,12 @@ const electron = require("electron");
const reactRefresh = require("@vitejs/plugin-react-refresh");
const path = require("path");
const { spawn } = require("child_process");
// const eslint = require("@rollup/plugin-eslint");
let electronProcess = null;
let manualRestart = false;
require("dotenv").config({
path: resolve(__dirname, `.env.${process.env.NODE_ENV}`),
path: resolve(__dirname, `../.env.${process.env.NODE_ENV}.local`),
});
function startMain() {
@@ -71,16 +70,7 @@ function startRenderer() {
{ find: /^~/, replacement: "" },
],
},
plugins: [
// {
// ...eslint({
// include: "**/*.+(js|jsx|ts|tsx)",
// }),
// enforce: "pre",
// apply: "serve",
// },
reactRefresh(),
],
plugins: [reactRefresh()],
css: {
preprocessorOptions: {
less: {
@@ -120,8 +110,9 @@ function electronLog(data, color) {
(async () => {
try {
const [server] = await Promise.all([startRenderer(), startMain()]);
const server = await startRenderer();
await server.listen();
await startMain();
startElectron();
} catch (e) {
console.error(e);
+16
View File
@@ -0,0 +1,16 @@
import installExtension, {
REACT_DEVELOPER_TOOLS,
REDUX_DEVTOOLS,
} from "electron-devtools-installer";
import { log } from "main/utils";
export default async function handleExtension(): Promise<void> {
if (process.env.NODE_ENV === "development") {
try {
await installExtension(REACT_DEVELOPER_TOOLS);
await installExtension(REDUX_DEVTOOLS);
} catch (e) {
log.info(e);
}
}
}
@@ -1,9 +1,9 @@
import { app, dialog, ipcMain, Menu, shell } from "electron";
import { failFn, successFn } from "./utils";
import windowManager from "./window/windowManager";
import executor from "main/executor";
import request from "main/request";
import { binDir, Windows } from "main/variables";
import { failFn, successFn } from "../utils";
import windowManager from "../window/windowManager";
import executor from "main/utils/executor";
import request from "main/utils/request";
import { binDir, Windows } from "main/utils/variables";
const handleIpc = (): void => {
ipcMain.on("close-main-window", async () => {
@@ -89,17 +89,29 @@ const handleIpc = (): void => {
});
});
ipcMain.handle("exec", async (event, exeFile: string, args: M3u8DLArgs) => {
let resp;
try {
const result = await executor(exeFile, args);
resp = successFn(result);
} catch (e: any) {
resp = failFn(-1, e.message);
ipcMain.on("window-minimize", (e, name) => {
let window;
if (name === "main") {
window = windowManager.get(Windows.MAIN_WINDOW);
} else {
window = windowManager.get(Windows.BROWSER_WINDOW);
}
return resp;
window.minimize();
});
ipcMain.handle(
"exec-command",
async (event, exeFile: string, args: M3u8DLArgs) => {
try {
const result = await executor(exeFile, args);
return successFn(result);
} catch (e: any) {
return failFn(-1, e.message);
}
}
);
ipcMain.handle("get-bin-dir", async () => binDir);
ipcMain.handle("set-store", (e, key, value) => global.store.set(key, value));
+25
View File
@@ -0,0 +1,25 @@
import Store from "electron-store";
import { workspace } from "main/utils/variables";
export default function handleStore(): void {
let exeFile = "";
if (process.platform === "win32") {
exeFile = "N_m3u8DL-CLI";
} else {
exeFile = "mediago";
}
global.store = new Store<AppStore>({
name: "config",
cwd: workspace,
fileExtension: "json",
watch: true,
defaults: {
workspace: "",
exeFile,
tip: true,
proxy: "",
useProxy: false,
},
});
}
+13
View File
@@ -0,0 +1,13 @@
import windowManager from "main/window/windowManager";
import { Windows } from "main/utils/variables";
import createBrowserView from "main/utils/browserView";
export default async function handleWindows(
webviewSession: Electron.Session
): Promise<void> {
await Promise.all([
windowManager.create(Windows.MAIN_WINDOW),
await windowManager.create(Windows.BROWSER_WINDOW),
]);
await createBrowserView(webviewSession);
}
+53 -80
View File
@@ -1,20 +1,26 @@
import { app, BrowserWindow, protocol, session } from "electron";
import { resolve } from "path";
import { log } from "main/utils";
import windowManager from "main/window/windowManager";
import handleIpc from "main/handleIpc";
import createBrowserView from "main/browserView";
import Store from "electron-store";
import {
defaultScheme,
webviewPartition,
Windows,
workspace,
} from "main/variables";
import createSession from "main/session";
import { URL } from "url";
import { join, extname } from "path";
import { readFile, readFileSync } from "fs";
import path from "path";
import { URL } from "url";
import { app, BrowserWindow, protocol, crashReporter } from "electron";
import { autoUpdater } from "electron-updater";
import { log } from "main/utils";
import handleIpc from "main/helper/handleIpc";
import { defaultScheme, webviewPartition } from "main/utils/variables";
import createSession from "main/utils/session";
import handleStore from "main/helper/handleStore";
import handleExtension from "main/helper/handleExtension";
import handleWindows from "main/helper/handleWindows";
import * as Sentry from "@sentry/electron/dist/main";
import { name, author } from "../../package.json";
Sentry.init({ dsn: process.env.VITE_APP_SENTRY_DSN });
crashReporter.start({
companyName: author,
productName: name,
ignoreSystemCrashHandler: true,
submitURL: process.env.VITE_APP_SENTRY_DSN,
});
if (require("electron-squirrel-startup")) {
app.quit();
@@ -26,12 +32,6 @@ app.on("window-all-closed", () => {
}
});
const init = async (webviewSession: Electron.Session) => {
windowManager.create(Windows.MAIN_WINDOW);
await windowManager.create(Windows.BROWSER_WINDOW);
await createBrowserView(webviewSession);
};
protocol.registerSchemesAsPrivileged([
{
scheme: defaultScheme,
@@ -42,24 +42,25 @@ protocol.registerSchemesAsPrivileged([
},
]);
app.whenReady().then(() => {
app.whenReady().then(async () => {
autoUpdater.logger = log;
autoUpdater.checkForUpdatesAndNotify();
protocol.registerBufferProtocol(defaultScheme, (request, callback) => {
let pathName = new URL(request.url).pathname;
pathName = decodeURI(pathName);
readFile(path.join(__dirname, "../renderer", pathName), (error, data) => {
readFile(join(__dirname, "../renderer", pathName), (error, data) => {
if (error) {
console.error(
`Failed to register ${webviewPartition} protocol\n`,
error,
"\n"
);
const data = readFileSync(
path.join(__dirname, "../renderer/index.html")
);
const data = readFileSync(join(__dirname, "../renderer/index.html"));
callback({ mimeType: "text/html", data });
} else {
const extension = path.extname(pathName).toLowerCase();
const extension = extname(pathName).toLowerCase();
let mimeType = "";
if (extension === ".js") {
@@ -82,65 +83,37 @@ app.whenReady().then(() => {
const webviewSession = createSession(webviewPartition);
app.on("activate", async () => {
if (BrowserWindow.getAllWindows().length === 0) {
await init(webviewSession);
await handleWindows(webviewSession);
}
});
init(webviewSession);
handleWindows(webviewSession);
handleExtension();
handleStore();
handleIpc();
if (process.env.NODE_ENV === "development") {
// 设置软件代理
const setProxy = (isInit?: boolean) => {
try {
const reactTool = resolve(__dirname, "../../devtools/react");
session.defaultSession.loadExtension(reactTool);
const reduxTool = resolve(__dirname, "../../devtools/redux");
session.defaultSession.loadExtension(reduxTool);
} catch (e) {
log.info(e);
}
}
let exeFile = "";
if (process.platform === "win32") {
exeFile = "N_m3u8DL-CLI";
} else {
exeFile = "mediago";
}
const store = new Store<AppStore>({
name: "config",
cwd: workspace,
fileExtension: "json",
watch: true,
defaults: {
workspace: "",
exeFile,
tip: true,
proxy: "",
useProxy: false,
},
});
const setProxy = () => {
const proxy = store.get("proxy");
if (proxy) {
log.info("proxy 设置成功");
webviewSession.setProxy({ proxyRules: proxy });
} else {
webviewSession.setProxy({});
const proxy = global.store.get("proxy");
const useProxy = global.store.get("useProxy");
if (proxy && useProxy) {
log.info(
`[proxy] ${isInit ? "初始化" : "开启"}成功,代理地址为${proxy}`
);
webviewSession.setProxy({ proxyRules: proxy });
} else {
if (!isInit) log.info(`[proxy] 关闭成功`);
webviewSession.setProxy({});
}
} catch (e: any) {
log.error(
`[proxy] ${isInit ? "初始化" : ""}设置代理失败:\n${e.message}`
);
}
};
setProxy();
setProxy(true);
store.onDidChange("useProxy", (newValue) => {
try {
if (newValue) setProxy();
} catch (e: any) {
log.error("设置代理失败:", e.message);
}
});
global.store = store;
global.store.onDidChange("useProxy", setProxy);
});
handleIpc();
@@ -1,7 +1,7 @@
import windowManager from "main/window/windowManager";
import { BrowserView } from "electron";
import { log } from "main/utils";
import { webviewPartition, Windows } from "main/variables";
import { webviewPartition, Windows } from "main/utils/variables";
import { nanoid } from "nanoid";
const createBrowserView = (session: Electron.Session): void => {
@@ -52,7 +52,7 @@ const createBrowserView = (session: Electron.Session): void => {
headers: details.requestHeaders,
duration: 0,
};
mainWindow.send("m3u8", value);
mainWindow.send("m3u8-notifier", value);
cancel = true;
}
callback({
@@ -1,7 +1,10 @@
import semver from "semver";
import { globWrapper, log, spawnWrapper } from "main/utils";
import { binDir } from "main/variables";
import { binDir } from "main/utils/variables";
import fs from "fs";
import path from "path";
// 找到可执行文件并进行参数拼接
const execM3u8DL = async (args: M3u8DLArgs): Promise<string[]> => {
let binNameList = await globWrapper("N_m3u8DL-CLI*.exe", {
cwd: binDir,
@@ -13,6 +16,13 @@ const execM3u8DL = async (args: M3u8DLArgs): Promise<string[]> => {
if (binNameList.length === 0) throw new Error("没有找到 N_m3u8DL-CLI");
const binName = `N_m3u8DL-CLI_v${binNameList[0]}`;
let binFileName = binName;
if (process.platform === "win32") {
binFileName = `${binName}.exe`;
}
const binExist = fs.existsSync(path.resolve(binDir, binFileName));
if (!binExist) throw new Error("没有找到 N_m3u8DL-CLI");
let argsStr = Object.entries(args)
.reduce((prev: string[], [key, value]) => {
if (key === "url") return prev;
+2 -2
View File
@@ -1,6 +1,6 @@
import { spawn, SpawnOptions } from "child_process";
import glob from "glob";
import { workspace } from "main/variables";
import { workspace } from "main/utils/variables";
import spawnArgs from "main/utils/spawn-args";
import log from "electron-log";
import path from "path";
@@ -59,7 +59,7 @@ interface IpcResponse {
data: any;
}
const successFn = (data: any): IpcResponse => ({ code: 0, msg: "", data });
const successFn = (data: unknown): IpcResponse => ({ code: 0, msg: "", data });
const failFn = (code: number, msg: string): IpcResponse => ({
code,
msg,
+1 -1
View File
@@ -1,6 +1,6 @@
import { IWindowListItem } from "types/main";
import { resolve } from "path";
import { Windows } from "main/variables";
import { Windows } from "main/utils/variables";
const windowList = new Map<Windows, IWindowListItem>();
+1 -1
View File
@@ -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 "../utils/variables";
class WindowManager implements IWindowManager {
private windowMap: Map<Windows | string, BrowserWindow> = new Map();
+2 -1
View File
@@ -13,7 +13,7 @@ const api: ElectronApi = {
},
isWindows: process.platform === "win32",
isMacos: process.platform === "darwin",
ipcExec: (exeFile, args) => ipcRenderer.invoke("exec", exeFile, args),
ipcExec: (exeFile, args) => ipcRenderer.invoke("exec-command", exeFile, args),
openBinDir: async () => {
const binDir = await ipcRenderer.invoke("get-bin-dir");
await shell.openPath(binDir);
@@ -47,6 +47,7 @@ const api: ElectronApi = {
request: (options) => ipcRenderer.invoke("request", options),
itemContextMenu: (item) =>
ipcRenderer.send("open-download-item-context-menu", item),
minimize: (name) => ipcRenderer.send("window-minimize", name),
};
contextBridge.exposeInMainWorld(apiKey, api);
-34
View File
@@ -1,34 +0,0 @@
import React, { ReactNode } from "react";
class Comment extends React.Component {
componentDidMount(): void {
// @ts-ignore
new Valine({
el: "#vcomments",
appId: import.meta.env.VITE_APP_VC_AK,
appKey: import.meta.env.VITE_APP_VC_SK,
placeholder: "快来评论下吧~",
avatar: "hide",
meta: ["nick", "mail"],
requiredFields: ["nick"],
path: import.meta.env.VITE_APP_VC_PATH,
});
setTimeout(() => {
const links = document.querySelectorAll("a[href]");
links.forEach((link) => {
link.addEventListener("click", (e) => {
const url = link.getAttribute("href")!;
e.preventDefault();
window.electron.openExternal(url);
});
});
}, 200);
}
render(): ReactNode {
return <div id="vcomments" />;
}
}
export default Comment;
@@ -0,0 +1,63 @@
import "./index.scss";
import React, { ChangeEvent, FC, useCallback } from "react";
import { Form, Input } from "antd";
const { TextArea } = Input;
export interface HeaderEditProps {
label: string;
name: string;
valuePropName?: string;
placeholder?: string;
}
interface PriceInputProps {
value?: Record<string, string>;
onChange?: (value: Record<string, string>) => void;
}
// 编辑 header
const HeaderFieldInput: FC<PriceInputProps> = ({ value, onChange }) => {
const processHeader = useCallback((value?: Record<string, string>) => {
if (!value) return "";
return Object.keys(value).map((key) => `${key}: ${value[key]}`);
}, []);
const parsedValue = processHeader(value);
const onInputChange = (e: ChangeEvent<HTMLTextAreaElement>): void => {
const value = e.target.value
.split("\n")
.reduce((cur: Record<string, string>, prev) => {
const [key, value] = prev.split(":");
cur[key] = value;
return cur;
}, {});
onChange?.(value);
};
return (
<TextArea
value={parsedValue}
onChange={onInputChange}
autoSize={{ minRows: 3, maxRows: 5 }}
allowClear
/>
);
};
// Http Header 编辑组件
const HeaderEdit: FC<HeaderEditProps> = ({
label,
valuePropName,
name,
placeholder,
}) => {
return (
<Form.Item name={name} label={label} valuePropName={valuePropName}>
<HeaderFieldInput />
</Form.Item>
);
};
export default HeaderEdit;
+32 -36
View File
@@ -1,47 +1,43 @@
import React, { Component, ReactNode } from "react";
import React, { FC, PropsWithChildren } from "react";
import "./index.scss";
import { CloseOutlined, MinusOutlined } from "@ant-design/icons";
import useElectron from "renderer/hooks/electron";
interface Props {
color?: string;
onClose: () => void;
onClose?: () => void;
onMinimize?: () => void;
}
class WindowToolBar extends Component<Props, Record<string, never>> {
// 最小化窗口
minimizeWindow = (): void => {
// TODO: 最小化窗口
// remote.getCurrentWindow().minimize();
};
const WindowToolBar: FC<PropsWithChildren<Props>> = ({
onClose,
onMinimize,
color = "#fff",
children,
}) => {
const { isMacos, isWindows } = useElectron();
render(): ReactNode {
const { children, onClose, color = "#fff" } = this.props;
return (
<div className="window-tool-bar" style={{ background: color }}>
<div className="window-tool-bar-left">
{window.electron.isMacos && (
<div className="mac-btn close" onClick={onClose} />
)}
{window.electron.isMacos && (
<div className="mac-btn min" onClick={this.minimizeWindow} />
)}
</div>
<div className="window-tool-bar-title">{children}</div>
<div className="window-tool-bar-right">
{window.electron.isWindows && (
<div className="btn" onClick={this.minimizeWindow}>
<MinusOutlined />
</div>
)}
{window.electron.isWindows && (
<div className="btn close" onClick={onClose}>
<CloseOutlined />
</div>
)}
</div>
return (
<div className="window-tool-bar" style={{ background: color }}>
<div className="window-tool-bar-left">
{isMacos && <div className="mac-btn close" onClick={onClose} />}
{isMacos && <div className="mac-btn min" onClick={onMinimize} />}
</div>
);
}
}
<div className="window-tool-bar-title">{children}</div>
<div className="window-tool-bar-right">
{isWindows && (
<div className="btn" onClick={onMinimize}>
<MinusOutlined />
</div>
)}
{isWindows && (
<div className="btn close" onClick={onClose}>
<CloseOutlined />
</div>
)}
</div>
</div>
);
};
export default WindowToolBar;
+8
View File
@@ -9,6 +9,14 @@ import BrowserPage from "./nodes/browser";
import MainPage from "./nodes/main";
import "./main.scss";
import store from "./store";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
Sentry.init({
dsn: String(import.meta.env.VITE_APP_SENTRY_DSN || ""),
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 1.0,
});
tdApp.init();
+14 -6
View File
@@ -38,15 +38,20 @@ const BrowserWindow: FC = () => {
browserViewGoBack,
browserViewReload,
browserViewLoadURL,
addEventListener,
setBrowserViewRect,
removeEventListener,
closeBrowserWindow,
minimize,
} = useElectron();
useEffect(() => {
initWebView();
window.electron.addEventListener("dom-ready", handleViewDOMReady);
addEventListener("dom-ready", handleViewDOMReady);
return () => {
window.electron.setBrowserViewRect({ x: 0, y: 0, height: 0, width: 0 });
window.electron.removeEventListener("dom-ready", handleViewDOMReady);
setBrowserViewRect({ x: 0, y: 0, height: 0, width: 0 });
removeEventListener("dom-ready", handleViewDOMReady);
resizeObserver.current?.disconnect();
};
}, []);
@@ -65,7 +70,7 @@ const BrowserWindow: FC = () => {
const initWebView = () => {
if (webviewRef.current) {
const rect = computeRect(webviewRef.current.getBoundingClientRect());
window.electron.setBrowserViewRect(rect);
setBrowserViewRect(rect);
// 监控 webview 元素的大小
resizeObserver.current = new ResizeObserver((entries) => {
@@ -73,7 +78,7 @@ const BrowserWindow: FC = () => {
const viewRect = computeRect(entry.contentRect);
viewRect.x += rect.x;
viewRect.y += rect.y;
window.electron.setBrowserViewRect(viewRect);
setBrowserViewRect(viewRect);
});
resizeObserver.current.observe(webviewRef.current);
@@ -116,7 +121,10 @@ const BrowserWindow: FC = () => {
<div className="browser-window">
<WindowToolBar
onClose={() => {
window.electron.closeBrowserWindow();
closeBrowserWindow();
}}
onMinimize={() => {
minimize("");
}}
>
{title}
@@ -20,6 +20,7 @@ import {
Form,
Input,
Menu,
message,
Modal,
Space,
Switch,
@@ -29,10 +30,9 @@ import onEvent from "renderer/utils/td-utils";
import {
AppstoreAddOutlined,
BlockOutlined,
CloseOutlined,
PlusOutlined,
QuestionCircleOutlined,
} from "@ant-design/icons";
import { helpUrl } from "renderer/utils/variables";
import { processHeaders } from "renderer/utils/utils";
import {
getFavs,
@@ -43,7 +43,7 @@ import {
removeVideos,
updateVideoStatus,
} from "renderer/utils/localforge";
import { ModalForm, ProFormText, ProFormTextArea } from "@ant-design/pro-form";
import { ModalForm, ProFormText } from "@ant-design/pro-form";
import { isUrl } from "renderer/utils";
import { useDispatch, useSelector } from "react-redux";
import { AppState } from "renderer/store/reducers";
@@ -52,10 +52,8 @@ import ProForm from "@ant-design/pro-form";
import useElectron from "renderer/hooks/electron";
import { nanoid } from "nanoid";
import { Settings } from "renderer/store/actions/settings.actions";
import {
MainState,
updateNotifyCount,
} from "renderer/store/actions/main.actions";
import { updateNotifyCount } from "renderer/store/actions/main.actions";
import HeaderEdit from "renderer/components/HeaderEdit";
type ActionButton = {
key: string;
@@ -116,7 +114,10 @@ const DownloadList: React.FC<Props> = ({
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
const [favsList, setFavsList] = useState<Fav[]>([]);
const [maxWidth, setMaxWidth] = useState<number>(winWidth);
const [currentSourceItem, setCurrentSourceItem] = useState<SourceItem>();
const [
currentSourceItem,
setCurrentSourceItem,
] = useState<SourceItem | null>();
const settings = useSelector<AppState, Settings>((state) => state.settings);
const dispatch = useDispatch();
const tableDataRef = useRef<SourceItem[]>([]);
@@ -125,6 +126,7 @@ const DownloadList: React.FC<Props> = ({
itemContextMenu,
addEventListener,
removeEventListener,
ipcExec,
} = useElectron();
const { exeFile } = settings;
const [formRef] = Form.useForm();
@@ -146,7 +148,7 @@ const DownloadList: React.FC<Props> = ({
return () => {
window.removeEventListener("resize", calcMaxWidth);
removeEventListener("context-menu-command", contextMenuDetail);
removeEventListener("download-context-menu-detail", contextMenuDetail);
removeEventListener(
"download-context-menu-download",
contextMenuDownload
@@ -164,7 +166,7 @@ const DownloadList: React.FC<Props> = ({
item: SourceItem
) => {
setCurrentSourceItem(item);
detailForm.setFieldsValue(preProcessFormData(item));
detailForm.setFieldsValue(item);
calcMaxWidth();
};
const contextMenuDownload = (
@@ -177,12 +179,12 @@ const DownloadList: React.FC<Props> = ({
event: Electron.IpcRendererEvent,
item: SourceItem
) => {
await removeVideo(item.url);
await removeVideo(item.id);
await updateTableData();
};
const contextMenuClearAll = async () => {
const keys = tableDataRef.current.map((item) => item.url);
await removeVideos(keys);
const ids = tableDataRef.current.map((item) => item.id);
await removeVideos(ids);
await updateTableData();
};
@@ -191,26 +193,6 @@ const DownloadList: React.FC<Props> = ({
setFavsList(favs);
};
// 表单数据预处理
const preProcessFormData = (data: SourceItem) => {
const formatter = (key: string, value: any) => {
switch (key) {
// case "headers":
// return Object.entries(value)
// .map(([key, value]) => `${key}:${value}`)
// .join("\n");
default:
return value;
}
};
const result: Record<string, any> = {};
Object.keys(data).forEach((key) => {
result[key] = formatter(key, data[key as keyof SourceItem]);
});
return result;
};
// 渲染视频下载的状态
const renderStatus = (item: SourceItem) => {
const status = item.status;
@@ -238,12 +220,6 @@ const DownloadList: React.FC<Props> = ({
window.electron.openBrowserWindow();
};
// 打开使用帮助
const openHelp = () => {
onEvent.mainPageHelp();
window.electron.openExternal(helpUrl);
};
// 向列表中插入一条数据并且请求详情
const insertUpdateTableData = async (
item: SourceItemForm
@@ -352,11 +328,12 @@ const DownloadList: React.FC<Props> = ({
};
}
const { code, msg } = await window.electron.ipcExec(exeFile, args);
const { code, msg } = await ipcExec(exeFile, args);
if (code === 0) {
await changeSourceStatus(item, SourceStatus.Success);
onEvent.mainPageDownloadSuccess({ msg, url, exeFile });
} else {
message.error(msg);
await changeSourceStatus(item, SourceStatus.Failed);
onEvent.mainPageDownloadFail({ msg, url, exeFile });
}
@@ -442,23 +419,24 @@ const DownloadList: React.FC<Props> = ({
return (
<Box p={10} borderBottom={"1px solid #EBEEF5"}>
<Space>
<Button key={"1"} onClick={newDownload}>
<AppstoreAddOutlined />
<Button
key={"1"}
onClick={newDownload}
icon={<AppstoreAddOutlined />}
size={"middle"}
>
</Button>
<Dropdown.Button
size={"middle"}
key={"2"}
trigger={["click"]}
onClick={openBrowser}
overlay={browserMenu}
icon={<BlockOutlined />}
>
<BlockOutlined />
</Dropdown.Button>
<Button key={"4"} onClick={openHelp}>
<QuestionCircleOutlined />
使
</Button>
</Space>
</Box>
);
@@ -547,7 +525,7 @@ const DownloadList: React.FC<Props> = ({
<Box display={"flex"}>
{buttons.map((button) =>
button.showTooltip ? (
<Tooltip title={button.tooltip}>
<Tooltip title={button.tooltip} placement={"left"}>
<Box
pl={10}
key={button.key}
@@ -577,7 +555,6 @@ const DownloadList: React.FC<Props> = ({
files: FileList | null,
event: ReactDragEvent<HTMLDivElement>
) => {
console.log(files, event);
if (files?.length === 1) {
// 只有一个文件被拽入
await setIsModalVisible(true);
@@ -603,6 +580,9 @@ const DownloadList: React.FC<Props> = ({
minHeight={"100%"}
minWidth={currentSourceItem ? "350px" : "100%"}
maxWidth={currentSourceItem ? maxWidth : "100%"}
style={{
borderRight: "1px solid rgb(235, 238, 245)",
}}
>
<AutoSizer className={"new-download-list"}>
{({ height, width }) => (
@@ -640,9 +620,7 @@ const DownloadList: React.FC<Props> = ({
className={"list-item-inner"}
onClick={() => {
setCurrentSourceItem(item);
detailForm.setFieldsValue(
preProcessFormData(item)
);
detailForm.setFieldsValue(item);
calcMaxWidth();
dispatch(updateNotifyCount(0));
}}
@@ -661,14 +639,29 @@ const DownloadList: React.FC<Props> = ({
{currentSourceItem && (
<Box
p={15}
pt={0}
height={"100%"}
flex={1}
overflowY={"auto"}
minW={"300px"}
>
<Box
display={"flex"}
alignItems={"center"}
justifyContent={"flex-end"}
h={40}
>
<Button
size={"small"}
icon={<CloseOutlined />}
type={"link"}
onClick={() => {
setCurrentSourceItem(null);
}}
/>
</Box>
<ProForm
form={detailForm}
size={"small"}
layout={"horizontal"}
submitter={{
searchConfig: {
@@ -687,31 +680,27 @@ const DownloadList: React.FC<Props> = ({
},
}}
>
<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"} />
<ProFormText
name={"title"}
label="视频名称"
placeholder="请输入视频名称"
/>
<ProFormText
name={"url"}
label="请求地址"
placeholder="请输入请求地址"
/>
<ProFormText
name={"workspace"}
label="本地路径"
placeholder="请输入本地路径"
/>
<ProFormText
name={"exeFile"}
label="执行程序"
placeholder="请选择可执行程序"
/>
<HeaderEdit label={"请求标头"} name={"headers"} />
</ProForm>
</Box>
)}
@@ -741,11 +730,6 @@ const DownloadList: React.FC<Props> = ({
<Button type={"link"} onClick={openBrowser}>
</Button>
<br />
<Button type={"link"} onClick={openHelp}>
</Button>
</span>
}
/>
@@ -43,7 +43,6 @@ const Setting: FC = () => {
const dispatch = useDispatch();
const formRef = useRef<FormInstance<FormData>>();
const [proxyChecked, setProxyChecked] = useState<boolean>(false);
console.log("settings: ", settings);
useEffect(() => {
const { useProxy } = settings;
+20 -27
View File
@@ -3,7 +3,6 @@ import "./index.scss";
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 { SourceStatus, SourceType } from "renderer/types";
import {
getVideos,
@@ -24,6 +23,8 @@ import {
MainState,
updateNotifyCount,
} from "renderer/store/actions/main.actions";
import { QuestionCircleOutlined } from "@ant-design/icons";
import { helpUrl } from "renderer/utils/variables";
const audio = new Audio(audioSrc);
@@ -35,7 +36,6 @@ enum TabKey {
const { TabPane } = Tabs;
const MainPage: FC = () => {
const [isDrawerVisible, setIsDrawerVisible] = useState<boolean>(false);
const [tableData, setTableData] = useState<SourceItem[]>([]);
const [activeKey, setActiveKey] = useState<TabKey>(TabKey.HomeTab);
const dispatch = useDispatch();
@@ -50,21 +50,23 @@ const MainPage: FC = () => {
addEventListener,
removeEventListener,
closeMainWindow,
store,
minimize,
} = useElectron();
useEffect(() => {
initData();
addEventListener("m3u8", handleWebViewMessage);
addEventListener("m3u8-notifier", handleWebViewMessage);
return () => {
removeEventListener("m3u8", handleWebViewMessage);
removeEventListener("m3u8-notifier", handleWebViewMessage);
};
}, []);
const initData = async () => {
// 开始初始化表格数据
const tableData = await getVideos();
const initialSettings = await window.electron.store.get();
const initialSettings = await store.get();
dispatch(updateSettings(initialSettings));
setTableData(tableData);
setActiveKey(
@@ -93,10 +95,6 @@ const MainPage: FC = () => {
dispatch(updateNotifyCount(countRef.current + 1));
};
const handleDrawerClose = (): void => {
setIsDrawerVisible(false);
};
// 首页面板切换事件
const onTabChange = (activeKey: TabKey): void => {
if (activeKey === TabKey.HomeTab) {
@@ -104,6 +102,12 @@ const MainPage: FC = () => {
}
};
// 打开使用帮助
const openHelp = () => {
onEvent.mainPageHelp();
window.electron.openExternal(helpUrl);
};
// 首页面板点击事件
const onTabClick = async (activeKey: TabKey): Promise<void> => {
if (!settings.workspace) {
@@ -146,6 +150,9 @@ const MainPage: FC = () => {
onClose={() => {
closeMainWindow();
}}
onMinimize={() => {
minimize("main");
}}
/>
<div className="main-window">
<Tabs
@@ -176,27 +183,13 @@ const MainPage: FC = () => {
</Tabs>
</div>
<Drawer
title="评论反馈"
placement="right"
closable={false}
width={500}
onClose={handleDrawerClose}
visible={isDrawerVisible}
className="comment-drawer"
>
<Comment />
</Drawer>
<div className="toolbar">
<Button
type="link"
onClick={() => {
onEvent.mainPageOpenComment();
setIsDrawerVisible(true);
}}
type={"link"}
onClick={openHelp}
icon={<QuestionCircleOutlined />}
>
使
</Button>
</div>
</div>
+8 -8
View File
@@ -29,7 +29,7 @@ const updateVideoStatus = async (
// fixme: 当数据量比较大的时候
let videos = await localforage.getItem<SourceItem[]>(keys.videos);
if (!Array.isArray(videos)) videos = [];
const findIndex = videos.findIndex((video) => source.url === video.url);
const findIndex = videos.findIndex((video) => source.id === video.id);
if (findIndex >= 0) {
videos.splice(findIndex, 1, { ...source, status });
await localforage.setItem(keys.videos, videos);
@@ -39,7 +39,7 @@ const updateVideoStatus = async (
const updateVideoTitle = async (source: SourceItem, title: string) => {
let videos = await localforage.getItem<SourceItem[]>(keys.videos);
if (!Array.isArray(videos)) videos = [];
const findIndex = videos.findIndex((video) => source.url === video.url);
const findIndex = videos.findIndex((video) => source.id === video.id);
if (findIndex >= 0) {
videos.splice(findIndex, 1, { ...source, title });
await localforage.setItem(keys.videos, videos);
@@ -49,25 +49,25 @@ const updateVideoTitle = async (source: SourceItem, title: string) => {
const updateVideoUrl = async (source: SourceItem, url: string) => {
let videos = await localforage.getItem<SourceItem[]>(keys.videos);
if (!Array.isArray(videos)) videos = [];
const findIndex = videos.findIndex((video) => source.url === video.url);
const findIndex = videos.findIndex((video) => source.id === video.id);
if (findIndex >= 0) {
videos.splice(findIndex, 1, { ...source, url });
await localforage.setItem(keys.videos, videos);
}
};
export const removeVideo = async (url: string) => {
export const removeVideo = async (id: string) => {
let videos = await localforage.getItem<SourceItem[]>(keys.videos);
if (!Array.isArray(videos)) videos = [];
const favIndex = videos.findIndex((item) => item.url === url);
const favIndex = videos.findIndex((item) => item.id === id);
if (favIndex >= 0) videos.splice(favIndex, 1);
await localforage.setItem(keys.videos, videos);
return videos;
};
const removeVideos = async (urls: (string | number)[]): Promise<void> => {
for (const url of urls) {
await removeVideo(String(url));
const removeVideos = async (ids: string[]): Promise<void> => {
for (const id of ids) {
await removeVideo(String(id));
}
};
+17 -23
View File
@@ -1,30 +1,24 @@
import tdApp from "renderer/utils/td";
const onEvent = {
browserPageGoBack: () => tdApp.onEvent("浏览器页面-点击返回按钮"),
browserPageReload: () => tdApp.onEvent("浏览器页面-点击刷新按钮"),
mainPageDownloadFail: (kv: any) => tdApp.onEvent("下载页面-下载视频失败", kv),
mainPageDownloadSuccess: (kv: any) =>
browserPageGoBack: (): void => tdApp.onEvent("浏览器页面-点击返回按钮"),
browserPageReload: (): void => tdApp.onEvent("浏览器页面-点击刷新按钮"),
mainPageDownloadFail: (kv: unknown): void =>
tdApp.onEvent("下载页面-下载视频失败", kv),
mainPageDownloadSuccess: (kv: unknown): void =>
tdApp.onEvent("下载页面-下载视频成功", kv),
mainPageOpenBrowserPage: () => tdApp.onEvent("下载页面-打开浏览器页面"),
// ===================================
mainPageNewSource: () => tdApp.onEvent("下载页面-新建下载"),
mainPageOpenLocalPath: () => tdApp.onEvent("下载页面-打开本地路径"),
mainPageHelp: () => tdApp.onEvent("下载页面-打开使用帮助"),
tableStartDownload: () => tdApp.onEvent("资源表格-下载按钮"),
tableOpenDetail: () => tdApp.onEvent("资源表格-详情按钮"),
tableReNewStatus: () => tdApp.onEvent("资源表格-重置状态"),
mainPageOpenComment: () => tdApp.onEvent("下载页面-点击评论反馈"),
mainPageSourceCode: () => tdApp.onEvent("下载页面-源码地址"),
mainUpdateLog: () => tdApp.onEvent("下载页面-更新日志"),
toFavPage: () => tdApp.onEvent("下载页面-点击切换收藏页面"),
toSettingPage: () => tdApp.onEvent("下载页面-点击切换设置页面"),
toMainPage: () => tdApp.onEvent("下载页面-点击切换主页面"),
favPageAddFav: () => tdApp.onEvent("收藏页面-添加收藏"),
favPageOpenLink: () => tdApp.onEvent("收藏页面-打开链接"),
favPageDeleteLink: () => tdApp.onEvent("收藏页面-删除链接"),
addSourceDownload: () => tdApp.onEvent("新建下载-立即下载"),
addSourceAddSource: () => tdApp.onEvent("新建下载-添加资源"),
mainPageOpenBrowserPage: (): void => tdApp.onEvent("下载页面-打开浏览器页面"),
mainPageNewSource: (): void => tdApp.onEvent("下载页面-新建下载"),
mainPageHelp: (): void => tdApp.onEvent("下载页面-打开使用帮助"),
tableStartDownload: (): void => tdApp.onEvent("资源表格-下载按钮"),
tableReNewStatus: (): void => tdApp.onEvent("资源表格-重置状态"),
toSettingPage: (): void => tdApp.onEvent("下载页面-点击切换设置页面"),
toMainPage: (): void => tdApp.onEvent("下载页面-点击切换主页面"),
favPageAddFav: (): void => tdApp.onEvent("收藏页面-添加收藏"),
favPageOpenLink: (): void => tdApp.onEvent("收藏页面-打开链接"),
favPageDeleteLink: (): void => tdApp.onEvent("收藏页面-删除链接"),
addSourceDownload: (): void => tdApp.onEvent("新建下载-立即下载"),
addSourceAddSource: (): void => tdApp.onEvent("新建下载-添加资源"),
};
export default onEvent;
+25 -19
View File
@@ -1,22 +1,28 @@
const onEvent = (eventId: string, mapKv: any = {}): void => {
try {
window.TDAPP.onEvent(eventId, "", mapKv);
console.log(`添加埋点成功:${eventId}`, mapKv);
} catch (err) {
console.warn(`添加埋点失败:${eventId}`, err);
import { version } from "../../../package.json";
const isProd = process.env.NODE_ENV === "production";
class TDEvent {
appId?: string | boolean;
vn: string;
vc: string;
constructor() {
this.appId = import.meta.env.VITE_APP_TDID;
this.vn = isProd ? `${version}生产版` : `${version}开发版`;
this.vc = `${version}`;
}
};
const init = (): void => {
const appId = import.meta.env.VITE_APP_TDID;
const vn = import.meta.env.VITE_APP_TDVN;
const vc = import.meta.env.VITE_APP_TDVC;
const script = document.createElement("script");
const src = `https://jic.talkingdata.com/app/h5/v1?appid=${appId}&vn=${vn}&vc=${vc}`;
script.src = src;
console.info("埋点地址是:", src);
const headElement = document.getElementsByTagName("head")[0];
headElement.appendChild(script);
};
init() {
const script = document.createElement("script");
script.src = `https://jic.talkingdata.com/app/h5/v1?appid=${this.appId}&vn=${this.vn}&vc=${this.vc}`;
const headElement = document.getElementsByTagName("head")[0];
headElement.appendChild(script);
}
export default { onEvent, init };
onEvent(eventId: string, mapKv: any = {}) {
window.TDAPP.onEvent(eventId, "", mapKv);
}
}
export default new TDEvent();
+1 -1
View File
@@ -1,6 +1,6 @@
import { BrowserWindow } from "electron";
import Store from "electron-store";
import { Windows } from "main/variables";
import { Windows } from "main/utils/variables";
declare global {
namespace NodeJS {
+1
View File
@@ -103,6 +103,7 @@ interface ElectronApi {
browserViewLoadURL: (url?: string) => void;
request: <T>(options: RequestOptions) => Promise<RequestResponse<T>>;
itemContextMenu: (item: SourceItem) => void;
minimize: (name: string) => void;
}
declare interface Window {