feat: implement library.rs, types.ts, api.ts, and extract CSS

- library.rs: full video library management (1948 lines, 10 tests)
  folder scanning, progress tracking, playlists, subtitle integration,
  background duration scanning
- types.ts: all TypeScript interfaces for Tauri command responses
- api.ts: typed wrappers for all 26 Tauri invoke commands
- 6 CSS files extracted from Python HTML into src/styles/
This commit is contained in:
Your Name
2026-02-19 02:08:23 +02:00
parent 4e91fe679f
commit 9c8474d24f
11 changed files with 3462 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
pub mod ffmpeg; pub mod ffmpeg;
pub mod fonts; pub mod fonts;
pub mod library;
pub mod prefs; pub mod prefs;
pub mod recents; pub mod recents;
pub mod state; pub mod state;

1954
src-tauri/src/library.rs Normal file

File diff suppressed because it is too large Load Diff

92
src/api.ts Normal file
View File

@@ -0,0 +1,92 @@
import { invoke } from '@tauri-apps/api/core';
import type {
LibraryInfo,
OkResponse,
PrefsResponse,
RecentsResponse,
NoteResponse,
VideoMetaResponse,
SubtitleResponse,
AvailableSubsResponse,
EmbeddedSubsResponse,
} from './types';
export const api = {
selectFolder: () =>
invoke<LibraryInfo>('select_folder'),
openFolderPath: (folder: string) =>
invoke<LibraryInfo>('open_folder_path', { folder }),
getRecents: () =>
invoke<RecentsResponse>('get_recents'),
removeRecent: (path: string) =>
invoke<OkResponse>('remove_recent', { path }),
getLibrary: () =>
invoke<LibraryInfo>('get_library'),
setCurrent: (index: number, timecode: number = 0) =>
invoke<OkResponse>('set_current', { index, timecode }),
tickProgress: (index: number, currentTime: number, duration: number | null, playing: boolean) =>
invoke<OkResponse>('tick_progress', { index, current_time: currentTime, duration, playing }),
setFolderVolume: (volume: number) =>
invoke<OkResponse>('set_folder_volume', { volume }),
setFolderAutoplay: (enabled: boolean) =>
invoke<OkResponse>('set_folder_autoplay', { enabled }),
setFolderRate: (rate: number) =>
invoke<OkResponse>('set_folder_rate', { rate }),
setOrder: (fids: string[]) =>
invoke<OkResponse>('set_order', { fids }),
startDurationScan: () =>
invoke<OkResponse>('start_duration_scan'),
getPrefs: () =>
invoke<PrefsResponse>('get_prefs'),
setPrefs: (patch: Record<string, unknown>) =>
invoke<OkResponse>('set_prefs', { patch }),
setAlwaysOnTop: (enabled: boolean) =>
invoke<OkResponse>('set_always_on_top', { enabled }),
saveWindowState: () =>
invoke<OkResponse>('save_window_state'),
getNote: (fid: string) =>
invoke<NoteResponse>('get_note', { fid }),
setNote: (fid: string, note: string) =>
invoke<OkResponse>('set_note', { fid, note }),
getCurrentVideoMeta: () =>
invoke<VideoMetaResponse>('get_current_video_meta'),
getCurrentSubtitle: () =>
invoke<SubtitleResponse>('get_current_subtitle'),
getEmbeddedSubtitles: () =>
invoke<EmbeddedSubsResponse>('get_embedded_subtitles'),
extractEmbeddedSubtitle: (trackIndex: number) =>
invoke<SubtitleResponse>('extract_embedded_subtitle', { track_index: trackIndex }),
getAvailableSubtitles: () =>
invoke<AvailableSubsResponse>('get_available_subtitles'),
loadSidecarSubtitle: (filePath: string) =>
invoke<SubtitleResponse>('load_sidecar_subtitle', { file_path: filePath }),
chooseSubtitleFile: () =>
invoke<SubtitleResponse>('choose_subtitle_file'),
resetWatchProgress: () =>
invoke<OkResponse>('reset_watch_progress'),
};

View File

@@ -1 +1,8 @@
console.log("TutorialDock frontend loaded"); import './styles/main.css';
import './styles/player.css';
import './styles/playlist.css';
import './styles/panels.css';
import './styles/components.css';
import './styles/animations.css';
console.log('TutorialDock frontend loaded');

View File

@@ -0,0 +1 @@
/* No standalone animations — included in main.css */

182
src/styles/components.css Normal file
View File

