Add files via upload
This commit is contained in:
parent
fe976004b8
commit
462b980ad1
13 changed files with 8859 additions and 0 deletions
BIN
assets/icon.ico
Normal file
BIN
assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
assets/icon.png
Normal file
BIN
assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
BIN
assets/installer-header.bmp
Normal file
BIN
assets/installer-header.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/installer-sidebar.bmp
Normal file
BIN
assets/installer-sidebar.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
BIN
assets/tray-icon.png
Normal file
BIN
assets/tray-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 213 B |
20
installer.nsh
Normal file
20
installer.nsh
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
; nodex.music — custom NSIS installer skin
|
||||||
|
; Dark theme overrides for MUI2
|
||||||
|
|
||||||
|
!define MUI_BGCOLOR "060606"
|
||||||
|
!define MUI_TEXTCOLOR "e0e0e0"
|
||||||
|
|
||||||
|
; Remove unused pages — keep only progress
|
||||||
|
!define MUI_INSTFILESPAGE_PROGRESSBAR "smooth"
|
||||||
|
|
||||||
|
; Abort confirm suppressed for one-click
|
||||||
|
!define MUI_ABORTWARNING_CANCEL_DEFAULT
|
||||||
|
|
||||||
|
; Custom welcome/finish — hide them (one-click install)
|
||||||
|
!macro customInstall
|
||||||
|
; nothing extra
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro customUnInstall
|
||||||
|
; nothing extra
|
||||||
|
!macroend
|
||||||
5378
package-lock.json
generated
Normal file
5378
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
60
package.json
Normal file
60
package.json
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"name": "nodex-music",
|
||||||
|
"version": "1.2.0",
|
||||||
|
"description": "nodex.pw — слушай всё без подписки",
|
||||||
|
"main": "src/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron .",
|
||||||
|
"build": "electron-builder build --win --publish never",
|
||||||
|
"publish": "electron-builder build --win --publish always",
|
||||||
|
"gen-icons": "powershell -ExecutionPolicy Bypass -File scripts/gen-icons.ps1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"electron-updater": "^6.3.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^32.2.7",
|
||||||
|
"electron-builder": "^25.1.8"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "pw.nodex.pw",
|
||||||
|
"productName": "nodex.pw",
|
||||||
|
"copyright": "© 2025 Nodex.pw LTD",
|
||||||
|
"directories": {
|
||||||
|
"output": "dist"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/**/*",
|
||||||
|
"assets/**/*"
|
||||||
|
],
|
||||||
|
"compression": "maximum",
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "nsis-web",
|
||||||
|
"arch": ["x64"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icon": "assets/icon.ico",
|
||||||
|
"artifactName": "${productName}-setup.${ext}",
|
||||||
|
"legalTrademarks": "Nodex.pw LTD",
|
||||||
|
"verifyUpdateCodeSignature": false,
|
||||||
|
"requestedExecutionLevel": "asInvoker"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": true,
|
||||||
|
"perMachine": false,
|
||||||
|
"deleteAppDataOnUninstall": false,
|
||||||
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true,
|
||||||
|
"shortcutName": "nodex.pw",
|
||||||
|
"installerSidebar": "assets/installer-sidebar.bmp",
|
||||||
|
"uninstallerSidebar": "assets/installer-sidebar.bmp",
|
||||||
|
"installerHeader": "assets/installer-header.bmp"
|
||||||
|
},
|
||||||
|
"publish": {
|
||||||
|
"provider": "generic",
|
||||||
|
"url": "https://music.nodex.pw/updates/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
314
src/main.js
Normal file
314
src/main.js
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { app, BrowserWindow, ipcMain, session, shell, nativeTheme, Tray, Menu, nativeImage } = require('electron');
|
||||||
|
const { autoUpdater } = require('electron-updater');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
nativeTheme.themeSource = 'dark';
|
||||||
|
|
||||||
|
// Windows taskbar / notification area name
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
app.setAppUserModelId('Nodex.pw LTD');
|
||||||
|
}
|
||||||
|
|
||||||
|
const CACHE_DIR = path.join(app.getPath('userData'), 'track_cache');
|
||||||
|
const API_BASE = 'https://music.nodex.pw';
|
||||||
|
const ICON_ICO = path.join(__dirname, '..', 'assets', 'icon.ico');
|
||||||
|
const ICON_PNG = path.join(__dirname, '..', 'assets', 'tray-icon.png');
|
||||||
|
|
||||||
|
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||||
|
|
||||||
|
let mainWin = null;
|
||||||
|
let tray = null;
|
||||||
|
|
||||||
|
/* ── Tray ────────────────────────────────────────────────── */
|
||||||
|
function buildTrayMenu() {
|
||||||
|
return Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: 'nodex.pw',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Показать',
|
||||||
|
click: () => { mainWin?.show(); mainWin?.focus(); },
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Выйти',
|
||||||
|
click: () => { app.isQuiting = true; app.quit(); },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTray() {
|
||||||
|
// Use PNG for tray (better on Win11), fall back to ICO
|
||||||
|
const imgPath = fs.existsSync(ICON_PNG) ? ICON_PNG : ICON_ICO;
|
||||||
|
const img = nativeImage.createFromPath(imgPath);
|
||||||
|
// Resize to 16x16 for tray — Win11 prefers this
|
||||||
|
const trayImg = img.resize({ width: 16, height: 16 });
|
||||||
|
|
||||||
|
tray = new Tray(trayImg);
|
||||||
|
tray.setToolTip('nodex.pw');
|
||||||
|
tray.setContextMenu(buildTrayMenu());
|
||||||
|
|
||||||
|
tray.on('click', () => {
|
||||||
|
if (!mainWin) return;
|
||||||
|
if (mainWin.isVisible()) {
|
||||||
|
mainWin.focus();
|
||||||
|
} else {
|
||||||
|
mainWin.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tray.on('double-click', () => {
|
||||||
|
mainWin?.show(); mainWin?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Window ──────────────────────────────────────────────── */
|
||||||
|
function createWindow() {
|
||||||
|
mainWin = new BrowserWindow({
|
||||||
|
width: 1100,
|
||||||
|
height: 700,
|
||||||
|
minWidth: 800,
|
||||||
|
minHeight: 540,
|
||||||
|
frame: false,
|
||||||
|
transparent: false,
|
||||||
|
backgroundColor: '#050508',
|
||||||
|
titleBarStyle: 'hidden',
|
||||||
|
titleBarOverlay: false,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
webSecurity: true,
|
||||||
|
partition: 'persist:main',
|
||||||
|
},
|
||||||
|
icon: ICON_ICO,
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWin.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
||||||
|
|
||||||
|
// Open external links in browser, not in the app
|
||||||
|
mainWin.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
shell.openExternal(url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
mainWin.webContents.on('will-navigate', (e, url) => {
|
||||||
|
if (!url.startsWith('file://')) { e.preventDefault(); shell.openExternal(url); }
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWin.once('ready-to-show', () => {
|
||||||
|
mainWin.show();
|
||||||
|
setupUpdater();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Minimize to tray on close
|
||||||
|
mainWin.on('close', (e) => {
|
||||||
|
if (!app.isQuiting) {
|
||||||
|
e.preventDefault();
|
||||||
|
mainWin.hide();
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
tray?.displayBalloon({
|
||||||
|
iconType: 'none',
|
||||||
|
title: 'nodex.pw',
|
||||||
|
content: 'Работает в фоне. Нажмите на иконку в трее.',
|
||||||
|
noSound: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWin.on('closed', () => { mainWin = null; });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── IPC: Window controls ────────────────────────────────── */
|
||||||
|
ipcMain.on('app-version', (e) => { e.returnValue = app.getVersion(); });
|
||||||
|
ipcMain.on('win-minimize', () => mainWin?.minimize());
|
||||||
|
ipcMain.on('win-maximize', () => {
|
||||||
|
if (mainWin?.isMaximized()) mainWin.unmaximize();
|
||||||
|
else mainWin?.maximize();
|
||||||
|
});
|
||||||
|
ipcMain.on('win-close', () => {
|
||||||
|
// Close button → hide to tray
|
||||||
|
mainWin?.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── IPC: Tray state update (play/pause title) ───────────── */
|
||||||
|
ipcMain.on('tray-update', (_, { title, artist, playing }) => {
|
||||||
|
if (!tray) return;
|
||||||
|
const label = title ? `${title}${artist ? ' — ' + artist : ''}` : 'nodex.pw';
|
||||||
|
tray.setToolTip(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── IPC: Cache management ───────────────────────────────── */
|
||||||
|
ipcMain.handle('cache-check', (_, cacheKey) => {
|
||||||
|
for (const ext of ['mp3', 'm4a', 'opus', 'ogg', 'webm']) {
|
||||||
|
const p = path.join(CACHE_DIR, `${cacheKey}.${ext}`);
|
||||||
|
if (fs.existsSync(p)) return { exists: true, path: p, ext };
|
||||||
|
}
|
||||||
|
return { exists: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('cache-list', () => {
|
||||||
|
try {
|
||||||
|
return fs.readdirSync(CACHE_DIR).map(f => {
|
||||||
|
const fp = path.join(CACHE_DIR, f);
|
||||||
|
const st = fs.statSync(fp);
|
||||||
|
return { file: f, size: st.size, mtime: st.mtimeMs };
|
||||||
|
});
|
||||||
|
} catch { return []; }
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('cache-delete', (_, cacheKey) => {
|
||||||
|
for (const ext of ['mp3', 'm4a', 'opus', 'ogg', 'webm']) {
|
||||||
|
const p = path.join(CACHE_DIR, `${cacheKey}.${ext}`);
|
||||||
|
if (fs.existsSync(p)) { fs.unlinkSync(p); return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('cache-size', () => {
|
||||||
|
try {
|
||||||
|
return fs.readdirSync(CACHE_DIR).reduce((s, f) => {
|
||||||
|
try { return s + fs.statSync(path.join(CACHE_DIR, f)).size; } catch { return s; }
|
||||||
|
}, 0);
|
||||||
|
} catch { return 0; }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── IPC: Download track ─────────────────────────────────── */
|
||||||
|
ipcMain.handle('download-track', async (_, { streamUrl, cacheKey, ext }) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const dest = path.join(CACHE_DIR, `${cacheKey}.${ext || 'mp3'}`);
|
||||||
|
const tmp = dest + '.tmp';
|
||||||
|
const file = fs.createWriteStream(tmp);
|
||||||
|
const fullUrl = streamUrl.startsWith('http') ? streamUrl : API_BASE + streamUrl;
|
||||||
|
const mod = fullUrl.startsWith('https') ? require('https') : require('http');
|
||||||
|
mod.get(fullUrl, { headers: { Cookie: global.authCookie || '' } }, res => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
file.close(); fs.unlink(tmp, () => {});
|
||||||
|
return reject(new Error(`HTTP ${res.statusCode}`));
|
||||||
|
}
|
||||||
|
res.pipe(file);
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close();
|
||||||
|
fs.rename(tmp, dest, err => { if (err) reject(err); else resolve(dest); });
|
||||||
|
});
|
||||||
|
}).on('error', err => {
|
||||||
|
file.close(); fs.unlink(tmp, () => {});
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── IPC: Auth cookie ────────────────────────────────────── */
|
||||||
|
ipcMain.on('set-auth-cookie', (_, cookie) => { global.authCookie = cookie; });
|
||||||
|
|
||||||
|
ipcMain.handle('refresh-auth-cookie', async () => {
|
||||||
|
const ses = session.fromPartition('persist:main');
|
||||||
|
const cookies = await ses.cookies.get({ domain: 'music.nodex.pw' });
|
||||||
|
const str = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||||
|
if (str) global.authCookie = str;
|
||||||
|
return str;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Google OAuth ────────────────────────────────────────── */
|
||||||
|
async function exchangeAuthToken(token) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(`${API_BASE}/auth/exchange`);
|
||||||
|
url.searchParams.set('t', token);
|
||||||
|
require('https').get(url.href, res => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', d => body += d);
|
||||||
|
res.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(body);
|
||||||
|
if (data.ok) {
|
||||||
|
const setCookies = res.headers['set-cookie'] || [];
|
||||||
|
const ses = session.fromPartition('persist:main');
|
||||||
|
for (const raw of setCookies) {
|
||||||
|
const [nameVal, ...attrs] = raw.split(';').map(s => s.trim());
|
||||||
|
const eqIdx = nameVal.indexOf('=');
|
||||||
|
const name = nameVal.slice(0, eqIdx);
|
||||||
|
const value = nameVal.slice(eqIdx + 1);
|
||||||
|
await ses.cookies.set({
|
||||||
|
url: API_BASE, name, value,
|
||||||
|
secure: attrs.some(a => a.toLowerCase() === 'secure'),
|
||||||
|
path: (attrs.find(a => a.toLowerCase().startsWith('path=')) || 'path=/').split('=')[1],
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
const cookies = await ses.cookies.get({ domain: 'music.nodex.pw' });
|
||||||
|
global.authCookie = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||||
|
resolve(data.user);
|
||||||
|
} else { reject(new Error('exchange failed')); }
|
||||||
|
} catch (e) { reject(e); }
|
||||||
|
});
|
||||||
|
}).on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('open-external', (_, url) => { shell.openExternal(url); });
|
||||||
|
|
||||||
|
ipcMain.handle('open-auth-window', () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
const u = new URL(req.url, 'http://localhost');
|
||||||
|
const token = u.searchParams.get('t');
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
|
res.end('<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{margin:0;background:#050508;color:#707070;font-family:\'Segoe UI\',monospace;display:flex;align-items:center;justify-content:center;height:100vh;font-size:13px}</style></head><body><p>Вошли в nodex.pw. Можно закрыть это окно.</p></body></html>');
|
||||||
|
server.close();
|
||||||
|
if (token) {
|
||||||
|
try { const user = await exchangeAuthToken(token); mainWin?.webContents.send('auth-done', global.authCookie); resolve(user); }
|
||||||
|
catch { mainWin?.webContents.send('auth-done', null); resolve(null); }
|
||||||
|
} else { mainWin?.webContents.send('auth-done', null); resolve(null); }
|
||||||
|
});
|
||||||
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
const port = server.address().port;
|
||||||
|
const cb = encodeURIComponent(`http://localhost:${port}`);
|
||||||
|
shell.openExternal(`${API_BASE}/auth/google?electron=${cb}`);
|
||||||
|
});
|
||||||
|
setTimeout(() => { try { server.close(); } catch {} resolve(null); }, 300000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── App ready ───────────────────────────────────────────── */
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow();
|
||||||
|
createTray();
|
||||||
|
app.on('activate', () => { if (!mainWin) createWindow(); });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
// Don't quit on close — stay in tray
|
||||||
|
// Only quit via tray menu "Выйти"
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', () => { app.isQuiting = true; });
|
||||||
|
|
||||||
|
/* ── Auto-updater ────────────────────────────────────────── */
|
||||||
|
function setupUpdater() {
|
||||||
|
autoUpdater.autoDownload = true;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
autoUpdater.setFeedURL({ provider: 'generic', url: `${API_BASE}/updates/` });
|
||||||
|
|
||||||
|
autoUpdater.on('update-available', info => mainWin?.webContents.send('update-available', info));
|
||||||
|
autoUpdater.on('update-not-available',() => mainWin?.webContents.send('update-not-available'));
|
||||||
|
autoUpdater.on('download-progress', p => mainWin?.webContents.send('update-progress', p));
|
||||||
|
autoUpdater.on('update-downloaded', () => mainWin?.webContents.send('update-downloaded'));
|
||||||
|
autoUpdater.on('error', err => mainWin?.webContents.send('update-error', err?.message));
|
||||||
|
|
||||||
|
ipcMain.on('update-download', () => autoUpdater.downloadUpdate());
|
||||||
|
ipcMain.on('update-install', () => autoUpdater.quitAndInstall());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const p = autoUpdater.checkForUpdates();
|
||||||
|
if (p && p.catch) p.catch(() => mainWin?.webContents.send('update-not-available'));
|
||||||
|
else mainWin?.webContents.send('update-not-available');
|
||||||
|
} catch(e) {
|
||||||
|
mainWin?.webContents.send('update-not-available');
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/preload.js
Normal file
43
src/preload.js
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
// Window controls
|
||||||
|
minimize: () => ipcRenderer.send('win-minimize'),
|
||||||
|
maximize: () => ipcRenderer.send('win-maximize'),
|
||||||
|
close: () => ipcRenderer.send('win-close'),
|
||||||
|
|
||||||
|
// Local track cache
|
||||||
|
cacheCheck: (key) => ipcRenderer.invoke('cache-check', key),
|
||||||
|
cacheList: () => ipcRenderer.invoke('cache-list'),
|
||||||
|
cacheDelete: (key) => ipcRenderer.invoke('cache-delete', key),
|
||||||
|
cacheSize: () => ipcRenderer.invoke('cache-size'),
|
||||||
|
|
||||||
|
// Download a stream URL to local cache
|
||||||
|
downloadTrack: (opts) => ipcRenderer.invoke('download-track', opts),
|
||||||
|
|
||||||
|
// Auth cookie
|
||||||
|
setAuthCookie: (c) => ipcRenderer.send('set-auth-cookie', c),
|
||||||
|
refreshAuthCookie: () => ipcRenderer.invoke('refresh-auth-cookie'),
|
||||||
|
|
||||||
|
// Google OAuth
|
||||||
|
openAuthWindow: () => ipcRenderer.invoke('open-auth-window'),
|
||||||
|
onAuthDone: (cb) => ipcRenderer.on('auth-done', (_, d) => cb(d)),
|
||||||
|
onAuthToken: (cb) => ipcRenderer.on('auth-token', (_, t) => cb(t)),
|
||||||
|
|
||||||
|
// Tray: update tooltip with current track
|
||||||
|
trayUpdate: (info) => ipcRenderer.send('tray-update', info),
|
||||||
|
|
||||||
|
// Auto-updater
|
||||||
|
onUpdateAvailable: (cb) => ipcRenderer.on('update-available', (_, i) => cb(i)),
|
||||||
|
onUpdateNotAvailable: (cb) => ipcRenderer.on('update-not-available', () => cb()),
|
||||||
|
onUpdateProgress: (cb) => ipcRenderer.on('update-progress', (_, p) => cb(p)),
|
||||||
|
onUpdateDownloaded: (cb) => ipcRenderer.on('update-downloaded', () => cb()),
|
||||||
|
onUpdateError: (cb) => ipcRenderer.on('update-error', (_, e) => cb(e)),
|
||||||
|
downloadUpdate: () => ipcRenderer.send('update-download'),
|
||||||
|
installUpdate: () => ipcRenderer.send('update-install'),
|
||||||
|
|
||||||
|
openExternal: (url) => ipcRenderer.invoke('open-external', url),
|
||||||
|
|
||||||
|
platform: process.platform,
|
||||||
|
appVersion: ipcRenderer.sendSync('app-version'),
|
||||||
|
});
|
||||||
1413
src/renderer/app.js
Normal file
1413
src/renderer/app.js
Normal file
File diff suppressed because it is too large
Load diff
692
src/renderer/index.html
Normal file
692
src/renderer/index.html
Normal file
|
|
@ -0,0 +1,692 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>nodex.pw</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ONBOARDING -->
|
||||||
|
<div id="onboarding" class="onboarding hidden">
|
||||||
|
<div class="ob-bg"></div>
|
||||||
|
<div class="ob-panel">
|
||||||
|
<div class="ob-step active" data-step="1">
|
||||||
|
<div class="ob-logo-wrap">
|
||||||
|
<svg viewBox="0 0 100 100" fill="none" width="64" height="64">
|
||||||
|
<rect width="100" height="100" rx="18" fill="#141414"/>
|
||||||
|
<rect x="18" y="18" width="10" height="64" fill="#fff"/>
|
||||||
|
<rect x="28" y="18" width="10" height="10" fill="#fff"/>
|
||||||
|
<rect x="38" y="28" width="10" height="10" fill="#fff"/>
|
||||||
|
<rect x="48" y="38" width="10" height="10" fill="#fff"/>
|
||||||
|
<rect x="58" y="48" width="10" height="10" fill="#fff"/>
|
||||||
|
<rect x="68" y="18" width="10" height="64" fill="#fff"/>
|
||||||
|
<rect x="74" y="74" width="6" height="6" fill="#fff" opacity=".5"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ob-title">nodex.pw</div>
|
||||||
|
<div class="ob-sub">made by savsis just for you</div>
|
||||||
|
<div class="ob-desc">слушай Apple Music, YouTube Music и SoundCloud без подписки. всё в одном.</div>
|
||||||
|
<button class="ob-next" data-next="2">начать →</button>
|
||||||
|
</div>
|
||||||
|
<div class="ob-step" data-step="2">
|
||||||
|
<div class="ob-plats">
|
||||||
|
<div class="ob-plat">
|
||||||
|
<svg viewBox="0 0 24 24" fill="#fff" width="28" height="28"><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09z"/><path d="M15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701z"/></svg>
|
||||||
|
<span>Apple Music</span>
|
||||||
|
</div>
|
||||||
|
<div class="ob-plat">
|
||||||
|
<svg viewBox="0 0 24 24" fill="#fff" width="28" height="28"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||||
|
<span>YouTube Music</span>
|
||||||
|
</div>
|
||||||
|
<div class="ob-plat">
|
||||||
|
<svg viewBox="0 0 24 24" fill="#fff" width="28" height="28"><path d="M1.175 12.225c-.041 0-.094.009-.134.009C.455 12.234 0 11.766 0 11.1s.455-1.134 1.041-1.134c.075 0 .15.009.225.025.534-2.866 3.2-5.041 6.384-5.041 3.6 0 6.516 2.941 6.516 6.55 0 .15-.009.3-.016.45h1.058c.734 0 1.325.6 1.325 1.341 0 .741-.591 1.341-1.325 1.341H1.175zm14.766-3.641c-.441 0-.8-.35-.8-.791V3.691a.796.796 0 0 1 .8-.8c.441 0 .8.35.8.8v4.1a.8.8 0 0 1-.8.793zm2.166 1.133c-.441 0-.8-.35-.8-.8V5.825a.796.796 0 0 1 .8-.8c.441 0 .8.35.8.8v3.091a.8.8 0 0 1-.8.8zm2.166-.45c-.441 0-.8-.35-.8-.8V7.375a.796.796 0 0 1 .8-.8c.441 0 .8.35.8.8v1.091a.8.8 0 0 1-.8.8z"/></svg>
|
||||||
|
<span>SoundCloud</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ob-title" style="margin-top:24px">три платформы</div>
|
||||||
|
<div class="ob-desc">ищи на всех сразу. качество до lossless. без рекламы.</div>
|
||||||
|
<button class="ob-next" data-next="3">дальше →</button>
|
||||||
|
<button class="ob-back" data-back="1">← назад</button>
|
||||||
|
</div>
|
||||||
|
<div class="ob-step" data-step="3">
|
||||||
|
<div class="ob-features-list">
|
||||||
|
<div class="ob-feat">пространственный звук 8D/12D/16D</div>
|
||||||
|
<div class="ob-feat">синхронизированные тексты песен</div>
|
||||||
|
<div class="ob-feat">видеоклипы в фоне</div>
|
||||||
|
<div class="ob-feat">офлайн — треки на устройстве</div>
|
||||||
|
<div class="ob-feat">кроссфейд между треками</div>
|
||||||
|
</div>
|
||||||
|
<div class="ob-title">всё включено</div>
|
||||||
|
<div class="ob-sub">и это только начало</div>
|
||||||
|
<button class="ob-next ob-done" data-done="1">открыть nodex.pw</button>
|
||||||
|
<button class="ob-back" data-back="2">← назад</button>
|
||||||
|
</div>
|
||||||
|
<div class="ob-dots">
|
||||||
|
<div class="ob-dot active" data-step="1"></div>
|
||||||
|
<div class="ob-dot" data-step="2"></div>
|
||||||
|
<div class="ob-dot" data-step="3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TITLEBAR -->
|
||||||
|
<div id="titlebar">
|
||||||
|
<div class="tb-left tb-drag">
|
||||||
|
<svg class="tb-logo-icon" viewBox="0 0 100 100" fill="none" width="20" height="20">
|
||||||
|
<rect width="100" height="100" rx="14" fill="#1a1a1a"/>
|
||||||
|
<rect x="18" y="18" width="9" height="56" fill="#fff"/>
|
||||||
|
<rect x="27" y="18" width="9" height="9" fill="#fff"/>
|
||||||
|
<rect x="36" y="27" width="9" height="9" fill="#fff"/>
|
||||||
|
<rect x="45" y="36" width="9" height="9" fill="#fff"/>
|
||||||
|
<rect x="54" y="45" width="9" height="9" fill="#fff"/>
|
||||||
|
<rect x="63" y="18" width="9" height="56" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
<span class="tb-brand">nodex<b>.pw</b></span>
|
||||||
|
<span class="tb-ver">v1.2.0</span>
|
||||||
|
</div>
|
||||||
|
<div class="tb-center tb-drag" id="tb-page-name">Главная</div>
|
||||||
|
<div class="tb-right">
|
||||||
|
<a class="tb-icon-btn" href="https://github.com/imsavsis" title="GitHub" id="tb-github">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||||
|
</a>
|
||||||
|
<button class="tb-win-btn" id="tb-min">
|
||||||
|
<svg viewBox="0 0 10 1" fill="currentColor" width="10" height="1"><rect width="10" height="1"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="tb-win-btn" id="tb-max">
|
||||||
|
<svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2" width="10" height="10"><rect x=".6" y=".6" width="8.8" height="8.8"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="tb-win-btn tb-close" id="tb-close">
|
||||||
|
<svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2" width="10" height="10"><line x1="1" y1="1" x2="9" y2="9"/><line x1="9" y1="1" x2="1" y2="9"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- APP SHELL -->
|
||||||
|
<div class="app-shell" id="app-shell">
|
||||||
|
|
||||||
|
<!-- SIDEBAR (icon-only, 52px) -->
|
||||||
|
<aside class="sidebar" id="sidebar">
|
||||||
|
<div class="sb-nav">
|
||||||
|
<button class="nav-item active" data-page="home" title="Главная">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="18" height="18"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.5z"/><path d="M9 21V12h6v9"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-page="search" title="Поиск">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="18" height="18"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-page="library" title="Избранное">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="18" height="18"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-page="downloads" title="Загрузки">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="18" height="18"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sb-bottom">
|
||||||
|
<button class="nav-item" data-page="settings" title="Настройки">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="18" height="18"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||||
|
</button>
|
||||||
|
<div class="sb-user" id="sidebar-user">
|
||||||
|
<button class="nav-item" id="login-btn" title="Войти">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="18" height="18"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- MAIN -->
|
||||||
|
<main class="main-content" id="main-content">
|
||||||
|
|
||||||
|
<!-- HOME -->
|
||||||
|
<div class="page active" id="page-home">
|
||||||
|
<div class="home-wave-section">
|
||||||
|
<div class="wave-meta">
|
||||||
|
<h2 class="wave-title">Моя волна</h2>
|
||||||
|
<p class="wave-sub">Персональная подборка треков и рекомендаций для вас</p>
|
||||||
|
<div class="wave-actions">
|
||||||
|
<button class="wave-btn-play" id="wave-play-btn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="wave-btn-tune" id="wave-tune-btn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><line x1="4" y1="6" x2="14" y2="6"/><line x1="16" y1="6" x2="20" y2="6"/><circle cx="15" cy="6" r="2"/><line x1="4" y1="12" x2="8" y2="12"/><line x1="10" y1="12" x2="20" y2="12"/><circle cx="9" cy="12" r="2"/><line x1="4" y1="18" x2="16" y2="18"/><line x1="18" y1="18" x2="20" y2="18"/><circle cx="17" cy="18" r="2"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas id="wave-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="home-section">
|
||||||
|
<div class="home-section-hd">
|
||||||
|
<span class="home-section-title">Быстрый выбор</span>
|
||||||
|
<span class="home-section-sub">Недавно прослушанное — запустите волну по любому треку</span>
|
||||||
|
</div>
|
||||||
|
<div id="quick-picks" class="quick-picks"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="home-section">
|
||||||
|
<div class="home-section-hd"><span class="home-section-title">Найти музыку</span></div>
|
||||||
|
<div class="home-search">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="15" height="15"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||||
|
<input id="home-q" placeholder="название, артист или ссылка..." autocomplete="off" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SEARCH -->
|
||||||
|
<div class="page" id="page-search">
|
||||||
|
<div class="search-top">
|
||||||
|
<div class="search-bar">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||||
|
<input id="q" placeholder="Поиск — название, артист, ссылка на трек или плейлист" autocomplete="off" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
<div class="plat-chips">
|
||||||
|
<button class="chip active" data-plat="all">все</button>
|
||||||
|
<button class="chip" data-plat="apple_music">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="11" height="11"><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09z"/><path d="M15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701z"/></svg>
|
||||||
|
Apple
|
||||||
|
</button>
|
||||||
|
<button class="chip" data-plat="soundcloud">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="11" height="11"><path d="M1.175 12.225c-.041 0-.094.009-.134.009C.455 12.234 0 11.766 0 11.1s.455-1.134 1.041-1.134c.075 0 .15.009.225.025.534-2.866 3.2-5.041 6.384-5.041 3.6 0 6.516 2.941 6.516 6.55 0 .15-.009.3-.016.45h1.058c.734 0 1.325.6 1.325 1.341 0 .741-.591 1.341-1.325 1.341H1.175zm14.766-3.641c-.441 0-.8-.35-.8-.791V3.691a.796.796 0 0 1 .8-.8c.441 0 .8.35.8.8v4.1a.8.8 0 0 1-.8.793zm2.166 1.133c-.441 0-.8-.35-.8-.8V5.825a.796.796 0 0 1 .8-.8c.441 0 .8.35.8.8v3.091a.8.8 0 0 1-.8.8zm2.166-.45c-.441 0-.8-.35-.8-.8V7.375a.796.796 0 0 1 .8-.8c.441 0 .8.35.8.8v1.091a.8.8 0 0 1-.8.8z"/></svg>
|
||||||
|
SoundCloud
|
||||||
|
</button>
|
||||||
|
<button class="chip" data-plat="youtube">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="11" height="11"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||||
|
YouTube
|
||||||
|
</button>
|
||||||
|
<button class="chip" data-plat="youtube_music">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="11" height="11"><path d="M12 0C5.376 0 0 5.376 0 12s5.376 12 12 12 12-5.376 12-12S18.624 0 12 0zm0 19.104c-3.924 0-7.104-3.18-7.104-7.104S8.076 4.896 12 4.896s7.104 3.18 7.104 7.104-3.18 7.104-7.104 7.104zm0-11.304c-2.316 0-4.2 1.884-4.2 4.2s1.884 4.2 4.2 4.2 4.2-1.884 4.2-4.2-1.884-4.2-4.2-4.2z"/></svg>
|
||||||
|
YTM
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="search-empty" class="state-empty">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.8" width="56" height="56" style="opacity:.2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||||
|
<div>введите запрос выше</div>
|
||||||
|
</div>
|
||||||
|
<div id="results-wrap" style="display:none">
|
||||||
|
<div class="results-hd">
|
||||||
|
<span id="results-label" class="results-label"></span>
|
||||||
|
<span id="results-count" class="results-count"></span>
|
||||||
|
</div>
|
||||||
|
<div id="results-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LIBRARY -->
|
||||||
|
<div class="page" id="page-library">
|
||||||
|
<div class="page-hd">
|
||||||
|
<h2>Избранное</h2>
|
||||||
|
<span id="lib-count" class="page-count"></span>
|
||||||
|
</div>
|
||||||
|
<div id="lib-list"><div class="state-empty" style="padding:40px 0"><div>войдите чтобы видеть избранное</div></div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DOWNLOADS -->
|
||||||
|
<div class="page" id="page-downloads">
|
||||||
|
<div class="page-hd">
|
||||||
|
<h2>Загрузки</h2>
|
||||||
|
<span id="dl-count" class="page-count"></span>
|
||||||
|
</div>
|
||||||
|
<div class="page-hint">треки на устройстве — воспроизводятся без интернета</div>
|
||||||
|
<div id="dl-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SETTINGS -->
|
||||||
|
<div class="page" id="page-settings">
|
||||||
|
<div class="page-hd"><h2>Настройки</h2></div>
|
||||||
|
|
||||||
|
<div class="settings-cols">
|
||||||
|
|
||||||
|
<div class="s-col">
|
||||||
|
<div class="s-section">
|
||||||
|
<div class="s-section-title">Профиль</div>
|
||||||
|
<div class="s-card">
|
||||||
|
<div id="profile-row-guest" class="profile-row">
|
||||||
|
<div class="profile-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="24" height="24"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="profile-name">Гость</div>
|
||||||
|
<div class="profile-email">войдите чтобы сохранять избранное</div>
|
||||||
|
</div>
|
||||||
|
<button class="s-btn-w" onclick="openLogin()">Войти</button>
|
||||||
|
</div>
|
||||||
|
<div id="profile-row-user" class="profile-row" style="display:none">
|
||||||
|
<div id="settings-avatar" class="profile-avatar"></div>
|
||||||
|
<div>
|
||||||
|
<div class="profile-name" id="settings-name">—</div>
|
||||||
|
<div class="profile-email" id="settings-email">—</div>
|
||||||
|
</div>
|
||||||
|
<button class="s-btn-danger" id="logout-btn">Выйти</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s-section">
|
||||||
|
<div class="s-section-title">Воспроизведение</div>
|
||||||
|
<div class="s-card">
|
||||||
|
<div class="s-row">
|
||||||
|
<span>Качество</span>
|
||||||
|
<select id="s-quality" class="s-select">
|
||||||
|
<option value="lossless">Lossless (FLAC)</option>
|
||||||
|
<option value="320" selected>320 kbps (MP3)</option>
|
||||||
|
<option value="192">192 kbps</option>
|
||||||
|
<option value="128">128 kbps</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="s-row">
|
||||||
|
<span>Громкость</span>
|
||||||
|
<input type="range" id="s-vol" min="0" max="100" value="80" class="s-range">
|
||||||
|
<span class="s-val" id="s-vol-val">80%</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-row">
|
||||||
|
<span>Кроссфейд</span>
|
||||||
|
<input type="range" id="s-crossfade" min="0" max="10" value="3" class="s-range">
|
||||||
|
<span class="s-val" id="s-crossfade-val">3 сек</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-row">
|
||||||
|
<span>Видеофон</span>
|
||||||
|
<label class="s-toggle">
|
||||||
|
<input type="checkbox" id="s-videobg" checked>
|
||||||
|
<span class="s-toggle-track"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="s-row">
|
||||||
|
<span>Нормализация</span>
|
||||||
|
<label class="s-toggle">
|
||||||
|
<input type="checkbox" id="s-normalize">
|
||||||
|
<span class="s-toggle-track"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s-section">
|
||||||
|
<div class="s-section-title">Тема</div>
|
||||||
|
<div class="s-card">
|
||||||
|
<div class="theme-grid" id="theme-grid">
|
||||||
|
<button class="theme-btn active" data-theme="dark">
|
||||||
|
<div class="theme-preview" style="background:#0c0c0c;border-color:rgba(255,255,255,.1)"><div style="background:#fff;width:30%;height:2px;margin:4px;border-radius:1px"></div></div>
|
||||||
|
<span>Тёмная</span>
|
||||||
|
</button>
|
||||||
|
<button class="theme-btn" data-theme="light">
|
||||||
|
<div class="theme-preview" style="background:#f5f5f5;border-color:rgba(0,0,0,.1)"><div style="background:#111;width:30%;height:2px;margin:4px;border-radius:1px"></div></div>
|
||||||
|
<span>Светлая</span>
|
||||||
|
</button>
|
||||||
|
<button class="theme-btn" data-theme="midnight">
|
||||||
|
<div class="theme-preview" style="background:#050510;border-color:rgba(100,100,255,.2)"><div style="background:#aaf;width:30%;height:2px;margin:4px;border-radius:1px"></div></div>
|
||||||
|
<span>Полночь</span>
|
||||||
|
</button>
|
||||||
|
<button class="theme-btn" data-theme="sepia">
|
||||||
|
<div class="theme-preview" style="background:#1a1209;border-color:rgba(200,150,50,.2)"><div style="background:#e8c87a;width:30%;height:2px;margin:4px;border-radius:1px"></div></div>
|
||||||
|
<span>Тёплая</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s-col">
|
||||||
|
<div class="s-section">
|
||||||
|
<div class="s-section-title">Кэш</div>
|
||||||
|
<div class="s-card">
|
||||||
|
<div class="s-row"><span>Размер кэша</span><span class="s-val" id="s-cache-size">—</span></div>
|
||||||
|
<div class="s-row"><button class="s-btn-danger" id="clear-cache-btn">Очистить</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s-section">
|
||||||
|
<div class="s-section-title">Обновления</div>
|
||||||
|
<div class="s-card">
|
||||||
|
<div class="s-row"><span>Версия</span><span class="s-val" id="s-ver">—</span></div>
|
||||||
|
<div class="s-row" id="update-row" style="display:none">
|
||||||
|
<span id="update-msg-s"></span>
|
||||||
|
<button class="s-btn-w" id="update-action-btn-s">Скачать</button>
|
||||||
|
</div>
|
||||||
|
<div class="s-row"><span>Автообновление</span><label class="s-toggle"><input type="checkbox" id="s-autoupdate" checked><span class="s-toggle-track"></span></label></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s-section">
|
||||||
|
<div class="s-section-title">UDP — что нового в v1.2.0</div>
|
||||||
|
<div class="s-card s-changelog">
|
||||||
|
<div class="cl-entry">пространственный звук — реальное вращение в наушниках</div>
|
||||||
|
<div class="cl-entry">видеоклипы в фоне полного плеера</div>
|
||||||
|
<div class="cl-entry">кроссфейд между треками</div>
|
||||||
|
<div class="cl-entry">темы приложения на выбор</div>
|
||||||
|
<div class="cl-entry">предзагрузка следующего трека</div>
|
||||||
|
<div class="cl-entry">lossless/320 kbps качество</div>
|
||||||
|
<div class="cl-entry">синхронизированные тексты — lrclib + Genius</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s-section">
|
||||||
|
<div class="s-section-title">О приложении</div>
|
||||||
|
<div class="s-card s-about">
|
||||||
|
<svg viewBox="0 0 100 100" fill="none" width="36" height="36"><rect width="100" height="100" rx="18" fill="#141414"/><rect x="18" y="18" width="10" height="64" fill="#fff"/><rect x="28" y="18" width="10" height="10" fill="#fff"/><rect x="38" y="28" width="10" height="10" fill="#fff"/><rect x="48" y="38" width="10" height="10" fill="#fff"/><rect x="58" y="48" width="10" height="10" fill="#fff"/><rect x="68" y="18" width="10" height="64" fill="#fff"/></svg>
|
||||||
|
<div class="about-name">nodex.pw</div>
|
||||||
|
<div class="about-credit">made by savsis just for you</div>
|
||||||
|
<div class="about-plats">Apple Music · SoundCloud · YouTube · YTM</div>
|
||||||
|
<a class="about-gh" href="https://github.com/imsavsis" id="about-github">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="13" height="13"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||||
|
github.com/imsavsis
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BOTTOM PLAYER BAR (Mimosa style) -->
|
||||||
|
<div id="player-bar" class="player-bar">
|
||||||
|
<div class="pb-prog-wrap">
|
||||||
|
<div class="pb-prog-bg">
|
||||||
|
<div class="pb-prog-fill" id="pb-fill"></div>
|
||||||
|
<div class="pb-prog-loading" id="pb-loading-bar" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
<input class="pb-seek" type="range" id="pb-seek" min="0" max="100" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="pb-body">
|
||||||
|
<!-- LEFT: art + meta + quick actions -->
|
||||||
|
<div class="pb-left">
|
||||||
|
<div class="pb-art-wrap" id="pb-art-open">
|
||||||
|
<img class="pb-art" id="pb-art" src="" alt="">
|
||||||
|
<div class="pb-art-ph" id="pb-art-ph">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" width="18" height="18"><path d="M9 18V5l12-2v13M9 18a3 3 0 1 1-6 0 3 3 0 0 1 6 0zm12 0a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pb-meta" id="pb-art-open2">
|
||||||
|
<div class="pb-title" id="pb-title">—</div>
|
||||||
|
<div class="pb-artist" id="pb-artist">—</div>
|
||||||
|
</div>
|
||||||
|
<button class="pb-action-btn" id="pb-fav" title="В избранное">
|
||||||
|
<svg id="pb-fav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="17" height="17"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="pb-action-btn" id="pb-dislike" title="Не нравится">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="17" height="17"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- CENTER: controls + seek -->
|
||||||
|
<div class="pb-center">
|
||||||
|
<div class="pb-controls">
|
||||||
|
<button class="pb-ctrl-btn" id="pb-shuffle" title="Перемешать">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="pb-ctrl-btn pb-prev" id="pb-prev" title="Назад">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="pb-play-btn" id="pb-play" title="Воспроизвести">
|
||||||
|
<svg id="pb-play-icon" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M8 5v14l11-7z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="pb-ctrl-btn pb-next" id="pb-next" title="Следующий">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="m6 18 8.5-6L6 6v12zm2.5-6 8.5 6V6z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="pb-ctrl-btn" id="pb-repeat" title="Повтор">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- RIGHT: extra + time -->
|
||||||
|
<div class="pb-right">
|
||||||
|
<button class="pb-ctrl-btn" id="pb-lyrics-btn" title="Текст">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="pb-ctrl-btn" id="pb-spatial-btn" title="Пространственный звук">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><path d="M12 2a10 10 0 0 1 0 20"/><path d="M12 6a6 6 0 0 1 0 12"/><path d="M12 10a2 2 0 0 1 0 4"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="pb-ctrl-btn" id="pb-vol-btn" title="Громкость">
|
||||||
|
<svg id="pb-vol-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M11 5 6 9H2v6h4l5 5V5zM15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>
|
||||||
|
</button>
|
||||||
|
<input class="pb-vol-range" type="range" id="pb-vol" min="0" max="100" value="80">
|
||||||
|
<div class="pb-time">
|
||||||
|
<span id="pb-cur">0:00</span>
|
||||||
|
<span class="pb-time-sep">/</span>
|
||||||
|
<span id="pb-dur">—</span>
|
||||||
|
</div>
|
||||||
|
<button class="pb-ctrl-btn pb-eq-btn" id="pb-eq-btn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><rect x="4" y="14" width="2" height="7"/><rect x="9" y="10" width="2" height="11"/><rect x="14" y="6" width="2" height="15"/><rect x="19" y="2" width="2" height="19"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="pb-ctrl-btn pb-expand" id="pb-expand" title="Открыть плеер">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="m5 15 7-7 7 7"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SPATIAL AUDIO PANEL (popover) -->
|
||||||
|
<div class="spatial-panel" id="spatial-panel">
|
||||||
|
<div class="sp-hd">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="14" height="14"><path d="M12 2a10 10 0 0 1 0 20"/><path d="M12 6a6 6 0 0 1 0 12"/><path d="M12 10a2 2 0 0 1 0 4"/></svg>
|
||||||
|
<span>Пространственный звук</span>
|
||||||
|
<button class="sp-power" id="sp-power">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13"><path d="M18.36 6.64A9 9 0 1 1 5.64 6.64"/><line x1="12" y1="2" x2="12" y2="12"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sp-headphone-note">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="12" height="12"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>
|
||||||
|
Эффект рассчитан на прослушивание в наушниках
|
||||||
|
</div>
|
||||||
|
<div class="sp-modes">
|
||||||
|
<button class="sp-mode active" data-mode="0">Выкл</button>
|
||||||
|
<button class="sp-mode" data-mode="8">8D</button>
|
||||||
|
<button class="sp-mode" data-mode="12">12D</button>
|
||||||
|
<button class="sp-mode" data-mode="16">16D</button>
|
||||||
|
</div>
|
||||||
|
<div class="sp-slider-row">
|
||||||
|
<label>Интенсивность</label>
|
||||||
|
<input type="range" class="sp-slider" id="sp-intensity" min="0" max="100" value="90">
|
||||||
|
<span id="sp-intensity-val">90%</span>
|
||||||
|
</div>
|
||||||
|
<div class="sp-slider-row">
|
||||||
|
<label>Скорость вращения</label>
|
||||||
|
<input type="range" class="sp-slider" id="sp-speed" min="5" max="100" value="32">
|
||||||
|
<span id="sp-speed-val">32%</span>
|
||||||
|
</div>
|
||||||
|
<div class="sp-rooms">
|
||||||
|
<label>Комната</label>
|
||||||
|
<div class="sp-room-btns">
|
||||||
|
<button class="sp-mode active" data-room="none">Нет</button>
|
||||||
|
<button class="sp-mode" data-room="small">Малая</button>
|
||||||
|
<button class="sp-mode" data-room="hall">Зал</button>
|
||||||
|
<button class="sp-mode" data-room="large">Большая</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LYRICS PANEL -->
|
||||||
|
<div class="lyrics-panel" id="lyrics-panel">
|
||||||
|
<div class="lp-hd">
|
||||||
|
<span>Текст песни</span>
|
||||||
|
<div class="lp-sources" id="lp-sources"></div>
|
||||||
|
<button class="lp-close" id="lp-close">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="lp-scroll" id="lp-scroll">
|
||||||
|
<div class="lp-inner" id="lp-inner">
|
||||||
|
<div class="lp-empty" id="lp-empty">ищем текст...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FULL PLAYER -->
|
||||||
|
<div class="full-player" id="full-player" style="display:none">
|
||||||
|
<div class="fp-video-bg" id="fp-video-bg">
|
||||||
|
<iframe id="fp-yt-frame" src="" allow="autoplay" allowfullscreen frameborder="0"></iframe>
|
||||||
|
<div class="fp-video-overlay"></div>
|
||||||
|
</div>
|
||||||
|
<div class="fp-art-bg" id="fp-art-bg"></div>
|
||||||
|
|
||||||
|
<div class="fp-inner">
|
||||||
|
<button class="fp-down" id="fp-down">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M7.41 8.59 12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
|
||||||
|
</button>
|
||||||
|
<div class="fp-label">сейчас играет</div>
|
||||||
|
|
||||||
|
<div class="fp-art-wrap" id="fp-art-wrap">
|
||||||
|
<img class="fp-art" id="fp-art" src="" alt="" crossorigin="anonymous">
|
||||||
|
<div class="fp-art-ph" id="fp-art-ph"><svg viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.8"><path d="M9 18V5l12-2v13M9 18a3 3 0 1 1-6 0 3 3 0 0 1 6 0zm12 0a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/></svg></div>
|
||||||
|
</div>
|
||||||
|
<div class="fp-karaoke" id="fp-karaoke" style="display:none">
|
||||||
|
<div class="fp-karaoke-scroll" id="fp-karaoke-scroll">
|
||||||
|
<div class="fp-karaoke-inner" id="fp-karaoke-inner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fp-meta-row">
|
||||||
|
<div class="fp-meta">
|
||||||
|
<div class="fp-title" id="fp-title">—</div>
|
||||||
|
<div class="fp-artist" id="fp-artist">—</div>
|
||||||
|
</div>
|
||||||
|
<button class="fp-fav" id="fp-fav">
|
||||||
|
<svg id="fp-fav-icon" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="2" width="22" height="22"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fp-seek-wrap">
|
||||||
|
<div class="fp-seek-track">
|
||||||
|
<div class="fp-seek-fill" id="fp-seek-fill"></div>
|
||||||
|
<div class="fp-seek-loading" id="fp-seek-loading" style="display:none"></div>
|
||||||
|
<input class="fp-seek" type="range" id="fp-seek" min="0" max="100" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="fp-times">
|
||||||
|
<span id="fp-cur">0:00</span>
|
||||||
|
<span class="fp-loading-label" id="fp-loading-label" style="display:none">загружаем...</span>
|
||||||
|
<span id="fp-dur">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fp-controls">
|
||||||
|
<button class="fp-ctrl fp-sm" id="fp-lyrics-btn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="17" height="17"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="fp-ctrl fp-sm" id="fp-shuffle2">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="17" height="17"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="fp-ctrl" id="fp-prev">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="22" height="22"><path d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="fp-ctrl fp-play-big" id="fp-play">
|
||||||
|
<svg id="fp-play-icon" viewBox="0 0 24 24" fill="currentColor" width="26" height="26"><path d="M8 5v14l11-7z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="fp-ctrl" id="fp-next">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="22" height="22"><path d="m6 18 8.5-6L6 6v12zm2.5-6 8.5 6V6z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="fp-ctrl fp-sm" id="fp-repeat2">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="17" height="17"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="fp-ctrl fp-sm" id="fp-spatial-btn2">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="17" height="17"><path d="M12 2a10 10 0 0 1 0 20"/><path d="M12 6a6 6 0 0 1 0 12"/><path d="M12 10a2 2 0 0 1 0 4"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fp-vol-row">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="13" height="13" style="opacity:.3"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>
|
||||||
|
<input class="fp-vol" type="range" id="fp-vol" min="0" max="100" value="80">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fp-cached" id="fp-cached" style="display:none">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="11" height="11"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
|
на устройстве
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LOGIN MODAL -->
|
||||||
|
<div class="modal-bg" id="m-login" style="display:none">
|
||||||
|
<div class="modal">
|
||||||
|
<button class="modal-x" id="m-login-x"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
|
||||||
|
<h3 class="modal-title">Войти</h3>
|
||||||
|
<div class="modal-sub">сохраняй избранное и историю треков</div>
|
||||||
|
<button id="google-btn" class="google-btn">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
|
||||||
|
Войти через Google
|
||||||
|
</button>
|
||||||
|
<div class="modal-divider">или email</div>
|
||||||
|
<div id="step1">
|
||||||
|
<input class="modal-input" type="email" id="e-email" placeholder="your@email.com">
|
||||||
|
<button class="btn-w" style="width:100%;margin-top:8px" id="send-btn">Отправить код</button>
|
||||||
|
</div>
|
||||||
|
<div id="step2" style="display:none">
|
||||||
|
<input class="modal-input" type="text" id="e-code" placeholder="Код из письма" maxlength="6">
|
||||||
|
<div class="modal-row-end">
|
||||||
|
<button class="btn-ghost" id="back-btn">← Назад</button>
|
||||||
|
<button class="btn-w" id="verify-btn">Подтвердить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="err-msg" id="auth-err" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LOGIN MODAL -->
|
||||||
|
<div id="login-modal" style="display:none;position:fixed;inset:0;z-index:950;background:rgba(0,0,0,.75);align-items:center;justify-content:center;backdrop-filter:blur(8px)">
|
||||||
|
<div style="background:var(--bg2);border:1px solid var(--border);border-radius:16px;padding:32px 28px;width:360px;display:flex;flex-direction:column;gap:0;position:relative">
|
||||||
|
<button id="modal-close" style="position:absolute;top:14px;right:14px;background:none;border:none;cursor:pointer;color:var(--dim);font-size:18px;line-height:1">✕</button>
|
||||||
|
|
||||||
|
<!-- view: main -->
|
||||||
|
<div id="auth-view-main">
|
||||||
|
<div style="font-size:18px;font-weight:700;margin-bottom:6px">Войти в nodex.pw</div>
|
||||||
|
<div style="font-size:13px;color:var(--dim);margin-bottom:20px">синхронизация избранного между устройствами</div>
|
||||||
|
|
||||||
|
<button id="google-login-btn" style="display:flex;align-items:center;justify-content:center;gap:10px;width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:10px;padding:12px 16px;cursor:pointer;color:var(--text);font-size:14px;font-family:inherit;font-weight:500;margin-bottom:10px;transition:background .15s">
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
|
||||||
|
Войти через Google
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;margin:4px 0 10px">
|
||||||
|
<div style="flex:1;height:1px;background:var(--border)"></div>
|
||||||
|
<span style="color:var(--dim);font-size:12px">или</span>
|
||||||
|
<div style="flex:1;height:1px;background:var(--border)"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:6px;margin-bottom:10px">
|
||||||
|
<button id="tab-login" style="flex:1;padding:8px;border-radius:7px;border:none;cursor:pointer;font-size:13px;font-family:inherit;background:var(--accent);color:#fff;font-weight:600">Войти</button>
|
||||||
|
<button id="tab-register" style="flex:1;padding:8px;border-radius:7px;border:1px solid var(--border);cursor:pointer;font-size:13px;font-family:inherit;background:none;color:var(--dim)">Регистрация</button>
|
||||||
|
</div>
|
||||||
|
<input id="auth-email" type="text" placeholder="Логин (a-z 0-9 _ - .)" style="background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text);font-size:14px;font-family:inherit;outline:none;width:100%;margin-bottom:8px">
|
||||||
|
<input id="auth-name" type="text" placeholder="Имя (необязательно)" style="display:none;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text);font-size:14px;font-family:inherit;outline:none;width:100%;margin-bottom:8px">
|
||||||
|
<input id="auth-pass" type="password" placeholder="Пароль" style="background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text);font-size:14px;font-family:inherit;outline:none;width:100%;margin-bottom:8px">
|
||||||
|
<div id="auth-error" style="display:none;color:#f56;font-size:12px;padding:0 2px;margin-bottom:6px"></div>
|
||||||
|
<button id="auth-submit-btn" style="background:var(--accent);border:none;border-radius:8px;padding:11px 16px;cursor:pointer;color:#fff;font-size:14px;font-weight:600;font-family:inherit;width:100%;margin-bottom:8px">Войти</button>
|
||||||
|
<button id="guest-login-btn" style="background:none;border:1px solid var(--border);border-radius:8px;padding:9px 16px;cursor:pointer;color:var(--dim);font-size:13px;font-family:inherit;width:100%">Продолжить как гость</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- view: code entry -->
|
||||||
|
<div id="auth-view-code" style="display:none;text-align:center">
|
||||||
|
<div style="font-size:18px;font-weight:700;margin-bottom:8px">Введи код</div>
|
||||||
|
<div style="font-size:13px;color:var(--dim);margin-bottom:24px">Сайт показал 6-значный код — введи его ниже</div>
|
||||||
|
<div style="display:flex;justify-content:center;gap:8px;margin-bottom:20px" id="code-boxes">
|
||||||
|
<input class="code-box" maxlength="1" inputmode="numeric" pattern="[0-9]">
|
||||||
|
<input class="code-box" maxlength="1" inputmode="numeric" pattern="[0-9]">
|
||||||
|
<input class="code-box" maxlength="1" inputmode="numeric" pattern="[0-9]">
|
||||||
|
<input class="code-box" maxlength="1" inputmode="numeric" pattern="[0-9]">
|
||||||
|
<input class="code-box" maxlength="1" inputmode="numeric" pattern="[0-9]">
|
||||||
|
<input class="code-box" maxlength="1" inputmode="numeric" pattern="[0-9]">
|
||||||
|
</div>
|
||||||
|
<div id="code-error" style="display:none;color:#f56;font-size:12px;margin-bottom:10px"></div>
|
||||||
|
<button id="code-submit-btn" style="background:var(--accent);border:none;border-radius:8px;padding:11px 24px;cursor:pointer;color:#fff;font-size:14px;font-weight:600;font-family:inherit;width:100%;margin-bottom:8px">Подтвердить</button>
|
||||||
|
<button id="code-back-btn" style="background:none;border:none;cursor:pointer;color:var(--dim);font-size:13px;font-family:inherit">← Назад</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- USER MENU -->
|
||||||
|
<div class="umenu" id="umenu"></div>
|
||||||
|
<!-- CTX MENU -->
|
||||||
|
<div class="ctx-menu" id="ctx-menu" style="display:none"></div>
|
||||||
|
<!-- UPDATE BAR -->
|
||||||
|
<div id="update-bar" style="display:none"><span id="update-msg">доступно обновление</span><button id="update-action-btn">скачать</button><button id="update-dismiss">✕</button></div>
|
||||||
|
<!-- LOAD OVERLAY -->
|
||||||
|
<div class="load-over" id="load-over" style="display:none"><div class="load-box"><div class="spinner"></div><p id="load-msg">загружаем...</p></div></div>
|
||||||
|
<!-- SPLASH -->
|
||||||
|
<div id="splash"><div class="splash-inner"><div class="splash-orb"></div><svg class="splash-logo" viewBox="0 0 100 100" fill="none" width="60" height="60"><rect width="100" height="100" rx="18" fill="#141414"/><rect x="18" y="18" width="10" height="64" fill="#fff"/><rect x="28" y="18" width="10" height="10" fill="#fff"/><rect x="38" y="28" width="10" height="10" fill="#fff"/><rect x="48" y="38" width="10" height="10" fill="#fff"/><rect x="58" y="48" width="10" height="10" fill="#fff"/><rect x="68" y="18" width="10" height="64" fill="#fff"/></svg><div class="splash-name">nodex<span class="splash-dot">.</span>pw</div><div class="splash-update-wrap"><div class="splash-bar"><div class="splash-fill" id="splash-fill"></div></div><div id="splash-status" class="splash-status-row"><span class="splash-status-dot" id="splash-dot-anim"></span><span class="splash-label" id="splash-label">проверяем обновления...</span></div></div><div id="splash-update-ok" class="splash-update-ok" style="display:none">обновление не требуется</div><div id="splash-update-err" class="splash-update-err" style="display:none">не получилось проверить обновление — проверьте интернет соединение</div><div class="splash-credit">made by savsis just for you</div></div></div>
|
||||||
|
|
||||||
|
<audio id="audio" playsinline preload="none"></audio>
|
||||||
|
<canvas id="color-canvas" width="8" height="8" style="display:none"></canvas>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
939
src/renderer/style.css
Normal file
939
src/renderer/style.css
Normal file
|
|
@ -0,0 +1,939 @@
|
||||||
|
/* ── RESET ── */
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
/* ── VARS (dark theme default) ── */
|
||||||
|
:root {
|
||||||
|
--bg: #0c0c0c;
|
||||||
|
--bg2: #141414;
|
||||||
|
--bg3: #1e1e1e;
|
||||||
|
--bg4: #282828;
|
||||||
|
--border: rgba(255,255,255,0.06);
|
||||||
|
--border2: rgba(255,255,255,0.11);
|
||||||
|
--text: #ffffff;
|
||||||
|
--dim: rgba(255,255,255,0.45);
|
||||||
|
--dim2: rgba(255,255,255,0.25);
|
||||||
|
--dim3: rgba(255,255,255,0.12);
|
||||||
|
--accent: #ffffff;
|
||||||
|
--red: #e84466;
|
||||||
|
--green: #4ade80;
|
||||||
|
--titlebar-h: 40px;
|
||||||
|
--sidebar-w: 52px;
|
||||||
|
--player-h: 80px;
|
||||||
|
--font: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme */
|
||||||
|
[data-theme="light"] {
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--bg2: #ebebeb;
|
||||||
|
--bg3: #e0e0e0;
|
||||||
|
--bg4: #d5d5d5;
|
||||||
|
--border: rgba(0,0,0,0.07);
|
||||||
|
--border2: rgba(0,0,0,0.12);
|
||||||
|
--text: #111111;
|
||||||
|
--dim: rgba(0,0,0,0.5);
|
||||||
|
--dim2: rgba(0,0,0,0.3);
|
||||||
|
--dim3: rgba(0,0,0,0.1);
|
||||||
|
--accent: #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Midnight theme */
|
||||||
|
[data-theme="midnight"] {
|
||||||
|
--bg: #06061a;
|
||||||
|
--bg2: #0e0e28;
|
||||||
|
--bg3: #16163a;
|
||||||
|
--bg4: #1e1e4a;
|
||||||
|
--border: rgba(120,120,255,0.1);
|
||||||
|
--border2: rgba(150,150,255,0.18);
|
||||||
|
--text: #e8e8ff;
|
||||||
|
--dim: rgba(200,200,255,0.5);
|
||||||
|
--dim2: rgba(180,180,255,0.3);
|
||||||
|
--dim3: rgba(160,160,255,0.12);
|
||||||
|
--accent: #aaaaff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sepia/Warm theme */
|
||||||
|
[data-theme="sepia"] {
|
||||||
|
--bg: #110e08;
|
||||||
|
--bg2: #1a1510;
|
||||||
|
--bg3: #241c13;
|
||||||
|
--bg4: #2e2418;
|
||||||
|
--border: rgba(200,160,80,0.1);
|
||||||
|
--border2: rgba(220,180,100,0.18);
|
||||||
|
--text: #f0e0b8;
|
||||||
|
--dim: rgba(240,220,160,0.5);
|
||||||
|
--dim2: rgba(230,210,140,0.3);
|
||||||
|
--dim3: rgba(220,190,120,0.12);
|
||||||
|
--accent: #e8c878;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── BASE ── */
|
||||||
|
html, body {
|
||||||
|
height: 100%; overflow: hidden;
|
||||||
|
background: var(--bg); color: var(--text);
|
||||||
|
font-family: var(--font); font-size: 14px; line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── SCROLLBAR ── */
|
||||||
|
::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: var(--dim3); }
|
||||||
|
|
||||||
|
/* ── TITLEBAR ── */
|
||||||
|
#titlebar {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; height: var(--titlebar-h);
|
||||||
|
display: flex; align-items: center;
|
||||||
|
background: var(--bg); border-bottom: 1px solid var(--border);
|
||||||
|
z-index: 500; -webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
.tb-drag { -webkit-app-region: drag; }
|
||||||
|
.tb-left {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 0 14px; flex: 1;
|
||||||
|
}
|
||||||
|
.tb-logo-icon { flex-shrink: 0; }
|
||||||
|
.tb-brand { font-size: 13px; font-weight: 600; letter-spacing: -.3px; }
|
||||||
|
.tb-brand b { font-weight: 400; opacity: .5; }
|
||||||
|
.tb-ver { font-size: 10px; color: var(--dim2); font-weight: 400; }
|
||||||
|
.tb-center { flex: 1; text-align: center; font-size: 13px; color: var(--dim); font-weight: 400; }
|
||||||
|
.tb-right {
|
||||||
|
display: flex; align-items: center; gap: 0;
|
||||||
|
-webkit-app-region: no-drag; padding-right: 0;
|
||||||
|
}
|
||||||
|
.tb-icon-btn {
|
||||||
|
width: 36px; height: var(--titlebar-h);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
color: var(--dim); text-decoration: none;
|
||||||
|
transition: color .15s, background .15s;
|
||||||
|
}
|
||||||
|
.tb-icon-btn:hover { color: var(--text); background: var(--bg3); }
|
||||||
|
.tb-win-btn {
|
||||||
|
width: 42px; height: var(--titlebar-h);
|
||||||
|
background: none; border: none; cursor: pointer;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
color: var(--dim); transition: background .15s, color .15s;
|
||||||
|
}
|
||||||
|
.tb-win-btn:hover { background: var(--bg3); color: var(--text); }
|
||||||
|
.tb-close:hover { background: #e81123; color: #fff; }
|
||||||
|
|
||||||
|
/* ── APP SHELL ── */
|
||||||
|
.app-shell {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--titlebar-h); left: 0; right: 0;
|
||||||
|
bottom: var(--player-h);
|
||||||
|
display: flex; overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── SIDEBAR ── */
|
||||||
|
.sidebar {
|
||||||
|
width: var(--sidebar-w); flex-shrink: 0;
|
||||||
|
display: flex; flex-direction: column; justify-content: space-between;
|
||||||
|
background: var(--bg); border-right: 1px solid var(--border);
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
.sb-nav { display: flex; flex-direction: column; gap: 2px; padding: 4px 0; }
|
||||||
|
.sb-bottom { display: flex; flex-direction: column; gap: 2px; padding: 4px 0; }
|
||||||
|
.nav-item {
|
||||||
|
width: var(--sidebar-w); height: 44px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
background: none; border: none; cursor: pointer;
|
||||||
|
color: var(--dim); border-radius: 0;
|
||||||
|
transition: color .15s, background .15s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.nav-item:hover { color: var(--text); background: var(--dim3); }
|
||||||
|
.nav-item.active { color: var(--text); }
|
||||||
|
.nav-item.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute; left: 0; top: 50%; transform: translateY(-50%);
|
||||||
|
width: 2px; height: 20px; background: var(--text); border-radius: 0 2px 2px 0;
|
||||||
|
}
|
||||||
|
.sb-user { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
|
/* ── MAIN CONTENT ── */
|
||||||
|
.main-content {
|
||||||
|
flex: 1; overflow: hidden; position: relative;
|
||||||
|
}
|
||||||
|
.page {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
opacity: 0; pointer-events: none;
|
||||||
|
transition: opacity .18s ease;
|
||||||
|
overflow-y: auto; overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.page.active { opacity: 1; pointer-events: all; }
|
||||||
|
|
||||||
|
/* ── HOME ── */
|
||||||
|
.home-wave-section {
|
||||||
|
position: relative; height: 260px; overflow: hidden;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
#wave-canvas {
|
||||||
|
position: absolute; inset: 0; width: 100%; height: 100%;
|
||||||
|
}
|
||||||
|
.wave-meta {
|
||||||
|
position: absolute; left: 32px; top: 32px; z-index: 2;
|
||||||
|
}
|
||||||
|
.wave-title { font-size: 28px; font-weight: 600; letter-spacing: -.5px; margin-bottom: 6px; }
|
||||||
|
.wave-sub { font-size: 13px; color: var(--dim); margin-bottom: 20px; }
|
||||||
|
.wave-actions { display: flex; gap: 8px; }
|
||||||
|
.wave-btn-play {
|
||||||
|
width: 38px; height: 38px; border-radius: 50%;
|
||||||
|
background: var(--text); color: var(--bg);
|
||||||
|
border: none; cursor: pointer;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: transform .15s, opacity .15s;
|
||||||
|
}
|
||||||
|
.wave-btn-play:hover { transform: scale(1.08); }
|
||||||
|
.wave-btn-tune {
|
||||||
|
width: 38px; height: 38px; border-radius: 50%;
|
||||||
|
background: var(--bg3); color: var(--dim);
|
||||||
|
border: 1px solid var(--border); cursor: pointer;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: background .15s, color .15s;
|
||||||
|
}
|
||||||
|
.wave-btn-tune:hover { color: var(--text); }
|
||||||
|
|
||||||
|
.home-section { padding: 24px 32px; border-bottom: 1px solid var(--border); }
|
||||||
|
.home-section-hd { margin-bottom: 16px; }
|
||||||
|
.home-section-title { font-size: 16px; font-weight: 600; }
|
||||||
|
.home-section-sub { font-size: 12px; color: var(--dim2); display: block; margin-top: 3px; }
|
||||||
|
|
||||||
|
.quick-picks { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.qp-card {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
background: var(--bg2); border: 1px solid var(--border);
|
||||||
|
border-radius: 6px; padding: 8px 12px 8px 8px;
|
||||||
|
cursor: pointer; min-width: 180px; max-width: 220px;
|
||||||
|
transition: background .15s, border-color .15s, transform .1s;
|
||||||
|
}
|
||||||
|
.qp-card:hover { background: var(--bg3); border-color: var(--border2); transform: translateY(-1px); }
|
||||||
|
.qp-art { width: 38px; height: 38px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
|
||||||
|
.qp-art-ph {
|
||||||
|
width: 38px; height: 38px; border-radius: 4px;
|
||||||
|
background: var(--bg3); display: flex; align-items: center; justify-content: center;
|
||||||
|
color: var(--dim2); flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.qp-meta { min-width: 0; }
|
||||||
|
.qp-title { font-size: 12px; font-weight: 500; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
|
.qp-artist { font-size: 11px; color: var(--dim2); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
|
.qp-empty { color: var(--dim2); font-size: 13px; padding: 4px 0; }
|
||||||
|
|
||||||
|
.home-search {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
background: var(--bg2); border: 1px solid var(--border);
|
||||||
|
border-radius: 6px; padding: 10px 14px; cursor: text;
|
||||||
|
transition: border-color .15s;
|
||||||
|
}
|
||||||
|
.home-search:focus-within { border-color: var(--border2); }
|
||||||
|
.home-search svg { color: var(--dim2); flex-shrink: 0; }
|
||||||
|
#home-q {
|
||||||
|
background: none; border: none; outline: none;
|
||||||
|
color: var(--text); font-size: 14px; font-family: var(--font); flex: 1;
|
||||||
|
}
|
||||||
|
#home-q::placeholder { color: var(--dim2); }
|
||||||
|
|
||||||
|
/* ── SEARCH ── */
|
||||||
|
.search-top { padding: 20px 24px 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.search-bar {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
background: var(--bg2); border: 1px solid var(--border);
|
||||||
|
border-radius: 6px; padding: 10px 14px; margin-bottom: 12px;
|
||||||
|
transition: border-color .15s;
|
||||||
|
}
|
||||||
|
.search-bar:focus-within { border-color: var(--border2); }
|
||||||
|
.search-bar svg { color: var(--dim2); flex-shrink: 0; }
|
||||||
|
#q {
|
||||||
|
background: none; border: none; outline: none;
|
||||||
|
color: var(--text); font-size: 14px; font-family: var(--font); flex: 1;
|
||||||
|
}
|
||||||
|
#q::placeholder { color: var(--dim2); }
|
||||||
|
.plat-chips { display: flex; gap: 6px; padding-bottom: 12px; flex-wrap: wrap; }
|
||||||
|
.chip {
|
||||||
|
display: flex; align-items: center; gap: 5px;
|
||||||
|
background: none; border: 1px solid var(--border);
|
||||||
|
color: var(--dim); font-size: 12px; font-family: var(--font);
|
||||||
|
padding: 4px 12px; border-radius: 20px; cursor: pointer;
|
||||||
|
transition: background .12s, color .12s, border-color .12s;
|
||||||
|
}
|
||||||
|
.chip:hover { color: var(--text); border-color: var(--border2); }
|
||||||
|
.chip.active { background: var(--text); color: var(--bg); border-color: var(--text); }
|
||||||
|
|
||||||
|
.state-empty {
|
||||||
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
|
gap: 12px; padding: 80px 24px;
|
||||||
|
color: var(--dim2); font-size: 13px;
|
||||||
|
}
|
||||||
|
.results-hd {
|
||||||
|
display: flex; align-items: baseline; gap: 10px;
|
||||||
|
padding: 14px 24px 8px;
|
||||||
|
}
|
||||||
|
.results-label { font-size: 13px; color: var(--dim2); }
|
||||||
|
.results-count { font-size: 11px; color: var(--dim2); }
|
||||||
|
|
||||||
|
/* ── TRACK ROW ── */
|
||||||
|
.track-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 32px 40px 1fr auto;
|
||||||
|
gap: 0 10px; align-items: center;
|
||||||
|
padding: 5px 24px; cursor: pointer;
|
||||||
|
transition: background .1s;
|
||||||
|
border-radius: 4px; margin: 0 8px;
|
||||||
|
}
|
||||||
|
.track-row:hover { background: var(--bg2); }
|
||||||
|
.track-row.playing { background: var(--bg2); }
|
||||||
|
.track-row.playing .track-name { color: var(--text); font-weight: 500; }
|
||||||
|
.track-num { color: var(--dim2); font-size: 12px; text-align: center; position: relative; }
|
||||||
|
.track-idx { display: block; }
|
||||||
|
.track-hover-play { display: none !important; position: absolute; inset: 0; align-items: center; justify-content: center; color: var(--text); }
|
||||||
|
.track-row:hover .track-idx { opacity: 0; }
|
||||||
|
.track-row:hover .track-hover-play { display: flex !important; }
|
||||||
|
.track-art { width: 40px; height: 40px; border-radius: 4px; overflow: hidden; flex-shrink: 0; }
|
||||||
|
.track-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||||
|
.track-art-ph { width: 100%; height: 100%; background: var(--bg3); display: flex; align-items: center; justify-content: center; color: var(--dim2); }
|
||||||
|
.track-meta { min-width: 0; }
|
||||||
|
.track-name { font-size: 13px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--dim); }
|
||||||
|
.track-row:hover .track-name { color: var(--text); }
|
||||||
|
.track-artist { font-size: 11px; color: var(--dim2); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
|
.plat-badge {
|
||||||
|
display: inline-flex; align-items: center;
|
||||||
|
font-size: 9px; font-weight: 500; padding: 1px 4px;
|
||||||
|
border-radius: 3px; margin-left: 5px; vertical-align: middle;
|
||||||
|
background: var(--bg3); color: var(--dim2);
|
||||||
|
}
|
||||||
|
.track-dur {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
font-size: 12px; color: var(--dim2); white-space: nowrap;
|
||||||
|
}
|
||||||
|
.pb-icon-btn {
|
||||||
|
background: none; border: none; cursor: pointer; color: var(--dim2);
|
||||||
|
display: flex; align-items: center; padding: 2px;
|
||||||
|
transition: color .12s;
|
||||||
|
}
|
||||||
|
.pb-icon-btn:hover { color: var(--text); }
|
||||||
|
.track-playing-bar { display: flex; gap: 2px; align-items: flex-end; height: 13px; }
|
||||||
|
.track-playing-bar span {
|
||||||
|
width: 2px; background: var(--text); border-radius: 1px;
|
||||||
|
animation: eq-bar .8s ease-in-out infinite alternate;
|
||||||
|
min-height: 2px;
|
||||||
|
}
|
||||||
|
.track-playing-bar span:nth-child(2) { animation-delay: .25s; }
|
||||||
|
.track-playing-bar span:nth-child(3) { animation-delay: .5s; }
|
||||||
|
@keyframes eq-bar { from { height: 3px; } to { height: 13px; } }
|
||||||
|
.track-cached-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--dim2); }
|
||||||
|
|
||||||
|
/* ── PAGE COMMON ── */
|
||||||
|
.page-hd { padding: 24px 24px 16px; border-bottom: 1px solid var(--border); }
|
||||||
|
.page-hd h2 { font-size: 22px; font-weight: 600; letter-spacing: -.3px; }
|
||||||
|
.page-count { font-size: 12px; color: var(--dim2); margin-left: 8px; }
|
||||||
|
.page-hint { padding: 10px 24px; font-size: 12px; color: var(--dim2); }
|
||||||
|
|
||||||
|
/* ── SETTINGS ── */
|
||||||
|
.settings-cols { display: flex; gap: 0; padding: 20px 24px; gap: 20px; overflow-y: auto; max-height: calc(100vh - var(--titlebar-h) - var(--player-h) - 80px); }
|
||||||
|
.s-col { flex: 1; display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
||||||
|
.s-section { }
|
||||||
|
.s-section-title { font-size: 11px; color: var(--dim2); text-transform: uppercase; letter-spacing: 1px; font-weight: 500; margin-bottom: 8px; }
|
||||||
|
.s-card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
||||||
|
.s-row {
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
padding: 12px 16px; border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.s-row:last-child { border-bottom: none; }
|
||||||
|
.s-row > span:first-child { flex: 1; color: var(--dim); }
|
||||||
|
.s-val { color: var(--dim2); font-size: 12px; }
|
||||||
|
.s-range {
|
||||||
|
flex: 1; accent-color: var(--text); cursor: pointer; height: 4px;
|
||||||
|
}
|
||||||
|
.s-select {
|
||||||
|
background: var(--bg3); border: 1px solid var(--border2);
|
||||||
|
color: var(--text); font-family: var(--font); font-size: 12px;
|
||||||
|
padding: 4px 8px; border-radius: 4px; cursor: pointer; outline: none;
|
||||||
|
}
|
||||||
|
.s-btn-w, .s-btn-danger {
|
||||||
|
padding: 6px 14px; border-radius: 4px; border: none;
|
||||||
|
font-family: var(--font); font-size: 12px; cursor: pointer;
|
||||||
|
transition: opacity .15s;
|
||||||
|
}
|
||||||
|
.s-btn-w { background: var(--text); color: var(--bg); font-weight: 500; }
|
||||||
|
.s-btn-danger { background: rgba(232,68,102,0.12); color: var(--red); border: 1px solid rgba(232,68,102,0.2); }
|
||||||
|
.s-btn-danger:hover { background: rgba(232,68,102,0.2); }
|
||||||
|
.s-toggle { position: relative; display: inline-flex; cursor: pointer; }
|
||||||
|
.s-toggle input { position: absolute; opacity: 0; width: 0; }
|
||||||
|
.s-toggle-track {
|
||||||
|
width: 34px; height: 18px; background: var(--bg3); border-radius: 9px;
|
||||||
|
border: 1px solid var(--border2); transition: background .2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.s-toggle-track::after {
|
||||||
|
content: ''; position: absolute;
|
||||||
|
width: 12px; height: 12px; border-radius: 50%;
|
||||||
|
background: var(--dim); top: 2px; left: 2px;
|
||||||
|
transition: left .2s, background .2s;
|
||||||
|
}
|
||||||
|
.s-toggle input:checked ~ .s-toggle-track { background: var(--text); border-color: var(--text); }
|
||||||
|
.s-toggle input:checked ~ .s-toggle-track::after { left: 18px; background: var(--bg); }
|
||||||
|
|
||||||
|
/* Theme grid */
|
||||||
|
.theme-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 12px; }
|
||||||
|
.theme-btn {
|
||||||
|
display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||||||
|
background: none; border: 1px solid var(--border); border-radius: 6px;
|
||||||
|
padding: 8px; cursor: pointer; color: var(--dim); font-size: 11px;
|
||||||
|
font-family: var(--font); transition: border-color .15s, color .15s;
|
||||||
|
}
|
||||||
|
.theme-btn:hover { border-color: var(--border2); color: var(--text); }
|
||||||
|
.theme-btn.active { border-color: var(--text); color: var(--text); }
|
||||||
|
.theme-preview { width: 100%; height: 28px; border-radius: 4px; border-width: 1px; border-style: solid; }
|
||||||
|
|
||||||
|
/* Changelog */
|
||||||
|
.s-changelog { padding: 4px 0; }
|
||||||
|
.cl-entry { padding: 8px 16px; font-size: 12px; color: var(--dim); border-bottom: 1px solid var(--border); }
|
||||||
|
.cl-entry:last-child { border-bottom: none; }
|
||||||
|
.cl-entry::before { content: '— '; color: var(--dim2); }
|
||||||
|
|
||||||
|
/* About */
|
||||||
|
.s-about {
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
padding: 24px; gap: 8px; text-align: center;
|
||||||
|
}
|
||||||
|
.about-name { font-size: 16px; font-weight: 600; }
|
||||||
|
.about-credit { font-size: 12px; color: var(--dim2); }
|
||||||
|
.about-plats { font-size: 11px; color: var(--dim2); }
|
||||||
|
.about-gh {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
color: var(--dim); font-size: 12px; text-decoration: none;
|
||||||
|
margin-top: 4px; transition: color .15s;
|
||||||
|
}
|
||||||
|
.about-gh:hover { color: var(--text); }
|
||||||
|
|
||||||
|
/* Profile */
|
||||||
|
.profile-row { display: flex; align-items: center; gap: 12px; padding: 14px 16px; }
|
||||||
|
.profile-icon { width: 40px; height: 40px; border-radius: 50%; background: var(--bg3); display: flex; align-items: center; justify-content: center; color: var(--dim2); flex-shrink: 0; }
|
||||||
|
.profile-avatar { width: 40px; height: 40px; border-radius: 50%; overflow: hidden; background: var(--bg3); flex-shrink: 0; }
|
||||||
|
.profile-name { font-size: 13px; font-weight: 500; }
|
||||||
|
.profile-email { font-size: 11px; color: var(--dim2); }
|
||||||
|
.profile-row > div:nth-child(2) { flex: 1; min-width: 0; }
|
||||||
|
|
||||||
|
/* ── BOTTOM PLAYER BAR ── */
|
||||||
|
.player-bar {
|
||||||
|
position: fixed; bottom: 0; left: 0; right: 0;
|
||||||
|
height: var(--player-h);
|
||||||
|
background: var(--bg); border-top: 1px solid var(--border);
|
||||||
|
z-index: 300;
|
||||||
|
display: none; flex-direction: column;
|
||||||
|
}
|
||||||
|
.player-bar.visible { display: flex; }
|
||||||
|
.pb-prog-wrap {
|
||||||
|
position: relative; height: 3px; background: transparent;
|
||||||
|
flex-shrink: 0; cursor: pointer;
|
||||||
|
}
|
||||||
|
.pb-prog-bg {
|
||||||
|
position: absolute; inset: 0; background: var(--bg3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.pb-prog-fill {
|
||||||
|
height: 100%; width: 0; background: var(--text);
|
||||||
|
transition: width .1s linear;
|
||||||
|
}
|
||||||
|
.pb-prog-loading {
|
||||||
|
height: 100%; background: linear-gradient(90deg, transparent, var(--dim2), transparent);
|
||||||
|
background-size: 200% 100%; animation: loading-shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
@keyframes loading-shimmer { from { background-position: -200% 0; } to { background-position: 200% 0; } }
|
||||||
|
.pb-seek {
|
||||||
|
position: absolute; inset: -6px 0; opacity: 0;
|
||||||
|
width: 100%; cursor: pointer;
|
||||||
|
}
|
||||||
|
.pb-prog-wrap:hover .pb-prog-fill { background: var(--text); }
|
||||||
|
|
||||||
|
.pb-body {
|
||||||
|
flex: 1; display: flex; align-items: center;
|
||||||
|
padding: 0 16px; gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left section */
|
||||||
|
.pb-left {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
min-width: 220px; max-width: 280px;
|
||||||
|
}
|
||||||
|
.pb-art-wrap {
|
||||||
|
width: 46px; height: 46px; flex-shrink: 0;
|
||||||
|
border-radius: 4px; overflow: hidden; cursor: pointer;
|
||||||
|
background: var(--bg3); position: relative;
|
||||||
|
}
|
||||||
|
.pb-art { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.pb-art-ph { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--dim2); }
|
||||||
|
.pb-meta { flex: 1; min-width: 0; cursor: pointer; }
|
||||||
|
.pb-title { font-size: 12.5px; font-weight: 500; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
|
.pb-artist { font-size: 11px; color: var(--dim2); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
|
.pb-action-btn {
|
||||||
|
background: none; border: none; cursor: pointer; color: var(--dim2);
|
||||||
|
display: flex; align-items: center; padding: 4px;
|
||||||
|
transition: color .12s; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.pb-action-btn:hover { color: var(--text); }
|
||||||
|
.pb-action-btn.active svg { fill: var(--red); stroke: none; }
|
||||||
|
|
||||||
|
/* Center */
|
||||||
|
.pb-center { flex: 1; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.pb-controls { display: flex; align-items: center; gap: 4px; }
|
||||||
|
.pb-ctrl-btn {
|
||||||
|
background: none; border: none; cursor: pointer; color: var(--dim2);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
width: 36px; height: 36px; border-radius: 4px;
|
||||||
|
transition: color .12s, background .12s;
|
||||||
|
}
|
||||||
|
.pb-ctrl-btn:hover { color: var(--text); background: var(--bg2); }
|
||||||
|
.pb-ctrl-btn.active { color: var(--text); }
|
||||||
|
.pb-play-btn {
|
||||||
|
width: 42px; height: 42px; border-radius: 50%;
|
||||||
|
background: var(--text); color: var(--bg);
|
||||||
|
border: none; cursor: pointer;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: transform .12s, opacity .12s;
|
||||||
|
}
|
||||||
|
.pb-play-btn:hover { transform: scale(1.06); }
|
||||||
|
|
||||||
|
/* Right */
|
||||||
|
.pb-right {
|
||||||
|
display: flex; align-items: center; gap: 2px;
|
||||||
|
min-width: 220px; justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.pb-vol-range {
|
||||||
|
width: 80px; accent-color: var(--text); cursor: pointer;
|
||||||
|
height: 3px; margin: 0 4px;
|
||||||
|
}
|
||||||
|
.pb-time {
|
||||||
|
display: flex; align-items: center; gap: 4px;
|
||||||
|
font-size: 12px; color: var(--dim2); white-space: nowrap;
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
.pb-time-sep { opacity: .4; }
|
||||||
|
.pb-eq-btn { }
|
||||||
|
.pb-expand { }
|
||||||
|
|
||||||
|
/* ── SPATIAL PANEL ── */
|
||||||
|
.spatial-panel {
|
||||||
|
position: fixed;
|
||||||
|
bottom: calc(var(--player-h) + 12px);
|
||||||
|
right: 160px;
|
||||||
|
width: 300px;
|
||||||
|
background: var(--bg2); border: 1px solid var(--border2);
|
||||||
|
border-radius: 10px; padding: 16px;
|
||||||
|
z-index: 400;
|
||||||
|
display: none; flex-direction: column; gap: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
||||||
|
animation: panel-up .18s ease;
|
||||||
|
}
|
||||||
|
.spatial-panel.open { display: flex; }
|
||||||
|
@keyframes panel-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
.sp-hd {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
font-size: 12.5px; font-weight: 500;
|
||||||
|
}
|
||||||
|
.sp-hd span { flex: 1; }
|
||||||
|
.sp-power {
|
||||||
|
background: none; border: none; cursor: pointer; color: var(--dim2);
|
||||||
|
display: flex; align-items: center; transition: color .12s;
|
||||||
|
}
|
||||||
|
.sp-power:hover { color: var(--text); }
|
||||||
|
.sp-power.on { color: var(--text); }
|
||||||
|
.sp-headphone-note {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
font-size: 11px; color: var(--dim2);
|
||||||
|
background: var(--bg3); border-radius: 6px; padding: 8px 10px;
|
||||||
|
}
|
||||||
|
.sp-modes, .sp-room-btns { display: flex; gap: 6px; }
|
||||||
|
.sp-mode {
|
||||||
|
flex: 1; padding: 7px; border-radius: 6px;
|
||||||
|
background: var(--bg3); border: 1px solid var(--border);
|
||||||
|
color: var(--dim); font-size: 12px; font-family: var(--font); cursor: pointer;
|
||||||
|
transition: background .12s, color .12s, border-color .12s;
|
||||||
|
}
|
||||||
|
.sp-mode:hover { color: var(--text); border-color: var(--border2); }
|
||||||
|
.sp-mode.active { background: var(--text); color: var(--bg); border-color: var(--text); }
|
||||||
|
.sp-slider-row { display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
||||||
|
.sp-slider-row label { color: var(--dim2); min-width: 100px; font-size: 11px; }
|
||||||
|
.sp-slider { flex: 1; accent-color: var(--text); cursor: pointer; height: 3px; }
|
||||||
|
.sp-slider-row span { color: var(--dim2); font-size: 11px; min-width: 30px; text-align: right; }
|
||||||
|
.sp-rooms { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.sp-rooms label { font-size: 11px; color: var(--dim2); }
|
||||||
|
|
||||||
|
/* ── LYRICS PANEL ── */
|
||||||
|
.lyrics-panel {
|
||||||
|
position: fixed;
|
||||||
|
bottom: var(--player-h); left: var(--sidebar-w); right: 0;
|
||||||
|
height: 50vh; min-height: 240px;
|
||||||
|
background: var(--bg); border-top: 1px solid var(--border);
|
||||||
|
z-index: 400;
|
||||||
|
display: none; flex-direction: column;
|
||||||
|
animation: panel-up-lg .22s ease;
|
||||||
|
}
|
||||||
|
.lyrics-panel.open { display: flex; }
|
||||||
|
@keyframes panel-up-lg { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
||||||
|
.lp-hd {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
padding: 12px 20px; border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 13px; font-weight: 500; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.lp-sources { display: flex; gap: 6px; }
|
||||||
|
.code-box {
|
||||||
|
width: 46px; height: 58px;
|
||||||
|
background: var(--bg3); border: 2px solid var(--border);
|
||||||
|
border-radius: 10px; color: var(--text);
|
||||||
|
font-size: 26px; font-weight: 700; text-align: center;
|
||||||
|
font-family: var(--font); outline: none;
|
||||||
|
transition: border-color .15s;
|
||||||
|
caret-color: transparent;
|
||||||
|
}
|
||||||
|
.code-box:focus { border-color: var(--accent); }
|
||||||
|
.code-box.filled { border-color: var(--accent); color: var(--accent); }
|
||||||
|
|
||||||
|
.lp-source-btn {
|
||||||
|
background: none; border: 1px solid var(--border2);
|
||||||
|
color: var(--dim2); font-size: 11px; font-family: var(--font);
|
||||||
|
padding: 2px 8px; border-radius: 4px; cursor: pointer;
|
||||||
|
transition: background .12s, color .12s;
|
||||||
|
}
|
||||||
|
.lp-source-btn:hover, .lp-source-btn.active { background: var(--text); color: var(--bg); border-color: var(--text); }
|
||||||
|
.lp-close {
|
||||||
|
margin-left: auto; background: none; border: none; cursor: pointer;
|
||||||
|
color: var(--dim2); display: flex; transition: color .12s;
|
||||||
|
}
|
||||||
|
.lp-close:hover { color: var(--text); }
|
||||||
|
.lp-scroll { flex: 1; overflow-y: auto; padding: 20px; }
|
||||||
|
.lp-inner { display: flex; flex-direction: column; align-items: center; gap: 0; }
|
||||||
|
.lp-line {
|
||||||
|
font-size: 18px; line-height: 1.7; color: var(--dim2);
|
||||||
|
cursor: pointer; transition: color .2s, transform .2s;
|
||||||
|
text-align: center; padding: 4px 0; border-radius: 4px;
|
||||||
|
width: 100%; max-width: 600px;
|
||||||
|
}
|
||||||
|
.lp-line:hover { color: var(--text); }
|
||||||
|
.lp-line.active {
|
||||||
|
color: var(--text); font-weight: 500;
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
.lp-empty { color: var(--dim2); font-size: 13px; text-align: center; padding: 40px; }
|
||||||
|
|
||||||
|
/* ── KARAOKE inside full player ── */
|
||||||
|
.fp-karaoke {
|
||||||
|
width: 100%; height: 260px;
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
mask-image: linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);
|
||||||
|
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);
|
||||||
|
}
|
||||||
|
.fp-karaoke-scroll {
|
||||||
|
width: 100%; height: 100%; overflow: hidden; position: relative;
|
||||||
|
}
|
||||||
|
.fp-karaoke-inner {
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
gap: 0; padding: 120px 0;
|
||||||
|
transition: transform .4s cubic-bezier(.25,.8,.25,1);
|
||||||
|
}
|
||||||
|
.fp-k-line {
|
||||||
|
font-size: 22px; font-weight: 600; line-height: 1.55;
|
||||||
|
color: rgba(255,255,255,0.2);
|
||||||
|
text-align: center; padding: 2px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color .3s, transform .3s, text-shadow .3s;
|
||||||
|
width: 100%; max-width: 380px;
|
||||||
|
}
|
||||||
|
.fp-k-line.active {
|
||||||
|
color: #fff;
|
||||||
|
transform: scale(1.08);
|
||||||
|
text-shadow: 0 0 24px rgba(255,255,255,0.6), 0 0 48px rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
.fp-k-line.prev1 { color: rgba(255,255,255,0.5); transform: scale(1.02); }
|
||||||
|
.fp-k-line.prev2 { color: rgba(255,255,255,0.25); }
|
||||||
|
.fp-k-line.next1 { color: rgba(255,255,255,0.4); }
|
||||||
|
.fp-k-line.next2 { color: rgba(255,255,255,0.2); }
|
||||||
|
.fp-k-empty { color: rgba(255,255,255,0.3); font-size: 14px; text-align: center; padding: 80px 0; }
|
||||||
|
|
||||||
|
/* ── FULL PLAYER ── */
|
||||||
|
.full-player {
|
||||||
|
position: fixed; inset: 0; z-index: 450;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.full-player { display: none; }
|
||||||
|
.full-player.visible { display: flex; }
|
||||||
|
|
||||||
|
.fp-video-bg {
|
||||||
|
position: absolute; inset: -10%; z-index: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.fp-video-bg.has-video { display: block; }
|
||||||
|
.fp-yt-frame { width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
|
||||||
|
.fp-video-overlay {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
background: rgba(0,0,0,0.65); backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
.fp-art-bg {
|
||||||
|
position: absolute; inset: 0; z-index: 1;
|
||||||
|
background-size: cover; background-position: center;
|
||||||
|
filter: blur(60px) brightness(0.3) saturate(1.4);
|
||||||
|
transform: scale(1.2);
|
||||||
|
transition: background-image .6s;
|
||||||
|
}
|
||||||
|
.fp-inner {
|
||||||
|
position: relative; z-index: 2;
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
width: 100%; max-width: 420px; padding: 20px;
|
||||||
|
}
|
||||||
|
.fp-down {
|
||||||
|
position: absolute; top: 16px; left: 16px;
|
||||||
|
background: rgba(255,255,255,0.08); border: none;
|
||||||
|
border-radius: 50%; width: 36px; height: 36px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
cursor: pointer; color: var(--text); transition: background .15s;
|
||||||
|
}
|
||||||
|
.fp-down:hover { background: rgba(255,255,255,0.14); }
|
||||||
|
.fp-label {
|
||||||
|
position: absolute; top: 24px; left: 50%; transform: translateX(-50%);
|
||||||
|
font-size: 11px; color: rgba(255,255,255,0.4); letter-spacing: .5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.fp-art-wrap {
|
||||||
|
width: 240px; height: 240px; border-radius: 12px; overflow: hidden;
|
||||||
|
margin: 48px 0 24px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.7);
|
||||||
|
position: relative;
|
||||||
|
animation: art-breathe 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes art-breathe { 0%,100%{transform:scale(1);} 50%{transform:scale(1.018);} }
|
||||||
|
.fp-art { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.fp-art-ph { position: absolute; inset: 0; background: var(--bg2); display: flex; align-items: center; justify-content: center; }
|
||||||
|
.fp-meta-row { display: flex; align-items: center; gap: 12px; width: 100%; margin-bottom: 20px; }
|
||||||
|
.fp-meta { flex: 1; min-width: 0; }
|
||||||
|
.fp-title { font-size: 18px; font-weight: 600; letter-spacing: -.3px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #fff; }
|
||||||
|
.fp-artist { font-size: 13px; color: rgba(255,255,255,0.55); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
|
.fp-fav { background: none; border: none; cursor: pointer; color: rgba(255,255,255,0.45); display: flex; transition: color .12s; }
|
||||||
|
.fp-fav:hover { color: #fff; }
|
||||||
|
.fp-seek-wrap { width: 100%; margin-bottom: 16px; }
|
||||||
|
.fp-seek-track {
|
||||||
|
position: relative; height: 4px; background: rgba(255,255,255,0.15);
|
||||||
|
border-radius: 2px; cursor: pointer; margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.fp-seek-fill { height: 100%; width: 0; background: #fff; border-radius: 2px; transition: width .1s linear; }
|
||||||
|
.fp-seek-loading { height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); background-size: 200% 100%; animation: loading-shimmer 1.5s infinite; }
|
||||||
|
.fp-seek { position: absolute; inset: -10px 0; opacity: 0; width: 100%; cursor: pointer; }
|
||||||
|
.fp-times { display: flex; justify-content: space-between; font-size: 11px; color: rgba(255,255,255,0.4); }
|
||||||
|
.fp-loading-label { position: absolute; left: 50%; transform: translateX(-50%); font-size: 11px; color: rgba(255,255,255,0.4); }
|
||||||
|
.fp-controls { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
|
||||||
|
.fp-ctrl {
|
||||||
|
background: none; border: none; cursor: pointer; color: rgba(255,255,255,0.55);
|
||||||
|
display: flex; align-items: center; justify-content: center; width: 40px; height: 40px;
|
||||||
|
border-radius: 50%; transition: color .12s, background .12s;
|
||||||
|
}
|
||||||
|
.fp-ctrl:hover { color: #fff; background: rgba(255,255,255,0.08); }
|
||||||
|
.fp-ctrl.active { color: #fff; }
|
||||||
|
.fp-sm { width: 36px; height: 36px; }
|
||||||
|
.fp-play-big {
|
||||||
|
width: 56px; height: 56px; background: #fff; color: #000;
|
||||||
|
border-radius: 50%; transition: transform .12s;
|
||||||
|
}
|
||||||
|
.fp-play-big:hover { transform: scale(1.06); }
|
||||||
|
.fp-vol-row { display: flex; align-items: center; gap: 8px; width: 100%; margin-bottom: 10px; }
|
||||||
|
.fp-vol { flex: 1; accent-color: #fff; cursor: pointer; height: 3px; }
|
||||||
|
.fp-cached {
|
||||||
|
display: flex; align-items: center; gap: 5px;
|
||||||
|
font-size: 11px; color: rgba(255,255,255,0.35);
|
||||||
|
background: rgba(255,255,255,0.06); padding: 4px 10px; border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── MODAL ── */
|
||||||
|
.modal-bg {
|
||||||
|
position: fixed; inset: 0; background: rgba(0,0,0,0.7);
|
||||||
|
display: flex; align-items: center; justify-content: center; z-index: 600;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
background: var(--bg2); border: 1px solid var(--border2);
|
||||||
|
border-radius: 12px; padding: 28px; width: 360px;
|
||||||
|
position: relative; animation: modal-in .2s ease;
|
||||||
|
}
|
||||||
|
@keyframes modal-in { from { opacity: 0; transform: scale(.96) translateY(8px); } to { opacity: 1; transform: none; } }
|
||||||
|
.modal-x {
|
||||||
|
position: absolute; top: 14px; right: 14px;
|
||||||
|
background: none; border: none; cursor: pointer; color: var(--dim2);
|
||||||
|
display: flex; transition: color .12s;
|
||||||
|
}
|
||||||
|
.modal-x:hover { color: var(--text); }
|
||||||
|
.modal-title { font-size: 18px; font-weight: 600; margin-bottom: 6px; }
|
||||||
|
.modal-sub { font-size: 13px; color: var(--dim2); margin-bottom: 20px; }
|
||||||
|
.google-btn {
|
||||||
|
display: flex; align-items: center; gap: 10px; width: 100%;
|
||||||
|
background: var(--bg3); border: 1px solid var(--border2);
|
||||||
|
color: var(--text); font-family: var(--font); font-size: 13px; font-weight: 500;
|
||||||
|
padding: 11px 16px; border-radius: 6px; cursor: pointer; margin-bottom: 16px;
|
||||||
|
transition: background .12s, border-color .12s;
|
||||||
|
}
|
||||||
|
.google-btn:hover { background: var(--bg4); border-color: var(--text); }
|
||||||
|
.modal-divider {
|
||||||
|
text-align: center; font-size: 12px; color: var(--dim2);
|
||||||
|
margin: 16px 0; position: relative;
|
||||||
|
}
|
||||||
|
.modal-divider::before {
|
||||||
|
content: ''; position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: var(--border);
|
||||||
|
}
|
||||||
|
.modal-divider span, .modal-divider { background: var(--bg2); padding: 0 10px; position: relative; }
|
||||||
|
.modal-input {
|
||||||
|
width: 100%; background: var(--bg3); border: 1px solid var(--border2);
|
||||||
|
color: var(--text); font-family: var(--font); font-size: 13px;
|
||||||
|
padding: 10px 12px; border-radius: 6px; outline: none;
|
||||||
|
transition: border-color .15s; margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.modal-input:focus { border-color: var(--text); }
|
||||||
|
.modal-row-end { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }
|
||||||
|
.btn-w {
|
||||||
|
background: var(--text); color: var(--bg);
|
||||||
|
border: none; border-radius: 6px; padding: 8px 18px;
|
||||||
|
font-family: var(--font); font-size: 13px; font-weight: 500; cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-ghost {
|
||||||
|
background: none; color: var(--dim); border: none;
|
||||||
|
font-family: var(--font); font-size: 13px; cursor: pointer;
|
||||||
|
}
|
||||||
|
.err-msg { color: var(--red); font-size: 12px; margin-top: 8px; }
|
||||||
|
|
||||||
|
/* ── USER MENU / CTX ── */
|
||||||
|
.umenu {
|
||||||
|
position: fixed; background: var(--bg2); border: 1px solid var(--border2);
|
||||||
|
border-radius: 12px; padding: 0; min-width: 220px; overflow: hidden;
|
||||||
|
z-index: 700; display: none; box-shadow: 0 12px 32px rgba(0,0,0,0.6);
|
||||||
|
animation: panel-up .15s ease;
|
||||||
|
}
|
||||||
|
#umenu button:hover { background: var(--bg3) !important; }
|
||||||
|
.umenu.open { display: block; }
|
||||||
|
.umenu-item {
|
||||||
|
padding: 8px 12px; font-size: 13px; cursor: pointer; border-radius: 5px;
|
||||||
|
color: var(--dim); transition: background .1s, color .1s;
|
||||||
|
}
|
||||||
|
.umenu-item:hover { background: var(--bg3); color: var(--text); }
|
||||||
|
.umenu-item.danger { color: var(--red); }
|
||||||
|
.umenu-item.danger:hover { background: rgba(232,68,102,0.1); }
|
||||||
|
.ctx-sep { height: 1px; background: var(--border); margin: 4px 0; }
|
||||||
|
.ctx-menu {
|
||||||
|
position: fixed; background: var(--bg2); border: 1px solid var(--border2);
|
||||||
|
border-radius: 8px; padding: 4px; min-width: 160px;
|
||||||
|
z-index: 700; box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||||
|
animation: panel-up .12s ease;
|
||||||
|
}
|
||||||
|
.ctx-item {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 8px 12px; font-size: 12px; cursor: pointer; border-radius: 5px;
|
||||||
|
color: var(--dim); transition: background .1s, color .1s;
|
||||||
|
}
|
||||||
|
.ctx-item:hover { background: var(--bg3); color: var(--text); }
|
||||||
|
|
||||||
|
/* ── ONBOARDING ── */
|
||||||
|
.onboarding {
|
||||||
|
position: fixed; inset: 0; z-index: 800;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.onboarding.hidden { display: none; }
|
||||||
|
.ob-bg {
|
||||||
|
position: absolute; inset: 0; background: rgba(0,0,0,0.92);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
.ob-panel {
|
||||||
|
position: relative; z-index: 1;
|
||||||
|
background: var(--bg2); border: 1px solid var(--border2);
|
||||||
|
border-radius: 16px; padding: 40px 36px; width: 420px;
|
||||||
|
text-align: center;
|
||||||
|
animation: modal-in .3s ease;
|
||||||
|
}
|
||||||
|
.ob-step { display: none; flex-direction: column; align-items: center; gap: 12px; }
|
||||||
|
.ob-step.active { display: flex; }
|
||||||
|
.ob-logo-wrap { margin-bottom: 8px; }
|
||||||
|
.ob-title { font-size: 22px; font-weight: 700; letter-spacing: -.5px; }
|
||||||
|
.ob-sub { font-size: 13px; color: var(--dim2); }
|
||||||
|
.ob-desc { font-size: 13px; color: var(--dim); line-height: 1.7; max-width: 300px; }
|
||||||
|
.ob-next {
|
||||||
|
margin-top: 16px; background: var(--text); color: var(--bg);
|
||||||
|
border: none; border-radius: 8px; padding: 11px 28px;
|
||||||
|
font-family: var(--font); font-size: 14px; font-weight: 600; cursor: pointer;
|
||||||
|
transition: opacity .15s;
|
||||||
|
}
|
||||||
|
.ob-next:hover { opacity: .85; }
|
||||||
|
.ob-back {
|
||||||
|
background: none; border: none; color: var(--dim2);
|
||||||
|
font-family: var(--font); font-size: 13px; cursor: pointer;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.ob-plats { display: flex; gap: 20px; justify-content: center; }
|
||||||
|
.ob-plat { display: flex; flex-direction: column; align-items: center; gap: 8px; font-size: 12px; color: var(--dim2); }
|
||||||
|
.ob-features-list { display: flex; flex-direction: column; gap: 6px; width: 100%; margin-bottom: 4px; }
|
||||||
|
.ob-feat {
|
||||||
|
background: var(--bg3); border: 1px solid var(--border);
|
||||||
|
border-radius: 6px; padding: 9px 14px; font-size: 13px; color: var(--dim);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.ob-feat::before { content: '✓ '; color: var(--dim2); }
|
||||||
|
.ob-dots { display: flex; gap: 6px; margin-top: 20px; }
|
||||||
|
.ob-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--bg3); border: 1px solid var(--border2); transition: background .2s; }
|
||||||
|
.ob-dot.active { background: var(--text); border-color: var(--text); }
|
||||||
|
|
||||||
|
/* ── SPLASH ── */
|
||||||
|
#splash {
|
||||||
|
position: fixed; inset: 0; z-index: 900;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
background: var(--bg); transition: opacity .4s ease;
|
||||||
|
}
|
||||||
|
#splash.hidden { opacity: 0; pointer-events: none; }
|
||||||
|
.splash-inner { display: flex; flex-direction: column; align-items: center; gap: 14px; }
|
||||||
|
.splash-orb {
|
||||||
|
position: absolute; width: 300px; height: 300px; border-radius: 50%;
|
||||||
|
background: radial-gradient(ellipse, rgba(255,255,255,0.04), transparent 70%);
|
||||||
|
animation: orb-pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes orb-pulse { 0%,100%{transform:scale(1);opacity:.6;} 50%{transform:scale(1.15);opacity:1;} }
|
||||||
|
.splash-logo { position: relative; z-index: 1; animation: logo-in .8s ease both; }
|
||||||
|
@keyframes logo-in { from{opacity:0;transform:scale(.8);} to{opacity:1;transform:none;} }
|
||||||
|
.splash-name { font-size: 22px; font-weight: 600; letter-spacing: -.5px; animation: fade-in .8s .2s ease both; }
|
||||||
|
.splash-dot { opacity: .5; }
|
||||||
|
.splash-update-wrap { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 200px; animation: fade-in .6s .5s ease both; }
|
||||||
|
.splash-bar { width: 100%; height: 2px; background: var(--bg3); border-radius: 1px; overflow: hidden; }
|
||||||
|
.splash-fill { height: 100%; width: 0; background: var(--text); border-radius: 1px; transition: width .3s ease; }
|
||||||
|
.splash-label { font-size: 11px; color: var(--dim2); }
|
||||||
|
.splash-credit { font-size: 11px; color: var(--dim2); animation: fade-in .6s .8s ease both; }
|
||||||
|
@keyframes fade-in { from{opacity:0;transform:translateY(6px);} to{opacity:1;transform:none;} }
|
||||||
|
.splash-status-row { display: flex; align-items: center; justify-content: center; gap: 6px; }
|
||||||
|
.splash-status-dot {
|
||||||
|
width: 6px; height: 6px; border-radius: 50%; background: var(--dim2);
|
||||||
|
animation: dot-pulse 1.2s ease-in-out infinite; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
@keyframes dot-pulse { 0%,100%{opacity:.3;transform:scale(.8);} 50%{opacity:1;transform:scale(1.2);} }
|
||||||
|
.splash-status-dot.ok { animation: none; background: #4ade80; }
|
||||||
|
.splash-status-dot.err { animation: none; background: #e84466; }
|
||||||
|
.splash-update-ok { font-size: 11px; color: #4ade80; animation: fade-in .3s ease both; margin-top: 4px; }
|
||||||
|
.splash-update-err {
|
||||||
|
font-size: 11px; color: rgba(232,68,102,.85);
|
||||||
|
animation: fade-in .3s ease both; margin-top: 4px;
|
||||||
|
max-width: 220px; text-align: center; line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── UPDATE BAR ── */
|
||||||
|
#update-bar {
|
||||||
|
position: fixed; top: var(--titlebar-h); left: 0; right: 0;
|
||||||
|
background: var(--bg2); border-bottom: 1px solid var(--border);
|
||||||
|
display: flex; align-items: center; gap: 10px; padding: 8px 16px;
|
||||||
|
font-size: 12px; color: var(--dim); z-index: 490;
|
||||||
|
}
|
||||||
|
#update-bar span { flex: 1; }
|
||||||
|
#update-action-btn { background: var(--text); color: var(--bg); border: none; border-radius: 4px; padding: 4px 12px; font-size: 11px; font-family: var(--font); cursor: pointer; font-weight: 500; }
|
||||||
|
#update-dismiss { background: none; border: none; color: var(--dim2); cursor: pointer; font-size: 14px; }
|
||||||
|
|
||||||
|
/* ── MISC ── */
|
||||||
|
.load-over { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 800; }
|
||||||
|
.load-box { background: var(--bg2); border: 1px solid var(--border2); border-radius: 10px; padding: 28px 40px; text-align: center; }
|
||||||
|
.spinner { width: 24px; height: 24px; border: 2px solid var(--border2); border-top-color: var(--text); border-radius: 50%; animation: spin .8s linear infinite; margin: 0 auto 12px; }
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
.state-box { padding: 40px; text-align: center; color: var(--dim2); font-size: 13px; }
|
||||||
|
input[type=range] { -webkit-appearance: none; appearance: none; background: var(--bg3); border-radius: 2px; cursor: pointer; }
|
||||||
|
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: var(--text); cursor: pointer; }
|
||||||
|
button { font-family: var(--font); }
|
||||||
Loading…
Add table
Add a link
Reference in a new issue