nekostream 2026.2.2 → 2026.2.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/dist/index.js +4 -2
- package/dist/player.js +63 -35
- package/dist/prompts-wrapper.js +124 -12
- package/package.json +1 -1
- package/player.html +64 -41
package/dist/index.js
CHANGED
|
@@ -215,9 +215,10 @@ async function showAnimeList(providerName, title, list) {
|
|
|
215
215
|
(0, ui_1.printBanner)(`Provider: ${providerName.toUpperCase()}`, title.toUpperCase());
|
|
216
216
|
flushStdin();
|
|
217
217
|
const { animeId } = await (0, prompts_wrapper_1.default)({
|
|
218
|
-
type: '
|
|
218
|
+
type: 'search',
|
|
219
219
|
name: 'animeId',
|
|
220
220
|
message: 'Chọn anime (Esc: quay lại)',
|
|
221
|
+
pageSize: 15,
|
|
221
222
|
choices: list.map((anime, idx) => {
|
|
222
223
|
const desc = anime.status || anime.year?.toString() || '';
|
|
223
224
|
const cleanTitle = anime.title.replace(/\r?\n|\r/g, ' ').trim();
|
|
@@ -403,9 +404,10 @@ async function showUserDataList(title, items, providerName) {
|
|
|
403
404
|
choices.push({ title: chalk_1.default.yellow('▲ Trang trước'), value: '__prev__' });
|
|
404
405
|
choices.push({ title: chalk_1.default.gray('← Quay lại'), value: '__back__' });
|
|
405
406
|
const { animeId } = await (0, prompts_wrapper_1.default)({
|
|
406
|
-
type: '
|
|
407
|
+
type: 'search',
|
|
407
408
|
name: 'animeId',
|
|
408
409
|
message: 'Chọn anime để xem (Esc: quay lại)',
|
|
410
|
+
pageSize: PAGE_SIZE,
|
|
409
411
|
choices
|
|
410
412
|
});
|
|
411
413
|
if (!animeId || animeId === '__back__')
|
package/dist/player.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.launchPlayer = launchPlayer;
|
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
10
11
|
function runWithTimeout(fn, ms) {
|
|
11
12
|
return new Promise((resolve, reject) => {
|
|
12
13
|
const timer = setTimeout(() => {
|
|
@@ -21,15 +22,50 @@ function runWithTimeout(fn, ms) {
|
|
|
21
22
|
});
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
function electronExecutablePath(rootDir) {
|
|
26
|
+
if (process.platform === 'win32')
|
|
27
|
+
return path_1.default.join(rootDir, 'electron.exe');
|
|
28
|
+
if (process.platform === 'darwin')
|
|
29
|
+
return path_1.default.join(rootDir, 'Electron.app', 'Contents', 'MacOS', 'Electron');
|
|
30
|
+
return path_1.default.join(rootDir, 'electron');
|
|
31
|
+
}
|
|
32
|
+
function resolveElectronPackageDir(basePath) {
|
|
33
|
+
try {
|
|
34
|
+
return path_1.default.dirname(require.resolve('electron/package.json', { paths: [basePath, __dirname] }));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return path_1.default.join(basePath, 'node_modules', 'electron');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function resolveInstalledElectron(basePath) {
|
|
41
|
+
const electronBinary = require('electron');
|
|
42
|
+
if (typeof electronBinary === 'string' && fs_1.default.existsSync(electronBinary)) {
|
|
43
|
+
return electronBinary;
|
|
44
|
+
}
|
|
45
|
+
const electronPkgDir = resolveElectronPackageDir(basePath);
|
|
46
|
+
const directBinary = electronExecutablePath(path_1.default.join(electronPkgDir, 'dist'));
|
|
47
|
+
if (fs_1.default.existsSync(directBinary)) {
|
|
48
|
+
return directBinary;
|
|
49
|
+
}
|
|
50
|
+
throw new Error('Electron binary executable file not found');
|
|
51
|
+
}
|
|
52
|
+
function getElectronVersion(basePath) {
|
|
53
|
+
const electronPkgPath = path_1.default.join(resolveElectronPackageDir(basePath), 'package.json');
|
|
26
54
|
if (!fs_1.default.existsSync(electronPkgPath)) {
|
|
27
55
|
throw new Error('electron package.json not found');
|
|
28
56
|
}
|
|
29
57
|
const pkg = JSON.parse(fs_1.default.readFileSync(electronPkgPath, 'utf8'));
|
|
30
|
-
|
|
58
|
+
return pkg.version;
|
|
59
|
+
}
|
|
60
|
+
async function resolveCachedElectron(basePath) {
|
|
61
|
+
const version = getElectronVersion(basePath);
|
|
31
62
|
const platform = process.platform;
|
|
32
63
|
const arch = process.arch;
|
|
64
|
+
const cacheDir = path_1.default.join(os_1.default.homedir(), '.nekostream-cli', 'electron', version, `${platform}-${arch}`);
|
|
65
|
+
const cachedBinary = electronExecutablePath(cacheDir);
|
|
66
|
+
if (fs_1.default.existsSync(cachedBinary)) {
|
|
67
|
+
return cachedBinary;
|
|
68
|
+
}
|
|
33
69
|
console.log(`[download] Downloading Electron v${version} for ${platform}-${arch}...`);
|
|
34
70
|
const { downloadArtifact } = require('@electron/get');
|
|
35
71
|
const zipPath = await downloadArtifact({
|
|
@@ -38,49 +74,44 @@ async function repairElectron(basePath) {
|
|
|
38
74
|
platform,
|
|
39
75
|
arch
|
|
40
76
|
});
|
|
41
|
-
console.log('[extract] Extracting Electron zip to
|
|
42
|
-
|
|
43
|
-
|
|
77
|
+
console.log('[extract] Extracting Electron zip to local cache...');
|
|
78
|
+
fs_1.default.mkdirSync(path_1.default.dirname(cacheDir), { recursive: true });
|
|
79
|
+
const tempDir = `${cacheDir}.tmp-${process.pid}`;
|
|
80
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
81
|
+
fs_1.default.mkdirSync(tempDir, { recursive: true });
|
|
44
82
|
try {
|
|
45
83
|
// Try extract-zip with a 10s timeout to avoid deadlocking on Node v24+
|
|
46
84
|
await runWithTimeout(async () => {
|
|
47
85
|
const extract = require('extract-zip');
|
|
48
|
-
await extract(zipPath, { dir:
|
|
86
|
+
await extract(zipPath, { dir: tempDir });
|
|
49
87
|
}, 10000);
|
|
50
88
|
console.log('[ok] Extraction via extract-zip complete.');
|
|
51
89
|
}
|
|
52
90
|
catch (extractErr) {
|
|
53
91
|
console.log('[warn] extract-zip timed out or failed. Falling back to native system extraction...');
|
|
54
92
|
if (process.platform === 'win32') {
|
|
55
|
-
(0, child_process_1.execSync)(`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${
|
|
93
|
+
(0, child_process_1.execSync)(`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${tempDir}' -Force"`, { stdio: 'inherit' });
|
|
56
94
|
}
|
|
57
95
|
else {
|
|
58
|
-
(0, child_process_1.execSync)(`unzip -o "${zipPath}" -d "${
|
|
96
|
+
(0, child_process_1.execSync)(`unzip -o "${zipPath}" -d "${tempDir}"`, { stdio: 'inherit' });
|
|
59
97
|
}
|
|
60
98
|
console.log('[ok] Extraction via native fallback complete.');
|
|
61
99
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
100
|
+
if (!fs_1.default.existsSync(electronExecutablePath(tempDir))) {
|
|
101
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
102
|
+
throw new Error('Electron binary missing after cache extraction');
|
|
103
|
+
}
|
|
104
|
+
fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
|
|
105
|
+
fs_1.default.renameSync(tempDir, cacheDir);
|
|
106
|
+
if (process.platform !== 'win32') {
|
|
66
107
|
try {
|
|
67
|
-
fs_1.default.
|
|
108
|
+
fs_1.default.chmodSync(cachedBinary, 0o755);
|
|
68
109
|
}
|
|
69
|
-
catch
|
|
70
|
-
// Ignore
|
|
110
|
+
catch {
|
|
111
|
+
// Ignore chmod failures on filesystems that do not support it.
|
|
71
112
|
}
|
|
72
113
|
}
|
|
73
|
-
|
|
74
|
-
const platformPath = process.platform === 'win32' ? 'electron.exe' : (process.platform === 'darwin' ? 'Electron.app/Contents/MacOS/Electron' : 'electron');
|
|
75
|
-
const pathTxtPath = path_1.default.join(basePath, 'node_modules', 'electron', 'path.txt');
|
|
76
|
-
fs_1.default.writeFileSync(pathTxtPath, platformPath);
|
|
77
|
-
// Clear require cache for electron module to force reload
|
|
78
|
-
delete require.cache[require.resolve('electron')];
|
|
79
|
-
const electronBinary = require('electron');
|
|
80
|
-
if (!fs_1.default.existsSync(electronBinary)) {
|
|
81
|
-
throw new Error('Electron binary still missing after fallback repair');
|
|
82
|
-
}
|
|
83
|
-
return electronBinary;
|
|
114
|
+
return cachedBinary;
|
|
84
115
|
}
|
|
85
116
|
async function launchPlayer(streamInfo) {
|
|
86
117
|
// If compiled, __dirname is .../dist. If dev, __dirname is root.
|
|
@@ -93,19 +124,16 @@ async function launchPlayer(streamInfo) {
|
|
|
93
124
|
};
|
|
94
125
|
let electronBinary;
|
|
95
126
|
try {
|
|
96
|
-
electronBinary =
|
|
97
|
-
if (!fs_1.default.existsSync(electronBinary)) {
|
|
98
|
-
throw new Error('Electron binary executable file not found');
|
|
99
|
-
}
|
|
127
|
+
electronBinary = resolveInstalledElectron(basePath);
|
|
100
128
|
}
|
|
101
129
|
catch (e) {
|
|
102
|
-
console.log('[warn] Electron is
|
|
130
|
+
console.log('[warn] Electron binary is missing. Resolving cached player runtime...');
|
|
103
131
|
try {
|
|
104
|
-
electronBinary = await
|
|
105
|
-
console.log('[ok] Electron
|
|
132
|
+
electronBinary = await resolveCachedElectron(basePath);
|
|
133
|
+
console.log('[ok] Electron runtime ready.');
|
|
106
134
|
}
|
|
107
135
|
catch (repairErr) {
|
|
108
|
-
throw new Error(`Failed to resolve
|
|
136
|
+
throw new Error(`Failed to resolve Electron runtime automatically. Please run "npm install electron" or reinstall NekoStream.\nDetails: ${repairErr.message}`);
|
|
109
137
|
}
|
|
110
138
|
}
|
|
111
139
|
return new Promise((resolve, reject) => {
|
package/dist/prompts-wrapper.js
CHANGED
|
@@ -65,19 +65,49 @@ function getGridChoices(choices) {
|
|
|
65
65
|
value: choice.value
|
|
66
66
|
}));
|
|
67
67
|
}
|
|
68
|
+
function normalized(value) {
|
|
69
|
+
return value.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
70
|
+
}
|
|
68
71
|
function gridLayout(choices) {
|
|
69
72
|
const width = (0, text_1.terminalWidth)(96, 58);
|
|
70
73
|
const longest = choices.reduce((max, choice) => Math.max(max, (0, text_1.visibleLength)(choice.name)), 0);
|
|
71
74
|
const columnWidth = Math.max(14, Math.min(28, longest + 2));
|
|
72
75
|
const columns = Math.max(1, Math.min(6, Math.floor(width / (columnWidth + 3)), choices.length));
|
|
73
|
-
const
|
|
74
|
-
|
|
76
|
+
const maxRows = Math.max(6, Math.min(12, (process.stdout.rows || 30) - 12));
|
|
77
|
+
const rows = Math.max(1, Math.min(maxRows, Math.ceil(choices.length / columns)));
|
|
78
|
+
const pageSize = columns * rows;
|
|
79
|
+
return { columnWidth, columns, rows, pageSize };
|
|
80
|
+
}
|
|
81
|
+
function filterGridChoices(choices, query) {
|
|
82
|
+
const term = normalized(query.trim());
|
|
83
|
+
if (!term)
|
|
84
|
+
return choices;
|
|
85
|
+
return choices.filter((choice, index) => {
|
|
86
|
+
const haystack = normalized(`${index + 1} ${choice.name} ${String(choice.value?.number ?? '')}`);
|
|
87
|
+
return haystack.includes(term);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function findNumericChoice(choices, query) {
|
|
91
|
+
const raw = query.trim();
|
|
92
|
+
if (!/^\d+$/.test(raw))
|
|
93
|
+
return undefined;
|
|
94
|
+
const target = Number(raw);
|
|
95
|
+
return choices.find((choice, index) => {
|
|
96
|
+
const valueNumber = Number(choice.value?.number);
|
|
97
|
+
if (Number.isFinite(valueNumber) && valueNumber === target)
|
|
98
|
+
return true;
|
|
99
|
+
if (index + 1 === target)
|
|
100
|
+
return true;
|
|
101
|
+
return new RegExp(`\\b0*${target}\\b`).test(choice.name);
|
|
102
|
+
});
|
|
75
103
|
}
|
|
76
104
|
async function selectGrid(options, signal) {
|
|
77
105
|
const choices = getGridChoices(options.choices || []);
|
|
78
106
|
if (choices.length === 0)
|
|
79
107
|
return undefined;
|
|
80
108
|
let selected = 0;
|
|
109
|
+
let page = 0;
|
|
110
|
+
let query = '';
|
|
81
111
|
let renderedLines = 0;
|
|
82
112
|
const message = normalizeMessage(options.message || 'Chọn');
|
|
83
113
|
const render = () => {
|
|
@@ -86,25 +116,34 @@ async function selectGrid(options, signal) {
|
|
|
86
116
|
readline_1.default.cursorTo(process.stdout, 0);
|
|
87
117
|
readline_1.default.clearScreenDown(process.stdout);
|
|
88
118
|
}
|
|
89
|
-
const
|
|
119
|
+
const filtered = filterGridChoices(choices, query);
|
|
120
|
+
const { columnWidth, columns, rows, pageSize } = gridLayout(filtered);
|
|
121
|
+
const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
122
|
+
page = Math.max(0, Math.min(page, pageCount - 1));
|
|
123
|
+
selected = Math.max(0, Math.min(selected, Math.max(0, filtered.length - 1)));
|
|
124
|
+
const pageStart = page * pageSize;
|
|
125
|
+
const pageChoices = filtered.slice(pageStart, pageStart + pageSize);
|
|
126
|
+
const selectedOnPage = selected - pageStart;
|
|
90
127
|
const lines = [
|
|
91
128
|
`${theme_1.uiText.focus('?')} ${promptMessage(message)}`,
|
|
129
|
+
`${theme_1.uiText.label('Search')} ${theme_1.uiText.value(query || '')}${theme_1.uiText.subtle(query ? '' : 'Gõ số tập/tên để lọc')}`,
|
|
92
130
|
formatSeparator('DANH SÁCH TẬP', false)
|
|
93
131
|
];
|
|
94
132
|
for (let row = 0; row < rows; row++) {
|
|
95
133
|
const cells = [];
|
|
96
134
|
for (let col = 0; col < columns; col++) {
|
|
97
135
|
const index = col * rows + row;
|
|
98
|
-
if (index >=
|
|
136
|
+
if (index >= pageChoices.length)
|
|
99
137
|
continue;
|
|
100
|
-
const prefix = index ===
|
|
101
|
-
const label = (0, text_1.truncate)(
|
|
102
|
-
const cell = `${prefix} ${index ===
|
|
138
|
+
const prefix = index === selectedOnPage ? theme_1.uiText.focus(theme_1.glyphs.pointer) : ' ';
|
|
139
|
+
const label = (0, text_1.truncate)(pageChoices[index].name, columnWidth - 2);
|
|
140
|
+
const cell = `${prefix} ${index === selectedOnPage ? theme_1.uiText.focus(label) : theme_1.uiText.value(label)}`;
|
|
103
141
|
cells.push((0, text_1.padRight)(cell, columnWidth));
|
|
104
142
|
}
|
|
105
143
|
lines.push(cells.join(theme_1.uiText.border(` ${theme_1.glyphs.vertical} `)));
|
|
106
144
|
}
|
|
107
|
-
|
|
145
|
+
const pageLabel = filtered.length > pageSize ? ` · Trang ${page + 1}/${pageCount}` : '';
|
|
146
|
+
lines.push(chalk_1.default.gray(`↑↓←→ di chuyển · PgUp/PgDn trang · Enter chọn · Esc quay lại${pageLabel}`));
|
|
108
147
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
109
148
|
renderedLines = lines.length;
|
|
110
149
|
};
|
|
@@ -128,23 +167,62 @@ async function selectGrid(options, signal) {
|
|
|
128
167
|
reject(new Error('ESC_PRESSED'));
|
|
129
168
|
};
|
|
130
169
|
const onKeypress = (_str, key) => {
|
|
131
|
-
const
|
|
170
|
+
const filtered = filterGridChoices(choices, query);
|
|
171
|
+
const { rows, pageSize } = gridLayout(filtered);
|
|
172
|
+
const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
132
173
|
if (key?.name === 'escape') {
|
|
133
174
|
done(undefined);
|
|
134
175
|
return;
|
|
135
176
|
}
|
|
177
|
+
if (key?.name === 'backspace') {
|
|
178
|
+
query = query.slice(0, -1);
|
|
179
|
+
selected = 0;
|
|
180
|
+
page = 0;
|
|
181
|
+
render();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
136
184
|
if (key?.name === 'return' || key?.name === 'enter') {
|
|
137
|
-
|
|
185
|
+
const numericChoice = findNumericChoice(choices, query);
|
|
186
|
+
if (numericChoice) {
|
|
187
|
+
done(numericChoice.value);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (filtered[selected]) {
|
|
191
|
+
done(filtered[selected].value);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
render();
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (_str && _str >= ' ' && !key?.ctrl && !key?.meta && key?.name !== 'return') {
|
|
199
|
+
query += _str;
|
|
200
|
+
selected = 0;
|
|
201
|
+
page = 0;
|
|
202
|
+
render();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (key?.name === 'pageup') {
|
|
206
|
+
page = Math.max(0, page - 1);
|
|
207
|
+
selected = page * pageSize;
|
|
208
|
+
render();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (key?.name === 'pagedown') {
|
|
212
|
+
page = Math.min(pageCount - 1, page + 1);
|
|
213
|
+
selected = page * pageSize;
|
|
214
|
+
render();
|
|
138
215
|
return;
|
|
139
216
|
}
|
|
140
217
|
if (key?.name === 'up')
|
|
141
218
|
selected = Math.max(0, selected - 1);
|
|
142
219
|
if (key?.name === 'down')
|
|
143
|
-
selected = Math.min(
|
|
220
|
+
selected = Math.min(filtered.length - 1, selected + 1);
|
|
144
221
|
if (key?.name === 'left')
|
|
145
222
|
selected = Math.max(0, selected - rows);
|
|
146
223
|
if (key?.name === 'right')
|
|
147
|
-
selected = Math.min(
|
|
224
|
+
selected = Math.min(filtered.length - 1, selected + rows);
|
|
225
|
+
page = Math.floor(selected / pageSize);
|
|
148
226
|
render();
|
|
149
227
|
};
|
|
150
228
|
signal.addEventListener('abort', onAbort);
|
|
@@ -198,6 +276,40 @@ async function prompts(options) {
|
|
|
198
276
|
return {};
|
|
199
277
|
return { [options.name]: result };
|
|
200
278
|
}
|
|
279
|
+
if (options.type === 'search') {
|
|
280
|
+
const message = normalizeMessage(options.message || 'Tìm');
|
|
281
|
+
const includeBack = shouldOfferBack(options.message || '');
|
|
282
|
+
const baseChoices = mapChoices(options.choices || [], includeBack).filter((choice) => !(choice instanceof prompts_1.Separator));
|
|
283
|
+
const result = await (0, prompts_1.search)({
|
|
284
|
+
message: promptMessage(message),
|
|
285
|
+
pageSize: options.pageSize || 15,
|
|
286
|
+
source: async (term) => {
|
|
287
|
+
const query = normalized(term || '');
|
|
288
|
+
if (!query)
|
|
289
|
+
return baseChoices.slice(0, options.pageSize || 15);
|
|
290
|
+
return baseChoices.filter((choice, index) => {
|
|
291
|
+
const haystack = normalized(`${index + 1} ${choice.name || ''} ${choice.description || ''}`);
|
|
292
|
+
return haystack.includes(query);
|
|
293
|
+
}).slice(0, options.searchLimit || 50);
|
|
294
|
+
},
|
|
295
|
+
theme: {
|
|
296
|
+
helpMode: 'always',
|
|
297
|
+
prefix: {
|
|
298
|
+
idle: theme_1.uiText.focus('?'),
|
|
299
|
+
done: theme_1.uiText.success('>')
|
|
300
|
+
},
|
|
301
|
+
icon: {
|
|
302
|
+
cursor: theme_1.uiText.focus(theme_1.glyphs.pointer)
|
|
303
|
+
},
|
|
304
|
+
style: {
|
|
305
|
+
keysHelpTip: () => chalk_1.default.gray('Gõ tên/số · Enter chọn · Esc quay lại')
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}, { signal: ac.signal, clearPromptOnDone: true });
|
|
309
|
+
if (result === '__GOBACK__')
|
|
310
|
+
return {};
|
|
311
|
+
return { [options.name]: result };
|
|
312
|
+
}
|
|
201
313
|
if (options.type === 'text') {
|
|
202
314
|
const result = await (0, prompts_1.input)({
|
|
203
315
|
message: promptMessage(normalizeMessage(options.message || 'Nhập')),
|
package/package.json
CHANGED
package/player.html
CHANGED
|
@@ -28,61 +28,86 @@
|
|
|
28
28
|
top: 0;
|
|
29
29
|
left: 0;
|
|
30
30
|
right: 0;
|
|
31
|
-
height:
|
|
31
|
+
height: 34px;
|
|
32
32
|
z-index: 10000;
|
|
33
|
-
|
|
34
|
-
align-items: center;
|
|
35
|
-
justify-content: space-between;
|
|
36
|
-
background: linear-gradient(to bottom, rgba(5, 8, 13, 0.82), rgba(5, 8, 13, 0.22), rgba(5, 8, 13, 0));
|
|
37
|
-
color: rgba(255, 255, 255, 0.92);
|
|
33
|
+
background: transparent;
|
|
38
34
|
-webkit-app-region: drag;
|
|
39
35
|
user-select: none;
|
|
40
36
|
pointer-events: auto;
|
|
41
37
|
}
|
|
42
|
-
.nav-title {
|
|
43
|
-
min-width: 0;
|
|
44
|
-
padding-left: 16px;
|
|
45
|
-
font-size: 13px;
|
|
46
|
-
font-weight: 600;
|
|
47
|
-
letter-spacing: 0.01em;
|
|
48
|
-
color: rgba(255, 255, 255, 0.88);
|
|
49
|
-
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
|
|
50
|
-
overflow: hidden;
|
|
51
|
-
text-overflow: ellipsis;
|
|
52
|
-
white-space: nowrap;
|
|
53
|
-
}
|
|
54
38
|
.window-controls {
|
|
39
|
+
position: absolute;
|
|
40
|
+
top: 8px;
|
|
41
|
+
right: 10px;
|
|
55
42
|
display: flex;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
background: rgba(8, 12, 20, 0.38);
|
|
62
|
-
backdrop-filter: blur(10px);
|
|
43
|
+
flex-direction: row-reverse;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 0;
|
|
46
|
+
min-width: 28px;
|
|
47
|
+
height: 28px;
|
|
63
48
|
pointer-events: auto;
|
|
64
49
|
-webkit-app-region: no-drag;
|
|
65
50
|
}
|
|
51
|
+
.window-dot {
|
|
52
|
+
width: 12px;
|
|
53
|
+
height: 12px;
|
|
54
|
+
padding: 0;
|
|
55
|
+
border: 1px solid rgba(255, 255, 255, 0.58);
|
|
56
|
+
border-radius: 999px;
|
|
57
|
+
background: rgba(255, 255, 255, 0.86);
|
|
58
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.45);
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
transition: transform 0.18s ease, background 0.18s ease, border-color 0.18s ease;
|
|
61
|
+
}
|
|
62
|
+
.window-controls:hover .window-dot,
|
|
63
|
+
.window-controls:focus-within .window-dot {
|
|
64
|
+
transform: scale(0.74);
|
|
65
|
+
background: rgba(255, 255, 255, 0.55);
|
|
66
|
+
border-color: rgba(255, 255, 255, 0.75);
|
|
67
|
+
}
|
|
66
68
|
.window-btn {
|
|
67
|
-
width:
|
|
68
|
-
height:
|
|
69
|
+
width: 0;
|
|
70
|
+
height: 28px;
|
|
71
|
+
margin-right: 0;
|
|
72
|
+
padding: 0;
|
|
69
73
|
display: inline-flex;
|
|
70
74
|
align-items: center;
|
|
71
75
|
justify-content: center;
|
|
72
76
|
border: 0;
|
|
73
|
-
border-
|
|
74
|
-
background:
|
|
77
|
+
border-radius: 999px;
|
|
78
|
+
background: rgba(8, 12, 20, 0.58);
|
|
75
79
|
color: rgba(255, 255, 255, 0.82);
|
|
76
|
-
|
|
80
|
+
backdrop-filter: blur(10px);
|
|
81
|
+
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.38);
|
|
82
|
+
font-size: 14px;
|
|
77
83
|
line-height: 1;
|
|
78
84
|
cursor: pointer;
|
|
79
|
-
|
|
85
|
+
opacity: 0;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
transform: translateX(12px) scale(0.82);
|
|
88
|
+
transition: width 0.2s ease, margin-right 0.2s ease, opacity 0.16s ease, transform 0.2s ease, background 0.15s ease, color 0.15s ease;
|
|
89
|
+
}
|
|
90
|
+
.window-controls:hover .window-btn,
|
|
91
|
+
.window-controls:focus-within .window-btn {
|
|
92
|
+
width: 28px;
|
|
93
|
+
margin-right: 7px;
|
|
94
|
+
opacity: 1;
|
|
95
|
+
transform: translateX(0) scale(1);
|
|
96
|
+
}
|
|
97
|
+
.window-controls:hover #window-minimize,
|
|
98
|
+
.window-controls:focus-within #window-minimize {
|
|
99
|
+
transition-delay: 0.02s;
|
|
80
100
|
}
|
|
81
|
-
.window-
|
|
82
|
-
|
|
101
|
+
.window-controls:hover #window-maximize,
|
|
102
|
+
.window-controls:focus-within #window-maximize {
|
|
103
|
+
transition-delay: 0.05s;
|
|
104
|
+
}
|
|
105
|
+
.window-controls:hover #window-close,
|
|
106
|
+
.window-controls:focus-within #window-close {
|
|
107
|
+
transition-delay: 0.08s;
|
|
83
108
|
}
|
|
84
109
|
.window-btn:hover {
|
|
85
|
-
background: rgba(255, 255, 255, 0.
|
|
110
|
+
background: rgba(255, 255, 255, 0.18);
|
|
86
111
|
color: #fff;
|
|
87
112
|
}
|
|
88
113
|
.window-btn.close:hover {
|
|
@@ -128,11 +153,11 @@
|
|
|
128
153
|
</head>
|
|
129
154
|
<body>
|
|
130
155
|
<header id="top-nav">
|
|
131
|
-
<div class="nav-title" id="nav-title">NekoStream Player</div>
|
|
132
156
|
<div class="window-controls">
|
|
133
|
-
<button class="window-btn" id="window-minimize" title="Minimize">-</button>
|
|
134
|
-
<button class="window-btn" id="window-maximize" title="Maximize">□</button>
|
|
135
157
|
<button class="window-btn close" id="window-close" title="Close">×</button>
|
|
158
|
+
<button class="window-btn" id="window-maximize" title="Maximize">□</button>
|
|
159
|
+
<button class="window-btn" id="window-minimize" title="Minimize">-</button>
|
|
160
|
+
<button class="window-dot" title="Window controls" aria-label="Window controls"></button>
|
|
136
161
|
</div>
|
|
137
162
|
</header>
|
|
138
163
|
<div id="loading-overlay">
|
|
@@ -165,10 +190,8 @@
|
|
|
165
190
|
|
|
166
191
|
window.initPlayer = function(streamInfo, baseDir, cleanUA) {
|
|
167
192
|
const container = document.getElementById('player-container');
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (streamInfo.type === 'iframe') {
|
|
193
|
+
|
|
194
|
+
if (streamInfo.type === 'iframe') {
|
|
172
195
|
const overlay = document.getElementById('loading-overlay');
|
|
173
196
|
overlay.style.display = 'flex';
|
|
174
197
|
|