Compare commits

...

45 Commits

Author SHA1 Message Date
士子☀️ 5461e74539 Merge pull request #216 from caorushizi/release-v2.2.3
feat:   v2.2.3
2024-07-06 17:42:02 +08:00
caorushizi 417484cb6d feat: v2.2.3 2024-07-06 17:38:30 +08:00
士子☀️ 9aaffd3550 Merge pull request #214 from caorushizi/dev-docs
docs: 📝  更新文档
2024-07-03 15:40:36 +08:00
caorushizi 94759d48c7 docs: 📝 更新文档 2024-07-03 15:37:16 +08:00
士子☀️ c0e343b696 Merge pull request #204 from caorushizi/fix-search
fix: 🐛  修复新版本在更换安装目录后下载失败的问题
2024-06-30 19:01:26 +08:00
caorushizi f18ac7c444 fix: 🐛 修复新版本在更换安装目录后下载失败的问题
下载中的视频无法停止 #202
2024-06-30 18:58:58 +08:00
士子☀️ f637610734 Merge pull request #196 from caorushizi/fix-search
fix: 🐛  修复搜索时跳转的链接
2024-06-24 23:43:47 +08:00
caorushizi 5e1046dd5b fix: 🐛 修复搜索时跳转的链接 2024-06-23 11:41:33 +08:00
士子☀️ f25584cb2f Merge pull request #195 from caorushizi/fix-videoname
fix: 🐛  修复获取视频名称的问题
2024-06-23 00:13:13 +08:00
caorushizi 03999585d0 fix: 🐛 修复获取视频名称的问题 2024-06-23 00:12:06 +08:00
士子☀️ 772c2b0300 Merge pull request #194 from caorushizi/dev-webview
Dev webview
2024-06-22 22:59:28 +08:00
caorushizi 3622e67a56 fix: 🐛 修复浏览器背景颜色 2024-06-22 22:58:25 +08:00
caorushizi c51630cdd6 fix: 🐛 修复切换隐私模式时 Proxy 和嗅探问题 2024-06-22 22:14:49 +08:00
士子☀️ dab1c55a93 Merge pull request #193 from caorushizi/dev-webview
feat(settings):   新增无痕浏览模式
2024-06-22 21:48:11 +08:00
caorushizi 7b966bc988 feat(settings): 新增无痕浏览模式
设置页面新增无痕浏览模式

新增无痕浏览模式 #52
2024-06-22 21:46:27 +08:00
士子☀️ e5e46bee38 Merge pull request #192 from caorushizi/dev-webview
feat(settings):   添加自动更新选项
2024-06-22 15:55:09 +08:00
caorushizi 3365d96a03 feat(settings): 添加自动更新选项
设置页面添加自动更新选项,下次启动时生效

