podflow 20250427__py3-none-any.whl → 20250429__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- podflow/download/show_progress.py +4 -2
- podflow/httpfs/app_bottle.py +1 -1
- podflow/httpfs/download_bar.py +5 -2
- podflow/main_podcast.py +3 -3
- podflow/templates/css/index.css +8 -1
- podflow/templates/index.html +2 -1
- podflow/templates/js/index.js +580 -336
- {podflow-20250427.dist-info → podflow-20250429.dist-info}/METADATA +1 -1
- {podflow-20250427.dist-info → podflow-20250429.dist-info}/RECORD +12 -12
- {podflow-20250427.dist-info → podflow-20250429.dist-info}/WHEEL +0 -0
- {podflow-20250427.dist-info → podflow-20250429.dist-info}/entry_points.txt +0 -0
- {podflow-20250427.dist-info → podflow-20250429.dist-info}/top_level.txt +0 -0
@@ -42,7 +42,8 @@ def show_progress(stream):
|
|
42
42
|
mod=1,
|
43
43
|
per=stream["downloaded_bytes"] / stream["total_bytes"],
|
44
44
|
retime=eta,
|
45
|
-
speed=f"{speed}/s
|
45
|
+
speed=f"{speed}/s",
|
46
|
+
part=f"{downloaded_bytes}/{total_bytes}",
|
46
47
|
status="下载中",
|
47
48
|
)
|
48
49
|
if stream["status"] == "finished":
|
@@ -59,6 +60,7 @@ def show_progress(stream):
|
|
59
60
|
mod=1,
|
60
61
|
per=1,
|
61
62
|
retime=elapsed,
|
62
|
-
speed=f"{speed}/s
|
63
|
+
speed=f"{speed}/s",
|
64
|
+
part=f"{downloaded_bytes}/{total_bytes}",
|
63
65
|
status="下载完成",
|
64
66
|
)
|
podflow/httpfs/app_bottle.py
CHANGED
@@ -363,7 +363,7 @@ class bottle_app:
|
|
363
363
|
filename = upload_file.filename
|
364
364
|
name = filename.split(".")[0]
|
365
365
|
suffix = filename.split(".")[1]
|
366
|
-
if suffix in ["mp4", "m4a"]:
|
366
|
+
if suffix not in ["mp4", "m4a"]:
|
367
367
|
self.print_out("upload", 404)
|
368
368
|
return {
|
369
369
|
"code": -6,
|
podflow/httpfs/download_bar.py
CHANGED
@@ -9,6 +9,7 @@ def download_bar(
|
|
9
9
|
per=0,
|
10
10
|
retime="00:00",
|
11
11
|
speed=" 0.00 B",
|
12
|
+
part="",
|
12
13
|
status="准备中",
|
13
14
|
idname="",
|
14
15
|
nametext="",
|
@@ -20,6 +21,7 @@ def download_bar(
|
|
20
21
|
per,
|
21
22
|
retime,
|
22
23
|
speed,
|
24
|
+
part,
|
23
25
|
status,
|
24
26
|
idname,
|
25
27
|
nametext,
|
@@ -30,6 +32,7 @@ def download_bar(
|
|
30
32
|
gVar.index_message["download"][-1][0] = per
|
31
33
|
gVar.index_message["download"][-1][1] = retime
|
32
34
|
gVar.index_message["download"][-1][2] = speed
|
33
|
-
gVar.index_message["download"][-1][3] =
|
35
|
+
gVar.index_message["download"][-1][3] = part
|
36
|
+
gVar.index_message["download"][-1][4] = status
|
34
37
|
elif mod == 2 and gVar.index_message["download"]:
|
35
|
-
gVar.index_message["download"][-1][
|
38
|
+
gVar.index_message["download"][-1][4] = status
|
podflow/main_podcast.py
CHANGED
@@ -69,7 +69,7 @@ def main_podcast():
|
|
69
69
|
# 初始化
|
70
70
|
build_original()
|
71
71
|
# http共享
|
72
|
-
port = gVar.config.get("port",
|
72
|
+
port = gVar.config.get("port", 8000) # 使用 .get 获取端口
|
73
73
|
hostip = "0.0.0.0"
|
74
74
|
|
75
75
|
if port_judge(hostip, port): # 假设 port_judge 存在
|
@@ -102,8 +102,8 @@ def main_podcast():
|
|
102
102
|
)
|
103
103
|
cherrypy.engine.start() # 启动 CherryPy 服务器
|
104
104
|
time_print(f"HTTP服务器启动, 端口: \033[32m{port}\033[0m")
|
105
|
-
if parse.index:
|
106
|
-
open_url(f"{gVar.config['address']}/index")
|
105
|
+
if parse.index:
|
106
|
+
open_url(f"{gVar.config['address']}/index")
|
107
107
|
if parse.httpfs: # HttpFS参数判断, 是否继续运行
|
108
108
|
cherrypy.engine.block() # 阻止程序退出, 保持HTTP服务运行
|
109
109
|
sys.exit(0)
|
podflow/templates/css/index.css
CHANGED
@@ -140,7 +140,6 @@ main.full {
|
|
140
140
|
.common-area {
|
141
141
|
width: 100%;
|
142
142
|
max-width: 520px;
|
143
|
-
padding: 0 8px;
|
144
143
|
color: var(--text-color);
|
145
144
|
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
|
146
145
|
box-sizing: border-box;
|
@@ -159,7 +158,11 @@ textarea {
|
|
159
158
|
background-color: var(--input-bg);
|
160
159
|
font-family: 'Courier New', Courier, monospace; /* 添加字体 */
|
161
160
|
}
|
161
|
+
#inputOutput {
|
162
|
+
padding: 4px 8px;
|
163
|
+
}
|
162
164
|
#messageDownload {
|
165
|
+
padding: 0 8px;
|
163
166
|
max-height: 180px;
|
164
167
|
max-width: 99%;
|
165
168
|
height: auto;
|
@@ -168,6 +171,7 @@ textarea {
|
|
168
171
|
background-color: var(--bg-color);
|
169
172
|
}
|
170
173
|
#messageArea {
|
174
|
+
padding: 4px 8px;
|
171
175
|
height: 250px;
|
172
176
|
font-size: 12px;
|
173
177
|
border: 1px solid var(--input-border);
|
@@ -176,6 +180,7 @@ textarea {
|
|
176
180
|
font-family: 'Courier New', Courier, monospace; /* 添加字体 */
|
177
181
|
}
|
178
182
|
#messageHttp {
|
183
|
+
padding: 4px 8px;
|
179
184
|
height: 100px;
|
180
185
|
font-size: 12px;
|
181
186
|
border: 1px solid var(--input-border);
|
@@ -347,6 +352,8 @@ button:hover {
|
|
347
352
|
margin-left: auto;
|
348
353
|
flex-shrink: 0;
|
349
354
|
align-self: flex-end;
|
355
|
+
position: relative;
|
356
|
+
top: -2px;
|
350
357
|
}
|
351
358
|
.scroll-container {
|
352
359
|
flex: 1; /* 自适应宽度 */
|
podflow/templates/index.html
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
6
|
<title>Podflow</title>
|
7
7
|
<link rel="stylesheet" href="/templates/css/index.css" />
|
8
|
+
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
8
9
|
</head>
|
9
10
|
<body>
|
10
11
|
<!-- 菜单栏 -->
|
@@ -53,7 +54,7 @@
|
|
53
54
|
</form>
|
54
55
|
</section>
|
55
56
|
</main>
|
56
|
-
<script src="/templates/js/qrcode.min.js"></script>
|
57
57
|
<script src="/templates/js/index.js"></script>
|
58
|
+
<script src="/templates/js/qrcode.min.js"></script>
|
58
59
|
</body>
|
59
60
|
</html>
|
podflow/templates/js/index.js
CHANGED
@@ -1,481 +1,725 @@
|
|
1
1
|
(function() {
|
2
|
-
//
|
2
|
+
// --- 缓存常用 DOM 节点 ---
|
3
|
+
// 菜单相关
|
3
4
|
const menu = document.getElementById('menu');
|
4
5
|
const toggleMenuBtn = document.getElementById('toggleMenu');
|
6
|
+
// 页面容器
|
5
7
|
const pages = {
|
6
8
|
pageChannel: document.getElementById('pageChannel'),
|
7
9
|
pageMessage: document.getElementById('pageMessage')
|
8
10
|
};
|
11
|
+
// Channel ID 相关表单
|
9
12
|
const inputForm = document.getElementById('inputForm');
|
10
13
|
const inputOutput = document.getElementById('inputOutput');
|
11
14
|
const pasteBtn = document.getElementById('pasteBtn');
|
12
15
|
const copyBtn = document.getElementById('copyBtn');
|
13
16
|
const clearBtn = document.getElementById('clearBtn');
|
14
|
-
//
|
17
|
+
// 主进度条相关
|
15
18
|
const mainProgress = document.getElementById('mainProgress');
|
16
19
|
const progressStatus = document.getElementById('progressStatus');
|
17
20
|
const progressPercentage = document.getElementById('progressPercentage');
|
18
|
-
|
19
|
-
const
|
20
|
-
const
|
21
|
+
// 消息显示区域
|
22
|
+
const messageArea = document.getElementById('messageArea'); // Podflow 消息
|
23
|
+
const messageHttp = document.getElementById('messageHttp'); // HTTP 消息
|
24
|
+
const messageDownload = document.getElementById('messageDownload'); // 下载进度条容器
|
25
|
+
const downloadLabel = document.getElementById('downloadLabel'); // 下载进度标题 (假设存在)
|
21
26
|
|
22
|
-
|
23
|
-
let
|
24
|
-
let userScrolled =
|
27
|
+
// --- 状态变量 ---
|
28
|
+
let lastMessage = { schedule: [], podflow: [], http: [], download: [] }; // 缓存上一次的消息数据
|
29
|
+
let userScrolled = { // 分别跟踪每个滚动区域的用户滚动状态
|
30
|
+
messageArea: false,
|
31
|
+
messageHttp: false,
|
32
|
+
messageDownload: false
|
33
|
+
};
|
25
34
|
let eventSource = null; // 用于存储 EventSource 实例
|
26
|
-
|
27
|
-
//
|
35
|
+
|
36
|
+
// --- 二维码生成 ---
|
37
|
+
/**
|
38
|
+
* 为指定的 DOM 容器生成二维码
|
39
|
+
* @param {HTMLElement} container - 要生成二维码的容器元素,需要有 data-url 属性
|
40
|
+
*/
|
28
41
|
function generateQRCodeForNode(container) {
|
29
42
|
const rootStyles = getComputedStyle(document.documentElement);
|
30
43
|
const textColor = rootStyles.getPropertyValue('--text-color').trim();
|
31
44
|
const inputBg = rootStyles.getPropertyValue('--input-bg').trim();
|
32
45
|
const url = container.dataset.url;
|
33
|
-
container.innerHTML = '';
|
46
|
+
container.innerHTML = ''; // 清空容器
|
34
47
|
if (url) {
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
48
|
+
try {
|
49
|
+
new QRCode(container, {
|
50
|
+
text: url,
|
51
|
+
width: 220,
|
52
|
+
height: 220,
|
53
|
+
colorDark: textColor,
|
54
|
+
colorLight: inputBg,
|
55
|
+
correctLevel: QRCode.CorrectLevel.L
|
56
|
+
});
|
57
|
+
} catch (e) {
|
58
|
+
console.error("生成二维码失败:", e);
|
59
|
+
container.textContent = '二维码生成失败';
|
60
|
+
}
|
43
61
|
} else {
|
44
62
|
container.textContent = 'URL 未提供';
|
45
63
|
}
|
46
64
|
}
|
47
65
|
|
48
|
-
//
|
66
|
+
// --- 菜单控制 ---
|
67
|
+
/**
|
68
|
+
* 切换侧边菜单的显示/隐藏状态
|
69
|
+
*/
|
49
70
|
function toggleMenu() {
|
50
71
|
menu.classList.toggle('hidden');
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
} else {
|
55
|
-
toggleMenuBtn.style.left = 'var(--menu-width)';
|
56
|
-
toggleMenuBtn.textContent = '❮';
|
57
|
-
}
|
72
|
+
const isHidden = menu.classList.contains('hidden');
|
73
|
+
toggleMenuBtn.style.left = isHidden ? '0px' : 'var(--menu-width)';
|
74
|
+
toggleMenuBtn.textContent = isHidden ? '❯' : '❮';
|
58
75
|
}
|
59
76
|
|
60
|
-
|
77
|
+
/**
|
78
|
+
* 根据页面 ID 显示对应的页面内容,并管理 SSE 连接
|
79
|
+
* @param {string} pageId - 'pageChannel' 或 'pageMessage'
|
80
|
+
*/
|
61
81
|
function showPage(pageId) {
|
82
|
+
// 隐藏所有页面
|
62
83
|
Object.values(pages).forEach(page => page.style.display = 'none');
|
84
|
+
|
85
|
+
// 显示目标页面
|
63
86
|
if (pages[pageId]) {
|
64
87
|
pages[pageId].style.display = 'block';
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
}
|
88
|
+
|
89
|
+
// 手机模式下,切换页面时自动隐藏菜单
|
90
|
+
if (window.innerWidth <= 600 && !menu.classList.contains('hidden')) {
|
91
|
+
toggleMenu();
|
70
92
|
}
|
93
|
+
|
71
94
|
// --- SSE 连接管理 ---
|
72
95
|
if (pageId === 'pageMessage') {
|
73
|
-
startMessageStream(); //
|
96
|
+
startMessageStream(); // 显示消息页面时,启动 SSE 连接
|
74
97
|
} else {
|
75
|
-
stopMessageStream(); //
|
98
|
+
stopMessageStream(); // 切换到其他页面时,关闭 SSE 连接
|
76
99
|
}
|
100
|
+
} else {
|
101
|
+
console.warn(`未找到 ID 为 "${pageId}" 的页面`);
|
77
102
|
}
|
78
103
|
}
|
79
104
|
|
80
|
-
//
|
105
|
+
// --- 滚动处理 ---
|
106
|
+
/**
|
107
|
+
* 监听滚动事件,判断用户是否手动滚动了消息区域
|
108
|
+
* @param {Event} event - 滚动事件对象
|
109
|
+
*/
|
81
110
|
function onUserScroll(event) {
|
82
111
|
const element = event.target;
|
83
|
-
//
|
84
|
-
|
85
|
-
|
112
|
+
const containerId = element.id; // 获取滚动容器的ID
|
113
|
+
if (userScrolled.hasOwnProperty(containerId)) { // 确保是我们关心的容器
|
114
|
+
// 判断是否滚动到接近底部 (增加 10px 容差)
|
115
|
+
const isNearBottom = element.scrollHeight - element.scrollTop <= element.clientHeight + 10;
|
116
|
+
userScrolled[containerId] = !isNearBottom; // 如果没在底部,则标记为用户已滚动
|
117
|
+
}
|
86
118
|
}
|
87
119
|
|
88
|
-
//
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
// 更新进度条
|
94
|
-
if (JSON.stringify(data.schedule) !== JSON.stringify(lastMessage.schedule)) {
|
95
|
-
updateProgress(data.schedule);
|
96
|
-
lastMessage.schedule = data.schedule;
|
97
|
-
}
|
98
|
-
// 追加新消息
|
99
|
-
if (JSON.stringify(data.podflow) !== JSON.stringify(lastMessage.podflow)) {
|
100
|
-
appendMessages(messageArea, data.podflow, lastMessage.podflow);
|
101
|
-
lastMessage.podflow = data.podflow;
|
102
|
-
}
|
103
|
-
if (JSON.stringify(data.http) !== JSON.stringify(lastMessage.http)) {
|
104
|
-
appendMessages(messageHttp, data.http, lastMessage.http);
|
105
|
-
lastMessage.http = data.http;
|
106
|
-
}
|
107
|
-
if (JSON.stringify(data.download) !== JSON.stringify(lastMessage.download)) {
|
108
|
-
appendBar(messageDownload, data.download, lastMessage.download);
|
109
|
-
lastMessage.download = data.download
|
110
|
-
}
|
111
|
-
})
|
112
|
-
.catch(error => console.error('获取消息失败:', error));
|
113
|
-
}
|
114
|
-
|
115
|
-
// 更新进度条并显示状态和百分比
|
120
|
+
// --- 消息处理 ---
|
121
|
+
/**
|
122
|
+
* 更新主进度条的状态和百分比
|
123
|
+
* @param {Array} scheduleData - 包含状态和进度的数组,例如 ["状态", 0.5]
|
124
|
+
*/
|
116
125
|
function updateProgress(scheduleData) {
|
117
|
-
|
118
|
-
|
119
|
-
const
|
126
|
+
if (Array.isArray(scheduleData) && scheduleData.length === 2) {
|
127
|
+
const [status, progress] = scheduleData;
|
128
|
+
const percentage = progress * 100;
|
129
|
+
|
130
|
+
// 确保 DOM 元素存在
|
131
|
+
if (!mainProgress || !progressStatus || !progressPercentage) {
|
132
|
+
console.warn("进度条相关 DOM 元素未找到");
|
133
|
+
return;
|
134
|
+
}
|
135
|
+
|
120
136
|
if (status === "准备中" || status === "构建中") {
|
121
|
-
mainProgress.style.width = `${
|
122
|
-
progressStatus.textContent = status;
|
123
|
-
progressPercentage.textContent = `${
|
137
|
+
mainProgress.style.width = `${percentage}%`;
|
138
|
+
progressStatus.textContent = status;
|
139
|
+
progressPercentage.textContent = `${percentage.toFixed(2)}%`;
|
124
140
|
} else if (status === "已完成") {
|
125
141
|
mainProgress.style.width = '100%';
|
126
|
-
progressStatus.textContent = '已完成';
|
127
|
-
progressPercentage.textContent = '100.
|
142
|
+
progressStatus.textContent = '已完成';
|
143
|
+
progressPercentage.textContent = '100.00%'; // 保持格式一致
|
128
144
|
}
|
145
|
+
// 可以选择性地处理其他状态或进度为 null/undefined 的情况
|
146
|
+
} else {
|
147
|
+
// console.warn("接收到的进度数据格式不正确:", scheduleData);
|
129
148
|
}
|
130
149
|
}
|
131
|
-
|
150
|
+
|
151
|
+
/**
|
152
|
+
* 创建单个消息元素的 DOM 结构
|
153
|
+
* @param {string} message - 包含 HTML 的消息字符串
|
154
|
+
* @returns {HTMLDivElement} - 创建的消息元素
|
155
|
+
*/
|
132
156
|
function createMessageElement(message) {
|
133
157
|
const div = document.createElement('div');
|
158
|
+
// 使用 innerHTML,因为消息内容可能包含 HTML 标签 (如二维码容器)
|
134
159
|
div.innerHTML = message;
|
135
|
-
div.className = 'message';
|
160
|
+
div.className = 'message'; // 假设 'message' 是单个消息的样式类
|
136
161
|
return div;
|
137
162
|
}
|
138
|
-
|
139
|
-
|
140
|
-
|
163
|
+
|
164
|
+
/**
|
165
|
+
* 处理消息元素内部的二维码容器
|
166
|
+
* @param {HTMLElement} element - 包含 .qrcode-container 的父元素
|
167
|
+
*/
|
168
|
+
function processQRCodeContainers(element) {
|
169
|
+
const qrContainers = element.querySelectorAll('.qrcode-container');
|
141
170
|
qrContainers.forEach(container => {
|
142
|
-
// 判断当前容器是否有 data-url 属性,并且值不为空
|
143
171
|
if (container.dataset.url) {
|
144
172
|
generateQRCodeForNode(container);
|
145
173
|
} else {
|
146
|
-
// 如果没有 data-url 或值为空,可以执行其他操作,例如输出提示信息
|
147
174
|
console.log('容器中未提供 URL,跳过二维码生成:', container);
|
148
|
-
container.textContent = '未提供二维码 URL';
|
175
|
+
container.textContent = '未提供二维码 URL';
|
149
176
|
}
|
150
177
|
});
|
151
178
|
}
|
152
|
-
|
153
|
-
|
179
|
+
|
180
|
+
/**
|
181
|
+
* 更新消息显示区域 (Podflow / HTTP),保留向上更新四行的逻辑
|
182
|
+
* @param {HTMLElement} container - 消息容器元素 (messageArea 或 messageHttp)
|
183
|
+
* @param {string[]} newMessages - 最新的消息数组
|
184
|
+
* @param {string[]} oldMessages - 上一次的消息数组
|
185
|
+
*/
|
154
186
|
function appendMessages(container, newMessages, oldMessages) {
|
155
|
-
//
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
187
|
+
if (!container) return; // 防御性编程
|
188
|
+
|
189
|
+
const containerId = container.id;
|
190
|
+
const wasAtBottom = !userScrolled[containerId] && (container.scrollHeight - container.scrollTop <= container.clientHeight + 10);
|
191
|
+
const newLength = newMessages.length;
|
192
|
+
const oldLength = oldMessages.length;
|
193
|
+
|
194
|
+
// --- 主要逻辑:比较新旧消息,更新 DOM ---
|
195
|
+
if (newLength === oldLength && newLength > 0) {
|
196
|
+
// --- 长度相同:检查最后几条消息是否有变化 ---
|
197
|
+
let replaceCount = 1; // 默认只检查最后一条
|
198
|
+
const lastMessageContent = newMessages[newLength - 1];
|
199
|
+
// 特殊逻辑:如果最后一条消息包含特定文本,则检查最后四条
|
200
|
+
if (lastMessageContent.includes("未扫描") || lastMessageContent.includes("二维码超时, 请重试")) {
|
201
|
+
replaceCount = Math.min(4, newLength); // 最多检查4条或数组长度
|
165
202
|
}
|
203
|
+
|
204
|
+
// 从后往前比较并替换变化的元素
|
166
205
|
for (let i = 0; i < replaceCount; i++) {
|
167
|
-
|
168
|
-
const index = newMessages.length - 1 - i;
|
206
|
+
const index = newLength - 1 - i;
|
169
207
|
const newMessage = newMessages[index];
|
170
208
|
const oldMessage = oldMessages[index];
|
209
|
+
|
171
210
|
if (newMessage !== oldMessage) {
|
172
|
-
// 先创建消息节点
|
173
211
|
const div = createMessageElement(newMessage);
|
174
|
-
//
|
175
|
-
processQRCodeContainers(div);
|
176
|
-
// 替换到容器中 - 注意这里要找到对应位置的子元素
|
212
|
+
processQRCodeContainers(div); // 处理二维码
|
177
213
|
const childToReplace = container.children[index];
|
178
214
|
if (childToReplace) {
|
179
215
|
container.replaceChild(div, childToReplace);
|
216
|
+
} else {
|
217
|
+
// 如果预期的子元素不存在(理论上不应发生),则追加
|
218
|
+
console.warn(`试图替换索引 ${index} 的子元素,但未找到。将追加。`);
|
219
|
+
container.appendChild(div);
|
180
220
|
}
|
181
221
|
}
|
182
222
|
}
|
183
|
-
} else {
|
184
|
-
//
|
185
|
-
//
|
186
|
-
if (
|
187
|
-
const replaceIndex =
|
188
|
-
const
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
if (
|
193
|
-
|
194
|
-
|
195
|
-
container.
|
223
|
+
} else if (newLength > oldLength) {
|
224
|
+
// --- 新消息比旧消息多 ---
|
225
|
+
// 1. 如果旧消息存在,替换旧消息数组中最后一条对应的 DOM 元素
|
226
|
+
if (oldLength > 0) {
|
227
|
+
const replaceIndex = oldLength - 1;
|
228
|
+
const newMessageToReplace = newMessages[replaceIndex];
|
229
|
+
const oldMessageToCompare = oldMessages[replaceIndex];
|
230
|
+
|
231
|
+
// 仅当内容实际改变时才替换
|
232
|
+
if (newMessageToReplace !== oldMessageToCompare) {
|
233
|
+
const div = createMessageElement(newMessageToReplace);
|
234
|
+
processQRCodeContainers(div); // 处理二维码
|
235
|
+
const childToReplace = container.children[replaceIndex]; // 替换对应索引的元素
|
236
|
+
if (childToReplace) {
|
237
|
+
container.replaceChild(div, childToReplace);
|
238
|
+
} else {
|
239
|
+
console.warn(`试图替换索引 ${replaceIndex} 的子元素,但未找到。`);
|
240
|
+
// 此处可以选择追加或其他错误处理
|
241
|
+
}
|
196
242
|
}
|
197
243
|
}
|
198
|
-
|
199
|
-
|
244
|
+
|
245
|
+
// 2. *** 优化点:使用 DocumentFragment 批量追加新增的消息 ***
|
246
|
+
const fragment = document.createDocumentFragment();
|
247
|
+
for (let i = oldLength; i < newLength; i++) {
|
248
|
+
const div = createMessageElement(newMessages[i]);
|
249
|
+
processQRCodeContainers(div); // 处理二维码
|
250
|
+
fragment.appendChild(div);
|
251
|
+
}
|
252
|
+
container.appendChild(fragment); // 一次性追加所有新消息
|
253
|
+
|
254
|
+
} else if (newLength < oldLength) {
|
255
|
+
// --- 新消息比旧消息少(通常意味着列表被清空或重置)---
|
256
|
+
// 简单处理:清空容器,重新添加所有新消息
|
257
|
+
console.log("新消息数量少于旧消息,将重新渲染整个列表。");
|
258
|
+
container.innerHTML = ''; // 清空
|
259
|
+
const fragment = document.createDocumentFragment();
|
260
|
+
newMessages.forEach(msg => {
|
200
261
|
const div = createMessageElement(msg);
|
201
|
-
// 先生成二维码
|
202
262
|
processQRCodeContainers(div);
|
203
|
-
|
204
|
-
container.appendChild(div);
|
263
|
+
fragment.appendChild(div);
|
205
264
|
});
|
265
|
+
container.appendChild(fragment);
|
266
|
+
// 重置滚动状态,因为内容完全变了
|
267
|
+
userScrolled[containerId] = false;
|
206
268
|
}
|
207
|
-
|
208
|
-
|
269
|
+
|
270
|
+
// --- 自动滚动到底部 ---
|
271
|
+
if (wasAtBottom && !userScrolled[containerId]) {
|
209
272
|
container.scrollTop = container.scrollHeight;
|
210
273
|
}
|
211
274
|
}
|
212
275
|
|
276
|
+
|
277
|
+
// --- 下载进度条处理 ---
|
278
|
+
/**
|
279
|
+
* 创建单个下载进度条的 DOM 结构 (!!! 内部逻辑未修改 !!!)
|
280
|
+
* @param {number} i - 索引
|
281
|
+
* @param {number} percentageText - 进度百分比 (0-1)
|
282
|
+
* @param {string} time - 剩余时间
|
283
|
+
* @param {string} speed - 下载速度
|
284
|
+
* @param {string} part - 分片信息
|
285
|
+
* @param {string} status - 状态文本
|
286
|
+
* @param {string} idname - 标识名称
|
287
|
+
* @param {string} nameText - 文件名 (可能需要滚动)
|
288
|
+
* @param {string} file - 文件后缀或类型
|
289
|
+
* @returns {HTMLDivElement} - 创建的进度条容器元素
|
290
|
+
*/
|
291
|
+
function addProgressBar(i, percentageText, time, speed, part, status, idname, nameText, file){
|
292
|
+
const download = document.createElement('div');
|
293
|
+
download.className = 'download-container';
|
294
|
+
// 创建 idname 文本节点(只创建一次)
|
295
|
+
const idnameText = document.createElement('div');
|
296
|
+
idnameText.className = 'scroll-text'; // 可能也需要滚动?根据 CSS 定义
|
297
|
+
idnameText.innerHTML = ' ' + idname; // 前导空格?
|
298
|
+
// 创建文件信息部分
|
299
|
+
const fileInfo = document.createElement('div');
|
300
|
+
fileInfo.className = 'scroll'; // 类名可能与滚动有关
|
301
|
+
const filesuffix = document.createElement('div');
|
302
|
+
filesuffix.className = 'scroll-suffix';
|
303
|
+
filesuffix.innerHTML = file;
|
304
|
+
// 创建滚动文本区域
|
305
|
+
const scroll = document.createElement('div');
|
306
|
+
scroll.className = 'scroll-container';
|
307
|
+
const namebar = document.createElement('div');
|
308
|
+
namebar.className = 'scroll-content';
|
309
|
+
const filename = document.createElement('div');
|
310
|
+
filename.className = 'scroll-text';
|
311
|
+
filename.innerHTML = nameText;
|
312
|
+
// 组合元素
|
313
|
+
namebar.appendChild(filename);
|
314
|
+
scroll.appendChild(namebar);
|
315
|
+
fileInfo.appendChild(scroll);
|
316
|
+
fileInfo.appendChild(filesuffix);
|
317
|
+
download.appendChild(idnameText);
|
318
|
+
download.appendChild(fileInfo);
|
319
|
+
// 延迟测量文本宽度,决定是否滚动 (!!! 原始逻辑 !!!)
|
320
|
+
setTimeout(() => {
|
321
|
+
const contentWidth = filename.scrollWidth; // 单份文本宽度
|
322
|
+
const containerWidth = scroll.clientWidth;
|
323
|
+
if (contentWidth > containerWidth) {
|
324
|
+
// 需要滚动,添加第二份文本实现无缝滚动
|
325
|
+
const filename1 = document.createElement('div');
|
326
|
+
filename1.className = 'scroll-text';
|
327
|
+
filename1.innerHTML = nameText;
|
328
|
+
namebar.appendChild(filename1);
|
329
|
+
// 重新计算宽度,这次是双倍宽度中的单份宽度用于计算时间
|
330
|
+
const singleContentWidth = namebar.scrollWidth / 2;
|
331
|
+
const scrollSpeed = 30; // 滚动速度 (像素/秒)
|
332
|
+
const duration = singleContentWidth / scrollSpeed;
|
333
|
+
namebar.style.animationDuration = duration + 's';
|
334
|
+
// 延迟添加滚动类
|
335
|
+
setTimeout(() => {
|
336
|
+
namebar.classList.add('scrolling');
|
337
|
+
}, 1500); // 1.5秒延迟
|
338
|
+
} else {
|
339
|
+
// 不需要滚动,确保移除动画类和样式
|
340
|
+
namebar.classList.remove('scrolling');
|
341
|
+
namebar.style.animationDuration = '';
|
342
|
+
}
|
343
|
+
}, 0); // 使用 setTimeout 0ms 延迟,等待浏览器渲染后测量
|
344
|
+
// 进度条部分
|
345
|
+
const pbBar = document.createElement('div');
|
346
|
+
pbBar.className = 'pb-bar';
|
347
|
+
const pbProgress = document.createElement('div');
|
348
|
+
pbProgress.className = 'pb-progress pb-animated'; // 假设有动画效果
|
349
|
+
pbProgress.style.width = `${percentageText * 100}%`;
|
350
|
+
pbProgress.id = 'pbProgress' + (i+1); // ID 基于索引
|
351
|
+
const pbStatusText = document.createElement('div');
|
352
|
+
pbStatusText.className = 'pb-status-text';
|
353
|
+
pbStatusText.innerHTML = status;
|
354
|
+
pbStatusText.id = 'pbStatusText' + (i+1);
|
355
|
+
const pbPercentageText = document.createElement('div');
|
356
|
+
pbPercentageText.className = 'pb-percentage-text';
|
357
|
+
pbPercentageText.innerHTML = `${(percentageText * 100).toFixed(2)}%`;
|
358
|
+
pbPercentageText.id = 'pbPercentageText' + (i+1);
|
359
|
+
pbBar.appendChild(pbProgress);
|
360
|
+
pbBar.appendChild(pbStatusText);
|
361
|
+
pbBar.appendChild(pbPercentageText);
|
362
|
+
download.appendChild(pbBar);
|
363
|
+
// 速度、分片、时间部分
|
364
|
+
const speedContainer = document.createElement('div'); // 使用 div 而非 table 可能更灵活
|
365
|
+
speedContainer.className = 'scroll';
|
366
|
+
const speedText = document.createElement('div');
|
367
|
+
speedText.className = 'speed-text';
|
368
|
+
speedText.innerHTML = speed;
|
369
|
+
speedText.id = 'speedText' + (i+1);
|
370
|
+
const partText = document.createElement('div');
|
371
|
+
partText.className = 'speed-text';
|
372
|
+
partText.innerHTML = part;
|
373
|
+
partText.id = 'partText' + (i+1);
|
374
|
+
const timeText = document.createElement('div');
|
375
|
+
timeText.className = 'time-text';
|
376
|
+
timeText.innerHTML = time;
|
377
|
+
timeText.id = 'timeText' + (i+1);
|
378
|
+
speedContainer.appendChild(speedText);
|
379
|
+
speedContainer.appendChild(partText);
|
380
|
+
speedContainer.appendChild(timeText);
|
381
|
+
download.appendChild(speedContainer);
|
382
|
+
return download;
|
383
|
+
}
|
384
|
+
|
385
|
+
/**
|
386
|
+
* 更新下载进度条区域 (!!! 内部逻辑未修改 !!!)
|
387
|
+
* @param {HTMLElement} container - 下载进度条容器 (messageDownload)
|
388
|
+
* @param {Array[]} newMessages - 最新的下载信息数组,每个元素是 [percentage, time, speed, part, status, idname, nameText, file]
|
389
|
+
* @param {Array[]} oldMessages - 上一次的下载信息数组
|
390
|
+
*/
|
213
391
|
function appendBar(container, newMessages, oldMessages) {
|
214
|
-
//
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
if (
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
392
|
+
if (!container) return; // 防御
|
393
|
+
|
394
|
+
const containerId = container.id;
|
395
|
+
const wasAtBottom = !userScrolled[containerId] && (container.scrollHeight - container.scrollTop <= container.clientHeight + 10);
|
396
|
+
const newlength = newMessages.length;
|
397
|
+
const oldlength = oldMessages.length;
|
398
|
+
|
399
|
+
// --- 原始逻辑开始 ---
|
400
|
+
if (newlength > 0) {
|
401
|
+
if (downloadLabel) downloadLabel.textContent = '下载进度:'; // 更新标题
|
402
|
+
|
403
|
+
// --- 更新已存在的进度条 (对应旧消息列表中的最后一项) ---
|
404
|
+
if (oldlength !== 0) {
|
405
|
+
// 假设进度条是按顺序添加的,旧消息的最后一个对应容器中的第一个子元素(因为是 insertBefore(firstChild))
|
406
|
+
// 或者需要更可靠的方式定位?如果 ID 总是连续且从1开始,可以查找 #pbProgress{oldlength} 的父元素
|
407
|
+
// 这里我们假设原始逻辑依赖于 DOM 结构顺序
|
408
|
+
const childToUpdate = container.children[newlength - oldlength]; // 找到对应旧列表最后一项的DOM元素
|
409
|
+
|
410
|
+
if(childToUpdate) {
|
411
|
+
const newMessage = newMessages[oldlength - 1];
|
412
|
+
const oldMessage = oldMessages[oldlength - 1];
|
413
|
+
|
414
|
+
// 比较整个数组是否相同可能更可靠,或者比较关键字段
|
415
|
+
if (JSON.stringify(newMessage) !== JSON.stringify(oldMessage)) {
|
416
|
+
const [percentageText, time, speed, part, status] = newMessage; // 只需要更新这些字段
|
417
|
+
const progressElement = childToUpdate.querySelector('#pbProgress' + oldlength);
|
418
|
+
const statusElement = childToUpdate.querySelector('#pbStatusText' + oldlength);
|
419
|
+
const percentageElement = childToUpdate.querySelector('#pbPercentageText' + oldlength);
|
420
|
+
const speedElement = childToUpdate.querySelector('#speedText' + oldlength);
|
421
|
+
const partElement = childToUpdate.querySelector('#partText' + oldlength);
|
422
|
+
const timeElement = childToUpdate.querySelector('#timeText' + oldlength);
|
423
|
+
|
424
|
+
if (progressElement) progressElement.style.width = `${percentageText * 100}%`;
|
425
|
+
if (statusElement) statusElement.innerHTML = status;
|
426
|
+
if (percentageElement) percentageElement.innerHTML = `${(percentageText * 100).toFixed(2)}%`;
|
427
|
+
if (speedElement) speedElement.innerHTML = speed;
|
428
|
+
if (partElement) partElement.innerHTML = part;
|
429
|
+
if (timeElement) timeElement.innerHTML = time;
|
430
|
+
}
|
431
|
+
} else {
|
432
|
+
console.warn(`appendBar: 尝试更新旧进度条 #${oldlength} 时未找到对应的 DOM 元素。`);
|
433
|
+
}
|
230
434
|
}
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
const
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
filesuffix.className = 'scroll-suffix';
|
247
|
-
filesuffix.innerHTML = file;
|
248
|
-
// 创建滚动文本区域
|
249
|
-
const scroll = document.createElement('div');
|
250
|
-
scroll.className = 'scroll-container';
|
251
|
-
const namebar = document.createElement('div');
|
252
|
-
namebar.className = 'scroll-content';
|
253
|
-
const filename = document.createElement('div');
|
254
|
-
filename.className = 'scroll-text';
|
255
|
-
filename.innerHTML = nameText;
|
256
|
-
// 组合元素
|
257
|
-
namebar.appendChild(filename);
|
258
|
-
scroll.appendChild(namebar);
|
259
|
-
fileInfo.appendChild(scroll);
|
260
|
-
fileInfo.appendChild(filesuffix);
|
261
|
-
download.appendChild(idnameText);
|
262
|
-
download.appendChild(fileInfo);
|
263
|
-
// 延迟测量文本宽度,决定是否滚动
|
264
|
-
setTimeout(() => {
|
265
|
-
const contentWidth = filename.scrollWidth; // 单份文本宽度
|
266
|
-
const containerWidth = scroll.clientWidth;
|
267
|
-
if (contentWidth > containerWidth) {
|
268
|
-
// 需要滚动,添加第二份文本实现无缝滚动
|
269
|
-
const filename1 = document.createElement('div');
|
270
|
-
filename1.className = 'scroll-text';
|
271
|
-
filename1.innerHTML = nameText;
|
272
|
-
namebar.appendChild(filename1);
|
273
|
-
// 重新计算宽度,这次是双倍宽度中的单份宽度用于计算时间
|
274
|
-
const singleContentWidth = namebar.scrollWidth / 2;
|
275
|
-
const speed = 30;
|
276
|
-
const duration = singleContentWidth / speed;
|
277
|
-
namebar.style.animationDuration = duration + 's';
|
278
|
-
// 延迟添加滚动类
|
279
|
-
setTimeout(() => {
|
280
|
-
namebar.classList.add('scrolling');
|
281
|
-
}, 1500);
|
282
|
-
} else {
|
283
|
-
// 不需要滚动,确保移除动画类和样式
|
284
|
-
namebar.classList.remove('scrolling');
|
285
|
-
namebar.style.animationDuration = '';
|
286
|
-
}
|
287
|
-
}, 0);
|
288
|
-
// 进度条部分
|
289
|
-
const pbBar = document.createElement('div');
|
290
|
-
pbBar.className = 'pb-bar';
|
291
|
-
const pbProgress = document.createElement('div');
|
292
|
-
pbProgress.className = 'pb-progress pb-animated';
|
293
|
-
pbProgress.style.width = `${percentageText * 100}%`;
|
294
|
-
pbProgress.id = 'pbProgress' + (i+1);
|
295
|
-
const pbStatusText = document.createElement('div');
|
296
|
-
pbStatusText.className = 'pb-status-text';
|
297
|
-
pbStatusText.innerHTML = status;
|
298
|
-
pbStatusText.id = 'pbStatusText' + (i+1);
|
299
|
-
const pbPercentageText = document.createElement('div');
|
300
|
-
pbPercentageText.className = 'pb-percentage-text';
|
301
|
-
pbPercentageText.innerHTML = `${(percentageText * 100).toFixed(2)}%`;
|
302
|
-
pbPercentageText.id = 'pbPercentageText' + (i+1);
|
303
|
-
pbBar.appendChild(pbProgress);
|
304
|
-
pbBar.appendChild(pbStatusText);
|
305
|
-
pbBar.appendChild(pbPercentageText);
|
306
|
-
download.appendChild(pbBar);
|
307
|
-
// 速度部分
|
308
|
-
const speedContainer = document.createElement('div');
|
309
|
-
speedContainer.className = 'scroll';
|
310
|
-
const speedText = document.createElement('div');
|
311
|
-
speedText.className = 'speed-text';
|
312
|
-
speedText.innerHTML = speed;
|
313
|
-
speedText.id = 'speedText' + (i+1);
|
314
|
-
const timeText = document.createElement('div');
|
315
|
-
timeText.className = 'time-text';
|
316
|
-
timeText.innerHTML = time;
|
317
|
-
timeText.id = 'timeText' + (i+1);
|
318
|
-
speedContainer.appendChild(speedText);
|
319
|
-
speedContainer.appendChild(timeText);
|
320
|
-
download.appendChild(speedContainer);
|
321
|
-
container.insertBefore(download, container.firstChild);
|
435
|
+
|
436
|
+
// --- 添加新的进度条 ---
|
437
|
+
if (newlength !== oldlength) {
|
438
|
+
// *** 优化点:同样可以使用 DocumentFragment 批量添加 ***
|
439
|
+
const fragment = document.createDocumentFragment();
|
440
|
+
for (let i = oldlength; i < newlength; i++) {
|
441
|
+
const messageContent = newMessages[i];
|
442
|
+
// 解构赋值,确保顺序正确
|
443
|
+
const [percentageText, time, speed, part, status, idname, nameText, file] = messageContent;
|
444
|
+
// 调用未修改的 addProgressBar 来创建元素
|
445
|
+
const downloadElement = addProgressBar(i, percentageText, time, speed, part, status, idname, nameText, file);
|
446
|
+
fragment.appendChild(downloadElement);
|
447
|
+
}
|
448
|
+
// 使用 insertBefore 将新的进度条批量插入到容器顶部
|
449
|
+
container.insertBefore(fragment, container.firstChild);
|
322
450
|
}
|
323
|
-
|
451
|
+
} else {
|
452
|
+
// 如果新消息为空,可以选择清空进度条区域或显示提示
|
453
|
+
if (downloadLabel) downloadLabel.textContent = '暂无下载任务';
|
454
|
+
// container.innerHTML = ''; // 清空旧的进度条
|
455
|
+
}
|
456
|
+
// --- 原始逻辑结束 ---
|
457
|
+
|
458
|
+
// --- 自动滚动到底部 (对于进度条区域,通常是希望看到最新的,即顶部,所以可能不需要自动滚动到底部) ---
|
459
|
+
// 如果确实需要滚动到底部(显示旧的在上面):
|
460
|
+
// if (wasAtBottom && !userScrolled[containerId]) {
|
461
|
+
// container.scrollTop = container.scrollHeight;
|
462
|
+
// }
|
463
|
+
// 如果希望保持在顶部看到新添加的:
|
464
|
+
if (newlength > oldlength && !userScrolled[containerId]) { // 如果是新增了进度条且用户没滚动
|
465
|
+
container.scrollTop = 0; // 滚动到顶部
|
466
|
+
} else if (wasAtBottom && !userScrolled[containerId]) { // 如果是更新现有条目且之前在底部
|
467
|
+
container.scrollTop = container.scrollHeight; // 保持在底部
|
324
468
|
}
|
325
469
|
}
|
326
470
|
|
327
|
-
|
471
|
+
|
472
|
+
// --- SSE (Server-Sent Events) 处理 ---
|
473
|
+
/**
|
474
|
+
* 启动 SSE 连接,接收服务器推送的消息
|
475
|
+
*/
|
328
476
|
function startMessageStream() {
|
329
477
|
// 如果已存在连接,先关闭
|
330
478
|
if (eventSource) {
|
479
|
+
console.log("SSE 连接已存在,将重新启动...");
|
331
480
|
eventSource.close();
|
332
481
|
}
|
333
|
-
|
334
|
-
|
482
|
+
|
483
|
+
console.log("正在启动 SSE 连接到 /stream ...");
|
484
|
+
eventSource = new EventSource('/stream');
|
485
|
+
|
335
486
|
// 监听 'message' 事件 (默认事件)
|
336
487
|
eventSource.onmessage = function(event) {
|
337
488
|
try {
|
338
|
-
// event.data 是服务器发送的 JSON 字符串
|
339
489
|
const data = JSON.parse(event.data);
|
340
|
-
|
341
|
-
//
|
342
|
-
|
343
|
-
|
490
|
+
|
491
|
+
// --- 使用接收到的数据更新 UI ---
|
492
|
+
// 确保 lastMessage 和 data 结构完整,提供默认值防止错误
|
493
|
+
lastMessage = lastMessage || { schedule: [], podflow: [], http: [], download: [] };
|
494
|
+
data.schedule = data.schedule || [];
|
344
495
|
data.podflow = data.podflow || [];
|
345
496
|
data.http = data.http || [];
|
346
497
|
data.download = data.download || [];
|
347
|
-
|
348
|
-
|
498
|
+
|
499
|
+
// 1. 更新主进度条
|
500
|
+
// 使用 stringify 比较是为了简单深比较,对于小对象性能影响不大
|
501
|
+
if (JSON.stringify(data.schedule) !== JSON.stringify(lastMessage.schedule)) {
|
349
502
|
updateProgress(data.schedule);
|
350
|
-
lastMessage.schedule = data.schedule;
|
503
|
+
lastMessage.schedule = data.schedule; // 更新缓存
|
351
504
|
}
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
}
|
358
|
-
if (JSON.stringify(data.http) !== JSON.stringify(lastMessage.http)) {
|
359
|
-
appendMessages(messageHttp, data.http, lastMessage.http || []); // 传入旧消息
|
360
|
-
lastMessage.http = [...data.http]; // 创建副本
|
361
|
-
}
|
505
|
+
|
506
|
+
// 2. 更新 Podflow 消息区域
|
507
|
+
if (JSON.stringify(data.podflow) !== JSON.stringify(lastMessage.podflow)) {
|
508
|
+
appendMessages(messageArea, data.podflow, lastMessage.podflow || []);
|
509
|
+
lastMessage.podflow = [...data.podflow]; // 更新缓存 (使用浅拷贝)
|
362
510
|
}
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
lastMessage.
|
511
|
+
|
512
|
+
// 3. 更新 HTTP 消息区域
|
513
|
+
if (JSON.stringify(data.http) !== JSON.stringify(lastMessage.http)) {
|
514
|
+
appendMessages(messageHttp, data.http, lastMessage.http || []);
|
515
|
+
lastMessage.http = [...data.http]; // 更新缓存 (使用浅拷贝)
|
367
516
|
}
|
517
|
+
|
518
|
+
// 4. 更新下载进度条区域
|
519
|
+
if (JSON.stringify(data.download) !== JSON.stringify(lastMessage.download)) {
|
520
|
+
appendBar(messageDownload, data.download, lastMessage.download || []);
|
521
|
+
lastMessage.download = [...data.download]; // 更新缓存 (使用浅拷贝)
|
522
|
+
}
|
523
|
+
|
368
524
|
} catch (error) {
|
369
525
|
console.error('处理 SSE 消息失败:', error, '原始数据:', event.data);
|
370
526
|
}
|
371
527
|
};
|
528
|
+
|
372
529
|
// 监听错误事件
|
373
530
|
eventSource.onerror = function(error) {
|
374
|
-
console.error('
|
375
|
-
//
|
376
|
-
//
|
531
|
+
console.error('SSE 连接发生错误:', error);
|
532
|
+
// EventSource 默认会自动尝试重连,通常不需要手动处理
|
533
|
+
// 如果需要彻底停止,可以在这里关闭
|
534
|
+
// eventSource.close();
|
535
|
+
// eventSource = null;
|
536
|
+
// 可以考虑在这里添加 UI 提示,告知用户连接中断
|
377
537
|
};
|
378
|
-
|
538
|
+
|
539
|
+
// 监听连接打开事件 (可选)
|
540
|
+
eventSource.onopen = function() {
|
541
|
+
console.log("SSE 连接已成功建立。");
|
542
|
+
// 连接成功后,可能需要立即获取一次全量数据?或者依赖于服务器推送
|
543
|
+
};
|
544
|
+
|
545
|
+
console.log("SSE 事件监听器已设置。");
|
379
546
|
}
|
380
547
|
|
381
|
-
|
548
|
+
/**
|
549
|
+
* 停止 SSE 连接
|
550
|
+
*/
|
382
551
|
function stopMessageStream() {
|
383
552
|
if (eventSource) {
|
384
553
|
eventSource.close(); // 关闭连接
|
385
554
|
eventSource = null; // 清除引用
|
386
|
-
console.log("SSE
|
555
|
+
console.log("SSE 连接已关闭。");
|
387
556
|
}
|
388
557
|
}
|
389
558
|
|
390
|
-
//
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
// 初始化默认页面
|
396
|
-
showPage('pageMessage');
|
559
|
+
// --- 事件监听器绑定 ---
|
560
|
+
// 滚动事件 (确保所有相关容器都监听)
|
561
|
+
if (messageArea) messageArea.addEventListener('scroll', onUserScroll);
|
562
|
+
if (messageHttp) messageHttp.addEventListener('scroll', onUserScroll);
|
563
|
+
if (messageDownload) messageDownload.addEventListener('scroll', onUserScroll); // 添加 messageDownload 的监听
|
397
564
|
|
398
|
-
//
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
565
|
+
// Channel ID 表单异步提交
|
566
|
+
if (inputForm) {
|
567
|
+
inputForm.addEventListener('submit', function(event) {
|
568
|
+
event.preventDefault(); // 阻止表单默认提交行为
|
569
|
+
const content = inputOutput.value.trim(); // 获取并去除首尾空格
|
570
|
+
if (!content) {
|
571
|
+
alert('请输入内容!');
|
572
|
+
return;
|
573
|
+
}
|
574
|
+
// 可以添加一个加载状态提示
|
575
|
+
console.log("正在提交内容获取 Channel ID...");
|
576
|
+
fetch('getid', {
|
577
|
+
method: 'POST',
|
578
|
+
headers: {
|
579
|
+
'Content-Type': 'application/json',
|
580
|
+
'Accept': 'application/json' // 明确希望接收 JSON
|
581
|
+
},
|
582
|
+
body: JSON.stringify({ content: content }) // 发送 JSON 数据
|
583
|
+
})
|
407
584
|
.then(response => {
|
408
585
|
if (!response.ok) {
|
409
|
-
|
586
|
+
// 如果服务器返回错误状态码,尝试读取错误信息
|
587
|
+
return response.json().catch(() => {
|
588
|
+
// 如果无法解析 JSON 错误体,抛出通用错误
|
589
|
+
throw new Error(`网络响应错误: ${response.status} ${response.statusText}`);
|
590
|
+
}).then(errorData => {
|
591
|
+
// 如果解析成功,抛出包含服务器信息的错误
|
592
|
+
throw new Error(errorData.error || `请求失败: ${response.status}`);
|
593
|
+
});
|
594
|
+
}
|
595
|
+
return response.json(); // 解析成功的 JSON 响应
|
596
|
+
})
|
597
|
+
.then(data => {
|
598
|
+
if (data && data.response) {
|
599
|
+
inputOutput.value = data.response; // 更新输入框内容
|
600
|
+
console.log("成功获取 Channel ID:", data.response);
|
601
|
+
} else {
|
602
|
+
console.warn("服务器响应格式不正确:", data);
|
603
|
+
alert('服务器返回数据格式错误!');
|
410
604
|
}
|
411
|
-
return response.json();
|
412
605
|
})
|
413
|
-
.then(data => inputOutput.value = data.response)
|
414
606
|
.catch(error => {
|
415
|
-
console.error('请求失败:', error);
|
416
|
-
alert('
|
417
|
-
});
|
418
|
-
});
|
419
|
-
|
420
|
-
// 粘贴功能
|
421
|
-
pasteBtn.addEventListener('click', function() {
|
422
|
-
if (navigator.clipboard && navigator.clipboard.readText) {
|
423
|
-
navigator.clipboard.readText().then(text => {
|
424
|
-
inputOutput.value = text;
|
425
|
-
inputOutput.focus();
|
426
|
-
}).catch(err => {
|
427
|
-
console.warn("剪贴板读取失败:", err);
|
428
|
-
alert("无法读取剪贴板,请手动粘贴!");
|
607
|
+
console.error('获取 Channel ID 请求失败:', error);
|
608
|
+
alert(`请求失败:${error.message || '请检查网络或联系管理员'}`);
|
429
609
|
});
|
430
|
-
}
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
610
|
+
});
|
611
|
+
}
|
612
|
+
|
613
|
+
// 粘贴按钮
|
614
|
+
if (pasteBtn) {
|
615
|
+
pasteBtn.addEventListener('click', function() {
|
616
|
+
if (navigator.clipboard && navigator.clipboard.readText) {
|
617
|
+
navigator.clipboard.readText()
|
618
|
+
.then(text => {
|
619
|
+
inputOutput.value = text;
|
620
|
+
inputOutput.focus(); // 粘贴后聚焦
|
621
|
+
})
|
622
|
+
.catch(err => {
|
623
|
+
console.warn("通过 navigator.clipboard 读取剪贴板失败:", err);
|
624
|
+
alert("无法自动读取剪贴板,请手动粘贴 (Ctrl+V)!");
|
625
|
+
inputOutput.focus(); // 提示后聚焦,方便手动粘贴
|
626
|
+
});
|
627
|
+
} else {
|
628
|
+
// 备选方案 (可能在非 HTTPS 或旧浏览器中需要)
|
629
|
+
try {
|
630
|
+
inputOutput.focus();
|
631
|
+
// execCommand 已不推荐使用,但作为后备
|
632
|
+
if (!document.execCommand('paste')) {
|
633
|
+
throw new Error('execCommand paste failed');
|
634
|
+
}
|
635
|
+
} catch (err) {
|
636
|
+
console.warn("execCommand 粘贴失败:", err);
|
637
|
+
alert("您的浏览器不支持自动粘贴,请手动操作 (Ctrl+V)!");
|
638
|
+
inputOutput.focus();
|
639
|
+
}
|
437
640
|
}
|
438
|
-
}
|
439
|
-
}
|
440
|
-
|
441
|
-
//
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
641
|
+
});
|
642
|
+
}
|
643
|
+
|
644
|
+
// 复制按钮
|
645
|
+
if (copyBtn) {
|
646
|
+
copyBtn.addEventListener('click', function() {
|
647
|
+
const textToCopy = inputOutput.value;
|
648
|
+
if (!textToCopy) return; // 没有内容则不复制
|
649
|
+
|
650
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
651
|
+
navigator.clipboard.writeText(textToCopy)
|
652
|
+
.then(() => {
|
653
|
+
// 可以给用户一个成功的视觉反馈,例如按钮变色或提示
|
654
|
+
console.log("内容已复制到剪贴板");
|
655
|
+
// alert("已复制!"); // 或者使用更友好的提示方式
|
656
|
+
})
|
657
|
+
.catch(err => {
|
658
|
+
console.warn("通过 navigator.clipboard 写入剪贴板失败:", err);
|
659
|
+
alert("自动复制失败,请手动选择文本后按 Ctrl+C 复制!");
|
660
|
+
inputOutput.select(); // 选中内容方便手动复制
|
661
|
+
});
|
662
|
+
} else {
|
663
|
+
// 备选方案
|
664
|
+
try {
|
665
|
+
inputOutput.select(); // 选中输入框中的文本
|
666
|
+
if (!document.execCommand('copy')) { // 执行复制命令
|
667
|
+
throw new Error('execCommand copy failed');
|
668
|
+
}
|
669
|
+
console.log("内容已通过 execCommand 复制");
|
670
|
+
// alert("已复制!");
|
671
|
+
} catch (err) {
|
672
|
+
console.warn("execCommand 复制失败:", err);
|
673
|
+
alert("您的浏览器不支持自动复制,请手动选择文本后按 Ctrl+C 复制!");
|
674
|
+
inputOutput.select();
|
675
|
+
}
|
455
676
|
}
|
456
|
-
}
|
457
|
-
}
|
458
|
-
|
459
|
-
// 清空输入框
|
460
|
-
clearBtn.addEventListener('click', function() {
|
461
|
-
inputOutput.value = '';
|
462
|
-
});
|
463
|
-
|
464
|
-
// 菜单项点击事件委托
|
465
|
-
menu.addEventListener('click', function(event) {
|
466
|
-
const target = event.target;
|
467
|
-
if (target.tagName.toLowerCase() === 'li' && target.dataset.page) {
|
468
|
-
showPage(target.dataset.page);
|
469
|
-
}
|
470
|
-
});
|
677
|
+
});
|
678
|
+
}
|
471
679
|
|
472
|
-
//
|
473
|
-
|
680
|
+
// 清空按钮
|
681
|
+
if (clearBtn) {
|
682
|
+
clearBtn.addEventListener('click', function() {
|
683
|
+
inputOutput.value = '';
|
684
|
+
inputOutput.focus(); // 清空后聚焦
|
685
|
+
});
|
686
|
+
}
|
687
|
+
|
688
|
+
// 菜单项点击事件 (使用事件委托)
|
689
|
+
if (menu) {
|
690
|
+
menu.addEventListener('click', function(event) {
|
691
|
+
// 确保点击的是 LI 元素且具有 data-page 属性
|
692
|
+
const target = event.target.closest('li[data-page]');
|
693
|
+
if (target) {
|
694
|
+
const pageId = target.dataset.page;
|
695
|
+
showPage(pageId); // 调用页面切换函数
|
696
|
+
}
|
697
|
+
});
|
698
|
+
}
|
474
699
|
|
475
|
-
//
|
700
|
+
// 菜单切换按钮
|
701
|
+
if (toggleMenuBtn) {
|
702
|
+
toggleMenuBtn.addEventListener('click', toggleMenu);
|
703
|
+
}
|
704
|
+
|
705
|
+
// --- 初始化 ---
|
706
|
+
// 根据屏幕宽度初始化菜单状态
|
476
707
|
if (window.innerWidth <= 600) {
|
477
708
|
menu.classList.add('hidden');
|
478
709
|
toggleMenuBtn.style.left = '0px';
|
479
710
|
toggleMenuBtn.textContent = '❯';
|
711
|
+
} else {
|
712
|
+
// 确保大屏幕下按钮初始状态正确 (如果默认不是展开的话)
|
713
|
+
if (menu.classList.contains('hidden')) {
|
714
|
+
toggleMenuBtn.style.left = '0px';
|
715
|
+
toggleMenuBtn.textContent = '❯';
|
716
|
+
} else {
|
717
|
+
toggleMenuBtn.style.left = 'var(--menu-width)';
|
718
|
+
toggleMenuBtn.textContent = '❮';
|
719
|
+
}
|
480
720
|
}
|
481
|
-
|
721
|
+
|
722
|
+
// 初始化时显示默认页面 (例如消息页面)
|
723
|
+
showPage('pageMessage');
|
724
|
+
|
725
|
+
})(); // IIFE 结束
|
@@ -2,7 +2,7 @@ podflow/__init__.py,sha256=aXqsJ8qN7VHvMdcgtRwR1p95my8mj9QsjOHklypRBiU,7381
|
|
2
2
|
podflow/download_and_build.py,sha256=GKQ1uX8Nuwdhr4wgnGr3jP1Mu0llRUPFcboQ3S05WkU,671
|
3
3
|
podflow/ffmpeg_judge.py,sha256=wM49pPXOFwFAA_8TKHal5fV6ka9sAA87yGQMDOssvXo,1340
|
4
4
|
podflow/main.py,sha256=Cz2E33-Kcc_1_oxNs4Z1OoqJYhonmClsrtoCW1oQmZA,739
|
5
|
-
podflow/main_podcast.py,sha256=
|
5
|
+
podflow/main_podcast.py,sha256=D1eBxeGNks0q8_zhVG2uUuE1pBcPEXGDAfd1CwAtuOs,12321
|
6
6
|
podflow/main_upload.py,sha256=H_T5KQMYzToqzQbjGQ6DWDGziy8iMnpmf7A1qOStJuo,2296
|
7
7
|
podflow/parse_arguments.py,sha256=h3a7EaRZS04kNMFYbxTW9Ch29KgZ7dyS-yqEEt_etQI,2592
|
8
8
|
podflow/basic/__init__.py,sha256=CAfI6mVQtz7KKbAiTIZ9_IbvaTXeAqxR1U7ov9GDoDo,44
|
@@ -36,13 +36,13 @@ podflow/download/__init__.py,sha256=1lATXiOAEx5oDUDR99mQRiTrQlQ9CQkJNAKpsLrnPCo,
|
|
36
36
|
podflow/download/convert_bytes.py,sha256=6Q3TcPGzCO2FlhOKWbp9RB_GPmfuyKY5suIyE9EJf6k,543
|
37
37
|
podflow/download/delete_part.py,sha256=wjY6WulpUMjLx38on0kTLXj0gA04rIuKAdwFidZtWGU,679
|
38
38
|
podflow/download/dl_aideo_video.py,sha256=HoiAYf1QpHG17U9y--ixKOT2aupeGA2vAleVvC1JoV8,10765
|
39
|
-
podflow/download/show_progress.py,sha256=
|
39
|
+
podflow/download/show_progress.py,sha256=y46chchUC9eZCg4ZdNMFnx_bXJQV_IUq15jVzZtLlJM,2248
|
40
40
|
podflow/download/wait_animation.py,sha256=AUTvszXF89QA7XYjocFIauPKV7Qj8cFqry44teClaLQ,1314
|
41
41
|
podflow/download/youtube_and_bilibili_download.py,sha256=VCEhz6pGXFWXusdbGWqkCzi4f4VsKQVn6sZz1pfGsns,1335
|
42
42
|
podflow/httpfs/__init__.py,sha256=BxEXkufjcx-a0F7sDVXo65hmyANqCCbZUd6EH9i8T2c,45
|
43
|
-
podflow/httpfs/app_bottle.py,sha256=
|
43
|
+
podflow/httpfs/app_bottle.py,sha256=_6yXYEMf9hxNwnj1kC1Sjf--fca3P8wadlmTKpcKSu8,20680
|
44
44
|
podflow/httpfs/browser.py,sha256=BJ4Xkfiki_tDr0Sc9RqAcEfIVpkAZ3RFOwo0aMHlY3U,197
|
45
|
-
podflow/httpfs/download_bar.py,sha256=
|
45
|
+
podflow/httpfs/download_bar.py,sha256=0n3HATEO3pdsIpx-E_IZG9OlXa6u-9SeBCoZVgUutyc,965
|
46
46
|
podflow/httpfs/get_channelid.py,sha256=gcwy4IVHBWNQz7qPCpjwiAklGFLRGzvM33-UZz7oFvo,2296
|
47
47
|
podflow/httpfs/port_judge.py,sha256=l_nLpsSiIhAzfJGCOWyYC-KkCCWPUW2ybe_5hdMOEzE,764
|
48
48
|
podflow/httpfs/progress_bar.py,sha256=mMkP92SrZZ-IBFz9ombWxzRJfMq20h5IbSc_eFcbZZA,682
|
@@ -83,9 +83,9 @@ podflow/remove/remove_dir.py,sha256=xQIhrnqnYjMzXjoSWaTvm7JwPYOFTN1muuTPdaLDXpQ,
|
|
83
83
|
podflow/remove/remove_file.py,sha256=8wAJQehs-XBqvu0vPlEme2_tt0FZxc5ELwGMxXA_558,982
|
84
84
|
podflow/repair/__init__.py,sha256=Gpc1i6xiSLodKjjmzH66c_Y1z0HQ9E9CS3p95FRnVFM,45
|
85
85
|
podflow/repair/reverse_log.py,sha256=Wc_vAH0WB-z1fNdWx7FYaVH4caRPtot7tDwDwFhmpz4,1106
|
86
|
-
podflow/templates/index.html,sha256=
|
87
|
-
podflow/templates/css/index.css,sha256=
|
88
|
-
podflow/templates/js/index.js,sha256=
|
86
|
+
podflow/templates/index.html,sha256=QFzKmMVfEgwaP-KRGJ_PuU178IS1G3Eojaw55R8aJC4,2302
|
87
|
+
podflow/templates/css/index.css,sha256=dgrlonF2BYdAtIFYpDChvSBnXAndbdoyY6c2ysOgdL0,9641
|
88
|
+
podflow/templates/js/index.js,sha256=D_vcWiUzWjdvaSc0zvk3wSwX3G5wjAfnFjBaBNnvtHI,30465
|
89
89
|
podflow/templates/js/qrcode.min.js,sha256=xUHvBjJ4hahBW8qN9gceFBibSFUzbe9PNttUvehITzY,19927
|
90
90
|
podflow/upload/__init__.py,sha256=AtOSXDrE5EjUe3z-iBd1NTDaH8n_X9qA5WXdBLkONjA,45
|
91
91
|
podflow/upload/add_upload.py,sha256=_2-V0z75Lwu-PUCfMD9HOSxZTB102yZlZW5hSdlHcsc,1432
|
@@ -101,8 +101,8 @@ podflow/youtube/__init__.py,sha256=pgXod8gq0IijZxIkPSwgAOcb9JI5rd1mqMomoR7bcJ4,4
|
|
101
101
|
podflow/youtube/build.py,sha256=67m74rQOrvqU6ZZybP85ORpg3pOCkpoikttBbtO_PxY,12315
|
102
102
|
podflow/youtube/get.py,sha256=oO32GjTFvUgP5AfFX5AlIuXU2UT6QtOUOXWLFzi8XtI,17157
|
103
103
|
podflow/youtube/login.py,sha256=KYl--ya6Z1u0uIcOp9l8i3DIIj9hsYUDH4dtJjI0MLM,1295
|
104
|
-
podflow-
|
105
|
-
podflow-
|
106
|
-
podflow-
|
107
|
-
podflow-
|
108
|
-
podflow-
|
104
|
+
podflow-20250429.dist-info/METADATA,sha256=CpRALr7Yyq_jsbypM6B-xyGBfiYb0PROY1GmNRr2FJ8,14195
|
105
|
+
podflow-20250429.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
106
|
+
podflow-20250429.dist-info/entry_points.txt,sha256=mn7hD_c_dmpKe3XU0KNekheBvD01LhlJ9htY-Df0j2A,131
|
107
|
+
podflow-20250429.dist-info/top_level.txt,sha256=fUujhhz-RrMI8aGvi-3Ey5y7FQnpOOgoFw9OWM3yLCU,8
|
108
|
+
podflow-20250429.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|