@@ -0,0 +1,182 @@
/* Notification toast */
#toast{
position:fixed;
left:28px;
bottom:28px;
z-index:999999;
transform:translateY(20px) scale(var(--zoom));
transform-origin:bottom left;
pointer-events:none;
opacity:0;
transition:opacity .25s ease, transform .25s cubic-bezier(.4,0,.2,1);
}
#toast.show{
opacity:1;
transform:translateY(0) scale(var(--zoom));
}
.toastInner{
pointer-events:none;
display:flex; align-items:center; gap:12px;
padding:12px 14px;
border-radius:7px;
border:1px solid rgba(255,255,255,.14);
background:rgba(18,20,26,.92);
box-shadow:0 26px 70px rgba(0,0,0,.70);
backdrop-filter:blur(16px);
}
.toastIcon{width:18px; height:18px; display:flex; align-items:center; justify-content:center;}
.toastIcon .fa{font-size:14px; color:rgba(230,236,252,.82)!important; opacity:.95;}
.toastMsg{
font-size:12.8px;
font-weight:760;
letter-spacing:.12px;
color:rgba(246,248,255,.92);
}
/* Toolbar icon buttons */
.toolbarIcon{
width:36px; height:36px;
border-radius:var(--r2);
background:linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.025));
border:1px solid rgba(255,255,255,.1);
color:rgba(246,248,255,.88);
font-size:14px;
transition:all .2s cubic-bezier(.4,0,.2,1);
position:relative;
overflow:hidden;
box-shadow:
0 2px 4px rgba(0,0,0,.12),
0 4px 12px rgba(0,0,0,.15),
inset 0 1px 0 rgba(255,255,255,.08);
}
.toolbarIcon::before{
content:"";
position:absolute;
inset:0;
background:linear-gradient(180deg, rgba(255,255,255,.1), transparent 50%);
opacity:0;
transition:opacity .2s ease;
}
.toolbarIcon:hover{
background:linear-gradient(180deg, rgba(255,255,255,.12), rgba(255,255,255,.05));
border-color:rgba(255,255,255,.18);
color:rgba(255,255,255,.98);
transform:translateY(-2px);
box-shadow:
0 4px 8px rgba(0,0,0,.15),
0 8px 20px rgba(0,0,0,.2),
inset 0 1px 0 rgba(255,255,255,.12);
}
.toolbarIcon:hover::before{opacity:1;}
.toolbarIcon:active{
transform:translateY(0);
box-shadow:0 2px 6px rgba(0,0,0,.18);
}
/* Fancy tooltip */
.tooltip{
position:fixed;
pointer-events:none;
z-index:99999;
border-radius:var(--r);
padding:16px 20px;
opacity:0;
transform:translateY(8px) scale(.97);
transition:opacity .25s ease, transform .25s cubic-bezier(.4,0,.2,1), left .12s ease, top .12s ease;
max-width:320px;
font-family:var(--sans);
overflow:hidden;
background:rgba(20,24,32,.5);
backdrop-filter:blur(8px) saturate(1.3);
-webkit-backdrop-filter:blur(8px) saturate(1.3);
border:1px solid rgba(255,255,255,.12);
box-shadow:
0 0 0 1px rgba(0,0,0,.3),
0 4px 8px rgba(0,0,0,.15),
0 12px 24px rgba(0,0,0,.25),
0 24px 48px rgba(0,0,0,.2);
}
.tooltip.visible{
opacity:1;
transform:translateY(0) scale(1);
}
.tooltip::before{
content:"";
position:absolute;
top:0; left:0; right:0;
height:1px;
background:linear-gradient(90deg, transparent 5%, rgba(95,175,255,.5) 30%, rgba(75,200,130,.4) 70%, transparent 95%);
border-radius:var(--r) var(--r) 0 0;
}
.tooltip::after{
content:"";
position:absolute;
top:1px; left:1px; right:1px;
height:40%;
background:linear-gradient(180deg, rgba(255,255,255,.05), transparent);
border-radius:var(--r) var(--r) 0 0;
pointer-events:none;
}
.tooltip-title{
font-family:var(--brand);
font-weight:800;
font-size:14px;
margin-bottom:8px;
letter-spacing:-.01em;
background:linear-gradient(135deg, #fff 0%, rgba(180,210,255,1) 50%, rgba(150,230,200,1) 100%);
-webkit-background-clip:text;
background-clip:text;
color:transparent;
text-shadow:none;
position:relative;
z-index:1;
}
.tooltip-desc{
font-family:var(--sans);
font-size:12px;
font-weight:500;
color:rgba(190,200,225,.88);
line-height:1.55;
letter-spacing:.01em;
position:relative;
z-index:1;
}
.tooltip-desc:empty{display:none;}
.tooltip-desc:empty ~ .tooltip-title{margin-bottom:0;}
.subsBox{position:relative;}
.subsMenu{
position:absolute; left:50%; bottom:calc(100% + 10px);
transform:translateX(-50%);
min-width:220px; padding:8px;
border-radius:7px; border:1px solid rgba(255,255,255,.12);
background:rgba(18,20,26,.94);
box-shadow:0 26px 70px rgba(0,0,0,.70);
backdrop-filter:blur(16px);
display:none; z-index:30;
}
.subsMenu.show{display:block;}
.subsMenuHeader{padding:6px 12px 4px; font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.08em; color:rgba(150,160,190,.6);}
.subsMenuItem{padding:10px 12px; border-radius:5px; cursor:pointer; user-select:none; font-size:12px; font-weight:600; color:rgba(246,248,255,.92); letter-spacing:.08px; display:flex; align-items:center; gap:10px; transition:background .12s ease;}
.subsMenuItem:hover{background:rgba(255,255,255,.06);}
.subsMenuItem .fa{font-size:13px; color:var(--iconStrong)!important; opacity:.85; width:18px; text-align:center;}
.subsMenuItem.embedded{color:rgba(180,220,255,.95);}
.subsDivider{height:1px; background:rgba(255,255,255,.08); margin:6px 4px;}
.subsEmpty{padding:10px 12px; color:rgba(165,172,196,.7); font-size:11.5px; text-align:center;}
.speedMenu{
position:absolute; right:0; bottom:calc(100% + 10px);
min-width:180px; padding:8px;
border-radius:7px; border:1px solid rgba(255,255,255,.12);
background:rgba(18,20,26,.94);
box-shadow:0 26px 70px rgba(0,0,0,.70);
backdrop-filter:blur(16px);
display:none; z-index:30;
transform:translateY(8px) scale(.96); opacity:0;
transition:transform .18s cubic-bezier(.175,.885,.32,1.275), opacity .15s ease;
}
.speedMenu.show{display:block; transform:translateY(0) scale(1); opacity:1;}
.speedItem{padding:10px 10px; border-radius:6px; cursor:pointer; user-select:none; font-family:var(--mono); font-size:14px; color:rgba(246,248,255,.92); letter-spacing:.10px; display:flex; align-items:center; justify-content:space-between; gap:10px; transition:all .12s ease;}
.speedItem:hover{background:rgba(255,255,255,.06); transform:translateX(3px);}
.speedItem .dot{width:8px; height:8px; border-radius:999px; background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.12); flex:0 0 auto; transition:all .15s ease;}
.speedItem.active .dot{background:radial-gradient(circle at 30% 30%, rgba(255,255,255,.92), rgba(100,180,255,.55)); box-shadow:0 0 0 3px rgba(100,180,255,.10); border-color:rgba(100,180,255,.24);}

514
src/styles/main.css Normal file
View File