auto upgrade #185
2024-06-22 15:53:42 +08:00
士子☀️ fb223cd493 Merge pull request #191 from caorushizi/dev-webview
feat:   添加下载代理开关 & 添加清空缓存
2024-06-22 15:36:18 +08:00
caorushizi 60d49c4773 feat: 添加下载代理开关 & 添加清空缓存 2024-06-22 15:35:08 +08:00
士子☀️ e7aa4ae147 Merge pull request #190 from caorushizi/dev-webview
build: 🛠  portable
2024-06-22 12:42:50 +08:00
caorushizi c45be41ddc build: 🛠 portable 2024-06-22 12:41:45 +08:00
士子☀️ 85b37facee Merge pull request #189 from caorushizi/dev-webview
Dev webview
2024-06-22 12:27:58 +08:00
caorushizi 90567009ef Merge branch 'dev-webview' of https://github.com/caorushizi/m3u8-downloader into dev-webview 2024-06-22 12:26:36 +08:00
caorushizi 899afc156e build: 🛠 upload artifact 2024-06-22 12:25:51 +08:00
caorushizi 9e33b7e488 build: 🛠 upload artifact 2024-06-22 12:24:24 +08:00
士子☀️ e7ba60a51f Merge pull request #188 from caorushizi/dev-webview
build: 🛠  portable
2024-06-22 12:11:55 +08:00
caorushizi 2a3ad325c2 build: 🛠 portable 2024-06-22 12:11:19 +08:00
caorushizi 7575b80d31 build: 🛠 portable 2024-06-22 12:08:33 +08:00
士子☀️ 8bb9d119b5 Merge pull request #187 from caorushizi/dev-webview
Dev webview
2024-06-22 11:54:18 +08:00
caorushizi 5da3f2b1c7 build: 🛠 优化构建 2024-06-22 11:50:40 +08:00
caorushizi da6cbcfb75 feat: 修复 webview 中加载的问题 2024-06-22 10:44:34 +08:00
caorushizi 3d7aff41ee feat: 优化内置浏览器逻辑 2024-06-22 03:42:15 +08:00
士子☀️ 2cc49e8679 Merge pull request #186 from caorushizi/dev-ap
feat:   ap
2024-06-21 23:44:42 +08:00
caorushizi 0e961c0647 feat: use td app 2024-06-21 23:39:45 +08:00
caorushizi 4d5757a6ce feat: ap 2024-06-21 00:04:21 +08:00
士子☀️ 62ae0e5075 Merge pull request #184 from caorushizi/feat/v2.2.2
feat:   v2.2.2
2024-06-18 09:08:22 +08:00
caorushizi 7513fe2882 feat: v2.2.2 2024-06-18 09:06:42 +08:00
士子☀️ c945dc8360 Merge pull request #183 from caorushizi/feat/v2.2.1
feat:   release v2.2.1
2024-06-17 23:59:20 +08:00
caorushizi de7e0da045 feat: release v2.2.1 2024-06-17 23:57:36 +08:00
士子☀️ a706429395 Merge pull request #182 from caorushizi/feat/v2.2.1
feat:   v2.2.1
2024-06-17 23:34:01 +08:00
caorushizi f26c9761c3 feat: v2.2.1 2024-06-17 23:32:58 +08:00
士子☀️ 9424dd4c0e Merge pull request #181 from caorushizi/dev/form
Dev/form
2024-06-17 23:30:55 +08:00
caorushizi 271294a8e7 feat: conversion page 2024-06-17 23:29:27 +08:00
caorushizi 6b2786292f feat: conversion page 2024-06-10 00:17:32 +08:00
caorushizi c36247b5e1 feat: update form 2024-06-09 23:09:55 +08:00
55 changed files with 1053 additions and 482 deletions
+37 -32
View File
@@ -1,39 +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
consola
+1 -1
View File
@@ -1,4 +1,4 @@
APP_NAME=mediago
APP_ID=mediago.ziying.site
APP_COPYRIGHT=caorushizi
APP_VERSION=2.2.1-beta.0
APP_VERSION=2.2.3
-1
View File
@@ -1,5 +1,4 @@
APP_TD_APPID=
APP_CLARITY_APPID=
GH_TOKEN=
LOAD_DEVTOOLS=
-1
View File
@@ -1,5 +1,4 @@
APP_TD_APPID=
APP_CLARITY_APPID=
GH_TOKEN=
DEBUG_PLUGINS=false
+5 -1
View File
@@ -8,7 +8,11 @@ outline: deep
这篇文章是软件的简单说明,可以快速上手使用本本软件。
::: tip
我建了一个 QQ 群,方便大家一起沟通交流: 574209001
方便大家一起沟通交流, 可以加入 QQ 反馈群:
MediaGo 反馈群 1 574209001
MediaGo 反馈群 2 921619626
:::
## 下载安装
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mediago",
"version": "2.2.1-beta.0",
"version": "2.2.3",
"description": "在线视频下载器",
"main": "main/index.js",
"author": "caorushizi",
+5
View File
@@ -8,4 +8,9 @@ export default [
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
"@typescript-eslint/no-explicit-any": "warn",
},
},
];
+4 -2
View File
@@ -50,8 +50,8 @@
"typescript-eslint": "^7.10.0"
},
"dependencies": {
"@cliqz/adblocker-electron": "^1.27.3",
"@cliqz/adblocker-electron-preload": "^1.27.3",
"@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",
@@ -66,7 +66,9 @@
"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.2",
"strip-ansi": "^7.1.0",
+9 -13
View File
@@ -39,7 +39,7 @@ function getConfig(): esbuild.BuildOptions {
function buildOptions(
entry: string,
platform: esbuild.Platform,
target: string,
target: string
): esbuild.BuildOptions {
return {
...getConfig(),
@@ -111,10 +111,12 @@ export function getReleaseConfig(): Configuration {
},
mac: {
icon: "../assets/icon.icns",
target: {
target: "dmg",
arch: ["x64"],
},
target: [
{
target: "dmg",
arch: ["x64", "arm64"],
},
],
},
linux: {
category: "Utility",
@@ -126,17 +128,11 @@ export function getReleaseConfig(): Configuration {
},
},
nsis: {
oneClick: true,
oneClick: false,
allowElevation: true,
allowToChangeInstallationDirectory: false,
installerIcon: "",
uninstallerIcon: "",
installerHeaderIcon: "",
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true,
createStartMenuShortcut: true,
shortcutName: "",
include: "",
script: "",
},
};
}
+1 -1
View File
@@ -84,7 +84,7 @@ async function pack() {
provider: "github",
repo: "mediago",
owner: "caorushizi",
releaseType: "prerelease",
releaseType: "draft",
};
}
await builder.build({ config });
+2 -3
View File
@@ -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);
}
}
+46 -4
View File
@@ -23,10 +23,13 @@ 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")
@@ -143,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);
}
@@ -206,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) {
@@ -252,4 +262,36 @@ export default class HomeController implements Controller {
const video = await this.videoRepository.findVideo(id);
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;
}
}
}
@@ -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();
}
}
+34
View File
@@ -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
View File
@@ -36,6 +36,7 @@ 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
+12
View File
@@ -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",
+6
View File
@@ -19,6 +19,8 @@ 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);
+6
View File
@@ -52,6 +52,12 @@ declare interface AppStore {
showTerminal: boolean;
// 隐私模式
privacy: boolean;
// 机器id
machineId: string;
// 下载代理设置
downloadProxySwitch: boolean;
// 自动更新
autoUpgrade: boolean;
}
declare interface BrowserStore {
+26 -2
View File
@@ -3,11 +3,14 @@ import { shell } from "electron/common";
import { AppStore, BrowserStore, EnvPath } from "./main.ts";
import { type Favorite } from "./entity/Favorite.ts";
import {
ConversionPagination,
ConversionResponse,
VideoResponse,
type DownloadItem,
type DownloadItemPagination,
} from "./interfaces.ts";
import { Video } from "./entity/Video.ts";
import { Conversion } from "./entity/Conversion.ts";
const apiFunctions: Record<string, any> = {};
@@ -57,13 +60,14 @@ const electronApi = {
): 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> {
@@ -134,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");
},
@@ -152,6 +159,23 @@ const electronApi = {
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 });
}
}
+15 -8
View File
@@ -74,6 +74,9 @@ const processList: Schema[] = [
proxy: {
argsName: ["--custom-proxy"],
},
__common__: {
argsName: ["--no-log"],
},
},
consoleReg: {
percent: "([\\d.]+)%",
@@ -121,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();
@@ -148,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);
@@ -174,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;
}
@@ -185,7 +188,7 @@ 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) {
@@ -194,7 +197,7 @@ export default class DownloadService extends EventEmitter {
// 下载暂停
await this.videoRepository.changeVideoStatus(
task.id,
DownloadStatus.Stopped,
DownloadStatus.Stopped
);
this.emit("download-stop", task.id);
} else {
@@ -202,7 +205,7 @@ export default class DownloadService extends EventEmitter {
// 下载失败
await this.videoRepository.changeVideoStatus(
task.id,
DownloadStatus.Failed,
DownloadStatus.Failed
);
this.emit("download-failed", task.id, err);
}
@@ -239,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;
@@ -326,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;
@@ -4,7 +4,11 @@ import { TYPES } from "../types.ts";
import ElectronLogger from "../vendor/ElectronLogger.ts";
import EventEmitter from "events";
import { OnSendHeadersListenerDetails, session } from "electron";
import { PERSIST_WEBVIEW, formatHeaders } from "../helper/index.ts";
import {
PERSIST_WEBVIEW,
PRIVACY_WEBVIEW,
formatHeaders,
} from "../helper/index.ts";
export interface SourceParams {
url: string;
@@ -61,6 +65,10 @@ export class SniffingHelper extends EventEmitter {
});
}
update(pageInfo: PageInfo) {
this.pageInfo = pageInfo;
}
reset(pageInfo: PageInfo) {
this.pageInfo = pageInfo;
this.ready = false;
@@ -85,8 +93,9 @@ export class SniffingHelper extends EventEmitter {
}
}
start() {
const viewSession = session.fromPartition(PERSIST_WEBVIEW);
start(privacy: boolean = false) {
const partition = privacy ? PRIVACY_WEBVIEW : PERSIST_WEBVIEW;
const viewSession = session.fromPartition(partition);
viewSession.webRequest.onSendHeaders(this.onSendHeaders);
}
+179 -93
View File
@@ -1,9 +1,10 @@
import { WebContentsView, session } from "electron";
import { Event, HandlerDetails, WebContentsView, session } from "electron";
import { inject, injectable } from "inversify";
import { TYPES } from "../types.ts";
import isDev from "electron-is-dev";
import {
PERSIST_WEBVIEW,
PRIVACY_WEBVIEW,
fetch,
mobileUA,
pcUA,
@@ -15,14 +16,15 @@ 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 } from "./SniffingHelperService.ts";
import { SniffingHelper, SourceParams } from "./SniffingHelperService.ts";
import { resolve } from "path";
import { readFileSync } from "fs-extra";
@injectable()
export default class WebviewService {
public view: WebContentsView;
private view: WebContentsView | null = null;
private blocker?: ElectronBlocker;
private defaultSession: string;
constructor(
@inject(TYPES.MainWindow)
@@ -36,129 +38,182 @@ 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 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.window.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 }) => {
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);
return { action: "deny" };
});
this.sniffingHelper.start();
this.sniffingHelper.on("source", async (item) => {
// 这里需要判断是否使用浏览器插件
const useExtension = this.store.get("useExtension");
if (useExtension) {
this.webContents.send("webview-link-message", item);
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 {
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);
const content = readFileSync(pluginPath, "utf-8");
await this.view.webContents.executeJavaScript(content);
}
});
} catch (err) {
// empty
}
};
onDidFailLoad = (e: Event, code: number, desc: string) => {
this.window.webContents.send("webview-fail-load", { code, desc });
};
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();
}
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() {
if (!this.view) return;
this.window.contentView.addChildView(this.view);
isDev && this.webContents.openDevTools();
}
hide() {
if (!this.view) return;
this.window.contentView.removeChildView(this.view);
isDev && this.webContents.closeDevTools();
}
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 window() {
@@ -168,12 +223,12 @@ export default class WebviewService {
}
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;
}
@@ -183,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 {
@@ -216,47 +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);
}
get debugger() {
return this.webContents.debugger;
}
get webContents() {
return this.view.webContents;
this.logger.info(`[UA] 设置为${isMobile ? "移动端" : " pc 端"}`);
}
captureView(): Promise<Electron.NativeImage> {
if (!this.view) {
throw new Error("未找到 view");
}
return this.view.webContents.capturePage();
}
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");
}
}
}
+1
View File
@@ -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"),
+3
View File
@@ -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,
},
});
}
+8 -1
View File
@@ -4,16 +4,23 @@ import { TYPES } from "../types.ts";
import isDev from "electron-is-dev";
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;
+3 -2
View File
@@ -6,6 +6,7 @@ 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: [],
});
+5
View File
@@ -6,4 +6,9 @@ export default [
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
"@typescript-eslint/no-explicit-any": "warn",
},
},
];
@@ -115,7 +115,7 @@ export class FloatButton extends LitElement {
};
receiveMessage = (_: unknown, data: SourceData) => {
this.data = [...this.data, data];
this.data = [...this.data, { ...data, name: document.title }];
};
handleMouseStart = (event: MouseEvent) => {
+4 -1
View File
@@ -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
}
}
+5
View File
@@ -9,4 +9,9 @@ export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...fixupConfigRules(pluginReactConfig),
{
rules: {
"@typescript-eslint/no-explicit-any": "warn",
},
},
];
+1
View File
@@ -25,6 +25,7 @@
"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.4",
+27 -5
View File
@@ -2,18 +2,21 @@ import { ConfigProvider, theme } from "antd";
import React, { FC, Suspense, lazy, useEffect } from "react";
import { useDispatch } from "react-redux";
import { BrowserRouter, Route, Routes } from "react-router-dom";
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("./nodes/HomePage"));
const SourceExtract = lazy(() => import("./nodes/SourceExtract"));
const SettingPage = lazy(() => import("./nodes/SettingPage"));
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;
@@ -22,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) {
@@ -41,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);
@@ -114,6 +128,14 @@ const App: FC = () => {
</Suspense>
}
/>
<Route
path="converter"
element={
<Suspense fallback={<Loading />}>
<ConverterPage />
</Suspense>
}
/>
<Route path="*" element={<div>404</div>} />
</Route>
<Route
@@ -41,6 +41,11 @@ interface EpisodeNumberProps {
canChangeType: boolean;
}
export interface DownloadItemForm extends DownloadItem {
batch?: boolean;
batchList?: string;
}
const EpisodeNumber: FC<EpisodeNumberProps> = ({
value = "",
onChange = () => {},
@@ -174,8 +179,10 @@ const DownloadForm = forwardRef<DownloadFormRef, DownloadFormProps>(
}, [open]);
useEffect(() => {
form.setFieldsValue(item);
}, [item]);
if (modalOpen) {
form.setFieldsValue(item);
}
}, [item, modalOpen]);
useImperativeHandle(
ref,
@@ -207,7 +214,7 @@ const DownloadForm = forwardRef<DownloadFormRef, DownloadFormProps>(
return (
<>
{renderTrigger()}
<ModalForm<DownloadItem>
<ModalForm<DownloadItemForm>
open={modalOpen}
key={isEdit ? "edit" : "new"}
title={isEdit ? t("editDownload") : t("newDownload")}
@@ -273,7 +280,7 @@ const DownloadForm = forwardRef<DownloadFormRef, DownloadFormProps>(
},
}}
submitTimeout={2000}
labelCol={{ span: 4 }}
labelCol={{ span: 5 }}
layout="horizontal"
colon={false}
>
@@ -298,6 +305,29 @@ const DownloadForm = forwardRef<DownloadFormRef, DownloadFormProps>(
},
]}
/>
<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")) {
@@ -305,7 +335,7 @@ const DownloadForm = forwardRef<DownloadFormRef, DownloadFormProps>(
<ProFormTextArea
name="batchList"
fieldProps={{
rows: 6,
rows: 4,
}}
label={t("videoLink")}
placeholder={t("videoLikeDescription")}
@@ -345,27 +375,6 @@ const DownloadForm = forwardRef<DownloadFormRef, DownloadFormProps>(
}
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
if (isEdit || !form.getFieldValue("batch")) {
return (
<Form.Item
shouldUpdate
name="name"
label={t("videoName")}
tooltip={
form.getFieldValue("type") === "m3u8" &&
t("canUseMouseWheelToAdjust")
}
>
<EpisodeNumber
canChangeType={form.getFieldValue("type") === "m3u8"}
/>
</Form.Item>
);
}
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
if (
+30 -13
View File
@@ -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,16 +109,14 @@ i18n
videoLink: "Video Link",
pleaseEnterVideoLink: "Please enter video link",
videoName: "Video Name",
pleaseEnterVideoName:
"Please enter video name, default is the current time",
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
@@ -123,6 +126,8 @@ 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",
addToDownloadList: "Confirm",
@@ -133,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: {
@@ -140,7 +149,7 @@ Referer: http://www.example.com`,
downloadList: "下载列表",
downloadComplete: "下载完成",
materialExtraction: "素材提取",
setting: "设置",
setting: "软件设置",
refresh: "刷新",
newDownload: "新建下载",
selectAll: "全选",
@@ -193,7 +202,7 @@ Referer: http://www.example.com`,
pleaseSelectLanguage: "请选择显示语言",
downloadPrompt: "下载完成提示",
browserSetting: "浏览器设置",
pleaseEnterProxy: "请填写代理地址",
pleaseEnterProxy: "需要填写完整的地址,例如:http://127.0.0.1:8000",
proxySetting: "代理设置",
pleaseEnterProxyFirst: "请先输入代理地址",
blockAds: "开启广告过滤",
@@ -202,11 +211,15 @@ Referer: http://www.example.com`,
deleteSegments: "下载完成删除分片",
moreAction: "更多操作",
configDir: "配置文件目录",
clearCache: "清除缓存",
clearCacheSuccess: "清除缓存成功",
clearCacheFailed: "清除缓存失败",
binPath: "可执行程序目录",
localDir: "本地存储目录",
currentVersion: "当前版本",
selectFolder: "选择文件夹",
proxySwitch: "代理开关",
downloadProxySwitch: "下载代理开关",
proxyDescription: "该代理会对软件自带浏览器以及下载时生效",
useImmersiveSniffing: "使用沉浸式嗅探",
immersiveSniffingDescription:
@@ -219,16 +232,15 @@ Referer: http://www.example.com`,
batchDownload: "批量下载",
videoLink: "视频链接",
pleaseEnterVideoLink: "请输入站点视频链接",
videoName: "视频名",
pleaseEnterVideoName: "请输入视频名称,默认以当前时间作为视频名称",
videoName: "视频名",
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
@@ -243,6 +255,8 @@ Referer: http://www.example.com`,
close: "关闭",
exportLog: "导出日志",
showTerminal: "显示控制台",
autoUpgrade: "自动更新",
autoUpgradeTooltip: "下次启动时生效",
consoleOutput: "控制台",
downloadNow: "下载",
addToDownloadList: "确认",
@@ -253,6 +267,9 @@ Referer: http://www.example.com`,
canUseMouseWheelToAdjust: "可以使用鼠标滚轮调整",
openBrowser: "打开浏览器",
privacy: "隐私模式",
privacyTooltip: "打开开关后浏览器将不保存任何数据",
converter: "格式转换",
addFile: "添加文件",
},
},
},
+44 -29
View File
@@ -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}
+40 -24
View File
@@ -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,
};
});
+2 -3
View File
@@ -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
@@ -1,13 +1,5 @@
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";
@@ -19,16 +11,16 @@ import {
EditOutlined,
PauseCircleOutlined,
SyncOutlined,
MoreOutlined,
CodeOutlined,
FileAddOutlined,
} from "@ant-design/icons";
import { useSelector } from "react-redux";
import { selectAppStore } from "../../store";
import DownloadForm from "../../components/DownloadForm";
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;
@@ -46,7 +38,6 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
stopDownload,
onDownloadListContextMenu,
deleteDownloadItem,
convertToAudio,
showBrowserWindow,
addDownloadItem,
addDownloadItems,
@@ -76,7 +67,6 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
refreshDeps: [filter],
}
);
const [converting, setConverting] = useState<Record<number, boolean>>({});
const [progress, setProgress] = useState<Record<number, DownloadProgress>>(
{}
);
@@ -87,11 +77,11 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
log: "",
});
const onDownloadProgress = (e: unknown, 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 = () => {
@@ -163,24 +153,6 @@ 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 (
@@ -284,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 => {
@@ -390,31 +340,30 @@ const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
return true;
};
const confirmAddItems = async (values: any, now?: boolean) => {
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() : moment();
return {
url,
name,
headers: values.headers,
type: values.type,
};
});
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 = {
name: values.name || moment(),
url: values.url,
headers: values.headers,
type: values.type,
const item: Omit<DownloadItem, "id"> = {
name,
url,
headers,
type,
};
if (now) {
await downloadNow(item);
@@ -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();
@@ -125,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
@@ -136,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" />
@@ -154,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}
@@ -174,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")}
@@ -105,9 +105,14 @@
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) {
@@ -116,6 +121,14 @@
}
}
.favorite-remove-btn {
position: absolute;
top: 2px;
right: -2px;
color: gray;
display: none;
}
.card-text {
width: 100%;
margin-top: 6px;
@@ -24,7 +24,7 @@ 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";
@@ -41,7 +41,6 @@ import {
import WebView from "../../components/WebView";
import { selectAppStore } from "../../store";
import { useTranslation } from "react-i18next";
import { nanoid } from "nanoid";
import DownloadForm, { DownloadFormRef } from "../../components/DownloadForm";
interface SourceExtractProps {
@@ -72,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 [modalShow, setModalShow] = useState(false);
const [placeHolder, setPlaceHolder] = useState<string>("");
const sessionId = useRef("");
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>) => {
@@ -155,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 }));
}
};
@@ -171,6 +145,7 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
dispatch(
setBrowserStore({
url: "",
title: "",
mode: PageMode.Default,
})
);
@@ -184,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 (
@@ -251,17 +214,56 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
};
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;
};
}, []);
// 合并到主页
@@ -385,7 +387,9 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
);
} 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")}
@@ -479,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 />} />
) : (
+9
View File
@@ -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;
}
+4
View File
@@ -16,6 +16,10 @@ const initialState: AppStore = {
maxRunner: 2,
language: AppLanguage.System,
showTerminal: false,
privacy: false,
machineId: "",
downloadProxySwitch: false,
autoUpgrade: true,
};
export const appSlice = createSlice({
-14
View File
@@ -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
}
}
+7 -2
View File
@@ -3,7 +3,6 @@ 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) => {
@@ -42,7 +41,7 @@ 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;
};
@@ -50,3 +49,9 @@ export const generateUrl = (url: string) => {
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);
}
+1 -1
View File
@@ -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) {
+1 -2
View File
@@ -8,10 +8,9 @@ declare global {
onEvent: (
eventId: string,
label: "",
mapKv: Record<string, string>,
mapKv: Record<string, string>
) => void;
};
clarity?: any;
}
}
+23 -9
View File
@@ -74,10 +74,10 @@ importers:
packages/main:
dependencies:
'@cliqz/adblocker-electron':
specifier: ^1.27.3
specifier: 1.27.3
version: 1.27.3(electron@30.0.8)
'@cliqz/adblocker-electron-preload':
specifier: ^1.27.3
specifier: 1.27.3
version: 1.27.3(electron@30.0.8)
axios:
specifier: ^1.7.2
@@ -121,9 +121,15 @@ importers:
mime-types:
specifier: ^2.1.35
version: 2.1.35
nanoid:
specifier: ^5.0.7
version: 5.0.7
node-fetch:
specifier: ^3.3.2
version: 3.3.2
node-machine-id:
specifier: ^1.1.12
version: 1.1.12
node-pty:
specifier: ^1.0.0
version: 1.0.0
@@ -328,6 +334,9 @@ importers:
i18next-browser-languagedetector:
specifier: ^8.0.0
version: 8.0.0
immer:
specifier: ^10.1.1
version: 10.1.1
lint-staged:
specifier: ^15.2.5
version: 15.2.5
@@ -4203,6 +4212,9 @@ packages:
engines: {node: ^12.13 || ^14.13 || >=16}
hasBin: true
node-machine-id@1.1.12:
resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
node-pty@1.0.0:
resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==}
@@ -7810,7 +7822,7 @@ snapshots:
app-builder-bin@4.0.0: {}
app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
dependencies:
'@develar/schema-utils': 2.6.5
'@electron/notarize': 2.2.1
@@ -7824,7 +7836,7 @@ snapshots:
builder-util-runtime: 9.2.4
chromium-pickle-js: 0.2.0
debug: 4.3.4
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3)
ejs: 3.1.10
electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3)
electron-publish: 24.13.1
@@ -8616,9 +8628,9 @@ snapshots:
dependencies:
path-type: 4.0.0
dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3):
dependencies:
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
builder-util: 24.13.1
builder-util-runtime: 9.2.4
fs-extra: 10.1.0
@@ -8691,7 +8703,7 @@ snapshots:
electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3):
dependencies:
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
archiver: 5.3.2
builder-util: 24.13.1
fs-extra: 10.1.0
@@ -8701,11 +8713,11 @@ snapshots:
electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
dependencies:
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
builder-util: 24.13.1
builder-util-runtime: 9.2.4
chalk: 4.1.2
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3)
fs-extra: 10.1.0
is-ci: 3.0.1
lazy-val: 1.0.5
@@ -10134,6 +10146,8 @@ snapshots:
- bluebird
- supports-color
node-machine-id@1.1.12: {}
node-pty@1.0.0:
dependencies:
nan: 2.19.0
+1 -1
View File
@@ -2,7 +2,7 @@ import { ProcessOutput } from "zx/core";
export function printObject(
object: Record<string, unknown> | ProcessOutput,
method: "log" | "warn" | "error" = "log",
method: "log" | "warn" | "error" = "log"
) {
for (const [key, value] of Object.entries(object)) {
// eslint-disable-next-line no-console