tronclass-auto 1.7.0 → 1.7.1
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.
- package/package.json +1 -1
- package/src/index.js +17 -6
- package/src/tui.js +275 -409
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -273,16 +273,17 @@ async function showStatus() {
|
|
|
273
273
|
const browser = await chromium.launch(launchOpts);
|
|
274
274
|
const context = await browser.newContext();
|
|
275
275
|
await context.addCookies(cookies);
|
|
276
|
-
const page = await context.newPage();
|
|
277
276
|
|
|
278
|
-
await
|
|
279
|
-
await
|
|
277
|
+
const checkPage = await context.newPage();
|
|
278
|
+
await checkPage.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
279
|
+
await checkPage.waitForTimeout(2000);
|
|
280
280
|
|
|
281
|
-
if (
|
|
281
|
+
if (checkPage.url().includes('login')) {
|
|
282
282
|
console.log(chalk.red('[ERROR] Cookie 過期'));
|
|
283
283
|
await browser.close();
|
|
284
284
|
return;
|
|
285
285
|
}
|
|
286
|
+
await checkPage.close();
|
|
286
287
|
|
|
287
288
|
const courses = config.courses || [];
|
|
288
289
|
console.log(chalk.cyan('\n┌' + '─'.repeat(48) + '┐'));
|
|
@@ -293,11 +294,21 @@ async function showStatus() {
|
|
|
293
294
|
let totalRemaining = 0;
|
|
294
295
|
let totalExams = 0;
|
|
295
296
|
|
|
296
|
-
|
|
297
|
+
const courseResults = await Promise.all(courses.map(async (courseUrl) => {
|
|
297
298
|
const match = courseUrl.match(/\/course\/(\d+)\//);
|
|
298
299
|
const courseId = match ? match[1] : '?';
|
|
299
300
|
try {
|
|
300
|
-
const
|
|
301
|
+
const pg = await context.newPage();
|
|
302
|
+
const allVideos = await getAllVideos(pg, courseId);
|
|
303
|
+
await pg.close();
|
|
304
|
+
return { courseId, allVideos };
|
|
305
|
+
} catch (e) {
|
|
306
|
+
return { courseId, allVideos: [] };
|
|
307
|
+
}
|
|
308
|
+
}));
|
|
309
|
+
|
|
310
|
+
for (const { courseId, allVideos } of courseResults) {
|
|
311
|
+
try {
|
|
301
312
|
const videos = allVideos.filter(u => u.type === 'online_video');
|
|
302
313
|
const completed = videos.filter(v => v.completeness === 'full').length;
|
|
303
314
|
const future = videos.filter(v => v.isFuture && v.completeness !== 'full').length;
|
package/src/tui.js
CHANGED
|
@@ -1,59 +1,141 @@
|
|
|
1
|
-
const
|
|
1
|
+
const readline = require('readline');
|
|
2
2
|
const chalk = require('chalk');
|
|
3
3
|
const { loadConfig, saveConfig, BASE_URL } = require('./config');
|
|
4
4
|
const { loadCookies } = require('./auth');
|
|
5
5
|
const pkg = require('../package.json');
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
timeout: 15000
|
|
12
|
-
});
|
|
13
|
-
await page.waitForTimeout(3000);
|
|
14
|
-
return await page.evaluate(() => {
|
|
15
|
-
const el = document.querySelector('.learning-activities');
|
|
16
|
-
if (!el) return null;
|
|
17
|
-
const scope = angular.element(el).scope();
|
|
18
|
-
if (!scope) return null;
|
|
19
|
-
return scope.course ? scope.course.title || scope.course.name : null;
|
|
20
|
-
});
|
|
21
|
-
} catch (e) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
7
|
+
function box(lines, width = 52) {
|
|
8
|
+
console.log(chalk.cyan('╔' + '═'.repeat(width - 2) + '╗'));
|
|
9
|
+
lines.forEach(l => console.log(chalk.cyan('║ ') + l.padEnd(width - 4) + chalk.cyan(' ║')));
|
|
10
|
+
console.log(chalk.cyan('╚' + '═'.repeat(width - 2) + '╝'));
|
|
24
11
|
}
|
|
25
12
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
13
|
+
function promptList(message, choices, header) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
let selected = 0;
|
|
16
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
17
|
+
|
|
18
|
+
function render() {
|
|
19
|
+
const lines = [];
|
|
20
|
+
lines.push('');
|
|
21
|
+
choices.forEach((c, i) => {
|
|
22
|
+
const prefix = i === selected ? chalk.green('>') : ' ';
|
|
23
|
+
if (typeof c === 'string' && c === '__sep__') {
|
|
24
|
+
lines.push(chalk.gray(' ─────────────'));
|
|
25
|
+
} else {
|
|
26
|
+
const label = typeof c === 'string' ? c : c.name;
|
|
27
|
+
lines.push(` ${prefix} ${label}`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push(chalk.gray(' ↑↓ 移動 Enter 確認 ESC 返回'));
|
|
32
|
+
|
|
33
|
+
const headerLines = header || [];
|
|
34
|
+
const totalLines = headerLines.length + lines.length;
|
|
35
|
+
|
|
36
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
37
|
+
headerLines.forEach(l => process.stdout.write(l + '\n'));
|
|
38
|
+
lines.forEach(l => process.stdout.write(l + '\n'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
render();
|
|
42
|
+
|
|
43
|
+
process.stdin.setRawMode(true);
|
|
44
|
+
process.stdin.resume();
|
|
45
|
+
|
|
46
|
+
function onData(data) {
|
|
47
|
+
const key = data.toString();
|
|
48
|
+
|
|
49
|
+
if (key === '\u001b[A') {
|
|
50
|
+
selected = Math.max(0, selected - 1);
|
|
51
|
+
while (selected < choices.length && (typeof choices[selected] === 'string' || choices[selected].value === '__sep__')) {
|
|
52
|
+
selected = Math.max(0, selected - 1);
|
|
53
|
+
}
|
|
54
|
+
render();
|
|
55
|
+
} else if (key === '\u001b[B') {
|
|
56
|
+
selected = Math.min(choices.length - 1, selected + 1);
|
|
57
|
+
while (selected < choices.length && (typeof choices[selected] === 'string' || choices[selected].value === '__sep__')) {
|
|
58
|
+
selected = Math.min(choices.length - 1, selected + 1);
|
|
59
|
+
}
|
|
60
|
+
render();
|
|
61
|
+
} else if (key === '\r' || key === '\n') {
|
|
62
|
+
cleanup();
|
|
63
|
+
const choice = choices[selected];
|
|
64
|
+
resolve(typeof choice === 'string' ? choice : choice.value);
|
|
65
|
+
} else if (key === '\u001b') {
|
|
66
|
+
cleanup();
|
|
67
|
+
resolve('__esc__');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cleanup() {
|
|
72
|
+
process.stdin.removeListener('data', onData);
|
|
34
73
|
process.stdin.setRawMode(false);
|
|
35
74
|
process.stdin.pause();
|
|
36
|
-
|
|
75
|
+
rl.close();
|
|
37
76
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
77
|
+
|
|
78
|
+
process.stdin.on('data', onData);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function promptConfirm(message, defaultVal = true) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
85
|
+
process.stdout.write(` ${message} (${defaultVal ? 'Y/n' : 'y/N'}) `);
|
|
86
|
+
|
|
87
|
+
process.stdin.setRawMode(true);
|
|
88
|
+
process.stdin.resume();
|
|
89
|
+
|
|
90
|
+
function onData(data) {
|
|
91
|
+
const key = data.toString();
|
|
92
|
+
if (key === '\r' || key === '\n') {
|
|
93
|
+
cleanup();
|
|
94
|
+
resolve(defaultVal);
|
|
95
|
+
} else if (key === '\u001b') {
|
|
96
|
+
cleanup();
|
|
97
|
+
resolve(null);
|
|
98
|
+
} else if (key.toLowerCase() === 'y') {
|
|
99
|
+
cleanup();
|
|
100
|
+
resolve(true);
|
|
101
|
+
} else if (key.toLowerCase() === 'n') {
|
|
102
|
+
cleanup();
|
|
103
|
+
resolve(false);
|
|
104
|
+
}
|
|
45
105
|
}
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
|
|
107
|
+
function cleanup() {
|
|
108
|
+
process.stdin.removeListener('data', onData);
|
|
109
|
+
process.stdin.setRawMode(false);
|
|
110
|
+
process.stdin.pause();
|
|
111
|
+
rl.close();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
process.stdin.on('data', onData);
|
|
115
|
+
});
|
|
48
116
|
}
|
|
49
117
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
118
|
+
function promptInput(message) {
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
121
|
+
rl.question(` ${message}: `, (answer) => {
|
|
122
|
+
rl.close();
|
|
123
|
+
resolve(answer);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function pressAnyKey() {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
process.stdout.write(chalk.gray('\n 按任意鍵返回...'));
|
|
131
|
+
process.stdin.setRawMode(true);
|
|
132
|
+
process.stdin.resume();
|
|
133
|
+
process.stdin.once('data', () => {
|
|
134
|
+
process.stdin.setRawMode(false);
|
|
135
|
+
process.stdin.pause();
|
|
136
|
+
resolve();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
57
139
|
}
|
|
58
140
|
|
|
59
141
|
async function mainMenu() {
|
|
@@ -65,47 +147,33 @@ async function mainMenu() {
|
|
|
65
147
|
|
|
66
148
|
let running = true;
|
|
67
149
|
while (running) {
|
|
68
|
-
console.clear();
|
|
69
150
|
const config = loadConfig();
|
|
70
151
|
|
|
152
|
+
console.clear();
|
|
71
153
|
box([
|
|
72
154
|
chalk.bold.white(`tronclass-auto v${pkg.version}`),
|
|
73
155
|
chalk.gray('自動觀看 eclass/TronClass 影片'),
|
|
74
156
|
'',
|
|
75
|
-
`課程: ${chalk.cyan(config.courses.length
|
|
157
|
+
`課程: ${chalk.cyan(config.courses.length)} 個 | Cookie: ${chalk.green('✓')} | 倍速: ${chalk.yellow(config.playbackRate || 2)}x`,
|
|
76
158
|
]);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
choices: [
|
|
96
|
-
{ name: chalk.green('▶ 開始自動觀看'), value: 'run' },
|
|
97
|
-
{ name: chalk.cyan('📋 管理課程列表'), value: 'courses' },
|
|
98
|
-
{ name: chalk.yellow('📊 查看進度統計'), value: 'status' },
|
|
99
|
-
{ name: chalk.magenta('⚙ 設定'), value: 'settings' },
|
|
100
|
-
new inquirer.Separator(),
|
|
101
|
-
{ name: chalk.gray('🚪 離開'), value: 'exit' }
|
|
102
|
-
],
|
|
103
|
-
pageSize: 10
|
|
104
|
-
}]).then(ans => {
|
|
105
|
-
process.stdin.removeListener('data', escHandler);
|
|
106
|
-
resolve(ans);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
159
|
+
|
|
160
|
+
const action = await promptList('選擇操作:', [
|
|
161
|
+
{ name: chalk.green('▶ 開始自動觀看'), value: 'run' },
|
|
162
|
+
{ name: chalk.cyan('📋 管理課程列表'), value: 'courses' },
|
|
163
|
+
{ name: chalk.yellow('📊 查看進度統計'), value: 'status' },
|
|
164
|
+
{ name: chalk.magenta('⚙ 設定'), value: 'settings' },
|
|
165
|
+
'__sep__',
|
|
166
|
+
{ name: chalk.gray('🚪 離開'), value: 'exit' }
|
|
167
|
+
], [
|
|
168
|
+
'',
|
|
169
|
+
chalk.white('╔' + '═'.repeat(50) + '╗'),
|
|
170
|
+
chalk.white('║') + chalk.bold.white(` tronclass-auto v${pkg.version}`.padEnd(50)) + chalk.white('║'),
|
|
171
|
+
chalk.white('║') + chalk.gray(' 自動觀看 eclass/TronClass 影片'.padEnd(50)) + chalk.white('║'),
|
|
172
|
+
chalk.white('║') + ` 課程: ${chalk.cyan(config.courses.length)} 個 | Cookie: ${chalk.green('✓')} | 倍速: ${chalk.yellow(config.playbackRate || 2)}x`.padEnd(50) + chalk.white('║'),
|
|
173
|
+
chalk.white('╚' + '═'.repeat(50) + '╝'),
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
if (action === '__esc__') { running = false; continue; }
|
|
109
177
|
|
|
110
178
|
switch (action) {
|
|
111
179
|
case 'run': await runMenu(config); break;
|
|
@@ -130,52 +198,11 @@ async function runMenu(config) {
|
|
|
130
198
|
return;
|
|
131
199
|
}
|
|
132
200
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
process.stdin.resume();
|
|
136
|
-
const escHandler = (data) => {
|
|
137
|
-
if (data.toString() === '\u001b') {
|
|
138
|
-
process.stdin.removeListener('data', escHandler);
|
|
139
|
-
process.stdin.setRawMode(false);
|
|
140
|
-
process.stdin.pause();
|
|
141
|
-
resolve({ confirm: false });
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
process.stdin.on('data', escHandler);
|
|
145
|
-
inquirer.prompt([{
|
|
146
|
-
type: 'confirm',
|
|
147
|
-
name: 'confirm',
|
|
148
|
-
message: `開始觀看全部 ${config.courses.length} 個課程?`,
|
|
149
|
-
default: true
|
|
150
|
-
}]).then(ans => {
|
|
151
|
-
process.stdin.removeListener('data', escHandler);
|
|
152
|
-
resolve(ans);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
if (!confirm) return;
|
|
201
|
+
const confirm = await promptConfirm(`開始觀看全部 ${config.courses.length} 個課程?`);
|
|
202
|
+
if (confirm === null) return;
|
|
156
203
|
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
process.stdin.resume();
|
|
160
|
-
const escHandler = (data) => {
|
|
161
|
-
if (data.toString() === '\u001b') {
|
|
162
|
-
process.stdin.removeListener('data', escHandler);
|
|
163
|
-
process.stdin.setRawMode(false);
|
|
164
|
-
process.stdin.pause();
|
|
165
|
-
resolve({ headless: false });
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
process.stdin.on('data', escHandler);
|
|
169
|
-
inquirer.prompt([{
|
|
170
|
-
type: 'confirm',
|
|
171
|
-
name: 'headless',
|
|
172
|
-
message: '無頭模式(背景執行)?',
|
|
173
|
-
default: false
|
|
174
|
-
}]).then(ans => {
|
|
175
|
-
process.stdin.removeListener('data', escHandler);
|
|
176
|
-
resolve(ans);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
204
|
+
const headless = await promptConfirm('無頭模式(背景執行)?', false);
|
|
205
|
+
if (headless === null) return;
|
|
179
206
|
|
|
180
207
|
console.log('');
|
|
181
208
|
box([
|
|
@@ -193,181 +220,104 @@ async function runMenu(config) {
|
|
|
193
220
|
|
|
194
221
|
async function courseMenu() {
|
|
195
222
|
let editing = true;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (!browser) {
|
|
202
|
-
const { loadCookies } = require('./auth');
|
|
203
|
-
const cookies = loadCookies();
|
|
204
|
-
const exePath = await ensureBrowser();
|
|
205
|
-
const launchOpts = { headless: true, slowMo: 50, args: ['--disable-gpu', '--disable-software-rasterizer'] };
|
|
206
|
-
if (exePath) launchOpts.executablePath = exePath;
|
|
207
|
-
browser = await chromium.launch(launchOpts);
|
|
208
|
-
const context = await browser.newContext();
|
|
209
|
-
await context.addCookies(cookies);
|
|
210
|
-
return await context.newPage();
|
|
211
|
-
}
|
|
212
|
-
return browser.newPage();
|
|
213
|
-
}
|
|
223
|
+
while (editing) {
|
|
224
|
+
const config = loadConfig();
|
|
225
|
+
const courses = config.courses || [];
|
|
226
|
+
|
|
227
|
+
console.clear();
|
|
214
228
|
|
|
215
|
-
|
|
229
|
+
let courseNames = {};
|
|
230
|
+
if (courses.length > 0) {
|
|
231
|
+
try {
|
|
232
|
+
const { chromium } = require('playwright');
|
|
233
|
+
const cookies = loadCookies();
|
|
234
|
+
if (cookies.length > 0) {
|
|
235
|
+
const browser = await chromium.launch({ headless: true, slowMo: 50 });
|
|
236
|
+
const ctx = await browser.newContext();
|
|
237
|
+
await ctx.addCookies(cookies);
|
|
216
238
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
239
|
+
const results = await Promise.all(courses.map(async (url) => {
|
|
240
|
+
const match = url.match(/\/course\/(\d+)\//);
|
|
241
|
+
if (!match) return { id: '?', name: '' };
|
|
242
|
+
const pg = await ctx.newPage();
|
|
243
|
+
try {
|
|
244
|
+
await pg.goto(`${BASE_URL}/course/${match[1]}/content#/`, { waitUntil: 'domcontentloaded', timeout: 8000 });
|
|
245
|
+
await pg.waitForTimeout(2000);
|
|
246
|
+
const name = await pg.evaluate(() => {
|
|
247
|
+
const el = document.querySelector('.learning-activities');
|
|
248
|
+
if (!el) return '';
|
|
249
|
+
const scope = angular.element(el).scope();
|
|
250
|
+
return (scope && scope.course) ? (scope.course.name || '') : '';
|
|
251
|
+
});
|
|
252
|
+
return { id: match[1], name: name.substring(0, 30) };
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return { id: match[1], name: '' };
|
|
255
|
+
} finally {
|
|
256
|
+
await pg.close();
|
|
257
|
+
}
|
|
258
|
+
}));
|
|
259
|
+
|
|
260
|
+
results.forEach(r => { if (r.name) courseNames[r.id] = r.name; });
|
|
261
|
+
await browser.close();
|
|
262
|
+
}
|
|
263
|
+
} catch (e) {}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const choices = [
|
|
267
|
+
{ name: chalk.green('➕ 新增課程'), value: 'add' },
|
|
268
|
+
'__sep__',
|
|
269
|
+
];
|
|
270
|
+
courses.forEach((url, i) => {
|
|
221
271
|
const match = url.match(/\/course\/(\d+)\//);
|
|
222
|
-
const id = match ? match[1] :
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
272
|
+
const id = match ? match[1] : '?';
|
|
273
|
+
const name = courseNames[id] || id;
|
|
274
|
+
choices.push({ name: `${chalk.red('✕')} 移除 ${name} [${id}]`, value: `remove_${i}` });
|
|
275
|
+
});
|
|
276
|
+
if (courses.length > 0) {
|
|
277
|
+
choices.push({ name: chalk.red('🗑 清空所有'), value: 'clear' });
|
|
227
278
|
}
|
|
228
|
-
|
|
229
|
-
|
|
279
|
+
choices.push('__sep__');
|
|
280
|
+
choices.push({ name: chalk.gray('⬅ 返回'), value: 'back' });
|
|
230
281
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
chalk.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const match = url.match(/\/course\/(\d+)\//);
|
|
245
|
-
const id = match ? match[1] : '?';
|
|
246
|
-
const name = courseNames[id] || null;
|
|
247
|
-
if (name) {
|
|
248
|
-
return ` ${chalk.white(i + 1 + '.')} ${chalk.cyan(name)} ${chalk.gray(`[${id}]`)}`;
|
|
249
|
-
}
|
|
250
|
-
return ` ${chalk.white(i + 1 + '.')} [${chalk.cyan(id)}] ${chalk.gray(url.substring(0, 45))}`;
|
|
251
|
-
})),
|
|
252
|
-
'',
|
|
253
|
-
chalk.gray(' 新增: tronclass course --add <ID或URL>'),
|
|
254
|
-
chalk.gray(' 移除: tronclass course --remove <編號>'),
|
|
255
|
-
chalk.gray(' ESC 返回'),
|
|
256
|
-
]);
|
|
257
|
-
console.log('');
|
|
258
|
-
|
|
259
|
-
const { action } = await new Promise((resolve) => {
|
|
260
|
-
const listChoices = [
|
|
261
|
-
{ name: chalk.green('➕ 新增課程(互動式)'), value: 'add' },
|
|
262
|
-
...(courses.length > 0 ? [
|
|
263
|
-
...courses.map((url, i) => {
|
|
264
|
-
const match = url.match(/\/course\/(\d+)\//);
|
|
265
|
-
const id = match ? match[1] : '?';
|
|
266
|
-
const name = courseNames[id] || null;
|
|
267
|
-
const label = name ? `${name} [${id}]` : `[${id}]`;
|
|
268
|
-
return { name: `${chalk.red('✕')} 移除 ${label}`, value: `remove_${i}` };
|
|
269
|
-
}),
|
|
270
|
-
new inquirer.Separator(),
|
|
271
|
-
{ name: chalk.red('🗑 清空所有'), value: 'clear' }
|
|
272
|
-
] : []),
|
|
273
|
-
new inquirer.Separator(),
|
|
274
|
-
{ name: chalk.gray('⬅ 返回'), value: 'back' }
|
|
275
|
-
];
|
|
276
|
-
|
|
277
|
-
process.stdin.setRawMode(true);
|
|
278
|
-
process.stdin.resume();
|
|
279
|
-
const escHandler = (data) => {
|
|
280
|
-
if (data.toString() === '\u001b') {
|
|
281
|
-
process.stdin.removeListener('data', escHandler);
|
|
282
|
-
process.stdin.setRawMode(false);
|
|
283
|
-
process.stdin.pause();
|
|
284
|
-
resolve({ action: 'back' });
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
process.stdin.on('data', escHandler);
|
|
288
|
-
|
|
289
|
-
inquirer.prompt([{
|
|
290
|
-
type: 'list',
|
|
291
|
-
name: 'action',
|
|
292
|
-
message: `課程管理 (${courses.length} 個)`,
|
|
293
|
-
choices: listChoices,
|
|
294
|
-
pageSize: 20
|
|
295
|
-
}]).then(ans => {
|
|
296
|
-
process.stdin.removeListener('data', escHandler);
|
|
297
|
-
resolve(ans);
|
|
298
|
-
});
|
|
299
|
-
});
|
|
282
|
+
const action = await promptList(`課程管理 (${courses.length} 個):`, choices, [
|
|
283
|
+
'',
|
|
284
|
+
chalk.white('┌' + '─'.repeat(50) + '┐'),
|
|
285
|
+
chalk.white('│') + chalk.bold.white(' 📋 課程列表'.padEnd(50)) + chalk.white('│'),
|
|
286
|
+
chalk.white('├' + '─'.repeat(50) + '┤'),
|
|
287
|
+
...courses.map((url, i) => {
|
|
288
|
+
const match = url.match(/\/course\/(\d+)\//);
|
|
289
|
+
const id = match ? match[1] : '?';
|
|
290
|
+
const name = courseNames[id] || id;
|
|
291
|
+
return chalk.white('│') + ` ${chalk.white(i + 1 + '.')} ${chalk.cyan(name)} [${id}]`.padEnd(51) + chalk.white('│');
|
|
292
|
+
}),
|
|
293
|
+
chalk.white('└' + '─'.repeat(50) + '┘'),
|
|
294
|
+
]);
|
|
300
295
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
process.stdin.pause();
|
|
312
|
-
resolve({ input: '' });
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
process.stdin.on('data', escHandler);
|
|
316
|
-
inquirer.prompt([{
|
|
317
|
-
type: 'input',
|
|
318
|
-
name: 'input',
|
|
319
|
-
message: '輸入課程 ID(如 127331)或完整 URL:',
|
|
320
|
-
validate: (v) => v.trim().length > 0 ? true : '不可為空'
|
|
321
|
-
}]).then(ans => {
|
|
322
|
-
process.stdin.removeListener('data', escHandler);
|
|
323
|
-
resolve(ans);
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
if (input) {
|
|
327
|
-
const config2 = loadConfig();
|
|
328
|
-
config2.courses = config2.courses || [];
|
|
329
|
-
config2.courses.push(normalizeUrl(input.trim()));
|
|
330
|
-
saveConfig(config2);
|
|
331
|
-
console.log(chalk.green(' 已新增!'));
|
|
332
|
-
}
|
|
333
|
-
} else if (action === 'clear') {
|
|
334
|
-
const { confirm } = await new Promise((resolve) => {
|
|
335
|
-
process.stdin.setRawMode(true);
|
|
336
|
-
process.stdin.resume();
|
|
337
|
-
const escHandler = (data) => {
|
|
338
|
-
if (data.toString() === '\u001b') {
|
|
339
|
-
process.stdin.removeListener('data', escHandler);
|
|
340
|
-
process.stdin.setRawMode(false);
|
|
341
|
-
process.stdin.pause();
|
|
342
|
-
resolve({ confirm: false });
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
process.stdin.on('data', escHandler);
|
|
346
|
-
inquirer.prompt([{
|
|
347
|
-
type: 'confirm', name: 'confirm', message: '確定要清空所有課程?', default: false
|
|
348
|
-
}]).then(ans => {
|
|
349
|
-
process.stdin.removeListener('data', escHandler);
|
|
350
|
-
resolve(ans);
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
if (confirm) {
|
|
354
|
-
const config2 = loadConfig();
|
|
355
|
-
config2.courses = [];
|
|
356
|
-
saveConfig(config2);
|
|
357
|
-
console.log(chalk.green(' 已清空'));
|
|
358
|
-
}
|
|
359
|
-
} else if (action.startsWith('remove_')) {
|
|
360
|
-
const idx = parseInt(action.split('_')[1]);
|
|
361
|
-
const config2 = loadConfig();
|
|
362
|
-
const removed = config2.courses.splice(idx, 1)[0];
|
|
363
|
-
saveConfig(config2);
|
|
364
|
-
courseNames = {};
|
|
365
|
-
console.log(chalk.green(` 已移除: ${removed}`));
|
|
296
|
+
if (action === '__esc__' || action === 'back') {
|
|
297
|
+
editing = false;
|
|
298
|
+
} else if (action === 'add') {
|
|
299
|
+
const input = await promptInput('輸入課程 ID 或 URL');
|
|
300
|
+
if (input && input.trim()) {
|
|
301
|
+
config.courses = config.courses || [];
|
|
302
|
+
config.courses.push(normalizeUrl(input.trim()));
|
|
303
|
+
saveConfig(config);
|
|
304
|
+
console.log(chalk.green(' 已新增!'));
|
|
305
|
+
await pressAnyKey();
|
|
366
306
|
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
307
|
+
} else if (action === 'clear') {
|
|
308
|
+
const confirm = await promptConfirm('確定要清空所有課程?', false);
|
|
309
|
+
if (confirm) {
|
|
310
|
+
config.courses = [];
|
|
311
|
+
saveConfig(config);
|
|
312
|
+
console.log(chalk.green(' 已清空'));
|
|
313
|
+
await pressAnyKey();
|
|
314
|
+
}
|
|
315
|
+
} else if (typeof action === 'string' && action.startsWith('remove_')) {
|
|
316
|
+
const idx = parseInt(action.split('_')[1]);
|
|
317
|
+
const removed = config.courses.splice(idx, 1)[0];
|
|
318
|
+
saveConfig(config);
|
|
319
|
+
console.log(chalk.green(` 已移除: ${removed}`));
|
|
320
|
+
await pressAnyKey();
|
|
371
321
|
}
|
|
372
322
|
}
|
|
373
323
|
}
|
|
@@ -375,112 +325,46 @@ async function courseMenu() {
|
|
|
375
325
|
async function settingsMenu() {
|
|
376
326
|
const config = loadConfig();
|
|
377
327
|
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
inquirer.prompt([{
|
|
391
|
-
type: 'list',
|
|
392
|
-
name: 'setting',
|
|
393
|
-
message: '設定',
|
|
394
|
-
choices: [
|
|
395
|
-
{ name: `倍速: ${chalk.cyan(config.playbackRate || 2)}x`, value: 'speed' },
|
|
396
|
-
{ name: `無頭模式: ${chalk.cyan(config.headless ? '是' : '否')}`, value: 'headless' },
|
|
397
|
-
{ name: `SlowMo: ${chalk.cyan(config.slowMo || 50)}ms`, value: 'slowmo' },
|
|
398
|
-
new inquirer.Separator(),
|
|
399
|
-
{ name: chalk.gray('⬅ 返回'), value: 'back' }
|
|
400
|
-
]
|
|
401
|
-
}]).then(ans => {
|
|
402
|
-
process.stdin.removeListener('data', escHandler);
|
|
403
|
-
resolve(ans);
|
|
404
|
-
});
|
|
405
|
-
});
|
|
328
|
+
const setting = await promptList('設定:', [
|
|
329
|
+
{ name: `倍速: ${chalk.cyan(config.playbackRate || 2)}x`, value: 'speed' },
|
|
330
|
+
{ name: `無頭模式: ${chalk.cyan(config.headless ? '是' : '否')}`, value: 'headless' },
|
|
331
|
+
{ name: `SlowMo: ${chalk.cyan(config.slowMo || 50)}ms`, value: 'slowmo' },
|
|
332
|
+
'__sep__',
|
|
333
|
+
{ name: chalk.gray('⬅ 返回'), value: 'back' }
|
|
334
|
+
], [
|
|
335
|
+
'',
|
|
336
|
+
chalk.white('╔' + '═'.repeat(50) + '╗'),
|
|
337
|
+
chalk.white('║') + chalk.bold.white(' ⚙ 設定'.padEnd(50)) + chalk.white('║'),
|
|
338
|
+
chalk.white('╚' + '═'.repeat(50) + '╝'),
|
|
339
|
+
]);
|
|
406
340
|
|
|
407
|
-
if (setting === 'back') return;
|
|
341
|
+
if (setting === '__esc__' || setting === 'back') return;
|
|
408
342
|
|
|
409
343
|
if (setting === 'speed') {
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
inquirer.prompt([{
|
|
423
|
-
type: 'list', name: 'speed', message: '選擇播放倍速:',
|
|
424
|
-
choices: [
|
|
425
|
-
{ name: '1x(正常)', value: 1 },
|
|
426
|
-
{ name: '1.5x', value: 1.5 },
|
|
427
|
-
{ name: '2x(推薦)', value: 2 },
|
|
428
|
-
{ name: '4x', value: 4 },
|
|
429
|
-
{ name: '8x', value: 8 }
|
|
430
|
-
],
|
|
431
|
-
default: config.playbackRate || 2
|
|
432
|
-
}]).then(ans => {
|
|
433
|
-
process.stdin.removeListener('data', escHandler);
|
|
434
|
-
resolve(ans);
|
|
435
|
-
});
|
|
436
|
-
});
|
|
437
|
-
config.playbackRate = speed;
|
|
438
|
-
saveConfig(config);
|
|
439
|
-
console.log(chalk.green(` 已設定為 ${speed}x`));
|
|
344
|
+
const speed = await promptList('選擇播放倍速:', [
|
|
345
|
+
{ name: '2x', value: 2 },
|
|
346
|
+
{ name: '1.5x', value: 1.5 },
|
|
347
|
+
{ name: '1.25x', value: 1.25 },
|
|
348
|
+
{ name: '1x', value: 1 },
|
|
349
|
+
{ name: '0.75x', value: 0.75 },
|
|
350
|
+
]);
|
|
351
|
+
if (speed !== '__esc__') {
|
|
352
|
+
config.playbackRate = speed;
|
|
353
|
+
saveConfig(config);
|
|
354
|
+
console.log(chalk.green(` 已設定為 ${speed}x`));
|
|
355
|
+
}
|
|
440
356
|
} else if (setting === 'headless') {
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
process.stdin.removeListener('data', escHandler);
|
|
447
|
-
process.stdin.setRawMode(false);
|
|
448
|
-
process.stdin.pause();
|
|
449
|
-
resolve({ hl: config.headless || false });
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
process.stdin.on('data', escHandler);
|
|
453
|
-
inquirer.prompt([{
|
|
454
|
-
type: 'confirm', name: 'hl', message: '啟用無頭模式?', default: config.headless || false
|
|
455
|
-
}]).then(ans => {
|
|
456
|
-
process.stdin.removeListener('data', escHandler);
|
|
457
|
-
resolve(ans);
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
config.headless = hl;
|
|
461
|
-
saveConfig(config);
|
|
357
|
+
const hl = await promptConfirm('啟用無頭模式?', config.headless || false);
|
|
358
|
+
if (hl !== null) {
|
|
359
|
+
config.headless = hl;
|
|
360
|
+
saveConfig(config);
|
|
361
|
+
}
|
|
462
362
|
} else if (setting === 'slowmo') {
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
process.stdin.removeListener('data', escHandler);
|
|
469
|
-
process.stdin.setRawMode(false);
|
|
470
|
-
process.stdin.pause();
|
|
471
|
-
resolve({ ms: config.slowMo || 50 });
|
|
472
|
-
}
|
|
473
|
-
};
|
|
474
|
-
process.stdin.on('data', escHandler);
|
|
475
|
-
inquirer.prompt([{
|
|
476
|
-
type: 'number', name: 'ms', message: 'SlowMo 毫秒數:', default: config.slowMo || 50
|
|
477
|
-
}]).then(ans => {
|
|
478
|
-
process.stdin.removeListener('data', escHandler);
|
|
479
|
-
resolve(ans);
|
|
480
|
-
});
|
|
481
|
-
});
|
|
482
|
-
config.slowMo = ms;
|
|
483
|
-
saveConfig(config);
|
|
363
|
+
const ms = await promptInput('SlowMo 毫秒數');
|
|
364
|
+
if (ms && !isNaN(parseInt(ms))) {
|
|
365
|
+
config.slowMo = parseInt(ms);
|
|
366
|
+
saveConfig(config);
|
|
367
|
+
}
|
|
484
368
|
}
|
|
485
369
|
}
|
|
486
370
|
|
|
@@ -494,22 +378,4 @@ function normalizeUrl(input) {
|
|
|
494
378
|
return input;
|
|
495
379
|
}
|
|
496
380
|
|
|
497
|
-
async function pressAnyKey() {
|
|
498
|
-
return new Promise((resolve) => {
|
|
499
|
-
process.stdin.setRawMode(true);
|
|
500
|
-
process.stdin.resume();
|
|
501
|
-
const handler = (data) => {
|
|
502
|
-
const str = data.toString();
|
|
503
|
-
if (str === '\u001b' || str === '\r' || str === '\n') {
|
|
504
|
-
process.stdin.removeListener('data', handler);
|
|
505
|
-
process.stdin.setRawMode(false);
|
|
506
|
-
process.stdin.pause();
|
|
507
|
-
resolve();
|
|
508
|
-
}
|
|
509
|
-
};
|
|
510
|
-
process.stdin.on('data', handler);
|
|
511
|
-
process.stdout.write(chalk.gray('按 Enter 或 ESC 返回...'));
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
|
|
515
381
|
module.exports = { mainMenu };
|