Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8ac4182fc | |||
| 55b70d3e6d | |||
| a19765b0d3 | |||
| 5c7b65385a | |||
| b6b84269a3 | |||
| e44cb249d0 | |||
| 8bea071865 | |||
| a782652fca | |||
| 3a02803d3c | |||
| 263a08ab4c | |||
| ecdbd985ce | |||
| dd403564cc | |||
| 69d741b39a | |||
| 4c9c2e2d68 | |||
| 8660701340 | |||
| 341c5570af | |||
| ee12753818 | |||
| 055638278c | |||
| bf06ff6482 | |||
| 459a5bc5b7 | |||
| a93f18037c | |||
| 67b5218002 | |||
| d3c745dda4 | |||
| e694818f8f | |||
| 729d7645e1 | |||
| 795e21df9c | |||
| bdc15e6c50 | |||
| c3398ec572 | |||
| 6b38b348fc | |||
| d650529842 | |||
| 2094408f52 | |||
| 89cb8b3a3a | |||
| 7a2d546972 | |||
| 69e801ba2b | |||
| 14edc27ed1 | |||
| 6f7fa7f067 | |||
| a5a1c02e10 | |||
| ce08cbce4b | |||
| e03a5d38bb | |||
| 720db5a476 | |||
| e97783b08e | |||
| 12566f8668 | |||
| d686819417 | |||
| ea7b4a77af | |||
| 6427a06879 | |||
| f2f7a50e95 | |||
| fbc57ebfca | |||
| 26a5681dab | |||
| 4640ff51e8 |
@@ -1,6 +1,6 @@
|
||||
APP_NAME=media-downloader
|
||||
APP_ID=mediago.ziying.site
|
||||
APP_COPYRIGHT=caorushizi
|
||||
APP_VERSION=2.0.0
|
||||
APP_VERSION=2.0.2
|
||||
|
||||
APP_SERVER_PORT=8433
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# build.yml
|
||||
|
||||
# Workflow's name
|
||||
name: Build
|
||||
name: Build Downloader App.
|
||||
|
||||
# Workflow's trigger
|
||||
on:
|
||||
@@ -47,3 +47,8 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
APP_TD_APPID: ${{ secrets.APP_TD_APPID }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifact
|
||||
path: packages/main/release/media-downloader-setup-*
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Deploy Downloader Docs.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
BUILD_PATH: "."
|
||||
BUCKET: downloader-docs
|
||||
ENDPOINT: oss-cn-beijing.aliyuncs.com
|
||||
ACCESS_KEY: LTAI5tLckcUrBtj7bCiUYwWz
|
||||
ACCESS_KEY_SECRET: ${{ secrets.ACCESS_KEY_SECRET }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
- name: Install PNPM
|
||||
run: npm i -g pnpm
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
working-directory: ${{ env.BUILD_PATH }}
|
||||
- name: Build with Vitepress
|
||||
run: pnpm run docs:build
|
||||
working-directory: ${{ env.BUILD_PATH }}
|
||||
- name: Install Alibaba Cloud OSSUTIL
|
||||
run: wget http://gosspublic.alicdn.com/ossutil/1.6.10/ossutil64 && chmod +x ossutil64
|
||||
- name: Configure Alibaba Cloud OSSUTIL
|
||||
run: ./ossutil64 config -i ${ACCESS_KEY} -k ${ACCESS_KEY_SECRET} -e ${ENDPOINT} -c .ossutilconfig
|
||||
- name: Upload the web folder to the chosen OSS bucket
|
||||
run: ./ossutil64 --config-file .ossutilconfig cp ${{ github.workspace }}/docs/.vitepress/dist oss://${BUCKET} -r -f
|
||||
@@ -2,3 +2,4 @@ node_modules
|
||||
.vscode
|
||||
*.local
|
||||
.idea
|
||||
.parcel-cache
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
registry=https://registry.npmmirror.com/
|
||||
electron_mirror=https://npm.taobao.org/mirrors/electron/
|
||||
registry=https://registry.npmmirror.com
|
||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||
package-manager=pnpm
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
# media-downloader
|
||||
|
||||
m3u8 视频在线提取工具 流媒体下载 m3u8 下载 桌面客户端 windows mac。
|
||||
可以直接在线获取 m3u8 链接地址,无需使用使用网络抓包,无需安装浏览器插件,可以直接带出请求标头……
|
||||
|
||||
模板: <https://github.com/caorushizi/electron-template>
|
||||
可以直接在线获取 m3u8 链接地址,无需使用使用网络抓包,无需安装浏览器插件,可以直接带出请求标头……详情请参考[使用帮助](https://downloader.caorushizi.cn/guides.html?form=github)
|
||||
|
||||
## 上手指南
|
||||
|
||||
以下指南将帮助你在本地机器上安装和运行该项目,进行开发和测试。关于如何将该项目部署到在线环境,请参考部署小节。
|
||||
|
||||
【使用帮助】[blog.ziying.site](https://ziying.site/post/media-downloader-how-to-use/?from=github)
|
||||
|
||||
## 安装要求
|
||||
|
||||
运行代码需要 node 和 pnpm,node 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
|
||||
@@ -25,15 +21,27 @@ m3u8 视频在线提取工具 流媒体下载 m3u8 下载 桌面客户端 window
|
||||
|
||||
## 下载链接
|
||||
|
||||
v2.0.0-beta.5 添加视频播放功能,支持移动设备播放
|
||||
**v2.0.1(2023.7.1 发布)**
|
||||
|
||||
- 【2023.06.18 v2.0.0-beta.6 windows】[media-downloader-setup-2.0.0-beta.6.exe](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.6/media-downloader-setup-2.0.0-beta.6.exe)
|
||||
- 【2023.06.18 v2.0.0-beta.6 macos】[media-downloader-windows-v2.0.0-beta.6.dmg](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.6/media-downloader-setup-2.0.0-beta.6.dmg)
|
||||
- 【2023.06.18 v2.0.0-beta.5 windows】[media-downloader-setup-2.0.0-beta.5.exe](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.5/media-downloader-setup-2.0.0-beta.5.exe)
|
||||
- 【2023.06.18 v2.0.0-beta.5 macos】[media-downloader-windows-v2.0.0-beta.5.dmg](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.5/media-downloader-setup-2.0.0-beta.5.dmg)
|
||||
- 【2023.04.01 v2.0.0-beta.4 windows】 [media-downloader-windows-v2.0.0-beta.4.exe](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.4/media-downloader-setup-2.0.0-beta.4.exe)
|
||||
- 【2023.04.01 v2.0.0-beta.4 macos】 [media-downloader-windows-v2.0.0-beta.4.dmg](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.0-beta.4/media-downloader-setup-2.0.0-beta.4.dmg)
|
||||
- 【2022.01.08 v1.1.5】 [media-downloader-windows-v1.1.5.exe](https://github.com/caorushizi/m3u8-downloader/releases/download/1.1.5/media-downloader-setup-1.1.5.exe)
|
||||
- [windows media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
|
||||
- [macos media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 暗黑模式
|
||||
- 更多下载配置
|
||||
- 支持请求标头自动带入
|
||||
- 支持开启广告过滤
|
||||
- 支持开启沉浸式嗅探
|
||||
- 支持切换手机和 PC 模式
|
||||
- 支持修改同时下载数量
|
||||
- 修复了一些 bug
|
||||
|
||||
**v1.1.5(2022.2.5 发布)**
|
||||
|
||||
- [windows media-downloader v1.1.5](https://github.com/caorushizi/m3u8-downloader/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
|
||||
**更新日志**
|
||||
|
||||
## 软件截图
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.vitepress/dist
|
||||
.vitepress/cache
|
||||
@@ -0,0 +1,63 @@
|
||||
import { HeadConfig } from "vitepress";
|
||||
import { defineConfig } from "vitepress";
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
const head: HeadConfig[] = [
|
||||
["link", { rel: "shortcut icon", href: "/favicon.svg" }],
|
||||
];
|
||||
if (!isDev) {
|
||||
head.push([
|
||||
"script",
|
||||
{},
|
||||
`var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?eefcbd14f0323044aa0ca678cd278381";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();`,
|
||||
]);
|
||||
}
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "media-downloader",
|
||||
description: "简单易用,快速下载",
|
||||
lastUpdated: true,
|
||||
head,
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: "Home", link: "/" },
|
||||
{ text: "教程", link: "/guides" },
|
||||
],
|
||||
|
||||
sidebar: [
|
||||
{
|
||||
text: "开始",
|
||||
items: [
|
||||
{ text: "快速开始", link: "/guides" },
|
||||
{ text: "使用说明", link: "/documents" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "其他",
|
||||
items: [
|
||||
{ text: "旧版本", link: "/history" },
|
||||
{ text: "意见收集", link: "/proposal" },
|
||||
{ text: "支持列表", link: "/list" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/caorushizi/m3u8-downloader" },
|
||||
],
|
||||
|
||||
footer: {
|
||||
message: '<a href="https://beian.miit.gov.cn">豫ICP备20012967号-2</a>',
|
||||
copyright: "Copyright © 2019-present caorushizi",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch, ref, watchEffect } from "vue";
|
||||
import { init, WalineInstance } from "@waline/client";
|
||||
import "@waline/client/waline.css";
|
||||
import { useData, useRoute } from "vitepress";
|
||||
|
||||
const route = useRoute();
|
||||
const { isDark } = useData();
|
||||
|
||||
const commentsRef = ref<WalineInstance | null>(null);
|
||||
|
||||
watchEffect(() => {
|
||||
commentsRef.value?.update({
|
||||
dark: isDark.value,
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
commentsRef.value?.destroy();
|
||||
initWaline();
|
||||
}
|
||||
);
|
||||
|
||||
function initWaline() {
|
||||
commentsRef.value = init({
|
||||
el: "#waline",
|
||||
serverURL: "https://comments.ziying.site",
|
||||
dark: isDark.value,
|
||||
lang: "zh-CN",
|
||||
reaction: true,
|
||||
pageview: true,
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initWaline();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="waline">hello world</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#waline {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { h } from "vue";
|
||||
import Theme from "vitepress/theme";
|
||||
import "./style/var.css";
|
||||
import Comments from "./components/Comments.vue";
|
||||
|
||||
export default {
|
||||
...Theme,
|
||||
Layout() {
|
||||
return h(Theme.Layout, null, {
|
||||
"doc-after": () => h(Comments),
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
:root {
|
||||
/* 标题 */
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: linear-gradient(
|
||||
135deg,
|
||||
#5e9ef3 10%,
|
||||
#2a82f6 100%
|
||||
);
|
||||
|
||||
/* 图标背景 */
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
135deg,
|
||||
#5e9ef3 10%,
|
||||
#2a82f6 100%
|
||||
);
|
||||
--vp-home-hero-image-filter: blur(150px);
|
||||
|
||||
/* brand按钮 */
|
||||
--vp-button-brand-border: #5e9ef3;
|
||||
--vp-button-brand-text: #f2f7fd;
|
||||
--vp-button-brand-bg: #2a82f6;
|
||||
|
||||
--vp-button-brand-hover-border: #5e9ef3;
|
||||
--vp-button-brand-hover-text: #fff;
|
||||
--vp-button-brand-hover-bg: #2a82f6;
|
||||
|
||||
--vp-button-brand-active-border: #f6ceec;
|
||||
|
||||
--vp-c-brand: #2a82f6;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 使用说明
|
||||
|
||||
这个页面主要讲解的是设置页面的参数的含义
|
||||
|
||||

|
||||
|
||||
## 基础设置
|
||||
|
||||
::: tip
|
||||
下载器全局设置
|
||||
:::
|
||||
|
||||
1. 选择文件夹: 视频下载的路径
|
||||
|
||||
2. 下载器主题: 支持浅色和深色模式
|
||||
|
||||
3. 在新窗口中打开浏览器: 开启了之后浏览器页面会独立出一个窗口。
|
||||
|
||||

|
||||
|
||||
4. 下载完成提示: 开启之后下载完成会出现系统提示
|
||||
|
||||
## 浏览器设置
|
||||
|
||||
::: tip
|
||||
浏览器窗口相关的设置
|
||||
:::
|
||||
|
||||
1. 代理设置: 填写自己的代理地址
|
||||
|
||||
2. 代理开关: 打开开关后浏览器默认会走填写的代理地址,如果代理地址这个开关是打不开的
|
||||
|
||||
3. 开启广告过滤: 打开开关后会过滤页面中的广告
|
||||
|
||||
4. 以手机模式进入: 打开后会将浏览器伪装成手机浏览器,请求移动端的站点(左侧是开启手机模式的截图)
|
||||
|
||||

|
||||
|
||||
5. 使用浏览器插件
|
||||
|
||||
- 打开状态: 从浏览器中嗅探到的资源不会自动添加到下载列表,需要手动点击页面下方的下载的链接来添加视频到下载列表 **(后面会有很强大的功能哦)**
|
||||
|
||||

|
||||
|
||||
- 关闭状态: 浏览器中嗅探到的资源会自动添加到下载列表
|
||||
|
||||
## 下载设置
|
||||
|
||||
::: tip
|
||||
和下载相关的设置
|
||||
:::
|
||||
|
||||
1. 下载完成删除分片: 开启后下载完成后会将分片文件删除
|
||||
2. 最大同时下载数量: 最多同时可以下载多少视频文件,目前最大是 10,最小是 1
|
||||
3. 更多操作
|
||||
- 配置文件目录: 软件数据库路径、日志等等路径
|
||||
- 可执行文件目录: 下载器二进制文件的路径
|
||||
- 本地存储路径: 下载视频的本地路径
|
||||
4. 当前版本: 当前软件版本信息
|
||||
|
||||
## 其他问题
|
||||
|
||||
**Q: 关于直播视频的下载**
|
||||
|
||||
A: 软件支持直播下载,软件目前没有好的办法区分直播流,所以将所有的下载控制台开启。用户自行分辨下载数据源
|
||||
|
||||
**Q: 关于 MacOS 版本**
|
||||
|
||||
A: 目前 MacOS 版本属于非稳定版本,2023 年 5 月发布。需要在 Mac 的安全设置中打开来自身份不明开发者的 Mac App
|
||||

|
||||
|
||||
**Q: 关于旧版本**
|
||||
|
||||
A: 1.1.5 版本确实发布了很长时间,经过大量用户的验证,比较稳定。所以想要使用旧版软件的话请访问[此链接](/history.html)
|
||||
|
||||
**Q: 关于 Win7 用户**
|
||||
|
||||
A: v2.0.0 之后的版本已经放弃不支持 Win7 如果需要在 win7 上面使用同样需要下载 1.1.5 版本
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 快速开始
|
||||
|
||||
这篇文章是软件的简单说明,可以快速上手使用本本软件。
|
||||
|
||||
::: tip
|
||||
我建了一个 QQ 群,方便大家一起沟通交流: 574209001
|
||||
:::
|
||||
|
||||
## 下载安装
|
||||
|
||||
**v2.0.1(2023.7.1 发布)**
|
||||
|
||||
- [windows media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
|
||||
- [macos media-downloader v2.0.1](https://github.com/caorushizi/m3u8-downloader/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 暗黑模式
|
||||
- 更多下载配置
|
||||
- 支持请求标头自动带入
|
||||
- 支持开启广告过滤
|
||||
- 支持开启沉浸式嗅探
|
||||
- 支持切换手机和 PC 模式
|
||||
- 支持修改同时下载数量
|
||||
- 修复了一些 bug
|
||||
|
||||
**v1.1.5(2022.2.5 发布)**
|
||||
|
||||
- [windows media-downloader v1.1.5](https://github.com/caorushizi/m3u8-downloader/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 支持视频下载
|
||||
|
||||
## 操作说明
|
||||
|
||||
### 视频自动嗅探
|
||||
|
||||
1. 选择【资源提取】
|
||||
|
||||

|
||||
|
||||
2. 进入视频的网址
|
||||
|
||||

|
||||
|
||||
3. 开始下载,即可下载视频
|
||||
|
||||

|
||||
|
||||
### 手动下载
|
||||
|
||||
1. 点击页面右上角的【新建下载】
|
||||
|
||||

|
||||
|
||||
2. 在新建下载弹窗中填入【视频名称】和【m3u8 地址】即可
|
||||
|
||||

|
||||
|
||||
3. 在列表中点击下载,即可下载视频
|
||||
|
||||

|
||||
|
||||
## 附加功能
|
||||
|
||||
1. 下载完成后可以在下载列表中点击转换音频按钮将视频转换为音频
|
||||
|
||||

|
||||
|
||||
2. 下载完成后播放视频
|
||||
|
||||
- PC 播放
|
||||
|
||||

|
||||
|
||||
- 移动端播放
|
||||
|
||||

|
||||
|
||||
3. 批量下载
|
||||
|
||||

|
||||
|
||||
## 开始你的视频下载吧
|
||||
|
||||
是不是很简单,快去下载你的视频吧
|
||||
|
||||
::: warning
|
||||
本软件仅可用于学习交流
|
||||
:::
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 旧版本
|
||||
|
||||
::: info
|
||||
这里是旧版本的永久链接,如果有需求的话,可以在这里下载
|
||||
|
||||
**这个链接的软件不会自动升级, 可放心使用**
|
||||
:::
|
||||
|
||||
**v1.1.5(2022.2.5 发布)**
|
||||
|
||||
- [windows media-downloader v1.1.5](https://github.com/caorushizi/m3u8-downloader/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
- ~~macos media-downloader v1.1.5 无~~
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 支持视频下载
|
||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 311 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,30 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "在线视频下载"
|
||||
text: "简单易用,快速下载"
|
||||
tagline: 简单易学,无需抓包,不需要安装插件
|
||||
image:
|
||||
src: /home.png
|
||||
alt: home
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 快速开始
|
||||
link: /guides
|
||||
- theme: alt
|
||||
text: 使用说明
|
||||
link: /documents
|
||||
|
||||
features:
|
||||
- icon: ⏩
|
||||
title: 无需抓包
|
||||
details: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
|
||||
- icon: 📱
|
||||
title: 移动播放
|
||||
details: 可以轻松无缝的在PC和移动设备之前切换,下载完成后即可使用手机观看视频。
|
||||
- icon: ⚡️
|
||||
title: 支持批量下载
|
||||
details: 支持同时最多下载10个视频和直播资源,高速带宽不闲置。
|
||||
---
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 支持列表
|
||||
|
||||
::: tip
|
||||
大家有什么网站想要支持的在这里提
|
||||
:::
|
||||
|
||||
**模板**
|
||||
|
||||
---
|
||||
|
||||
- 网站首页: xxx
|
||||
- 视频(直播)列表页: xxx
|
||||
- 视频(直播)页: xxx
|
||||
|
||||
---
|
||||
|
||||
支持完成后我会在评论区回复
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 意见收集
|
||||
|
||||
::: info
|
||||
这里手机大家使用软件的问题,或者意见建议
|
||||
:::
|
||||
|
||||
如果软件报错,需要描述清楚在什么网站下载什么视频的时候报错
|
||||
|
||||
如果是建议请描述清楚你的需求
|
||||
|
||||
如果是意见请尽量提出来,我会尽量满足大家的需求
|
||||
@@ -0,0 +1,40 @@
|
||||
<svg t="1686326276609" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="6962" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="200" height="200">
|
||||
<path
|
||||
d="M110.933333 42.666667h716.8A68.266667 68.266667 0 0 1 896 110.933333v802.133334a68.266667 68.266667 0 0 1-68.266667 68.266666H110.933333A68.266667 68.266667 0 0 1 42.666667 913.066667V110.933333A68.266667 68.266667 0 0 1 110.933333 42.666667z"
|
||||
fill="#111314" fill-opacity=".72" p-id="6963"></path>
|
||||
<path
|
||||
d="M853.333333 42.666667h85.333334a68.266667 68.266667 0 0 1 68.266666 68.266666v802.133334A68.266667 68.266667 0 0 1 938.666667 981.333333h-85.333334a68.266667 68.266667 0 0 0 68.266667-68.266666V110.933333A68.266667 68.266667 0 0 0 853.333333 42.666667z"
|
||||
fill="#CACBCC" p-id="6964"></path>
|
||||
<path
|
||||
d="M162.133333 128h614.4a34.133333 34.133333 0 0 1 34.133334 34.133333v699.733334a34.133333 34.133333 0 0 1-34.133334 34.133333H162.133333a34.133333 34.133333 0 0 1-34.133333-34.133333V162.133333a34.133333 34.133333 0 0 1 34.133333-34.133333z"
|
||||
fill="#FFFFFF" p-id="6965"></path>
|
||||
<path
|
||||
d="M187.733333 170.666667h563.2c9.386667 0 17.066667 7.68 17.066667 17.066666v221.866667a17.066667 17.066667 0 0 1-17.066667 17.066667H187.733333a17.066667 17.066667 0 0 1-17.066666-17.066667V187.733333c0-9.386667 7.68-17.066667 17.066666-17.066666z"
|
||||
fill="#EDEEF0" p-id="6966"></path>
|
||||
<path
|
||||
d="M256 298.666667m17.066667 0l136.533333 0q17.066667 0 17.066667 17.066666l0 136.533334q0 17.066667-17.066667 17.066666l-136.533333 0q-17.066667 0-17.066667-17.066666l0-136.533334q0-17.066667 17.066667-17.066666Z"
|
||||
fill="#FFC13C" p-id="6967"></path>
|
||||
<path
|
||||
d="M341.333333 392.533333a34.133333 34.133333 0 1 1 0-68.266666 34.133333 34.133333 0 0 1 0 68.266666z m65.109334 56.192H276.224C276.224 426.666667 276.224 401.066667 341.333333 401.066667s65.109333 25.6 65.109334 47.658666z"
|
||||
fill="#FFFFFF" p-id="6968"></path>
|
||||
<path
|
||||
d="M469.333333 341.333333m17.066667 0l136.533333 0q17.066667 0 17.066667 17.066667l0 8.533333q0 17.066667-17.066667 17.066667l-136.533333 0q-17.066667 0-17.066667-17.066667l0-8.533333q0-17.066667 17.066667-17.066667Z"
|
||||
fill="#5BADFF" p-id="6969"></path>
|
||||
<path
|
||||
d="M512 554.666667m17.066667 0l179.2 0q17.066667 0 17.066666 17.066666l0 51.2q0 17.066667-17.066666 17.066667l-179.2 0q-17.066667 0-17.066667-17.066667l0-51.2q0-17.066667 17.066667-17.066666Z"
|
||||
fill="#EDEEF0" p-id="6970"></path>
|
||||
<path
|
||||
d="M512 682.666667m17.066667 0l179.2 0q17.066667 0 17.066666 17.066666l0 51.2q0 17.066667-17.066666 17.066667l-179.2 0q-17.066667 0-17.066667-17.066667l0-51.2q0-17.066667 17.066667-17.066666Z"
|
||||
fill="#EDEEF0" p-id="6971"></path>
|
||||
<path
|
||||
d="M213.333333 554.666667m17.066667 0l221.866667 0q17.066667 0 17.066666 17.066666l0 8.533334q0 17.066667-17.066666 17.066666l-221.866667 0q-17.066667 0-17.066667-17.066666l0-8.533334q0-17.066667 17.066667-17.066666Z"
|
||||
fill="#EDEEF0" p-id="6972"></path>
|
||||
<path
|
||||
d="M213.333333 640m17.066667 0l221.866667 0q17.066667 0 17.066666 17.066667l0 8.533333q0 17.066667-17.066666 17.066667l-221.866667 0q-17.066667 0-17.066667-17.066667l0-8.533333q0-17.066667 17.066667-17.066667Z"
|
||||
fill="#EDEEF0" p-id="6973"></path>
|
||||
<path
|
||||
d="M213.333333 725.333333m17.066667 0l221.866667 0q17.066667 0 17.066666 17.066667l0 8.533333q0 17.066667-17.066666 17.066667l-221.866667 0q-17.066667 0-17.066667-17.066667l0-8.533333q0-17.066667 17.066667-17.066667Z"
|
||||
fill="#EDEEF0" p-id="6974"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 120 KiB |
@@ -4,10 +4,16 @@
|
||||
"description": "electron-template",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "pnpm --parallel -F \"./packages/*\" run dev",
|
||||
"build": "pnpm --parallel -F \"./packages/*\" run build",
|
||||
"dev": "pnpm run build:mobile && pnpm --parallel -F \"./packages/*\" run dev",
|
||||
"build": "pnpm run build:mobile && pnpm run build:main && pnpm run build:renderer",
|
||||
"build:mobile": "pnpm -F mobile run build",
|
||||
"build:main": "pnpm -F main run build",
|
||||
"build:renderer": "pnpm -F renderer run build",
|
||||
"pack": "pnpm -F media-downloader run pack",
|
||||
"release": "pnpm run build && pnpm -F media-downloader run release"
|
||||
"release": "pnpm run build && pnpm -F main run release",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -16,5 +22,10 @@
|
||||
"patchedDependencies": {
|
||||
"@cliqz/adblocker@1.26.6": "patches/@cliqz__adblocker@1.26.6.patch"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@waline/client": "^2.15.5",
|
||||
"vitepress": "1.0.0-beta.3",
|
||||
"vue": "^3.3.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ node_modules
|
||||
build
|
||||
dist
|
||||
bin/Logs
|
||||
release
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
output
|
||||
bin
|
||||
mobile
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "media-downloader",
|
||||
"version": "2.0.2",
|
||||
"description": "在线视频下载器",
|
||||
"main": "main/index.js",
|
||||
"scripts": {},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "^1.26.6",
|
||||
"@cliqz/adblocker-electron-preload": "^1.26.6"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"name": "media-downloader",
|
||||
"version": "2.0.0",
|
||||
"name": "main",
|
||||
"version": "0.1.0",
|
||||
"description": "在线视频下载器",
|
||||
"main": "main/index.js",
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"rebuild": "electron-rebuild -f -w better-sqlite3",
|
||||
"dev": "cross-env NODE_ENV=development node script/dev.mjs",
|
||||
"build": "cross-env NODE_ENV=production node script/build.mjs",
|
||||
@@ -59,6 +58,7 @@
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"execa": "^7.1.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"glob": "^10.3.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import { rmSync } from "node:fs";
|
||||
import { mainResolve, loadDotEnvDefined } from "./utils.mjs";
|
||||
import {
|
||||
mainResolve,
|
||||
loadDotEnvDefined,
|
||||
copyResource,
|
||||
removeResource,
|
||||
} from "./utils.mjs";
|
||||
|
||||
const mainDefined = loadDotEnvDefined();
|
||||
|
||||
rmSync(mainResolve("build"), { recursive: true, force: true });
|
||||
rmSync(mainResolve("dist"), { recursive: true, force: true });
|
||||
removeResource([mainResolve("app/build")]);
|
||||
|
||||
const path = "build/Release/better_sqlite3.node";
|
||||
|
||||
copyResource([
|
||||
{
|
||||
from: mainResolve("node_modules/better-sqlite3", path),
|
||||
to: mainResolve("app", path),
|
||||
},
|
||||
]);
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: [mainResolve("src/index.ts"), mainResolve("src/preload.ts")],
|
||||
entryPoints: [
|
||||
mainResolve("src/index.ts"),
|
||||
mainResolve("src/preload.ts"),
|
||||
mainResolve("src/webview.ts"),
|
||||
],
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
sourcemap: false,
|
||||
@@ -24,7 +40,7 @@ esbuild.build({
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
...mainDefined,
|
||||
},
|
||||
outdir: mainResolve("build/main"),
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
minify: true,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { spawn } from "child_process";
|
||||
import { cpSync } from "node:fs";
|
||||
import electron from "electron";
|
||||
import * as esbuild from "esbuild";
|
||||
import chokidar from "chokidar";
|
||||
import { loadDotEnvRuntime, mainResolve, log } from "./utils.mjs";
|
||||
import { loadDotEnvRuntime, mainResolve, log, copyResource } from "./utils.mjs";
|
||||
|
||||
let electronProcess = null;
|
||||
|
||||
@@ -11,25 +10,26 @@ process.env.NODE_ENV = "development";
|
||||
loadDotEnvRuntime();
|
||||
|
||||
async function copySource() {
|
||||
const oldSqlite3Path = mainResolve(
|
||||
"node_modules/better-sqlite3/build/Release"
|
||||
);
|
||||
const newSqlite3Path = mainResolve("build/Release");
|
||||
const path = "build/Release/better_sqlite3.node";
|
||||
|
||||
cpSync(oldSqlite3Path, newSqlite3Path, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
const oldBinPath = mainResolve("bin");
|
||||
const newBinPath = mainResolve("build/bin");
|
||||
|
||||
cpSync(oldBinPath, newBinPath, {
|
||||
recursive: true,
|
||||
});
|
||||
copyResource([
|
||||
{
|
||||
from: mainResolve("node_modules/better-sqlite3", path),
|
||||
to: mainResolve("app", path),
|
||||
},
|
||||
{
|
||||
from: mainResolve("bin"),
|
||||
to: mainResolve("app/bin"),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: [mainResolve("src/index.ts"), mainResolve("src/preload.ts")],
|
||||
entryPoints: [
|
||||
mainResolve("src/index.ts"),
|
||||
mainResolve("src/preload.ts"),
|
||||
mainResolve("src/webview.ts"),
|
||||
],
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
sourcemap: true,
|
||||
@@ -43,10 +43,10 @@ const ctx = await esbuild.context({
|
||||
],
|
||||
define: {
|
||||
// 开发环境中二进制可执行文件的路径
|
||||
__bin__: `"${mainResolve("bin").replace(/\\/g, "\\\\")}"`,
|
||||
__bin__: `"${mainResolve("bin", process.platform).replace(/\\/g, "\\\\")}"`,
|
||||
},
|
||||
plugins: [],
|
||||
outdir: mainResolve("build/main"),
|
||||
outdir: mainResolve("app/build/main"),
|
||||
loader: { ".png": "file" },
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ watcher.on("change", async () => {
|
||||
});
|
||||
|
||||
function startElectron() {
|
||||
const args = ["--inspect=5858", mainResolve("build/main/index.js")];
|
||||
const args = ["--inspect=5858", mainResolve("app/build/main/index.js")];
|
||||
|
||||
electronProcess = spawn(String(electron), args);
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import builder from "electron-builder";
|
||||
import { readFileSync } from "node:fs";
|
||||
import semver from "semver";
|
||||
import { mainResolve, loadDotEnvRuntime, log } from "./utils.mjs";
|
||||
import {
|
||||
mainResolve,
|
||||
loadDotEnvRuntime,
|
||||
log,
|
||||
removeResource,
|
||||
} from "./utils.mjs";
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(mainResolve("./package.json")));
|
||||
removeResource([mainResolve("release")]);
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(mainResolve("./app/package.json")));
|
||||
loadDotEnvRuntime();
|
||||
|
||||
if (semver.neq(process.env.APP_VERSION, packageJson.version)) {
|
||||
@@ -11,20 +18,6 @@ if (semver.neq(process.env.APP_VERSION, packageJson.version)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const extraResources = [
|
||||
{
|
||||
from: "build/mobile",
|
||||
to: "mobile",
|
||||
},
|
||||
];
|
||||
if (process.platform === "win32") {
|
||||
// windows
|
||||
extraResources.push("bin/ffmpeg.exe", "bin/N_m3u8DL-CLI_v3.0.2.exe");
|
||||
} else {
|
||||
// mac
|
||||
extraResources.push("bin/N_m3u8DL-RE", "bin/ffmpeg");
|
||||
}
|
||||
|
||||
// Let's get that intellisense working
|
||||
/**
|
||||
* @type {import('electron-builder').Configuration}
|
||||
@@ -37,21 +30,25 @@ const options = {
|
||||
copyright: process.env.APP_COPYRIGHT,
|
||||
artifactName: "${productName}-setup-${buildVersion}.${ext}",
|
||||
directories: {
|
||||
output: "./dist",
|
||||
output: "./release",
|
||||
},
|
||||
files: [
|
||||
{
|
||||
from: "./build",
|
||||
to: "./",
|
||||
filter: ["**/*"],
|
||||
},
|
||||
"./package.json",
|
||||
],
|
||||
extraResources: [
|
||||
{
|
||||
from: "./node_modules/better-sqlite3/build/Release",
|
||||
to: "./build/Release",
|
||||
from: "./app/mobile",
|
||||
to: "mobile",
|
||||
},
|
||||
{
|
||||
from: "./app/bin/${platform}/",
|
||||
to: "bin",
|
||||
},
|
||||
],
|
||||
extraResources,
|
||||
win: {
|
||||
icon: "../assets/icon.ico",
|
||||
target: [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { existsSync, cpSync, rmSync } from "node:fs";
|
||||
import dotenv from "dotenv";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -8,8 +8,8 @@ const con = console;
|
||||
export const log = con.log;
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
export const mainResolve = (r) => resolve(__dirname, "..", r);
|
||||
export const rootResolve = (r) => resolve(__dirname, "../../..", r);
|
||||
export const mainResolve = (...r) => resolve(__dirname, "..", ...r);
|
||||
export const rootResolve = (...r) => resolve(__dirname, "../../..", ...r);
|
||||
const nodeEnv = process.env.NODE_ENV;
|
||||
log("当前的环境是: ", nodeEnv);
|
||||
|
||||
@@ -63,3 +63,18 @@ export function loadDotEnvDefined() {
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function copyResource(resource) {
|
||||
resource.forEach((r) => {
|
||||
const { from, to } = r;
|
||||
cpSync(from, to, {
|
||||
recursive: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function removeResource(resource) {
|
||||
resource.forEach((r) => {
|
||||
rmSync(r, { recursive: true, force: true });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
StoreService,
|
||||
} from "./interfaces";
|
||||
import { TYPES } from "./types";
|
||||
import { app, nativeTheme } from "electron";
|
||||
import { Menu, Tray, app, nativeImage, nativeTheme } from "electron";
|
||||
import TrayIcon from "./tray-icon.png";
|
||||
import path from "path";
|
||||
|
||||
@injectable()
|
||||
export default class ElectronApp implements App {
|
||||
@@ -60,6 +62,8 @@ export default class ElectronApp implements App {
|
||||
|
||||
this.initAppTheme();
|
||||
this.resetDownloadStatus();
|
||||
|
||||
this.initTray();
|
||||
}
|
||||
|
||||
initAppTheme(): void {
|
||||
@@ -67,6 +71,21 @@ export default class ElectronApp implements App {
|
||||
nativeTheme.themeSource = theme;
|
||||
}
|
||||
|
||||
initTray() {
|
||||
const iconPath = path.resolve(__dirname, TrayIcon);
|
||||
const icon = nativeImage.createFromPath(iconPath);
|
||||
const tray = new Tray(icon);
|
||||
tray.setToolTip("在线视频下载");
|
||||
tray.addListener("click", () => {
|
||||
this.mainWindow.init();
|
||||
});
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: "显示主窗口", click: () => this.mainWindow.init() },
|
||||
{ label: "退出 app", role: "quit" },
|
||||
]);
|
||||
tray.setContextMenu(contextMenu);
|
||||
}
|
||||
|
||||
// 如果重启后还有正在下载的视频,就将状态改成下载失败
|
||||
async resetDownloadStatus(): Promise<void> {
|
||||
// 重启后如果还有 downloading 状态的数据, 全部重置为失败
|
||||
|
||||
@@ -3,7 +3,6 @@ import { inject, injectable } from "inversify";
|
||||
import { handle } from "../helper/decorator";
|
||||
import {
|
||||
StoreService,
|
||||
LoggerService,
|
||||
type Controller,
|
||||
VideoRepository,
|
||||
DownloadItem,
|
||||
@@ -14,13 +13,11 @@ import {
|
||||
} from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
import { spawnDownload } from "helper";
|
||||
import MainWindowServiceImpl from "services/MainWindowServiceImpl";
|
||||
import MainWindowServiceImpl from "windows/Main";
|
||||
|
||||
@injectable()
|
||||
export default class DownloadController implements Controller {
|
||||
constructor(
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerService,
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.VideoRepository)
|
||||
@@ -66,7 +63,7 @@ export default class DownloadController implements Controller {
|
||||
if (!video) {
|
||||
return Promise.reject("没有找到该视频");
|
||||
}
|
||||
const { name, url } = video;
|
||||
const { name, url, headers } = video;
|
||||
const local = this.storeService.get("local");
|
||||
|
||||
// 从配置中添加参数
|
||||
@@ -78,6 +75,7 @@ export default class DownloadController implements Controller {
|
||||
url,
|
||||
local,
|
||||
name,
|
||||
headers,
|
||||
deleteSegments,
|
||||
},
|
||||
process: spawnDownload,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MenuItemConstructorOptions,
|
||||
nativeTheme,
|
||||
shell,
|
||||
clipboard,
|
||||
} from "electron";
|
||||
import { Favorite } from "entity/Favorite";
|
||||
import { convertToAudio } from "helper";
|
||||
@@ -16,7 +17,6 @@ import path from "path";
|
||||
import { handle, getLocalIP } from "../helper";
|
||||
import {
|
||||
StoreService,
|
||||
LoggerService,
|
||||
type Controller,
|
||||
FavoriteRepository,
|
||||
MainWindowService,
|
||||
@@ -33,8 +33,6 @@ export default class HomeController implements Controller {
|
||||
private sharedState: Record<string, any> = {};
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerService,
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.FavoriteRepository)
|
||||
@@ -149,6 +147,10 @@ export default class HomeController implements Controller {
|
||||
if (key === "theme") {
|
||||
nativeTheme.themeSource = val;
|
||||
}
|
||||
// isMobile
|
||||
if (key === "isMobile") {
|
||||
this.webviewService.setUserAgent(val);
|
||||
}
|
||||
|
||||
this.storeService.set(key, val);
|
||||
}
|
||||
@@ -174,7 +176,14 @@ export default class HomeController implements Controller {
|
||||
payload: id,
|
||||
});
|
||||
};
|
||||
const item = await this.videoRepository.findVideo(id);
|
||||
const template: Array<MenuItemConstructorOptions | MenuItem> = [
|
||||
{
|
||||
label: "拷贝链接地址",
|
||||
click: () => {
|
||||
clipboard.writeText(item?.url || "");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "选择",
|
||||
click: () => {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { IpcMainEvent } from "electron";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { handle } from "../helper/decorator";
|
||||
import { LoggerService, type Controller, WebviewService } from "../interfaces";
|
||||
import { type Controller, WebviewService, StoreService } from "../interfaces";
|
||||
import { TYPES } from "../types";
|
||||
|
||||
@injectable()
|
||||
export default class WebviewController implements Controller {
|
||||
constructor(
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerService,
|
||||
@inject(TYPES.WebviewService)
|
||||
private readonly webview: WebviewService
|
||||
private readonly webview: WebviewService,
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService
|
||||
) {}
|
||||
|
||||
@handle("set-webview-bounds")
|
||||
@@ -47,4 +47,10 @@ export default class WebviewController implements Controller {
|
||||
async webviewGoHome() {
|
||||
await this.webview.goHome();
|
||||
}
|
||||
|
||||
@handle("webview-change-user-agent")
|
||||
async webviewChangeUserAgent(e: IpcMainEvent, isMobile: boolean) {
|
||||
this.webview.setUserAgent(isMobile);
|
||||
this.storeService.set("isMobile", isMobile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,19 @@ export class Video {
|
||||
})
|
||||
url: string;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: true,
|
||||
})
|
||||
headers: string;
|
||||
|
||||
@Column({
|
||||
type: "boolean",
|
||||
default: false,
|
||||
nullable: false,
|
||||
})
|
||||
isLive: boolean;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { spawn } from "child_process";
|
||||
import { ffmpegPath, macDownloaderPath, winDownloaderPath } from "./variables";
|
||||
import { execa } from "execa";
|
||||
import { macDownloaderPath, winDownloaderPath } from "./variables";
|
||||
import iconv from "iconv-lite";
|
||||
import { event, stripColors } from "./utils";
|
||||
import { formatHeaders, stripColors } from "./utils";
|
||||
import { DownloadParams, DownloadProgress } from "interfaces";
|
||||
|
||||
export const spawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
@@ -12,36 +12,71 @@ export const spawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
const winSpawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
const { id, abortSignal, url, local, name, deleteSegments } = params;
|
||||
const winSpawnDownload = async (params: DownloadParams): Promise<void> => {
|
||||
const {
|
||||
id,
|
||||
abortSignal,
|
||||
url,
|
||||
local,
|
||||
name,
|
||||
deleteSegments,
|
||||
headers,
|
||||
callback,
|
||||
} = params;
|
||||
const progressReg = /Progress:\s(\d+)\/(\d+)\s\(.+?\).+?\((.+?\/s).*?\)/g;
|
||||
const isLiveReg = /识别为直播流, 开始录制/g;
|
||||
const startDownloadReg = /开始下载文件/g;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawnParams = [url, "--workDir", local, "--saveName", name];
|
||||
|
||||
if (headers) {
|
||||
spawnParams.push("--headers", formatHeaders(headers));
|
||||
}
|
||||
|
||||
if (deleteSegments) {
|
||||
spawnParams.push("--enableDelAfterDone");
|
||||
}
|
||||
|
||||
const downloader = spawn(winDownloaderPath, spawnParams, {
|
||||
const downloader = execa(winDownloaderPath, spawnParams, {
|
||||
signal: abortSignal.signal,
|
||||
});
|
||||
|
||||
downloader.stdout.on("data", (data) => {
|
||||
let isLive = false;
|
||||
downloader.stdout?.on("data", (data) => {
|
||||
const str = iconv.decode(Buffer.from(data), "gbk");
|
||||
str.split("\n").forEach((item) => {
|
||||
if (item.trim() == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLiveReg.test(item) || startDownloadReg.test(item)) {
|
||||
callback({
|
||||
id,
|
||||
type: "ready",
|
||||
isLive,
|
||||
cur: "",
|
||||
total: "",
|
||||
speed: "",
|
||||
});
|
||||
isLive = true;
|
||||
}
|
||||
|
||||
const result = progressReg.exec(item);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, cur, total, speed] = result;
|
||||
const progress: DownloadProgress = { id, cur, total, speed };
|
||||
event.emit("download-progress", progress);
|
||||
const progress: DownloadProgress = {
|
||||
id,
|
||||
type: "progress",
|
||||
cur,
|
||||
total,
|
||||
speed,
|
||||
isLive,
|
||||
};
|
||||
callback(progress);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,8 +95,21 @@ const winSpawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
};
|
||||
|
||||
const macSpawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
const { id, abortSignal, url, local, name, deleteSegments } = params;
|
||||
const progressReg = /([\d.]+)% .*? ([\d.\w]+?) /g;
|
||||
const {
|
||||
id,
|
||||
abortSignal,
|
||||
url,
|
||||
local,
|
||||
name,
|
||||
deleteSegments,
|
||||
headers,
|
||||
callback,
|
||||
} = params;
|
||||
// const progressReg = /([\d.]+)% .*? ([\d.\w]+?) /g;
|
||||
const progressReg = /([\d.]+)%/g;
|
||||
const errorReg = /ERROR/g;
|
||||
const startDownloadReg = /保存文件名:/g;
|
||||
const isLiveReg = /检测到直播流/g;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawnParams = [
|
||||
@@ -72,23 +120,52 @@ const macSpawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
local,
|
||||
"--save-name",
|
||||
name,
|
||||
"--auto-select",
|
||||
];
|
||||
|
||||
if (headers) {
|
||||
const h: Record<string, unknown> = JSON.parse(headers);
|
||||
Object.entries(h).forEach(([k, v]) => {
|
||||
spawnParams.push("-H", `${k}: ${v}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteSegments) {
|
||||
spawnParams.push("--del-after-done");
|
||||
}
|
||||
|
||||
const downloader = spawn(macDownloaderPath, spawnParams, {
|
||||
const downloader = execa(macDownloaderPath, spawnParams, {
|
||||
signal: abortSignal.signal,
|
||||
});
|
||||
|
||||
downloader.stdout.on("data", (data) => {
|
||||
let isLive = false;
|
||||
downloader.stdout?.on("data", (data) => {
|
||||
const str = String(Buffer.from(data));
|
||||
str.split("\n").forEach((item) => {
|
||||
if (item.trim() == "") {
|
||||
return;
|
||||
}
|
||||
const result = progressReg.exec(stripColors(item));
|
||||
|
||||
if (isLiveReg.test(item) || startDownloadReg.test(item)) {
|
||||
callback({
|
||||
id,
|
||||
type: "ready",
|
||||
isLive,
|
||||
cur: "",
|
||||
total: "",
|
||||
speed: "",
|
||||
});
|
||||
isLive = true;
|
||||
}
|
||||
|
||||
const log = stripColors(item);
|
||||
|
||||
if (errorReg.test(log)) {
|
||||
reject(new Error(log));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = progressReg.exec(log);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
@@ -100,8 +177,16 @@ const macSpawnDownload = (params: DownloadParams): Promise<void> => {
|
||||
}
|
||||
|
||||
const total = "1000";
|
||||
const progress: DownloadProgress = { id, cur, total, speed };
|
||||
event.emit("download-progress", progress);
|
||||
// FIXME: 无法获取是否为直播流
|
||||
const progress: DownloadProgress = {
|
||||
id,
|
||||
type: "progress",
|
||||
cur,
|
||||
total,
|
||||
speed,
|
||||
isLive,
|
||||
};
|
||||
callback(progress);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// 创建 axios 实例,并指定自定义的 HTTPS Agent
|
||||
const instance = axios.create({
|
||||
httpsAgent: agent,
|
||||
});
|
||||
|
||||
export default instance;
|
||||
@@ -28,3 +28,4 @@ export * from "./variables";
|
||||
export { on, handle } from "./decorator";
|
||||
export { spawnDownload } from "./download";
|
||||
export { convertToAudio } from "./ffmpeg";
|
||||
export { default as http } from "./http";
|
||||
|
||||
@@ -12,4 +12,13 @@ export function stripColors(str: string) {
|
||||
return str.replace(colorRegex, "");
|
||||
}
|
||||
|
||||
export function formatHeaders(headersStr: string): string {
|
||||
const headers: Record<string, any> | null = JSON.parse(headersStr);
|
||||
if (!headers) return "";
|
||||
const formatted = Object.entries(headers)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join("|");
|
||||
return formatted;
|
||||
}
|
||||
|
||||
export const event = new EventEmitter();
|
||||
|
||||
@@ -15,12 +15,17 @@ export const PERSIST_MEDIAGO = "persist:mediago";
|
||||
export const PERSIST_WEBVIEW = "persist:webview";
|
||||
export const db = path.resolve(workspace, "app.db");
|
||||
export const macDownloaderPath = path.resolve(__bin__, "N_m3u8DL-RE");
|
||||
export const winDownloaderPath = path.resolve(
|
||||
__bin__,
|
||||
"N_m3u8DL-CLI_v3.0.2.exe"
|
||||
);
|
||||
export const winDownloaderPath = path.resolve(__bin__, "N_m3u8DL-CLI.exe");
|
||||
export const ffmpegPath =
|
||||
process.platform === "win32"
|
||||
? path.resolve(__bin__, "ffmpeg.exe")
|
||||
: path.resolve(__bin__, "ffmpeg");
|
||||
export const mobilePath = path.resolve(app.getAppPath(), "../mobile");
|
||||
export const mobilePath = isDev
|
||||
? path.resolve(app.getAppPath(), "../../mobile")
|
||||
: path.resolve(app.getAppPath(), "../mobile");
|
||||
|
||||
// user agent
|
||||
export const pcUA =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36";
|
||||
export const mobileUA =
|
||||
"Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
declare module "*.svg";
|
||||
declare module "*.png";
|
||||
declare module "*.jpg";
|
||||
declare module "*.jpeg";
|
||||
declare module "*.gif";
|
||||
declare module "*.bmp";
|
||||
declare module "*.tiff";
|
||||
@@ -25,9 +25,7 @@ const start = async (): Promise<void> => {
|
||||
mediago.init();
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
// empty
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type BrowserWindow } from "electron";
|
||||
import { type BrowserWindow, BrowserView } from "electron";
|
||||
import { ElectronLog } from "electron-log";
|
||||
import Store from "electron-store";
|
||||
import { Favorite } from "entity/Favorite";
|
||||
@@ -8,7 +8,6 @@ import { AppStore } from "main";
|
||||
import { DataSource, EntityManager, UpdateResult, DeleteResult } from "typeorm";
|
||||
|
||||
export interface MainWindowService {
|
||||
show: boolean;
|
||||
window: BrowserWindow | null;
|
||||
init: () => void;
|
||||
}
|
||||
@@ -18,7 +17,6 @@ export interface PlayerWindowService {
|
||||
}
|
||||
|
||||
export interface BrowserWindowService {
|
||||
show: boolean;
|
||||
window: BrowserWindow | null;
|
||||
showWindow: () => void;
|
||||
hideWindow: () => void;
|
||||
@@ -64,6 +62,7 @@ export interface DownloadItem {
|
||||
id?: number;
|
||||
name: string;
|
||||
url: string;
|
||||
headers: string;
|
||||
}
|
||||
|
||||
export enum DownloadFilter {
|
||||
@@ -94,6 +93,11 @@ export interface VideoRepository {
|
||||
findWattingAndDownloadingVideos: () => Promise<Video[]>;
|
||||
deleteDownloadItem: (id: number) => Promise<DeleteResult>;
|
||||
findAllVideos(): Promise<Video[]>;
|
||||
changeVideoIsLive: (
|
||||
id: number | number[],
|
||||
isLive: boolean
|
||||
) => Promise<UpdateResult>;
|
||||
findVideoByUrl: (url: string) => Promise<Video | null>;
|
||||
}
|
||||
|
||||
export interface FavoriteRepository {
|
||||
@@ -103,6 +107,7 @@ export interface FavoriteRepository {
|
||||
}
|
||||
|
||||
export interface WebviewService {
|
||||
view: BrowserView;
|
||||
init: () => void;
|
||||
getBounds: () => Electron.Rectangle;
|
||||
setAutoResize: (options: Electron.AutoResizeOptions) => void;
|
||||
@@ -116,6 +121,7 @@ export interface WebviewService {
|
||||
show: () => void;
|
||||
setProxy: (useProxy: boolean, proxy: string) => void;
|
||||
setBlocking: (enableBlocking: boolean) => void;
|
||||
setUserAgent: (isMobile: boolean) => void;
|
||||
}
|
||||
|
||||
export interface WebService {
|
||||
@@ -144,9 +150,11 @@ export type Task = {
|
||||
|
||||
export interface DownloadProgress {
|
||||
id: number;
|
||||
type: string;
|
||||
cur: string;
|
||||
total: string;
|
||||
speed: string;
|
||||
isLive: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadParams {
|
||||
@@ -154,8 +162,10 @@ export interface DownloadParams {
|
||||
url: string;
|
||||
local: string;
|
||||
name: string;
|
||||
headers: string;
|
||||
abortSignal: AbortController;
|
||||
deleteSegments?: boolean;
|
||||
callback: (progress: DownloadProgress) => void;
|
||||
}
|
||||
|
||||
export interface DevToolsService {
|
||||
|
||||
@@ -4,7 +4,6 @@ import WebviewController from "controller/WebviewController";
|
||||
import { Container } from "inversify";
|
||||
import FavoriteRepositoryImpl from "repository/favoriteRepositoryImpl";
|
||||
import VideoRepositoryImpl from "repository/videoRepositoryImpl";
|
||||
import BrowserWindowServiceImpl from "services/BrowserWindowServiceImpl";
|
||||
import DatabaseServiceImpl from "services/DatabaseServiceImpl";
|
||||
import DevToolsServiceImpl from "services/DevToolsServiceImpl";
|
||||
import DownloadServiceImpl from "services/DownloadServiceImpl";
|
||||
@@ -31,24 +30,20 @@ import {
|
||||
} from "./interfaces";
|
||||
import IpcHandlerServiceImpl from "./services/IpcHandlerServiceImpl";
|
||||
import LoggerServiceImpl from "./services/LoggerServiceImpl";
|
||||
import MainWindowServiceImpl from "./services/MainWindowServiceImpl";
|
||||
import ProtocolServiceImpl from "./services/ProtocolServiceImpl";
|
||||
import UpdateServiceImpl from "./services/UpdateServiceImpl";
|
||||
import { TYPES } from "./types";
|
||||
import WebServiceImpl from "services/WebServiceImpl";
|
||||
import PlayerWindowServiceImpl from "services/PlayerWindowServiceImpl";
|
||||
import MainWindow from "./windows/Main";
|
||||
import PlayerWindow from "windows/Player";
|
||||
import BrowserWin from "windows/Browser";
|
||||
|
||||
const container = new Container({
|
||||
skipBaseClassChecks: true,
|
||||
defaultScope: "Singleton",
|
||||
autoBindInjectable: true,
|
||||
});
|
||||
container
|
||||
.bind<MainWindowService>(TYPES.MainWindowService)
|
||||
.to(MainWindowServiceImpl);
|
||||
container
|
||||
.bind<BrowserWindowService>(TYPES.BrowserWindowService)
|
||||
.to(BrowserWindowServiceImpl);
|
||||
|
||||
container.bind<App>(TYPES.App).to(ElectronApp);
|
||||
container
|
||||
.bind<IpcHandlerService>(TYPES.IpcHandlerService)
|
||||
@@ -64,9 +59,11 @@ container
|
||||
.to(DownloadServiceImpl);
|
||||
container.bind<DevToolsService>(TYPES.DevToolsService).to(DevToolsServiceImpl);
|
||||
container.bind<WebService>(TYPES.WebService).to(WebServiceImpl);
|
||||
container
|
||||
.bind<PlayerWindowService>(TYPES.PlayerWindowService)
|
||||
.to(PlayerWindowServiceImpl);
|
||||
|
||||
// windows
|
||||
container.bind<MainWindowService>(TYPES.MainWindowService).to(MainWindow);
|
||||
container.bind<BrowserWindowService>(TYPES.BrowserWindowService).to(BrowserWin);
|
||||
container.bind<PlayerWindowService>(TYPES.PlayerWindowService).to(PlayerWindow);
|
||||
|
||||
// === controller
|
||||
container.bind<Controller>(TYPES.Controller).to(HomeController);
|
||||
|
||||
@@ -16,7 +16,7 @@ declare interface EnvPath {
|
||||
|
||||
declare interface BrowserWindowInitialVal {
|
||||
url?: string;
|
||||
sourceList?: LinkMessage[];
|
||||
sourceList?: WebSource[];
|
||||
}
|
||||
|
||||
declare interface ElectronAPI {
|
||||
@@ -57,11 +57,13 @@ declare interface ElectronAPI {
|
||||
openBrowser: (url: string) => Promise<void>;
|
||||
getSharedState: () => Promise<any>;
|
||||
setSharedState: (state: any) => Promise<void>;
|
||||
setUserAgent: (isMobile: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
declare interface LinkMessage {
|
||||
declare interface WebSource {
|
||||
url: string;
|
||||
title: string;
|
||||
name: string;
|
||||
headers: string;
|
||||
}
|
||||
|
||||
declare interface AppStore {
|
||||
@@ -82,9 +84,15 @@ declare interface AppStore {
|
||||
blockAds: boolean;
|
||||
// 主题
|
||||
theme: "system" | "light" | "dark";
|
||||
// 使用浏览器插件
|
||||
useExtension: boolean;
|
||||
// 是否使用手机UA
|
||||
isMobile: boolean;
|
||||
// 最大同时下载数
|
||||
maxRunner: number;
|
||||
}
|
||||
|
||||
declare interface BrowserStore {
|
||||
url: string;
|
||||
sourceList: LinkMessage[];
|
||||
sourceList: WebSource[];
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ const handleApi: ElectronAPI = {
|
||||
openBrowser: (url: string) => shell.openExternal(url),
|
||||
getSharedState: () => ipcRenderer.invoke("get-shared-state"),
|
||||
setSharedState: (state) => ipcRenderer.invoke("set-shared-state", state),
|
||||
setUserAgent: (isMobile: boolean) =>
|
||||
ipcRenderer.invoke("webview-change-user-agent", isMobile),
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld(apiKey, handleApi);
|
||||
|
||||
@@ -25,6 +25,7 @@ export default class VideoRepositoryImpl implements VideoRepository {
|
||||
const item = new Video();
|
||||
item.name = video.name;
|
||||
item.url = video.url;
|
||||
item.headers = video.headers;
|
||||
return await this.dataService.manager.save(item);
|
||||
}
|
||||
|
||||
@@ -95,6 +96,16 @@ export default class VideoRepositoryImpl implements VideoRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
async changeVideoIsLive(id: number | number[], isLive: boolean) {
|
||||
const ids = !Array.isArray(id) ? [id] : id;
|
||||
return this.dataService.appDataSource
|
||||
.createQueryBuilder()
|
||||
.update(Video)
|
||||
.set({ isLive })
|
||||
.where({ id: In(ids) })
|
||||
.execute();
|
||||
}
|
||||
|
||||
async findWattingAndDownloadingVideos() {
|
||||
return await this.dataService.appDataSource.getRepository(Video).find({
|
||||
where: {
|
||||
@@ -106,4 +117,10 @@ export default class VideoRepositoryImpl implements VideoRepository {
|
||||
async deleteDownloadItem(id: number) {
|
||||
return await this.dataService.appDataSource.getRepository(Video).delete(id);
|
||||
}
|
||||
|
||||
async findVideoByUrl(url: string) {
|
||||
return this.dataService.appDataSource.getRepository(Video).findOneBy({
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { inject, injectable } from "inversify";
|
||||
import {
|
||||
DownloadService,
|
||||
DownloadStatus,
|
||||
StoreService,
|
||||
Task,
|
||||
VideoRepository,
|
||||
} from "../interfaces";
|
||||
@@ -18,7 +19,7 @@ export default class DownloadServiceImpl
|
||||
|
||||
private active: Task[] = [];
|
||||
|
||||
private limit = 2;
|
||||
private limit: number;
|
||||
|
||||
private debug = process.env.APP_DOWNLOAD_DEBUG;
|
||||
|
||||
@@ -28,9 +29,18 @@ export default class DownloadServiceImpl
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerServiceImpl,
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService
|
||||
) {
|
||||
super();
|
||||
|
||||
const maxRunner = this.storeService.get("maxRunner");
|
||||
this.limit = maxRunner;
|
||||
|
||||
this.storeService.onDidChange("maxRunner", (maxRunner) => {
|
||||
maxRunner && (this.limit = maxRunner);
|
||||
});
|
||||
}
|
||||
|
||||
async addTask(task: Task) {
|
||||
@@ -60,6 +70,16 @@ export default class DownloadServiceImpl
|
||||
...task.params,
|
||||
id: task.id,
|
||||
abortSignal: controller,
|
||||
callback: (progress) => {
|
||||
if (progress.type === "progress") {
|
||||
this.emit("download-progress", progress);
|
||||
} else if (progress.type === "ready") {
|
||||
this.emit("download-ready-start", progress);
|
||||
if (progress.isLive) {
|
||||
this.removeTask(progress.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
delete this.signal[task.id];
|
||||
this.log(`taskId: ${task.id} success`);
|
||||
@@ -87,11 +107,8 @@ export default class DownloadServiceImpl
|
||||
this.emit("download-failed", task.id, err);
|
||||
}
|
||||
} finally {
|
||||
// 处理当前正在活动的任务
|
||||
const doneId = this.active.findIndex((i) => i.id === task.id);
|
||||
this.active.splice(doneId, 1);
|
||||
// 处理完成的任务
|
||||
this.runTask();
|
||||
this.removeTask(task.id);
|
||||
|
||||
// 传输完成
|
||||
if (this.queue.length === 0 && this.active.length === 0) {
|
||||
// this.emit("download-finish");
|
||||
@@ -99,6 +116,16 @@ export default class DownloadServiceImpl
|
||||
}
|
||||
}
|
||||
|
||||
removeTask(id: number) {
|
||||
// 处理当前正在活动的任务
|
||||
const doneId = this.active.findIndex((i) => i.id === id);
|
||||
this.active.splice(doneId, 1);
|
||||
// 处理完成的任务
|
||||
if (this.active.length < this.limit) {
|
||||
this.runTask();
|
||||
}
|
||||
}
|
||||
|
||||
runTask() {
|
||||
while (this.active.length < this.limit && this.queue.length > 0) {
|
||||
const task = this.queue.shift();
|
||||
|
||||
@@ -25,6 +25,9 @@ export default class StoreServiceImpl
|
||||
openInNewWindow: false,
|
||||
blockAds: true,
|
||||
theme: AppTheme.System,
|
||||
useExtension: false,
|
||||
isMobile: false,
|
||||
maxRunner: 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,14 +10,18 @@ export default class UpdateServiceImpl implements UpdateService {
|
||||
@inject(TYPES.LoggerService) private readonly logger: LoggerService
|
||||
) {}
|
||||
|
||||
init(): void {
|
||||
async init(): Promise<void> {
|
||||
if (isDev) return;
|
||||
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
autoUpdater.logger = this.logger.logger;
|
||||
autoUpdater.checkForUpdatesAndNotify({
|
||||
title: "自动更新完成",
|
||||
body: "下次重启时将会自动安装",
|
||||
});
|
||||
try {
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
autoUpdater.logger = this.logger.logger;
|
||||
await autoUpdater.checkForUpdatesAndNotify({
|
||||
title: "自动更新完成",
|
||||
body: "下次重启时将会自动安装",
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.info("update error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { StoreService, VideoRepository, WebService } from "../interfaces";
|
||||
import {
|
||||
LoggerService,
|
||||
StoreService,
|
||||
VideoRepository,
|
||||
WebService,
|
||||
} from "../interfaces";
|
||||
import { inject, injectable } from "inversify";
|
||||
import Koa, { Context } from "koa";
|
||||
import Router from "@koa/router";
|
||||
@@ -20,7 +25,9 @@ export default class WebServiceImpl implements WebService {
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerService
|
||||
) {
|
||||
this.app = new Koa();
|
||||
this.router = new Router();
|
||||
@@ -37,18 +44,22 @@ export default class WebServiceImpl implements WebService {
|
||||
this.app.use(cors());
|
||||
this.app.use(range);
|
||||
this.app.use(serve(local));
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
this.app.use(
|
||||
serve(mobilePath, {
|
||||
extensions: ["html", "js", "css"],
|
||||
index: "index.html",
|
||||
})
|
||||
);
|
||||
}
|
||||
this.app.use(
|
||||
serve(mobilePath, {
|
||||
extensions: ["html", "js", "css"],
|
||||
index: "index.html",
|
||||
})
|
||||
);
|
||||
this.app.use(this.router.routes());
|
||||
this.app.use(this.router.allowedMethods());
|
||||
|
||||
this.app.listen(process.env.APP_SERVER_PORT);
|
||||
this.app
|
||||
.listen(process.env.APP_SERVER_PORT, () => {
|
||||
this.logger.info("web server init success.");
|
||||
})
|
||||
.on("error", (err: unknown) => {
|
||||
this.logger.error("server error", err);
|
||||
});
|
||||
}
|
||||
|
||||
private videoList = async (ctx: Context) => {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
BrowserView,
|
||||
HeadersReceivedResponse,
|
||||
OnHeadersReceivedListenerDetails,
|
||||
OnResponseStartedListenerDetails,
|
||||
WebContents,
|
||||
CallbackResponse,
|
||||
OnBeforeSendHeadersListenerDetails,
|
||||
session,
|
||||
} from "electron";
|
||||
import {
|
||||
@@ -11,20 +9,24 @@ import {
|
||||
LoggerService,
|
||||
MainWindowService,
|
||||
StoreService,
|
||||
VideoRepository,
|
||||
WebviewService,
|
||||
} from "../interfaces";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "../types";
|
||||
import isDev from "electron-is-dev";
|
||||
import { PERSIST_WEBVIEW } from "helper/variables";
|
||||
import { LinkMessage } from "main";
|
||||
import { PERSIST_WEBVIEW, mobileUA, pcUA } from "helper/variables";
|
||||
import { ElectronBlocker } from "@cliqz/adblocker-electron";
|
||||
import fetch from "cross-fetch";
|
||||
import path from "path";
|
||||
import { WebSource } from "main";
|
||||
|
||||
// FIXME: 需要重构
|
||||
@injectable()
|
||||
export default class WebviewServiceImpl implements WebviewService {
|
||||
private _view: BrowserView;
|
||||
public view: BrowserView;
|
||||
private blocker?: ElectronBlocker;
|
||||
private sources = new Set();
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.MainWindowService)
|
||||
@@ -34,57 +36,34 @@ export default class WebviewServiceImpl implements WebviewService {
|
||||
@inject(TYPES.BrowserWindowService)
|
||||
private readonly browserWindow: BrowserWindowService,
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService
|
||||
private readonly storeService: StoreService,
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository
|
||||
) {
|
||||
// 初始化 blocker
|
||||
// this.initBlocker();
|
||||
this.initBlocker();
|
||||
}
|
||||
|
||||
private create(): void {
|
||||
this._view = new BrowserView({
|
||||
webPreferences: {
|
||||
partition: PERSIST_WEBVIEW,
|
||||
},
|
||||
});
|
||||
const webContents = this._view.webContents;
|
||||
this._view.setBackgroundColor("#fff");
|
||||
webContents.setAudioMuted(true);
|
||||
|
||||
const useProxy = this.storeService.get("useProxy");
|
||||
const proxy = this.storeService.get("proxy");
|
||||
this.setProxy(useProxy, proxy);
|
||||
}
|
||||
|
||||
get view(): BrowserView {
|
||||
if (!this._view) this.create();
|
||||
return this._view;
|
||||
}
|
||||
|
||||
onHeadersReceived = (details: OnResponseStartedListenerDetails): void => {
|
||||
const { url } = details;
|
||||
|
||||
const sourceReg = /\.m3u8$/;
|
||||
const detailsUrl = new URL(url);
|
||||
|
||||
if (sourceReg.test(detailsUrl.pathname)) {
|
||||
this.logger.info("在窗口中捕获 m3u8 链接: ", detailsUrl.toString());
|
||||
const webContents = details.webContents;
|
||||
const linkMessage: LinkMessage = {
|
||||
url: detailsUrl.toString(),
|
||||
title: webContents?.getTitle() || "没有获取到名称",
|
||||
};
|
||||
this.curWindow?.webContents.send("webview-link-message", linkMessage);
|
||||
}
|
||||
};
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.view = new BrowserView({
|
||||
webPreferences: {
|
||||
partition: PERSIST_WEBVIEW,
|
||||
preload: path.resolve(__dirname, "./webview.js"),
|
||||
},
|
||||
});
|
||||
this.view.setBackgroundColor("#fff");
|
||||
this.view.webContents.setAudioMuted(true);
|
||||
|
||||
const { useProxy, proxy, isMobile } = this.storeService.store;
|
||||
this.setProxy(useProxy, proxy);
|
||||
this.setUserAgent(isMobile);
|
||||
|
||||
this.view.webContents.on("dom-ready", () => {
|
||||
this.sources.clear();
|
||||
const title = this.view.webContents.getTitle();
|
||||
const url = this.view.webContents.getURL();
|
||||
|
||||
this.curWindow?.webContents.send("webview-dom-ready", { title, url });
|
||||
});
|
||||
|
||||
this.view.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url === "about:blank") {
|
||||
// 兼容一些网站跳转到 about:blank
|
||||
@@ -98,10 +77,8 @@ export default class WebviewServiceImpl implements WebviewService {
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
this.session.webRequest.onResponseStarted(
|
||||
{ urls: ["<all_urls>"] },
|
||||
this.onHeadersReceived
|
||||
);
|
||||
const urls = ["<all_urls>"];
|
||||
this.session.webRequest.onBeforeSendHeaders({ urls }, this.before);
|
||||
}
|
||||
|
||||
getBounds(): Electron.Rectangle {
|
||||
@@ -137,11 +114,13 @@ export default class WebviewServiceImpl implements WebviewService {
|
||||
const canGoBack = this.view.webContents.canGoBack();
|
||||
try {
|
||||
await this.view.webContents.loadURL(url || "");
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
this.logger.error("加载 url 时出现错误: ", err);
|
||||
}
|
||||
if (!canGoBack && !isNewWindow) {
|
||||
this.view.webContents.clearHistory();
|
||||
throw err;
|
||||
} finally {
|
||||
if (!canGoBack && !isNewWindow) {
|
||||
this.view.webContents.clearHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,12 +142,8 @@ export default class WebviewServiceImpl implements WebviewService {
|
||||
}
|
||||
|
||||
get curWindow() {
|
||||
if (this.browserWindow.window && this.browserWindow.show) {
|
||||
return this.browserWindow.window;
|
||||
}
|
||||
if (this.mainWindow.window && this.mainWindow.show) {
|
||||
return this.mainWindow.window;
|
||||
}
|
||||
if (this.browserWindow.window) return this.browserWindow.window;
|
||||
if (this.mainWindow.window) return this.mainWindow.window;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -239,4 +214,64 @@ export default class WebviewServiceImpl implements WebviewService {
|
||||
this.blocker.disableBlockingInSession(this.session);
|
||||
this.logger.info("关闭 blocker 成功");
|
||||
}
|
||||
|
||||
before = (
|
||||
details: OnBeforeSendHeadersListenerDetails,
|
||||
callback: (response: CallbackResponse) => void
|
||||
): void => {
|
||||
const { url } = details;
|
||||
|
||||
const sourceReg = /\.m3u8$/;
|
||||
const detailsUrl = new URL(url);
|
||||
|
||||
if (sourceReg.test(detailsUrl.pathname)) {
|
||||
try {
|
||||
this.handleM3u8(details);
|
||||
} catch (e) {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
|
||||
callback({});
|
||||
};
|
||||
|
||||
handleM3u8 = async (
|
||||
details: OnBeforeSendHeadersListenerDetails
|
||||
): Promise<void> => {
|
||||
const { id, url } = details;
|
||||
|
||||
this.logger.info(`在窗口中捕获 m3u8 链接: ${url} id: ${id}`);
|
||||
const webContents = details.webContents;
|
||||
|
||||
const source: WebSource = {
|
||||
url,
|
||||
name: webContents?.getTitle() || "没有获取到名称",
|
||||
headers: JSON.stringify(details.requestHeaders),
|
||||
};
|
||||
// 这里需要判断是否使用浏览器插件
|
||||
const useExtension = this.storeService.get("useExtension");
|
||||
|
||||
if (!this.sources.has(source.url)) {
|
||||
this.sources.add(source.url);
|
||||
if (useExtension) {
|
||||
this.view.webContents.send("webview-link-message", source);
|
||||
} else {
|
||||
const item = await this.videoRepository.addVideo(source);
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
this.mainWindow.window?.webContents.send(
|
||||
"download-item-notifier",
|
||||
item
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setUserAgent(isMobile?: boolean) {
|
||||
if (isMobile) {
|
||||
this.view.webContents.setUserAgent(mobileUA);
|
||||
} else {
|
||||
this.view.webContents.setUserAgent(pcUA);
|
||||
}
|
||||
this.logger.info("设置 user-agent 成功", isMobile);
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 625 B |
@@ -0,0 +1,107 @@
|
||||
import { WebSource } from "main";
|
||||
import { IpcRendererEvent, ipcRenderer } from "electron/renderer";
|
||||
|
||||
// 创建浮窗容器元素
|
||||
const floatingContainer = document.createElement("div");
|
||||
floatingContainer.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 200px;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
// 创建浮窗按钮元素
|
||||
const floatingButton = document.createElement("div");
|
||||
floatingButton.textContent = "检测到视频资源……";
|
||||
floatingButton.style.cssText = `
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
`;
|
||||
|
||||
// 创建浮窗列表元素
|
||||
const floatingList = document.createElement("ul");
|
||||
floatingList.style.cssText = `
|
||||
display: none;
|
||||
background-color: #000;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const itemStyle = `
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const items: WebSource[] = [];
|
||||
ipcRenderer.on(
|
||||
"webview-link-message",
|
||||
(e: IpcRendererEvent, data: WebSource) => {
|
||||
floatingContainer.style.display = "block";
|
||||
|
||||
items.push(data);
|
||||
|
||||
// 清空列表
|
||||
while (floatingList?.firstChild) {
|
||||
floatingList.removeChild(floatingList.firstChild);
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
// 创建列表项元素
|
||||
const elItem = document.createElement("li");
|
||||
elItem.textContent = item.name;
|
||||
elItem.title = item.name;
|
||||
elItem.style.cssText = itemStyle;
|
||||
elItem.addEventListener("click", () => {
|
||||
ipcRenderer.invoke("add-download-item", {
|
||||
name: data.name,
|
||||
url: data.url,
|
||||
});
|
||||
});
|
||||
elItem.addEventListener("mouseenter", () => {
|
||||
elItem.style.backgroundColor = "#fff";
|
||||
elItem.style.color = "#000";
|
||||
});
|
||||
elItem.addEventListener("mouseleave", () => {
|
||||
elItem.style.backgroundColor = "#000";
|
||||
elItem.style.color = "#fff";
|
||||
});
|
||||
|
||||
// 将列表项添加到浮窗列表
|
||||
floatingList.appendChild(elItem);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 将按钮和列表添加到浮窗容器
|
||||
floatingContainer.appendChild(floatingList);
|
||||
floatingContainer.appendChild(floatingButton);
|
||||
|
||||
// 鼠标进入浮窗按钮时的处理函数
|
||||
floatingContainer.addEventListener("mouseenter", () => {
|
||||
floatingButton.style.opacity = "1";
|
||||
floatingList.style.display = "block";
|
||||
});
|
||||
|
||||
// 鼠标离开浮窗按钮时的处理函数
|
||||
floatingContainer.addEventListener("mouseleave", () => {
|
||||
floatingButton.style.opacity = "0.8";
|
||||
floatingList.style.display = "none";
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// 将浮窗容器添加到body元素
|
||||
document.body.appendChild(floatingContainer);
|
||||
});
|
||||
@@ -1,23 +1,27 @@
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
|
||||
import isDev from "electron-is-dev";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { resolve } from "path";
|
||||
import { TYPES } from "../types";
|
||||
import { BrowserWindowService, StoreService } from "../interfaces";
|
||||
import _ from "lodash";
|
||||
import Window from "./window";
|
||||
|
||||
@injectable()
|
||||
export default class BrowserWindowServiceImpl implements BrowserWindowService {
|
||||
window: BrowserWindow | null = null;
|
||||
private options: BrowserWindowConstructorOptions;
|
||||
export default class BrowserWindowServiceImpl
|
||||
extends Window
|
||||
implements BrowserWindowService
|
||||
{
|
||||
url = isDev
|
||||
? "http://localhost:8555/browser"
|
||||
: "mediago://index.html/browser";
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService
|
||||
) {
|
||||
this.options = {
|
||||
super({
|
||||
width: 1100,
|
||||
minWidth: 1100,
|
||||
minWidth: 414,
|
||||
height: 680,
|
||||
minHeight: 680,
|
||||
show: false,
|
||||
@@ -25,35 +29,29 @@ export default class BrowserWindowServiceImpl implements BrowserWindowService {
|
||||
webPreferences: {
|
||||
preload: resolve(__dirname, "./preload.js"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get show() {
|
||||
return !!this.window && !this.window.isDestroyed();
|
||||
}
|
||||
|
||||
private create() {
|
||||
const window = new BrowserWindow(this.options);
|
||||
|
||||
const url = isDev
|
||||
? "http://localhost:8555/browser"
|
||||
: "mediago://index.html/browser";
|
||||
void window.loadURL(url);
|
||||
|
||||
this.storeService.onDidChange("openInNewWindow", (newValue) => {
|
||||
// 向所有窗口发送通知
|
||||
if (newValue === false) {
|
||||
if (window && !window.isDestroyed()) {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.on("resized", this.handleResize);
|
||||
|
||||
return window;
|
||||
this.storeService.onDidChange("openInNewWindow", this.handleNewWindowsVal);
|
||||
this.storeService.onDidAnyChange(this.storeChange);
|
||||
}
|
||||
|
||||
storeChange = (store: any) => {
|
||||
if (!this.window) return;
|
||||
// 向所有窗口发送通知
|
||||
this.window.webContents.send("store-change", store);
|
||||
};
|
||||
|
||||
handleNewWindowsVal = (newValue: any) => {
|
||||
if (!this.window) return;
|
||||
|
||||
// 向所有窗口发送通知
|
||||
if (newValue === false) {
|
||||
if (this.window && !this.window.isDestroyed()) {
|
||||
this.window.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleResize = () => {
|
||||
if (!this.window) return;
|
||||
|
||||
@@ -62,8 +60,9 @@ export default class BrowserWindowServiceImpl implements BrowserWindowService {
|
||||
};
|
||||
|
||||
showWindow = () => {
|
||||
if (!this.window || this.window.isDestroyed()) {
|
||||
if (!this.window) {
|
||||
this.window = this.create();
|
||||
this.window.on("resized", this.handleResize);
|
||||
}
|
||||
|
||||
this.window.show();
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
BrowserWindow,
|
||||
BrowserWindowConstructorOptions,
|
||||
Menu,
|
||||
Notification,
|
||||
app,
|
||||
} from "electron";
|
||||
import { Menu, Notification, app } from "electron";
|
||||
import isDev from "electron-is-dev";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { resolve } from "path";
|
||||
@@ -19,13 +13,14 @@ import {
|
||||
} from "../interfaces";
|
||||
import { event } from "helper/utils";
|
||||
import _ from "lodash";
|
||||
import Window from "./window";
|
||||
|
||||
@injectable()
|
||||
export default class MainWindowServiceImpl implements MainWindowService {
|
||||
private readonly options: BrowserWindowConstructorOptions;
|
||||
|
||||
window: BrowserWindow | null = null;
|
||||
|
||||
export default class MainWindowServiceImpl
|
||||
extends Window
|
||||
implements MainWindowService
|
||||
{
|
||||
url = isDev ? "http://localhost:8555/" : "mediago://index.html/";
|
||||
constructor(
|
||||
@inject(TYPES.LoggerService)
|
||||
private readonly logger: LoggerService,
|
||||
@@ -36,7 +31,7 @@ export default class MainWindowServiceImpl implements MainWindowService {
|
||||
@inject(TYPES.StoreService)
|
||||
private readonly storeService: StoreService
|
||||
) {
|
||||
this.options = {
|
||||
super({
|
||||
width: 1100,
|
||||
minWidth: 1100,
|
||||
height: 680,
|
||||
@@ -46,59 +41,41 @@ export default class MainWindowServiceImpl implements MainWindowService {
|
||||
webPreferences: {
|
||||
preload: resolve(__dirname, "./preload.js"),
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
create(): BrowserWindow {
|
||||
if (this.window && !this.window.isDestroyed()) {
|
||||
return this.window;
|
||||
}
|
||||
|
||||
const window = new BrowserWindow(this.options);
|
||||
const url = isDev ? "http://localhost:8555/" : "mediago://index.html/";
|
||||
void window.loadURL(url);
|
||||
|
||||
window.once("ready-to-show", this.readyToShow);
|
||||
event.on("download-progress", this.onDownloadProgress);
|
||||
this.downloadService.on("download-ready-start", this.onDownloadReadyStart);
|
||||
this.downloadService.on("download-progress", this.onDownloadProgress);
|
||||
this.downloadService.on("download-success", this.onDownloadSuccess);
|
||||
this.downloadService.on("download-failed", this.onDownloadFailed);
|
||||
this.downloadService.on("download-start", this.onDownloadStart);
|
||||
this.downloadService.on("download-stop", this.onDownloadStart);
|
||||
|
||||
this.storeService.onDidAnyChange((store) => {
|
||||
// 向所有窗口发送通知
|
||||
window.webContents.send("store-change", store);
|
||||
});
|
||||
|
||||
// 处理当前窗口改变大小
|
||||
window.on("resized", this.handleResize);
|
||||
|
||||
app.on("second-instance", () => {
|
||||
if (process.platform === "win32") {
|
||||
if (window) {
|
||||
if (window.isMinimized()) {
|
||||
window.restore();
|
||||
}
|
||||
if (window.isVisible()) {
|
||||
window.focus();
|
||||
} else {
|
||||
window.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return window;
|
||||
this.storeService.onDidAnyChange(this.storeChange);
|
||||
app.on("second-instance", this.secondInstance);
|
||||
}
|
||||
|
||||
get show() {
|
||||
return !!this.window && !this.window.isDestroyed();
|
||||
}
|
||||
onDownloadReadyStart = ({ id, isLive }: { id: number; isLive: boolean }) => {
|
||||
this.videoRepository.changeVideoIsLive(id, isLive);
|
||||
this.send("change-video-is-live", { id, isLive });
|
||||
};
|
||||
|
||||
init(): void {
|
||||
if (this.window) {
|
||||
// 如果窗口已经存在,则直接显示
|
||||
this.window.show();
|
||||
return;
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(null);
|
||||
|
||||
this.window = this.create();
|
||||
|
||||
const mainBounds = this.storeService.get("mainBounds");
|
||||
if (mainBounds) {
|
||||
this.window.setBounds(mainBounds);
|
||||
}
|
||||
|
||||
// 处理当前窗口改变大小
|
||||
this.window.on("resized", this.handleResize);
|
||||
}
|
||||
|
||||
handleResize = () => {
|
||||
@@ -108,27 +85,32 @@ export default class MainWindowServiceImpl implements MainWindowService {
|
||||
this.storeService.set("mainBounds", _.omit(bounds, ["x", "y"]));
|
||||
};
|
||||
|
||||
readyToShow = () => {
|
||||
secondInstance = () => {
|
||||
if (!this.window) return;
|
||||
|
||||
this.window.show();
|
||||
isDev && this.window.webContents.openDevTools();
|
||||
|
||||
const mainBounds = this.storeService.get("mainBounds");
|
||||
if (mainBounds) {
|
||||
this.window.setBounds(mainBounds);
|
||||
if (process.platform === "win32") {
|
||||
if (this.window) {
|
||||
if (this.window.isMinimized()) {
|
||||
this.window.restore();
|
||||
}
|
||||
if (this.window.isVisible()) {
|
||||
this.window.focus();
|
||||
} else {
|
||||
this.window.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onDownloadProgress = (progress: DownloadProgress) => {
|
||||
if (!this.window) return;
|
||||
storeChange = (store: any) => {
|
||||
// 向所有窗口发送通知
|
||||
this.send("store-change", store);
|
||||
};
|
||||
|
||||
this.window.webContents.send("download-progress", progress);
|
||||
onDownloadProgress = (progress: DownloadProgress) => {
|
||||
this.send("download-progress", progress);
|
||||
};
|
||||
|
||||
onDownloadSuccess = async (id: number) => {
|
||||
if (!this.window) return;
|
||||
|
||||
const promptTone = this.storeService.get("promptTone");
|
||||
if (promptTone) {
|
||||
const video = await this.videoRepository.findVideo(id);
|
||||
@@ -139,12 +121,10 @@ export default class MainWindowServiceImpl implements MainWindowService {
|
||||
}).show();
|
||||
}
|
||||
|
||||
this.window.webContents.send("download-success", id);
|
||||
this.send("download-success", id);
|
||||
};
|
||||
|
||||
onDownloadFailed = async (id: number, err: any) => {
|
||||
if (!this.window) return;
|
||||
|
||||
const promptTone = this.storeService.get("promptTone");
|
||||
if (promptTone) {
|
||||
const video = await this.videoRepository.findVideo(id);
|
||||
@@ -155,18 +135,20 @@ export default class MainWindowServiceImpl implements MainWindowService {
|
||||
}).show();
|
||||
}
|
||||
this.logger.error("下载失败:", err);
|
||||
this.window.webContents.send("download-failed", id);
|
||||
this.send("download-failed", id);
|
||||
};
|
||||
|
||||
onDownloadStart = async (id: number) => {
|
||||
if (!this.window) return;
|
||||
|
||||
this.window.webContents.send("download-start", id);
|
||||
this.send("download-start", id);
|
||||
};
|
||||
|
||||
onDownloadStop = async (id: number) => {
|
||||
this.send("download-stop", id);
|
||||
};
|
||||
|
||||
send(channel: string, ...args: any[]) {
|
||||
if (!this.window) return;
|
||||
|
||||
this.window.webContents.send("download-stop", id);
|
||||
};
|
||||
this.window.webContents.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, Menu } from "electron";
|
||||
import isDev from "electron-is-dev";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { resolve } from "path";
|
||||
import { PlayerWindowService, VideoRepository } from "../interfaces";
|
||||
import { TYPES } from "types";
|
||||
import Window from "./window";
|
||||
|
||||
@injectable()
|
||||
export default class PlayerWindowServiceImpl implements PlayerWindowService {
|
||||
private options: BrowserWindowConstructorOptions;
|
||||
private window: BrowserWindow | null = null;
|
||||
export default class PlayerWindowServiceImpl
|
||||
extends Window
|
||||
implements PlayerWindowService
|
||||
{
|
||||
url = isDev ? "http://localhost:8555/player" : "mediago://index.html/player";
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository
|
||||
) {
|
||||
this.options = {
|
||||
super({
|
||||
width: 1100,
|
||||
minWidth: 1100,
|
||||
height: 680,
|
||||
@@ -24,23 +26,11 @@ export default class PlayerWindowServiceImpl implements PlayerWindowService {
|
||||
webPreferences: {
|
||||
preload: resolve(__dirname, "./preload.js"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
create(): BrowserWindow {
|
||||
const window = new BrowserWindow(this.options);
|
||||
|
||||
Menu.setApplicationMenu(null);
|
||||
|
||||
const url = isDev
|
||||
? "http://localhost:8555/player"
|
||||
: "mediago://index.html/player";
|
||||
void window.loadURL(url);
|
||||
return window;
|
||||
});
|
||||
}
|
||||
|
||||
openWindow = async (id: number) => {
|
||||
if (!this.window || this.window.isDestroyed()) {
|
||||
if (!this.window) {
|
||||
this.window = this.create();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
|
||||
import isDev from "electron-is-dev";
|
||||
|
||||
export default class Window {
|
||||
window: BrowserWindow | null = null;
|
||||
options: BrowserWindowConstructorOptions;
|
||||
url: string;
|
||||
|
||||
constructor(options: BrowserWindowConstructorOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
create() {
|
||||
if (!this.url) {
|
||||
throw new Error("url is required");
|
||||
}
|
||||
|
||||
const window = new BrowserWindow(this.options);
|
||||
void window.loadURL(this.url);
|
||||
|
||||
window.once("ready-to-show", this.readyToShow);
|
||||
window.on("close", this.windowClose);
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
readyToShow = () => {
|
||||
if (!this.window) return;
|
||||
|
||||
this.window.show();
|
||||
isDev && this.window.webContents.openDevTools();
|
||||
};
|
||||
|
||||
windowClose = () => {
|
||||
if (!this.window) return;
|
||||
|
||||
// 防止 webview 同时被销毁
|
||||
this.window.setBrowserView(null);
|
||||
|
||||
// 销毁窗口
|
||||
this.window = null;
|
||||
};
|
||||
}
|
||||
@@ -10,10 +10,10 @@
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "Node",
|
||||
"strictPropertyInitialization": false,
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"outDir": "build/main"
|
||||
"outDir": "app/output/build/main"
|
||||
},
|
||||
"include": ["./src/**/*"]
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript",
|
||||
"@vue/eslint-config-prettier/skip-formatting",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
||||
ecmaVersion: "latest",
|
||||
},
|
||||
rules: {
|
||||
"prettier/prettier": "error",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -13,8 +13,8 @@ TypeScript cannot handle type information for `.vue` imports by default, so we r
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// See here how to get started:
|
||||
// https://playwright.dev/docs/intro
|
||||
test('visits the app root url', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('div.greetings > h1')).toHaveText('You did it!');
|
||||
})
|
||||
test("visits the app root url", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.locator("div.greetings > h1")).toHaveText("You did it!");
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:mobile": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test'
|
||||
import { devices } from '@playwright/test'
|
||||
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||
import { devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
@@ -11,7 +11,7 @@ import { devices } from '@playwright/test'
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './e2e',
|
||||
testDir: "./e2e",
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
@@ -19,7 +19,7 @@ const config: PlaywrightTestConfig = {
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000
|
||||
timeout: 5000,
|
||||
},
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
@@ -28,41 +28,41 @@ const config: PlaywrightTestConfig = {
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:5173',
|
||||
baseURL: "http://localhost:5173",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
trace: "on-first-retry",
|
||||
|
||||
/* Only on CI systems run the tests headless */
|
||||
headless: !!process.env.CI
|
||||
headless: !!process.env.CI,
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices['Desktop Chrome']
|
||||
}
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
name: "firefox",
|
||||
use: {
|
||||
...devices['Desktop Firefox']
|
||||
}
|
||||
...devices["Desktop Firefox"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
name: "webkit",
|
||||
use: {
|
||||
...devices['Desktop Safari']
|
||||
}
|
||||
}
|
||||
...devices["Desktop Safari"],
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
@@ -103,10 +103,10 @@ const config: PlaywrightTestConfig = {
|
||||
* Use the preview server on CI for more realistic testing.
|
||||
Playwright will re-use the local server if there is already a dev-server running.
|
||||
*/
|
||||
command: process.env.CI ? 'vite preview --port 5173' : 'vite dev',
|
||||
command: process.env.CI ? "vite preview --port 5173" : "vite dev",
|
||||
port: 5173,
|
||||
reuseExistingServer: !process.env.CI
|
||||
}
|
||||
}
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
};
|
||||
|
||||
export default config
|
||||
export default config;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
import { RouterView } from "vue-router";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import './assets/main.css'
|
||||
import "./assets/main.css";
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
const app = createApp(App)
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
|
||||
app.mount('#app')
|
||||
app.mount("#app");
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('../views/HomeView.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: () => import("../views/HomeView.vue"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
export const useCounterStore = defineStore("counter", () => {
|
||||
const count = ref(0);
|
||||
const doubleCount = computed(() => count.value * 2);
|
||||
function increment() {
|
||||
count.value++
|
||||
count.value++;
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
return { count, doubleCount, increment };
|
||||
});
|
||||
|
||||
@@ -1,43 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import Player, { type IPlayerOptions } from 'xgplayer'
|
||||
import 'xgplayer/dist/index.min.css'
|
||||
import axios from 'axios'
|
||||
import { onMounted, ref } from "vue";
|
||||
import Player, { type IPlayerOptions } from "xgplayer";
|
||||
import "xgplayer/dist/index.min.css";
|
||||
import axios from "axios";
|
||||
|
||||
interface VideoData {
|
||||
id: number
|
||||
name: string
|
||||
url: string
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const videoRef = ref(null)
|
||||
const player = ref<Player | null>(null)
|
||||
const list = ref<VideoData[]>([])
|
||||
const videoRef = ref(null);
|
||||
const player = ref<Player | null>(null);
|
||||
const list = ref<VideoData[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
let baseUrl = location.href
|
||||
if (import.meta.env.MODE === 'development') {
|
||||
baseUrl = `http://${location.hostname}:8433/`
|
||||
}
|
||||
const res = await axios.get(`${baseUrl}api/video-list`)
|
||||
const res = await axios.get(`/api/video-list`);
|
||||
if (videoRef.value) {
|
||||
const options: IPlayerOptions = {
|
||||
el: videoRef.value,
|
||||
fluid: true,
|
||||
videoInit: true
|
||||
}
|
||||
videoInit: true,
|
||||
};
|
||||
if (Array.isArray(res.data) && res.data.length > 0) {
|
||||
list.value = res.data
|
||||
options.src = list.value[0].url
|
||||
list.value = res.data;
|
||||
options.src = list.value[0].url;
|
||||
}
|
||||
player.value = new Player(options)
|
||||
player.value = new Player(options);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function itemClick(item: VideoData) {
|
||||
if (!player.value) return
|
||||
if (!player.value) return;
|
||||
|
||||
player.value.src = item.url
|
||||
player.value.src = item.url;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -46,7 +42,12 @@ function itemClick(item: VideoData) {
|
||||
<div v-show="list.length" ref="videoRef"></div>
|
||||
<div v-if="list.length">
|
||||
<van-list finished-text="没有更多了">
|
||||
<van-cell v-for="item in list" :key="item.id" :title="item.name" @click="itemClick(item)" />
|
||||
<van-cell
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:title="item.name"
|
||||
@click="itemClick(item)"
|
||||
/>
|
||||
</van-list>
|
||||
</div>
|
||||
<van-empty v-else description="暂无视频" />
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import path from 'path'
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import path from "path";
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { VantResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import vueJsx from "@vitejs/plugin-vue-jsx";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import { VantResolver } from "unplugin-vue-components/resolvers";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -13,21 +13,21 @@ export default defineConfig({
|
||||
vue(),
|
||||
vueJsx(),
|
||||
Components({
|
||||
resolvers: [VantResolver()]
|
||||
})
|
||||
resolvers: [VantResolver()],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
envDir: '../..',
|
||||
envDir: "../..",
|
||||
server: {
|
||||
host: true,
|
||||
port: 8556
|
||||
port: 8556,
|
||||
},
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "../main/build/mobile"),
|
||||
outDir: path.resolve(__dirname, "../main/app/mobile"),
|
||||
emptyOutDir: true,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { mergeConfig } from 'vite'
|
||||
import { configDefaults, defineConfig } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { mergeConfig } from "vite";
|
||||
import { configDefaults, defineConfig } from "vitest/config";
|
||||
import viteConfig from "./vite.config";
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/*'],
|
||||
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||
environment: "jsdom",
|
||||
exclude: [...configDefaults.exclude, "e2e/*"],
|
||||
root: fileURLToPath(new URL("./", import.meta.url)),
|
||||
transformMode: {
|
||||
web: [/\.[jt]sx$/],
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -23,15 +23,14 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.1.1",
|
||||
"react-resizable-panels": "^0.0.51",
|
||||
"react-router-dom": "^6.13.0",
|
||||
"sort-by": "^1.2.0",
|
||||
"xgplayer": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/react": "^18.2.13",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/sort-by": "^1.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||
"@typescript-eslint/parser": "5.60.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import React, { FC, useState } from "react";
|
||||
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Badge, Button, Layout, Menu, MenuProps } from "antd";
|
||||
import "./App.scss";
|
||||
@@ -11,9 +11,8 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import useElectron from "./hooks/electron";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { selectStore, setAppStore } from "./store/appSlice";
|
||||
import { selectAppStore, setAppStore, clearCount, selectCount } from "./store";
|
||||
import { useAsyncEffect } from "ahooks";
|
||||
import { clearCount, selectCount } from "./store/downloadSlice";
|
||||
import { tdApp } from "./utils";
|
||||
|
||||
const { Footer, Sider, Content } = Layout;
|
||||
@@ -26,15 +25,13 @@ const App: FC = () => {
|
||||
openUrl,
|
||||
setAppStore: ipcSetAppStore,
|
||||
showBrowserWindow,
|
||||
rendererEvent,
|
||||
removeEventListener,
|
||||
} = useElectron();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [showExport, setShowExport] = useState(false);
|
||||
const count = useSelector(selectCount);
|
||||
const appStore = useSelector(selectStore);
|
||||
const appStore = useSelector(selectAppStore);
|
||||
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
@@ -101,27 +98,13 @@ const App: FC = () => {
|
||||
},
|
||||
];
|
||||
|
||||
// 监听store变化
|
||||
const onAppStoreChange = (event: any, store: AppStore) => {
|
||||
dispatch(setAppStore(store));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
rendererEvent("store-change", onAppStoreChange);
|
||||
|
||||
return () => {
|
||||
removeEventListener("store-change", onAppStoreChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const finalItems = items.filter((item) =>
|
||||
appStore.openInNewWindow ? item?.key !== "source" : true
|
||||
);
|
||||
|
||||
const openHelpUrl = () => {
|
||||
tdApp.openHelpPage();
|
||||
const url =
|
||||
"https://blog.ziying.site/post/media-downloader-how-to-use/?form=client";
|
||||
const url = "https://downloader.caorushizi.cn/guides.html?form=client";
|
||||
openUrl(url);
|
||||
};
|
||||
|
||||
@@ -132,30 +115,28 @@ const App: FC = () => {
|
||||
|
||||
return (
|
||||
<Layout className="container">
|
||||
<Sider className="container-sider" theme="light">
|
||||
<Menu
|
||||
style={{ height: "100%" }}
|
||||
defaultSelectedKeys={["home"]}
|
||||
mode="vertical"
|
||||
theme="light"
|
||||
items={finalItems}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Sider className="container-sider" theme="light">
|
||||
<Menu
|
||||
style={{ height: "100%" }}
|
||||
defaultSelectedKeys={["home"]}
|
||||
mode="vertical"
|
||||
theme="light"
|
||||
items={finalItems}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Content className="container-inner">
|
||||
<Outlet />
|
||||
</Content>
|
||||
<Footer className="container-footer">
|
||||
<Button
|
||||
type={"link"}
|
||||
onClick={openHelpUrl}
|
||||
icon={<QuestionCircleOutlined />}
|
||||
>
|
||||
使用帮助
|
||||
</Button>
|
||||
</Footer>
|
||||
</Layout>
|
||||
<Content className="container-inner">
|
||||
<Outlet />
|
||||
</Content>
|
||||
<Footer className="container-footer">
|
||||
<Button
|
||||
type={"link"}
|
||||
onClick={openHelpUrl}
|
||||
icon={<QuestionCircleOutlined />}
|
||||
>
|
||||
使用帮助
|
||||
</Button>
|
||||
</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -2,19 +2,20 @@ import { ConfigProvider, theme } from "antd";
|
||||
import "antd/dist/reset.css";
|
||||
import React, { FC, StrictMode, useEffect } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Provider, useSelector } from "react-redux";
|
||||
import { Provider, useDispatch } from "react-redux";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import App from "./App";
|
||||
import HomePage from "./nodes/HomePage";
|
||||
import SettingPage from "./nodes/SettingPage";
|
||||
import PlayerPage from "./nodes/PlayerPage";
|
||||
import SourceExtract from "./nodes/SourceExtract";
|
||||
import store from "./store";
|
||||
import store, { setAppStore, increase } from "./store";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
import "./index.scss";
|
||||
import { tdApp } from "./utils";
|
||||
import useElectron from "./hooks/electron";
|
||||
|
||||
dayjs.locale("zh-cn");
|
||||
tdApp.init();
|
||||
@@ -24,7 +25,9 @@ function getAlgorithm(appTheme: "dark" | "light") {
|
||||
}
|
||||
|
||||
const Root: FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [appTheme, setAppTheme] = React.useState<"dark" | "light">("light");
|
||||
const { rendererEvent, removeEventListener } = useElectron();
|
||||
|
||||
const themeChange = (event: MediaQueryListEvent) => {
|
||||
if (event.matches) {
|
||||
@@ -34,6 +37,25 @@ const Root: FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 监听store变化
|
||||
const onAppStoreChange = (event: any, store: AppStore) => {
|
||||
dispatch(setAppStore(store));
|
||||
};
|
||||
|
||||
const onReceiveDownloadItem = () => {
|
||||
dispatch(increase());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
rendererEvent("store-change", onAppStoreChange);
|
||||
rendererEvent("download-item-notifier", onReceiveDownloadItem);
|
||||
|
||||
return () => {
|
||||
removeEventListener("store-change", onAppStoreChange);
|
||||
removeEventListener("download-item-notifier", onReceiveDownloadItem);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isDarkTheme = matchMedia("(prefers-color-scheme: dark)");
|
||||
isDarkTheme.addEventListener("change", themeChange);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import React, { FC, ReactNode, useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
Tag,
|
||||
Popover,
|
||||
QRCode,
|
||||
Dropdown,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import "./index.scss";
|
||||
import PageContainer from "../../components/PageContainer";
|
||||
@@ -25,12 +27,13 @@ import {
|
||||
PlayCircleOutlined,
|
||||
SyncOutlined,
|
||||
MobileOutlined,
|
||||
MoreOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { selectStore } from "../../store/appSlice";
|
||||
import { selectAppStore } from "../../store";
|
||||
import { tdApp } from "../../utils";
|
||||
import { increase } from "../../store/downloadSlice";
|
||||
import { selectBrowserStore } from "../../store/browserSlice";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
enum DownloadFilter {
|
||||
list = "list",
|
||||
@@ -55,7 +58,7 @@ const HomePage: FC = () => {
|
||||
getLocalIP,
|
||||
} = useElectron();
|
||||
const dispatch = useDispatch();
|
||||
const appStore = useSelector(selectStore);
|
||||
const appStore = useSelector(selectAppStore);
|
||||
const [filter, setFilter] = useState(DownloadFilter.list);
|
||||
const { data, loading, pagination, refresh } = usePagination(
|
||||
({ current, pageSize }) => {
|
||||
@@ -79,13 +82,9 @@ const HomePage: FC = () => {
|
||||
const [editVideoForm] = Form.useForm<DownloadItem>();
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
|
||||
const browserStore = useSelector(selectBrowserStore);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const isDev = import.meta.env.MODE === "development";
|
||||
const localIP = await getLocalIP();
|
||||
const port = isDev ? 8556 : import.meta.env.APP_SERVER_PORT;
|
||||
setBaseUrl(`http://${localIP}:${port}/`);
|
||||
setBaseUrl(`http://${localIP}:${import.meta.env.APP_SERVER_PORT}/`);
|
||||
}, []);
|
||||
|
||||
const onDownloadProgress = (e: any, progress: DownloadProgress) => {
|
||||
@@ -128,7 +127,10 @@ const HomePage: FC = () => {
|
||||
};
|
||||
|
||||
const onReceiveDownloadItem = () => {
|
||||
dispatch(increase());
|
||||
refresh();
|
||||
};
|
||||
|
||||
const onChangeVideoIsLive = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
@@ -139,6 +141,7 @@ const HomePage: FC = () => {
|
||||
rendererEvent("download-start", onDownloadStart);
|
||||
rendererEvent("download-item-event", onDownloadMenuEvent);
|
||||
rendererEvent("download-item-notifier", onReceiveDownloadItem);
|
||||
rendererEvent("change-video-is-live", onChangeVideoIsLive);
|
||||
|
||||
return () => {
|
||||
removeEventListener("download-progress", onDownloadProgress);
|
||||
@@ -147,6 +150,7 @@ const HomePage: FC = () => {
|
||||
removeEventListener("download-start", onDownloadStart);
|
||||
removeEventListener("download-item-event", onDownloadMenuEvent);
|
||||
removeEventListener("download-item-notifier", onReceiveDownloadItem);
|
||||
removeEventListener("change-video-is-live", onChangeVideoIsLive);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -302,13 +306,6 @@ const HomePage: FC = () => {
|
||||
// 下载成功
|
||||
const curConverting = converting[item.id];
|
||||
return [
|
||||
<Button
|
||||
type="text"
|
||||
key="play-video"
|
||||
icon={<PlayCircleOutlined />}
|
||||
title="播放视频"
|
||||
onClick={() => openPlayerWindow(item.id)}
|
||||
/>,
|
||||
<Button
|
||||
type="text"
|
||||
key="open-file"
|
||||
@@ -318,12 +315,39 @@ const HomePage: FC = () => {
|
||||
/>,
|
||||
<Button
|
||||
type="text"
|
||||
key="more"
|
||||
icon={<SyncOutlined />}
|
||||
title="转换为音频"
|
||||
loading={curConverting}
|
||||
onClick={() => onClickConvertToAudio(item)}
|
||||
key="redownload"
|
||||
title="重新下载"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => onStartDownload(item.id)}
|
||||
/>,
|
||||
<Dropdown
|
||||
key="more"
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: "播放视频",
|
||||
key: "play",
|
||||
icon: <PlayCircleOutlined />,
|
||||
},
|
||||
{
|
||||
label: "转换为音频",
|
||||
key: "convert",
|
||||
icon: <SyncOutlined />,
|
||||
disabled: curConverting,
|
||||
},
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === "play") {
|
||||
openPlayerWindow(item.id);
|
||||
}
|
||||
if (key === "convert") {
|
||||
onClickConvertToAudio(item);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button type="text" title="更多" icon={<MoreOutlined />} />
|
||||
</Dropdown>,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -342,10 +366,14 @@ const HomePage: FC = () => {
|
||||
} else if (item.status === DownloadStatus.Stopped) {
|
||||
tag = <Tag color="default">下载暂停</Tag>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{item.name}
|
||||
{tag}
|
||||
<Text>{item.name}</Text>
|
||||
<Space size={[0, 8]}>
|
||||
{tag}
|
||||
{item.isLive && <Tag color={"default"}>直播资源</Tag>}
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
@@ -470,6 +498,7 @@ const HomePage: FC = () => {
|
||||
]}
|
||||
/>
|
||||
</ModalForm>
|
||||
<Button onClick={() => openDir(appStore.local)}>打开文件夹</Button>
|
||||
</Space>
|
||||
}
|
||||
className="home-page"
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.video-list {
|
||||
background: #141414;
|
||||
}
|
||||
}
|
||||
.list-toggle {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.setting-form-inner {
|
||||
.item-label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
&-text {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
ProFormGroup,
|
||||
ProFormSwitch,
|
||||
ProFormSelect,
|
||||
ProFormDigit,
|
||||
} from "@ant-design/pro-components";
|
||||
import "./index.scss";
|
||||
import { Button, FormInstance, message, Space, Tooltip } from "antd";
|
||||
import { FolderOpenOutlined, QuestionCircleOutlined } from "@ant-design/icons";
|
||||
import { selectStore, setAppStore } from "../../store/appSlice";
|
||||
import { selectAppStore, setAppStore } from "../../store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useElectron from "../../hooks/electron";
|
||||
import { useRequest } from "ahooks";
|
||||
@@ -27,7 +28,7 @@ const SettingPage: React.FC = () => {
|
||||
} = useElectron();
|
||||
const dispatch = useDispatch();
|
||||
const formRef = useRef<FormInstance<AppStore>>();
|
||||
const settings = useSelector(selectStore);
|
||||
const settings = useSelector(selectAppStore);
|
||||
const { data: envPath } = useRequest(getEnvPath);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
@@ -43,7 +44,7 @@ const SettingPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderButtonLable = () => {
|
||||
const renderButtonLabel = () => {
|
||||
return (
|
||||
<Button onClick={onSelectDir} icon={<FolderOpenOutlined />}>
|
||||
选择文件夹
|
||||
@@ -51,20 +52,11 @@ const SettingPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderTooltipLable = () => {
|
||||
const renderTooltipLabel = (label: string, tooltip: string) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginRight: "5px" }}>代理开关</div>
|
||||
<Tooltip
|
||||
title={"该代理会对软件自带浏览器以及下载时生效"}
|
||||
placement={"right"}
|
||||
>
|
||||
<div className="item-label">
|
||||
<div className="item-label-text">{label}</div>
|
||||
<Tooltip title={tooltip} placement={"right"}>
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -91,7 +83,7 @@ const SettingPage: React.FC = () => {
|
||||
formRef={formRef}
|
||||
layout="horizontal"
|
||||
submitter={false}
|
||||
labelCol={{ style: { width: "130px" } }}
|
||||
labelCol={{ style: { width: "140px" } }}
|
||||
labelAlign={"left"}
|
||||
colon={false}
|
||||
initialValues={settings}
|
||||
@@ -104,10 +96,22 @@ const SettingPage: React.FC = () => {
|
||||
disabled
|
||||
name="local"
|
||||
placeholder="请选择视频下载目录"
|
||||
label={renderButtonLable()}
|
||||
label={renderButtonLabel()}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="theme"
|
||||
label="下载器主题"
|
||||
valueEnum={{
|
||||
[AppTheme.System]: "跟随系统",
|
||||
[AppTheme.Dark]: "深色",
|
||||
[AppTheme.Light]: "浅色",
|
||||
}}
|
||||
placeholder="请选择主题色"
|
||||
/>
|
||||
<ProFormSwitch label="新窗口打开浏览器" name="openInNewWindow" />
|
||||
<ProFormSwitch label="下载完成提示" name="promptTone" />
|
||||
</ProFormGroup>
|
||||
<ProFormGroup title="浏览器设置" direction={"vertical"}>
|
||||
<ProFormText
|
||||
width="xl"
|
||||
name="proxy"
|
||||
@@ -116,7 +120,10 @@ const SettingPage: React.FC = () => {
|
||||
/>
|
||||
<ProFormSwitch
|
||||
name="useProxy"
|
||||
label={renderTooltipLable()}
|
||||
label={renderTooltipLabel(
|
||||
"代理开关",
|
||||
"该代理会对软件自带浏览器以及下载时生效"
|
||||
)}
|
||||
rules={[
|
||||
({ getFieldValue, setFieldValue }) => ({
|
||||
validator() {
|
||||
@@ -130,19 +137,27 @@ const SettingPage: React.FC = () => {
|
||||
]}
|
||||
/>
|
||||
<ProFormSwitch label="开启广告过滤" name="blockAds" />
|
||||
<ProFormSelect
|
||||
name="theme"
|
||||
label="主题"
|
||||
valueEnum={{
|
||||
[AppTheme.System]: "跟随系统",
|
||||
[AppTheme.Dark]: "深色",
|
||||
[AppTheme.Light]: "浅色",
|
||||
}}
|
||||
placeholder="请选择主题色"
|
||||
<ProFormSwitch label="以手机模式进入" name="isMobile" />
|
||||
<ProFormSwitch
|
||||
label={renderTooltipLabel(
|
||||
"使用沉浸式嗅探",
|
||||
"开启后沉浸式嗅探到的资源将不会直接显示在下载列表中,所有资源嗅探会交给浏览器插件处理"
|
||||
)}
|
||||
name="useExtension"
|
||||
/>
|
||||
</ProFormGroup>
|
||||
<ProFormGroup title="下载设置" direction={"vertical"}>
|
||||
<ProFormSwitch label="下载完成删除分片" name="deleteSegments" />
|
||||
<ProFormDigit
|
||||
label={renderTooltipLabel(
|
||||
"最大同时下载数量",
|
||||
"直播录制不占用同时下载数量"
|
||||
)}
|
||||
name="maxRunner"
|
||||
min={1}
|
||||
max={10}
|
||||
fieldProps={{ precision: 0 }}
|
||||
/>
|
||||
<ProFormText label="更多操作">
|
||||
<Space>
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.source-extract {
|
||||
&.is-page {
|
||||
padding-left: 10px;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
@@ -16,8 +15,11 @@
|
||||
overflow: auto;
|
||||
.webview-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.webview-inner {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@@ -2,10 +2,11 @@ import {
|
||||
ArrowLeftOutlined,
|
||||
ArrowRightOutlined,
|
||||
CloseOutlined,
|
||||
DownloadOutlined,
|
||||
HomeOutlined,
|
||||
ImportOutlined,
|
||||
LinkOutlined,
|
||||
MobileFilled,
|
||||
MobileOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
StarFilled,
|
||||
@@ -15,40 +16,35 @@ import { useAsyncEffect, useRequest } from "ahooks";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Collapse,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
List,
|
||||
message,
|
||||
Space,
|
||||
Spin,
|
||||
} from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import PageContainer from "../../components/PageContainer";
|
||||
import useElectron from "../../hooks/electron";
|
||||
import { increase } from "../../store/downloadSlice";
|
||||
import { generateUrl, getFavIcon } from "../../utils";
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
||||
import "./index.scss";
|
||||
import { ModalForm, ProFormText } from "@ant-design/pro-components";
|
||||
import {
|
||||
addSource,
|
||||
BrowserStatus,
|
||||
PageMode,
|
||||
selectBrowserStore,
|
||||
setAppStore,
|
||||
setBrowserStore,
|
||||
} from "../../store/browserSlice";
|
||||
} from "../../store";
|
||||
import WebView from "../../components/WebView";
|
||||
|
||||
const { Panel: AntDPanel } = Collapse;
|
||||
import { selectAppStore } from "../../store";
|
||||
|
||||
interface SourceExtractProps {
|
||||
page?: boolean;
|
||||
}
|
||||
|
||||
export enum PageMode {
|
||||
Default = "default",
|
||||
Browser = "browser",
|
||||
}
|
||||
|
||||
const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
const {
|
||||
getFavorites,
|
||||
@@ -58,13 +54,12 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
rendererEvent,
|
||||
removeEventListener,
|
||||
webviewGoBack,
|
||||
webviewReload,
|
||||
webwiewGoHome,
|
||||
addDownloadItem,
|
||||
onFavoriteItemContextMenu,
|
||||
downloadNow,
|
||||
combineToHomePage,
|
||||
getSharedState,
|
||||
setUserAgent,
|
||||
getAppStore: ipcGetAppStore,
|
||||
} = useElectron();
|
||||
const dispatch = useDispatch();
|
||||
const { data: favoriteList = [], refresh } = useRequest(getFavorites);
|
||||
@@ -72,17 +67,38 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [hoverId, setHoverId] = useState<number>(-1);
|
||||
const store = useSelector(selectBrowserStore);
|
||||
const appStore = useSelector(selectAppStore);
|
||||
|
||||
const curIsFavorite = favoriteList.find((item) => item.url === store.url);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const store = await ipcGetAppStore();
|
||||
dispatch(setAppStore(store));
|
||||
}, []);
|
||||
|
||||
const loadUrl = async (url: string) => {
|
||||
await webviewLoadURL(url);
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
url: url,
|
||||
mode: PageMode.Browser,
|
||||
})
|
||||
);
|
||||
try {
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
mode: PageMode.Browser,
|
||||
status: BrowserStatus.Loading,
|
||||
})
|
||||
);
|
||||
await webviewLoadURL(url);
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
url: url,
|
||||
status: BrowserStatus.Loaded,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
dispatch(
|
||||
setBrowserStore({
|
||||
status: BrowserStatus.Failed,
|
||||
errMsg: (err as any).message,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const goto = async () => {
|
||||
@@ -138,10 +154,6 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onClickReload = () => {
|
||||
webviewReload();
|
||||
};
|
||||
|
||||
const onClickEnter = async () => {
|
||||
if (!store.url) {
|
||||
return;
|
||||
@@ -166,10 +178,6 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const receiveLinkMessage = (e: unknown, msg: LinkMessage) => {
|
||||
dispatch(addSource(msg));
|
||||
};
|
||||
|
||||
const onFavoriteEvent = async (
|
||||
e: unknown,
|
||||
{
|
||||
@@ -199,92 +207,42 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
useEffect(() => {
|
||||
const prevTitle = document.title;
|
||||
rendererEvent("webview-dom-ready", onDomReady);
|
||||
rendererEvent("webview-link-message", receiveLinkMessage);
|
||||
rendererEvent("favorite-item-event", onFavoriteEvent);
|
||||
|
||||
return () => {
|
||||
document.title = prevTitle;
|
||||
removeEventListener("webview-dom-ready", onDomReady);
|
||||
removeEventListener("webview-link-message", receiveLinkMessage);
|
||||
removeEventListener("favorite-item-event", onFavoriteEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onAddDownloadItem = (item: LinkMessage) => {
|
||||
dispatch(increase());
|
||||
addDownloadItem({
|
||||
name: item.title,
|
||||
url: item.url,
|
||||
});
|
||||
};
|
||||
|
||||
const onDownloadNow = (item: LinkMessage) => {
|
||||
dispatch(increase());
|
||||
downloadNow({
|
||||
name: item.title,
|
||||
url: item.url,
|
||||
});
|
||||
};
|
||||
|
||||
// 渲染收藏夹
|
||||
const renderWebviewSider = () => {
|
||||
return (
|
||||
<div className="webview-sider">
|
||||
<Collapse className="webview-sider-inner" bordered={false}>
|
||||
{store.sourceList.map((item) => {
|
||||
return (
|
||||
<AntDPanel
|
||||
className="sider-list-container"
|
||||
header={item.title}
|
||||
key={item.url}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
title="添加到下载列表"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onAddDownloadItem(item);
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
/>
|
||||
<Button
|
||||
type="link"
|
||||
title="立即下载"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDownloadNow(item);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="sider-list">
|
||||
<div className="sider-item">视频名: {item.title}</div>
|
||||
<div className="sider-item">视频链接: {item.url}</div>
|
||||
</div>
|
||||
</AntDPanel>
|
||||
);
|
||||
})}
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 合并到主页
|
||||
const onCombineToHome = () => {
|
||||
combineToHomePage(store);
|
||||
};
|
||||
|
||||
// 设置默认UA
|
||||
const onSetDefaultUA = () => {
|
||||
const nextMode = !appStore.isMobile;
|
||||
setUserAgent(nextMode);
|
||||
dispatch(
|
||||
setAppStore({
|
||||
isMobile: nextMode,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染工具栏
|
||||
const renderToolbar = () => {
|
||||
const disabled =
|
||||
store.status !== BrowserStatus.Loaded || store.mode !== PageMode.Browser;
|
||||
return (
|
||||
<Space.Compact className="action-bar" block>
|
||||
<Button type="text" title="切换为手机模式" onClick={onSetDefaultUA}>
|
||||
{appStore.isMobile ? <MobileFilled /> : <MobileOutlined />}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={store.mode === PageMode.Default}
|
||||
disabled={disabled}
|
||||
title="首页"
|
||||
type="text"
|
||||
onClick={onClickGoHome}
|
||||
@@ -292,27 +250,21 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
<HomeOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={store.mode === PageMode.Default}
|
||||
disabled={disabled}
|
||||
title="回退"
|
||||
type="text"
|
||||
onClick={onClickGoBack}
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={store.mode === PageMode.Default}
|
||||
title="刷新"
|
||||
type="text"
|
||||
onClick={onClickReload}
|
||||
>
|
||||
<Button disabled={disabled} title="刷新" type="text" onClick={goto}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
title={curIsFavorite ? "取消收藏" : "收藏"}
|
||||
onClick={onClickAddFavorite}
|
||||
disabled={store.mode === PageMode.Default}
|
||||
disabled={disabled}
|
||||
>
|
||||
{curIsFavorite ? <StarFilled /> : <StarOutlined />}
|
||||
</Button>
|
||||
@@ -323,10 +275,18 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
const url = e.target.value;
|
||||
dispatch(setBrowserStore({ url }));
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.target.select();
|
||||
}}
|
||||
onKeyDown={onInputKeyDown}
|
||||
placeholder="请输入网址链接……"
|
||||
/>
|
||||
<Button title="访问" type="text" onClick={onClickEnter}>
|
||||
<Button
|
||||
title="访问"
|
||||
type="text"
|
||||
onClick={onClickEnter}
|
||||
disabled={!store.url}
|
||||
>
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
{page && (
|
||||
@@ -340,23 +300,24 @@ const SourceExtract: React.FC<SourceExtractProps> = ({ page = false }) => {
|
||||
|
||||
// 渲染浏览器面板
|
||||
const renderBrowserPanel = () => {
|
||||
return (
|
||||
<div className="webview-container">
|
||||
<PanelGroup autoSaveId="example" direction="horizontal">
|
||||
<Panel minSize={50}>
|
||||
<WebView className="webview-inner" />
|
||||
</Panel>
|
||||
{store.sourceList.length > 0 && (
|
||||
<>
|
||||
<PanelResizeHandle className="divider">
|
||||
<div className="handle" />
|
||||
</PanelResizeHandle>
|
||||
<Panel minSize={20}>{renderWebviewSider()}</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
</div>
|
||||
);
|
||||
let content = <div></div>;
|
||||
if (store.status === BrowserStatus.Loading) {
|
||||
content = <Spin />;
|
||||
} else if (store.status === BrowserStatus.Failed) {
|
||||
content = (
|
||||
<Empty description={store.errMsg || "加载失败"}>
|
||||
<Space>
|
||||
<Button type="primary" onClick={onClickGoHome}>
|
||||
返回首页
|
||||
</Button>
|
||||
<Button onClick={goto}>刷新</Button>
|
||||
</Space>
|
||||
</Empty>
|
||||
);
|
||||
} else if (store.status === BrowserStatus.Loaded) {
|
||||
content = <WebView className="webview-inner" />;
|
||||
}
|
||||
return <div className="webview-container">{content}</div>;
|
||||
};
|
||||
|
||||
// 渲染收藏 item
|
||||
|
||||
@@ -10,7 +10,9 @@ declare interface DownloadItem {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
headers: string;
|
||||
status: string;
|
||||
isLive: boolean;
|
||||
}
|
||||
|
||||
declare interface VideoResponse {
|
||||
@@ -62,6 +64,7 @@ declare interface ElectronAPI {
|
||||
openBrowser: (url: string) => Promise<void>;
|
||||
getSharedState: () => Promise<any>;
|
||||
setSharedState: (state: any) => Promise<void>;
|
||||
setUserAgent: (isMobile: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
declare interface Favorite {
|
||||
@@ -71,9 +74,10 @@ declare interface Favorite {
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
declare interface LinkMessage {
|
||||
declare interface WebSource {
|
||||
url: string;
|
||||
title: string;
|
||||
name: string;
|
||||
headers: string;
|
||||
}
|
||||
|
||||
declare interface UrlDetail {
|
||||
@@ -98,13 +102,18 @@ declare interface AppStore {
|
||||
blockAds?: boolean;
|
||||
// 主题
|
||||
theme?: AppTheme;
|
||||
// 是否使用扩展
|
||||
useExtension?: boolean;
|
||||
// 默认使用移动端UA
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
declare interface BrowserStore {
|
||||
mode: PageMode;
|
||||
url: string;
|
||||
sourceList: LinkMessage[];
|
||||
title: string;
|
||||
status: BrowserStatus;
|
||||
errMsg?: string;
|
||||
}
|
||||
|
||||
declare interface DownloadProgress {
|
||||
@@ -112,6 +121,7 @@ declare interface DownloadProgress {
|
||||
cur: string;
|
||||
total: string;
|
||||
speed: string;
|
||||
isLive: boolean;
|
||||
}
|
||||
|
||||
interface ObjectConstructor {
|
||||
|
||||
@@ -10,6 +10,8 @@ const initialState: AppStore = {
|
||||
deleteSegments: true,
|
||||
openInNewWindow: false,
|
||||
theme: AppTheme.System,
|
||||
useExtension: false,
|
||||
isMobile: false,
|
||||
};
|
||||
|
||||
export const appSlice = createSlice({
|
||||
@@ -27,5 +29,5 @@ export const appSlice = createSlice({
|
||||
});
|
||||
|
||||
export const { setAppStore } = appSlice.actions;
|
||||
export const selectStore = (state: RootState) => state.app;
|
||||
export const selectAppStore = (state: RootState) => state.app;
|
||||
export default appSlice.reducer;
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { RootState } from ".";
|
||||
import { PageMode } from "../nodes/SourceExtract";
|
||||
import useElectron from "../hooks/electron";
|
||||
|
||||
const { setSharedState } = useElectron();
|
||||
|
||||
export enum PageMode {
|
||||
Default = "default",
|
||||
Browser = "browser",
|
||||
}
|
||||
|
||||
export enum BrowserStatus {
|
||||
Default = "default",
|
||||
Loaded = "loaded",
|
||||
Loading = "loading",
|
||||
Failed = "failed",
|
||||
}
|
||||
|
||||
const initialState: BrowserStore = {
|
||||
mode: PageMode.Default,
|
||||
url: "",
|
||||
sourceList: [],
|
||||
title: "",
|
||||
status: BrowserStatus.Default,
|
||||
errMsg: "",
|
||||
};
|
||||
|
||||
const convertPlainObject = (obj: unknown) => {
|
||||
@@ -30,19 +42,10 @@ export const browserSlice = createSlice({
|
||||
// FIXME: 异步函数
|
||||
setSharedState(convertPlainObject(state));
|
||||
},
|
||||
addSource(state, { payload }: PayloadAction<LinkMessage>) {
|
||||
if (state.sourceList.find((item) => item.url === payload.url)) {
|
||||
return;
|
||||
}
|
||||
state.sourceList = [payload, ...state.sourceList];
|
||||
// FIXME: 异步函数
|
||||
setSharedState(convertPlainObject(state));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setBrowserStore, addSource } = browserSlice.actions;
|
||||
export const { setBrowserStore } = browserSlice.actions;
|
||||
export const selectUrl = (state: RootState) => state.browser.url;
|
||||
export const selectSourceList = (state: RootState) => state.browser.sourceList;
|
||||
export const selectBrowserStore = (state: RootState) => state.browser;
|
||||
export default browserSlice.reducer;
|
||||
|
||||
@@ -15,5 +15,7 @@ export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
export * from "./appSlice";
|
||||
export * from "./browserSlice";
|
||||
export * from "./downloadSlice";
|
||||
|
||||
export default store;
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineConfig({
|
||||
envDir: "../..",
|
||||
envPrefix: "APP",
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "../main/build/renderer"),
|
||||
outDir: path.resolve(__dirname, "../main/app/build/renderer"),
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
|
||||