Compare commits
115 Commits
v2.2.0-beta.1
...
v2.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 5461e74539 | |||
| 417484cb6d | |||
| 9aaffd3550 | |||
| 94759d48c7 | |||
| c0e343b696 | |||
| f18ac7c444 | |||
| f637610734 | |||
| 5e1046dd5b | |||
| f25584cb2f | |||
| 03999585d0 | |||
| 772c2b0300 | |||
| 3622e67a56 | |||
| c51630cdd6 | |||
| dab1c55a93 | |||
| 7b966bc988 | |||
| e5e46bee38 | |||
| 3365d96a03 | |||
| fb223cd493 | |||
| 60d49c4773 | |||
| e7aa4ae147 | |||
| c45be41ddc | |||
| 85b37facee | |||
| 90567009ef | |||
| 899afc156e | |||
| 9e33b7e488 | |||
| e7ba60a51f | |||
| 2a3ad325c2 | |||
| 7575b80d31 | |||
| 8bb9d119b5 | |||
| 5da3f2b1c7 | |||
| da6cbcfb75 | |||
| 3d7aff41ee | |||
| 2cc49e8679 | |||
| 0e961c0647 | |||
| 4d5757a6ce | |||
| 62ae0e5075 | |||
| 7513fe2882 | |||
| c945dc8360 | |||
| de7e0da045 | |||
| a706429395 | |||
| f26c9761c3 | |||
| 9424dd4c0e | |||
| 271294a8e7 | |||
| 6b2786292f | |||
| c36247b5e1 | |||
| c182afff2c | |||
| 1129d70dca | |||
| bde71d13f9 | |||
| 67b513d380 | |||
| 8d8d1ee238 | |||
| 7c10715607 | |||
| a939f06fcd | |||
| c50625b0c8 | |||
| bfa2d88935 | |||
| b04ea61e83 | |||
| 2a42a02f3d | |||
| 1e80278a1d | |||
| f38d9156c5 | |||
| 0f32864079 | |||
| f8fbecebc9 | |||
| 40c5656080 | |||
| ddf36a7091 | |||
| 258c67554d | |||
| 007ff31200 | |||
| ff75e3d94b | |||
| aaa18e0048 | |||
| 5c6f89a067 | |||
| c4efc4c293 | |||
| 2d955257e6 | |||
| 3cf84a2992 | |||
| 94a795fe69 | |||
| cc10ae8b2e | |||
| 63ed7cdaca | |||
| 064919e0a2 | |||
| 31b2652c9b | |||
| d080d709d1 | |||
| 0c301c79f2 | |||
| a1dbb6234f | |||
| 30248194af | |||
| 8f54ebeb3a | |||
| d8ef0b9eee | |||
| 6a3bfd071a | |||
| 849df04c05 | |||
| a40ae5ef82 | |||
| 00c535665f | |||
| 8b4a4d8ff8 | |||
| f9fecd541c | |||
| 93b1028498 | |||
| a0a3dba188 | |||
| 92ded00495 | |||
| bcbe258f60 | |||
| 660f9c7c9b | |||
| 8c5ef29894 | |||
| 612fc4c533 | |||
| 635377d227 | |||
| dc86ab8f4c | |||
| 36b63ad916 | |||
| e1b378811d | |||
| 37fb9bba75 | |||
| 848253c678 | |||
| b64e779ed1 | |||
| 046a62cde3 | |||
| 1a9e6efb90 | |||
| fd422c15ba | |||
| c338f76f8d | |||
| 4285b3f4f7 | |||
| d0109869a6 | |||
| d29ad44c54 | |||
| be61b07fea | |||
| e5e171fb6f | |||
| 805d97c3ca | |||
| 040222f259 | |||
| 09ed55eba4 | |||
| a6d4f8cf98 | |||
| d19b2394e8 |
+37
-31
@@ -1,38 +1,44 @@
|
||||
behaviour
|
||||
commitlint
|
||||
conventionalcommits
|
||||
optimizelegibility
|
||||
pinia
|
||||
tiptap
|
||||
vuedraggable
|
||||
vuejs
|
||||
TDAPP
|
||||
reduxjs
|
||||
bilibili
|
||||
localforage
|
||||
acodec
|
||||
adblocker
|
||||
ahooks
|
||||
antd
|
||||
aplus
|
||||
behaviour
|
||||
bili
|
||||
bilibili
|
||||
bodys
|
||||
cancle
|
||||
cliqz
|
||||
commitlint
|
||||
Conpty
|
||||
consola
|
||||
conventionalcommits
|
||||
datetime
|
||||
DDTHH
|
||||
execa
|
||||
idtype
|
||||
immer
|
||||
inversify
|
||||
KHTML
|
||||
languagedetector
|
||||
localforage
|
||||
mediago
|
||||
metas
|
||||
nocheck
|
||||
optimizelegibility
|
||||
pinia
|
||||
redownload
|
||||
Redownload
|
||||
reduxjs
|
||||
Sider
|
||||
stylelint
|
||||
svgz
|
||||
TDAPP
|
||||
tiptap
|
||||
typeorm
|
||||
vitepress
|
||||
vuedraggable
|
||||
vuejs
|
||||
waline
|
||||
watting
|
||||
Watting
|
||||
cancle
|
||||
Sider
|
||||
inversify
|
||||
datetime
|
||||
typeorm
|
||||
bili
|
||||
mediago
|
||||
nocheck
|
||||
metas
|
||||
DDTHH
|
||||
Conpty
|
||||
execa
|
||||
bodys
|
||||
cliqz
|
||||
adblocker
|
||||
acodec
|
||||
KHTML
|
||||
svgz
|
||||
stylelint
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
APP_NAME=mediago
|
||||
APP_ID=mediago.ziying.site
|
||||
APP_COPYRIGHT=caorushizi
|
||||
APP_VERSION=2.2.0-beta.1
|
||||
|
||||
APP_SERVER_PORT=8433
|
||||
APP_VERSION=2.2.3
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
APP_TD_APPID=
|
||||
APP_CLARITY_APPID=
|
||||
|
||||
GH_TOKEN=
|
||||
LOAD_DEVTOOLS=
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
APP_TD_APPID=
|
||||
APP_CLARITY_APPID=
|
||||
|
||||
GH_TOKEN=
|
||||
DEBUG_PLUGINS=false
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
overrides: [],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {},
|
||||
};
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, macos-12]
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
|
||||
# create steps
|
||||
steps:
|
||||
@@ -48,6 +48,7 @@ jobs:
|
||||
with:
|
||||
version: latest
|
||||
run_install: true
|
||||
standalone: true
|
||||
|
||||
- name: Build & release app
|
||||
run: pnpm run release
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
本项目支持 m3u8 视频在线提取工具 流媒体下载 m3u8 下载。
|
||||
|
||||
- **✅ 无需抓包**: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
|
||||
- **📱 移动播放**: 可以轻松无缝的在PC和移动设备之前切换,下载完成后即可使用手机观看视频。
|
||||
- **📱 移动播放**: 可以轻松无缝的在 PC 和移动设备之前切换,下载完成后即可使用手机观看视频。
|
||||
- **⚡️ 批量下载**: 支持同时下载多个视频和直播资源,高速带宽不闲置。
|
||||
|
||||
## Quickstart
|
||||
|
||||
运行代码需要 node 、 pnpm 和 go,node、go 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
|
||||
运行代码需要 node 和 pnpm,node 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
|
||||
|
||||
## 运行代码
|
||||
|
||||
@@ -50,6 +50,19 @@ pnpm release
|
||||
|
||||
## Releases
|
||||
|
||||
**v2.2.0 (2024.5.22 发布)**
|
||||
|
||||
- [windows mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.exe)
|
||||
- [macos mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.dmg)
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 支持下载直播流
|
||||
- 支持哔哩哔哩视频下载
|
||||
- 优化沉浸式嗅探流程
|
||||
- 支持下载控制台输出
|
||||
- 修复了一些 bug
|
||||
|
||||
**v2.0.2(2023.7.9 发布)**
|
||||
|
||||
- [windows mediago v2.0.2](https://github.com/caorushizi/mediago/releases/download/v2.0.2/media-downloader-setup-2.0.2.exe)
|
||||
@@ -66,12 +79,6 @@ pnpm release
|
||||
- 支持修改同时下载数量
|
||||
- 修复了一些 bug
|
||||
|
||||
**v1.1.5(2022.2.5 发布)**
|
||||
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
|
||||
**更新日志**
|
||||
|
||||
## 软件截图
|
||||
|
||||

|
||||
@@ -88,6 +95,4 @@ pnpm release
|
||||
|
||||
## 鸣谢
|
||||
|
||||
N_m3u8DL-CLI 来自于 <https://github.com/nilaoda/N_m3u8DL-CLI>
|
||||
|
||||
mediago 来自于 <https://github.com/caorushizi/mediago>
|
||||
N_m3u8DL-RE 来自于 <https://github.com/nilaoda/N_m3u8DL-RE>
|
||||
|
||||
+18
-1
@@ -8,11 +8,28 @@ outline: deep
|
||||
这篇文章是软件的简单说明,可以快速上手使用本本软件。
|
||||
|
||||
::: tip
|
||||
我建了一个 QQ 群,方便大家一起沟通交流: 574209001
|
||||
方便大家一起沟通交流, 可以加入 QQ 反馈群:
|
||||
|
||||
MediaGo 反馈群 1: 574209001
|
||||
|
||||
MediaGo 反馈群 2: 921619626
|
||||
:::
|
||||
|
||||
## 下载安装
|
||||
|
||||
**v2.2.0 (2024.5.22 发布)**
|
||||
|
||||
- [windows mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.exe)
|
||||
- [macos mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.dmg)
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 支持下载直播流
|
||||
- 支持哔哩哔哩视频下载
|
||||
- 优化沉浸式嗅探流程
|
||||
- 支持下载控制台输出
|
||||
- 修复了一些 bug
|
||||
|
||||
**v2.0.1(2023.7.1 发布)**
|
||||
|
||||
- [windows mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{ languageOptions: { globals: globals.browser } },
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
+20
-19
@@ -2,6 +2,8 @@
|
||||
"name": "mediago",
|
||||
"version": "0.1.0",
|
||||
"description": "mediago",
|
||||
"author": "caorushizi",
|
||||
"homepage": "https://downloader.caorushizi.cn/",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -10,7 +12,7 @@
|
||||
"build:plugin": "pnpm -F plugin run build",
|
||||
"build:main": "pnpm -F main run build",
|
||||
"build:renderer": "pnpm -F renderer run build",
|
||||
"prerelease": "pnpm run build && pnpm -F mediago run pack",
|
||||
"beta": "pnpm run build && pnpm -F mediago run pack",
|
||||
"release": "pnpm run build && pnpm -F main run release",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
@@ -24,7 +26,6 @@
|
||||
"lint-staged": "pnpm --recursive -F \"./packages/*\" run lint-staged"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"config": {
|
||||
"commitizen": {
|
||||
@@ -32,25 +33,25 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^18.6.0",
|
||||
"@commitlint/config-conventional": "^18.6.0",
|
||||
"@commitlint/cz-commitlint": "^18.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@waline/client": "^3.1.3",
|
||||
"@commitlint/cli": "^19.3.0",
|
||||
"@commitlint/config-conventional": "^19.2.2",
|
||||
"@commitlint/core": "^19.3.0",
|
||||
"@commitlint/cz-commitlint": "^19.2.0",
|
||||
"@commitlint/format": "^19.3.0",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@waline/client": "^3.2.0",
|
||||
"commitizen": "^4.3.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cspell": "^8.3.2",
|
||||
"cz-git": "^1.8.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"husky": "^9.0.10",
|
||||
"prettier": "3.2.5",
|
||||
"tsx": "^4.7.1",
|
||||
"vitepress": "1.0.1",
|
||||
"vue": "^3.4.15",
|
||||
"zx": "^7.2.3"
|
||||
"cspell": "^8.8.3",
|
||||
"cz-git": "^1.9.2",
|
||||
"eslint": "^9.3.0",
|
||||
"globals": "^15.3.0",
|
||||
"husky": "^9.0.11",
|
||||
"tsx": "^4.11.0",
|
||||
"typescript-eslint": "^7.10.0",
|
||||
"vitepress": "1.2.2",
|
||||
"vue": "^3.4.27",
|
||||
"zx": "^8.1.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
./types
|
||||
@@ -1,23 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
overrides: [],
|
||||
ignorePatterns: ["app"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
"prettier/prettier": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "mediago",
|
||||
"version": "2.2.0-beta.1",
|
||||
"version": "2.2.3",
|
||||
"description": "在线视频下载器",
|
||||
"main": "main/index.js",
|
||||
"author": "",
|
||||
"author": "caorushizi",
|
||||
"homepage": "https://downloader.caorushizi.cn/",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "^1.27.3",
|
||||
@@ -11,6 +12,6 @@
|
||||
"node-pty": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^28.2.6"
|
||||
"electron": "30.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,16 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{
|
||||
languageOptions: { globals: globals.node },
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export { dev, build, release } from "./scripts";
|
||||
+42
-41
@@ -2,15 +2,16 @@
|
||||
"name": "main",
|
||||
"version": "0.1.0",
|
||||
"description": "在线视频下载器",
|
||||
"type": "module",
|
||||
"main": "main/index.js",
|
||||
"scripts": {
|
||||
"postinstall": "electron-rebuild -f -w",
|
||||
"dev": "cross-env NODE_ENV=development VITE_CJS_IGNORE_WARNING=true tsx scripts/dev.ts",
|
||||
"build": "cross-env NODE_ENV=production tsx scripts/build.ts",
|
||||
"pack": "cross-env NODE_ENV=development tsx scripts/release.ts",
|
||||
"dev": "cross-env NODE_ENV=development gulp dev",
|
||||
"pack": "cross-env NODE_ENV=development gulp release",
|
||||
"build": "cross-env NODE_ENV=production gulp build",
|
||||
"release": "cross-env NODE_ENV=production gulp release",
|
||||
"types": "tsc",
|
||||
"types:watch": "tsc -w",
|
||||
"release": "cross-env NODE_ENV=production tsx scripts/release.ts",
|
||||
"lint-staged": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -19,58 +20,58 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "^3.5.0",
|
||||
"@rushstack/eslint-patch": "^1.7.0",
|
||||
"@electron/rebuild": "^3.6.0",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@types/electron-devtools-installer": "^2.2.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/gulp": "^4.0.17",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "^20.11.5",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"@types/semver": "^7.5.8",
|
||||
"consola": "^3.2.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.3.2",
|
||||
"electron": "^28.2.6",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-updater": "^6.1.7",
|
||||
"esbuild": "^0.19.11",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard-with-typescript": "^43.0.1",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-n": "16.6.2",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"prebuild-install": "^7.1.1",
|
||||
"prettier": "3.2.5",
|
||||
"semver": "^7.5.4",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12"
|
||||
"del": "^7.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"electron": "30.0.8",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-updater": "^6.1.8",
|
||||
"esbuild": "^0.21.4",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^9.3.0",
|
||||
"glob": "^10.4.1",
|
||||
"globals": "^15.3.0",
|
||||
"gulp": "^5.0.0",
|
||||
"prebuild-install": "^7.1.2",
|
||||
"semver": "^7.6.2",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^7.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "^1.27.3",
|
||||
"@cliqz/adblocker-electron-preload": "^1.27.3",
|
||||
"axios": "^1.6.5",
|
||||
"better-sqlite3": "^9.3.0",
|
||||
"@cliqz/adblocker-electron": "1.27.3",
|
||||
"@cliqz/adblocker-electron-preload": "1.27.3",
|
||||
"axios": "^1.7.2",
|
||||
"better-sqlite3": "^10.0.0",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"dayjs": "^1.11.10",
|
||||
"dayjs": "^1.11.11",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-is-dev": "^3.0.1",
|
||||
"electron-log": "^5.0.3",
|
||||
"electron-store": "^8.1.0",
|
||||
"execa": "^8.0.1",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^9.0.0",
|
||||
"execa": "^9.1.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"inversify": "^6.0.2",
|
||||
"lint-staged": "^15.2.2",
|
||||
"lint-staged": "^15.2.5",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "^5.0.7",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"node-pty": "^1.0.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"typeorm": "^0.3.19"
|
||||
"typeorm": "0.3.20"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import {
|
||||
mainResolve,
|
||||
loadDotEnvDefined,
|
||||
copyResource,
|
||||
removeResource,
|
||||
} from "./utils";
|
||||
import { external } from "./config";
|
||||
|
||||
const mainDefined = loadDotEnvDefined();
|
||||
|
||||
removeResource([mainResolve("app/build")]);
|
||||
removeResource([mainResolve("app/bin")]);
|
||||
|
||||
const path = "build/Release/better_sqlite3.node";
|
||||
|
||||
copyResource([
|
||||
{
|
||||
from: mainResolve("node_modules/better-sqlite3", path),
|
||||
to: mainResolve("app", path),
|
||||
},
|
||||
{
|
||||
from: mainResolve("bin"),
|
||||
to: mainResolve("app/bin"),
|
||||
},
|
||||
]);
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: [mainResolve("src/index.ts")],
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
sourcemap: false,
|
||||
target: ["node16.13"],
|
||||
external,
|
||||
define: {
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
...mainDefined,
|
||||
},
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
minify: true,
|
||||
plugins: [],
|
||||
});
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: [mainResolve("src/preload.ts")],
|
||||
bundle: true,
|
||||
platform: "browser",
|
||||
target: ["chrome89"],
|
||||
sourcemap: false,
|
||||
external,
|
||||
define: {
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
...mainDefined,
|
||||
},
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
minify: true,
|
||||
});
|
||||
@@ -1,4 +1,8 @@
|
||||
export const external = [
|
||||
import { Configuration } from "electron-builder";
|
||||
import { Env, isDev, mainResolve } from "./utils";
|
||||
import esbuild from "esbuild";
|
||||
|
||||
const external = [
|
||||
"electron",
|
||||
"nock",
|
||||
"aws-sdk",
|
||||
@@ -6,3 +10,129 @@ export const external = [
|
||||
"@cliqz/adblocker-electron-preload",
|
||||
"node-pty",
|
||||
];
|
||||
|
||||
function getConfig(): esbuild.BuildOptions {
|
||||
const getDefine = (): Record<string, string> => {
|
||||
if (isDev) {
|
||||
return {
|
||||
__bin__: `"${mainResolve("app/bin").replace(/\\/g, "\\\\")}"`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...Env.getInstance().loadDotEnvDefined(),
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
bundle: true,
|
||||
sourcemap: process.env.NODE_ENV === "development",
|
||||
external,
|
||||
define: getDefine(),
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
minify: process.env.NODE_ENV === "production",
|
||||
};
|
||||
}
|
||||
|
||||
function buildOptions(
|
||||
entry: string,
|
||||
platform: esbuild.Platform,
|
||||
target: string
|
||||
): esbuild.BuildOptions {
|
||||
return {
|
||||
...getConfig(),
|
||||
entryPoints: [mainResolve(entry)],
|
||||
platform: platform,
|
||||
target: [target],
|
||||
};
|
||||
}
|
||||
|
||||
export function browserOptions(entry: string): esbuild.BuildOptions {
|
||||
return buildOptions(entry, "browser", "chrome89");
|
||||
}
|
||||
|
||||
export function nodeOptions(entry: string): esbuild.BuildOptions {
|
||||
return buildOptions(entry, "node", "node16.13");
|
||||
}
|
||||
|
||||
export function getReleaseConfig(): Configuration {
|
||||
return {
|
||||
productName: process.env.APP_NAME,
|
||||
buildVersion: process.env.APP_VERSION,
|
||||
appId: process.env.APP_ID,
|
||||
copyright: process.env.APP_COPYRIGHT,
|
||||
artifactName: "${productName}-setup-${arch}-${buildVersion}.${ext}",
|
||||
npmRebuild: true,
|
||||
directories: {
|
||||
output: "./release",
|
||||
},
|
||||
files: [
|
||||
{
|
||||
from: "./build",
|
||||
to: "./",
|
||||
},
|
||||
"./package.json",
|
||||
],
|
||||
extraResources: [
|
||||
{
|
||||
from: "./app/plugin",
|
||||
to: "plugin",
|
||||
},
|
||||
{
|
||||
from: "./app/bin/",
|
||||
to: "bin",
|
||||
},
|
||||
],
|
||||
win: {
|
||||
icon: "../assets/icon.ico",
|
||||
target: [
|
||||
{
|
||||
target: "nsis",
|
||||
arch: ["x64"],
|
||||
},
|
||||
],
|
||||
},
|
||||
dmg: {
|
||||
contents: [
|
||||
{
|
||||
x: 410,
|
||||
y: 150,
|
||||
type: "link",
|
||||
path: "/Applications",
|
||||
},
|
||||
{
|
||||
x: 130,
|
||||
y: 150,
|
||||
type: "file",
|
||||
},
|
||||
],
|
||||
},
|
||||
mac: {
|
||||
icon: "../assets/icon.icns",
|
||||
target: [
|
||||
{
|
||||
target: "dmg",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
],
|
||||
},
|
||||
linux: {
|
||||
category: "Utility",
|
||||
icon: "../assets/icon.icns",
|
||||
maintainer: "caorushizi <84996057@qq.com>",
|
||||
target: {
|
||||
target: "deb",
|
||||
arch: ["x64"],
|
||||
},
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
allowElevation: true,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
|
||||
import electron from "electron";
|
||||
import * as esbuild from "esbuild";
|
||||
import chokidar from "chokidar";
|
||||
import { loadDotEnvRuntime, mainResolve, log, copyResource } from "./utils";
|
||||
import { external } from "./config";
|
||||
|
||||
let electronProcess: ChildProcessWithoutNullStreams | null = null;
|
||||
|
||||
process.env.NODE_ENV = "development";
|
||||
loadDotEnvRuntime();
|
||||
|
||||
async function copySource() {
|
||||
const path = "build/Release/better_sqlite3.node";
|
||||
|
||||
copyResource([
|
||||
{
|
||||
from: mainResolve("node_modules/better-sqlite3", path),
|
||||
to: mainResolve("app", path),
|
||||
},
|
||||
{
|
||||
from: mainResolve("bin"),
|
||||
to: mainResolve("app/bin"),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const buildConfig: esbuild.BuildOptions = {
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
sourcemap: true,
|
||||
target: ["node20.9"],
|
||||
external,
|
||||
define: {
|
||||
// 开发环境中二进制可执行文件的路径
|
||||
__bin__: `"${mainResolve("bin", process.platform).replace(/\\/g, "\\\\")}"`,
|
||||
},
|
||||
plugins: [],
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
};
|
||||
|
||||
function startElectron() {
|
||||
const args = ["--inspect=5858", mainResolve("app/build/main/index.js")];
|
||||
|
||||
electronProcess = spawn(String(electron), args);
|
||||
|
||||
electronProcess.stdout.on("data", (data) => {
|
||||
log(String(data));
|
||||
});
|
||||
|
||||
electronProcess.stderr.on("data", (data) => {
|
||||
log(String(data));
|
||||
});
|
||||
}
|
||||
|
||||
function restartElectron() {
|
||||
if (electronProcess && electronProcess.pid) {
|
||||
if (process.platform === "darwin") {
|
||||
spawn("kill", ["-9", String(electronProcess.pid)]);
|
||||
} else {
|
||||
process.kill(electronProcess.pid);
|
||||
}
|
||||
electronProcess = null;
|
||||
startElectron();
|
||||
}
|
||||
}
|
||||
|
||||
async function start() {
|
||||
const mainContext = await esbuild.context({
|
||||
...buildConfig,
|
||||
entryPoints: [mainResolve("src/index.ts")],
|
||||
});
|
||||
|
||||
const preloadContext = await esbuild.context({
|
||||
...buildConfig,
|
||||
entryPoints: [mainResolve("src/preload.ts")],
|
||||
platform: "browser",
|
||||
});
|
||||
|
||||
const watcher = chokidar.watch("./src");
|
||||
watcher.on("change", async () => {
|
||||
await mainContext.rebuild();
|
||||
await preloadContext.rebuild();
|
||||
log("watch build succeed.");
|
||||
restartElectron();
|
||||
});
|
||||
|
||||
try {
|
||||
await mainContext.rebuild();
|
||||
await preloadContext.rebuild();
|
||||
await copySource();
|
||||
await startElectron();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,98 @@
|
||||
import { deleteSync } from "del";
|
||||
import { ElectronApp, mainResolve, Env, isWin } from "./utils";
|
||||
import gulp from "gulp";
|
||||
import * as esbuild from "esbuild";
|
||||
import consola from "consola";
|
||||
import { browserOptions, nodeOptions, getReleaseConfig } from "./config";
|
||||
import semver from "semver";
|
||||
import pkg from "../app/package.json";
|
||||
import * as builder from "electron-builder";
|
||||
import { globSync } from "glob";
|
||||
import fs from "fs";
|
||||
|
||||
const env = Env.getInstance();
|
||||
env.loadDotEnvRuntime();
|
||||
|
||||
async function clean() {
|
||||
return deleteSync([
|
||||
mainResolve("app/build"),
|
||||
mainResolve("app/bin"),
|
||||
mainResolve("release"),
|
||||
]);
|
||||
}
|
||||
|
||||
async function copySqlite() {
|
||||
const path = "build/Release/better_sqlite3.node";
|
||||
const source = mainResolve("node_modules/better-sqlite3", path);
|
||||
const target = mainResolve("app", path);
|
||||
fs.cpSync(source, target, { recursive: true });
|
||||
}
|
||||
|
||||
async function copyBin() {
|
||||
const source = mainResolve("bin", process.platform);
|
||||
const target = mainResolve("app/bin");
|
||||
fs.cpSync(source, target, { recursive: true });
|
||||
}
|
||||
|
||||
async function chmodBin() {
|
||||
// 遍历文件夹下的所有文件,将文件的权限设置为 777
|
||||
if (isWin) return;
|
||||
|
||||
const files = globSync(mainResolve("app/bin/*"));
|
||||
for (const file of files) {
|
||||
fs.chmodSync(file, 0o777);
|
||||
}
|
||||
}
|
||||
|
||||
const copy = gulp.parallel(copyBin, copySqlite);
|
||||
|
||||
async function watchTask() {
|
||||
const app = new ElectronApp();
|
||||
const main = await esbuild.context(nodeOptions("src/index.ts"));
|
||||
const preload = await esbuild.context(browserOptions("src/preload.ts"));
|
||||
|
||||
const watcher = gulp.watch(["./src"]);
|
||||
watcher
|
||||
.on("ready", async () => {
|
||||
await main.rebuild();
|
||||
await preload.rebuild();
|
||||
app.start();
|
||||
})
|
||||
.on("change", async () => {
|
||||
await main.rebuild();
|
||||
await preload.rebuild();
|
||||
app.restart();
|
||||
})
|
||||
.on("error", (error) => {
|
||||
consola.error(error);
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async function buildTask() {
|
||||
await esbuild.build(nodeOptions("src/index.ts"));
|
||||
await esbuild.build(browserOptions("src/preload.ts"));
|
||||
}
|
||||
|
||||
async function pack() {
|
||||
if (semver.neq(process.env.APP_VERSION || "", pkg.version)) {
|
||||
throw new Error("请先同步构建版本和发布版本");
|
||||
}
|
||||
const config = getReleaseConfig();
|
||||
if (process.env.GH_TOKEN) {
|
||||
config.publish = {
|
||||
provider: "github",
|
||||
repo: "mediago",
|
||||
owner: "caorushizi",
|
||||
releaseType: "draft",
|
||||
};
|
||||
}
|
||||
await builder.build({ config });
|
||||
}
|
||||
|
||||
// 开发环境
|
||||
export const dev = gulp.series(clean, copy, chmodBin, watchTask);
|
||||
// 构建打包
|
||||
export const build = gulp.series(clean, copy, chmodBin, buildTask);
|
||||
// release
|
||||
export const release = gulp.series(pack);
|
||||
@@ -1,118 +0,0 @@
|
||||
import { loadDotEnvRuntime, log, mainResolve, removeResource } from "./utils";
|
||||
import * as builder from "electron-builder";
|
||||
import semver from "semver";
|
||||
import pkg from "../app/package.json";
|
||||
|
||||
removeResource([mainResolve("release")]);
|
||||
|
||||
loadDotEnvRuntime();
|
||||
|
||||
if (semver.neq(process.env.APP_VERSION || "", pkg.version)) {
|
||||
log("请先同步构建版本和发布版本");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const options: builder.Configuration = {
|
||||
productName: process.env.APP_NAME,
|
||||
buildVersion: process.env.APP_VERSION,
|
||||
appId: process.env.APP_ID,
|
||||
copyright: process.env.APP_COPYRIGHT,
|
||||
artifactName: "${productName}-setup-${buildVersion}.${ext}",
|
||||
// FIXME: 这里屏蔽 node-pty 自动重构,因为会导致打包失败
|
||||
npmRebuild: false,
|
||||
directories: {
|
||||
output: "./release",
|
||||
},
|
||||
files: [
|
||||
{
|
||||
from: "./build",
|
||||
to: "./",
|
||||
},
|
||||
"./package.json",
|
||||
],
|
||||
extraResources: [
|
||||
{
|
||||
from: "./app/mobile",
|
||||
to: "mobile",
|
||||
},
|
||||
{
|
||||
from: "./app/plugin",
|
||||
to: "plugin",
|
||||
},
|
||||
{
|
||||
from: "./app/bin/${platform}/",
|
||||
to: "bin",
|
||||
},
|
||||
],
|
||||
win: {
|
||||
icon: "../assets/icon.ico",
|
||||
target: [
|
||||
{
|
||||
target: "nsis",
|
||||
},
|
||||
],
|
||||
},
|
||||
dmg: {
|
||||
contents: [
|
||||
{
|
||||
x: 410,
|
||||
y: 150,
|
||||
type: "link",
|
||||
path: "/Applications",
|
||||
},
|
||||
{
|
||||
x: 130,
|
||||
y: 150,
|
||||
type: "file",
|
||||
},
|
||||
],
|
||||
},
|
||||
mac: {
|
||||
icon: "../assets/icon.icns",
|
||||
target: {
|
||||
target: "default",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
},
|
||||
linux: {},
|
||||
nsis: {
|
||||
oneClick: true,
|
||||
allowElevation: true,
|
||||
allowToChangeInstallationDirectory: false,
|
||||
installerIcon: "",
|
||||
uninstallerIcon: "",
|
||||
installerHeaderIcon: "",
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true,
|
||||
shortcutName: "",
|
||||
include: "",
|
||||
script: "",
|
||||
},
|
||||
publish: {
|
||||
provider: "github",
|
||||
repo: "mediago",
|
||||
owner: "caorushizi",
|
||||
releaseType: "prerelease",
|
||||
},
|
||||
};
|
||||
|
||||
async function start() {
|
||||
const target =
|
||||
process.env.NODE_ENV === "development"
|
||||
? builder.DIR_TARGET
|
||||
: builder.DEFAULT_TARGET;
|
||||
try {
|
||||
const targets =
|
||||
process.platform === "win32"
|
||||
? builder.Platform.WINDOWS.createTarget(target)
|
||||
: builder.Platform.MAC.createTarget(target);
|
||||
await builder.build({
|
||||
targets,
|
||||
config: options,
|
||||
});
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -1,79 +1,108 @@
|
||||
import { existsSync, cpSync, rmSync } from "fs";
|
||||
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
|
||||
import dotenv from "dotenv";
|
||||
import { resolve } from "path";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import electron from "electron";
|
||||
|
||||
// FIXME: 有没有什么办法可以不用这么写?
|
||||
const con = console;
|
||||
export const log = con.log;
|
||||
const baseResolve = (...r) => resolve(__dirname, ...r);
|
||||
export const mainResolve = (...r: string[]) => baseResolve("..", ...r);
|
||||
export const rootResolve = (...r: string[]) => baseResolve("../../..", ...r);
|
||||
|
||||
export const mainResolve = (...r: any[]) => resolve(__dirname, "..", ...r);
|
||||
export const rootResolve = (...r: any[]) =>
|
||||
resolve(__dirname, "../../..", ...r);
|
||||
const nodeEnv = process.env.NODE_ENV;
|
||||
log("当前的环境是: ", nodeEnv);
|
||||
export const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
function loadEnv(path: string) {
|
||||
const result: Record<string, string> = {};
|
||||
export const isLinux = process.platform === "linux";
|
||||
export const isMac = process.platform === "darwin";
|
||||
export const isWin = process.platform === "win32";
|
||||
|
||||
const _loadEnv = (path: string) => {
|
||||
if (!existsSync(path)) {
|
||||
export class ElectronApp {
|
||||
process: ChildProcessWithoutNullStreams | null = null;
|
||||
|
||||
start() {
|
||||
const args = ["--inspect=5858", mainResolve("app/build/main/index.js")];
|
||||
|
||||
this.process = spawn(String(electron), args);
|
||||
|
||||
this.process.stdout.on("data", (data) => {
|
||||
consola.log(String(data));
|
||||
});
|
||||
|
||||
this.process.stderr.on("data", (data) => {
|
||||
consola.log(String(data));
|
||||
});
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.kill();
|
||||
this.start();
|
||||
}
|
||||
|
||||
kill() {
|
||||
if (this.process && this.process.pid) {
|
||||
if (isMac) {
|
||||
spawn("kill", ["-9", String(this.process.pid)]);
|
||||
} else {
|
||||
process.kill(this.process.pid);
|
||||
}
|
||||
this.process = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Env {
|
||||
env: Record<string, string> = {};
|
||||
nodeEnv = "";
|
||||
static instance: Env;
|
||||
|
||||
private constructor(nodeEnv = process.env.NODE_ENV) {
|
||||
const env = this.parseEnv(rootResolve(".env"));
|
||||
const modeEnv = this.parseEnv(rootResolve(`.env.${nodeEnv}`));
|
||||
const localEnv = this.parseEnv(rootResolve(`.env.${nodeEnv}.local`));
|
||||
|
||||
this.nodeEnv = nodeEnv || "development";
|
||||
this.env = { ...env, ...modeEnv, ...localEnv };
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!Env.instance) {
|
||||
Env.instance = new Env();
|
||||
}
|
||||
|
||||
return Env.instance;
|
||||
}
|
||||
|
||||
private parseEnv(path: string) {
|
||||
if (!fs.existsSync(path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { error, parsed } = dotenv.config({ path });
|
||||
if (error != null || !parsed) {
|
||||
const parsed = dotenv.parse(fs.readFileSync(path));
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.keys(parsed).forEach((key) => {
|
||||
result[key] = parsed[key];
|
||||
return Object.keys(parsed).reduce((prev, curr) => {
|
||||
prev[curr] = parsed[curr];
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
loadDotEnvRuntime() {
|
||||
Object.keys(this.env).forEach((key) => {
|
||||
if (process.env[key] != null || !this.env[key]) return;
|
||||
process.env[key] = this.env[key];
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
_loadEnv(path);
|
||||
_loadEnv(`${path}.local`);
|
||||
loadDotEnvDefined() {
|
||||
return Object.keys(this.env).reduce<Record<string, string>>((prev, cur) => {
|
||||
if (!cur.startsWith("APP_")) return prev;
|
||||
prev[`process.env.${[cur]}`] = JSON.stringify(this.env[cur]);
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadDotEnv() {
|
||||
const env = loadEnv(rootResolve(".env"));
|
||||
const envMode = loadEnv(rootResolve(`.env.${nodeEnv}`));
|
||||
|
||||
return { ...env, ...envMode };
|
||||
}
|
||||
|
||||
export function loadDotEnvRuntime() {
|
||||
const env = loadDotEnv();
|
||||
|
||||
Object.keys(env).forEach((key) => {
|
||||
if (!process.env[key]) {
|
||||
process.env[key] = env[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function loadDotEnvDefined() {
|
||||
const env = loadDotEnv();
|
||||
|
||||
return Object.keys(env).reduce<Record<string, string>>((prev, cur) => {
|
||||
if (!cur.startsWith("APP_")) return prev;
|
||||
prev[`process.env.${[cur]}`] = JSON.stringify(env[cur]);
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function copyResource(resource: { from: string; to: string }[]) {
|
||||
resource.forEach((r) => {
|
||||
const { from, to } = r;
|
||||
cpSync(from, to, {
|
||||
recursive: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function removeResource(resource: string[]) {
|
||||
resource.forEach((r) => {
|
||||
rmSync(r, { recursive: true, force: true });
|
||||
});
|
||||
get isDev() {
|
||||
return this.nodeEnv === "development";
|
||||
}
|
||||
}
|
||||
|
||||
+13
-14
@@ -1,18 +1,18 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { DownloadStatus } from "./interfaces";
|
||||
import { TYPES } from "./types";
|
||||
import { DownloadStatus } from "./interfaces.ts";
|
||||
import { TYPES } from "./types.ts";
|
||||
import { Menu, Tray, app, nativeImage, nativeTheme } from "electron";
|
||||
import TrayIcon from "./tray-icon.png";
|
||||
import path from "path";
|
||||
import MainWindow from "./windows/MainWindow";
|
||||
import WebviewService from "./services/WebviewService";
|
||||
import VideoRepository from "./repository/VideoRepository";
|
||||
import ElectronDevtools from "./vendor/ElectronDevtools";
|
||||
import ElectronStore from "./vendor/ElectronStore";
|
||||
import ElectronUpdater from "./vendor/ElectronUpdater";
|
||||
import TypeORM from "./vendor/TypeORM";
|
||||
import ProtocolService from "./core/protocol";
|
||||
import IpcHandlerService from "./core/ipc";
|
||||
import MainWindow from "./windows/MainWindow.ts";
|
||||
import WebviewService from "./services/WebviewService.ts";
|
||||
import VideoRepository from "./repository/VideoRepository.ts";
|
||||
import ElectronDevtools from "./vendor/ElectronDevtools.ts";
|
||||
import ElectronStore from "./vendor/ElectronStore.ts";
|
||||
import ElectronUpdater from "./vendor/ElectronUpdater.ts";
|
||||
import TypeORM from "./vendor/TypeORM.ts";
|
||||
import ProtocolService from "./core/protocol.ts";
|
||||
import IpcHandlerService from "./core/ipc.ts";
|
||||
|
||||
@injectable()
|
||||
export default class ElectronApp {
|
||||
@@ -34,12 +34,11 @@ export default class ElectronApp {
|
||||
@inject(TYPES.ElectronDevtools)
|
||||
private readonly devTools: ElectronDevtools,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore,
|
||||
private readonly store: ElectronStore
|
||||
) {}
|
||||
|
||||
private async serviceInit(): Promise<void> {
|
||||
this.mainWindow.init();
|
||||
this.webview.init();
|
||||
}
|
||||
|
||||
private async vendorInit() {
|
||||
@@ -94,7 +93,7 @@ export default class ElectronApp {
|
||||
const videoIds = videos.map((video) => video.id);
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
videoIds,
|
||||
DownloadStatus.Failed,
|
||||
DownloadStatus.Failed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { ConversionPagination, type Controller } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import ConversionRepository from "../repository/ConversionRepository.ts";
|
||||
import { handle } from "../helper/decorator.ts";
|
||||
import { Conversion } from "../entity/Conversion.ts";
|
||||
import { IpcMainEvent } from "electron/main";
|
||||
|
||||
@injectable()
|
||||
export default class ConversionController implements Controller {
|
||||
constructor(
|
||||
@inject(TYPES.ConversionRepository)
|
||||
private readonly conversionRepository: ConversionRepository
|
||||
) {}
|
||||
|
||||
@handle("get-conversions")
|
||||
async getConversions(e: IpcMainEvent, pagination: ConversionPagination) {
|
||||
return await this.conversionRepository.getConversions(pagination);
|
||||
}
|
||||
|
||||
@handle("add-conversion")
|
||||
async addConversion(e: IpcMainEvent, conversion: Conversion) {
|
||||
return await this.conversionRepository.addConversion(conversion);
|
||||
}
|
||||
|
||||
@handle("delete-conversion")
|
||||
async deleteConversion(e: IpcMainEvent, id: number) {
|
||||
return await this.conversionRepository.deleteConversion(id);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import { IpcMainEvent } from "electron/main";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { handle } from "../helper";
|
||||
import { handle } from "../helper/index.ts";
|
||||
import {
|
||||
type Controller,
|
||||
DownloadItem,
|
||||
DownloadItemPagination,
|
||||
Task,
|
||||
DownloadStatus,
|
||||
} from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import MainWindow from "../windows/MainWindow";
|
||||
import ElectronStore from "../vendor/ElectronStore";
|
||||
import DownloadService from "../services/DownloadService";
|
||||
import VideoRepository from "../repository/VideoRepository";
|
||||
} from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import MainWindow from "../windows/MainWindow.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import DownloadService from "../services/DownloadService.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import WebviewService from "../services/WebviewService.ts";
|
||||
|
||||
@injectable()
|
||||
export default class DownloadController implements Controller {
|
||||
@@ -25,26 +26,33 @@ export default class DownloadController implements Controller {
|
||||
private readonly downloadService: DownloadService,
|
||||
@inject(TYPES.MainWindow)
|
||||
private readonly mainWindow: MainWindow,
|
||||
@inject(TYPES.WebviewService)
|
||||
private readonly webviewService: WebviewService
|
||||
) {}
|
||||
|
||||
@handle("show-download-dialog")
|
||||
async showDownloadDialog(e: IpcMainEvent, data: DownloadItem) {
|
||||
this.mainWindow.window?.webContents.send("show-download-dialog", data);
|
||||
const image = await this.webviewService.captureView();
|
||||
this.webviewService.sendToWindow(
|
||||
"show-download-dialog",
|
||||
data,
|
||||
image.toDataURL()
|
||||
);
|
||||
}
|
||||
|
||||
@handle("add-download-item")
|
||||
async addDownloadItem(e: IpcMainEvent, video: DownloadItem) {
|
||||
async addDownloadItem(e: IpcMainEvent, video: Omit<DownloadItem, "id">) {
|
||||
const item = await this.videoRepository.addVideo(video);
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
this.mainWindow.window?.webContents.send("download-item-notifier", item);
|
||||
this.mainWindow.send("download-item-notifier", item);
|
||||
return item;
|
||||
}
|
||||
|
||||
@handle("add-download-items")
|
||||
async addDownloadItems(e: IpcMainEvent, videos: DownloadItem[]) {
|
||||
async addDownloadItems(e: IpcMainEvent, videos: Omit<DownloadItem, "id">[]) {
|
||||
const items = await this.videoRepository.addVideos(videos);
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
this.mainWindow.window?.webContents.send("download-item-notifier", items);
|
||||
this.mainWindow.send("download-item-notifier", items);
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -54,8 +62,15 @@ export default class DownloadController implements Controller {
|
||||
return item;
|
||||
}
|
||||
|
||||
@handle("edit-download-now")
|
||||
async editDownloadNow(e: IpcMainEvent, video: DownloadItem) {
|
||||
const item = await this.editDownloadItem(e, video);
|
||||
await this.startDownload(e, item.id);
|
||||
return item;
|
||||
}
|
||||
|
||||
@handle("download-now")
|
||||
async downloadNow(e: IpcMainEvent, video: DownloadItem) {
|
||||
async downloadNow(e: IpcMainEvent, video: Omit<DownloadItem, "id">) {
|
||||
// 添加下载项
|
||||
const item = await this.addDownloadItem(e, video);
|
||||
// 开始下载
|
||||
@@ -63,6 +78,15 @@ export default class DownloadController implements Controller {
|
||||
return item;
|
||||
}
|
||||
|
||||
@handle("download-items-now")
|
||||
async downloadItemsNow(e: IpcMainEvent, videos: Omit<DownloadItem, "id">[]) {
|
||||
// 添加下载项
|
||||
const items = await this.addDownloadItems(e, videos);
|
||||
// 开始下载
|
||||
items.forEach((item) => this.startDownload(e, item.id));
|
||||
return items;
|
||||
}
|
||||
|
||||
@handle("get-download-items")
|
||||
async getDownloadItems(e: IpcMainEvent, pagination: DownloadItemPagination) {
|
||||
const videos = await this.videoRepository.findVideos(pagination);
|
||||
@@ -73,9 +97,6 @@ export default class DownloadController implements Controller {
|
||||
async startDownload(e: IpcMainEvent, vid: number) {
|
||||
// 查找将要下载的视频
|
||||
const video = await this.videoRepository.findVideo(vid);
|
||||
if (!video) {
|
||||
return Promise.reject("没有找到该视频");
|
||||
}
|
||||
const { name, url, headers, type } = video;
|
||||
const local = this.store.get("local");
|
||||
|
||||
|
||||
@@ -8,25 +8,28 @@ import {
|
||||
shell,
|
||||
clipboard,
|
||||
} from "electron";
|
||||
import { Favorite } from "../entity/Favorite";
|
||||
import { convertToAudio, db, workspace } from "../helper";
|
||||
import { Favorite } from "../entity/Favorite.ts";
|
||||
import { convertToAudio, db, workspace } from "../helper/index.ts";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { AppStore, EnvPath } from "../main";
|
||||
import { AppStore, EnvPath } from "../main.ts";
|
||||
import path from "path";
|
||||
import { handle, getLocalIP } from "../helper";
|
||||
import { type Controller } from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import { handle, getLocalIP } from "../helper/index.ts";
|
||||
import { type Controller } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import fs from "fs-extra";
|
||||
import MainWindow from "../windows/MainWindow";
|
||||
import BrowserWindow from "../windows/BrowserWindow";
|
||||
import ElectronStore from "../vendor/ElectronStore";
|
||||
import WebviewService from "../services/WebviewService";
|
||||
import FavoriteRepository from "../repository/FavoriteRepository";
|
||||
import VideoRepository from "../repository/VideoRepository";
|
||||
import MainWindow from "../windows/MainWindow.ts";
|
||||
import BrowserWindow from "../windows/BrowserWindow.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import WebviewService from "../services/WebviewService.ts";
|
||||
import FavoriteRepository from "../repository/FavoriteRepository.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import ConversionRepository from "../repository/ConversionRepository.ts";
|
||||
import { machineId } from "node-machine-id";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
@injectable()
|
||||
export default class HomeController implements Controller {
|
||||
private sharedState: Record<string, any> = {};
|
||||
private sharedState: Record<string, unknown> = {};
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.ElectronStore)
|
||||
@@ -41,6 +44,8 @@ export default class HomeController implements Controller {
|
||||
private readonly browserWindow: BrowserWindow,
|
||||
@inject(TYPES.WebviewService)
|
||||
private readonly webviewService: WebviewService,
|
||||
@inject(TYPES.ConversionRepository)
|
||||
private readonly conversionRepository: ConversionRepository
|
||||
) {}
|
||||
|
||||
@handle("get-env-path")
|
||||
@@ -77,9 +82,7 @@ export default class HomeController implements Controller {
|
||||
@handle("on-favorite-item-context-menu")
|
||||
async onFavoriteItemContextMenu(e: IpcMainEvent, id: number) {
|
||||
const send = (action: string) => {
|
||||
const window = this.mainWindow.window;
|
||||
if (!window) return;
|
||||
window.webContents.send("favorite-item-event", {
|
||||
this.mainWindow.send("favorite-item-event", {
|
||||
action,
|
||||
payload: id,
|
||||
});
|
||||
@@ -145,6 +148,10 @@ export default class HomeController implements Controller {
|
||||
if (key === "isMobile") {
|
||||
this.webviewService.setUserAgent(val);
|
||||
}
|
||||
// privacy
|
||||
if (key === "privacy") {
|
||||
this.webviewService.setDefaultSession(val);
|
||||
}
|
||||
|
||||
this.store.set(key, val);
|
||||
}
|
||||
@@ -162,10 +169,7 @@ export default class HomeController implements Controller {
|
||||
@handle("on-download-list-context-menu")
|
||||
async downloadListContextMenu(e: IpcMainEvent, id: number) {
|
||||
const send = (action: string) => {
|
||||
const window = this.mainWindow.window;
|
||||
if (!window) return;
|
||||
|
||||
window.webContents.send("download-item-event", {
|
||||
this.mainWindow.send("download-item-event", {
|
||||
action,
|
||||
payload: id,
|
||||
});
|
||||
@@ -175,7 +179,7 @@ export default class HomeController implements Controller {
|
||||
{
|
||||
label: "拷贝链接地址",
|
||||
click: () => {
|
||||
clipboard.writeText(item?.url || "");
|
||||
clipboard.writeText(item.url || "");
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -211,10 +215,11 @@ export default class HomeController implements Controller {
|
||||
|
||||
@handle("convert-to-audio")
|
||||
async convertToAudio(e: IpcMainEvent, id: number) {
|
||||
const video = await this.videoRepository.findVideo(id);
|
||||
const conversion = await this.conversionRepository.findConversion(id);
|
||||
const local = this.store.get("local");
|
||||
const input = path.join(local, `${video?.name}.mp4`);
|
||||
const output = path.join(local, `${video?.name}.mp3`);
|
||||
const input = conversion.path;
|
||||
const outName = `${path.basename(input, path.extname(input))}.mp3`;
|
||||
const output = path.join(local, outName);
|
||||
|
||||
const exist = await fs.exists(input);
|
||||
if (exist) {
|
||||
@@ -255,6 +260,38 @@ export default class HomeController implements Controller {
|
||||
@handle("get-download-log")
|
||||
async getDownloadLog(event: IpcMainEvent, id: number) {
|
||||
const video = await this.videoRepository.findVideo(id);
|
||||
return video?.log || "";
|
||||
return video.log || "";
|
||||
}
|
||||
|
||||
@handle("select-file")
|
||||
async selectFile() {
|
||||
const window = this.mainWindow.window;
|
||||
if (!window) return Promise.reject("未找到主窗口");
|
||||
|
||||
const result = await dialog.showOpenDialog(window, {
|
||||
properties: ["openFile"],
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
return result.filePaths[0];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@handle("get-machine-id")
|
||||
async getMachineId() {
|
||||
try {
|
||||
const id = this.store.get("machineId");
|
||||
if (id) return id;
|
||||
const newId = await machineId();
|
||||
this.store.set("machineId", newId);
|
||||
return newId;
|
||||
} catch (e) {
|
||||
const id = this.store.get("machineId");
|
||||
if (id) return id;
|
||||
const newId = nanoid();
|
||||
this.store.set("machineId", newId);
|
||||
return newId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
MenuItemConstructorOptions,
|
||||
} from "electron";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { handle } from "../helper";
|
||||
import { type Controller } from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import WebviewService from "../services/WebviewService";
|
||||
import ElectronStore from "../vendor/ElectronStore";
|
||||
import { SniffingHelper } from "../services/SniffingHelperService";
|
||||
import { handle } from "../helper/index.ts";
|
||||
import { type Controller } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import WebviewService from "../services/WebviewService.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import { SniffingHelper } from "../services/SniffingHelperService.ts";
|
||||
|
||||
@injectable()
|
||||
export default class WebviewController implements Controller {
|
||||
@@ -20,7 +20,7 @@ export default class WebviewController implements Controller {
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore,
|
||||
@inject(TYPES.SniffingHelper)
|
||||
private readonly sniffingHelper: SniffingHelper,
|
||||
private readonly sniffingHelper: SniffingHelper
|
||||
) {}
|
||||
|
||||
@handle("set-webview-bounds")
|
||||
@@ -29,7 +29,7 @@ export default class WebviewController implements Controller {
|
||||
}
|
||||
|
||||
@handle("webview-load-url")
|
||||
async browserViewLoadUrl(e: IpcMainEvent, url?: string): Promise<void> {
|
||||
async browserViewLoadUrl(e: IpcMainEvent, url: string): Promise<void> {
|
||||
await this.webview.loadURL(url);
|
||||
}
|
||||
|
||||
@@ -85,4 +85,9 @@ export default class WebviewController implements Controller {
|
||||
async pluginReady() {
|
||||
this.sniffingHelper.pluginReady();
|
||||
}
|
||||
|
||||
@handle("clear-webview-cache")
|
||||
async clearWebviewCache() {
|
||||
return this.webview.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { inject, injectable, multiInject } from "inversify";
|
||||
import { Controller } from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import ElectronLogger from "../vendor/ElectronLogger";
|
||||
import { error, success } from "../helper/utils";
|
||||
import { Controller } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import ElectronLogger from "../vendor/ElectronLogger.ts";
|
||||
import { error, success } from "../helper/utils.ts";
|
||||
|
||||
@injectable()
|
||||
export default class IpcHandlerService {
|
||||
@@ -43,7 +43,7 @@ export default class IpcHandlerService {
|
||||
}
|
||||
return success(res);
|
||||
} catch (e: unknown) {
|
||||
this.logger.error("处理 ipc 事件时异常: ", e);
|
||||
this.logger.error(`处理 ipc [${channel}] 事件时异常: `, e);
|
||||
if (e instanceof Error) {
|
||||
return error(e.message);
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { pathExists, readFile } from "fs-extra";
|
||||
import { injectable } from "inversify";
|
||||
import { join } from "path";
|
||||
import { URL } from "url";
|
||||
import { defaultScheme } from "../helper";
|
||||
import { defaultScheme } from "../helper/index.ts";
|
||||
import mime from "mime-types";
|
||||
|
||||
@injectable()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm";
|
||||
|
||||
@Entity({
|
||||
name: "conversion",
|
||||
})
|
||||
export class Conversion {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: true,
|
||||
})
|
||||
name: string;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
default: "",
|
||||
})
|
||||
path: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdDate?: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedDate?: Date;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DownloadStatus, DownloadType } from "../interfaces";
|
||||
import { DownloadStatus, DownloadType } from "../interfaces.ts";
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawn } from "child_process";
|
||||
import { ffmpegPath } from "./variables";
|
||||
import { ffmpegPath } from "./variables.ts";
|
||||
|
||||
export const convertToAudio = async (
|
||||
input: string,
|
||||
|
||||
@@ -34,8 +34,8 @@ function fetchWrapper(url: string) {
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
export { sleep, formatHeaders } from "./utils";
|
||||
export * from "./variables";
|
||||
export { on, handle } from "./decorator";
|
||||
export { convertToAudio } from "./ffmpeg";
|
||||
export { sleep, formatHeaders } from "./utils.ts";
|
||||
export * from "./variables.ts";
|
||||
export { on, handle } from "./decorator.ts";
|
||||
export { convertToAudio } from "./ffmpeg.ts";
|
||||
export { fetchWrapper as fetch };
|
||||
|
||||
@@ -4,12 +4,11 @@ export async function sleep(second = 1): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, second * 1000));
|
||||
}
|
||||
|
||||
export function formatHeaders(headersStr: string): string {
|
||||
const headers: Record<string, any> | null = JSON.parse(headersStr);
|
||||
export function formatHeaders(headers: Record<string, string>): string {
|
||||
if (!headers) return "";
|
||||
const formatted = Object.entries(headers)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join("|");
|
||||
.join("\n");
|
||||
return formatted;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,15 @@ const appPath = app.getAppPath();
|
||||
export const appData = app.getPath("appData");
|
||||
export const download = app.getPath("downloads");
|
||||
|
||||
export const isMac = process.platform === "darwin";
|
||||
export const isWin = process.platform === "win32";
|
||||
export enum Platform {
|
||||
Windows = "win32",
|
||||
MacOS = "darwin",
|
||||
Linux = "linux",
|
||||
}
|
||||
|
||||
export const isMac = process.platform === Platform.MacOS;
|
||||
export const isWin = process.platform === Platform.Windows;
|
||||
export const isLinux = process.platform === Platform.Linux;
|
||||
|
||||
if (!isDev) {
|
||||
global.__bin__ = resolve(appPath, "../bin");
|
||||
@@ -29,17 +36,14 @@ export const workspace = resolve(appData, appName);
|
||||
export const defaultScheme = "mediago";
|
||||
export const PERSIST_MEDIAGO = "persist:mediago";
|
||||
export const PERSIST_WEBVIEW = "persist:webview";
|
||||
export const PRIVACY_WEBVIEW = "webview";
|
||||
export const db = resolve(workspace, "app.db");
|
||||
|
||||
// bin path
|
||||
const downloaderBinName = isWin ? "N_m3u8DL-CLI" : "N_m3u8DL-RE";
|
||||
export const ffmpegPath = resolveBin("ffmpeg");
|
||||
export const biliDownloaderBin = resolveBin("BBDown");
|
||||
export const m3u8DownloaderBin = resolveBin(downloaderBinName);
|
||||
export const videoServerBin = resolveBin("server");
|
||||
export const m3u8DownloaderBin = resolveBin("N_m3u8DL-RE");
|
||||
|
||||
// mobile path
|
||||
export const mobilePath = resolveStatic("mobile");
|
||||
// plugin path
|
||||
export const pluginPath = resolveStatic("plugin/index.js");
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import "reflect-metadata";
|
||||
import { app, protocol } from "electron";
|
||||
import { defaultScheme } from "./helper";
|
||||
import { container } from "./inversify.config";
|
||||
import { TYPES } from "./types";
|
||||
import ElectronApp from "./app";
|
||||
import { defaultScheme } from "./helper/index.ts";
|
||||
import { container } from "./inversify.config.ts";
|
||||
import { TYPES } from "./types.ts";
|
||||
import ElectronApp from "./app.ts";
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
const start = async (): Promise<void> => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Conversion } from "./entity/Conversion.ts";
|
||||
|
||||
export type Controller = Record<string | symbol, any>;
|
||||
|
||||
export interface DownloadItem {
|
||||
@@ -21,11 +23,21 @@ export interface DownloadItemPagination {
|
||||
filter?: DownloadFilter;
|
||||
}
|
||||
|
||||
export interface ConversionPagination {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface VideoResponse {
|
||||
total: number;
|
||||
list: DownloadItem[];
|
||||
}
|
||||
|
||||
export interface ConversionResponse {
|
||||
total: number;
|
||||
list: Conversion[];
|
||||
}
|
||||
|
||||
export enum DownloadStatus {
|
||||
Ready = "ready",
|
||||
Watting = "watting",
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import DownloadController from "./controller/DownloadController";
|
||||
import HomeController from "./controller/HomeController";
|
||||
import WebviewController from "./controller/WebviewController";
|
||||
import DownloadController from "./controller/DownloadController.ts";
|
||||
import HomeController from "./controller/HomeController.ts";
|
||||
import WebviewController from "./controller/WebviewController.ts";
|
||||
import { Container } from "inversify";
|
||||
import FavoriteRepository from "./repository/FavoriteRepository";
|
||||
import VideoRepository from "./repository/VideoRepository";
|
||||
import WebviewService from "./services/WebviewService";
|
||||
import ElectronApp from "./app";
|
||||
import { Controller } from "./interfaces";
|
||||
import { TYPES } from "./types";
|
||||
import MainWindow from "./windows/MainWindow";
|
||||
import BrowserWindow from "./windows/BrowserWindow";
|
||||
import { SniffingHelper } from "./services/SniffingHelperService";
|
||||
import DownloadService from "./services/DownloadService";
|
||||
import ElectronLogger from "./vendor/ElectronLogger";
|
||||
import ElectronUpdater from "./vendor/ElectronUpdater";
|
||||
import TypeORM from "./vendor/TypeORM";
|
||||
import ElectronDevtools from "./vendor/ElectronDevtools";
|
||||
import ElectronStore from "./vendor/ElectronStore";
|
||||
import IpcHandler from "./core/ipc";
|
||||
import ProtocolService from "./core/protocol";
|
||||
import FavoriteRepository from "./repository/FavoriteRepository.ts";
|
||||
import VideoRepository from "./repository/VideoRepository.ts";
|
||||
import WebviewService from "./services/WebviewService.ts";
|
||||
import ElectronApp from "./app.ts";
|
||||
import { Controller } from "./interfaces.ts";
|
||||
import { TYPES } from "./types.ts";
|
||||
import MainWindow from "./windows/MainWindow.ts";
|
||||
import BrowserWindow from "./windows/BrowserWindow.ts";
|
||||
import { SniffingHelper } from "./services/SniffingHelperService.ts";
|
||||
import DownloadService from "./services/DownloadService.ts";
|
||||
import ElectronLogger from "./vendor/ElectronLogger.ts";
|
||||
import ElectronUpdater from "./vendor/ElectronUpdater.ts";
|
||||
import TypeORM from "./vendor/TypeORM.ts";
|
||||
import ElectronDevtools from "./vendor/ElectronDevtools.ts";
|
||||
import ElectronStore from "./vendor/ElectronStore.ts";
|
||||
import IpcHandler from "./core/ipc.ts";
|
||||
import ProtocolService from "./core/protocol.ts";
|
||||
import ConversionController from "./controller/ConversionController.ts";
|
||||
import ConversionRepository from "./repository/ConversionRepository.ts";
|
||||
|
||||
const container = new Container({
|
||||
skipBaseClassChecks: true,
|
||||
@@ -41,12 +43,16 @@ container.bind<BrowserWindow>(TYPES.BrowserWindow).to(BrowserWindow);
|
||||
container.bind<Controller>(TYPES.Controller).to(HomeController);
|
||||
container.bind<Controller>(TYPES.Controller).to(WebviewController);
|
||||
container.bind<Controller>(TYPES.Controller).to(DownloadController);
|
||||
container.bind<Controller>(TYPES.Controller).to(ConversionController);
|
||||
|
||||
// repository
|
||||
container.bind<VideoRepository>(TYPES.VideoRepository).to(VideoRepository);
|
||||
container
|
||||
.bind<FavoriteRepository>(TYPES.FavoriteRepository)
|
||||
.to(FavoriteRepository);
|
||||
container
|
||||
.bind<ConversionRepository>(TYPES.ConversionRepository)
|
||||
.to(ConversionRepository);
|
||||
|
||||
// vendor
|
||||
container.bind<ElectronDevtools>(TYPES.ElectronDevtools).to(ElectronDevtools);
|
||||
|
||||
Vendored
+6
@@ -52,6 +52,12 @@ declare interface AppStore {
|
||||
showTerminal: boolean;
|
||||
// 隐私模式
|
||||
privacy: boolean;
|
||||
// 机器id
|
||||
machineId: string;
|
||||
// 下载代理设置
|
||||
downloadProxySwitch: boolean;
|
||||
// 自动更新
|
||||
autoUpgrade: boolean;
|
||||
}
|
||||
|
||||
declare interface BrowserStore {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { contextBridge, ipcRenderer } from "electron/renderer";
|
||||
import { shell } from "electron/common";
|
||||
import { AppStore, BrowserStore, EnvPath } from "./main";
|
||||
import { type Favorite } from "./entity/Favorite";
|
||||
import { AppStore, BrowserStore, EnvPath } from "./main.ts";
|
||||
import { type Favorite } from "./entity/Favorite.ts";
|
||||
import {
|
||||
ConversionPagination,
|
||||
ConversionResponse,
|
||||
VideoResponse,
|
||||
type DownloadItem,
|
||||
type DownloadItemPagination,
|
||||
} from "./interfaces";
|
||||
import { Video } from "./entity/Video";
|
||||
} from "./interfaces.ts";
|
||||
import { Video } from "./entity/Video.ts";
|
||||
import { Conversion } from "./entity/Conversion.ts";
|
||||
|
||||
const apiFunctions: Record<string, any> = {};
|
||||
|
||||
@@ -23,7 +26,7 @@ const electronApi = {
|
||||
return ipcRenderer.invoke("get-favorites");
|
||||
},
|
||||
addFavorite(
|
||||
favorite: Omit<Favorite, "id" | "createdDate" | "updatedDate">,
|
||||
favorite: Omit<Favorite, "id" | "createdDate" | "updatedDate">
|
||||
): Promise<Favorite> {
|
||||
return ipcRenderer.invoke("add-favorite", favorite);
|
||||
},
|
||||
@@ -53,17 +56,18 @@ const electronApi = {
|
||||
},
|
||||
setAppStore(
|
||||
key: keyof AppStore,
|
||||
val: AppStore[keyof AppStore],
|
||||
val: AppStore[keyof AppStore]
|
||||
): Promise<void> {
|
||||
return ipcRenderer.invoke("set-app-store", key, val);
|
||||
},
|
||||
openDir(dir: string): Promise<void> {
|
||||
async openDir(dir?: string): Promise<void> {
|
||||
if (!dir) return;
|
||||
return ipcRenderer.invoke("open-dir", dir);
|
||||
},
|
||||
addDownloadItem(video: Omit<DownloadItem, "id">): Promise<Video> {
|
||||
return ipcRenderer.invoke("add-download-item", video);
|
||||
},
|
||||
addDownloadItems(videos: DownloadItem[]): Promise<Video[]> {
|
||||
addDownloadItems(videos: Omit<DownloadItem, "id">[]): Promise<Video[]> {
|
||||
return ipcRenderer.invoke("add-download-items", videos);
|
||||
},
|
||||
getDownloadItems(p: DownloadItemPagination): Promise<VideoResponse> {
|
||||
@@ -116,6 +120,12 @@ const electronApi = {
|
||||
downloadNow(video: Omit<DownloadItem, "id">): Promise<void> {
|
||||
return ipcRenderer.invoke("download-now", video);
|
||||
},
|
||||
downloadItemsNow(videos: Omit<DownloadItem, "id">[]): Promise<void> {
|
||||
return ipcRenderer.invoke("download-items-now", videos);
|
||||
},
|
||||
editDownloadNow(video: DownloadItem): Promise<void> {
|
||||
return ipcRenderer.invoke("edit-download-now", video);
|
||||
},
|
||||
combineToHomePage(store: BrowserStore): Promise<void> {
|
||||
return ipcRenderer.invoke("combine-to-home-page", store);
|
||||
},
|
||||
@@ -128,6 +138,9 @@ const electronApi = {
|
||||
openBrowser(url: string): Promise<void> {
|
||||
return shell.openExternal(url);
|
||||
},
|
||||
selectFile(): Promise<string> {
|
||||
return ipcRenderer.invoke("select-file");
|
||||
},
|
||||
getSharedState(): Promise<any> {
|
||||
return ipcRenderer.invoke("get-shared-state");
|
||||
},
|
||||
@@ -137,18 +150,32 @@ const electronApi = {
|
||||
setUserAgent(isMobile: boolean): Promise<void> {
|
||||
return ipcRenderer.invoke("webview-change-user-agent", isMobile);
|
||||
},
|
||||
downloadItem(data: Omit<DownloadItem, "id">) {
|
||||
return ipcRenderer.invoke("add-download-item", data);
|
||||
},
|
||||
getDownloadLog(id: number): Promise<string> {
|
||||
return ipcRenderer.invoke("get-download-log", id);
|
||||
},
|
||||
showDownloadDialog(data: Omit<DownloadItem, "id">) {
|
||||
showDownloadDialog(data: Omit<DownloadItem, "id">[]) {
|
||||
return ipcRenderer.invoke("show-download-dialog", data);
|
||||
},
|
||||
pluginReady() {
|
||||
return ipcRenderer.invoke("plugin-ready");
|
||||
},
|
||||
getConversions(
|
||||
pagination: ConversionPagination
|
||||
): Promise<ConversionResponse> {
|
||||
return ipcRenderer.invoke("get-conversions", pagination);
|
||||
},
|
||||
addConversion(conversion: Omit<Conversion, "id">): Promise<Conversion> {
|
||||
return ipcRenderer.invoke("add-conversion", conversion);
|
||||
},
|
||||
deleteConversion(id: number): Promise<void> {
|
||||
return ipcRenderer.invoke("delete-conversion", id);
|
||||
},
|
||||
getMachineId(): Promise<string> {
|
||||
return ipcRenderer.invoke("get-machine-id");
|
||||
},
|
||||
clearWebviewCache(): Promise<void> {
|
||||
return ipcRenderer.invoke("clear-webview-cache");
|
||||
},
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld(apiKey, electronApi);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { ConversionPagination } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import TypeORM from "../vendor/TypeORM.ts";
|
||||
import { Conversion } from "../entity/Conversion.ts";
|
||||
|
||||
@injectable()
|
||||
export default class ConversionRepository {
|
||||
constructor(
|
||||
@inject(TYPES.TypeORM)
|
||||
private readonly db: TypeORM
|
||||
) {}
|
||||
|
||||
async findConversion(id: number) {
|
||||
const repository = this.db.appDataSource.getRepository(Conversion);
|
||||
const conversion = await repository.findOneBy({ id });
|
||||
|
||||
if (!conversion) {
|
||||
throw new Error("没有找到该转换任务");
|
||||
}
|
||||
|
||||
return conversion;
|
||||
}
|
||||
|
||||
async getConversions(pagination: ConversionPagination) {
|
||||
const { current = 0, pageSize = 50 } = pagination;
|
||||
|
||||
const [items, count] = await this.db.appDataSource
|
||||
.getRepository(Conversion)
|
||||
.findAndCount({
|
||||
order: {
|
||||
createdDate: "ASC",
|
||||
},
|
||||
skip: (current - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
return {
|
||||
total: count,
|
||||
list: items,
|
||||
};
|
||||
}
|
||||
|
||||
async addConversion(conversion: Conversion) {
|
||||
await this.db.appDataSource.getRepository(Conversion).save(conversion);
|
||||
}
|
||||
|
||||
async deleteConversion(id: number) {
|
||||
await this.db.appDataSource.getRepository(Conversion).delete({ id });
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "../types";
|
||||
import { Favorite } from "../entity/Favorite";
|
||||
import TypeORM from "../vendor/TypeORM";
|
||||
import { TYPES } from "../types.ts";
|
||||
import { Favorite } from "../entity/Favorite.ts";
|
||||
import TypeORM from "../vendor/TypeORM.ts";
|
||||
|
||||
@injectable()
|
||||
export default class FavoriteRepository {
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import { Video } from "../entity/Video";
|
||||
import { Video } from "../entity/Video.ts";
|
||||
import { inject, injectable } from "inversify";
|
||||
import {
|
||||
DownloadFilter,
|
||||
DownloadItem,
|
||||
DownloadItemPagination,
|
||||
DownloadStatus,
|
||||
} from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
} from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import { In, Not } from "typeorm";
|
||||
import TypeORM from "../vendor/TypeORM";
|
||||
import TypeORM from "../vendor/TypeORM.ts";
|
||||
|
||||
@injectable()
|
||||
export default class VideoRepository {
|
||||
constructor(
|
||||
@inject(TYPES.TypeORM)
|
||||
private readonly db: TypeORM,
|
||||
private readonly db: TypeORM
|
||||
) {}
|
||||
|
||||
async addVideo(video: Omit<DownloadItem, "id">) {
|
||||
// 先判断有没有同名的视频
|
||||
const exist = await this.findVideoByName(video.name);
|
||||
if (exist) {
|
||||
throw new Error("视频名称已存在,请更换视频名称");
|
||||
}
|
||||
const item = new Video();
|
||||
item.name = video.name;
|
||||
item.url = video.url;
|
||||
@@ -26,7 +31,7 @@ export default class VideoRepository {
|
||||
return await this.db.manager.save(item);
|
||||
}
|
||||
|
||||
async addVideos(videos: DownloadItem[]) {
|
||||
async addVideos(videos: Omit<DownloadItem, "id">[]) {
|
||||
const items = videos.map((video) => {
|
||||
const item = new Video();
|
||||
item.name = video.name;
|
||||
@@ -91,9 +96,14 @@ export default class VideoRepository {
|
||||
}
|
||||
|
||||
async findVideo(id: number) {
|
||||
return this.db.appDataSource.getRepository(Video).findOneBy({
|
||||
id,
|
||||
});
|
||||
const repository = this.db.appDataSource.getRepository(Video);
|
||||
const video = await repository.findOneBy({ id });
|
||||
|
||||
if (!video) {
|
||||
throw new Error("没有找到该视频");
|
||||
}
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
async findVideoByName(name: string) {
|
||||
@@ -104,22 +114,18 @@ export default class VideoRepository {
|
||||
|
||||
async changeVideoStatus(id: number | number[], status: DownloadStatus) {
|
||||
const ids = !Array.isArray(id) ? [id] : id;
|
||||
return this.db.appDataSource
|
||||
.createQueryBuilder()
|
||||
.update(Video)
|
||||
.set({ status })
|
||||
.where({ id: In(ids) })
|
||||
.execute();
|
||||
const videoRepository = this.db.appDataSource.getRepository(Video);
|
||||
const videos = await videoRepository.findBy({ id: In(ids) });
|
||||
for (const video of videos) {
|
||||
video.status = status;
|
||||
}
|
||||
await videoRepository.save(videos);
|
||||
}
|
||||
|
||||
async changeVideoIsLive(id: number | number[], isLive: boolean) {
|
||||
const ids = !Array.isArray(id) ? [id] : id;
|
||||
return this.db.appDataSource
|
||||
.createQueryBuilder()
|
||||
.update(Video)
|
||||
.set({ isLive })
|
||||
.where({ id: In(ids) })
|
||||
.execute();
|
||||
async changeVideoIsLive(id: number) {
|
||||
const video = await this.findVideo(id);
|
||||
video.isLive = true;
|
||||
return this.db.manager.save(video);
|
||||
}
|
||||
|
||||
async findWattingAndDownloadingVideos() {
|
||||
@@ -142,18 +148,12 @@ export default class VideoRepository {
|
||||
|
||||
async appendDownloadLog(id: number, message: string) {
|
||||
const video = await this.findVideo(id);
|
||||
if (!video) {
|
||||
throw new Error("视频不存在");
|
||||
}
|
||||
video.log = video.log ? `${video.log}\n${message}` : message;
|
||||
return await this.db.manager.save(video);
|
||||
}
|
||||
|
||||
async getDownloadLog(id: number) {
|
||||
const video = await this.findVideo(id);
|
||||
if (!video) {
|
||||
throw new Error("视频不存在");
|
||||
}
|
||||
return video.log;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,34 @@ import {
|
||||
DownloadProgress,
|
||||
DownloadStatus,
|
||||
Task,
|
||||
} from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import ElectronLogger from "../vendor/ElectronLogger";
|
||||
import ElectronStore from "../vendor/ElectronStore";
|
||||
import VideoRepository from "../repository/VideoRepository";
|
||||
import { biliDownloaderBin, m3u8DownloaderBin } from "../helper";
|
||||
} from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import ElectronLogger from "../vendor/ElectronLogger.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import {
|
||||
Platform,
|
||||
biliDownloaderBin,
|
||||
m3u8DownloaderBin,
|
||||
} from "../helper/index.ts";
|
||||
import * as pty from "node-pty";
|
||||
import stripAnsi from "strip-ansi";
|
||||
|
||||
interface DownloadContext {
|
||||
// 是否为直播
|
||||
isLive: boolean;
|
||||
// 下载进度
|
||||
percent: string;
|
||||
// 下载速度
|
||||
speed: string;
|
||||
// 是否已经 ready
|
||||
ready: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadOptions {
|
||||
abortSignal: AbortController;
|
||||
encoding?: string;
|
||||
onMessage?: (ctx: any, message: string) => void;
|
||||
onMessage?: (ctx: DownloadContext, message: string) => void;
|
||||
id: number;
|
||||
}
|
||||
|
||||
@@ -38,39 +53,7 @@ interface Schema {
|
||||
const processList: Schema[] = [
|
||||
{
|
||||
type: "m3u8",
|
||||
platform: ["win32"],
|
||||
bin: m3u8DownloaderBin,
|
||||
args: {
|
||||
url: {
|
||||
argsName: null,
|
||||
},
|
||||
localDir: {
|
||||
argsName: ["--workDir"],
|
||||
},
|
||||
name: {
|
||||
argsName: ["--saveName"],
|
||||
},
|
||||
// headers: {
|
||||
// argsName: ["--headers"],
|
||||
// },
|
||||
deleteSegments: {
|
||||
argsName: ["--enableDelAfterDone"],
|
||||
},
|
||||
proxy: {
|
||||
argsName: ["--proxyAddress"],
|
||||
},
|
||||
},
|
||||
consoleReg: {
|
||||
percent: "([\\d.]+)%",
|
||||
speed: "([\\d.]+\\s[GMK]B/s)",
|
||||
error: "ERROR",
|
||||
start: "开始下载文件",
|
||||
isLive: "识别为直播流, 开始录制",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m3u8",
|
||||
platform: ["darwin"],
|
||||
platform: [Platform.MacOS, Platform.Linux, Platform.Windows],
|
||||
bin: m3u8DownloaderBin,
|
||||
args: {
|
||||
url: {
|
||||
@@ -91,6 +74,9 @@ const processList: Schema[] = [
|
||||
proxy: {
|
||||
argsName: ["--custom-proxy"],
|
||||
},
|
||||
__common__: {
|
||||
argsName: ["--no-log"],
|
||||
},
|
||||
},
|
||||
consoleReg: {
|
||||
percent: "([\\d.]+)%",
|
||||
@@ -102,7 +88,7 @@ const processList: Schema[] = [
|
||||
},
|
||||
{
|
||||
type: "bilibili",
|
||||
platform: ["win32", "darwin"],
|
||||
platform: [Platform.Linux, Platform.MacOS, Platform.Windows],
|
||||
bin: biliDownloaderBin,
|
||||
args: {
|
||||
url: {
|
||||
@@ -116,7 +102,7 @@ const processList: Schema[] = [
|
||||
speed: "([\\d.]+\\s[GMK]B/s)",
|
||||
percent: "([\\d.]+)%",
|
||||
error: "ERROR",
|
||||
start: "保存文件名:",
|
||||
start: "开始下载",
|
||||
isLive: "检测到直播流",
|
||||
},
|
||||
},
|
||||
@@ -138,7 +124,7 @@ export default class DownloadService extends EventEmitter {
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly storeService: ElectronStore,
|
||||
private readonly storeService: ElectronStore
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -157,7 +143,6 @@ export default class DownloadService extends EventEmitter {
|
||||
|
||||
async stopTask(id: number) {
|
||||
if (this.signal[id]) {
|
||||
this.logger.info(`taskId: ${id} stop`);
|
||||
this.signal[id].abort();
|
||||
}
|
||||
}
|
||||
@@ -166,7 +151,7 @@ export default class DownloadService extends EventEmitter {
|
||||
try {
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Downloading,
|
||||
DownloadStatus.Downloading
|
||||
);
|
||||
this.emit("download-start", task.id);
|
||||
|
||||
@@ -192,8 +177,8 @@ export default class DownloadService extends EventEmitter {
|
||||
callback,
|
||||
};
|
||||
|
||||
const { proxy, useProxy } = this.storeService.store;
|
||||
if (useProxy) {
|
||||
const { proxy, downloadProxySwitch } = this.storeService.store;
|
||||
if (downloadProxySwitch && proxy) {
|
||||
params.proxy = proxy;
|
||||
}
|
||||
|
||||
@@ -203,23 +188,24 @@ export default class DownloadService extends EventEmitter {
|
||||
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Success,
|
||||
DownloadStatus.Success
|
||||
);
|
||||
this.emit("download-success", task.id);
|
||||
} catch (err: any) {
|
||||
this.logger.info(`taskId: ${task.id} failed`);
|
||||
if (err.message === "AbortError") {
|
||||
this.logger.info(`taskId: ${task.id} stopped`);
|
||||
// 下载暂停
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Stopped,
|
||||
DownloadStatus.Stopped
|
||||
);
|
||||
this.emit("download-stop", task.id);
|
||||
} else {
|
||||
this.logger.info(`taskId: ${task.id} failed`);
|
||||
// 下载失败
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Failed,
|
||||
DownloadStatus.Failed
|
||||
);
|
||||
this.emit("download-failed", task.id, err);
|
||||
}
|
||||
@@ -256,7 +242,7 @@ export default class DownloadService extends EventEmitter {
|
||||
private _execa(
|
||||
binPath: string,
|
||||
args: string[],
|
||||
params: DownloadOptions,
|
||||
params: DownloadOptions
|
||||
): Promise<void> {
|
||||
const { abortSignal, onMessage, id } = params;
|
||||
|
||||
@@ -269,7 +255,12 @@ export default class DownloadService extends EventEmitter {
|
||||
});
|
||||
|
||||
if (onMessage) {
|
||||
const ctx = {};
|
||||
const ctx: DownloadContext = {
|
||||
ready: false,
|
||||
isLive: false,
|
||||
percent: "",
|
||||
speed: "",
|
||||
};
|
||||
ptyProcess.onData((data) => {
|
||||
try {
|
||||
this.emit("download-message", id, data);
|
||||
@@ -338,6 +329,10 @@ export default class DownloadService extends EventEmitter {
|
||||
if (key === "proxy" && proxy) {
|
||||
argsName && argsName.forEach((i) => spawnParams.push(i, proxy));
|
||||
}
|
||||
|
||||
if (key === "__common__") {
|
||||
argsName && spawnParams.push(...argsName);
|
||||
}
|
||||
}
|
||||
|
||||
const { consoleReg } = schema;
|
||||
@@ -347,7 +342,7 @@ export default class DownloadService extends EventEmitter {
|
||||
const speedReg = RegExp(consoleReg.speed, "g");
|
||||
const percentReg = RegExp(consoleReg.percent, "g");
|
||||
|
||||
const onMessage = (ctx: any, message: string) => {
|
||||
const onMessage = (ctx: DownloadContext, message: string) => {
|
||||
// 解析是否为直播资源
|
||||
if (isLiveReg.test(message)) {
|
||||
ctx.isLive = true;
|
||||
@@ -367,10 +362,11 @@ export default class DownloadService extends EventEmitter {
|
||||
callback({
|
||||
id,
|
||||
type: "ready",
|
||||
isLive: !!ctx.isLive,
|
||||
isLive: ctx.isLive,
|
||||
speed: "",
|
||||
percent: "",
|
||||
});
|
||||
ctx.ready = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -378,17 +374,18 @@ export default class DownloadService extends EventEmitter {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// FIXME: 无法获取是否为直播流
|
||||
callback({
|
||||
id,
|
||||
type: "progress",
|
||||
percent: ctx.percent || "",
|
||||
speed: ctx.speed || "",
|
||||
isLive: !!ctx.isLive,
|
||||
});
|
||||
if (ctx.ready && (ctx.percent || ctx.speed)) {
|
||||
callback({
|
||||
id,
|
||||
type: "progress",
|
||||
percent: ctx.percent || "",
|
||||
speed: ctx.speed || "",
|
||||
isLive: ctx.isLive,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.logger.debug("download params: ", spawnParams);
|
||||
this.logger.debug("download params: ", spawnParams.join(" "));
|
||||
await this._execa(schema.bin, spawnParams, {
|
||||
id,
|
||||
abortSignal,
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { DownloadType } from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import ElectronLogger from "../vendor/ElectronLogger";
|
||||
import { DownloadType } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import ElectronLogger from "../vendor/ElectronLogger.ts";
|
||||
import EventEmitter from "events";
|
||||
import { session } from "electron";
|
||||
import { PERSIST_WEBVIEW } from "../helper";
|
||||
import { OnSendHeadersListenerDetails, session } from "electron";
|
||||
import {
|
||||
CallbackResponse,
|
||||
OnBeforeRequestListenerDetails,
|
||||
} from "electron/main";
|
||||
PERSIST_WEBVIEW,
|
||||
PRIVACY_WEBVIEW,
|
||||
formatHeaders,
|
||||
} from "../helper/index.ts";
|
||||
|
||||
export interface SourceParams {
|
||||
id: number;
|
||||
url: string;
|
||||
filter: SourceFilter;
|
||||
documentURL: string;
|
||||
name: string;
|
||||
type: DownloadType;
|
||||
headers?: string;
|
||||
}
|
||||
|
||||
export interface SourceFilter {
|
||||
matches: RegExp[];
|
||||
hosts?: RegExp[];
|
||||
matches?: RegExp[];
|
||||
type: DownloadType;
|
||||
schema?: Record<string, string>;
|
||||
}
|
||||
@@ -37,7 +37,7 @@ const filterList: SourceFilter[] = [
|
||||
},
|
||||
{
|
||||
// TODO: 合集、列表、收藏夹
|
||||
matches: [/^https?:\/\/(www\.)?bilibili.com\/video/],
|
||||
hosts: [/^https?:\/\/(www\.)?bilibili.com\/video/],
|
||||
type: DownloadType.bilibili,
|
||||
schema: {
|
||||
name: "title",
|
||||
@@ -53,7 +53,7 @@ export class SniffingHelper extends EventEmitter {
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger,
|
||||
private readonly logger: ElectronLogger
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -65,54 +65,72 @@ export class SniffingHelper extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
update(pageInfo: PageInfo) {
|
||||
this.pageInfo = pageInfo;
|
||||
}
|
||||
|
||||
reset(pageInfo: PageInfo) {
|
||||
this.pageInfo = pageInfo;
|
||||
this.ready = false;
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
start() {
|
||||
const viewSession = session.fromPartition(PERSIST_WEBVIEW);
|
||||
viewSession.webRequest.onBeforeRequest(this.requestWillBeSent.bind(this));
|
||||
}
|
||||
listLoop: for (const filter of filterList) {
|
||||
if (filter.hosts) {
|
||||
for (const host of filter.hosts) {
|
||||
if (!host.test(pageInfo.url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
private requestWillBeSent(
|
||||
details: OnBeforeRequestListenerDetails,
|
||||
callback: (response: CallbackResponse) => void,
|
||||
): void {
|
||||
const { id, webContents, url } = details;
|
||||
const { title } = this.pageInfo;
|
||||
|
||||
const documentURL = webContents?.getURL() || "";
|
||||
|
||||
for (const filter of filterList) {
|
||||
for (const match of filter.matches) {
|
||||
const u = new URL(url);
|
||||
if (!match.test(u.pathname)) {
|
||||
continue;
|
||||
this.send({
|
||||
url: pageInfo.url,
|
||||
documentURL: pageInfo.url,
|
||||
name: pageInfo.title,
|
||||
type: filter.type,
|
||||
});
|
||||
break listLoop;
|
||||
}
|
||||
|
||||
const item: SourceParams = {
|
||||
id,
|
||||
url,
|
||||
filter,
|
||||
documentURL,
|
||||
name: title,
|
||||
type: filter.type,
|
||||
};
|
||||
|
||||
this.logger.info(`在窗口中捕获视频链接: ${url}`);
|
||||
// 等待 DOM 中浮窗加载完成
|
||||
if (this.ready) {
|
||||
this.emit("source", item);
|
||||
} else {
|
||||
this.queue.push(item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
callback({});
|
||||
}
|
||||
|
||||
start(privacy: boolean = false) {
|
||||
const partition = privacy ? PRIVACY_WEBVIEW : PERSIST_WEBVIEW;
|
||||
const viewSession = session.fromPartition(partition);
|
||||
viewSession.webRequest.onSendHeaders(this.onSendHeaders);
|
||||
}
|
||||
|
||||
send = (item: SourceParams) => {
|
||||
this.logger.info(`在窗口中捕获视频链接: ${item.url}`);
|
||||
// 等待 DOM 中浮窗加载完成
|
||||
if (this.ready) {
|
||||
this.emit("source", item);
|
||||
} else {
|
||||
this.queue.push(item);
|
||||
}
|
||||
};
|
||||
|
||||
private onSendHeaders = (details: OnSendHeadersListenerDetails): void => {
|
||||
const { url, requestHeaders } = details;
|
||||
const { title, url: documentURL } = this.pageInfo;
|
||||
|
||||
listLoop: for (const filter of filterList) {
|
||||
if (filter.matches) {
|
||||
for (const match of filter.matches) {
|
||||
const u = new URL(url);
|
||||
if (!match.test(u.pathname)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.send({
|
||||
url,
|
||||
documentURL,
|
||||
name: title,
|
||||
type: filter.type,
|
||||
headers: formatHeaders(requestHeaders),
|
||||
});
|
||||
break listLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import { BrowserView, session } from "electron";
|
||||
import { Event, HandlerDetails, WebContentsView, session } from "electron";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "../types";
|
||||
import { TYPES } from "../types.ts";
|
||||
import isDev from "electron-is-dev";
|
||||
import { PERSIST_WEBVIEW, fetch, mobileUA, pcUA, pluginPath } from "../helper";
|
||||
import {
|
||||
PERSIST_WEBVIEW,
|
||||
PRIVACY_WEBVIEW,
|
||||
fetch,
|
||||
mobileUA,
|
||||
pcUA,
|
||||
pluginPath,
|
||||
} from "../helper/index.ts";
|
||||
import { ElectronBlocker } from "@cliqz/adblocker-electron";
|
||||
import ElectronLogger from "../vendor/ElectronLogger";
|
||||
import ElectronStore from "../vendor/ElectronStore";
|
||||
import MainWindow from "../windows/MainWindow";
|
||||
import BrowserWindow from "../windows/BrowserWindow";
|
||||
import VideoRepository from "../repository/VideoRepository";
|
||||
import { SniffingHelper } from "./SniffingHelperService";
|
||||
import ElectronLogger from "../vendor/ElectronLogger.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import MainWindow from "../windows/MainWindow.ts";
|
||||
import BrowserWindow from "../windows/BrowserWindow.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import { SniffingHelper, SourceParams } from "./SniffingHelperService.ts";
|
||||
import { resolve } from "path";
|
||||
import { readFileSync } from "fs";
|
||||
import { readFileSync } from "fs-extra";
|
||||
|
||||
@injectable()
|
||||
export default class WebviewService {
|
||||
public view: BrowserView;
|
||||
private view: WebContentsView | null = null;
|
||||
private blocker?: ElectronBlocker;
|
||||
private defaultSession: string;
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.MainWindow)
|
||||
@@ -30,155 +38,197 @@ export default class WebviewService {
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.SniffingHelper)
|
||||
private readonly sniffingHelper: SniffingHelper,
|
||||
private readonly sniffingHelper: SniffingHelper
|
||||
) {
|
||||
// 初始化 blocker
|
||||
this.initBlocker();
|
||||
|
||||
const { useProxy, proxy, privacy } = this.store.store;
|
||||
|
||||
this.sniffingHelper.start(privacy);
|
||||
this.sniffingHelper.on("source", this.onSource);
|
||||
|
||||
this.setDefaultSession(privacy, true);
|
||||
this.setProxy(useProxy, proxy);
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.view = new BrowserView({
|
||||
this.view = new WebContentsView({
|
||||
webPreferences: {
|
||||
partition: PERSIST_WEBVIEW,
|
||||
partition: this.defaultSession,
|
||||
preload: resolve(__dirname, "./preload.js"),
|
||||
},
|
||||
});
|
||||
this.view.setBackgroundColor("#fff");
|
||||
this.webContents.setAudioMuted(true);
|
||||
this.view.webContents.setAudioMuted(true);
|
||||
|
||||
const { useProxy, proxy, isMobile } = this.store.store;
|
||||
this.setProxy(useProxy, proxy);
|
||||
const { isMobile } = this.store.store;
|
||||
this.setUserAgent(isMobile);
|
||||
|
||||
this.webContents.on("dom-ready", async () => {
|
||||
const baseInfo = {
|
||||
title: this.webContents.getTitle(),
|
||||
url: this.webContents.getURL(),
|
||||
};
|
||||
this.sniffingHelper.reset(baseInfo);
|
||||
this.curWindow?.webContents.send("webview-dom-ready", baseInfo);
|
||||
this.view.webContents.on("dom-ready", this.onDomReady);
|
||||
this.view.webContents.on("did-navigate", this.onDidNavigate);
|
||||
this.view.webContents.on("did-fail-load", this.onDidFailLoad);
|
||||
this.view.webContents.on("did-navigate-in-page", this.onDidNavigateInPage);
|
||||
this.view.webContents.setWindowOpenHandler(this.onOpenNewWindow);
|
||||
}
|
||||
|
||||
try {
|
||||
if (isDev && process.env.DEBUG_PLUGINS === "true") {
|
||||
const content =
|
||||
'function addScript(src){const script=document.createElement("script");script.src=src;script.type="module";document.body.appendChild(script)}addScript("http://localhost:8080/src/main.ts");';
|
||||
await this.webContents.executeJavaScript(content);
|
||||
} else {
|
||||
const content = readFileSync(pluginPath, "utf-8");
|
||||
await this.webContents.executeJavaScript(content);
|
||||
}
|
||||
} catch (err) {
|
||||
// empty
|
||||
}
|
||||
});
|
||||
onDomReady = () => {
|
||||
if (!this.view) return;
|
||||
const pageInfo = this.getPageInfo();
|
||||
this.sniffingHelper.update(pageInfo);
|
||||
this.window.webContents.send("webview-dom-ready", pageInfo);
|
||||
};
|
||||
|
||||
// 兼容网站在当前页面中打开
|
||||
this.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url === "about:blank") {
|
||||
// 兼容一些网站跳转到 about:blank
|
||||
this.webContents.once("will-redirect", async (event, url) => {
|
||||
this.loadURL(url, true);
|
||||
});
|
||||
onDidNavigate = async () => {
|
||||
if (!this.view) return;
|
||||
const pageInfo = this.getPageInfo();
|
||||
this.sniffingHelper.reset(pageInfo);
|
||||
this.window.webContents.send("webview-did-navigate", pageInfo);
|
||||
|
||||
try {
|
||||
if (isDev && process.env.DEBUG_PLUGINS === "true") {
|
||||
const content =
|
||||
'function addScript(src){const script=document.createElement("script");script.src=src;script.type="module";document.body.appendChild(script)}addScript("http://localhost:8080/src/main.ts");';
|
||||
await this.view.webContents.executeJavaScript(content);
|
||||
} else {
|
||||
this.loadURL(url, true);
|
||||
const content = readFileSync(pluginPath, "utf-8");
|
||||
await this.view.webContents.executeJavaScript(content);
|
||||
}
|
||||
} catch (err) {
|
||||
// empty
|
||||
}
|
||||
};
|
||||
|
||||
return { action: "deny" };
|
||||
});
|
||||
onDidFailLoad = (e: Event, code: number, desc: string) => {
|
||||
this.window.webContents.send("webview-fail-load", { code, desc });
|
||||
};
|
||||
|
||||
this.sniffingHelper.start();
|
||||
this.sniffingHelper.on("source", async (item) => {
|
||||
// 这里需要判断是否使用浏览器插件
|
||||
const useExtension = this.store.get("useExtension");
|
||||
if (useExtension) {
|
||||
this.webContents.send("webview-link-message", item);
|
||||
} else {
|
||||
const video = await this.videoRepository.findVideoByName(item.name);
|
||||
if (video) {
|
||||
item.name = `${item.name}-${Date.now()}`;
|
||||
}
|
||||
const res = await this.videoRepository.addVideo(item);
|
||||
const mainWebContents = this.mainWindow.window?.webContents;
|
||||
if (!mainWebContents) return;
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
mainWebContents.send("download-item-notifier", res);
|
||||
onDidNavigateInPage = () => {
|
||||
if (!this.view) return;
|
||||
const pageInfo = this.getPageInfo();
|
||||
this.sniffingHelper.update(pageInfo);
|
||||
this.window.webContents.send("webview-did-navigate-in-page", pageInfo);
|
||||
};
|
||||
|
||||
onOpenNewWindow = ({ url }: HandlerDetails) => {
|
||||
this.loadURL(url);
|
||||
|
||||
return { action: "deny" } as { action: "deny" };
|
||||
};
|
||||
|
||||
onSource = async (item: SourceParams) => {
|
||||
if (!this.view) return;
|
||||
// 这里需要判断是否使用浏览器插件
|
||||
const useExtension = this.store.get("useExtension");
|
||||
if (useExtension) {
|
||||
this.view.webContents.send("webview-link-message", item);
|
||||
} else {
|
||||
const video = await this.videoRepository.findVideoByName(item.name);
|
||||
if (video) {
|
||||
item.name = `${item.name}-${Date.now()}`;
|
||||
}
|
||||
});
|
||||
const res = await this.videoRepository.addVideo(item);
|
||||
const mainWebContents = this.mainWindow.window?.webContents;
|
||||
if (!mainWebContents) return;
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
mainWebContents.send("download-item-notifier", res);
|
||||
}
|
||||
};
|
||||
|
||||
getPageInfo() {
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
return {
|
||||
title: this.view.webContents.getTitle(),
|
||||
url: this.view.webContents.getURL(),
|
||||
};
|
||||
}
|
||||
|
||||
getBounds(): Electron.Rectangle {
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
return this.view.getBounds();
|
||||
}
|
||||
|
||||
setAutoResize(options: Electron.AutoResizeOptions): void {
|
||||
this.view.setAutoResize(options);
|
||||
setBounds(bounds: Electron.Rectangle): void {
|
||||
if (!this.view) return;
|
||||
this.view.setBounds(bounds);
|
||||
}
|
||||
|
||||
setBackgroundColor(color: string): void {
|
||||
if (!this.view) return;
|
||||
this.view.setBackgroundColor(color);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.curWindow?.setBrowserView(this.view);
|
||||
isDev && this.webContents.openDevTools();
|
||||
if (!this.view) return;
|
||||
this.window.contentView.addChildView(this.view);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.curWindow?.setBrowserView(null);
|
||||
isDev && this.webContents.closeDevTools();
|
||||
if (!this.view) return;
|
||||
this.window.contentView.removeChildView(this.view);
|
||||
}
|
||||
|
||||
setBounds(bounds: Electron.Rectangle): void {
|
||||
this.view.setBounds(bounds);
|
||||
}
|
||||
|
||||
async loadURL(url?: string, isNewWindow?: boolean) {
|
||||
const canGoBack = this.webContents.canGoBack();
|
||||
|
||||
try {
|
||||
this.webContents.stop();
|
||||
await this.webContents.loadURL(url || "");
|
||||
} catch (err: unknown) {
|
||||
this.logger.error("加载 url 时出现错误: ", err);
|
||||
throw err;
|
||||
} finally {
|
||||
if (!canGoBack && !isNewWindow) {
|
||||
this.webContents.clearHistory();
|
||||
}
|
||||
loadURL(url: string) {
|
||||
// 开始加载 url
|
||||
if (!this.view) {
|
||||
this.init();
|
||||
}
|
||||
if (!this.view) return;
|
||||
|
||||
// 1. 停止当前导航
|
||||
this.view.webContents.stop();
|
||||
|
||||
// 2. 加载新的 url
|
||||
this.view.webContents.loadURL(url);
|
||||
}
|
||||
|
||||
async goBack() {
|
||||
if (this.webContents.canGoBack()) {
|
||||
this.webContents.goBack();
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
const { webContents } = this.view;
|
||||
if (webContents.canGoBack()) {
|
||||
webContents.goBack();
|
||||
return true;
|
||||
} else {
|
||||
this.destroyView();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async reload() {
|
||||
this.webContents.reload();
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
this.view.webContents.reload();
|
||||
}
|
||||
|
||||
async goHome() {
|
||||
this.webContents.stop();
|
||||
this.webContents.clearHistory();
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
this.view.webContents.stop();
|
||||
this.view.webContents.clearHistory();
|
||||
this.destroyView();
|
||||
}
|
||||
|
||||
get curWindow() {
|
||||
get window() {
|
||||
if (this.browserWindow.window) return this.browserWindow.window;
|
||||
if (this.mainWindow.window) return this.mainWindow.window;
|
||||
return null;
|
||||
throw new Error("未找到当前窗口");
|
||||
}
|
||||
|
||||
private get session() {
|
||||
return session.fromPartition(PERSIST_WEBVIEW);
|
||||
return session.fromPartition(this.defaultSession);
|
||||
}
|
||||
|
||||
private enableProxy(proxy: string) {
|
||||
if (!proxy) {
|
||||
this.logger.error("[proxy] 代理地址不能为空");
|
||||
this.logger.error("[Proxy] 代理地址不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -188,12 +238,12 @@ export default class WebviewService {
|
||||
}
|
||||
|
||||
this.session.setProxy({ proxyRules: proxy });
|
||||
this.logger.info(`[proxy] 代理开启成功,代理地址为${proxy}`);
|
||||
this.logger.info(`[Proxy] 代理开启(${proxy})`);
|
||||
}
|
||||
|
||||
private disableProxy() {
|
||||
this.session.setProxy({ proxyRules: "" });
|
||||
this.logger.info("[proxy] 代理关闭成功");
|
||||
this.logger.info("[Proxy] 代理关闭");
|
||||
}
|
||||
|
||||
setProxy(useProxy: boolean, proxy: string): void {
|
||||
@@ -221,39 +271,78 @@ export default class WebviewService {
|
||||
|
||||
private enableBlocking() {
|
||||
if (!this.blocker) {
|
||||
this.logger.error("开启 blocker 失败,未初始化");
|
||||
this.logger.error("[AdBlocker] 开启失败(未初始化)");
|
||||
return;
|
||||
}
|
||||
this.blocker.enableBlockingInSession(this.session);
|
||||
this.logger.info("开启 blocker 成功");
|
||||
this.logger.info("[AdBlocker] 开启");
|
||||
}
|
||||
|
||||
private disableBlocking() {
|
||||
if (!this.blocker) {
|
||||
this.logger.error("关闭 blocker 失败,未初始化");
|
||||
this.logger.error("[AdBlocker] 关闭失败(未初始化)");
|
||||
return;
|
||||
}
|
||||
if (!this.blocker.isBlockingEnabled(this.session)) {
|
||||
return;
|
||||
}
|
||||
this.blocker.disableBlockingInSession(this.session);
|
||||
this.logger.info("关闭 blocker 成功");
|
||||
this.logger.info("[AdBlocker] 关闭");
|
||||
}
|
||||
|
||||
setUserAgent(isMobile?: boolean) {
|
||||
if (!this.view) return;
|
||||
if (isMobile) {
|
||||
this.webContents.setUserAgent(mobileUA);
|
||||
this.view.webContents.setUserAgent(mobileUA);
|
||||
} else {
|
||||
this.webContents.setUserAgent(pcUA);
|
||||
this.view.webContents.setUserAgent(pcUA);
|
||||
}
|
||||
this.logger.info("设置 user-agent 成功", isMobile);
|
||||
this.logger.info(`[UA] 设置为${isMobile ? "移动端" : " pc 端"}`);
|
||||
}
|
||||
|
||||
get debugger() {
|
||||
return this.webContents.debugger;
|
||||
captureView(): Promise<Electron.NativeImage> {
|
||||
if (!this.view) {
|
||||
throw new Error("未找到 view");
|
||||
}
|
||||
return this.view.webContents.capturePage();
|
||||
}
|
||||
|
||||
get webContents() {
|
||||
return this.view.webContents;
|
||||
sendToWindow(channel: string, ...args: unknown[]) {
|
||||
this.window.webContents.send(channel, ...args);
|
||||
}
|
||||
|
||||
destroyView() {
|
||||
if (this.view && this.window) {
|
||||
this.view.webContents.close();
|
||||
this.window.contentView.removeChildView(this.view);
|
||||
}
|
||||
// FIXME: 为了避免内存泄漏,这里需要销毁 view
|
||||
this.view = null;
|
||||
}
|
||||
|
||||
async clearCache() {
|
||||
await this.session.clearCache();
|
||||
await this.session.clearStorageData();
|
||||
}
|
||||
|
||||
setDefaultSession(isPrivacy = false, init = false) {
|
||||
this.logger.info(`[Session] ${isPrivacy ? "隐私" : "正常"}模式`);
|
||||
if (isPrivacy) {
|
||||
this.defaultSession = PRIVACY_WEBVIEW;
|
||||
} else {
|
||||
this.defaultSession = PERSIST_WEBVIEW;
|
||||
}
|
||||
|
||||
if (this.view) {
|
||||
const { useProxy, proxy } = this.store.store;
|
||||
this.destroyView();
|
||||
this.init();
|
||||
this.setProxy(useProxy, proxy);
|
||||
this.sniffingHelper.start(isPrivacy);
|
||||
}
|
||||
|
||||
if (!init) {
|
||||
this.window.webContents.send("change-privacy");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export const TYPES = {
|
||||
// repository
|
||||
VideoRepository: Symbol.for("VideoRepository"),
|
||||
FavoriteRepository: Symbol.for("FavoriteRepository"),
|
||||
ConversionRepository: Symbol.for("ConversionRepository"),
|
||||
// windows
|
||||
BrowserWindow: Symbol.for("BrowserWindow"),
|
||||
MainWindow: Symbol.for("MainWindow"),
|
||||
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "../types";
|
||||
import { TYPES } from "../types.ts";
|
||||
import isDev from "electron-is-dev";
|
||||
import installExtension, {
|
||||
import install, {
|
||||
REDUX_DEVTOOLS,
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
} from "electron-devtools-installer";
|
||||
import ElectronLogger from "./ElectronLogger";
|
||||
import { Vendor } from "../core/vendor";
|
||||
import ElectronLogger from "./ElectronLogger.ts";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
|
||||
@injectable()
|
||||
export default class DevToolsService implements Vendor {
|
||||
@@ -26,7 +26,7 @@ export default class DevToolsService implements Vendor {
|
||||
|
||||
try {
|
||||
this.logger.debug("当前环境为开发环境,开始加载开发者工具");
|
||||
await installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS]);
|
||||
await install([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS]);
|
||||
this.logger.debug("加载开发者工具成功");
|
||||
} catch (err: unknown) {
|
||||
this.logger.error("加载开发者工具失败", err);
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import logger, { Logger } from "electron-log";
|
||||
import { injectable } from "inversify";
|
||||
import dayjs from "dayjs";
|
||||
import path from "path";
|
||||
import { appName, workspace } from "../helper";
|
||||
import { Vendor } from "../core/vendor";
|
||||
import { appName, workspace } from "../helper/index.ts";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
|
||||
@injectable()
|
||||
export default class ElectronLogger implements Vendor {
|
||||
|
||||
+7
-4
@@ -1,9 +1,9 @@
|
||||
import Store from "electron-store";
|
||||
import { injectable } from "inversify";
|
||||
import { AppStore } from "../main";
|
||||
import { download, workspace } from "../helper";
|
||||
import { AppLanguage, AppTheme } from "../types";
|
||||
import { Vendor } from "../core/vendor";
|
||||
import { AppStore } from "../main.ts";
|
||||
import { download, workspace } from "../helper/index.ts";
|
||||
import { AppLanguage, AppTheme } from "../types.ts";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
|
||||
@injectable()
|
||||
export default class StoreService extends Store<AppStore> implements Vendor {
|
||||
@@ -28,6 +28,9 @@ export default class StoreService extends Store<AppStore> implements Vendor {
|
||||
language: AppLanguage.System,
|
||||
showTerminal: false,
|
||||
privacy: false,
|
||||
machineId: "",
|
||||
downloadProxySwitch: false,
|
||||
autoUpgrade: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+12
-4
@@ -1,22 +1,30 @@
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "../types";
|
||||
import { TYPES } from "../types.ts";
|
||||
import isDev from "electron-is-dev";
|
||||
import ElectronLogger from "./ElectronLogger";
|
||||
import { Vendor } from "../core/vendor";
|
||||
import ElectronLogger from "./ElectronLogger.ts";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
import ElectronStore from "electron-store";
|
||||
|
||||
@injectable()
|
||||
export default class UpdateService implements Vendor {
|
||||
constructor(
|
||||
@inject(TYPES.ElectronLogger) private readonly logger: ElectronLogger,
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
if (isDev) return;
|
||||
|
||||
const { autoUpgrade } = this.store.store;
|
||||
if (!autoUpgrade) return;
|
||||
|
||||
try {
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
autoUpdater.logger = this.logger.logger;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
await autoUpdater.checkForUpdatesAndNotify({
|
||||
title: "自动更新完成",
|
||||
body: "下次重启时将会自动安装",
|
||||
|
||||
Vendored
+9
-8
@@ -1,11 +1,12 @@
|
||||
import { db } from "../helper";
|
||||
import { db } from "../helper/index.ts";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { DataSource, EntityManager } from "typeorm";
|
||||
import { TYPES } from "../types";
|
||||
import { Video } from "../entity/Video";
|
||||
import { Favorite } from "../entity/Favorite";
|
||||
import ElectronLogger from "./ElectronLogger";
|
||||
import { Vendor } from "../core/vendor";
|
||||
import { TYPES } from "../types.ts";
|
||||
import { Video } from "../entity/Video.ts";
|
||||
import { Favorite } from "../entity/Favorite.ts";
|
||||
import ElectronLogger from "./ElectronLogger.ts";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
import { Conversion } from "../entity/Conversion.ts";
|
||||
|
||||
@injectable()
|
||||
export default class DatabaseService implements Vendor {
|
||||
@@ -13,14 +14,14 @@ export default class DatabaseService implements Vendor {
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger,
|
||||
private readonly logger: ElectronLogger
|
||||
) {
|
||||
this.appDataSource = new DataSource({
|
||||
type: "better-sqlite3",
|
||||
database: db,
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
entities: [Video, Favorite],
|
||||
entities: [Video, Favorite, Conversion],
|
||||
migrations: [],
|
||||
subscribers: [],
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import isDev from "electron-is-dev";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { resolve } from "path";
|
||||
import { TYPES } from "../types";
|
||||
import { TYPES } from "../types.ts";
|
||||
import _ from "lodash";
|
||||
import Window from "../core/window";
|
||||
import ElectronStore from "../vendor/ElectronStore";
|
||||
import Window from "../core/window.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
|
||||
@injectable()
|
||||
export default class BrowserWindow extends Window {
|
||||
@@ -32,13 +32,11 @@ export default class BrowserWindow extends Window {
|
||||
this.store.onDidAnyChange(this.storeChange);
|
||||
}
|
||||
|
||||
storeChange = (store: any) => {
|
||||
if (!this.window) return;
|
||||
// 向所有窗口发送通知
|
||||
this.window.webContents.send("store-change", store);
|
||||
storeChange = (store: unknown) => {
|
||||
this.send("store-change", store);
|
||||
};
|
||||
|
||||
handleNewWindowsVal = (newValue: any) => {
|
||||
handleNewWindowsVal = (newValue: unknown) => {
|
||||
if (!this.window) return;
|
||||
|
||||
// 向所有窗口发送通知
|
||||
@@ -76,4 +74,10 @@ export default class BrowserWindow extends Window {
|
||||
|
||||
this.window.close();
|
||||
};
|
||||
|
||||
send(channel: string, ...args: unknown[]) {
|
||||
if (!this.window) return;
|
||||
|
||||
this.window.webContents.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ import { Menu, Notification, app } from "electron";
|
||||
import isDev from "electron-is-dev";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { resolve } from "path";
|
||||
import { TYPES } from "../types";
|
||||
import { DownloadProgress } from "../interfaces";
|
||||
import { TYPES } from "../types.ts";
|
||||
import { DownloadProgress } from "../interfaces.ts";
|
||||
import _ from "lodash";
|
||||
import Window from "../core/window";
|
||||
import ElectronLogger from "../vendor/ElectronLogger";
|
||||
import DownloadService from "../services/DownloadService";
|
||||
import ElectronStore from "../vendor/ElectronStore";
|
||||
import VideoRepository from "../repository/VideoRepository";
|
||||
import { isWin } from "../helper";
|
||||
import Window from "../core/window.ts";
|
||||
import ElectronLogger from "../vendor/ElectronLogger.ts";
|
||||
import DownloadService from "../services/DownloadService.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import { isWin } from "../helper/index.ts";
|
||||
|
||||
@injectable()
|
||||
export default class MainWindow extends Window {
|
||||
@@ -48,9 +48,11 @@ export default class MainWindow extends Window {
|
||||
app.on("second-instance", this.secondInstance);
|
||||
}
|
||||
|
||||
onDownloadReadyStart = ({ id, isLive }: { id: number; isLive: boolean }) => {
|
||||
this.videoRepository.changeVideoIsLive(id, isLive);
|
||||
this.send("change-video-is-live", { id, isLive });
|
||||
onDownloadReadyStart = async ({ id, isLive }: DownloadProgress) => {
|
||||
if (isLive) {
|
||||
await this.videoRepository.changeVideoIsLive(id);
|
||||
this.send("change-video-is-live", { id });
|
||||
}
|
||||
};
|
||||
|
||||
init(): void {
|
||||
@@ -96,7 +98,7 @@ export default class MainWindow extends Window {
|
||||
}
|
||||
};
|
||||
|
||||
storeChange = (store: any) => {
|
||||
storeChange = (store: unknown) => {
|
||||
// 向所有窗口发送通知
|
||||
this.send("store-change", store);
|
||||
};
|
||||
@@ -112,21 +114,21 @@ export default class MainWindow extends Window {
|
||||
|
||||
new Notification({
|
||||
title: "下载成功",
|
||||
body: `${video?.name} 下载成功`,
|
||||
body: `${video.name} 下载成功`,
|
||||
}).show();
|
||||
}
|
||||
|
||||
this.send("download-success", id);
|
||||
};
|
||||
|
||||
onDownloadFailed = async (id: number, err: any) => {
|
||||
onDownloadFailed = async (id: number, err: unknown) => {
|
||||
const promptTone = this.store.get("promptTone");
|
||||
if (promptTone) {
|
||||
const video = await this.videoRepository.findVideo(id);
|
||||
|
||||
new Notification({
|
||||
title: "下载失败",
|
||||
body: `${video?.name} 下载失败`,
|
||||
body: `${video.name} 下载失败`,
|
||||
}).show();
|
||||
}
|
||||
this.logger.error("下载失败:", err);
|
||||
@@ -150,7 +152,7 @@ export default class MainWindow extends Window {
|
||||
}
|
||||
};
|
||||
|
||||
send(channel: string, ...args: any[]) {
|
||||
send(channel: string, ...args: unknown[]) {
|
||||
if (!this.window) return;
|
||||
|
||||
this.window.webContents.send(channel, ...args);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "CommonJS",
|
||||
"module": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
@@ -9,13 +9,14 @@
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strictPropertyInitialization": false,
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./types",
|
||||
"outDir": "app/output/build/main"
|
||||
"outDir": "app/output/build/main",
|
||||
"allowImportingTsExtensions": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
overrides: [],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{ languageOptions: { globals: globals.browser } },
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -12,24 +12,22 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"jquery": "^3.7.1",
|
||||
"lit": "^3.1.2",
|
||||
"nanoid": "^5.0.4"
|
||||
"lit": "^3.1.3",
|
||||
"nanoid": "^5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jquery": "^3.5.29",
|
||||
"@types/node": "^20.11.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"lint-staged": "^15.2.2",
|
||||
"prettier": "3.2.5",
|
||||
"stylelint": "^16.2.1",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@types/jquery": "^3.5.30",
|
||||
"@types/node": "^20.12.12",
|
||||
"eslint": "^9.3.0",
|
||||
"globals": "^15.3.0",
|
||||
"lint-staged": "^15.2.5",
|
||||
"stylelint": "^16.6.0",
|
||||
"stylelint-config-standard": "^36.0.0",
|
||||
"stylelint-config-standard-scss": "^13.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12"
|
||||
"stylelint-config-standard-scss": "^13.1.0",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^7.10.0",
|
||||
"vite": "^5.2.11"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": "eslint",
|
||||
|
||||
@@ -2,8 +2,9 @@ import { LitElement, html, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { BILIBILI_DOWNLOAD_BUTTON, showDownloadDialog } from "../helper";
|
||||
import $ from "jquery";
|
||||
import { DownloadType } from "../types";
|
||||
|
||||
@customElement("one-button")
|
||||
@customElement("bilibili-button")
|
||||
export class BilibiliButton extends LitElement {
|
||||
@property({ type: Number })
|
||||
index = 0;
|
||||
@@ -39,7 +40,7 @@ export class BilibiliButton extends LitElement {
|
||||
const videoImage = $(BILIBILI_DOWNLOAD_BUTTON).eq(this.index);
|
||||
const url = videoImage.attr("href") || "";
|
||||
const name = videoImage.parent().find(".bili-video-card__info--tit").text();
|
||||
showDownloadDialog({ name, url, type: "bilibili" });
|
||||
showDownloadDialog([{ name, url, type: DownloadType.bilibili }]);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -53,7 +54,7 @@ function bilibili() {
|
||||
const imageDOM = card.querySelector(BILIBILI_DOWNLOAD_BUTTON);
|
||||
if (!imageDOM) return;
|
||||
|
||||
const oldBtn = imageDOM.querySelectorAll("one-button");
|
||||
const oldBtn = card.querySelectorAll("bilibili-button");
|
||||
if (oldBtn.length) return;
|
||||
|
||||
const isAd = $(card).find(".bili-video-card__info--ad");
|
||||
@@ -65,7 +66,9 @@ function bilibili() {
|
||||
});
|
||||
}
|
||||
|
||||
bilibili();
|
||||
setInterval(() => {
|
||||
if (location.hostname === "www.bilibili.com") {
|
||||
bilibili();
|
||||
}, 3000);
|
||||
setInterval(() => {
|
||||
bilibili();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import logo from "../assets/logo.png";
|
||||
import { addIpcListener, pluginReady, showDownloadDialog } from "../helper";
|
||||
import {
|
||||
addIpcListener,
|
||||
pluginReady,
|
||||
removeIpcListener,
|
||||
showDownloadDialog,
|
||||
} from "../helper";
|
||||
import { DownloadType } from "../../../main/types/interfaces";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
interface SourceData {
|
||||
id: number;
|
||||
@@ -13,6 +19,7 @@ interface SourceData {
|
||||
documentURL: string;
|
||||
name: string;
|
||||
type: DownloadType;
|
||||
headers?: string;
|
||||
}
|
||||
|
||||
@customElement("float-button")
|
||||
@@ -22,81 +29,190 @@ export class FloatButton extends LitElement {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 99999;
|
||||
z-index: 9999999;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.3s ease-in-out;
|
||||
border-radius: 5px;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
visibility: hidden;
|
||||
&.show {
|
||||
visibility: visible;
|
||||
}
|
||||
&:hover {
|
||||
background: #f2f2f2;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.logo-img {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
flex: 1;
|
||||
}
|
||||
.badge {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: -2px;
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
background: red;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
data: SourceData | null = null;
|
||||
@property({ type: Array })
|
||||
private data: SourceData[] = [];
|
||||
|
||||
@property({ type: Number })
|
||||
count = 0;
|
||||
|
||||
onClick(e: Event) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log("this.data:", this.data);
|
||||
private dragging = false;
|
||||
private dragOccurred: boolean = false;
|
||||
private offsetX: number = 0;
|
||||
private offsetY: number = 0;
|
||||
private button: HTMLElement | null = null;
|
||||
|
||||
onClick() {
|
||||
if (this.dragOccurred) return;
|
||||
if (!this.data) return;
|
||||
|
||||
showDownloadDialog({
|
||||
name: this.data.name,
|
||||
url: this.data.url,
|
||||
type: this.data.type,
|
||||
});
|
||||
showDownloadDialog(this.data);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
addIpcListener("webview-link-message", this.receiveMessage);
|
||||
window.addEventListener("resize", this.handleWindowResize);
|
||||
document.addEventListener("mousemove", this.handleMouseMove);
|
||||
document.addEventListener("mouseup", this.handleMouseUp);
|
||||
|
||||
this.button = this.renderRoot.querySelector(".mg-float-button");
|
||||
}
|
||||
|
||||
receiveMessage = (_: any, data: any) => {
|
||||
this.count = 1;
|
||||
this.data = data;
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
removeIpcListener("webview-link-message", this.receiveMessage);
|
||||
window.removeEventListener("resize", this.handleWindowResize);
|
||||
document.removeEventListener("mousemove", this.handleMouseMove);
|
||||
document.removeEventListener("mouseup", this.handleMouseUp);
|
||||
}
|
||||
|
||||
handleWindowResize = () => {
|
||||
if (!this.button) return;
|
||||
|
||||
const react = this.button.getBoundingClientRect();
|
||||
if (react.left === 0 && react.top === 0) return;
|
||||
|
||||
const newPos = this.getPosition(react.left, react.top);
|
||||
this.button.style.left = `${newPos.left}px`;
|
||||
this.button.style.top = `${newPos.top}px`;
|
||||
};
|
||||
|
||||
receiveMessage = (_: unknown, data: SourceData) => {
|
||||
this.data = [...this.data, { ...data, name: document.title }];
|
||||
};
|
||||
|
||||
handleMouseStart = (event: MouseEvent) => {
|
||||
if (!this.button) return;
|
||||
|
||||
this.dragging = true;
|
||||
this.dragOccurred = false;
|
||||
const react = this.button.getBoundingClientRect();
|
||||
this.offsetX = event.clientX - react.left;
|
||||
this.offsetY = event.clientY - react.top;
|
||||
};
|
||||
|
||||
handleMouseUp = () => {
|
||||
this.dragging = false;
|
||||
};
|
||||
|
||||
getPosition = (
|
||||
newLeft: number,
|
||||
newTop: number
|
||||
): { left: number; top: number } => {
|
||||
if (!this.button) return { left: 0, top: 0 };
|
||||
|
||||
// 获取滚动条的宽度
|
||||
const scrollbarWidth =
|
||||
window.innerWidth - document.documentElement.clientWidth;
|
||||
|
||||
// 获取窗口的宽度和高度
|
||||
const windowWidth =
|
||||
window.innerWidth ||
|
||||
document.documentElement.clientWidth ||
|
||||
document.body.clientWidth;
|
||||
const windowHeight =
|
||||
window.innerHeight ||
|
||||
document.documentElement.clientHeight ||
|
||||
document.body.clientHeight;
|
||||
|
||||
// 确保按钮不会被拖出屏幕
|
||||
if (newLeft < 0) {
|
||||
newLeft = 0;
|
||||
} else if (
|
||||
newLeft + this.button.offsetWidth >
|
||||
windowWidth - scrollbarWidth
|
||||
) {
|
||||
newLeft = windowWidth - this.button.offsetWidth - scrollbarWidth;
|
||||
}
|
||||
|
||||
if (newTop < 0) {
|
||||
newTop = 0;
|
||||
} else if (newTop + this.button.offsetHeight > windowHeight) {
|
||||
newTop = windowHeight - this.button.offsetHeight;
|
||||
}
|
||||
|
||||
return {
|
||||
left: newLeft,
|
||||
top: newTop,
|
||||
};
|
||||
};
|
||||
|
||||
handleMouseMove = (event: MouseEvent) => {
|
||||
if (this.dragging && this.button) {
|
||||
this.dragOccurred = true;
|
||||
const newLeft = event.clientX - this.offsetX;
|
||||
const newTop = event.clientY - this.offsetY;
|
||||
|
||||
// 使用 requestAnimationFrame 来优化性能
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.button) return;
|
||||
|
||||
const position = this.getPosition(newLeft, newTop);
|
||||
this.button.style.left = `${position.left}px`;
|
||||
this.button.style.top = `${position.top}px`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.count === 0) {
|
||||
return html``;
|
||||
}
|
||||
const classes = {
|
||||
"mg-float-button": true,
|
||||
show: this.data.length > 0,
|
||||
};
|
||||
|
||||
return html`<div class="mg-float-button" @click=${this.onClick}>
|
||||
<img class="logo-img" src=${logo} />
|
||||
<span class="badge"></span>
|
||||
return html`<div
|
||||
@click=${this.onClick}
|
||||
@mousedown=${this.handleMouseStart}
|
||||
draggable="false"
|
||||
class=${classMap(classes)}
|
||||
>
|
||||
<img class="logo-img" src=${logo} draggable="false" />
|
||||
<span class="badge">${this.data.length}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
const floatButton = document.createElement("float-button");
|
||||
floatButton.style.position = "fixed";
|
||||
floatButton.style.zIndex = "9999999999999";
|
||||
document.body.appendChild(floatButton);
|
||||
|
||||
// 向主进程发送插件准备好的消息
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { DownloadItem } from "../../../main/types/interfaces";
|
||||
|
||||
const eventMap = new Map();
|
||||
|
||||
@@ -29,11 +30,7 @@ export interface Item {
|
||||
type: any;
|
||||
}
|
||||
|
||||
export function downloadItem(item: Item) {
|
||||
window.electron.downloadItem(item);
|
||||
}
|
||||
|
||||
export function showDownloadDialog(item: Item) {
|
||||
export function showDownloadDialog(item: Omit<DownloadItem, "id">[]) {
|
||||
window.electron.showDownloadDialog(item);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
overrides: [],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["react", "@typescript-eslint"],
|
||||
rules: {
|
||||
"prettier/prettier": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard", "stylelint-config-standard-scss"]
|
||||
"extends": ["stylelint-config-standard", "stylelint-config-standard-scss"],
|
||||
"rules": {
|
||||
"no-descending-specificity": null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginReactConfig from "eslint-plugin-react/configs/recommended.js";
|
||||
import { fixupConfigRules } from "@eslint/compat";
|
||||
|
||||
export default [
|
||||
{ languageOptions: { globals: globals.browser } },
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...fixupConfigRules(pluginReactConfig),
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -8,57 +8,54 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"report": "vite-bundle-visualizer",
|
||||
"lint-staged": "lint-staged"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@ant-design/pro-components": "^2.6.48",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@xterm/addon-fit": "^0.9.0",
|
||||
"ahooks": "^3.7.8",
|
||||
"antd": "^5.13.2",
|
||||
"antd-style": "^3.6.1",
|
||||
"axios": "^1.6.5",
|
||||
"@ant-design/icons": "^5.3.7",
|
||||
"@ant-design/pro-components": "^2.7.9",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"ahooks": "^3.8.0",
|
||||
"antd": "^5.18.0",
|
||||
"antd-style": "^3.6.2",
|
||||
"axios": "^1.7.2",
|
||||
"classnames": "^2.5.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"i18next": "^23.8.2",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"immer": "^10.1.1",
|
||||
"lint-staged": "^15.2.5",
|
||||
"localforage": "^1.10.0",
|
||||
"match-sorter": "^6.3.3",
|
||||
"nanoid": "^5.0.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.0.1",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-router-dom": "^6.21.3",
|
||||
"sort-by": "^1.2.0",
|
||||
"xterm": "^5.3.0"
|
||||
"match-sorter": "^6.3.4",
|
||||
"nanoid": "^5.0.7",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"sort-by": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.5",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@eslint/compat": "^1.0.1",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-redux": "^7.1.33",
|
||||
"@types/sort-by": "^1.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard-with-typescript": "^43.0.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"prettier": "3.2.5",
|
||||
"sass": "^1.70.0",
|
||||
"stylelint": "^16.2.1",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"eslint": "^9.3.0",
|
||||
"globals": "^15.3.0",
|
||||
"sass": "^1.77.2",
|
||||
"stylelint": "^16.6.0",
|
||||
"stylelint-config-standard": "^36.0.0",
|
||||
"stylelint-config-standard-scss": "^13.0.0",
|
||||
"typescript": "~5.3.3",
|
||||
"vite": "^5.0.12"
|
||||
"stylelint-config-standard-scss": "^13.1.0",
|
||||
"typescript": "~5.4.5",
|
||||
"typescript-eslint": "^7.10.0",
|
||||
"vite": "^5.2.11",
|
||||
"vite-bundle-visualizer": "^1.2.1"
|
||||
},
|
||||
"license": "MIT",
|
||||
"lint-staged": {
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { ConfigProvider, theme } from "antd";
|
||||
import "antd/dist/reset.css";
|
||||
import React, { FC, useEffect } from "react";
|
||||
import React, { FC, Suspense, lazy, useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import AppLayout from "./layout/App";
|
||||
import HomePage, { DownloadFilter } from "./nodes/HomePage";
|
||||
import SettingPage from "./nodes/SettingPage";
|
||||
import SourceExtract from "./nodes/SourceExtract";
|
||||
import { setAppStore, increase } from "./store";
|
||||
import { setAppStore, increase, setBrowserStore, PageMode } from "./store";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
import "./App.scss";
|
||||
import useElectron from "./hooks/electron";
|
||||
import Loading from "./components/Loading";
|
||||
import { DownloadFilter } from "./types";
|
||||
import { tdApp } from "./utils";
|
||||
import { useAsyncEffect } from "ahooks";
|
||||
|
||||
const AppLayout = lazy(() => import("./layout/App"));
|
||||
const HomePage = lazy(() => import("./pages/HomePage"));
|
||||
const SourceExtract = lazy(() => import("./pages/SourceExtract"));
|
||||
const SettingPage = lazy(() => import("./pages/SettingPage"));
|
||||
const ConverterPage = lazy(() => import("./pages/Converter"));
|
||||
|
||||
function getAlgorithm(appTheme: "dark" | "light") {
|
||||
return appTheme === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm;
|
||||
@@ -20,7 +25,7 @@ function getAlgorithm(appTheme: "dark" | "light") {
|
||||
const App: FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [appTheme, setAppTheme] = React.useState<"dark" | "light">("light");
|
||||
const { addIpcListener, removeIpcListener } = useElectron();
|
||||
const { addIpcListener, removeIpcListener, getMachineId } = useElectron();
|
||||
|
||||
const themeChange = (event: MediaQueryListEvent) => {
|
||||
if (event.matches) {
|
||||
@@ -39,16 +44,27 @@ const App: FC = () => {
|
||||
dispatch(increase());
|
||||
};
|
||||
|
||||
const onChangePrivacy = () => {
|
||||
dispatch(setBrowserStore({ url: "", title: "", mode: PageMode.Default }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
addIpcListener("store-change", onAppStoreChange);
|
||||
addIpcListener("download-item-notifier", onReceiveDownloadItem);
|
||||
addIpcListener("change-privacy", onChangePrivacy);
|
||||
|
||||
return () => {
|
||||
removeIpcListener("store-change", onAppStoreChange);
|
||||
removeIpcListener("download-item-notifier", onReceiveDownloadItem);
|
||||
removeIpcListener("change-privacy", onChangePrivacy);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const deviceId = await getMachineId();
|
||||
tdApp.onEvent("页面加载", { deviceId });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isDarkTheme = matchMedia("(prefers-color-scheme: dark)");
|
||||
isDarkTheme.addEventListener("change", themeChange);
|
||||
@@ -72,17 +88,64 @@ const App: FC = () => {
|
||||
>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<AppLayout />}>
|
||||
<Route index element={<HomePage />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Suspense fallback={<Loading />}>
|
||||
<AppLayout />
|
||||
</Suspense>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<Suspense fallback={<Loading />}>
|
||||
<HomePage />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="done"
|
||||
element={<HomePage filter={DownloadFilter.done} />}
|
||||
element={
|
||||
<Suspense fallback={<Loading />}>
|
||||
<HomePage filter={DownloadFilter.done} />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="source"
|
||||
element={
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SourceExtract />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
element={
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SettingPage />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="converter"
|
||||
element={
|
||||
<Suspense fallback={<Loading />}>
|
||||
<ConverterPage />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path="source" element={<SourceExtract />} />
|
||||
<Route path="settings" element={<SettingPage />} />
|
||||
<Route path="*" element={<div>404</div>} />
|
||||
</Route>
|
||||
<Route path="/browser" element={<SourceExtract page={true} />} />
|
||||
<Route
|
||||
path="/browser"
|
||||
element={
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SourceExtract page={true} />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</ConfigProvider>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
// empty
|
||||
|
||||
@@ -1,184 +1,403 @@
|
||||
import {
|
||||
ModalForm,
|
||||
ProFormSelect,
|
||||
ProFormSwitch,
|
||||
ProFormText,
|
||||
ProFormTextArea,
|
||||
} from "@ant-design/pro-components";
|
||||
import { Form } from "antd";
|
||||
import React, { FC } from "react";
|
||||
import { Button, Form, Input, InputNumber, Select, Space, message } from "antd";
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
FC,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import "./index.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DownloadOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
|
||||
export interface DownloadFormProps {
|
||||
onFinish: (values: DownloadItem) => Promise<boolean | void>;
|
||||
trigger: JSX.Element;
|
||||
trigger?: JSX.Element;
|
||||
isEdit?: boolean;
|
||||
item?: DownloadItem;
|
||||
open?: boolean;
|
||||
destroyOnClose?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
onAddToList: (values: DownloadItem) => Promise<boolean | void>;
|
||||
onDownloadNow: (values: DownloadItem) => Promise<boolean | void>;
|
||||
}
|
||||
|
||||
const preProcessHeaders = (value: string | null) => {
|
||||
if (!value) return;
|
||||
export interface DownloadFormRef {
|
||||
setFieldsValue: (value: Partial<DownloadItem>) => void;
|
||||
getFieldsValue: () => DownloadItem;
|
||||
}
|
||||
|
||||
let headers: Record<string, any> = {};
|
||||
try {
|
||||
headers = JSON.parse(value);
|
||||
} catch (err) {
|
||||
// empty
|
||||
}
|
||||
return Object.entries(headers)
|
||||
.map(([key, value]) => {
|
||||
return `${key}: ${value}`;
|
||||
})
|
||||
.join("\n");
|
||||
};
|
||||
interface EpisodeNumberProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
canChangeType: boolean;
|
||||
}
|
||||
|
||||
const postProcessHeaders = (value: string | null) => {
|
||||
if (!value) return "";
|
||||
export interface DownloadItemForm extends DownloadItem {
|
||||
batch?: boolean;
|
||||
batchList?: string;
|
||||
}
|
||||
|
||||
const headers = value
|
||||
.split("\n")
|
||||
.reduce<Record<string, any>>((prev, curr) => {
|
||||
const [key, value] = curr.split(":");
|
||||
if (key) {
|
||||
prev[key.trim()] = value ? value.trim() : "";
|
||||
}
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
return JSON.stringify(headers);
|
||||
};
|
||||
|
||||
const DownloadFrom: FC<DownloadFormProps> = ({
|
||||
onFinish,
|
||||
trigger,
|
||||
isEdit,
|
||||
item,
|
||||
const EpisodeNumber: FC<EpisodeNumberProps> = ({
|
||||
value = "",
|
||||
onChange = () => {},
|
||||
canChangeType,
|
||||
}) => {
|
||||
const [form] = Form.useForm<DownloadItem>();
|
||||
const { t } = useTranslation();
|
||||
const [type, setType] = useState("teleplay");
|
||||
const [name, setName] = useState("");
|
||||
const [number, setNumber] = useState(1);
|
||||
const isEpisode = useMemo(
|
||||
() => type === "teleplay" && canChangeType,
|
||||
[type, canChangeType]
|
||||
);
|
||||
|
||||
// FIXME: localforage
|
||||
// await localforage.setItem<NumberOfEpisodes>("numberOfEpisodes", {
|
||||
// numberOfEpisodes,
|
||||
// teleplay,
|
||||
// });
|
||||
|
||||
const parseName = (value: string = "") => {
|
||||
const res = {
|
||||
name: "",
|
||||
number: 1,
|
||||
};
|
||||
|
||||
if (/_第(\d+)集$/.test(value)) {
|
||||
const [, name, number] = value.match(/(.*?)_第(\d+)集$/);
|
||||
res.name = name;
|
||||
res.number = Number(number);
|
||||
} else {
|
||||
res.name = value;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { name, number } = parseName(value);
|
||||
setName(name);
|
||||
setNumber(number);
|
||||
if (isEpisode) {
|
||||
onChange(`${name}_第${number}集`);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!name || !number) return;
|
||||
if (isEpisode) {
|
||||
onChange(`${name}_第${number}集`);
|
||||
} else {
|
||||
onChange(name);
|
||||
}
|
||||
}, [type]);
|
||||
|
||||
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setName(e.target.value);
|
||||
if (isEpisode) {
|
||||
onChange(`${e.target.value}_第${number}集`);
|
||||
} else {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const onNumberChange = (val: number) => {
|
||||
setNumber(val);
|
||||
if (isEpisode) {
|
||||
onChange(`${name}_第${val}集`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalForm<DownloadItem>
|
||||
key={isEdit ? "edit" : "new"}
|
||||
title={isEdit ? t("editDownload") : t("newDownload")}
|
||||
width={500}
|
||||
trigger={trigger}
|
||||
form={form}
|
||||
autoFocusFirstInput
|
||||
onOpenChange={() => {
|
||||
if (item) {
|
||||
// 处理 headers 字段
|
||||
const headers = preProcessHeaders(item.headers);
|
||||
|
||||
form.setFieldsValue({
|
||||
...item,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
}}
|
||||
modalProps={{
|
||||
destroyOnClose: true,
|
||||
styles: {
|
||||
body: {
|
||||
paddingTop: 5,
|
||||
},
|
||||
},
|
||||
}}
|
||||
submitTimeout={2000}
|
||||
onFinish={(values) => {
|
||||
// 处理 headers 字段
|
||||
const headers = postProcessHeaders(values.headers);
|
||||
|
||||
return onFinish({ ...values, headers });
|
||||
}}
|
||||
labelCol={{ span: 4 }}
|
||||
layout="horizontal"
|
||||
colon={false}
|
||||
>
|
||||
{!isEdit && <ProFormSwitch label={t("batchDownload")} name={"batch"} />}
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{(form) => {
|
||||
if (!isEdit && form.getFieldValue("batch")) {
|
||||
return (
|
||||
<ProFormTextArea
|
||||
name="batchList"
|
||||
fieldProps={{
|
||||
rows: 6,
|
||||
}}
|
||||
label={t("videoLink")}
|
||||
placeholder={t("videoLikeDescription")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("pleaseEnterVideoLink"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ProFormText
|
||||
name="url"
|
||||
label={t("videoLink")}
|
||||
placeholder={t("pleaseEnterOnlineVideoUrlOrDragM3U8Here")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("pleaseEnterOnlineVideoUrl"),
|
||||
},
|
||||
{
|
||||
pattern: /^(file|https?):\/\/.+/,
|
||||
message: t("pleaseEnterCorrectVideoLink"),
|
||||
},
|
||||
]}
|
||||
fieldProps={{
|
||||
onDrop: (e) => {
|
||||
const file: any = e.dataTransfer.files[0];
|
||||
form.setFieldValue("url", `file://${file.path}`);
|
||||
form.validateFields(["url"]);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{(form) => {
|
||||
if (isEdit || !form.getFieldValue("batch")) {
|
||||
return (
|
||||
<ProFormText
|
||||
name="name"
|
||||
label={t("videoName")}
|
||||
placeholder={t("pleaseEnterVideoName")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{(form) => {
|
||||
if (isEdit || !form.getFieldValue("batch")) {
|
||||
return (
|
||||
<ProFormText
|
||||
name="key"
|
||||
label={t("privateKey")}
|
||||
placeholder={t("keyAndIv")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
</Form.Item>
|
||||
<ProFormTextArea
|
||||
name="headers"
|
||||
label={t("additionalHeaders")}
|
||||
fieldProps={{
|
||||
rows: 4,
|
||||
}}
|
||||
placeholder={t("additionalHeadersDescription")}
|
||||
<Space.Compact block>
|
||||
{canChangeType && (
|
||||
<Select
|
||||
style={{ width: 100 }}
|
||||
defaultValue="teleplay"
|
||||
options={[
|
||||
{
|
||||
label: "剧集",
|
||||
value: "teleplay",
|
||||
},
|
||||
{
|
||||
label: "电影",
|
||||
value: "movie",
|
||||
},
|
||||
]}
|
||||
value={type}
|
||||
onChange={setType}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
placeholder={t("pleaseEnterVideoName")}
|
||||
/>
|
||||
</ModalForm>
|
||||
{canChangeType && isEpisode && (
|
||||
<InputNumber
|
||||
style={{ width: 300 }}
|
||||
addonBefore="第"
|
||||
addonAfter="集"
|
||||
changeOnWheel
|
||||
value={number}
|
||||
min={1}
|
||||
onChange={onNumberChange}
|
||||
/>
|
||||
)}
|
||||
</Space.Compact>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadFrom;
|
||||
const DownloadForm = forwardRef<DownloadFormRef, DownloadFormProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
trigger,
|
||||
isEdit,
|
||||
open,
|
||||
destroyOnClose,
|
||||
onOpenChange,
|
||||
onAddToList,
|
||||
onDownloadNow,
|
||||
item,
|
||||
} = props;
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [form] = Form.useForm<DownloadItem>();
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
useEffect(() => {
|
||||
setModalOpen(open);
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (modalOpen) {
|
||||
form.setFieldsValue(item);
|
||||
}
|
||||
}, [item, modalOpen]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
setFieldsValue: (value) => {
|
||||
form.setFieldsValue(value);
|
||||
},
|
||||
getFieldsValue: () => {
|
||||
return form.getFieldsValue();
|
||||
},
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const renderTrigger = () => {
|
||||
if (!trigger) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.cloneElement(trigger, {
|
||||
onClick: () => {
|
||||
setModalOpen(true);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderTrigger()}
|
||||
<ModalForm<DownloadItemForm>
|
||||
open={modalOpen}
|
||||
key={isEdit ? "edit" : "new"}
|
||||
title={isEdit ? t("editDownload") : t("newDownload")}
|
||||
width={500}
|
||||
form={form}
|
||||
autoFocusFirstInput
|
||||
submitter={{
|
||||
render: () => {
|
||||
return [
|
||||
<Button
|
||||
key="addToList"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue();
|
||||
const close = await onAddToList(values);
|
||||
if (close) {
|
||||
setModalOpen(false);
|
||||
}
|
||||
} catch (e: any) {
|
||||
messageApi.error(e?.message);
|
||||
}
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
{t("addToDownloadList")}
|
||||
</Button>,
|
||||
<Button
|
||||
key="downloadNow"
|
||||
type="primary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue();
|
||||
const close = await onDownloadNow(values);
|
||||
if (close) {
|
||||
setModalOpen(false);
|
||||
}
|
||||
} catch (e: any) {
|
||||
messageApi.error(e?.message);
|
||||
}
|
||||
}}
|
||||
icon={<DownloadOutlined />}
|
||||
>
|
||||
{t("downloadNow")}
|
||||
</Button>,
|
||||
];
|
||||
},
|
||||
}}
|
||||
onOpenChange={(open) => {
|
||||
if (onOpenChange) {
|
||||
onOpenChange(open);
|
||||
} else {
|
||||
setModalOpen(open);
|
||||
}
|
||||
}}
|
||||
modalProps={{
|
||||
destroyOnClose,
|
||||
styles: {
|
||||
body: {
|
||||
paddingTop: 5,
|
||||
},
|
||||
},
|
||||
}}
|
||||
submitTimeout={2000}
|
||||
labelCol={{ span: 5 }}
|
||||
layout="horizontal"
|
||||
colon={false}
|
||||
>
|
||||
{contextHolder}
|
||||
{!isEdit && (
|
||||
<ProFormSwitch label={t("batchDownload")} name={"batch"} />
|
||||
)}
|
||||
<ProFormSelect
|
||||
key="type"
|
||||
width="xl"
|
||||
disabled={isEdit}
|
||||
name="type"
|
||||
label={t("videoType")}
|
||||
valueEnum={{
|
||||
m3u8: "流媒体视频(m3u8)",
|
||||
bilibili: "哔哩哔哩视频",
|
||||
}}
|
||||
placeholder={t("pleaseSelectVideoType")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{(form) => {
|
||||
const canChangeType =
|
||||
!form.getFieldValue("batch") &&
|
||||
form.getFieldValue("type") === "m3u8";
|
||||
return (
|
||||
<Form.Item
|
||||
shouldUpdate
|
||||
name="name"
|
||||
label={t("videoName")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("pleaseEnterVideoName"),
|
||||
},
|
||||
]}
|
||||
tooltip={canChangeType && t("canUseMouseWheelToAdjust")}
|
||||
>
|
||||
<EpisodeNumber canChangeType={canChangeType} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{(form) => {
|
||||
if (!isEdit && form.getFieldValue("batch")) {
|
||||
return (
|
||||
<ProFormTextArea
|
||||
name="batchList"
|
||||
fieldProps={{
|
||||
rows: 4,
|
||||
}}
|
||||
label={t("videoLink")}
|
||||
placeholder={t("videoLikeDescription")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("pleaseEnterVideoLink"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ProFormText
|
||||
name="url"
|
||||
label={t("videoLink")}
|
||||
placeholder={t("pleaseEnterOnlineVideoUrlOrDragM3U8Here")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("pleaseEnterOnlineVideoUrl"),
|
||||
},
|
||||
{
|
||||
pattern: /^(file|https?):\/\/.+/,
|
||||
message: t("pleaseEnterCorrectVideoLink"),
|
||||
},
|
||||
]}
|
||||
fieldProps={{
|
||||
onDrop: (e) => {
|
||||
const file: any = e.dataTransfer.files[0];
|
||||
form.setFieldValue("url", `file://${file.path}`);
|
||||
form.validateFields(["url"]);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{(form) => {
|
||||
if (
|
||||
form.getFieldValue("type") === "m3u8" ||
|
||||
form.getFieldValue("batch")
|
||||
) {
|
||||
return (
|
||||
<ProFormTextArea
|
||||
name="headers"
|
||||
label={t("additionalHeaders")}
|
||||
fieldProps={{
|
||||
rows: 4,
|
||||
}}
|
||||
placeholder={t("additionalHeadersDescription")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
</Form.Item>
|
||||
</ModalForm>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default DownloadForm;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import React, { FC } from "react";
|
||||
import { useStyles } from "./styles";
|
||||
import { Spin } from "antd";
|
||||
|
||||
const Loading: FC = () => {
|
||||
const { styles } = useStyles();
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Spin />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createStyles } from "antd-style";
|
||||
|
||||
export const useStyles = createStyles(({ css, token }) => ({
|
||||
container: css`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${token.colorBgContainer};
|
||||
`,
|
||||
}));
|
||||
@@ -13,18 +13,21 @@
|
||||
border-bottom: #f0f0f0 solid 1px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.page-container-header {
|
||||
background: #141414;
|
||||
border-bottom: #313131 solid 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-container-inner {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.page-container-inner {
|
||||
background: #141414;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FC, useEffect, useRef } from "react";
|
||||
import { useStyles } from "./style";
|
||||
import "xterm/css/xterm.css";
|
||||
import { Terminal as XTerminal } from "xterm";
|
||||
import { useStyles } from "./styles";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { Terminal as XTerminal } from "@xterm/xterm";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import classNames from "classnames";
|
||||
import { Flex, Typography } from "antd";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
// empty
|
||||
|
||||
@@ -10,12 +10,15 @@ interface DivRect {
|
||||
height: number;
|
||||
}
|
||||
|
||||
const computeRect = ({ left, top, width, height }: DivRect) => ({
|
||||
x: Math.floor(left),
|
||||
y: Math.floor(top),
|
||||
width: Math.floor(width),
|
||||
height: Math.floor(height),
|
||||
});
|
||||
const computeRect = (rect: DivRect) => {
|
||||
if (!rect) return { x: 0, y: 0, width: 0, height: 0 };
|
||||
return {
|
||||
x: Math.floor(rect.left),
|
||||
y: Math.floor(rect.top),
|
||||
width: Math.floor(rect.width),
|
||||
height: Math.floor(rect.height),
|
||||
};
|
||||
};
|
||||
|
||||
interface WebViewProps {
|
||||
className?: string;
|
||||
|
||||
@@ -75,7 +75,8 @@ i18n
|
||||
pleaseSelectLanguage: "Please select language",
|
||||
downloadPrompt: "Download Prompt",
|
||||
browserSetting: "Browser Setting",
|
||||
pleaseEnterProxy: "Please enter proxy",
|
||||
pleaseEnterProxy:
|
||||
"You need to enter the full address, for example: http://127.0.0.1:8000",
|
||||
proxySetting: "Proxy Setting",
|
||||
pleaseEnterProxyFirst: "Please enter proxy first",
|
||||
blockAds: "Block Ads",
|
||||
@@ -84,11 +85,15 @@ i18n
|
||||
deleteSegments: "Delete segments after download",
|
||||
moreAction: "More Action",
|
||||
configDir: "Config Directory",
|
||||
clearCache: "Clear Cache",
|
||||
clearCacheSuccess: "Clear Cache Success",
|
||||
clearCacheFailed: "Clear Cache Failed",
|
||||
binPath: "Executable Path",
|
||||
localDir: "Local Directory",
|
||||
currentVersion: "Current Version",
|
||||
selectFolder: "Select Folder",
|
||||
proxySwitch: "Proxy Switch",
|
||||
downloadProxySwitch: "Download Proxy Switch",
|
||||
proxyDescription:
|
||||
"The proxy will take effect on the built-in browser and download",
|
||||
useImmersiveSniffing: "Use immersive sniffing",
|
||||
@@ -104,18 +109,14 @@ i18n
|
||||
videoLink: "Video Link",
|
||||
pleaseEnterVideoLink: "Please enter video link",
|
||||
videoName: "Video Name",
|
||||
pleaseEnterVideoName:
|
||||
"Please enter video name, default is the current time",
|
||||
privateKey: "Private Key",
|
||||
keyAndIv: "Key and IV (HEX format)",
|
||||
pleaseEnterVideoName: "Please enter video name",
|
||||
additionalHeaders: "Additional Headers",
|
||||
pleaseEnterOnlineVideoUrl: "Please enter online network video URL",
|
||||
pleaseEnterCorrectVideoLink: "Please enter correct video link",
|
||||
pleaseEnterOnlineVideoUrlOrDragM3U8Here:
|
||||
"Please enter online network video URL, or drag M3U8 here",
|
||||
videoLikeDescription: `Please enter the video link, make sure one line one, format: video link video name (optional), for example
|
||||
https://www.bilibili.com/video/XXX Bilibili video
|
||||
http://example.com/xxx.m3u8 m3u8 video
|
||||
videoLikeDescription: `Please enter the video link, make sure one line one, for example
|
||||
http://example.com/xxx.m3u8
|
||||
http://example.com/xxx.m3u8`,
|
||||
additionalHeadersDescription: `Please enter additional headers, make sure one line one header, for example:
|
||||
Origin: http://www.example.com
|
||||
@@ -125,9 +126,11 @@ Referer: http://www.example.com`,
|
||||
close: "Close",
|
||||
exportLog: "Export Log",
|
||||
showTerminal: "Show Console",
|
||||
autoUpgrade: "Auto Upgrade",
|
||||
autoUpgradeTooltip: "Take effect next time you start",
|
||||
consoleOutput: "Console",
|
||||
downloadNow: "Download Now",
|
||||
addToDownloadList: "Add to Download List",
|
||||
downloadNow: "Download",
|
||||
addToDownloadList: "Confirm",
|
||||
videoType: "Video Type",
|
||||
pleaseSelectVideoType: "Please select video type",
|
||||
numberOfEpisodes: "Number of episodes",
|
||||
@@ -135,6 +138,10 @@ Referer: http://www.example.com`,
|
||||
canUseMouseWheelToAdjust: "Can use mouse wheel to adjust",
|
||||
openBrowser: "Open Browser",
|
||||
privacy: "Privacy Mode",
|
||||
privacyTooltip:
|
||||
"After turning on the switch, the browser will not save any data",
|
||||
converter: "Converter",
|
||||
addFile: "Add File",
|
||||
},
|
||||
},
|
||||
zh: {
|
||||
@@ -142,7 +149,7 @@ Referer: http://www.example.com`,
|
||||
downloadList: "下载列表",
|
||||
downloadComplete: "下载完成",
|
||||
materialExtraction: "素材提取",
|
||||
setting: "设置",
|
||||
setting: "软件设置",
|
||||
refresh: "刷新",
|
||||
newDownload: "新建下载",
|
||||
selectAll: "全选",
|
||||
@@ -195,7 +202,7 @@ Referer: http://www.example.com`,
|
||||
pleaseSelectLanguage: "请选择显示语言",
|
||||
downloadPrompt: "下载完成提示",
|
||||
browserSetting: "浏览器设置",
|
||||
pleaseEnterProxy: "请填写代理地址",
|
||||
pleaseEnterProxy: "需要填写完整的地址,例如:http://127.0.0.1:8000",
|
||||
proxySetting: "代理设置",
|
||||
pleaseEnterProxyFirst: "请先输入代理地址",
|
||||
blockAds: "开启广告过滤",
|
||||
@@ -204,11 +211,15 @@ Referer: http://www.example.com`,
|
||||
deleteSegments: "下载完成删除分片",
|
||||
moreAction: "更多操作",
|
||||
configDir: "配置文件目录",
|
||||
clearCache: "清除缓存",
|
||||
clearCacheSuccess: "清除缓存成功",
|
||||
clearCacheFailed: "清除缓存失败",
|
||||
binPath: "可执行程序目录",
|
||||
localDir: "本地存储目录",
|
||||
currentVersion: "当前版本",
|
||||
selectFolder: "选择文件夹",
|
||||
proxySwitch: "代理开关",
|
||||
downloadProxySwitch: "下载代理开关",
|
||||
proxyDescription: "该代理会对软件自带浏览器以及下载时生效",
|
||||
useImmersiveSniffing: "使用沉浸式嗅探",
|
||||
immersiveSniffingDescription:
|
||||
@@ -222,17 +233,14 @@ Referer: http://www.example.com`,
|
||||
videoLink: "视频链接",
|
||||
pleaseEnterVideoLink: "请输入站点视频链接",
|
||||
videoName: "视频名称",
|
||||
pleaseEnterVideoName: "请输入视频名称,默认以当前时间作为视频名称",
|
||||
privateKey: "私有KEY",
|
||||
keyAndIv: "KEY和IV值(HEX格式)",
|
||||
pleaseEnterVideoName: "请输入视频名称",
|
||||
additionalHeaders: "附加标头",
|
||||
pleaseEnterOnlineVideoUrl: "请输入在线网络视频URL",
|
||||
pleaseEnterCorrectVideoLink: "请输入正确的视频链接",
|
||||
pleaseEnterOnlineVideoUrlOrDragM3U8Here:
|
||||
"请输入在线网络视频URL,或者将M3U8拖拽至此",
|
||||
videoLikeDescription: `请输入视频链接, 确保一行一个,格式: 视频链接 视频名称(非必填), 例如
|
||||
https://www.bilibili.com/video/XXX 哔哩哔哩视频
|
||||
http://example.com/xxx.m3u8 m3u8视频
|
||||
videoLikeDescription: `请输入视频链接, 确保一行一个,例如:
|
||||
http://example.com/xxx.m3u8
|
||||
http://example.com/xxx.m3u8`,
|
||||
additionalHeadersDescription: `请输入附加标头,确保一行一个 Header, 例如:
|
||||
Origin: http://www.example.com
|
||||
@@ -247,9 +255,11 @@ Referer: http://www.example.com`,
|
||||
close: "关闭",
|
||||
exportLog: "导出日志",
|
||||
showTerminal: "显示控制台",
|
||||
autoUpgrade: "自动更新",
|
||||
autoUpgradeTooltip: "下次启动时生效",
|
||||
consoleOutput: "控制台",
|
||||
downloadNow: "立即下载",
|
||||
addToDownloadList: "添加到下载列表",
|
||||
downloadNow: "下载",
|
||||
addToDownloadList: "确认",
|
||||
videoType: "视频类型",
|
||||
pleaseSelectVideoType: "请选择视频类型",
|
||||
numberOfEpisodes: "集数",
|
||||
@@ -257,6 +267,9 @@ Referer: http://www.example.com`,
|
||||
canUseMouseWheelToAdjust: "可以使用鼠标滚轮调整",
|
||||
openBrowser: "打开浏览器",
|
||||
privacy: "隐私模式",
|
||||
privacyTooltip: "打开开关后浏览器将不保存任何数据",
|
||||
converter: "格式转换",
|
||||
addFile: "添加文件",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Badge, Button, Layout, Menu, MenuProps, Flex } from "antd";
|
||||
import { Badge, Button, Layout, Menu, MenuProps, Flex, Typography } from "antd";
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
DownloadOutlined,
|
||||
ExportOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
ProfileOutlined,
|
||||
QuestionCircleOutlined,
|
||||
SettingOutlined,
|
||||
SyncOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import useElectron from "../../hooks/electron";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
@@ -20,8 +22,10 @@ import {
|
||||
import { useAsyncEffect } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useStyles } from "./style";
|
||||
import classNames from "classnames";
|
||||
|
||||
const { Footer, Sider, Content } = Layout;
|
||||
const { Text } = Typography;
|
||||
|
||||
type MenuItem = Required<MenuProps>["items"][number];
|
||||
|
||||
@@ -44,7 +48,6 @@ const App: FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [showExport, setShowExport] = useState(false);
|
||||
const count = useSelector(selectCount);
|
||||
const appStore = useSelector(selectAppStore);
|
||||
const { styles } = useStyles();
|
||||
@@ -79,38 +82,42 @@ const App: FC = () => {
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Link to="/source" className={styles.linkItem}>
|
||||
<Link to="/converter" className={styles.linkItem}>
|
||||
<SyncOutlined />
|
||||
<span>{t("converter")}</span>
|
||||
</Link>
|
||||
),
|
||||
key: "converter",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Link
|
||||
to="/source"
|
||||
className={classNames([styles.linkItem, styles.extract])}
|
||||
>
|
||||
<ProfileOutlined />
|
||||
<span>{t("materialExtraction")}</span>
|
||||
{showExport && (
|
||||
<Button
|
||||
title={t("openInNewWindow")}
|
||||
type="text"
|
||||
style={{ marginLeft: "auto" }}
|
||||
icon={<ExportOutlined />}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
<Button
|
||||
title={t("openInNewWindow")}
|
||||
type="text"
|
||||
className={styles.hoverButton}
|
||||
icon={<ExportOutlined />}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
dispatch(setAppStore({ openInNewWindow: true }));
|
||||
if (location.pathname === "/source") {
|
||||
navigate("/");
|
||||
}
|
||||
// FIXME: 有可能 webview 还没有完全隐藏
|
||||
await ipcSetAppStore("openInNewWindow", true);
|
||||
await showBrowserWindow();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
dispatch(setAppStore({ openInNewWindow: true }));
|
||||
if (location.pathname === "/source") {
|
||||
navigate("/");
|
||||
}
|
||||
// FIXME: 有可能 webview 还没有完全隐藏
|
||||
await ipcSetAppStore("openInNewWindow", true);
|
||||
await showBrowserWindow();
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
),
|
||||
key: "source",
|
||||
onMouseEnter: () => {
|
||||
setShowExport(true);
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
setShowExport(false);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: (
|
||||
@@ -124,7 +131,7 @@ const App: FC = () => {
|
||||
];
|
||||
|
||||
const finalItems = items.filter((item) =>
|
||||
appStore.openInNewWindow ? item?.key !== "source" : true,
|
||||
appStore.openInNewWindow ? item?.key !== "source" : true
|
||||
);
|
||||
|
||||
const openHelpUrl = () => {
|
||||
@@ -159,6 +166,14 @@ const App: FC = () => {
|
||||
<Outlet />
|
||||
</Content>
|
||||
<Footer className={styles.containerFooter}>
|
||||
<Text>
|
||||
{appStore.privacy && (
|
||||
<>
|
||||
<EyeInvisibleOutlined /> 隐私模式
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
type={"link"}
|
||||
onClick={openHelpUrl}
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
import { createStyles } from "antd-style";
|
||||
|
||||
export const useStyles = createStyles(({ css, token }) => ({
|
||||
container: css`
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
`,
|
||||
containerSider: css`
|
||||
border-right: ${token.colorBorderSecondary} solid 1px;
|
||||
`,
|
||||
containerInner: css`
|
||||
height: 100%;
|
||||
`,
|
||||
linkItem: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`,
|
||||
containerFooter: css`
|
||||
padding: 5px;
|
||||
background: ${token.colorBgContainer};
|
||||
text-align: right;
|
||||
border-top: ${token.colorBorderSecondary} solid 1px;
|
||||
`,
|
||||
}));
|
||||
export const useStyles = createStyles(({ css, token, cx }) => {
|
||||
const hoverButton = cx(css`
|
||||
margin-left: auto;
|
||||
display: none;
|
||||
`);
|
||||
return {
|
||||
container: css`
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
`,
|
||||
containerSider: css`
|
||||
border-right: ${token.colorBorderSecondary} solid 1px;
|
||||
`,
|
||||
containerInner: css`
|
||||
height: 100%;
|
||||
`,
|
||||
linkItem: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`,
|
||||
extract: css`
|
||||
&:hover {
|
||||
.${hoverButton} {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
`,
|
||||
containerFooter: css`
|
||||
padding: 5px 5px 5px 15px;
|
||||
background: ${token.colorBgContainer};
|
||||
text-align: right;
|
||||
border-top: ${token.colorBorderSecondary} solid 1px;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
`,
|
||||
hoverButton,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,18 +5,17 @@ import { Provider } from "react-redux";
|
||||
import store from "./store";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import { tdApp, initClarity } from "./utils";
|
||||
import { tdApp } from "./utils";
|
||||
import App from "./App";
|
||||
import "./i18n";
|
||||
|
||||
dayjs.locale("zh-cn");
|
||||
tdApp.init();
|
||||
initClarity();
|
||||
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { ReactNode, useState } from "react";
|
||||
import PageContainer from "../../components/PageContainer";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Space, message } from "antd";
|
||||
import useElectron from "../../hooks/electron";
|
||||
import { ProList } from "@ant-design/pro-components";
|
||||
import { usePagination } from "ahooks";
|
||||
import { getFileName } from "../../utils";
|
||||
import { Conversion } from "../../../../main/types/entity/Conversion";
|
||||
import { DeleteOutlined, SyncOutlined } from "@ant-design/icons";
|
||||
import { produce } from "immer";
|
||||
|
||||
const Converter = () => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
selectFile,
|
||||
getConversions,
|
||||
addConversion,
|
||||
convertToAudio,
|
||||
deleteConversion,
|
||||
} = useElectron();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [converting, setConverting] = useState<Record<number, boolean>>({});
|
||||
|
||||
const {
|
||||
data = { total: 0, list: [] },
|
||||
loading,
|
||||
pagination,
|
||||
refresh,
|
||||
} = usePagination(
|
||||
({ current, pageSize }) => {
|
||||
return getConversions({
|
||||
current,
|
||||
pageSize,
|
||||
});
|
||||
},
|
||||
{
|
||||
defaultPageSize: 50,
|
||||
refreshDeps: [],
|
||||
}
|
||||
);
|
||||
|
||||
const onClickConvertToAudio = async (item: Conversion) => {
|
||||
const nextState = produce((draft) => {
|
||||
draft[item.id] = true;
|
||||
});
|
||||
setConverting(nextState);
|
||||
try {
|
||||
await convertToAudio(item.id);
|
||||
messageApi.success(t("convertSuccess"));
|
||||
} catch (e: any) {
|
||||
messageApi.error(e.message);
|
||||
} finally {
|
||||
const nextState = produce((draft) => {
|
||||
draft[item.id] = false;
|
||||
});
|
||||
setConverting(nextState);
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteConversion = async (id: number) => {
|
||||
await deleteConversion(id);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const renderActionButtons = (dom: ReactNode, item: Conversion): ReactNode => {
|
||||
// 下载成功
|
||||
return [
|
||||
<Button
|
||||
type="text"
|
||||
key="restart"
|
||||
icon={<SyncOutlined spin={converting[item.id]} />}
|
||||
title={t("convertToAudio")}
|
||||
onClick={() => onClickConvertToAudio(item)}
|
||||
/>,
|
||||
<Button
|
||||
type="text"
|
||||
key="restart"
|
||||
icon={<DeleteOutlined />}
|
||||
title={t("delete")}
|
||||
onClick={() => onDeleteConversion(item.id)}
|
||||
/>,
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title={t("converter")}
|
||||
rightExtra={
|
||||
<Space>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const file = await selectFile();
|
||||
await addConversion({
|
||||
name: getFileName(file),
|
||||
path: file,
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
{t("addFile")}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{contextHolder}
|
||||
<ProList<Conversion>
|
||||
loading={loading}
|
||||
className="download-list"
|
||||
pagination={pagination}
|
||||
metas={{
|
||||
title: {
|
||||
render: (text, record) => {
|
||||
return record.name;
|
||||
},
|
||||
},
|
||||
description: {
|
||||
render: (text, record) => {
|
||||
return record.path;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
render: renderActionButtons,
|
||||
},
|
||||
}}
|
||||
rowKey="id"
|
||||
dataSource={data.list}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Converter;
|
||||
@@ -0,0 +1 @@
|
||||
// empty
|
||||
+12
-1
@@ -3,23 +3,29 @@
|
||||
padding-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.download-list {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-pro-card {
|
||||
height: 100%;
|
||||
|
||||
.ant-pro-card-body {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-list {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
.ant-list-item {
|
||||
padding-left: 10px;
|
||||
}
|
||||
@@ -27,24 +33,29 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.download-progress {
|
||||
.ant-progress {
|
||||
align-items: center;
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.progress-speed {
|
||||
line-height: 25px;
|
||||
min-width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pro-list-row-title {
|
||||
cursor: default;
|
||||
|
||||
.title-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
+91
-123
@@ -1,42 +1,29 @@
|
||||
import React, { FC, ReactNode, useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
message,
|
||||
Progress,
|
||||
Space,
|
||||
Tag,
|
||||
Dropdown,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import { Button, message, Progress, Space, Tag, Typography } from "antd";
|
||||
import "./index.scss";
|
||||
import PageContainer from "../../components/PageContainer";
|
||||
import { usePagination } from "ahooks";
|
||||
import useElectron from "../../hooks/electron";
|
||||
import { DownloadStatus, DownloadType } from "../../types";
|
||||
import { DownloadFilter, DownloadStatus } from "../../types";
|
||||
import { ProList } from "@ant-design/pro-components";
|
||||
import {
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
PauseCircleOutlined,
|
||||
SyncOutlined,
|
||||
MoreOutlined,
|
||||
CodeOutlined,
|
||||
FileAddOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectAppStore } from "../../store";
|
||||
import DownloadFrom from "../../components/DownloadForm";
|
||||
import dayjs from "dayjs";
|
||||
import DownloadForm, { DownloadItemForm } from "../../components/DownloadForm";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import Terminal from "../../components/Terminal";
|
||||
import { moment } from "../../utils";
|
||||
import { produce } from "immer";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export enum DownloadFilter {
|
||||
list = "list",
|
||||
done = "done",
|
||||
}
|
||||
|
||||
interface Props {
|
||||
filter?: DownloadFilter;
|
||||
}
|
||||
@@ -51,16 +38,23 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
stopDownload,
|
||||
onDownloadListContextMenu,
|
||||
deleteDownloadItem,
|
||||
convertToAudio,
|
||||
showBrowserWindow,
|
||||
addDownloadItem,
|
||||
addDownloadItems,
|
||||
editDownloadItem,
|
||||
downloadItemsNow,
|
||||
downloadNow,
|
||||
editDownloadNow,
|
||||
getDownloadLog,
|
||||
} = useElectron();
|
||||
const appStore = useSelector(selectAppStore);
|
||||
const { t } = useTranslation();
|
||||
const { data, loading, pagination, refresh } = usePagination(
|
||||
const {
|
||||
data = { total: 0, list: [] },
|
||||
loading,
|
||||
pagination,
|
||||
refresh,
|
||||
} = usePagination(
|
||||
({ current, pageSize }) => {
|
||||
return getDownloadItems({
|
||||
current,
|
||||
@@ -71,11 +65,10 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
{
|
||||
defaultPageSize: 50,
|
||||
refreshDeps: [filter],
|
||||
},
|
||||
}
|
||||
);
|
||||
const [converting, setConverting] = useState<Record<number, boolean>>({});
|
||||
const [progress, setProgress] = useState<Record<number, DownloadProgress>>(
|
||||
{},
|
||||
{}
|
||||
);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [terminal, setTerminal] = useState({
|
||||
@@ -84,11 +77,11 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
log: "",
|
||||
});
|
||||
|
||||
const onDownloadProgress = (e: any, progress: DownloadProgress) => {
|
||||
setProgress((curProgress) => ({
|
||||
...curProgress,
|
||||
[progress.id]: progress,
|
||||
}));
|
||||
const onDownloadProgress = (e: unknown, currProgress: DownloadProgress) => {
|
||||
const nextState = produce((draft) => {
|
||||
draft[currProgress.id] = currProgress;
|
||||
});
|
||||
setProgress(nextState);
|
||||
};
|
||||
|
||||
const onDownloadSuccess = () => {
|
||||
@@ -105,7 +98,7 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
|
||||
const onDownloadMenuEvent = async (
|
||||
e: any,
|
||||
params: { action: string; payload: number },
|
||||
params: { action: string; payload: number }
|
||||
) => {
|
||||
const { action, payload } = params;
|
||||
|
||||
@@ -160,48 +153,18 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
const onClickConvertToAudio = async (item: DownloadItem) => {
|
||||
setConverting((curConverting) => ({
|
||||
...curConverting,
|
||||
[item.id]: true,
|
||||
}));
|
||||
try {
|
||||
await convertToAudio(item.id);
|
||||
messageApi.success(t("convertSuccess"));
|
||||
} catch (e: any) {
|
||||
messageApi.error(e.message);
|
||||
} finally {
|
||||
setConverting((curConverting) => ({
|
||||
...curConverting,
|
||||
[item.id]: false,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑表单
|
||||
const renderEditForm = (item: DownloadItem) => {
|
||||
return (
|
||||
<DownloadFrom
|
||||
<DownloadForm
|
||||
key={"edit"}
|
||||
isEdit
|
||||
item={item}
|
||||
trigger={
|
||||
<Button type="text" title={t("edit")} icon={<EditOutlined />} />
|
||||
}
|
||||
onFinish={async (values) => {
|
||||
try {
|
||||
await editDownloadItem({
|
||||
id: item.id,
|
||||
name: values.name,
|
||||
url: values.url,
|
||||
headers: values.headers,
|
||||
type: DownloadType.m3u8,
|
||||
});
|
||||
refresh();
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
messageApi.error(err.message);
|
||||
}
|
||||
}}
|
||||
onAddToList={(values) => confirmAddItem(values)}
|
||||
onDownloadNow={(values) => confirmAddItem(values, true)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -235,7 +198,7 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
|
||||
const renderActionButtons = (
|
||||
dom: ReactNode,
|
||||
item: DownloadItem,
|
||||
item: DownloadItem
|
||||
): ReactNode => {
|
||||
if (item.status === DownloadStatus.Ready) {
|
||||
return [
|
||||
@@ -293,29 +256,7 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
}
|
||||
|
||||
// 下载成功
|
||||
const curConverting = converting[item.id];
|
||||
return [
|
||||
<Dropdown
|
||||
key="more"
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: t("convertToAudio"),
|
||||
key: "convert",
|
||||
icon: <SyncOutlined />,
|
||||
disabled: curConverting,
|
||||
},
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === "convert") {
|
||||
onClickConvertToAudio(item);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button type="text" title={t("more")} icon={<MoreOutlined />} />
|
||||
</Dropdown>,
|
||||
];
|
||||
return [];
|
||||
};
|
||||
|
||||
const renderTitle = (dom: ReactNode, item: DownloadItem): ReactNode => {
|
||||
@@ -346,7 +287,7 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
};
|
||||
|
||||
const renderDescription = (dom: ReactNode, item: DownloadItem): ReactNode => {
|
||||
if (progress[item.id] && filter === DownloadFilter.list) {
|
||||
if (progress[item.id] && filter === DownloadFilter.list && !item.isLive) {
|
||||
const curProgress = progress[item.id];
|
||||
const { percent, speed } = curProgress;
|
||||
|
||||
@@ -380,6 +321,61 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
setSelectedRowKeys([]);
|
||||
};
|
||||
|
||||
const confirmAddItem = async (values: any, now?: boolean) => {
|
||||
const item = {
|
||||
id: values.id,
|
||||
name: values.name || moment(),
|
||||
url: values.url,
|
||||
headers: values.headers,
|
||||
type: values.type,
|
||||
};
|
||||
|
||||
if (now) {
|
||||
await editDownloadNow(item);
|
||||
} else {
|
||||
await editDownloadItem(item);
|
||||
}
|
||||
|
||||
refresh();
|
||||
return true;
|
||||
};
|
||||
|
||||
const confirmAddItems = async (values: DownloadItemForm, now?: boolean) => {
|
||||
const { batch, batchList = "", name, headers, type, url } = values;
|
||||
if (batch) {
|
||||
const items: Omit<DownloadItem, "id">[] = batchList
|
||||
.split("\n")
|
||||
.map((url: string, i: number) => {
|
||||
return {
|
||||
url: url.trim(),
|
||||
name: `${name}_${i}`,
|
||||
headers,
|
||||
type,
|
||||
};
|
||||
});
|
||||
if (now) {
|
||||
await downloadItemsNow(items);
|
||||
} else {
|
||||
await addDownloadItems(items);
|
||||
}
|
||||
} else {
|
||||
const item: Omit<DownloadItem, "id"> = {
|
||||
name,
|
||||
url,
|
||||
headers,
|
||||
type,
|
||||
};
|
||||
if (now) {
|
||||
await downloadNow(item);
|
||||
} else {
|
||||
await addDownloadItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title={
|
||||
@@ -394,45 +390,17 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
{t("openBrowser")}
|
||||
</Button>
|
||||
)}
|
||||
{filter === DownloadFilter.list && (
|
||||
<Button onClick={() => refresh()}>{t("refresh")}</Button>
|
||||
)}
|
||||
<Button onClick={() => openDir(appStore.local)}>
|
||||
{t("openFolder")}
|
||||
</Button>
|
||||
{filter === DownloadFilter.list && (
|
||||
<DownloadFrom
|
||||
<DownloadForm
|
||||
destroyOnClose
|
||||
trigger={
|
||||
<Button icon={<FileAddOutlined />}>{t("newDownload")}</Button>
|
||||
}
|
||||
onFinish={async (values: any) => {
|
||||
if (values.batch) {
|
||||
const { batchList = "" } = values;
|
||||
const items = batchList.split("\n").map((item: any) => {
|
||||
let [url, name] = item.split(" ");
|
||||
url = url ? url.trim() : "";
|
||||
name = name
|
||||
? name.trim()
|
||||
: dayjs().format("YYYY-MM-DDTHH:mm:ssZ");
|
||||
return {
|
||||
url,
|
||||
name,
|
||||
headers: values.headers,
|
||||
};
|
||||
});
|
||||
await addDownloadItems(items);
|
||||
} else {
|
||||
await addDownloadItem({
|
||||
name: values.name || dayjs().format("YYYY-MM-DDTHH:mm:ssZ"),
|
||||
url: values.url,
|
||||
headers: values.headers,
|
||||
type: DownloadType.m3u8,
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
return true;
|
||||
}}
|
||||
onAddToList={async (values) => confirmAddItems(values)}
|
||||
onDownloadNow={async (values) => confirmAddItems(values, true)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
@@ -464,7 +432,7 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
}}
|
||||
rowKey="id"
|
||||
rowSelection={rowSelection}
|
||||
dataSource={data?.list || []}
|
||||
dataSource={data.list || []}
|
||||
tableAlertOptionRender={({ selectedRowKeys, onCleanSelected }) => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
return null;
|
||||
@@ -496,11 +464,11 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
|
||||
tableAlertRender={({ selectedRows }) => {
|
||||
return (
|
||||
<>
|
||||
{data?.list.length !== 0 && (
|
||||
{data.list.length !== 0 && (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
const list = data?.list || [];
|
||||
const list = data.list || [];
|
||||
const ids = list.map((item) => item.id || 0);
|
||||
if (ids) {
|
||||
setSelectedRowKeys(ids);
|
||||
+1
@@ -3,6 +3,7 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
&-text {
|
||||
margin-right: 5px;
|
||||
}
|
||||
+58
-12
@@ -10,7 +10,11 @@ import {
|
||||
} from "@ant-design/pro-components";
|
||||
import "./index.scss";
|
||||
import { Button, FormInstance, message, Space, Tooltip } from "antd";
|
||||
import { FolderOpenOutlined, QuestionCircleOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
ClearOutlined,
|
||||
FolderOpenOutlined,
|
||||
QuestionCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { selectAppStore, setAppStore } from "../../store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useElectron from "../../hooks/electron";
|
||||
@@ -26,6 +30,7 @@ const SettingPage: React.FC = () => {
|
||||
setAppStore: ipcSetAppStore,
|
||||
getEnvPath,
|
||||
openDir,
|
||||
clearWebviewCache,
|
||||
} = useElectron();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
@@ -115,6 +120,7 @@ const SettingPage: React.FC = () => {
|
||||
name="language"
|
||||
label={t("displayLanguage")}
|
||||
valueEnum={{
|
||||
[AppLanguage.System]: t("followSystem"),
|
||||
[AppLanguage.ZH]: t("chinese"),
|
||||
[AppLanguage.EN]: t("english"),
|
||||
}}
|
||||
@@ -124,7 +130,17 @@ const SettingPage: React.FC = () => {
|
||||
<ProFormSwitch label={t("openInNewWindow")} name="openInNewWindow" />
|
||||
<ProFormSwitch label={t("downloadPrompt")} name="promptTone" />
|
||||
<ProFormSwitch label={t("showTerminal")} name="showTerminal" />
|
||||
{/* <ProFormSwitch label={t("privacy")} name="privacy" /> */}
|
||||
<ProFormSwitch
|
||||
label={renderTooltipLabel(
|
||||
t("autoUpgrade"),
|
||||
t("autoUpgradeTooltip")
|
||||
)}
|
||||
name="autoUpgrade"
|
||||
/>
|
||||
<ProFormSwitch
|
||||
label={renderTooltipLabel(t("privacy"), t("privacyTooltip"))}
|
||||
name="privacy"
|
||||
/>
|
||||
</ProFormGroup>
|
||||
<ProFormGroup title={t("browserSetting")} direction={"vertical"}>
|
||||
<ProFormText
|
||||
@@ -135,17 +151,16 @@ const SettingPage: React.FC = () => {
|
||||
/>
|
||||
<ProFormSwitch
|
||||
name="useProxy"
|
||||
label={renderTooltipLabel(t("proxySwitch"), t("proxyDescription"))}
|
||||
label={t("proxySwitch")}
|
||||
rules={[
|
||||
({ getFieldValue, setFieldValue }) => ({
|
||||
validator() {
|
||||
if (getFieldValue("proxy") === "") {
|
||||
setFieldValue("useProxy", false);
|
||||
{
|
||||
validator(rules, value) {
|
||||
if (value && formRef.current.getFieldValue("proxy") === "") {
|
||||
return Promise.reject(t("pleaseEnterProxyFirst"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormSwitch label={t("blockAds")} name="blockAds" />
|
||||
@@ -153,17 +168,48 @@ const SettingPage: React.FC = () => {
|
||||
<ProFormSwitch
|
||||
label={renderTooltipLabel(
|
||||
t("useImmersiveSniffing"),
|
||||
t("immersiveSniffingDescription"),
|
||||
t("immersiveSniffingDescription")
|
||||
)}
|
||||
name="useExtension"
|
||||
/>
|
||||
<ProFormText label={t("moreAction")}>
|
||||
<Space>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await clearWebviewCache();
|
||||
messageApi.success(t("clearCacheSuccess"));
|
||||
} catch (err) {
|
||||
messageApi.error(t("clearCacheFailed"));
|
||||
}
|
||||
}}
|
||||
icon={<ClearOutlined />}
|
||||
>
|
||||
{t("clearCache")}
|
||||
</Button>
|
||||
</Space>
|
||||
</ProFormText>
|
||||
</ProFormGroup>
|
||||
<ProFormGroup title={t("downloadSetting")} direction={"vertical"}>
|
||||
<ProFormSwitch
|
||||
name="downloadProxySwitch"
|
||||
label={t("downloadProxySwitch")}
|
||||
rules={[
|
||||
{
|
||||
validator(rules, value) {
|
||||
if (value && formRef.current.getFieldValue("proxy") === "") {
|
||||
return Promise.reject(t("pleaseEnterProxyFirst"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormSwitch label={t("deleteSegments")} name="deleteSegments" />
|
||||
<ProFormDigit
|
||||
label={renderTooltipLabel(
|
||||
t("maxRunner"),
|
||||
t("maxRunnerDescription"),
|
||||
t("maxRunnerDescription")
|
||||
)}
|
||||
name="maxRunner"
|
||||
min={1}
|
||||
@@ -173,13 +219,13 @@ const SettingPage: React.FC = () => {
|
||||
<ProFormText label={t("moreAction")}>
|
||||
<Space>
|
||||
<Button
|
||||
onClick={() => envPath?.workspace && openDir(envPath.workspace)}
|
||||
onClick={() => openDir(envPath.workspace)}
|
||||
icon={<FolderOpenOutlined />}
|
||||
>
|
||||
{t("configDir")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => envPath?.binPath && openDir(envPath.binPath)}
|
||||
onClick={() => openDir(envPath.binPath)}
|
||||
icon={<FolderOpenOutlined />}
|
||||
>
|
||||
{t("binPath")}
|
||||
+32
-2
@@ -3,16 +3,20 @@
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.source-extract-inner {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.action-bar {
|
||||
padding: 10px 5px;
|
||||
}
|
||||
|
||||
.source-extract-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
.webview-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@@ -20,23 +24,28 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.webview-inner {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.webview-sider {
|
||||
height: 100%;
|
||||
padding: 0 10px 10px 5px;
|
||||
overflow: auto;
|
||||
|
||||
.webview-sider-inner {
|
||||
.ant-collapse-header {
|
||||
align-items: center;
|
||||
|
||||
.ant-collapse-header-text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.sider-list-container {
|
||||
.sider-list {
|
||||
.sider-item {
|
||||
@@ -44,6 +53,7 @@
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
margin-top: 5px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: -5px;
|
||||
}
|
||||
@@ -52,11 +62,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: relative;
|
||||
width: 6px;
|
||||
background-color: #ddd;
|
||||
cursor: ew-resize;
|
||||
|
||||
.handle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -73,32 +85,50 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-container {
|
||||
padding: 30px;
|
||||
flex: 1;
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.list-tem-card {
|
||||
position: relative;
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
padding: 10px;
|
||||
width: 70px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f2f6fc;
|
||||
|
||||
.favorite-remove-btn {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
&:hover {
|
||||
background: #595959;
|
||||
}
|
||||
}
|
||||
|
||||
.favorite-remove-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: -2px;
|
||||
color: gray;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
+124
-252
@@ -24,19 +24,13 @@ import {
|
||||
Space,
|
||||
Spin,
|
||||
} from "antd";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import PageContainer from "../../components/PageContainer";
|
||||
import useElectron from "../../hooks/electron";
|
||||
import { generateUrl, getFavIcon } from "../../utils";
|
||||
import "./index.scss";
|
||||
import {
|
||||
ModalForm,
|
||||
ProFormDigit,
|
||||
ProFormSelect,
|
||||
ProFormSwitch,
|
||||
ProFormText,
|
||||
} from "@ant-design/pro-components";
|
||||
import { ModalForm, ProFormText } from "@ant-design/pro-components";
|
||||
import {
|
||||
BrowserStatus,
|
||||
PageMode,
|
||||
@@ -47,29 +41,12 @@ import {
|
||||
import WebView from "../../components/WebView";
|
||||
import { selectAppStore } from "../../store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { nanoid } from "nanoid";
|
||||
import localforage from "localforage";
|
||||
import { DownloadType } from "../../types";
|
||||
import DownloadForm, { DownloadFormRef } from "../../components/DownloadForm";
|
||||
|
||||
interface SourceExtractProps {
|
||||
page?: boolean;
|
||||
}
|
||||
|
||||
// 下载表单
|
||||
interface DownloadForm {
|
||||
teleplay: boolean;
|
||||
type: DownloadType;
|
||||
name: string;
|
||||
url: string;
|
||||
numberOfEpisodes: number;
|
||||
}
|
||||
|
||||
// 集数
|
||||
interface NumberOfEpisodes {
|
||||
teleplay: boolean;
|
||||
numberOfEpisodes: number;
|
||||
}
|
||||
|
||||
const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
const {
|
||||
getFavorites,
|
||||
@@ -94,56 +71,36 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
const { data: favoriteList = [], refresh } = useRequest(getFavorites);
|
||||
const [favoriteAddForm] = Form.useForm<Favorite>();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [hoverId, setHoverId] = useState<number>(-1);
|
||||
const store = useSelector(selectBrowserStore);
|
||||
const appStore = useSelector(selectAppStore);
|
||||
const [form] = Form.useForm<DownloadForm>();
|
||||
const [modalShow, setModalShow] = useState(false);
|
||||
const [modalReadyShow, setModalReadyShow] = useState(false);
|
||||
const sessionId = useRef("");
|
||||
const [placeHolder, setPlaceHolder] = useState<string>("");
|
||||
const downloadForm = useRef<DownloadFormRef>(null);
|
||||
const originTitle = useRef(document.title);
|
||||
|
||||
const curIsFavorite = favoriteList.find((item) => item.url === store.url);
|
||||
const curIsFavorite = useMemo(() => {
|
||||
return favoriteList.find((item) => item.url === store.url);
|
||||
}, [favoriteList, store.url]);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const store = await ipcGetAppStore();
|
||||
dispatch(setAppStore(store));
|
||||
}, []);
|
||||
|
||||
const loadUrl = async (url: string) => {
|
||||
const id = nanoid();
|
||||
sessionId.current = id;
|
||||
try {
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
url: "",
|
||||
mode: PageMode.Browser,
|
||||
status: BrowserStatus.Loading,
|
||||
}),
|
||||
);
|
||||
await webviewLoadURL(url);
|
||||
if (sessionId.current === id) {
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
url: url,
|
||||
status: BrowserStatus.Loaded,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (sessionId.current === id) {
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
status: BrowserStatus.Failed,
|
||||
errMsg: (err as any).message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
const loadUrl = (url: string) => {
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
url,
|
||||
mode: PageMode.Browser,
|
||||
status: BrowserStatus.Loading,
|
||||
})
|
||||
);
|
||||
webviewLoadURL(url);
|
||||
};
|
||||
|
||||
const goto = async () => {
|
||||
const goto = () => {
|
||||
const link = generateUrl(store.url);
|
||||
await loadUrl(link);
|
||||
loadUrl(link);
|
||||
};
|
||||
|
||||
const onInputKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@@ -177,14 +134,9 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
|
||||
const onClickGoBack = async () => {
|
||||
const back = await webviewGoBack();
|
||||
|
||||
if (!back) {
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
url: "",
|
||||
mode: PageMode.Default,
|
||||
}),
|
||||
);
|
||||
document.title = originTitle.current;
|
||||
dispatch(setBrowserStore({ url: "", title: "", mode: PageMode.Default }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -193,8 +145,9 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
url: "",
|
||||
title: "",
|
||||
mode: PageMode.Default,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -206,20 +159,8 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
await goto();
|
||||
};
|
||||
|
||||
const onClickLoadItem = async (item: Favorite) => {
|
||||
await loadUrl(item.url);
|
||||
};
|
||||
|
||||
const onDomReady = (e: unknown, data: UrlDetail) => {
|
||||
if (data.url) {
|
||||
document.title = data.title;
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
url: data.url,
|
||||
title: data.title,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const onClickLoadItem = (item: Favorite) => {
|
||||
loadUrl(item.url);
|
||||
};
|
||||
|
||||
const onFavoriteEvent = async (
|
||||
@@ -230,7 +171,7 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
}: {
|
||||
action: string;
|
||||
payload: number;
|
||||
},
|
||||
}
|
||||
) => {
|
||||
if (action === "open") {
|
||||
const item = favoriteList.find((item) => item.id === payload);
|
||||
@@ -248,39 +189,81 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
dispatch(setBrowserStore(state));
|
||||
}, []);
|
||||
|
||||
const onShowDownloadDialog = async (e: any, data: DownloadForm) => {
|
||||
const { type, url, name } = data;
|
||||
const noe = await localforage.getItem<NumberOfEpisodes>("numberOfEpisodes");
|
||||
// 设置当前的下载表单
|
||||
const setCurrentDownloadForm = async (data: DownloadItem) => {
|
||||
const { type, url, name, headers } = data;
|
||||
|
||||
let teleplay = false;
|
||||
let numberOfEpisodes = 1;
|
||||
if (data.type !== "bilibili") {
|
||||
teleplay = noe?.teleplay || false;
|
||||
numberOfEpisodes = noe?.numberOfEpisodes || 1;
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
downloadForm.current.setFieldsValue({
|
||||
type,
|
||||
url,
|
||||
name,
|
||||
teleplay,
|
||||
numberOfEpisodes,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
|
||||
const onShowDownloadDialog = async (
|
||||
e: unknown,
|
||||
data: DownloadItem[],
|
||||
image: string
|
||||
) => {
|
||||
// FIXME: 选择
|
||||
setCurrentDownloadForm(data[data.length - 1]);
|
||||
|
||||
setPlaceHolder(image);
|
||||
setModalShow(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const prevTitle = document.title;
|
||||
addIpcListener("webview-dom-ready", onDomReady);
|
||||
addIpcListener("webview-fail-load", onFailLoad);
|
||||
addIpcListener("webview-did-navigate", onDidNavigate);
|
||||
addIpcListener("webview-did-navigate-in-page", onDidNavigateInPage);
|
||||
addIpcListener("favorite-item-event", onFavoriteEvent);
|
||||
addIpcListener("show-download-dialog", onShowDownloadDialog);
|
||||
|
||||
return () => {
|
||||
document.title = prevTitle;
|
||||
removeIpcListener("webview-dom-ready", onDomReady);
|
||||
removeIpcListener("webview-fail-load", onFailLoad);
|
||||
removeIpcListener("webview-did-navigate", onDidNavigate);
|
||||
removeIpcListener("webview-did-navigate-in-page", onDidNavigateInPage);
|
||||
removeIpcListener("favorite-item-event", onFavoriteEvent);
|
||||
removeIpcListener("show-download-dialog", onShowDownloadDialog);
|
||||
};
|
||||
}, [store.status]);
|
||||
|
||||
const setPageInfo = ({ url, title }: UrlDetail) => {
|
||||
document.title = title;
|
||||
dispatch(setBrowserStore({ url, title }));
|
||||
};
|
||||
|
||||
const onDomReady = (e: unknown, info: UrlDetail) => {
|
||||
setPageInfo(info);
|
||||
};
|
||||
|
||||
const onFailLoad = (e: unknown, data: { code: number; desc: string }) => {
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
status: BrowserStatus.Failed,
|
||||
errCode: data.code,
|
||||
errMsg: data.desc,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onDidNavigate = (e: unknown, info: UrlDetail) => {
|
||||
setPageInfo(info);
|
||||
dispatch(setBrowserStore({ status: BrowserStatus.Loaded }));
|
||||
};
|
||||
|
||||
const onDidNavigateInPage = (e: unknown, info: UrlDetail) => {
|
||||
setPageInfo(info);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.title = store.title || document.title;
|
||||
return () => {
|
||||
document.title = originTitle.current;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 合并到主页
|
||||
@@ -295,7 +278,7 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
dispatch(
|
||||
setAppStore({
|
||||
isMobile: nextMode,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -391,11 +374,22 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
let content = <div></div>;
|
||||
if (store.status === BrowserStatus.Loading) {
|
||||
content = <Spin />;
|
||||
} else if (modalReadyShow || modalShow) {
|
||||
content = <></>;
|
||||
} else if (modalShow) {
|
||||
content = (
|
||||
<img
|
||||
src={placeHolder}
|
||||
alt=""
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (store.status === BrowserStatus.Failed) {
|
||||
content = (
|
||||
<Empty description={store.errMsg || t("loadFailed")}>
|
||||
<Empty
|
||||
description={`${store.errMsg || t("loadFailed")} (${store.errCode})`}
|
||||
>
|
||||
<Space>
|
||||
<Button type="primary" onClick={onClickGoHome}>
|
||||
{t("backToHome")}
|
||||
@@ -489,35 +483,19 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
onFavoriteItemContextMenu(item.id);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="list-tem-card"
|
||||
onClick={() => onClickLoadItem(item)}
|
||||
onMouseLeave={() => {
|
||||
setHoverId(-1);
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
setHoverId(item.id);
|
||||
}}
|
||||
>
|
||||
{hoverId === item.id && (
|
||||
<Button
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: -1,
|
||||
top: 2,
|
||||
color: "gray",
|
||||
}}
|
||||
type="link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
removeFavorite(item.id);
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
<CloseOutlined />
|
||||
</Button>
|
||||
)}
|
||||
<div className="list-tem-card" onClick={() => onClickLoadItem(item)}>
|
||||
<Button
|
||||
className="favorite-remove-btn"
|
||||
type="link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
removeFavorite(item.id);
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
<CloseOutlined />
|
||||
</Button>
|
||||
{item.icon ? (
|
||||
<Avatar shape="square" src={item.icon} icon={<LinkOutlined />} />
|
||||
) : (
|
||||
@@ -545,141 +523,35 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
|
||||
const confirmDownload = async (now?: boolean) => {
|
||||
try {
|
||||
const data = form.getFieldsValue();
|
||||
const { numberOfEpisodes, teleplay, ...item } = data;
|
||||
|
||||
if (teleplay) {
|
||||
await localforage.setItem<NumberOfEpisodes>("numberOfEpisodes", {
|
||||
numberOfEpisodes,
|
||||
teleplay,
|
||||
});
|
||||
}
|
||||
|
||||
if (teleplay) {
|
||||
item.name = `${item.name}——第${numberOfEpisodes}集`;
|
||||
}
|
||||
const data = downloadForm.current.getFieldsValue();
|
||||
|
||||
if (now) {
|
||||
await downloadNow(item);
|
||||
await downloadNow(data);
|
||||
} else {
|
||||
await addDownloadItem(item);
|
||||
await addDownloadItem(data);
|
||||
}
|
||||
|
||||
// 提交成功后关闭弹窗
|
||||
setModalShow(false);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
message.error((e as any).message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderModalForm = () => {
|
||||
return (
|
||||
<ModalForm<DownloadForm>
|
||||
<DownloadForm
|
||||
isEdit
|
||||
ref={downloadForm}
|
||||
open={modalShow}
|
||||
title={t("newDownload")}
|
||||
onOpenChange={setModalShow}
|
||||
form={form}
|
||||
modalProps={{
|
||||
destroyOnClose: true,
|
||||
afterOpenChange: setModalReadyShow,
|
||||
okText: t("downloadNow"),
|
||||
styles: {
|
||||
body: {
|
||||
paddingTop: 5,
|
||||
},
|
||||
},
|
||||
}}
|
||||
submitter={{
|
||||
render: (props, [, okButton]) => {
|
||||
return [
|
||||
<Button
|
||||
key="addToList"
|
||||
onClick={async () => {
|
||||
await confirmDownload();
|
||||
}}
|
||||
>
|
||||
{t("addToDownloadList")}
|
||||
</Button>,
|
||||
okButton,
|
||||
];
|
||||
},
|
||||
}}
|
||||
submitTimeout={2000}
|
||||
onFinish={async () => {
|
||||
await confirmDownload(true);
|
||||
return true;
|
||||
}}
|
||||
labelCol={{ style: { width: "120px" } }}
|
||||
layout="horizontal"
|
||||
width={550}
|
||||
labelAlign={"left"}
|
||||
colon={false}
|
||||
>
|
||||
<ProFormSelect
|
||||
width="xl"
|
||||
name="type"
|
||||
label={t("videoType")}
|
||||
disabled
|
||||
placeholder={t("pleaseSelectVideoType")}
|
||||
/>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{(form) => {
|
||||
if (form.getFieldValue("type") !== "bilibili") {
|
||||
return (
|
||||
<ProFormSwitch
|
||||
label={t("showNumberOfEpisodes")}
|
||||
name="teleplay"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{(form) => {
|
||||
if (
|
||||
form.getFieldValue("type") !== "bilibili" &&
|
||||
form.getFieldValue("teleplay")
|
||||
) {
|
||||
return (
|
||||
<ProFormDigit
|
||||
label={t("numberOfEpisodes")}
|
||||
tooltip={t("canUseMouseWheelToAdjust")}
|
||||
name="numberOfEpisodes"
|
||||
width="xl"
|
||||
min={1}
|
||||
max={10000}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<ProFormText
|
||||
width="xl"
|
||||
name="name"
|
||||
label={t("videoName")}
|
||||
placeholder={t("pleaseEnterVideoName")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("pleaseEnterVideoName"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormText
|
||||
width="xl"
|
||||
name="url"
|
||||
label={t("videoLink")}
|
||||
placeholder={t("pleaseEnterVideoLink")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("pleaseEnterVideoLink"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ModalForm>
|
||||
onDownloadNow={() => confirmDownload(true)}
|
||||
onAddToList={() => confirmDownload()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Vendored
+9
@@ -72,6 +72,14 @@ declare interface AppStore {
|
||||
language?: AppLanguage;
|
||||
// 是否显示终端
|
||||
showTerminal?: boolean;
|
||||
// 隐私模式
|
||||
privacy?: boolean;
|
||||
// 机器id
|
||||
machineId?: string;
|
||||
// 下载代理设置
|
||||
downloadProxySwitch?: boolean;
|
||||
// 自动更新
|
||||
autoUpgrade?: boolean;
|
||||
}
|
||||
|
||||
declare interface BrowserStore {
|
||||
@@ -79,6 +87,7 @@ declare interface BrowserStore {
|
||||
url: string;
|
||||
title: string;
|
||||
status: BrowserStatus;
|
||||
errCode?: number;
|
||||
errMsg?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { RootState } from ".";
|
||||
import { AppTheme } from "../types";
|
||||
import { AppLanguage, AppTheme } from "../types";
|
||||
import i18n from "../i18n";
|
||||
|
||||
const initialState: AppStore = {
|
||||
@@ -14,8 +14,12 @@ const initialState: AppStore = {
|
||||
useExtension: false,
|
||||
isMobile: false,
|
||||
maxRunner: 2,
|
||||
language: "system",
|
||||
language: AppLanguage.System,
|
||||
showTerminal: false,
|
||||
privacy: false,
|
||||
machineId: "",
|
||||
downloadProxySwitch: false,
|
||||
autoUpgrade: true,
|
||||
};
|
||||
|
||||
export const appSlice = createSlice({
|
||||
|
||||
@@ -23,3 +23,8 @@ export enum DownloadType {
|
||||
m3u8 = "m3u8",
|
||||
bilibili = "bilibili",
|
||||
}
|
||||
|
||||
export enum DownloadFilter {
|
||||
list = "list",
|
||||
done = "done",
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// prettier-ignore
|
||||
export function initClarity() {
|
||||
try {
|
||||
(function(c,l,a,r,i,t,y){
|
||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, "clarity", "script", import.meta.env.APP_CLARITY_APPID);
|
||||
} catch (e) {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import dayjs from "dayjs";
|
||||
import { isUrl } from "./url";
|
||||
|
||||
export { http } from "./http";
|
||||
export { tdApp } from "./tdapp";
|
||||
export { initClarity } from "./clarity";
|
||||
|
||||
export const requestImage = (url: string, timeout = 1000): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -41,7 +41,17 @@ export const generateUrl = (url: string) => {
|
||||
result = `http://${url}`;
|
||||
}
|
||||
if (!isUrl(result)) {
|
||||
result = `https://baidu.com/s?word=${url}`;
|
||||
result = `https://www.baidu.com/s?word=${url}`;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export function moment() {
|
||||
return dayjs().format("YYYY-MM-DDTHH:mm:ssZ");
|
||||
}
|
||||
|
||||
export function getFileName(url: string) {
|
||||
const urlObject = new URL(url);
|
||||
const name = urlObject.pathname.split("/").pop() || "";
|
||||
return decodeURIComponent(name);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class TDEvent {
|
||||
headElement.appendChild(script);
|
||||
}
|
||||
|
||||
private onEvent(eventId: string, mapKv: Record<string, string> = {}) {
|
||||
onEvent(eventId: string, mapKv: Record<string, string> = {}) {
|
||||
try {
|
||||
window.TDAPP?.onEvent(eventId, "", mapKv);
|
||||
} catch (e) {
|
||||
|
||||
Vendored
+1
-2
@@ -8,10 +8,9 @@ declare global {
|
||||
onEvent: (
|
||||
eventId: string,
|
||||
label: "",
|
||||
mapKv: Record<string, string>,
|
||||
mapKv: Record<string, string>
|
||||
) => void;
|
||||
};
|
||||
clarity?: any;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,22 +14,5 @@ export default defineConfig({
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "../main/app/build/renderer"),
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
react: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react-router-dom",
|
||||
"@reduxjs/toolkit",
|
||||
"react-redux",
|
||||
"sort-by",
|
||||
],
|
||||
antd: ["antd"],
|
||||
icons: ["@ant-design/icons"],
|
||||
"pro-components": ["@ant-design/pro-components"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user