Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e6b7315ca | |||
| 5760855516 | |||
| db956ecd89 | |||
| 56663aa26b | |||
| 3358a19185 | |||
| 02d061be9f | |||
| 90952299cc | |||
| af21763eca | |||
| 71739a3ffb | |||
| db3becb071 | |||
| a458f69990 | |||
| 82cf627488 | |||
| e00bd5dab0 |
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "media-downloader",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.2",
|
||||
"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"
|
||||
|
||||
Vendored
-26
File diff suppressed because one or more lines are too long
Vendored
-21
File diff suppressed because one or more lines are too long
+10
-2
@@ -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
@@ -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);
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import windowManager from "main/window/windowManager";
|
||||
import { BrowserView } from "electron";
|
||||
import { log } from "main/utils";
|
||||
import { webviewPartition, Windows } from "main/variables";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const createBrowserView = (session: Electron.Session): void => {
|
||||
const browserWindow = windowManager.get(Windows.BROWSER_WINDOW);
|
||||
|
||||
const view = new BrowserView({
|
||||
webPreferences: {
|
||||
partition: webviewPartition,
|
||||
},
|
||||
});
|
||||
browserWindow.setBrowserView(view);
|
||||
view.setBounds({ x: 0, y: 0, height: 0, width: 0 });
|
||||
|
||||
const { webContents } = view;
|
||||
if (process.env.NODE_ENV === "development") webContents.openDevTools();
|
||||
|
||||
webContents.on("dom-ready", () => {
|
||||
const title = webContents.getTitle();
|
||||
const url = webContents.getURL();
|
||||
|
||||
browserWindow.webContents.send("dom-ready", { title, url });
|
||||
|
||||
webContents.setWindowOpenHandler((details) => {
|
||||
webContents.loadURL(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
});
|
||||
|
||||
const filter = { urls: ["*://*/*"] };
|
||||
session.webRequest.onBeforeSendHeaders(
|
||||
filter,
|
||||
(
|
||||
details,
|
||||
callback: (beforeSendResponse: Electron.BeforeSendResponse) => void
|
||||
) => {
|
||||
const m3u8Reg = /\.m3u8$/;
|
||||
let cancel = false;
|
||||
const myURL = new URL(details.url);
|
||||
if (m3u8Reg.test(myURL.pathname)) {
|
||||
log.info("在窗口中捕获 m3u8 链接: ", details.url);
|
||||
const { webContents: mainWindow } = windowManager.get(
|
||||
Windows.MAIN_WINDOW
|
||||
);
|
||||
const value: SourceUrl = {
|
||||
id: nanoid(),
|
||||
title: webContents.getTitle(),
|
||||
url: details.url,
|
||||
headers: details.requestHeaders,
|
||||
duration: 0,
|
||||
};
|
||||
mainWindow.send("m3u8", value);
|
||||
cancel = true;
|
||||
}
|
||||
callback({
|
||||
cancel,
|
||||
requestHeaders: details.requestHeaders,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default createBrowserView;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { windowManager } from "main/core/window";
|
||||
import { BrowserView } from "electron";
|
||||
import { Sessions, Windows } from "main/utils/variables";
|
||||
import { nanoid } from "nanoid";
|
||||
import { sessionList } from "main/core/session";
|
||||
import logger from "main/core/logger";
|
||||
|
||||
const createBrowserView = (): void => {
|
||||
const browserWindow = windowManager.get(Windows.BROWSER_WINDOW);
|
||||
|
||||
const view = new BrowserView({
|
||||
webPreferences: {
|
||||
partition: Sessions.PERSIST_MEDIAGO,
|
||||
},
|
||||
});
|
||||
browserWindow.setBrowserView(view);
|
||||
view.setBounds({ x: 0, y: 0, height: 0, width: 0 });
|
||||
|
||||
const { webContents } = view;
|
||||
if (process.env.NODE_ENV === "development") webContents.openDevTools();
|
||||
|
||||
webContents.on("dom-ready", () => {
|
||||
const title = webContents.getTitle();
|
||||
const url = webContents.getURL();
|
||||
|
||||
browserWindow.webContents.send("dom-ready", { title, url });
|
||||
|
||||
webContents.setWindowOpenHandler((details) => {
|
||||
webContents.loadURL(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
});
|
||||
|
||||
const filter = { urls: ["*://*/*"] };
|
||||
sessionList
|
||||
.get(Sessions.PERSIST_MEDIAGO)!
|
||||
.webRequest.onBeforeSendHeaders(
|
||||
filter,
|
||||
(
|
||||
details,
|
||||
callback: (beforeSendResponse: Electron.BeforeSendResponse) => void
|
||||
) => {
|
||||
const m3u8Reg = /\.m3u8$/;
|
||||
let cancel = false;
|
||||
const myURL = new URL(details.url);
|
||||
if (m3u8Reg.test(myURL.pathname)) {
|
||||
logger.info("在窗口中捕获 m3u8 链接: ", details.url);
|
||||
const { webContents: mainWindow } = windowManager.get(
|
||||
Windows.MAIN_WINDOW
|
||||
);
|
||||
const value: SourceUrl = {
|
||||
id: nanoid(),
|
||||
title: webContents.getTitle(),
|
||||
url: details.url,
|
||||
headers: details.requestHeaders,
|
||||
duration: 0,
|
||||
};
|
||||
mainWindow.send("m3u8-notifier", value);
|
||||
cancel = true;
|
||||
}
|
||||
callback({
|
||||
cancel,
|
||||
requestHeaders: details.requestHeaders,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default createBrowserView;
|
||||
|
||||
class Browser {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import Runner from "../runner";
|
||||
|
||||
class Downloader {
|
||||
protected bin = ""; // 可执行文件地址
|
||||
protected args = ""; // runner 参数
|
||||
|
||||
constructor(public type: string) {}
|
||||
|
||||
handle(runner: Runner): void {
|
||||
runner.setDownloader(this);
|
||||
}
|
||||
|
||||
parseArgs(args: Record<string, string>): void {
|
||||
// empty
|
||||
}
|
||||
|
||||
getBin(): string {
|
||||
return this.bin;
|
||||
}
|
||||
|
||||
getArgs(): string {
|
||||
return this.args;
|
||||
}
|
||||
}
|
||||
|
||||
export default Downloader;
|
||||
@@ -0,0 +1,15 @@
|
||||
import MediagoDownloader from "main/core/downloader/mediago-downloader";
|
||||
import NM3u8dlCliDownloader from "main/core/downloader/n-m3u8dl-cli-downloader";
|
||||
import Downloader from "main/core/downloader/downloader";
|
||||
|
||||
const createDownloader = (type: string): Downloader => {
|
||||
if (type === "mediago") {
|
||||
return new MediagoDownloader();
|
||||
}
|
||||
if (type === "N_m3u8DL-CLI") {
|
||||
return new NM3u8dlCliDownloader();
|
||||
}
|
||||
throw new Error("暂不支持该下载方式");
|
||||
};
|
||||
|
||||
export { createDownloader };
|
||||
@@ -0,0 +1,21 @@
|
||||
import Downloader from "./downloader";
|
||||
|
||||
// mediago 下载器
|
||||
class MediagoDownloader extends Downloader {
|
||||
constructor() {
|
||||
super("mediago");
|
||||
|
||||
this.bin = process.platform === "win32" ? "mediago" : "./mediago";
|
||||
}
|
||||
|
||||
parseArgs(args: Record<string, string>): void {
|
||||
this.args = Object.entries(args)
|
||||
.reduce((prev: string[], [key, value]) => {
|
||||
if (value) prev.push(`-${key} "${value}"`);
|
||||
return prev;
|
||||
}, [])
|
||||
.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
export default MediagoDownloader;
|
||||
@@ -0,0 +1,44 @@
|
||||
import Downloader from "./downloader";
|
||||
import { binDir } from "main/utils/variables";
|
||||
import semver from "semver";
|
||||
import glob from "glob";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
// N_m3u8DL-CLI 下载器
|
||||
class NM3u8dlCliDownloader extends Downloader {
|
||||
constructor() {
|
||||
super("N_m3u8DL-CLI");
|
||||
|
||||
const binNameList = glob.sync("N_m3u8DL-CLI*.exe", {
|
||||
cwd: binDir,
|
||||
});
|
||||
const [version] = binNameList
|
||||
.map((item) => /N_m3u8DL-CLI_v(.*).exe/.exec(item)?.[1] || "0.0.0")
|
||||
.filter((item) => semver.valid(item))
|
||||
.sort((a, b) => (semver.gt(a, b) ? -1 : 1));
|
||||
if (!version) throw new Error("没有找到 N_m3u8DL-CLI");
|
||||
this.bin = `N_m3u8DL-CLI_v${version}`;
|
||||
if (process.platform === "win32") {
|
||||
this.bin = `${this.bin}.exe`;
|
||||
}
|
||||
}
|
||||
|
||||
parseArgs(args: Record<string, string>): void {
|
||||
const binExist = fs.existsSync(path.resolve(binDir, this.bin));
|
||||
if (!binExist) throw new Error("没有找到 N_m3u8DL-CLI");
|
||||
|
||||
const argsStr = Object.entries(args)
|
||||
.reduce((prev: string[], [key, value]) => {
|
||||
if (key === "url") return prev;
|
||||
if (value && typeof value === "boolean") prev.push(`--${key}`);
|
||||
if (value && (typeof value === "string" || typeof value === "number"))
|
||||
prev.push(`--${key} "${value}"`);
|
||||
return prev;
|
||||
}, [])
|
||||
.join(" ");
|
||||
this.args = `"${args.url}" ${argsStr}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default NM3u8dlCliDownloader;
|
||||
@@ -0,0 +1,12 @@
|
||||
import path from "path";
|
||||
import moment from "moment";
|
||||
import logger from "electron-log";
|
||||
import { workspace } from "main/utils/variables";
|
||||
|
||||
const datetime = moment().format("YYYY-MM-DD");
|
||||
const logPath = path.resolve(workspace, `logs/${datetime}-mediago.log`);
|
||||
logger.transports.console.format = "{h}:{i}:{s} {text}";
|
||||
logger.transports.file.getFile();
|
||||
logger.transports.file.resolvePath = () => logPath;
|
||||
|
||||
export default logger;
|
||||
@@ -0,0 +1,61 @@
|
||||
import Downloader from "./downloader/downloader";
|
||||
import { spawn, SpawnOptions } from "child_process";
|
||||
import { workspace } from "main/utils/variables";
|
||||
import { argsBuilder } from "main/utils";
|
||||
import logger from "main/core/logger";
|
||||
|
||||
// runner
|
||||
class Runner {
|
||||
private static instance: Runner;
|
||||
|
||||
private constructor(private downloader?: Downloader) {}
|
||||
|
||||
static getInstance(): Runner {
|
||||
if (!Runner.instance) {
|
||||
Runner.instance = new Runner();
|
||||
}
|
||||
return Runner.instance;
|
||||
}
|
||||
|
||||
setDownloader(downloader: Downloader): void {
|
||||
this.downloader = downloader;
|
||||
}
|
||||
|
||||
run(options: SpawnOptions): Promise<void> {
|
||||
const command = this.downloader?.getBin();
|
||||
const args = this.downloader?.getArgs();
|
||||
|
||||
if (!command || !args) throw new Error("请先初始化downloader");
|
||||
logger.info("下载参数:", options.cwd, command, args);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawnCommand = spawn(command, argsBuilder(args), {
|
||||
cwd: workspace,
|
||||
detached: true,
|
||||
shell: true,
|
||||
...options,
|
||||
});
|
||||
|
||||
spawnCommand.stdout?.on("data", (data) => {
|
||||
const value = data.toString().trim();
|
||||
console.log(`stdout: ${value}`);
|
||||
});
|
||||
|
||||
spawnCommand.stderr?.on("data", (data) => {
|
||||
const value = data.toString().trim();
|
||||
console.error(`stderr: ${value}`);
|
||||
});
|
||||
|
||||
spawnCommand.on("close", (code) => {
|
||||
if (code !== 0) reject(new Error(`调用 ${command} 可执行文件执行失败`));
|
||||
else resolve();
|
||||
});
|
||||
|
||||
spawnCommand.on("error", (err) => {
|
||||
console.error(`err: ${err}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Runner;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { session, Session } from "electron";
|
||||
import { Sessions } from "main/utils/variables";
|
||||
|
||||
const sessionList = new Map<Sessions, Session>();
|
||||
|
||||
function createSession(partition: Sessions): void {
|
||||
sessionList.set(partition, session.fromPartition(partition));
|
||||
}
|
||||
|
||||
export { sessionList, createSession };
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IWindowListItem } from "types/main";
|
||||
import { BrowserWindow } from "electron";
|
||||
import { IWindowListItem, IWindowManager } from "types/main";
|
||||
import { Windows } from "main/utils/variables";
|
||||
import { resolve } from "path";
|
||||
import { Windows } from "main/variables";
|
||||
|
||||
const windowList = new Map<Windows, IWindowListItem>();
|
||||
|
||||
@@ -24,7 +25,7 @@ windowList.set(Windows.MAIN_WINDOW, {
|
||||
},
|
||||
};
|
||||
},
|
||||
callback(window) {
|
||||
async callback(window) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
window.webContents.openDevTools();
|
||||
}
|
||||
@@ -59,4 +60,42 @@ windowList.set(Windows.BROWSER_WINDOW, {
|
||||
},
|
||||
});
|
||||
|
||||
export default windowList;
|
||||
class Window implements IWindowManager {
|
||||
private windowMap: Map<Windows | string, BrowserWindow> = new Map();
|
||||
|
||||
private windowIdMap: Map<number, Windows | string> = new Map();
|
||||
|
||||
async create(name: Windows) {
|
||||
const windowConfig: IWindowListItem = windowList.get(name)!;
|
||||
const window = new BrowserWindow(windowConfig.options());
|
||||
const { id } = window;
|
||||
this.windowMap.set(name, window);
|
||||
this.windowIdMap.set(window.id, name);
|
||||
window.loadURL(windowConfig.url);
|
||||
windowConfig.callback(window, this);
|
||||
window.on("close", () => {
|
||||
this.deleteById(id);
|
||||
});
|
||||
return window;
|
||||
}
|
||||
|
||||
get(name: Windows) {
|
||||
return this.windowMap.get(name)!;
|
||||
}
|
||||
|
||||
has(name: Windows) {
|
||||
return this.windowMap.has(name);
|
||||
}
|
||||
|
||||
deleteById = (id: number) => {
|
||||
const name = this.windowIdMap.get(id);
|
||||
if (name) {
|
||||
this.windowMap.delete(name);
|
||||
this.windowIdMap.delete(id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const windowManager = new Window();
|
||||
|
||||
export { windowManager, windowList };
|
||||
@@ -1,69 +0,0 @@
|
||||
import semver from "semver";
|
||||
import { globWrapper, log, spawnWrapper } from "main/utils";
|
||||
import { binDir } from "main/variables";
|
||||
|
||||
const execM3u8DL = async (args: M3u8DLArgs): Promise<string[]> => {
|
||||
let binNameList = await globWrapper("N_m3u8DL-CLI*.exe", {
|
||||
cwd: binDir,
|
||||
});
|
||||
binNameList = binNameList
|
||||
.map((item) => /N_m3u8DL-CLI_v(.*).exe/.exec(item)?.[1] || "0.0.0")
|
||||
.filter((item) => semver.valid(item))
|
||||
.sort((a, b) => (semver.gt(a, b) ? -1 : 1));
|
||||
if (binNameList.length === 0) throw new Error("没有找到 N_m3u8DL-CLI");
|
||||
const binName = `N_m3u8DL-CLI_v${binNameList[0]}`;
|
||||
|
||||
let argsStr = Object.entries(args)
|
||||
.reduce((prev: string[], [key, value]) => {
|
||||
if (key === "url") return prev;
|
||||
if (value && typeof value === "boolean") prev.push(`--${key}`);
|
||||
if (value && typeof value === "string") prev.push(`--${key} "${value}"`);
|
||||
return prev;
|
||||
}, [])
|
||||
.join(" ");
|
||||
argsStr = `"${args.url}" ${argsStr}`;
|
||||
return [binName, argsStr];
|
||||
};
|
||||
|
||||
// 执行 mediago 二进制文件
|
||||
const execMediaGo = async (args: MediaGoArgs): Promise<string[]> => {
|
||||
const binName = process.platform === "win32" ? "mediago" : "./mediago";
|
||||
const argsStr = Object.entries(args)
|
||||
.reduce((prev: string[], [key, value]) => {
|
||||
if (value) prev.push(`-${key} "${value}"`);
|
||||
return prev;
|
||||
}, [])
|
||||
.join(" ");
|
||||
return [binName, argsStr];
|
||||
};
|
||||
|
||||
// 开始调用执行可执行文件
|
||||
const executor = async (
|
||||
exeFile: string,
|
||||
args: M3u8DLArgs | MediaGoArgs
|
||||
): Promise<void> => {
|
||||
let binName = "";
|
||||
let argsStr = "";
|
||||
|
||||
switch (exeFile) {
|
||||
case "mediago":
|
||||
[binName, argsStr] = await execMediaGo(args as MediaGoArgs);
|
||||
break;
|
||||
case "N_m3u8DL-CLI": {
|
||||
[binName, argsStr] = await execM3u8DL(args as M3u8DLArgs);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error("暂不支持该下载方式");
|
||||
}
|
||||
|
||||
log.info("下载参数:", binDir, binName, argsStr);
|
||||
|
||||
return spawnWrapper(binName, argsStr, {
|
||||
detached: true,
|
||||
shell: true,
|
||||
cwd: binDir,
|
||||
});
|
||||
};
|
||||
|
||||
export default executor;
|
||||
@@ -0,0 +1,16 @@
|
||||
import installExtension, {
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
REDUX_DEVTOOLS,
|
||||
} from "electron-devtools-installer";
|
||||
import logger from "main/core/logger";
|
||||
|
||||
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) {
|
||||
logger.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, request } from "main/utils";
|
||||
import { windowManager } from "../core/window";
|
||||
import { binDir, Windows } from "main/utils/variables";
|
||||
import Runner from "main/core/runner";
|
||||
import { createDownloader } from "main/core/downloader";
|
||||
|
||||
const handleIpc = (): void => {
|
||||
ipcMain.on("close-main-window", async () => {
|
||||
@@ -89,17 +89,33 @@ 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: Record<string, string>) => {
|
||||
try {
|
||||
const runner = Runner.getInstance();
|
||||
const downloader = createDownloader(exeFile);
|
||||
downloader.handle(runner);
|
||||
downloader.parseArgs(args);
|
||||
const result = await runner.run({ cwd: binDir });
|
||||
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));
|
||||
@@ -0,0 +1,41 @@
|
||||
import { protocol } from "electron";
|
||||
import { defaultScheme } from "main/utils/variables";
|
||||
import { URL } from "url";
|
||||
import { readFile, readFileSync } from "fs";
|
||||
import { extname, join } from "path";
|
||||
|
||||
export default function handleProtocol() {
|
||||
protocol.registerBufferProtocol(defaultScheme, (request, callback) => {
|
||||
let pathName = new URL(request.url).pathname;
|
||||
pathName = decodeURI(pathName);
|
||||
|
||||
readFile(join(__dirname, "../renderer", pathName), (error, data) => {
|
||||
if (error) {
|
||||
console.error(
|
||||
`Failed to register ${defaultScheme} protocol\n`,
|
||||
error,
|
||||
"\n"
|
||||
);
|
||||
const data = readFileSync(join(__dirname, "../renderer/index.html"));
|
||||
callback({ mimeType: "text/html", data });
|
||||
} else {
|
||||
const extension = extname(pathName).toLowerCase();
|
||||
let mimeType = "";
|
||||
|
||||
if (extension === ".js") {
|
||||
mimeType = "text/javascript";
|
||||
} else if (extension === ".html") {
|
||||
mimeType = "text/html";
|
||||
} else if (extension === ".css") {
|
||||
mimeType = "text/css";
|
||||
} else if (extension === ".svg" || extension === ".svgz") {
|
||||
mimeType = "image/svg+xml";
|
||||
} else if (extension === ".json") {
|
||||
mimeType = "application/json";
|
||||
}
|
||||
|
||||
callback({ mimeType, data });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import Store from "electron-store";
|
||||
import { Sessions, workspace } from "main/utils/variables";
|
||||
import { sessionList } from "main/core/session";
|
||||
import logger from "main/core/logger";
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
// 设置软件代理
|
||||
const setProxy = (isInit?: boolean) => {
|
||||
try {
|
||||
const webviewSession = sessionList.get(Sessions.PERSIST_MEDIAGO)!;
|
||||
const proxy = global.store.get("proxy");
|
||||
const useProxy = global.store.get("useProxy");
|
||||
if (proxy && useProxy) {
|
||||
logger.info(
|
||||
`[proxy] ${isInit ? "初始化" : "开启"}成功,代理地址为${proxy}`
|
||||
);
|
||||
webviewSession.setProxy({ proxyRules: proxy });
|
||||
} else {
|
||||
if (!isInit) logger.info(`[proxy] 关闭成功`);
|
||||
webviewSession.setProxy({});
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(
|
||||
`[proxy] ${isInit ? "初始化" : ""}设置代理失败:\n${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
setProxy(true);
|
||||
|
||||
global.store.onDidChange("useProxy", setProxy);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import logger from "main/core/logger";
|
||||
import { protocol } from "electron";
|
||||
import { defaultScheme } from "main/utils/variables";
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: defaultScheme,
|
||||
privileges: {
|
||||
secure: true,
|
||||
standard: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
export default function handleUpdater() {
|
||||
autoUpdater.logger = logger;
|
||||
autoUpdater.checkForUpdatesAndNotify({
|
||||
title: "发现新版本",
|
||||
body: "已经下载完成,下次打开时安装~",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { windowManager } from "main/core/window";
|
||||
import { Windows } from "main/utils/variables";
|
||||
import createBrowserView from "main/core/browser";
|
||||
|
||||
export default async function handleWindows(): Promise<void> {
|
||||
await Promise.all([
|
||||
windowManager.create(Windows.MAIN_WINDOW),
|
||||
await windowManager.create(Windows.BROWSER_WINDOW),
|
||||
]);
|
||||
await createBrowserView();
|
||||
}
|
||||
+32
-132
@@ -1,146 +1,46 @@
|
||||
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 { readFile, readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import { app, BrowserWindow, crashReporter } from "electron";
|
||||
import handleIpc from "main/helper/handleIpc";
|
||||
import { Sessions } from "main/utils/variables";
|
||||
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 { author, name } from "../../package.json";
|
||||
import { createSession } from "main/core/session";
|
||||
import handleProtocol from "main/helper/handleProtocol";
|
||||
import handleUpdater from "main/helper/handleUpdater";
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
const init = async (webviewSession: Electron.Session) => {
|
||||
windowManager.create(Windows.MAIN_WINDOW);
|
||||
await windowManager.create(Windows.BROWSER_WINDOW);
|
||||
await createBrowserView(webviewSession);
|
||||
};
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: defaultScheme,
|
||||
privileges: {
|
||||
secure: true,
|
||||
standard: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
protocol.registerBufferProtocol(defaultScheme, (request, callback) => {
|
||||
let pathName = new URL(request.url).pathname;
|
||||
pathName = decodeURI(pathName);
|
||||
|
||||
readFile(path.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")
|
||||
);
|
||||
callback({ mimeType: "text/html", data });
|
||||
} else {
|
||||
const extension = path.extname(pathName).toLowerCase();
|
||||
let mimeType = "";
|
||||
|
||||
if (extension === ".js") {
|
||||
mimeType = "text/javascript";
|
||||
} else if (extension === ".html") {
|
||||
mimeType = "text/html";
|
||||
} else if (extension === ".css") {
|
||||
mimeType = "text/css";
|
||||
} else if (extension === ".svg" || extension === ".svgz") {
|
||||
mimeType = "image/svg+xml";
|
||||
} else if (extension === ".json") {
|
||||
mimeType = "application/json";
|
||||
}
|
||||
|
||||
callback({ mimeType, data });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const webviewSession = createSession(webviewPartition);
|
||||
app.whenReady().then(async () => {
|
||||
app.on("activate", async () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
await init(webviewSession);
|
||||
await handleWindows();
|
||||
}
|
||||
});
|
||||
|
||||
init(webviewSession);
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
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({});
|
||||
}
|
||||
};
|
||||
|
||||
setProxy();
|
||||
|
||||
store.onDidChange("useProxy", (newValue) => {
|
||||
try {
|
||||
if (newValue) setProxy();
|
||||
} catch (e: any) {
|
||||
log.error("设置代理失败:", e.message);
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
global.store = store;
|
||||
handleUpdater();
|
||||
handleProtocol();
|
||||
createSession(Sessions.PERSIST_MEDIAGO);
|
||||
handleWindows();
|
||||
handleExtension();
|
||||
handleStore();
|
||||
handleIpc();
|
||||
});
|
||||
|
||||
handleIpc();
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { session } from "electron";
|
||||
import path from "path";
|
||||
|
||||
export default function createSession(partition: string): Electron.Session {
|
||||
const ses = session.fromPartition(partition);
|
||||
|
||||
ses.protocol.registerFileProtocol("webview", (request, callback) => {
|
||||
const url = request.url.slice(0, 10);
|
||||
callback({ path: path.normalize(`${__dirname}/${url}`) });
|
||||
});
|
||||
|
||||
return ses;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const spawnArgs = (args: string): string[] => {
|
||||
const argsBuilder = (args: string): string[] => {
|
||||
args = args || "";
|
||||
const arr = [];
|
||||
|
||||
@@ -20,13 +20,13 @@ const spawnArgs = (args: string): string[] => {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const c = args.charAt(i);
|
||||
|
||||
if (c == " ") {
|
||||
if (c === " ") {
|
||||
if (quoted) {
|
||||
quoted += c;
|
||||
} else {
|
||||
addCurrent();
|
||||
}
|
||||
} else if (c == "'" || c == '"') {
|
||||
} else if (c === "'" || c === '"') {
|
||||
if (quoted) {
|
||||
quoted += c;
|
||||
// only end this arg if the end quote is the same type as start quote
|
||||
@@ -61,4 +61,4 @@ const spawnArgs = (args: string): string[] => {
|
||||
return arr;
|
||||
};
|
||||
|
||||
export default spawnArgs;
|
||||
export default argsBuilder;
|
||||
+4
-68
@@ -1,75 +1,11 @@
|
||||
import { spawn, SpawnOptions } from "child_process";
|
||||
import glob from "glob";
|
||||
import { workspace } from "main/variables";
|
||||
import spawnArgs from "main/utils/spawn-args";
|
||||
import log from "electron-log";
|
||||
import path from "path";
|
||||
import moment from "moment";
|
||||
import argsBuilder from "main/utils/args-builder";
|
||||
import request from "./request";
|
||||
|
||||
// 封装 spawn 方法
|
||||
const spawnWrapper = (
|
||||
command: string,
|
||||
args: string,
|
||||
options: SpawnOptions
|
||||
): Promise<void> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const spawnCommand = spawn(command, spawnArgs(args), {
|
||||
cwd: workspace,
|
||||
...options,
|
||||
});
|
||||
|
||||
spawnCommand.stdout?.on("data", (data) => {
|
||||
const value = data.toString().trim();
|
||||
console.log(`stdout: ${value}`);
|
||||
});
|
||||
|
||||
spawnCommand.stderr?.on("data", (data) => {
|
||||
const value = data.toString().trim();
|
||||
console.error(`stderr: ${value}`);
|
||||
});
|
||||
|
||||
spawnCommand.on("close", (code) => {
|
||||
if (code !== 0) reject(new Error(`调用 ${command} 可执行文件执行失败`));
|
||||
else resolve();
|
||||
});
|
||||
|
||||
spawnCommand.on("error", (err) => {
|
||||
console.error(`err: ${err}`);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取文件目录
|
||||
*/
|
||||
const globWrapper: (
|
||||
pattern: string,
|
||||
options: glob.IOptions
|
||||
) => Promise<string[]> = (pattern, options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
glob(pattern, { cwd: workspace, ...options }, (err, files) => {
|
||||
if (err) reject(err);
|
||||
resolve(files);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
interface IpcResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
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,
|
||||
data: null,
|
||||
});
|
||||
|
||||
const datetime = moment().format("YYYY-MM-DD");
|
||||
const logPath = path.resolve(workspace, `logs/${datetime}-mediago.log`);
|
||||
log.transports.console.format = "{h}:{i}:{s} {text}";
|
||||
log.transports.file.getFile();
|
||||
log.transports.file.resolvePath = () => logPath;
|
||||
|
||||
export { successFn, failFn, globWrapper, spawnWrapper, log, spawnArgs };
|
||||
export { successFn, failFn, argsBuilder, request };
|
||||
|
||||
@@ -13,7 +13,6 @@ export const appName =
|
||||
? "media downloader dev"
|
||||
: "media downloader";
|
||||
export const workspace = path.resolve(appData, appName);
|
||||
export const webviewPartition = "persist:mediago";
|
||||
export const defaultScheme = "mediago";
|
||||
|
||||
export enum Windows {
|
||||
@@ -21,4 +20,8 @@ export enum Windows {
|
||||
BROWSER_WINDOW = "BROWSER_WINDOW",
|
||||
}
|
||||
|
||||
export enum Sessions {
|
||||
PERSIST_MEDIAGO = "persist:mediago",
|
||||
}
|
||||
|
||||
export const binDir = __bin__;
|
||||
@@ -1,42 +0,0 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { IWindowListItem, IWindowManager } from "types/main";
|
||||
import windowList from "./windowList";
|
||||
import { Windows } from "../variables";
|
||||
|
||||
class WindowManager implements IWindowManager {
|
||||
private windowMap: Map<Windows | string, BrowserWindow> = new Map();
|
||||
|
||||
private windowIdMap: Map<number, Windows | string> = new Map();
|
||||
|
||||
async create(name: Windows) {
|
||||
const windowConfig: IWindowListItem = windowList.get(name)!;
|
||||
const window = new BrowserWindow(windowConfig.options());
|
||||
const { id } = window;
|
||||
this.windowMap.set(name, window);
|
||||
this.windowIdMap.set(window.id, name);
|
||||
window.loadURL(windowConfig.url).then((r) => r);
|
||||
windowConfig.callback(window, this);
|
||||
window.on("close", () => {
|
||||
this.deleteById(id);
|
||||
});
|
||||
return window;
|
||||
}
|
||||
|
||||
get(name: Windows) {
|
||||
return this.windowMap.get(name)!;
|
||||
}
|
||||
|
||||
has(name: Windows) {
|
||||
return this.windowMap.has(name);
|
||||
}
|
||||
|
||||
deleteById = (id: number) => {
|
||||
const name = this.windowIdMap.get(id);
|
||||
if (name) {
|
||||
this.windowMap.delete(name);
|
||||
this.windowIdMap.delete(id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default new WindowManager();
|
||||
@@ -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);
|
||||
|
||||
@@ -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,39 @@
|
||||
$border-color: rgb(217, 217, 217);
|
||||
|
||||
.header-edit {
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.header-field-container {
|
||||
border: 1px solid $border-color;
|
||||
|
||||
.header-item-container {
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
border-right: 1px solid rgb(217, 217, 217);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.form-item-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import "./index.scss";
|
||||
import React, { FC, useCallback, useEffect, useState } from "react";
|
||||
import { AutoComplete, Form, Input, Row, Col, Button } from "antd";
|
||||
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export interface HeaderEditProps {
|
||||
label: string;
|
||||
name: string;
|
||||
valuePropName?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface HeaderFieldInputProps {
|
||||
value?: Record<string, string>;
|
||||
onChange?: (value: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
const options = [
|
||||
{ value: "Accept" },
|
||||
{ value: "Accept-Charset" },
|
||||
{ value: "Accept-Encoding" },
|
||||
{ value: "Accept-Language" },
|
||||
{ value: "Accept-Ranges" },
|
||||
{ value: "Authorization" },
|
||||
{ value: "Cache-Control" },
|
||||
{ value: "Connection" },
|
||||
{ value: "Cookie" },
|
||||
{ value: "Content-Length" },
|
||||
{ value: "Content-Type" },
|
||||
{ value: "Date" },
|
||||
{ value: "From" },
|
||||
{ value: "Host" },
|
||||
{ value: "If-Match" },
|
||||
{ value: "If-Modified-Since" },
|
||||
{ value: "If-None-Match" },
|
||||
{ value: "If-Range" },
|
||||
{ value: "If-Unmodified-Since" },
|
||||
{ value: "Max-Forwards" },
|
||||
{ value: "Pragma" },
|
||||
{ value: "Proxy-Authorization" },
|
||||
{ value: "Range" },
|
||||
{ value: "Referer" },
|
||||
{ value: "TE" },
|
||||
{ value: "Upgrade" },
|
||||
{ value: "User-Agent" },
|
||||
{ value: "Via" },
|
||||
{ value: "Warning" },
|
||||
];
|
||||
|
||||
interface FormItem {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const renderItem = (
|
||||
item: FormItem,
|
||||
onChange: (item: FormItem) => void,
|
||||
onDelete: (item: FormItem) => void
|
||||
) => {
|
||||
return (
|
||||
<Row className={"header-item-container"} key={item.id}>
|
||||
<Col span={8}>
|
||||
<AutoComplete
|
||||
value={item.key}
|
||||
style={{ width: "100%" }}
|
||||
options={options}
|
||||
dropdownMatchSelectWidth={false}
|
||||
filterOption={(inputValue, option) => {
|
||||
if (!option) return false;
|
||||
return (
|
||||
option.value.toUpperCase().indexOf(inputValue.toUpperCase()) !==
|
||||
-1
|
||||
);
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onChange({ ...item, key: value });
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={14}>
|
||||
<Input
|
||||
value={item.value}
|
||||
onChange={(e) => {
|
||||
onChange({ ...item, value: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={1} className={"form-item-action"}>
|
||||
<DeleteOutlined
|
||||
style={{ color: "#F56C6C", cursor: "pointer" }}
|
||||
onClick={() => onDelete(item)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
// 编辑 header
|
||||
const HeaderFieldInput: FC<HeaderFieldInputProps> = ({ value, onChange }) => {
|
||||
const [formValues, setFormValues] = useState<FormItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const values = processHeader(value);
|
||||
setFormValues(values);
|
||||
}, []);
|
||||
|
||||
const processHeader = (value?: Record<string, string>) => {
|
||||
if (!value) return [];
|
||||
return Object.entries(value).map(([key, value]) => ({
|
||||
id: nanoid(),
|
||||
key,
|
||||
value,
|
||||
}));
|
||||
};
|
||||
|
||||
const postHeader = (values: FormItem[]) => {
|
||||
return values.reduce((cur: Record<string, string>, prev) => {
|
||||
if (!prev.key) return cur;
|
||||
|
||||
cur[prev.key] = prev.value;
|
||||
return cur;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const onInputChange = (item: FormItem): void => {
|
||||
const copiedFormValues = formValues.slice();
|
||||
|
||||
const changeItem = copiedFormValues.find((i) => i.id === item.id);
|
||||
if (!changeItem) return;
|
||||
|
||||
changeItem.key = item.key;
|
||||
changeItem.value = item.value;
|
||||
|
||||
setFormValues(copiedFormValues);
|
||||
onChange?.(postHeader(copiedFormValues));
|
||||
};
|
||||
|
||||
const onInputDelete = (item: FormItem) => {
|
||||
const changeItemIndex = formValues.findIndex((i) => i.id === item.id);
|
||||
if (changeItemIndex < 0) return;
|
||||
|
||||
const copiedFormValues = formValues.slice();
|
||||
copiedFormValues.splice(changeItemIndex, 1);
|
||||
setFormValues(copiedFormValues);
|
||||
onChange?.(postHeader(copiedFormValues));
|
||||
};
|
||||
|
||||
const onInputAdd = () => {
|
||||
const changedValue = [...formValues, { id: nanoid(), key: "", value: "" }];
|
||||
setFormValues(changedValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{formValues.length > 0 && (
|
||||
<Box className={"header-field-container"} mb={6}>
|
||||
{formValues.map((formItem) => {
|
||||
return renderItem(formItem, onInputChange, onInputDelete);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
<Box d={"flex"} justifyContent={"space-between"} alignItems={"center"}>
|
||||
<Box>{formValues.length <= 0 && "点击添加 header"}</Box>
|
||||
<Button
|
||||
size={"small"}
|
||||
type={"link"}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onInputAdd}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Http Header 编辑组件
|
||||
const HeaderEdit: FC<HeaderEditProps> = ({ label, valuePropName, name }) => {
|
||||
return (
|
||||
<Form.Item
|
||||
className={"header-edit"}
|
||||
name={name}
|
||||
label={label}
|
||||
valuePropName={valuePropName}
|
||||
>
|
||||
<HeaderFieldInput />
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderEdit;
|
||||
@@ -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;
|
||||
|
||||
@@ -37,4 +37,5 @@
|
||||
|
||||
* {
|
||||
font-family: Alibaba-PuHuiTi-Regular;
|
||||
@include scrollbar();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
@import "../../../main";
|
||||
|
||||
.new-download-list {
|
||||
.list-item-container {
|
||||
cursor: pointer;
|
||||
.download-list-container {
|
||||
.new-download-list {
|
||||
.list-item-container {
|
||||
cursor: pointer;
|
||||
|
||||
.list-item-inner {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
.list-item-inner {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.favorite-menu {
|
||||
.ant-dropdown-menu-title-content {
|
||||
width: 100%;
|
||||
.favorite-menu {
|
||||
.ant-dropdown-menu-title-content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,18 @@ import { SourceStatus, SourceType } from "renderer/types";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Col,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
Row,
|
||||
Space,
|
||||
Switch,
|
||||
Tooltip,
|
||||
@@ -29,10 +35,11 @@ import onEvent from "renderer/utils/td-utils";
|
||||
import {
|
||||
AppstoreAddOutlined,
|
||||
BlockOutlined,
|
||||
CloseOutlined,
|
||||
DownOutlined,
|
||||
PlusOutlined,
|
||||
QuestionCircleOutlined,
|
||||
UpOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { helpUrl } from "renderer/utils/variables";
|
||||
import { processHeaders } from "renderer/utils/utils";
|
||||
import {
|
||||
getFavs,
|
||||
@@ -43,7 +50,7 @@ import {
|
||||
removeVideos,
|
||||
updateVideoStatus,
|
||||
} from "renderer/utils/localforge";
|
||||
import { ModalForm, ProFormText, ProFormTextArea } from "@ant-design/pro-form";
|
||||
import { ModalForm, ProFormSelect, 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 +59,9 @@ 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";
|
||||
import { downloaderOptions } from "renderer/utils/variables";
|
||||
|
||||
type ActionButton = {
|
||||
key: string;
|
||||
@@ -76,20 +82,6 @@ interface Props {
|
||||
updateTableData: () => Promise<void>;
|
||||
}
|
||||
|
||||
// if (currentSourceItem) {
|
||||
// if (key === "title") {
|
||||
// await updateVideoTitle(currentSourceItem, row.title);
|
||||
// } else if (key === "url") {
|
||||
// await updateVideoUrl(currentSourceItem, row.url);
|
||||
// }
|
||||
// await updateTableData();
|
||||
// setCurrentSourceItem(row);
|
||||
// }
|
||||
|
||||
const headersPlaceholder = `[可空] 请输入一行一个Header,例如:
|
||||
Origin: https://www.sample.com
|
||||
Referer: https://www.sample.com`;
|
||||
|
||||
const colorMap = {
|
||||
ready: "#108ee9",
|
||||
downloading: "#2db7f5",
|
||||
@@ -116,7 +108,12 @@ 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 [expanded, setExpanded] = useState<boolean>(true);
|
||||
const [moreOptions, setMoreOptions] = useState<boolean>(false); // todo: 初始化判断mediago
|
||||
const [
|
||||
currentSourceItem,
|
||||
setCurrentSourceItem,
|
||||
] = useState<SourceItem | null>();
|
||||
const settings = useSelector<AppState, Settings>((state) => state.settings);
|
||||
const dispatch = useDispatch();
|
||||
const tableDataRef = useRef<SourceItem[]>([]);
|
||||
@@ -125,6 +122,7 @@ const DownloadList: React.FC<Props> = ({
|
||||
itemContextMenu,
|
||||
addEventListener,
|
||||
removeEventListener,
|
||||
ipcExec,
|
||||
} = useElectron();
|
||||
const { exeFile } = settings;
|
||||
const [formRef] = Form.useForm();
|
||||
@@ -146,7 +144,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 +162,7 @@ const DownloadList: React.FC<Props> = ({
|
||||
item: SourceItem
|
||||
) => {
|
||||
setCurrentSourceItem(item);
|
||||
detailForm.setFieldsValue(preProcessFormData(item));
|
||||
detailForm.setFieldsValue(item);
|
||||
calcMaxWidth();
|
||||
};
|
||||
const contextMenuDownload = (
|
||||
@@ -177,12 +175,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 +189,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,21 +216,16 @@ const DownloadList: React.FC<Props> = ({
|
||||
window.electron.openBrowserWindow();
|
||||
};
|
||||
|
||||
// 打开使用帮助
|
||||
const openHelp = () => {
|
||||
onEvent.mainPageHelp();
|
||||
window.electron.openExternal(helpUrl);
|
||||
};
|
||||
|
||||
// 向列表中插入一条数据并且请求详情
|
||||
const insertUpdateTableData = async (
|
||||
item: SourceItemForm
|
||||
): Promise<SourceItem> => {
|
||||
const { workspace } = settings;
|
||||
const { workspace, exeFile } = settings;
|
||||
const sourceItem: SourceItem = {
|
||||
id: nanoid(),
|
||||
status: SourceStatus.Ready,
|
||||
type: SourceType.M3u8,
|
||||
exeFile,
|
||||
directory: workspace,
|
||||
title: item.title,
|
||||
duration: 0,
|
||||
@@ -324,9 +297,10 @@ const DownloadList: React.FC<Props> = ({
|
||||
const downloadFile = async (item: SourceItem): Promise<void> => {
|
||||
await changeSourceStatus(item, SourceStatus.Downloading);
|
||||
onEvent.tableStartDownload();
|
||||
const exeFile = await window.electron.store.get("exeFile");
|
||||
const { title, headers, url, exeFile: formExeFile } = item;
|
||||
|
||||
const exeFile = formExeFile || (await window.electron.store.get("exeFile"));
|
||||
const workspace = await window.electron.store.get("workspace");
|
||||
const { title, headers, url } = item;
|
||||
|
||||
let args: MediaGoArgs | M3u8DLArgs;
|
||||
if (exeFile === "mediago") {
|
||||
@@ -340,6 +314,22 @@ const DownloadList: React.FC<Props> = ({
|
||||
headers: headersString,
|
||||
};
|
||||
} else {
|
||||
const {
|
||||
checkbox,
|
||||
maxThreads,
|
||||
minThreads,
|
||||
retryCount,
|
||||
timeOut,
|
||||
stopSpeed,
|
||||
maxSpeed,
|
||||
} = item;
|
||||
const checkboxObj = Object.values(checkbox! || []).reduce(
|
||||
(prev: Record<string, boolean>, cur) => {
|
||||
prev[cur] = true;
|
||||
return prev;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const headersString = Object.entries(headers || {})
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join("|");
|
||||
@@ -349,14 +339,23 @@ const DownloadList: React.FC<Props> = ({
|
||||
saveName: title, // 设定存储文件名(不包括后缀)
|
||||
headers: headersString,
|
||||
enableDelAfterDone: item.deleteSegments,
|
||||
...checkboxObj,
|
||||
maxThreads,
|
||||
minThreads,
|
||||
retryCount,
|
||||
timeOut,
|
||||
stopSpeed,
|
||||
maxSpeed,
|
||||
};
|
||||
}
|
||||
console.log("args: ", args);
|
||||
|
||||
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 });
|
||||
}
|
||||
@@ -431,7 +430,7 @@ const DownloadList: React.FC<Props> = ({
|
||||
</Box>
|
||||
</Menu.Item>
|
||||
))}
|
||||
<Menu.Divider />
|
||||
{favsList.length > 0 && <Menu.Divider />}
|
||||
<Menu.Item key="add">{renderAddFav()}</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
@@ -442,23 +441,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 +547,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 +577,6 @@ const DownloadList: React.FC<Props> = ({
|
||||
files: FileList | null,
|
||||
event: ReactDragEvent<HTMLDivElement>
|
||||
) => {
|
||||
console.log(files, event);
|
||||
if (files?.length === 1) {
|
||||
// 只有一个文件被拽入
|
||||
await setIsModalVisible(true);
|
||||
@@ -588,7 +587,13 @@ const DownloadList: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<FileDrop onDrop={onDrop}>
|
||||
<Box h={"100%"} w={"100%"} display={"flex"} flexDirection={"column"}>
|
||||
<Box
|
||||
className={"download-list-container"}
|
||||
h={"100%"}
|
||||
w={"100%"}
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
>
|
||||
{renderToolBar()}
|
||||
{tableData.length > 0 ? (
|
||||
<Box
|
||||
@@ -603,6 +608,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 }) => (
|
||||
@@ -610,8 +618,8 @@ const DownloadList: React.FC<Props> = ({
|
||||
height={height}
|
||||
itemSize={35}
|
||||
width={width}
|
||||
itemData={tableData}
|
||||
itemCount={tableData.length}
|
||||
itemData={tableDataRef.current}
|
||||
itemCount={tableDataRef.current.length}
|
||||
itemKey={(index, data) => {
|
||||
const item = data[index];
|
||||
return item.id || `${item.title}-${index}`;
|
||||
@@ -639,11 +647,11 @@ const DownloadList: React.FC<Props> = ({
|
||||
flex={1}
|
||||
className={"list-item-inner"}
|
||||
onClick={() => {
|
||||
const { exeFile } = settings;
|
||||
setCurrentSourceItem(item);
|
||||
detailForm.setFieldsValue(
|
||||
preProcessFormData(item)
|
||||
);
|
||||
detailForm.setFieldsValue({ ...item, exeFile });
|
||||
calcMaxWidth();
|
||||
setMoreOptions(exeFile !== "mediago");
|
||||
dispatch(updateNotifyCount(0));
|
||||
}}
|
||||
>
|
||||
@@ -661,14 +669,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: {
|
||||
@@ -686,32 +709,184 @@ const DownloadList: React.FC<Props> = ({
|
||||
await downloadFile(item);
|
||||
},
|
||||
}}
|
||||
onValuesChange={(changedFields) => {
|
||||
if (changedFields.hasOwnProperty("exeFile")) {
|
||||
setMoreOptions(changedFields.exeFile !== "mediago");
|
||||
}
|
||||
}}
|
||||
size={"small"}
|
||||
>
|
||||
<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="请输入视频名称"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name={"exeFile"}
|
||||
options={downloaderOptions}
|
||||
label={"下载程序"}
|
||||
placeholder={"请选择下载程序"}
|
||||
/>
|
||||
<ProFormText
|
||||
name={"url"}
|
||||
label="请求地址"
|
||||
placeholder="请输入请求地址"
|
||||
/>
|
||||
<HeaderEdit label={"请求标头"} name={"headers"} />
|
||||
{moreOptions && (
|
||||
<>
|
||||
<Divider plain style={{ margin: "-10px 0 5px 0" }}>
|
||||
<Box
|
||||
d={"flex"}
|
||||
alignItems={"center"}
|
||||
justifyContent={"center"}
|
||||
cursor={"pointer"}
|
||||
color={"#409EFF"}
|
||||
onClick={() => {
|
||||
setExpanded((state) => !state);
|
||||
}}
|
||||
>
|
||||
{expanded ? (
|
||||
<>
|
||||
<DownOutlined />
|
||||
<Box ml={5}>展开更多高级选项</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UpOutlined />
|
||||
<Box ml={5}>收起</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Divider>
|
||||
{!expanded && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={"checkbox"}
|
||||
initialValue={["enableDelAfterDone"]}
|
||||
>
|
||||
<Checkbox.Group style={{ width: "100%" }}>
|
||||
<Row>
|
||||
<Col span={8} style={{ marginBottom: "8px" }}>
|
||||
<Checkbox value="enableDelAfterDone">
|
||||
合并后删除分片
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8} style={{ marginBottom: "8px" }}>
|
||||
<Checkbox value="disableDateInfo">
|
||||
不写入日期
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8} style={{ marginBottom: "8px" }}>
|
||||
<Checkbox value="noProxy">
|
||||
不使用系统代理
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8} style={{ marginBottom: "8px" }}>
|
||||
<Checkbox value="enableParseOnly">
|
||||
仅解析m3u8
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8} style={{ marginBottom: "8px" }}>
|
||||
<Checkbox value="enableMuxFastStart">
|
||||
混流MP4
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8} style={{ marginBottom: "8px" }}>
|
||||
<Checkbox value="noMerge">
|
||||
下载完不合并
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Checkbox value="enableBinaryMerge">
|
||||
使用二进制合并
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Checkbox value="enableAudioOnly">
|
||||
仅合并音频轨道
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Checkbox value="disableIntegrityCheck">
|
||||
关闭完整性检查
|
||||
</Checkbox>
|
||||
</Col>
|
||||
</Row>
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name={"maxThreads"}
|
||||
label={"最大线程"}
|
||||
labelCol={{ style: { width: "86px" } }}
|
||||
labelAlign={"left"}
|
||||
initialValue={32}
|
||||
>
|
||||
<InputNumber placeholder="placeholder" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name={"minThreads"}
|
||||
label={"最小线程"}
|
||||
labelCol={{ style: { width: "86px" } }}
|
||||
labelAlign={"left"}
|
||||
initialValue={16}
|
||||
>
|
||||
<InputNumber placeholder="placeholder" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name={"retryCount"}
|
||||
label={"重试次数"}
|
||||
labelCol={{ style: { width: "86px" } }}
|
||||
labelAlign={"left"}
|
||||
initialValue={15}
|
||||
>
|
||||
<InputNumber placeholder="placeholder" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name={"timeOut"}
|
||||
label={"超时时长(s)"}
|
||||
labelCol={{ style: { width: "86px" } }}
|
||||
labelAlign={"left"}
|
||||
initialValue={10}
|
||||
>
|
||||
<InputNumber placeholder="placeholder" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name={"stopSpeed"}
|
||||
label={"停速(KB/s)"}
|
||||
labelCol={{ style: { width: "86px" } }}
|
||||
labelAlign={"left"}
|
||||
initialValue={0}
|
||||
>
|
||||
<InputNumber placeholder="placeholder" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name={"maxSpeed"}
|
||||
label={"限速(KB/s)"}
|
||||
labelCol={{ style: { width: "86px" } }}
|
||||
labelAlign={"left"}
|
||||
initialValue={0}
|
||||
>
|
||||
<InputNumber placeholder="placeholder" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ProForm>
|
||||
</Box>
|
||||
)}
|
||||
@@ -741,11 +916,6 @@ const DownloadList: React.FC<Props> = ({
|
||||
<Button type={"link"} onClick={openBrowser}>
|
||||
打开浏览器
|
||||
</Button>
|
||||
<br />
|
||||
或者
|
||||
<Button type={"link"} onClick={openHelp}>
|
||||
查看帮助
|
||||
</Button>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
@@ -788,13 +958,7 @@ const DownloadList: React.FC<Props> = ({
|
||||
>
|
||||
<Input placeholder="[可空] 默认当前时间戳" allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item label="请求标头" name="headers">
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder={headersPlaceholder}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<HeaderEdit label={"请求标头"} name={"headers"} />
|
||||
<Form.Item
|
||||
label="下载完成是否删除"
|
||||
name="delete"
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
} from "renderer/store/actions/settings.actions";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { version } from "../../../../../../package.json";
|
||||
import { downloaderOptions } from "renderer/utils/variables";
|
||||
|
||||
interface FormData {
|
||||
exeFile: string;
|
||||
@@ -23,27 +25,12 @@ interface FormData {
|
||||
proxy: string;
|
||||
}
|
||||
|
||||
const downloaderOptions = [
|
||||
{
|
||||
value: "mediago",
|
||||
label: "mediago",
|
||||
},
|
||||
];
|
||||
|
||||
if (window.electron.isWindows) {
|
||||
downloaderOptions.push({
|
||||
value: "N_m3u8DL-CLI",
|
||||
label: "N_m3u8DL-CLI(推荐)",
|
||||
});
|
||||
}
|
||||
|
||||
// 设置页面
|
||||
const Setting: FC = () => {
|
||||
const settings = useSelector<AppState, Settings>((state) => state.settings);
|
||||
const dispatch = useDispatch();
|
||||
const formRef = useRef<FormInstance<FormData>>();
|
||||
const [proxyChecked, setProxyChecked] = useState<boolean>(false);
|
||||
console.log("settings: ", settings);
|
||||
|
||||
useEffect(() => {
|
||||
const { useProxy } = settings;
|
||||
@@ -100,6 +87,9 @@ const Setting: FC = () => {
|
||||
formRef={formRef}
|
||||
layout="horizontal"
|
||||
submitter={false}
|
||||
labelCol={{ style: { width: "130px" } }}
|
||||
labelAlign={"left"}
|
||||
colon={false}
|
||||
initialValues={{ workspace, exeFile, tip, proxy }}
|
||||
onValuesChange={async (changedValue) => {
|
||||
if (Object.keys(changedValue).includes("tip")) {
|
||||
@@ -120,7 +110,7 @@ const Setting: FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ProFormGroup label="基础设置">
|
||||
<ProFormGroup label="基础设置" direction={"vertical"}>
|
||||
<ProFormText
|
||||
width="xl"
|
||||
disabled
|
||||
@@ -152,7 +142,7 @@ const Setting: FC = () => {
|
||||
}}
|
||||
/>
|
||||
</ProFormGroup>
|
||||
<ProFormGroup label="下载设置">
|
||||
<ProFormGroup label="下载设置" direction={"vertical"}>
|
||||
<ProFormSelect
|
||||
allowClear={false}
|
||||
width="xl"
|
||||
@@ -174,6 +164,9 @@ const Setting: FC = () => {
|
||||
</Button>
|
||||
</Space>
|
||||
</ProForm.Item>
|
||||
<ProForm.Item label={"当前版本"}>
|
||||
<div>{version}</div>
|
||||
</ProForm.Item>
|
||||
</ProFormGroup>
|
||||
</ProForm>
|
||||
</Box>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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,3 +1,22 @@
|
||||
export const helpUrl =
|
||||
const helpUrl =
|
||||
"https://blog.ziying.site/post/media-downloader-how-to-use/?form=client";
|
||||
export const browserHomePageUrl = "https://baidu.com";
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const downloaderOptions: Option[] = [];
|
||||
|
||||
if (window.electron.isWindows) {
|
||||
downloaderOptions.push({
|
||||
value: "N_m3u8DL-CLI",
|
||||
label: "N_m3u8DL-CLI(推荐)",
|
||||
});
|
||||
}
|
||||
downloaderOptions.push({
|
||||
value: "mediago",
|
||||
label: "mediago",
|
||||
});
|
||||
|
||||
export { downloaderOptions, helpUrl };
|
||||
|
||||
Vendored
+15
@@ -15,6 +15,15 @@ declare type SourceItem = SourceUrl & {
|
||||
deleteSegments: boolean;
|
||||
directory: string;
|
||||
createdAt: number;
|
||||
exeFile: string;
|
||||
// 额外字段
|
||||
checkbox?: string[];
|
||||
maxThreads?: number;
|
||||
minThreads?: number;
|
||||
retryCount?: number;
|
||||
timeOut?: number;
|
||||
stopSpeed?: number;
|
||||
maxSpeed?: number;
|
||||
};
|
||||
|
||||
declare interface Fav {
|
||||
@@ -71,3 +80,9 @@ declare interface VideoDetail {
|
||||
segmentsLen: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
declare interface IpcResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
Vendored
+5
-2
@@ -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 {
|
||||
@@ -21,7 +21,10 @@ declare interface IWindowManager {
|
||||
declare interface IWindowListItem {
|
||||
url: string;
|
||||
options: () => Electron.BrowserWindowConstructorOptions;
|
||||
callback: (window: BrowserWindow, windowManager: IWindowManager) => void;
|
||||
callback: (
|
||||
window: BrowserWindow,
|
||||
windowManager: IWindowManager
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export { IWindowManager, IWindowListItem };
|
||||
|
||||
Vendored
+1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user