tronclass-auto 1.7.3 → 1.7.5
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/bin/tronclass.js +23 -0
- package/package.json +1 -1
- package/src/index.js +22 -9
- package/src/tui.js +156 -196
package/bin/tronclass.js
CHANGED
|
@@ -64,6 +64,29 @@ program
|
|
|
64
64
|
manageCourses(opts);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
program
|
|
68
|
+
.command('update')
|
|
69
|
+
.description('更新到最新版本')
|
|
70
|
+
.action(async () => {
|
|
71
|
+
const { execSync } = require('child_process');
|
|
72
|
+
console.log(`目前版本: v${pkg.version}`);
|
|
73
|
+
console.log('檢查最新版本...');
|
|
74
|
+
try {
|
|
75
|
+
const latest = execSync('npm view tronclass-auto version', { encoding: 'utf8' }).trim();
|
|
76
|
+
console.log(`最新版本: v${latest}`);
|
|
77
|
+
if (latest === pkg.version) {
|
|
78
|
+
console.log('已經是最新版本!');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log('正在更新...');
|
|
82
|
+
execSync('npm install -g tronclass-auto@latest', { stdio: 'inherit' });
|
|
83
|
+
console.log('更新完成!');
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error('更新失敗:', e.message);
|
|
86
|
+
console.error('請手動執行: npm install -g tronclass-auto@latest');
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
67
90
|
program
|
|
68
91
|
.command('config')
|
|
69
92
|
.description('顯示目前設定')
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -268,22 +268,33 @@ async function showStatus() {
|
|
|
268
268
|
const { chromium } = re('playwright');
|
|
269
269
|
const { ensureBrowser } = re('./browser');
|
|
270
270
|
const exePath = await ensureBrowser();
|
|
271
|
-
const launchOpts = {
|
|
271
|
+
const launchOpts = {
|
|
272
|
+
headless: true,
|
|
273
|
+
slowMo: 50,
|
|
274
|
+
args: [
|
|
275
|
+
'--disable-gpu',
|
|
276
|
+
'--disable-software-rasterizer',
|
|
277
|
+
'--disable-dev-shm-usage',
|
|
278
|
+
'--no-sandbox',
|
|
279
|
+
'--disable-setuid-sandbox',
|
|
280
|
+
'--disable-cache',
|
|
281
|
+
'--disable-application-cache'
|
|
282
|
+
]
|
|
283
|
+
};
|
|
272
284
|
if (exePath) launchOpts.executablePath = exePath;
|
|
273
285
|
const browser = await chromium.launch(launchOpts);
|
|
274
286
|
const context = await browser.newContext();
|
|
275
287
|
await context.addCookies(cookies);
|
|
276
288
|
|
|
277
|
-
const
|
|
278
|
-
await
|
|
279
|
-
await
|
|
289
|
+
const page = await context.newPage();
|
|
290
|
+
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
291
|
+
await page.waitForTimeout(2000);
|
|
280
292
|
|
|
281
|
-
if (
|
|
293
|
+
if (page.url().includes('login')) {
|
|
282
294
|
console.log(chalk.red('[ERROR] Cookie 過期'));
|
|
283
295
|
await browser.close();
|
|
284
296
|
return;
|
|
285
297
|
}
|
|
286
|
-
await checkPage.close();
|
|
287
298
|
|
|
288
299
|
const courses = config.courses || [];
|
|
289
300
|
console.log(chalk.cyan('\n┌' + '─'.repeat(48) + '┐'));
|
|
@@ -297,13 +308,14 @@ async function showStatus() {
|
|
|
297
308
|
const courseResults = await Promise.all(courses.map(async (courseUrl) => {
|
|
298
309
|
const match = courseUrl.match(/\/course\/(\d+)\//);
|
|
299
310
|
const courseId = match ? match[1] : '?';
|
|
311
|
+
const pg = await context.newPage();
|
|
300
312
|
try {
|
|
301
|
-
const pg = await context.newPage();
|
|
302
313
|
const allVideos = await getAllVideos(pg, courseId);
|
|
303
|
-
await pg.close();
|
|
304
314
|
return { courseId, allVideos };
|
|
305
315
|
} catch (e) {
|
|
306
316
|
return { courseId, allVideos: [] };
|
|
317
|
+
} finally {
|
|
318
|
+
await pg.close();
|
|
307
319
|
}
|
|
308
320
|
}));
|
|
309
321
|
|
|
@@ -395,10 +407,11 @@ async function run(opts = {}) {
|
|
|
395
407
|
await page.waitForTimeout(2000);
|
|
396
408
|
|
|
397
409
|
if (page.url().includes('login')) {
|
|
398
|
-
console.log(chalk.red('[ERROR] Cookie
|
|
410
|
+
console.log(chalk.red('[ERROR] Cookie 過期'));
|
|
399
411
|
await browser.close();
|
|
400
412
|
return;
|
|
401
413
|
}
|
|
414
|
+
await page.close();
|
|
402
415
|
|
|
403
416
|
console.log(chalk.green('[OK] Login verified'));
|
|
404
417
|
|
package/src/tui.js
CHANGED
|
@@ -4,38 +4,47 @@ const { loadConfig, saveConfig, BASE_URL } = require('./config');
|
|
|
4
4
|
const { loadCookies } = require('./auth');
|
|
5
5
|
const pkg = require('../package.json');
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
lines.forEach(l => console.log(chalk.cyan('║ ') + l.padEnd(width - 4) + chalk.cyan(' ║')));
|
|
10
|
-
console.log(chalk.cyan('╚' + '═'.repeat(width - 2) + '╝'));
|
|
7
|
+
function clear() {
|
|
8
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
function
|
|
11
|
+
function header() {
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
const lines = [
|
|
14
|
+
'',
|
|
15
|
+
chalk.white('╔' + '═'.repeat(52) + '╗'),
|
|
16
|
+
chalk.white('║ ') + chalk.bold.white(`tronclass-auto v${pkg.version}`.padEnd(52)) + chalk.white('║'),
|
|
17
|
+
chalk.white('║ ') + chalk.gray('自動觀看 eclass/TronClass 影片'.padEnd(52)) + chalk.white('║'),
|
|
18
|
+
chalk.white('║ ') + `課程: ${chalk.cyan(config.courses.length)} 個 | Cookie: ${chalk.green('✓')} | 倍速: ${chalk.yellow(config.playbackRate || 2)}x`.padEnd(52) + chalk.white('║'),
|
|
19
|
+
chalk.white('╚' + '═'.repeat(52) + '╝'),
|
|
20
|
+
];
|
|
21
|
+
lines.forEach(l => process.stdout.write(l + '\n'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function draw(extraLines) {
|
|
25
|
+
clear();
|
|
26
|
+
header();
|
|
27
|
+
if (extraLines) extraLines.forEach(l => process.stdout.write(l + '\n'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function menu(title, choices, extraLines) {
|
|
14
31
|
return new Promise((resolve) => {
|
|
15
32
|
let selected = 0;
|
|
16
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
17
33
|
|
|
18
34
|
function render() {
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
clear();
|
|
36
|
+
header();
|
|
37
|
+
if (extraLines) extraLines.forEach(l => process.stdout.write(l + '\n'));
|
|
38
|
+
process.stdout.write('\n');
|
|
21
39
|
choices.forEach((c, i) => {
|
|
22
40
|
const prefix = i === selected ? chalk.green('>') : ' ';
|
|
23
41
|
if (typeof c === 'string' && c === '__sep__') {
|
|
24
|
-
|
|
42
|
+
process.stdout.write(chalk.gray(' ─────────────') + '\n');
|
|
25
43
|
} else {
|
|
26
|
-
|
|
27
|
-
lines.push(` ${prefix} ${label}`);
|
|
44
|
+
process.stdout.write(` ${prefix} ${c.name}\n`);
|
|
28
45
|
}
|
|
29
46
|
});
|
|
30
|
-
|
|
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'));
|
|
47
|
+
process.stdout.write('\n' + chalk.gray(' ↑↓ 移動 Enter 確認 ESC 返回') + '\n');
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
render();
|
|
@@ -45,26 +54,22 @@ function promptList(message, choices, header) {
|
|
|
45
54
|
|
|
46
55
|
function onData(data) {
|
|
47
56
|
const key = data.toString();
|
|
48
|
-
|
|
49
57
|
if (key === '\u001b[A') {
|
|
50
58
|
selected = Math.max(0, selected - 1);
|
|
51
|
-
while (selected
|
|
52
|
-
|
|
53
|
-
}
|
|
59
|
+
while (selected >= 0 && typeof choices[selected] === 'string' && choices[selected] === '__sep__') selected--;
|
|
60
|
+
if (selected < 0) selected = 0;
|
|
54
61
|
render();
|
|
55
62
|
} else if (key === '\u001b[B') {
|
|
56
63
|
selected = Math.min(choices.length - 1, selected + 1);
|
|
57
|
-
while (selected < choices.length &&
|
|
58
|
-
|
|
59
|
-
}
|
|
64
|
+
while (selected < choices.length && typeof choices[selected] === 'string' && choices[selected] === '__sep__') selected++;
|
|
65
|
+
if (selected >= choices.length) selected = choices.length - 1;
|
|
60
66
|
render();
|
|
61
67
|
} else if (key === '\r' || key === '\n') {
|
|
62
68
|
cleanup();
|
|
63
|
-
|
|
64
|
-
resolve(typeof choice === 'string' ? choice : choice.value);
|
|
69
|
+
resolve(choices[selected]);
|
|
65
70
|
} else if (key === '\u001b') {
|
|
66
71
|
cleanup();
|
|
67
|
-
resolve(
|
|
72
|
+
resolve(null);
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -72,72 +77,98 @@ function promptList(message, choices, header) {
|
|
|
72
77
|
process.stdin.removeListener('data', onData);
|
|
73
78
|
process.stdin.setRawMode(false);
|
|
74
79
|
process.stdin.pause();
|
|
75
|
-
rl.close();
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
process.stdin.on('data', onData);
|
|
79
83
|
});
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
function
|
|
86
|
+
function flushStdin() {
|
|
87
|
+
process.stdin.setRawMode(true);
|
|
88
|
+
process.stdin.resume();
|
|
89
|
+
while (process.stdin.readableLength > 0) process.stdin.read();
|
|
90
|
+
process.stdin.setRawMode(false);
|
|
91
|
+
process.stdin.pause();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function confirm(message, defaultVal = true) {
|
|
83
95
|
return new Promise((resolve) => {
|
|
84
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
85
96
|
process.stdout.write(` ${message} (${defaultVal ? 'Y/n' : 'y/N'}) `);
|
|
86
|
-
|
|
87
97
|
process.stdin.setRawMode(true);
|
|
88
98
|
process.stdin.resume();
|
|
89
99
|
|
|
90
100
|
function onData(data) {
|
|
91
101
|
const key = data.toString();
|
|
92
|
-
if (key === '\r' || key === '\n') {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
}
|
|
102
|
+
if (key === '\r' || key === '\n') { cleanup(); resolve(defaultVal); }
|
|
103
|
+
else if (key === '\u001b') { cleanup(); resolve(null); }
|
|
104
|
+
else if (key.toLowerCase() === 'y') { cleanup(); resolve(true); }
|
|
105
|
+
else if (key.toLowerCase() === 'n') { cleanup(); resolve(false); }
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
function cleanup() {
|
|
108
109
|
process.stdin.removeListener('data', onData);
|
|
109
110
|
process.stdin.setRawMode(false);
|
|
110
111
|
process.stdin.pause();
|
|
111
|
-
rl.close();
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
process.stdin.on('data', onData);
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
function
|
|
118
|
+
function input(message) {
|
|
119
119
|
return new Promise((resolve) => {
|
|
120
120
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
121
|
-
rl.question(` ${message}: `, (answer) => {
|
|
122
|
-
rl.close();
|
|
123
|
-
resolve(answer);
|
|
124
|
-
});
|
|
121
|
+
rl.question(` ${message}: `, (answer) => { rl.close(); resolve(answer); });
|
|
125
122
|
});
|
|
126
123
|
}
|
|
127
124
|
|
|
128
125
|
function pressAnyKey() {
|
|
129
126
|
return new Promise((resolve) => {
|
|
130
|
-
process.stdout.write(chalk.gray('\n 按任意鍵返回...'));
|
|
131
127
|
process.stdin.setRawMode(true);
|
|
132
128
|
process.stdin.resume();
|
|
133
|
-
process.stdin.
|
|
134
|
-
|
|
135
|
-
process.
|
|
136
|
-
resolve();
|
|
137
|
-
});
|
|
129
|
+
while (process.stdin.readableLength > 0) process.stdin.read();
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
process.stdout.write(chalk.gray('\n 按任意鍵返回...'));
|
|
132
|
+
process.stdin.once('data', () => { process.stdin.setRawMode(false); process.stdin.pause(); resolve(); });
|
|
133
|
+
}, 100);
|
|
138
134
|
});
|
|
139
135
|
}
|
|
140
136
|
|
|
137
|
+
async function fetchCourseNames(config) {
|
|
138
|
+
const courses = config.courses || [];
|
|
139
|
+
if (!courses.length) return {};
|
|
140
|
+
const cookies = loadCookies();
|
|
141
|
+
if (!cookies.length) return {};
|
|
142
|
+
try {
|
|
143
|
+
const { chromium } = require('playwright');
|
|
144
|
+
const browser = await chromium.launch({ headless: true, slowMo: 50, args: ['--disable-gpu', '--no-sandbox', '--disable-cache'] });
|
|
145
|
+
const ctx = await browser.newContext();
|
|
146
|
+
await ctx.addCookies(cookies);
|
|
147
|
+
const pg = await ctx.newPage();
|
|
148
|
+
const results = await Promise.all(courses.map(async (url) => {
|
|
149
|
+
const match = url.match(/\/course\/(\d+)\//);
|
|
150
|
+
if (!match) return { id: '?', name: '' };
|
|
151
|
+
const p = await ctx.newPage();
|
|
152
|
+
try {
|
|
153
|
+
await p.goto(`${BASE_URL}/course/${match[1]}/content#/`, { waitUntil: 'domcontentloaded', timeout: 8000 });
|
|
154
|
+
await p.waitForTimeout(2000);
|
|
155
|
+
const name = await p.evaluate(() => {
|
|
156
|
+
const el = document.querySelector('.learning-activities');
|
|
157
|
+
if (!el) return '';
|
|
158
|
+
try { const s = angular.element(el).scope(); return (s && s.course) ? (s.course.name || '') : ''; } catch { return ''; }
|
|
159
|
+
});
|
|
160
|
+
return { id: match[1], name: name.substring(0, 25) };
|
|
161
|
+
} catch (e) { return { id: match[1], name: '' }; }
|
|
162
|
+
finally { await p.close(); }
|
|
163
|
+
}));
|
|
164
|
+
await pg.close();
|
|
165
|
+
await browser.close();
|
|
166
|
+
const names = {};
|
|
167
|
+
results.forEach(r => { if (r.name) names[r.id] = r.name; });
|
|
168
|
+
return names;
|
|
169
|
+
} catch (e) { return {}; }
|
|
170
|
+
}
|
|
171
|
+
|
|
141
172
|
async function mainMenu() {
|
|
142
173
|
const cookies = loadCookies();
|
|
143
174
|
if (!cookies.length) {
|
|
@@ -147,71 +178,55 @@ async function mainMenu() {
|
|
|
147
178
|
|
|
148
179
|
let running = true;
|
|
149
180
|
while (running) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
console.clear();
|
|
153
|
-
box([
|
|
154
|
-
chalk.bold.white(`tronclass-auto v${pkg.version}`),
|
|
155
|
-
chalk.gray('自動觀看 eclass/TronClass 影片'),
|
|
156
|
-
'',
|
|
157
|
-
`課程: ${chalk.cyan(config.courses.length)} 個 | Cookie: ${chalk.green('✓')} | 倍速: ${chalk.yellow(config.playbackRate || 2)}x`,
|
|
158
|
-
]);
|
|
159
|
-
|
|
160
|
-
const action = await promptList('選擇操作:', [
|
|
181
|
+
const result = await menu('選擇操作:', [
|
|
161
182
|
{ name: chalk.green('▶ 開始自動觀看'), value: 'run' },
|
|
162
183
|
{ name: chalk.cyan('📋 管理課程列表'), value: 'courses' },
|
|
163
184
|
{ name: chalk.yellow('📊 查看進度統計'), value: 'status' },
|
|
164
185
|
{ name: chalk.magenta('⚙ 設定'), value: 'settings' },
|
|
165
186
|
'__sep__',
|
|
166
187
|
{ 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
188
|
]);
|
|
175
189
|
|
|
176
|
-
|
|
190
|
+
const action = result?.value;
|
|
191
|
+
if (!action || action === 'exit') { running = false; continue; }
|
|
177
192
|
|
|
178
193
|
switch (action) {
|
|
179
|
-
case 'run': await runMenu(
|
|
194
|
+
case 'run': await runMenu(); break;
|
|
180
195
|
case 'courses': await courseMenu(); break;
|
|
181
196
|
case 'status': {
|
|
197
|
+
draw([chalk.gray(' 載入進度統計中,請稍候...')]);
|
|
182
198
|
const { showStatus } = require('./index');
|
|
183
199
|
await showStatus();
|
|
184
200
|
await pressAnyKey();
|
|
185
201
|
break;
|
|
186
202
|
}
|
|
187
203
|
case 'settings': await settingsMenu(); break;
|
|
188
|
-
case 'exit': running = false; break;
|
|
189
204
|
}
|
|
190
205
|
}
|
|
191
206
|
console.log(chalk.gray('\n再見!\n'));
|
|
192
207
|
}
|
|
193
208
|
|
|
194
|
-
async function runMenu(
|
|
209
|
+
async function runMenu() {
|
|
210
|
+
const config = loadConfig();
|
|
195
211
|
if (!config.courses.length) {
|
|
196
|
-
|
|
212
|
+
draw([chalk.yellow(' 還沒有設定課程,請先新增')]);
|
|
197
213
|
await pressAnyKey();
|
|
198
214
|
return;
|
|
199
215
|
}
|
|
200
216
|
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
217
|
+
const yes = await confirm(`開始觀看全部 ${config.courses.length} 個課程?`);
|
|
218
|
+
if (!yes) return;
|
|
203
219
|
|
|
204
|
-
const headless = await
|
|
220
|
+
const headless = await confirm('無頭模式(背景執行)?', false);
|
|
205
221
|
if (headless === null) return;
|
|
206
222
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
chalk.green('開始自動觀看'),
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
223
|
+
draw([
|
|
224
|
+
'',
|
|
225
|
+
chalk.green(' 開始自動觀看'),
|
|
226
|
+
` 課程: ${chalk.cyan(config.courses.length)} 個`,
|
|
227
|
+
` 模式: ${headless ? chalk.gray('無頭') : chalk.white('有頭')}`,
|
|
228
|
+
` 倍速: ${chalk.yellow(config.playbackRate || 2)}x`,
|
|
213
229
|
]);
|
|
214
|
-
console.log('');
|
|
215
230
|
|
|
216
231
|
const { runWithSelection } = require('./index');
|
|
217
232
|
await runWithSelection(config.courses, headless);
|
|
@@ -224,99 +239,53 @@ async function courseMenu() {
|
|
|
224
239
|
const config = loadConfig();
|
|
225
240
|
const courses = config.courses || [];
|
|
226
241
|
|
|
227
|
-
|
|
228
|
-
|
|
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);
|
|
238
|
-
|
|
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
|
-
}
|
|
242
|
+
draw([chalk.cyan(' 📋 課程列表')]);
|
|
265
243
|
|
|
266
|
-
const
|
|
244
|
+
const result = await menu(`課程管理 (${courses.length} 個):`, [
|
|
267
245
|
{ name: chalk.green('➕ 新增課程'), value: 'add' },
|
|
268
246
|
'__sep__',
|
|
269
|
-
];
|
|
270
|
-
courses.forEach((url, i) => {
|
|
271
|
-
const match = url.match(/\/course\/(\d+)\//);
|
|
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' });
|
|
278
|
-
}
|
|
279
|
-
choices.push('__sep__');
|
|
280
|
-
choices.push({ name: chalk.gray('⬅ 返回'), value: 'back' });
|
|
281
|
-
|
|
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
247
|
...courses.map((url, i) => {
|
|
288
248
|
const match = url.match(/\/course\/(\d+)\//);
|
|
289
249
|
const id = match ? match[1] : '?';
|
|
290
|
-
const name = courseNames
|
|
291
|
-
|
|
250
|
+
const name = (config.courseNames || {})[id];
|
|
251
|
+
const label = name ? `${name} [${id}]` : `[${id}]`;
|
|
252
|
+
return { name: `${chalk.red('✕')} 移除 ${label}`, value: `remove_${i}` };
|
|
292
253
|
}),
|
|
293
|
-
chalk.
|
|
254
|
+
...(courses.length > 0 ? [{ name: chalk.red('🗑 清空所有'), value: 'clear' }] : []),
|
|
255
|
+
'__sep__',
|
|
256
|
+
{ name: chalk.gray('⬅ 返回'), value: 'back' }
|
|
294
257
|
]);
|
|
295
258
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
259
|
+
const action = result?.value;
|
|
260
|
+
if (!action || action === 'back') { editing = false; continue; }
|
|
261
|
+
|
|
262
|
+
if (action === 'add') {
|
|
263
|
+
const val = await input('輸入課程 ID 或 URL');
|
|
264
|
+
if (val && val.trim()) {
|
|
265
|
+
let url = val.trim();
|
|
266
|
+
if (/^\d+$/.test(url)) url = `${BASE_URL}/course/${url}/content#/`;
|
|
267
|
+
else if (!url.startsWith('http')) url = `${BASE_URL}/course/${url}/content#/`;
|
|
268
|
+
config.courses.push(url);
|
|
269
|
+
delete config.courseNames;
|
|
303
270
|
saveConfig(config);
|
|
304
|
-
|
|
271
|
+
draw([chalk.green(' 已新增!')]);
|
|
305
272
|
await pressAnyKey();
|
|
306
273
|
}
|
|
307
274
|
} else if (action === 'clear') {
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
275
|
+
const yes = await confirm('確定要清空所有課程?', false);
|
|
276
|
+
if (yes) {
|
|
310
277
|
config.courses = [];
|
|
278
|
+
config.courseNames = {};
|
|
311
279
|
saveConfig(config);
|
|
312
|
-
|
|
280
|
+
draw([chalk.green(' 已清空')]);
|
|
313
281
|
await pressAnyKey();
|
|
314
282
|
}
|
|
315
283
|
} else if (typeof action === 'string' && action.startsWith('remove_')) {
|
|
316
284
|
const idx = parseInt(action.split('_')[1]);
|
|
317
|
-
|
|
285
|
+
config.courses.splice(idx, 1);
|
|
286
|
+
delete config.courseNames;
|
|
318
287
|
saveConfig(config);
|
|
319
|
-
|
|
288
|
+
draw([chalk.green(' 已移除')]);
|
|
320
289
|
await pressAnyKey();
|
|
321
290
|
}
|
|
322
291
|
}
|
|
@@ -324,58 +293,49 @@ async function courseMenu() {
|
|
|
324
293
|
|
|
325
294
|
async function settingsMenu() {
|
|
326
295
|
const config = loadConfig();
|
|
296
|
+
const speedOptions = [2, 1.5, 1.25, 1, 0.75];
|
|
327
297
|
|
|
328
|
-
const
|
|
298
|
+
const result = await menu('設定:', [
|
|
329
299
|
{ name: `倍速: ${chalk.cyan(config.playbackRate || 2)}x`, value: 'speed' },
|
|
330
300
|
{ name: `無頭模式: ${chalk.cyan(config.headless ? '是' : '否')}`, value: 'headless' },
|
|
331
301
|
{ name: `SlowMo: ${chalk.cyan(config.slowMo || 50)}ms`, value: 'slowmo' },
|
|
302
|
+
{ name: `快取課程名稱: ${config.courseNames && Object.keys(config.courseNames).length > 0 ? chalk.green('已快取') + ` (${Object.keys(config.courseNames).length} 個)` : chalk.red('未快取')}`, value: 'cache' },
|
|
332
303
|
'__sep__',
|
|
333
304
|
{ 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
305
|
]);
|
|
340
306
|
|
|
341
|
-
|
|
307
|
+
const action = result?.value;
|
|
308
|
+
if (!action || action === 'back') return;
|
|
342
309
|
|
|
343
|
-
if (
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
{ name: '1.25x', value: 1.25 },
|
|
348
|
-
{ name: '1x', value: 1 },
|
|
349
|
-
{ name: '0.75x', value: 0.75 },
|
|
350
|
-
]);
|
|
351
|
-
if (speed !== '__esc__') {
|
|
310
|
+
if (action === 'speed') {
|
|
311
|
+
const r = await menu('選擇播放倍速:', speedOptions.map(s => ({ name: `${s}x`, value: s })));
|
|
312
|
+
const speed = r?.value;
|
|
313
|
+
if (speed) {
|
|
352
314
|
config.playbackRate = speed;
|
|
353
315
|
saveConfig(config);
|
|
354
|
-
|
|
316
|
+
draw([chalk.green(` 已設定為 ${speed}x`)]);
|
|
317
|
+
await pressAnyKey();
|
|
355
318
|
}
|
|
356
|
-
} else if (
|
|
357
|
-
const hl = await
|
|
319
|
+
} else if (action === 'headless') {
|
|
320
|
+
const hl = await confirm('啟用無頭模式?', config.headless || false);
|
|
358
321
|
if (hl !== null) {
|
|
359
322
|
config.headless = hl;
|
|
360
323
|
saveConfig(config);
|
|
361
324
|
}
|
|
362
|
-
} else if (
|
|
363
|
-
const ms = await
|
|
325
|
+
} else if (action === 'slowmo') {
|
|
326
|
+
const ms = await input('SlowMo 毫秒數');
|
|
364
327
|
if (ms && !isNaN(parseInt(ms))) {
|
|
365
328
|
config.slowMo = parseInt(ms);
|
|
366
329
|
saveConfig(config);
|
|
367
330
|
}
|
|
331
|
+
} else if (action === 'cache') {
|
|
332
|
+
draw([chalk.gray(' 載入課程名稱...')]);
|
|
333
|
+
const names = await fetchCourseNames(config);
|
|
334
|
+
config.courseNames = names;
|
|
335
|
+
saveConfig(config);
|
|
336
|
+
draw([chalk.green(` 已快取 ${Object.keys(names).length} 個課程名稱`)]);
|
|
337
|
+
await pressAnyKey();
|
|
368
338
|
}
|
|
369
339
|
}
|
|
370
340
|
|
|
371
|
-
function normalizeUrl(input) {
|
|
372
|
-
if (/^\d+$/.test(input)) {
|
|
373
|
-
return `${BASE_URL}/course/${input}/content#/`;
|
|
374
|
-
}
|
|
375
|
-
if (!input.startsWith('http')) {
|
|
376
|
-
return `${BASE_URL}/course/${input}/content#/`;
|
|
377
|
-
}
|
|
378
|
-
return input;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
341
|
module.exports = { mainMenu };
|