@@ -0,0 +1,514 @@
:root{
--zoom:1;
/* Base backgrounds */
--bg0:#060709; --bg1:#0a0c10;
/* Strokes - consistent opacity scale */
--stroke:rgba(255,255,255,.07);
--strokeLight:rgba(255,255,255,.04);
--strokeMed:rgba(255,255,255,.10);
/* Text - consistent hierarchy */
--text:rgba(240,244,255,.91);
--textMuted:rgba(155,165,190,.68);
--textDim:rgba(120,132,165,.50);
/* Surfaces */
--surface:rgba(255,255,255,.025);
--surfaceHover:rgba(255,255,255,.045);
--surfaceActive:rgba(255,255,255,.06);
/* Shadows */
--shadow:0 16px 48px rgba(0,0,0,.50);
--shadow2:0 8px 24px rgba(0,0,0,.32);
/* Radii */
--r:6px; --r2:5px;
/* Fonts */
--mono:"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--sans:"Manrope", ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
--brand:"Sora","Manrope", ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
/* Icons */
--icon:rgba(175,185,210,.65);
--iconStrong:rgba(220,228,248,.85);
/* Accent - consistent blue */
--accent:rgb(95,175,255);
--accentGlow:rgba(95,175,255,.12);
--accentBorder:rgba(95,175,255,.22);
--accentBg:rgba(95,175,255,.07);
/* Success - consistent green */
--success:rgb(75,200,130);
--successBg:rgba(75,200,130,.07);
--successBorder:rgba(75,200,130,.20);
/* Tree */
--tree:rgba(195,205,230,.10);
--treeNode:rgba(200,212,238,.52);
}
*{box-sizing:border-box;}
html,body{height:100%;}
body{
margin:0; padding:0; font-family:var(--sans); color:var(--text); overflow:hidden;
width:100vw; height:100vh;
background:
radial-gradient(800px 500px at 10% 5%, rgba(95,175,255,.08), transparent 60%),
radial-gradient(700px 500px at 90% 8%, rgba(75,200,130,.05), transparent 65%),
linear-gradient(180deg, var(--bg1), var(--bg0));
letter-spacing:.04px;
}
#zoomRoot{
transform:scale(var(--zoom));
transform-origin:0 0;
width:calc(100vw / var(--zoom));
height:calc(100vh / var(--zoom));
overflow:hidden;
box-sizing:border-box;
}
.app{height:100%; display:flex; flex-direction:column; position:relative; overflow:hidden;}
.fa, i.fa-solid, i.fa-regular, i.fa-light, i.fa-thin{color:var(--icon)!important;}
.topbar{
display:flex; align-items:center; gap:14px;
padding:12px 14px;
height:72px;
flex:0 0 72px;
border-bottom:1px solid var(--stroke);
background:
linear-gradient(135deg, transparent 0%, transparent 48%, rgba(255,255,255,.015) 50%, transparent 52%, transparent 100%) 0 0 / 8px 8px,
linear-gradient(180deg, rgba(22,26,38,1), rgba(18,22,32,1));
min-width:0; z-index:5; position:relative;
box-sizing:border-box;
}
.topbar::before{
content:"";
position:absolute;
inset:0;
background:
radial-gradient(circle, rgba(180,210,255,.096) 2px, transparent 2.5px) 0 0 / 12px 12px,
radial-gradient(circle, rgba(220,230,255,.08) 2px, transparent 2.5px) 6px 6px / 12px 12px;
pointer-events:none;
z-index:1;
-webkit-mask-image: linear-gradient(90deg, black 0%, rgba(0,0,0,.5) 20%, transparent 40%);
mask-image: linear-gradient(90deg, black 0%, rgba(0,0,0,.5) 20%, transparent 40%);
}
.topbar::after{
content:"";
position:absolute;
inset:0;
background:linear-gradient(180deg, rgba(255,255,255,.03), transparent);
pointer-events:none;
z-index:0;
}
.brand{display:flex; align-items:center; gap:12px; min-width:0; flex:1 1 auto; position:relative; z-index:1;}
.appIcon{
display:flex; align-items:center; justify-content:center;
flex:0 0 auto;
filter: drop-shadow(0 8px 16px rgba(0,0,0,.35));
overflow:visible;
transition: transform .4s cubic-bezier(.34,1.56,.64,1), filter .3s ease;
cursor:pointer;
position:relative;
}
.appIconGlow{
position:absolute;
inset:-15px;
border-radius:50%;
pointer-events:none;
opacity:0;
transition:opacity .4s ease;
}
.appIconGlow::before{
content:"";
position:absolute;
inset:0;
border-radius:50%;
background:radial-gradient(circle, rgba(100,180,255,.25), rgba(130,230,180,.15), transparent 70%);
transform:scale(.5);
transition:transform .4s ease;
}
.appIconGlow::after{
content:"";
position:absolute;
inset:0;
border-radius:50%;
background:conic-gradient(from 0deg, rgba(100,180,255,.5), rgba(130,230,180,.5), rgba(210,160,255,.5), rgba(100,180,255,.5));
mask:radial-gradient(circle, transparent 45%, black 47%, black 53%, transparent 55%);
-webkit-mask:radial-gradient(circle, transparent 45%, black 47%, black 53%, transparent 55%);
animation:none;
}
@keyframes logoSpin{
0%{transform:rotate(0deg);}
100%{transform:rotate(360deg);}
}
@keyframes logoWiggle{
0%,100%{transform:rotate(0deg) scale(1);}
10%{transform:rotate(-12deg) scale(1.15);}
20%{transform:rotate(10deg) scale(1.12);}
30%{transform:rotate(-8deg) scale(1.18);}
40%{transform:rotate(6deg) scale(1.14);}
50%{transform:rotate(-4deg) scale(1.2);}
60%{transform:rotate(3deg) scale(1.16);}
70%{transform:rotate(-2deg) scale(1.12);}
80%{transform:rotate(1deg) scale(1.08);}
90%{transform:rotate(0deg) scale(1.04);}
}
.appIcon:hover{
animation:logoWiggle .8s ease-out;
filter: drop-shadow(0 0 20px rgba(100,180,255,.5)) drop-shadow(0 0 40px rgba(130,230,180,.3)) drop-shadow(0 12px 24px rgba(0,0,0,.4));
}
.appIcon:hover .appIconGlow{
opacity:1;
}
.appIcon:hover .appIconGlow::before{
transform:scale(1.2);
}
.appIcon:hover .appIconGlow::after{
animation:logoSpin 3s linear infinite;
}
.appIcon i{
font-size:36px;
line-height:1;
background:linear-gradient(135deg, rgba(100,180,255,.98), rgba(130,230,180,.92), rgba(210,160,255,.82));
-webkit-background-clip:text;
background-clip:text;
color:transparent!important;
-webkit-text-stroke: 0.5px rgba(0,0,0,.16);
opacity:.98;
transition:all .3s ease;
position:relative;
z-index:2;
}
.appIcon:hover i{
background:linear-gradient(135deg, rgba(130,210,255,1), rgba(160,250,200,1), rgba(230,180,255,1));
-webkit-background-clip:text;
background-clip:text;
}
.brandText{min-width:0; position:relative; z-index:1;}
.appName{
font-family:var(--brand);
font-weight:900;
font-size:18px;
line-height:1.02;
letter-spacing:.35px;
margin:0; padding:0;
transition:text-shadow .3s ease;
}
.brand:hover .appName{
text-shadow:0 0 20px rgba(100,180,255,.4), 0 0 40px rgba(130,230,180,.2);
}
.tagline{
margin-top:5px;
font-size:11.5px;
line-height:1.2;
color:rgba(180,188,210,.76);
letter-spacing:.18px;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
max-width:52vw;
transition:color .3s ease;
}
.brand:hover .tagline{
color:rgba(200,210,230,.9);
}
.actions{
display:flex; align-items:center; gap:8px;
flex:0 0 auto; flex-wrap:nowrap; white-space:nowrap;
position:relative; z-index:7;
}
.actionGroup{
display:flex; align-items:center; gap:6px;
}
.actionDivider{
width:1px; height:28px;
background:linear-gradient(180deg, transparent, rgba(255,255,255,.12) 20%, rgba(255,255,255,.12) 80%, transparent);
margin:0 4px;
}
/* Zoom control */
.zoomControl{
display:flex; align-items:center; gap:0;
background:linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.015));
border:1px solid rgba(255,255,255,.08);
border-radius:8px;
padding:2px;
box-shadow:0 2px 8px rgba(0,0,0,.15), inset 0 1px 0 rgba(255,255,255,.05);
}
.zoomBtn{
width:28px; height:28px;
border:none; background:transparent;
border-radius:6px;
color:var(--text);
cursor:pointer;
display:flex; align-items:center; justify-content:center;
transition:all .15s ease;
}
.zoomBtn:hover{
background:rgba(255,255,255,.08);
}
.zoomBtn:active{
background:rgba(255,255,255,.12);
transform:scale(.95);
}
.zoomBtn .fa{font-size:10px; opacity:.8;}
.zoomValue{
min-width:48px;
text-align:center;
font-family:var(--mono);
font-size:11px;
font-weight:600;
color:var(--text);
opacity:.9;
cursor:pointer;
padding:4px 6px;
border-radius:4px;
transition:background .15s ease;
}
.zoomValue:hover{
background:rgba(255,255,255,.06);
}
/* Toolbar buttons */
.toolbarBtn{
width:34px; height:34px;
border:1px solid rgba(255,255,255,.08);
border-radius:8px;
background:linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.02));
color:var(--text);
cursor:pointer;
display:flex; align-items:center; justify-content:center;
transition:all .2s cubic-bezier(.4,0,.2,1);
position:relative;
overflow:hidden;
box-shadow:0 2px 6px rgba(0,0,0,.12), inset 0 1px 0 rgba(255,255,255,.06);
}
.toolbarBtn::before{
content:"";
position:absolute;
inset:0;
background:radial-gradient(circle at 50% 0%, rgba(255,255,255,.15), transparent 70%);
opacity:0;
transition:opacity .2s ease;
}
.toolbarBtn:hover{
border-color:rgba(255,255,255,.15);
background:linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03));
transform:translateY(-1px);
box-shadow:0 4px 12px rgba(0,0,0,.2), inset 0 1px 0 rgba(255,255,255,.1);
}
.toolbarBtn:hover::before{opacity:1;}
.toolbarBtn:active{
transform:translateY(0);
box-shadow:0 1px 4px rgba(0,0,0,.15);
}
.toolbarBtn .fa{font-size:13px; opacity:.85; transition:transform .2s ease;}
.toolbarBtn:hover .fa{transform:scale(1.1);}
/* Primary split button styling */
.splitBtn.primary{
border-color:rgba(95,175,255,.25);
background:linear-gradient(180deg, rgba(95,175,255,.12), rgba(95,175,255,.04));
box-shadow:0 2px 8px rgba(0,0,0,.15), 0 4px 20px rgba(95,175,255,.1), inset 0 1px 0 rgba(255,255,255,.1);
}
.splitBtn.primary:hover{
border-color:rgba(95,175,255,.4);
box-shadow:0 4px 12px rgba(0,0,0,.2), 0 8px 32px rgba(95,175,255,.15), inset 0 1px 0 rgba(255,255,255,.15);
}
.splitBtn.primary .drop{
border-left-color:rgba(95,175,255,.2);
}
.btn{
display:inline-flex; align-items:center; justify-content:center; gap:8px;
padding:9px 14px;
border-radius:var(--r2);
border:1px solid rgba(255,255,255,.08);
background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02));
color:var(--text);
cursor:pointer; user-select:none;
box-shadow:
0 2px 4px rgba(0,0,0,.2),
0 8px 24px rgba(0,0,0,.25),
inset 0 1px 0 rgba(255,255,255,.08),
inset 0 -1px 0 rgba(0,0,0,.1);
transition:all .2s cubic-bezier(.4,0,.2,1);
font-size:12.5px; font-weight:700; letter-spacing:.02em;
position:relative;
overflow:hidden;
}
.btn::before{
content:"";
position:absolute;
inset:0;
background:linear-gradient(180deg, rgba(255,255,255,.1), transparent 50%);
opacity:0;
transition:opacity .2s ease;
}
.btn:hover{
border-color:rgba(255,255,255,.15);
background:linear-gradient(180deg, rgba(255,255,255,.09), rgba(255,255,255,.04));
transform:translateY(-2px);
box-shadow:
0 4px 8px rgba(0,0,0,.2),
0 12px 32px rgba(0,0,0,.3),
inset 0 1px 0 rgba(255,255,255,.12),
inset 0 -1px 0 rgba(0,0,0,.1);
}
.btn:hover::before{opacity:1;}
.btn:active{
transform:translateY(0);
box-shadow:
0 1px 2px rgba(0,0,0,.2),
0 4px 12px rgba(0,0,0,.2),
inset 0 1px 0 rgba(255,255,255,.06);
}
.btn.primary{
border-color:rgba(95,175,255,.3);
background:linear-gradient(180deg, rgba(95,175,255,.2), rgba(95,175,255,.08));
color:#fff;
text-shadow:0 1px 2px rgba(0,0,0,.3);
}
.btn.primary::before{
background:linear-gradient(180deg, rgba(255,255,255,.15), transparent 60%);
}
.btn.primary:hover{
border-color:rgba(95,175,255,.45);
background:linear-gradient(180deg, rgba(95,175,255,.28), rgba(95,175,255,.12));
box-shadow:
0 4px 8px rgba(0,0,0,.2),
0 12px 32px rgba(95,175,255,.2),
0 0 0 1px rgba(95,175,255,.1),
inset 0 1px 0 rgba(255,255,255,.15);
}
.btn .fa{font-size:14px; opacity:.95; color:var(--iconStrong)!important; transition:transform .2s ease; position:relative; z-index:1;}
.btn.primary .fa{color:#fff!important;}
.btn:hover .fa{transform:scale(1.1);}
.splitBtn{
display:inline-flex;
border-radius:var(--r2);
overflow:hidden;
border:1px solid rgba(255,255,255,.08);
box-shadow:
0 2px 4px rgba(0,0,0,.2),
0 8px 24px rgba(0,0,0,.25),
inset 0 1px 0 rgba(255,255,255,.06);
background:linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.02));
position:relative; z-index:8;
transition:all .2s ease;
}
.splitBtn:hover{
border-color:rgba(255,255,255,.14);
box-shadow:
0 4px 8px rgba(0,0,0,.2),
0 12px 32px rgba(0,0,0,.3),
inset 0 1px 0 rgba(255,255,255,.08);
transform:translateY(-1px);
}
.splitBtn .btn{border:none; box-shadow:none; border-radius:0; background:transparent; padding:9px 12px; transform:none;}
.splitBtn .btn::before{display:none;}
.splitBtn .btn:hover{background:rgba(255,255,255,.06); transform:none; box-shadow:none;}
.splitBtn .drop{
width:40px; padding:8px 0;
border-left:1px solid rgba(255,255,255,.08);
display:flex; align-items:center; justify-content:center;
transition:background .15s ease;
background:linear-gradient(180deg, rgba(255,255,255,.02), transparent);
}
.splitBtn .drop:hover{background:rgba(255,255,255,.06);}
.splitBtn .drop .fa{font-size:16px; opacity:.88; color:var(--iconStrong)!important; transition:transform .2s ease;}
.splitBtn .drop:hover .fa{transform:translateY(2px);}
.dropdownPortal{
position:fixed; z-index:99999;
min-width:320px; max-width:560px; max-height:360px;
overflow:auto;
border-radius:7px;
border:1px solid rgba(255,255,255,.12);
background:rgba(18,20,26,.94);
box-shadow:0 26px 70px rgba(0,0,0,.70);
backdrop-filter:blur(16px);
padding:6px;
display:none;
transform:scale(var(--zoom));
transform-origin:top left;
scrollbar-width:thin;
scrollbar-color:rgba(255,255,255,.14) rgba(255,255,255,.02);
}
.dropdownPortal::-webkit-scrollbar{width:4px; height:4px;}
.dropdownPortal::-webkit-scrollbar-track{background:rgba(255,255,255,.015);}
.dropdownPortal::-webkit-scrollbar-thumb{background:rgba(255,255,255,.11); border-radius:999px;}
.dropdownPortal::-webkit-scrollbar-button{width:0; height:0; display:none;}
.dropItem{display:flex; align-items:center; gap:10px; padding:10px 10px; border-radius:6px; cursor:pointer; user-select:none; color:rgba(246,248,255,.92); font-weight:760; font-size:12.7px; letter-spacing:.12px; line-height:1.25; transition:all .15s ease; position:relative;}
.dropItem:hover{background:rgba(255,255,255,.06); padding-right:36px;}
.dropItem:active{transform:none;}
.dropIcon{width:18px; height:18px; display:flex; align-items:center; justify-content:center; flex:0 0 auto; opacity:.9; transition:transform .2s ease;}
.dropItem:hover .dropIcon{transform:scale(1.1);}
.dropIcon .fa{font-size:14px; color:var(--iconStrong)!important;}
.dropName{white-space:nowrap; flex:1 1 auto; min-width:0; transition:mask-image .15s ease, -webkit-mask-image .15s ease;}
.dropItem:hover .dropName{overflow:hidden; mask-image:linear-gradient(90deg, #000 80%, transparent 100%); -webkit-mask-image:linear-gradient(90deg, #000 80%, transparent 100%);}
.dropRemove{position:absolute; right:8px; top:50%; transform:translateY(-50%); width:24px; height:24px; border-radius:8px; background:rgba(255,100,100,.15); border:1px solid rgba(255,100,100,.25); color:rgba(255,180,180,.9); display:none; align-items:center; justify-content:center; font-size:12px; cursor:pointer; transition:all .15s ease;}
.dropItem:hover .dropRemove{display:flex;}
.dropRemove:hover{background:rgba(255,100,100,.25); border-color:rgba(255,100,100,.4); color:rgba(255,220,220,1);}
.dropEmpty{padding:10px 10px; color:rgba(165,172,196,.78); font-size:12.5px;}
.seg{display:inline-flex; border:1px solid rgba(255,255,255,.09); border-radius:var(--r2); overflow:hidden; background:rgba(255,255,255,.02); box-shadow:var(--shadow2); transition:border-color .15s ease;}
.seg:hover{border-color:rgba(255,255,255,.14);}
.seg .btn{border:none; box-shadow:none; border-radius:0; padding:8px 9px; background:transparent; font-weight:820; transform:none;}
.seg .btn:hover{background:rgba(255,255,255,.06); transform:none; box-shadow:none;}
.seg .btn:active{background:rgba(255,255,255,.08);}
.seg .mid{border-left:1px solid rgba(255,255,255,.10); border-right:1px solid rgba(255,255,255,.10); min-width:62px; font-variant-numeric:tabular-nums;}
.switch{
display:inline-flex; align-items:center; justify-content:center; gap:8px;
padding:6px 10px;
border-radius:8px;
border:1px solid rgba(255,255,255,.08);
background:linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.015));
box-shadow:0 2px 6px rgba(0,0,0,.12), inset 0 1px 0 rgba(255,255,255,.05);
cursor:pointer; user-select:none;
font-size:11.5px; font-weight:650; letter-spacing:.01em;
color:var(--text);
line-height:1;
transition:all .2s ease;
}
.switch:hover{
border-color:rgba(255,255,255,.14);
background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.025));
box-shadow:0 4px 10px rgba(0,0,0,.18), inset 0 1px 0 rgba(255,255,255,.08);
}
.switch input{display:none;}
.track{
width:32px; height:18px;
border-radius:999px;
background:rgba(255,255,255,.06);
border:1px solid rgba(255,255,255,.10);
position:relative;
transition:all .25s cubic-bezier(.4,0,.2,1);
flex:0 0 auto;
display:flex; align-items:center;
}
.knob{
margin-left:2px;
width:14px; height:14px;
border-radius:999px;
background:linear-gradient(180deg, rgba(255,255,255,.35), rgba(255,255,255,.18));
box-shadow:0 2px 6px rgba(0,0,0,.25);
transition:transform .2s cubic-bezier(.4,0,.2,1), background .2s ease, box-shadow .2s ease;
}
.switch input:checked + .track{
background:linear-gradient(90deg, rgba(95,175,255,.25), rgba(130,200,255,.2));
border-color:rgba(95,175,255,.3);
box-shadow:0 0 12px rgba(95,175,255,.15);
}
.switch input:checked + .track .knob{
transform:translateX(14px);
background:linear-gradient(180deg, rgba(255,255,255,.9), rgba(200,230,255,.8));
box-shadow:0 2px 8px rgba(95,175,255,.3), 0 0 0 2px rgba(95,175,255,.15);
}
.content{flex:1 1 auto; min-height:0; padding:12px; display:grid; grid-template-columns:calc(62% - 7px) 14px calc(38% - 7px); gap:0; overflow:hidden;}
@media (max-width:1100px){
.content{grid-template-columns:1fr; gap:12px; padding:12px;}
.dividerWrap{display:none;}
.actions{flex-wrap:wrap;}
.seg{width:100%;}
.dock{grid-template-columns:1fr;}
.dockDividerWrap{display:none;}
.tagline{max-width:70vw;}
}

