Compare commits

...

13 Commits

Author SHA1 Message Date
caorushizi 6e6b7315ca 主进程代码优化 2022-01-24 23:39:43 +08:00
caorushizi 5760855516 主进程代码优化 2022-01-24 00:07:45 +08:00
caorushizi db956ecd89 主进程代码优化 2022-01-23 23:53:55 +08:00
caorushizi 56663aa26b 主进程代码优化 2022-01-23 19:34:57 +08:00
caorushizi 3358a19185 主进程代码优化 2022-01-23 14:27:14 +08:00
caorushizi 02d061be9f 主进程代码优化 2022-01-23 01:38:09 +08:00
caorushizi 90952299cc 主进程代码优化 2022-01-23 01:37:17 +08:00
caorushizi af21763eca feature: 修复一些问题 2022-01-08 17:41:44 +08:00
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
49 changed files with 1252 additions and 775 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.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"
-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);
-66
View File
@@ -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;
+73
View File
@@ -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() {}
}
+26
View File
@@ -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;
+15
View File
@@ -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;
+12
View File
@@ -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;
+61
View File
@@ -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;
+10
View File
@@ -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 };
-69
View File
@@ -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;
+16
View File
@@ -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));
+41
View File
@@ -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 });
}
});
});
}
+53
View File
@@ -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);
}
+22
View File
@@ -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: "已经下载完成,下次打开时安装~",
});
}
+11
View File
@@ -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
View File
@@ -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();
-13
View File
@@ -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
View File
@@ -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__;
-42
View File
@@ -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();
+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,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;
+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;
+1
View File
@@ -37,4 +37,5 @@
* {
font-family: Alibaba-PuHuiTi-Regular;
@include scrollbar();
}
+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}
@@ -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>
+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();
+21 -2
View File
@@ -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 };
+15
View File
@@ -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;
}
+5 -2
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 {
@@ -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 };
+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 {