144
src/styles/panels.css Normal file
View File

@@ -0,0 +1,144 @@
.panel{border:1px solid var(--stroke); border-radius:var(--r); background:linear-gradient(180deg, var(--surface), rgba(255,255,255,.015)); box-shadow:0 8px 32px rgba(0,0,0,.35); overflow:hidden; min-height:0; display:flex; flex-direction:column; backdrop-filter:blur(12px);}
.panelHeader{padding:12px 12px 10px; border-bottom:1px solid var(--stroke); display:flex; align-items:flex-start; justify-content:space-between; gap:12px; flex:0 0 auto; min-width:0;}
.nowTitle{font-weight:860; font-size:13.4px; letter-spacing:.14px; max-width:60ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.nowSub{margin-top:4px; color:var(--textMuted); font-size:11.6px; font-family:var(--mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:80ch;}
.dividerWrap{display:flex; align-items:stretch; justify-content:center;}
.divider{width:14px; cursor:col-resize; position:relative; background:transparent; border:none;}
.divider::after{
content:""; position:absolute; top:50%; left:50%;
width:4px; height:54px; transform:translate(-50%,-50%);
border-radius:999px;
background:
radial-gradient(circle, rgba(255,255,255,.10) 35%, transparent 40%) 0 0/4px 12px,
radial-gradient(circle, rgba(255,255,255,.10) 35%, transparent 40%) 0 6px/4px 12px;
opacity:.20; pointer-events:none; transition:opacity .15s ease;
}
.divider:hover::after{opacity:.52;}
.dock{flex:1 1 auto; min-height:0; border-top:1px solid rgba(255,255,255,.09); display:grid; grid-template-columns:62% 14px 38%; background:radial-gradient(900px 240px at 20% 0%, rgba(100,180,255,.06), transparent 60%), rgba(0,0,0,.10); overflow:hidden;}
.dockPane{min-height:0; display:flex; flex-direction:column; overflow:hidden;}
.dockInner{padding:12px; min-height:0; display:flex; flex-direction:column; gap:10px; height:100%;}
.dockHeader{padding:12px 14px 11px; border:1px solid rgba(255,255,255,.08); border-radius:7px; display:flex; align-items:center; justify-content:space-between; gap:10px; background:linear-gradient(180deg, rgba(255,255,255,.035), rgba(255,255,255,.012)); flex:0 0 auto; box-shadow:0 14px 36px rgba(0,0,0,.25);}
#notesHeader{border-bottom-right-radius:7px;}
#infoHeader{border-bottom-left-radius:7px; margin-right:12px;}
.dockTitle{font-family:var(--brand); font-weight:800; letter-spacing:.02px; font-size:13.5px; color:rgba(246,248,255,.95); display:flex; align-items:center; gap:10px;}
.dockTitle .fa{color:var(--iconStrong)!important; opacity:.88; font-size:14px;}
.notesArea{flex:1 1 auto; min-height:0; overflow:hidden; position:relative;}
.notesSaved{
position:absolute;
bottom:12px; right:12px;
padding:6px 10px;
border-radius:6px;
background:linear-gradient(135deg, rgba(100,200,130,.2), rgba(80,180,120,.15));
border:1px solid rgba(100,200,130,.25);
color:rgba(150,230,170,.95);
font-size:11px;
font-weight:600;
letter-spacing:.02em;
display:flex; align-items:center; gap:6px;
opacity:0;
transform:translateY(4px);
transition:opacity .4s ease, transform .4s ease;
pointer-events:none;
box-shadow:0 4px 12px rgba(0,0,0,.2);
}
.notesSaved.show{
opacity:1;
transform:translateY(0);
}
.notesSaved .fa{font-size:10px; color:rgba(130,220,160,.95)!important;}
.notes{width:100%; height:100%; resize:none; border-radius:6px; border:1px solid rgba(255,255,255,.10); background:radial-gradient(900px 280px at 18% 0%, rgba(100,180,255,.09), transparent 62%), rgba(255,255,255,.02); color:rgba(246,248,255,.92); padding:12px 12px; outline:none; font-family:var(--sans); font-size:12.9px; line-height:1.45; letter-spacing:.08px; box-shadow:0 18px 45px rgba(0,0,0,.40); overflow:auto; scrollbar-width:thin; scrollbar-color:rgba(255,255,255,.14) transparent;}
.notes::-webkit-scrollbar{width:2px; height:2px;}
.notes::-webkit-scrollbar-track{background:transparent;}
.notes::-webkit-scrollbar-thumb{background:rgba(255,255,255,.16); border-radius:0;}
.notes::-webkit-scrollbar-button{width:0; height:0; display:none;}
.notes::placeholder{color:rgba(165,172,196,.55);}
.infoGrid{
flex:1 1 auto;
min-height:0;
overflow:auto;
padding-right:12px;
padding-top:8px;
padding-bottom:8px;
scrollbar-width:none;
--fade-top:30px;
--fade-bottom:30px;
mask-image:linear-gradient(180deg, transparent 0%, #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent 100%);
-webkit-mask-image:linear-gradient(180deg, transparent 0%, #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent 100%);
transition:--fade-top 0.8s ease, --fade-bottom 0.8s ease;
}
@property --fade-top {
syntax: '<length>';
initial-value: 30px;
inherits: false;
}
@property --fade-bottom {
syntax: '<length>';
initial-value: 30px;
inherits: false;
}
.infoGrid.at-top{--fade-top:0px;}
.infoGrid.at-bottom{--fade-bottom:0px;}
.infoGrid::-webkit-scrollbar{width:0; height:0;}
.kv{
display:grid;
grid-template-columns:78px 1fr;
gap:4px 14px;
align-items:baseline;
padding:12px 14px;
border-radius:var(--r2);
border:1px solid var(--strokeLight);
background:linear-gradient(170deg, rgba(20,25,35,.65), rgba(14,18,26,.75));
box-shadow:var(--shadow2), inset 0 1px 0 rgba(255,255,255,.02);
margin-bottom:8px;
}
.k{
font-family:var(--sans);
font-size:9px;
font-weight:800;
text-transform:uppercase;
letter-spacing:.12em;
color:var(--textDim);
padding-top:3px;
white-space:nowrap;
}
.v{
font-family:var(--brand);
font-size:12.5px;
font-weight:650;
color:var(--text);
letter-spacing:-.01em;
word-break:break-word;
overflow-wrap:anywhere;
line-height:1.35;
padding-left:6px;
}
.v.mono{
font-family:var(--mono);
font-size:11px;
font-weight:500;
color:var(--textMuted);
letter-spacing:.01em;
font-variant-numeric:tabular-nums;
background:linear-gradient(90deg, var(--accentBg), transparent 80%);
padding:2px 6px;
border-radius:3px;
margin:-2px 0;
}
.dockDividerWrap{display:flex; align-items:stretch; justify-content:center;}
.dockDivider{width:14px; cursor:col-resize; position:relative; background:transparent; border:none;}
.dockDivider::after{
content:""; position:absolute; top:50%; left:50%;
width:4px; height:44px; transform:translate(-50%,-50%);
border-radius:999px;
background:
radial-gradient(circle, rgba(255,255,255,.10) 35%, transparent 40%) 0 0/4px 12px,
radial-gradient(circle, rgba(255,255,255,.10) 35%, transparent 40%) 0 6px/4px 12px;
opacity:.18; pointer-events:none; transition:opacity .15s ease;
}
.dockDivider:hover::after{opacity:.44;}

278
src/styles/player.css Normal file
View File

@@ -0,0 +1,278 @@
.videoWrap{position:relative; background:#000; flex:0 0 auto;}
video{width:100%; height:auto; display:block; background:#000; aspect-ratio:16/9; outline:none; cursor:pointer;}
video::cue{
background:transparent;
color:#fff;
font-family:var(--sans);
font-size:1.1em;
font-weight:600;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
-2px 0 0 #000,
2px 0 0 #000,
0 -2px 0 #000,
0 2px 0 #000;
}
.videoOverlay{
position:absolute;
inset:0;
display:flex;
align-items:center;
justify-content:center;
pointer-events:none;
z-index:5;
}
.overlayIcon{
position:relative;
width:100px;
height:100px;
display:flex;
align-items:center;
justify-content:center;
opacity:0;
transition:opacity 0.8s ease;
border-radius:50%;
background:rgba(20,25,35,.5);
backdrop-filter:blur(8px) saturate(1.3);
-webkit-backdrop-filter:blur(8px) saturate(1.3);
border:1.5px solid rgba(255,255,255,.15);
box-shadow:
0 8px 32px rgba(0,0,0,.5),
0 0 0 1px rgba(0,0,0,.3);
}
.overlayIcon.show{
opacity:1;
}
.overlayIcon.pulse{
animation:overlayPulse 0.4s ease-out;
}
@keyframes overlayPulse{
0%{transform:scale(1);}
50%{transform:scale(1.15);}
100%{transform:scale(1);}
}
.overlayIcon::before{
content:"";
position:absolute;
inset:0;
border-radius:50%;
background:
radial-gradient(circle at 30% 30%, rgba(255,255,255,.1), transparent 50%),
radial-gradient(circle at 70% 70%, rgba(95,175,255,.08), transparent 50%);
pointer-events:none;
}
.overlayIcon.show:hover{
border-color:rgba(255,255,255,.22);
box-shadow:
0 12px 40px rgba(0,0,0,.6),
0 0 0 1px rgba(0,0,0,.4),
0 0 40px rgba(95,175,255,.1);
}
.overlayIcon i{
font-size:36px;
color:rgba(255,255,255,.92)!important;
filter:drop-shadow(0 2px 10px rgba(0,0,0,.6));
position:relative;
z-index:2;
transition:transform 0.3s ease, color 0.3s ease;
margin-left:4px; /* center play icon visually */
}
.overlayIcon.pause i{
margin-left:0;
}
.overlayIcon.show:hover i{
transform:scale(1.1);
color:rgba(255,255,255,1)!important;
}
.controls{display:flex; flex-direction:column; gap:10px; padding:12px; border-top:1px solid var(--stroke); flex:0 0 auto; background:linear-gradient(180deg, rgba(0,0,0,.06), rgba(0,0,0,.0));}
.controlsRow{display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;}
.group{display:flex; align-items:center; gap:10px; flex-wrap:wrap;}
.iconBtn{
width:40px; height:36px;
border-radius:8px;
border:1px solid rgba(255,255,255,.08);
background:linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.02));
box-shadow:
0 2px 6px rgba(0,0,0,.12),
inset 0 1px 0 rgba(255,255,255,.06);
display:inline-flex; align-items:center; justify-content:center;
cursor:pointer; user-select:none;
transition:all .2s cubic-bezier(.4,0,.2,1);
position:relative;
overflow:hidden;
}
.iconBtn::before{
content:"";
position:absolute;
inset:0;
background:radial-gradient(circle at 50% 0%, rgba(255,255,255,.15), transparent 70%);
opacity:0;
transition:opacity .2s ease;
}
.iconBtn:hover{
border-color:rgba(255,255,255,.15);
background:linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03));
transform:translateY(-1px);
box-shadow:
0 4px 12px rgba(0,0,0,.2),
inset 0 1px 0 rgba(255,255,255,.1);
}
.iconBtn:hover::before{opacity:1;}
.iconBtn:active{
transform:translateY(0);
box-shadow:0 1px 4px rgba(0,0,0,.15);
}
.iconBtn.primary{
border-color:rgba(95,175,255,.25);
background:linear-gradient(180deg, rgba(95,175,255,.15), rgba(95,175,255,.05));
box-shadow:
0 2px 8px rgba(0,0,0,.15),
0 4px 16px rgba(95,175,255,.08),
inset 0 1px 0 rgba(255,255,255,.1);
}
.iconBtn.primary::before{
background:radial-gradient(circle at 50% 0%, rgba(255,255,255,.2), transparent 70%);
}
.iconBtn.primary:hover{
border-color:rgba(95,175,255,.4);
background:linear-gradient(180deg, rgba(95,175,255,.22), rgba(95,175,255,.08));
box-shadow:
0 4px 12px rgba(0,0,0,.2),
0 8px 24px rgba(95,175,255,.12),
inset 0 1px 0 rgba(255,255,255,.15);
}
.iconBtn .fa{font-size:15px; color:var(--iconStrong)!important; opacity:.9; transition:transform .2s ease; position:relative; z-index:1;}
.iconBtn:hover .fa{transform:scale(1.1);}
.timeChip{display:inline-flex; align-items:center; gap:10px; padding:8px 10px; border-radius:999px; border:1px solid var(--strokeMed); background:var(--surface); box-shadow:var(--shadow2); font-family:var(--mono); font-size:12px; color:var(--text); letter-spacing:.15px; font-variant-numeric:tabular-nums; transition:border-color .15s ease;}
.timeDot{width:8px; height:8px; border-radius:999px; background:radial-gradient(circle at 35% 35%, rgba(255,255,255,.90), rgba(130,230,180,.55)); box-shadow:0 0 0 3px rgba(130,230,180,.10); opacity:.95; transition:transform .3s ease; animation:pulse 2s ease-in-out infinite;}
@keyframes pulse{0%,100%{transform:scale(1);opacity:.95;} 50%{transform:scale(1.15);opacity:1;}}
.seekWrap{display:flex; align-items:center; gap:10px; width:100%; position:relative;}
.seekTrack{
position:absolute;
left:0; right:0; top:50%;
height:10px;
transform:translateY(-50%);
border-radius:999px;
background:rgba(255,255,255,.06);
border:1px solid rgba(255,255,255,.10);
box-shadow:0 8px 18px rgba(0,0,0,.28);
overflow:hidden;
pointer-events:none;
}
.seekFill{
height:100%;
width:0%;
background:linear-gradient(90deg, rgba(95,175,255,.7), rgba(130,200,255,.5) 60%, rgba(180,230,200,.4));
border-radius:999px 0 0 999px;
box-shadow:0 0 12px rgba(95,175,255,.3);
transition:width .1s linear;
}
.seek{-webkit-appearance:none; appearance:none; width:100%; height:18px; border-radius:999px; background:transparent; border:none; box-shadow:none; outline:none; position:relative; z-index:2; cursor:pointer; margin:0;}
.seek::-webkit-slider-runnable-track{background:transparent; height:18px;}
.seek::-webkit-slider-thumb{-webkit-appearance:none; appearance:none; width:18px; height:18px; border-radius:999px; border:2px solid rgba(255,255,255,.25); background:linear-gradient(180deg, rgba(255,255,255,.95), rgba(200,220,255,.8)); box-shadow:0 4px 12px rgba(0,0,0,.4), 0 0 0 4px rgba(95,175,255,.15); cursor:pointer; transition:transform .15s ease, box-shadow .15s ease; margin-top:0;}
.seek:hover::-webkit-slider-thumb{transform:scale(1.15); box-shadow:0 6px 16px rgba(0,0,0,.5), 0 0 0 6px rgba(95,175,255,.2);}
.seek:active::-webkit-slider-thumb{transform:scale(1.05); box-shadow:0 2px 8px rgba(0,0,0,.4), 0 0 0 8px rgba(95,175,255,.25);}
.seek::-moz-range-track{background:transparent; height:18px;}
.seek::-moz-range-thumb{width:18px; height:18px; border-radius:999px; border:2px solid rgba(255,255,255,.25); background:linear-gradient(180deg, rgba(255,255,255,.95), rgba(200,220,255,.8)); box-shadow:0 4px 12px rgba(0,0,0,.4); cursor:pointer;}
.miniCtl{display:flex; align-items:center; gap:10px; padding:8px 10px; border-radius:999px; border:1px solid var(--strokeMed); background:var(--surface); box-shadow:var(--shadow2); position:relative; transition:border-color .15s ease;}
.miniCtl:hover{border-color:rgba(255,255,255,.16);}
.miniCtl .fa{font-size:14px; color:var(--iconStrong)!important; opacity:.95; flex:0 0 auto;}
.volWrap{position:relative; width:120px; height:14px; display:flex; align-items:center;}
.volTrack{
position:absolute;
left:0; right:0; top:50%;
height:6px;
transform:translateY(-50%);
border-radius:999px;
background:rgba(255,255,255,.06);
border:1px solid rgba(255,255,255,.10);
overflow:hidden;
pointer-events:none;
}
.volFill{
height:100%;
width:100%;
background:linear-gradient(90deg, rgba(95,175,255,.6), rgba(130,200,255,.4));
border-radius:999px 0 0 999px;
box-shadow:0 0 8px rgba(95,175,255,.2);
transition:width .05s linear;
}
.vol{-webkit-appearance:none; appearance:none; width:100%; height:14px; border-radius:999px; background:transparent; border:none; outline:none; position:relative; z-index:2; cursor:pointer; margin:0;}
.vol::-webkit-slider-runnable-track{background:transparent; height:14px;}
.vol::-webkit-slider-thumb{-webkit-appearance:none; appearance:none; width:14px; height:14px; border-radius:999px; border:2px solid rgba(255,255,255,.25); background:linear-gradient(180deg, rgba(255,255,255,.95), rgba(200,220,255,.8)); box-shadow:0 2px 8px rgba(0,0,0,.35), 0 0 0 3px rgba(95,175,255,.12); cursor:pointer; transition:transform .15s ease, box-shadow .15s ease;}
.vol:hover::-webkit-slider-thumb{transform:scale(1.15); box-shadow:0 3px 10px rgba(0,0,0,.4), 0 0 0 4px rgba(95,175,255,.18);}
.vol::-moz-range-track{background:transparent; height:14px;}
.vol::-moz-range-thumb{width:14px; height:14px; border-radius:999px; border:2px solid rgba(255,255,255,.25); background:linear-gradient(180deg, rgba(255,255,255,.95), rgba(200,220,255,.8)); box-shadow:0 2px 8px rgba(0,0,0,.35); cursor:pointer;}
.volTooltip{
position:absolute;
bottom:calc(100% + 12px);
left:0;
padding:8px 12px;
border-radius:var(--r2);
background:
radial-gradient(ellipse 120% 100% at 20% 0%, rgba(95,175,255,.12), transparent 50%),
linear-gradient(175deg, rgba(28,34,48,.97), rgba(16,20,30,.98));
border:1px solid rgba(255,255,255,.12);
color:#fff;
font-family:var(--mono);
font-size:13px;
font-weight:600;
letter-spacing:.03em;
white-space:nowrap;
pointer-events:none;
opacity:0;
transform:translateX(-50%) translateY(4px);
transition:opacity .15s ease, transform .15s ease, left .05s linear;
box-shadow:
0 0 0 1px rgba(0,0,0,.3),
0 4px 8px rgba(0,0,0,.2),
0 12px 24px rgba(0,0,0,.25),
inset 0 1px 0 rgba(255,255,255,.08);
backdrop-filter:blur(16px);
z-index:100;
}
.volTooltip::before{
content:"";
position:absolute;
top:0; left:0; right:0;
height:1px;
background:linear-gradient(90deg, transparent, rgba(95,175,255,.4) 50%, transparent);
border-radius:var(--r2) var(--r2) 0 0;
}
.volTooltip::after{
content:"";
position:absolute;
top:100%;
left:50%;
transform:translateX(-50%);
border:6px solid transparent;
border-top-color:rgba(20,26,36,.95);
}
.volTooltip.show{
opacity:1;
transform:translateX(-50%) translateY(0);
}
.speedBox{display:flex; align-items:center; gap:10px; position:relative;}
.speedBtn{border:none; outline:none; background:transparent; color:rgba(246,248,255,.92); font-family:var(--mono); font-size:12px; letter-spacing:.10px; padding:0 2px; cursor:pointer; line-height:1; display:inline-flex; align-items:center; gap:8px; transition:color .15s ease;}
.speedBtn span:first-child{min-width:3.5ch; text-align:right;}
.speedBtn:hover{color:rgba(255,255,255,1);}
.speedCaret .fa{font-size:12px; opacity:.85; color:var(--icon)!important; transition:transform .2s ease;}
.speedBtn:hover .speedCaret .fa{transform:translateY(2px);}
.progressPill{flex:0 0 auto; display:flex; align-items:center; gap:10px; padding:8px 10px; border-radius:999px; border:1px solid var(--strokeMed); background:radial-gradient(400px 100px at 20% 0%, var(--accentGlow), transparent 60%), var(--surface); box-shadow:var(--shadow2); min-width:220px;}
.progressLabel{font-size:11.2px; font-weight:820; letter-spacing:.12px; text-transform:uppercase; color:rgba(190,198,220,.78); margin-right:2px;}
.progressBar{width:120px; height:8px; border-radius:999px; border:1px solid rgba(255,255,255,.09); background:rgba(255,255,255,.05); overflow:hidden;}
.progressBar>div{height:100%; width:0%; background:linear-gradient(90deg, rgba(100,180,255,.95), rgba(130,230,180,.88));}
.progressPct{font-family:var(--mono); font-size:11.6px; color:rgba(230,235,255,.92); font-variant-numeric:tabular-nums; letter-spacing:.10px; min-width:58px; text-align:right;}

100
src/styles/playlist.css Normal file
View File

@@ -0,0 +1,100 @@
.listWrap{
flex:1 1 auto; min-height:0; position:relative; overflow:hidden;
}
.list{
position:absolute; inset:0;
overflow-y:scroll; overflow-x:hidden;
--fade-top:75px; --fade-bottom:75px;
mask-image:linear-gradient(180deg, transparent 0%, #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent 100%);
-webkit-mask-image:linear-gradient(180deg, transparent 0%, #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent 100%);
transition:--fade-top 0.8s ease, --fade-bottom 0.8s ease;
scrollbar-width:none;
}
.list::-webkit-scrollbar{display:none;}
@property --list-fade-top {
syntax: '<length>';
initial-value: 30px;
inherits: false;
}
@property --list-fade-bottom {
syntax: '<length>';
initial-value: 30px;
inherits: false;
}
.list.at-top{--fade-top:0px;}
.list.at-bottom{--fade-bottom:0px;}
.listScrollbar{
position:absolute;
top:12px; right:6px; bottom:12px;
width:3px;
border-radius:2px;
pointer-events:none;
opacity:0;
transition:opacity .4s ease;
z-index:10;
}
.listWrap:hover .listScrollbar, .listScrollbar.active{opacity:1;}
.listScrollbarThumb{
position:absolute;
top:0; left:0; right:0;
min-height:24px;
background:linear-gradient(180deg, rgba(95,175,255,.3), rgba(95,175,255,.15));
border:1px solid rgba(95,175,255,.2);
border-radius:2px;
box-shadow:0 2px 8px rgba(0,0,0,.2);
transition:background .2s ease, border-color .2s ease, box-shadow .2s ease;
cursor:grab;
}
.listScrollbarThumb:hover{
background:linear-gradient(180deg, rgba(95,175,255,.45), rgba(95,175,255,.25));
border-color:rgba(95,175,255,.35);
}
.listScrollbarThumb:active{
cursor:grabbing;
}
.listScrollbar.active .listScrollbarThumb{
background:linear-gradient(180deg, rgba(95,175,255,.5), rgba(95,175,255,.3));
border-color:rgba(95,175,255,.4);
box-shadow:0 2px 12px rgba(95,175,255,.15);
}
.row{position:relative; display:flex; align-items:flex-start; justify-content:space-between; gap:12px; padding:11px 12px; border-bottom:1px solid var(--strokeLight); cursor:pointer; user-select:none; transition:background .2s ease, box-shadow .2s ease; box-shadow:inset 3px 0 0 transparent;}
.row:hover{background:var(--surfaceHover); box-shadow:inset 3px 0 0 rgba(95,175,255,.45);}
.row:active{transform:none;}
.row.active{background:linear-gradient(90deg, var(--accentBg), transparent); box-shadow:inset 3px 0 0 rgba(95,175,255,.7), 0 0 0 1px var(--accentGlow) inset;}
.row.dragging{opacity:.55;}
.left{min-width:0; display:flex; align-items:flex-start; gap:10px; flex:1 1 auto;}
.numBadge{flex:0 0 auto; min-width:38px; height:22px; padding:0 8px; border-radius:999px; border:1px solid var(--strokeMed); background:radial-gradient(200px 60px at 20% 0%, var(--accentGlow), transparent 55%), var(--surface); box-shadow:var(--shadow2); display:flex; align-items:center; justify-content:center; font-family:var(--mono); font-size:11.8px; letter-spacing:.08px; color:var(--text); font-variant-numeric:tabular-nums; margin-top:1px; transition:all .2s ease; transform:translateX(0);}
.row:hover .numBadge{border-color:var(--accentBorder); background:radial-gradient(200px 60px at 20% 0%, var(--accentGlow), transparent 50%), var(--surfaceHover); transform:translateX(4px);}
.treeSvg{flex:0 0 auto; margin-top:1px; overflow:visible;}
.treeSvg line{stroke:rgb(65,75,95); stroke-width:1.5;}
.treeSvg circle{fill:rgba(230,240,255,.70); stroke:rgba(100,180,255,.22); stroke-width:1; transition:all .15s ease;}
.row:hover .treeSvg circle{fill:rgba(240,250,255,.85); stroke:rgba(100,180,255,.35);}
.textWrap{min-width:0; flex:1 1 auto; transition:transform .2s ease; transform:translateX(0);}
.row:hover .textWrap{transform:translateX(4px);}
.name{font-size:12.9px; font-weight:820; letter-spacing:.12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .15s ease;}
.row:hover .name{color:rgba(255,255,255,.98);}
.small{margin-top:4px; font-size:11.4px; color:var(--textMuted); font-family:var(--mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .15s ease;}
.row:hover .small{color:rgba(175,185,210,.85);}
.tag{flex:0 0 auto; display:inline-flex; align-items:center; padding:5px 9px; border-radius:999px; border:1px solid var(--stroke); background:var(--surface); color:var(--textMuted); font-size:11px; font-weight:820; letter-spacing:.12px; text-transform:uppercase; margin-top:2px; transition:all .15s ease;}
.tag.now{border-color:var(--accentBorder); color:rgba(215,240,255,.90); background:var(--accentBg);}
.tag.done{border-color:var(--successBorder); color:rgba(210,255,230,.88); background:var(--successBg);}
.tag.hidden{display:none;}
.row:hover .tag{transform:scale(1.02);}
.row.drop-before::before,.row.drop-after::after{
content:""; position:absolute; left:10px; right:10px; border-top:2px solid var(--accent);
pointer-events:none; filter:drop-shadow(0 0 10px var(--accentGlow));
}
.row.drop-before::before{top:-1px;}
.row.drop-after::after{bottom:-1px;}
.empty{padding:14px 12px; color:var(--textMuted); font-size:12.5px; line-height:1.4;}
.playlistHeader{font-weight:900; letter-spacing:.16px; font-size:13.8px; cursor:help; display:flex; align-items:center; gap:10px;}
.playlistHeader .fa{color:var(--iconStrong)!important; opacity:.92;}

188
src/types.ts Normal file
View File

@@ -0,0 +1,188 @@
// ===== Video Item (from get_library_info items array) =====
export interface VideoItem {
index: number;
fid: string;
name: string;
title: string;
relpath: string;
depth: number;
pipes: boolean[];
is_last: boolean;
has_prev_in_parent: boolean;
pos: number;
watched: number;
duration: number | null;
finished: boolean;
note_len: number;
last_open: number;
has_sub: boolean;
}
// ===== Library Info (from get_library / open_folder_path / select_folder) =====
export interface NextUp {
index: number;
title: string;
}
export interface LibraryInfo {
ok: boolean;
error?: string;
cancelled?: boolean;
folder?: string;
library_id?: string;
count?: number;
current_index?: number;
current_fid?: string | null;
current_time?: number;
folder_volume?: number;
folder_autoplay?: boolean;
folder_rate?: number;
items?: VideoItem[];
has_subdirs?: boolean;
overall_progress?: number | null;
durations_known?: number;
finished_count?: number;
remaining_count?: number;
remaining_seconds_known?: number | null;
top_folders?: [string, number][];
next_up?: NextUp | null;
}
// ===== Preferences =====
export interface WindowState {
width: number;
height: number;
x: number | null;
y: number | null;
}
export interface Prefs {
version: number;
ui_zoom: number;
split_ratio: number;
dock_ratio: number;
always_on_top: boolean;
window: WindowState;
last_folder_path: string | null;
last_library_id: string | null;
updated_at: number;
}
// ===== API Responses =====
export interface OkResponse {
ok: boolean;
error?: string;
}
export interface PrefsResponse {
ok: boolean;
prefs: Prefs;
}
export interface RecentItem {
name: string;
path: string;
}
export interface RecentsResponse {
ok: boolean;
items: RecentItem[];
}
export interface NoteResponse {
ok: boolean;
note: string;
len?: number;
}
// ===== Video Metadata =====
export interface BasicFileMeta {
ext: string;
size: number;
mtime: number;
folder: string;
}
export interface SubtitleTrack {
index: number;
codec: string;
language: string;
title: string;
}
export interface ProbeMeta {
v_codec?: string;
width?: number;
height?: number;
fps?: number;
v_bitrate?: number;
pix_fmt?: string;
color_space?: string;
a_codec?: string;
channels?: number;
sample_rate?: string;
a_bitrate?: number;
subtitle_tracks?: SubtitleTrack[];
container_bitrate?: number;
duration?: number;
format_name?: string;
container_title?: string;
encoder?: string;
}
export interface VideoMetaResponse {
ok: boolean;
error?: string;
fid?: string;
basic?: BasicFileMeta;
probe?: ProbeMeta | null;
ffprobe_found?: boolean;
}
// ===== Subtitles =====
export interface SubtitleResponse {
ok: boolean;
has?: boolean;
url?: string;
label?: string;
cancelled?: boolean;
error?: string;
}
export interface SidecarSub {
path: string;
label: string;
format: string;
}
export interface EmbeddedSub {
index: number;
label: string;
codec: string;
language: string;
}
export interface AvailableSubsResponse {
ok: boolean;
sidecar: SidecarSub[];
embedded: EmbeddedSub[];
}
export interface EmbeddedSubsResponse {
ok: boolean;
tracks: SubtitleTrack[];
}
// ===== FFmpeg Download Progress =====
export interface FfmpegProgress {
percent: number;
downloaded_bytes: number;
total_bytes: number;
}