diff --git a/README.md b/README.md index 1ed5a51..11c839a 100644 --- a/README.md +++ b/README.md @@ -282,7 +282,7 @@ TutorialVault is a [Tauri v2](https://v2.tauri.app/) application with a Rust bac - `store.ts` -- Shared state, pure utilities, cross-module callback registry - `api.ts` -- Typed wrapper around Tauri's invoke API -**🎨 Design theme.** The interface uses a "Cold Open" dark theme built on cool slate backgrounds (`#0f1117` base) with a steel blue accent (`#88A4C4`). Typography uses [Fraunces](https://fonts.google.com/specimen/Fraunces) for headings, [Inter](https://fonts.google.com/specimen/Inter) for body text, and [Space Mono](https://fonts.google.com/specimen/Space+Mono) for monospace elements. All fonts are bundled -- no external requests at runtime. +**🎨 Design theme.** The interface uses a "Cold Open" dark theme built on cool slate backgrounds (`#0f1117` base) with a steel blue accent (`#88A4C4`). Typography uses [Bricolage Grotesque](https://fonts.google.com/specimen/Bricolage+Grotesque) for headings, [Inter](https://fonts.google.com/specimen/Inter) for body text, and [Space Mono](https://fonts.google.com/specimen/Space+Mono) for monospace elements. All fonts are bundled -- no external requests at runtime. **💾 State is local.** All data is stored in JSON files with atomic writes and backup rotation. No database, no server, no network requests beyond the one-time ffmpeg download. diff --git a/package-lock.json b/package-lock.json index 2cb5236..4d5faeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "tutorialvault", "version": "0.1.0", "dependencies": { - "@fontsource/fraunces": "^5.2.9", + "@fontsource/bricolage-grotesque": "^5.2.10", "@fontsource/inter": "^5.2.8", "@fontsource/space-mono": "^5.2.9", "@fortawesome/fontawesome-free": "^7.2.0", @@ -411,10 +411,10 @@ "node": ">=12" } }, - "node_modules/@fontsource/fraunces": { - "version": "5.2.9", - "resolved": "https://registry.npmjs.org/@fontsource/fraunces/-/fraunces-5.2.9.tgz", - "integrity": "sha512-XDzuddBtoC7BZgZdBn6b7hsFZY2+V1hgN7yca5fBTKuHjb/lOd45a0Ji8dTUgFhPoL7RdGupo+bC2BFSt6UH8Q==", + "node_modules/@fontsource/bricolage-grotesque": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@fontsource/bricolage-grotesque/-/bricolage-grotesque-5.2.10.tgz", + "integrity": "sha512-V2xS+1P7C8IrSypXLUx/bLtX/LsTlYtV2k2CsU+S/0t8qepZ2hvKSlyJIx7Ub/iY8Bbnj+IjAuUF9nvFz+BbIg==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" diff --git a/package.json b/package.json index 711e61b..088f19d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "tauri": "tauri" }, "dependencies": { - "@fontsource/fraunces": "^5.2.9", + "@fontsource/bricolage-grotesque": "^5.2.10", "@fontsource/inter": "^5.2.8", "@fontsource/space-mono": "^5.2.9", "@fortawesome/fontawesome-free": "^7.2.0", diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index b780f5c..4843397 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index 337ec1c..fab991a 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index 6a91148..80740c0 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png index 74df55f..a7dc749 100644 Binary files a/src-tauri/icons/64x64.png and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index 35397df..8819797 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index e8ad538..0e39e0d 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index 98c928f..77dee07 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index 4de9707..1bcc6ad 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index c320915..f541b85 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index 58caa80..d574087 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index f22ddab..0f4119f 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index 99531b9..49cadc2 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index bbfad72..782a511 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index e453ab8..93832b4 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/app-icon.png b/src-tauri/icons/app-icon.png index 1297215..d51ed7b 100644 Binary files a/src-tauri/icons/app-icon.png and b/src-tauri/icons/app-icon.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 4324189..9a5517c 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index 9b8c195..9c2d1ff 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index aa3049c..d51ed7b 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src/main.ts b/src/main.ts index 01060a1..b230bba 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,9 +3,9 @@ * Orchestrates all modules and holds cross-module callbacks. */ import '@fortawesome/fontawesome-free/css/all.min.css'; -import '@fontsource/fraunces/600.css'; -import '@fontsource/fraunces/700.css'; -import '@fontsource/fraunces/800.css'; +import '@fontsource/bricolage-grotesque/500.css'; +import '@fontsource/bricolage-grotesque/600.css'; +import '@fontsource/bricolage-grotesque/700.css'; import '@fontsource/inter/400.css'; import '@fontsource/inter/500.css'; import '@fontsource/inter/600.css'; @@ -171,7 +171,7 @@ async function tick(): Promise { try { await api.tickProgress(currentIndex, t, d, playing); } catch (_) {} } - if (now % 3000 < 1000) { + if (now % 3000 < 1000 && !suppressTick) { try { const info = await api.getLibrary(); if (info && info.ok) { diff --git a/src/player.ts b/src/player.ts index f7c2725..f410f4d 100644 --- a/src/player.ts +++ b/src/player.ts @@ -155,6 +155,12 @@ export function initPlayer(): void { if (d > 0) seek.value = String(Math.round((t / d) * 1000)); updateSeekFill(); updateTimeReadout(); + // Update active row's mini progress bar in real time + const activeRow = document.querySelector('.row.active'); + if (activeRow && d > 0) { + const bar = activeRow.querySelector('.rowProgress') as HTMLElement; + if (bar) bar.style.width = clamp((t / d) * 100, 0, 100) + '%'; + } } cb.updateInfoPanel?.(); }); @@ -434,8 +440,8 @@ export function cycleSpeed(delta: number): void { cb.updateInfoPanel?.(); cb.notify?.(`Speed: ${r}x`); setSuppressTick(true); - if (library) { api.setFolderRate(r).catch(() => {}); } - setSuppressTick(false); + if (library) { api.setFolderRate(r).catch(() => {}).finally(() => setSuppressTick(false)); } + else { setSuppressTick(false); } } /** Load a video by index and handle the onloadedmetadata callback. */ @@ -530,7 +536,7 @@ export function buildSpeedMenu(active: number): void { left.appendChild(dot); left.appendChild(txt); row.appendChild(left); - row.onclick = async () => { + row.onclick = () => { closeSpeedMenu(); const r = clamp(Number(s), 0.25, 3); player.playbackRate = r; @@ -540,8 +546,8 @@ export function buildSpeedMenu(active: number): void { buildSpeedMenu(r); cb.updateInfoPanel?.(); setSuppressTick(true); - if (library) { try { await api.setFolderRate(r); } catch (_) {} } - setSuppressTick(false); + if (library) { api.setFolderRate(r).catch(() => {}).finally(() => setSuppressTick(false)); } + else { setSuppressTick(false); } }; speedMenu.appendChild(row); } diff --git a/src/styles/components.css b/src/styles/components.css index d763485..34a1857 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -33,7 +33,7 @@ } .toastMsg{ font-size:13px; - font-weight:600; + font-weight:500; letter-spacing:0; color:var(--text); } @@ -82,17 +82,17 @@ .tooltip::after{display:none;} .tooltip-title{ font-family:var(--brand); - font-weight:700; + font-weight:600; font-size:14px; margin-bottom:6px; - letter-spacing:-.01em; + letter-spacing:-.02em; color:rgba(235,240,252,.95); } .tooltip-desc{ font-family:var(--sans); font-size:12px; - font-weight:500; - color:rgba(170,182,210,.82); + font-weight:400; + color:rgba(170,182,210,.86); line-height:1.55; letter-spacing:0; position:relative; @@ -115,7 +115,7 @@ } .subsMenu.show{display:block; opacity:1; transform:translateX(-50%) scale(1);} .subsMenuHeader{padding:6px 12px 4px; font-size:10px; font-weight:600; text-transform:uppercase; letter-spacing:.08em; color:var(--textDim); transition:color .2s ease;} -.subsMenuItem{padding:10px 12px; border-radius:var(--r3); cursor:pointer; user-select:none; font-size:12px; font-weight:600; color:var(--text); letter-spacing:0; display:flex; align-items:center; gap:10px; transition:all .2s var(--ease-bounce);} +.subsMenuItem{padding:10px 12px; border-radius:var(--r3); cursor:pointer; user-select:none; font-size:12px; font-weight:500; color:var(--text); letter-spacing:0; display:flex; align-items:center; gap:10px; transition:all .2s var(--ease-bounce);} .subsMenuItem:hover{background:var(--surfaceHover); padding-left:16px;} .subsMenuItem:active{transform:scale(.97); transition-duration:.08s;} .subsMenuItem .fa{font-size:13px; color:var(--iconStrong)!important; opacity:.85; width:18px; text-align:center; transition:transform .2s var(--ease-bounce), opacity .15s ease;} @@ -175,11 +175,12 @@ } .shortcutHelpTitle{ font-family:var(--brand); - font-weight:700; - font-size:16px; + font-weight:600; + font-size:17px; + line-height:1.2; margin:0 0 20px; color:rgba(235,240,252,.95); - letter-spacing:-.01em; + letter-spacing:-.02em; } .shortcutGrid{ display:grid; diff --git a/src/styles/main.css b/src/styles/main.css index 8209ab1..0aac07d 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -1,5 +1,6 @@ :root{ --zoom:1; + /* Type scale — Minor Third (1.2) from 13px base: 10 · 11 · 12 · 13 · 15 · 17 · 19 */ /* Base backgrounds — cool dark slate, lighter */ --bg0:#0f1117; --bg1:#151821; /* Strokes — cool, very subtle */ @@ -9,8 +10,8 @@ --strokeStrong:rgba(140,160,210,.14); /* Text — cool white hierarchy */ --text:rgba(218,225,240,.90); - --textMuted:rgba(160,174,204,.72); - --textDim:rgba(158,174,208,.68); + --textMuted:rgba(185,196,222,.86); + --textDim:rgba(172,184,214,.88); /* Surfaces — cool-tinted, subtle fills */ --surface-0:rgba(140,165,220,.04); --surface:rgba(140,165,220,.06); @@ -34,7 +35,7 @@ /* Fonts */ --mono:"Space Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; --sans:"Inter", ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; - --brand:"Fraunces", Georgia, "Times New Roman", serif; + --brand:"Bricolage Grotesque", "Inter", ui-sans-serif, system-ui, sans-serif; /* Icons — cool */ --icon:rgba(160,175,210,.62); --iconStrong:rgba(200,212,238,.75); @@ -58,7 +59,7 @@ body{ margin:0; padding:0; font-family:var(--sans); color:var(--text); overflow:hidden; width:100vw; height:100vh; font-weight:400; - line-height:1.4; + line-height:1.45; background: radial-gradient(900px 600px at 8% 3%, rgba(100,140,210,.04), transparent 55%), radial-gradient(600px 400px at 92% 97%, rgba(90,120,180,.03), transparent 60%), @@ -197,9 +198,9 @@ body{ .brandText{min-width:0; position:relative; z-index:1;} .appName{ font-family:var(--brand); - font-weight:800; - font-size:18px; - line-height:1.02; + font-weight:700; + font-size:19px; + line-height:1.1; letter-spacing:-.02em; margin:0; padding:0; transition:text-shadow .3s var(--ease-spring), color .2s ease; @@ -218,7 +219,7 @@ body{ transition:color .3s var(--ease-spring); } .brand:hover .tagline{ - color:rgba(180,192,218,.82); + color:rgba(192,202,226,.90); } .actions{ @@ -429,7 +430,7 @@ body{ .dropdownPortal::-webkit-scrollbar-thumb{background:rgba(140,160,210,.10); border-radius:999px;} .dropdownPortal::-webkit-scrollbar-button{width:0; height:0; display:none;} -.dropItem{display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:var(--r3); cursor:pointer; user-select:none; color:var(--text); font-weight:600; font-size:13px; letter-spacing:0; line-height:1.25; transition:all .2s var(--ease-bounce); position:relative;} +.dropItem{display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:var(--r3); cursor:pointer; user-select:none; color:var(--text); font-weight:500; font-size:13px; letter-spacing:0; line-height:1.25; transition:all .2s var(--ease-bounce); position:relative;} .dropItem:hover{background:var(--surfaceHover); padding-left:16px; padding-right:36px;} .dropItem:active{transform:scale(.98); transition-duration:.08s;} .dropIcon{width:18px; height:18px; display:flex; align-items:center; justify-content:center; flex:0 0 auto; opacity:.9; transition:transform .25s var(--ease-bounce), opacity .15s ease;} @@ -446,7 +447,7 @@ body{ .seg{display:inline-flex; border:none; border-radius:var(--r2); overflow:hidden; background:var(--surface-2); transition:background .15s ease;} .seg:hover{background:var(--surface-3);} -.seg .btn{border:none; box-shadow:none; border-radius:0; padding:8px 9px; background:transparent; font-weight:800; transform:none;} +.seg .btn{border:none; box-shadow:none; border-radius:0; padding:8px 9px; background:transparent; font-weight:700; transform:none;} .seg .btn:hover{background:var(--surface-3); transform:none; box-shadow:none;} .seg .btn:active{background:var(--surface-4); transform:scale(.95);} .seg .mid{border-left:1px solid rgba(140,160,210,.06); border-right:1px solid rgba(140,160,210,.06); min-width:62px; font-variant-numeric:tabular-nums;} diff --git a/src/styles/panels.css b/src/styles/panels.css index 7dfb66d..08374e1 100644 --- a/src/styles/panels.css +++ b/src/styles/panels.css @@ -4,9 +4,9 @@ .panelHeader{padding:10px 12px 8px; border-bottom:1px solid rgba(140,160,210,.04); display:flex; align-items:flex-start; justify-content:space-between; gap:12px; flex:0 0 auto; min-width:0; background:rgba(140,165,220,.015); transition:background .3s ease;} .panelHeader:hover{background:rgba(140,165,220,.025);} .panelHeader::after{display:none;} -.nowTitle{font-family:var(--brand); font-weight:700; font-size:14px; letter-spacing:-.01em; max-width:60ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .2s ease;} +.nowTitle{font-family:var(--brand); font-weight:600; font-size:15px; letter-spacing:-.02em; line-height:1.2; max-width:60ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .2s ease;} .nowTitle:hover{color:rgba(235,240,252,.98);} -.nowSub{margin-top:4px; color:var(--textMuted); font-size:11px; font-family:var(--mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:80ch; transition:color .2s ease;} +.nowSub{margin-top:3px; color:var(--textMuted); font-size:11px; font-family:var(--mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:80ch; transition:color .2s ease;} .dividerWrap{display:flex; align-items:stretch; justify-content:center;} .divider{width:10px; cursor:col-resize; position:relative; background:transparent; border:none;} @@ -25,11 +25,11 @@ .dock{flex:1 1 auto; min-height:0; border-top:1px solid rgba(140,160,210,.04); display:grid; grid-template-columns:62% 10px 38%; background:rgba(0,0,0,.06); overflow:hidden;} .dockPane{min-height:0; display:flex; flex-direction:column; overflow:hidden;} .dockInner{padding:10px; min-height:0; display:flex; flex-direction:column; gap:8px; height:100%;} -.dockHeader{padding:10px 12px 9px; border:none; border-radius:var(--r2); display:flex; align-items:center; justify-content:space-between; gap:8px; background:var(--surface-2); flex:0 0 auto; transition:background .2s ease;} +.dockHeader{padding:10px 12px 9px; border:none; border-radius:var(--r2); display:flex; align-items:center; justify-content:space-between; gap:8px; background:var(--surface-2); flex:0 0 auto; min-height:47px; transition:background .2s ease;} .dockHeader:hover{background:var(--surface-3);} #notesHeader{border-bottom-right-radius:7px;} #infoHeader{border-bottom-left-radius:7px; margin-right:12px;} -.dockTitle{font-family:var(--brand); font-weight:700; letter-spacing:0; font-size:13px; color:rgba(218,225,240,.92); display:flex; align-items:center; gap:10px; transition:color .2s ease;} +.dockTitle{font-family:var(--brand); font-weight:600; letter-spacing:-.02em; font-size:15px; line-height:1.2; color:rgba(218,225,240,.92); display:flex; align-items:center; gap:10px; transition:color .2s ease;} .dockHeader:hover .dockTitle{color:rgba(235,240,252,.98);} .dockTitle .fa{color:var(--iconStrong)!important; opacity:.88; font-size:14px; transition:transform .3s var(--ease-bounce), opacity .2s ease;} .dockHeader:hover .dockTitle .fa{transform:scale(1.12) rotate(-5deg); opacity:1;} @@ -37,6 +37,7 @@ .notesArea{flex:1 1 auto; min-height:0; overflow:hidden; position:relative;} .notesSaved{ position:absolute; + z-index:2; bottom:12px; right:12px; padding:6px 10px; border-radius:var(--r2); @@ -63,9 +64,20 @@ 60%{transform:scale(1.3) rotate(5deg);} 100%{transform:scale(1) rotate(0);} } -.notes{width:100%; height:100%; resize:none; border-radius:var(--r3); border:1px solid transparent; background:var(--surface-0); color:rgba(218,225,240,.90); padding:10px 12px; outline:none; font-family:var(--sans); font-size:13px; line-height:1.45; letter-spacing:0; box-shadow:var(--shadowInset); overflow:auto; scrollbar-width:thin; scrollbar-color:rgba(140,160,210,.10) transparent; transition:border-color .25s ease, box-shadow .25s ease, background .2s ease;} -.notes:hover{background:rgba(140,165,220,.03);} -.notes:focus{border-color:rgba(136,164,196,.25); box-shadow:var(--shadowInset), 0 0 0 2px rgba(136,164,196,.12);} +/* Notes timestamp highlighting backdrop */ +.notesBackdrop{position:absolute; inset:0; padding:10px 12px; border:1px solid transparent; border-radius:var(--r3); background:var(--surface-0); box-shadow:var(--shadowInset); font-family:var(--sans); font-size:13px; line-height:1.45; letter-spacing:0; color:rgba(218,225,240,.90); white-space:pre-wrap; word-break:break-word; overflow-wrap:anywhere; overflow-y:scroll; scrollbar-width:thin; scrollbar-color:transparent transparent; pointer-events:none; transition:border-color .25s ease, box-shadow .25s ease, background .2s ease;} +.notesBackdrop::-webkit-scrollbar{width:2px; height:2px;} +.notesBackdrop::-webkit-scrollbar-track{background:transparent;} +.notesBackdrop::-webkit-scrollbar-thumb{background:transparent;} +.notesBackdrop::-webkit-scrollbar-button{width:0; height:0; display:none;} +.notesBackdrop.hover{background:rgba(140,165,220,.03);} +.notesBackdrop.focus{border-color:rgba(136,164,196,.25); box-shadow:var(--shadowInset), 0 0 0 2px rgba(136,164,196,.12);} +.tsLink{color:rgba(136,164,196,.85); pointer-events:auto; cursor:pointer; transition:color .15s ease;} +.notesBackdrop.hover .tsLink,.notesBackdrop.focus .tsLink{color:rgba(136,164,196,.95);} +.notes{width:100%; height:100%; resize:none; border-radius:var(--r3); border:1px solid transparent; background:transparent; color:transparent; caret-color:rgba(218,225,240,.90); padding:10px 12px; outline:none; font-family:var(--sans); font-size:13px; line-height:1.45; letter-spacing:0; box-shadow:none; overflow:auto; scrollbar-width:thin; scrollbar-color:rgba(140,160,210,.10) transparent; position:relative; z-index:1;} +.notes:hover{background:transparent;} +.notes:focus{border-color:transparent; box-shadow:none;} +.notes::selection{background:rgba(136,164,196,.30);} .notes::-webkit-scrollbar{width:2px; height:2px;} .notes::-webkit-scrollbar-track{background:transparent;} .notes::-webkit-scrollbar-thumb{background:rgba(140,160,210,.10); border-radius:0;} @@ -127,11 +139,11 @@ dl.kv dt,dl.kv dd{margin:0; padding:0;} white-space:nowrap; transition:color .2s ease; } -.kv:hover .k{color:rgba(148,162,192,.50);} +.kv:hover .k{color:var(--textDim);} .v{ font-family:var(--brand); font-size:13px; - font-weight:600; + font-weight:500; color:var(--text); letter-spacing:-.01em; word-break:break-word; @@ -154,7 +166,7 @@ dl.kv dt,dl.kv dd{margin:0; padding:0;} margin:-2px 0; transition:background .2s ease, color .2s ease; } -.kv:hover .v.mono{background:rgba(136,164,196,.09); color:rgba(170,185,215,.70);} +.kv:hover .v.mono{background:rgba(136,164,196,.09); color:var(--textMuted);} .dockDividerWrap{display:flex; align-items:stretch; justify-content:center;} .dockDivider{width:10px; cursor:col-resize; position:relative; background:transparent; border:none;} diff --git a/src/styles/player.css b/src/styles/player.css index b7adf3a..4bb954f 100644 --- a/src/styles/player.css +++ b/src/styles/player.css @@ -72,7 +72,7 @@ video::cue{ color:rgba(255,255,255,1)!important; } -.controls{display:flex; flex-direction:column; gap:10px; padding:12px; border-top:1px solid rgba(140,160,210,.04); flex:0 0 auto;} +.controls{display:flex; flex-direction:column; gap:10px; padding:12px; border-top:1px solid rgba(140,160,210,.04); flex:0 0 auto; position:relative; z-index:10;} .controlsStrip{ display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap; padding:5px 6px; @@ -151,7 +151,7 @@ video::cue{ .seek::-moz-range-track{background:transparent; height:18px;} .seek::-moz-range-thumb{width:16px; height:16px; border-radius:999px; border:2px solid rgba(200,210,230,.28); background:rgba(220,228,240,.88); box-shadow:0 2px 6px rgba(0,0,0,.30); cursor:pointer;} -.miniCtl{display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:var(--r2); border:none; background:var(--surface-2); box-shadow:var(--shadow3); position:relative; transition:all .2s var(--ease-bounce);} +.miniCtl{display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:var(--r2); border:none; background:var(--surface-2); box-shadow:var(--shadow3); position:relative; z-index:3; transition:all .2s var(--ease-bounce);} .miniCtl:hover{background:var(--surface-3); transform:translateY(-1px);} .miniCtl:active{transform:scale(.97) translateY(0); transition-duration:.08s;} .miniCtl .fa{font-size:14px; color:var(--iconStrong)!important; opacity:.95; flex:0 0 auto; transition:transform .2s var(--ease-bounce), opacity .15s ease;} @@ -234,7 +234,7 @@ video::cue{ .progressPill{flex:0 0 auto; display:flex; align-items:center; gap:8px; padding:6px 10px; border-radius:var(--r2); border:none; background:var(--surface-2); box-shadow:var(--shadow3); min-width:220px; transition:background .2s ease;} .progressPill:hover{background:var(--surface-3);} .progressLabel{font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--textMuted); margin-right:2px; transition:color .2s ease;} -.progressPill:hover .progressLabel{color:rgba(170,185,215,.70);} +.progressPill:hover .progressLabel{color:var(--textMuted);} .progressBar{width:120px; height:8px; border-radius:999px; border:none; background:rgba(140,165,220,.04); overflow:hidden; transition:height .2s var(--ease-bounce);} .progressPill:hover .progressBar{height:10px;} .progressBar>div{height:100%; width:0%; background:rgba(136,164,196,.75); transition:width .4s var(--ease-spring);} diff --git a/src/styles/playlist.css b/src/styles/playlist.css index 00474c6..7262bc3 100644 --- a/src/styles/playlist.css +++ b/src/styles/playlist.css @@ -75,10 +75,10 @@ .treeSvg circle{fill:rgba(200,212,238,.60); stroke:rgba(136,164,196,.18); stroke-width:1;} .textWrap{min-width:0; flex:1 1 auto;} -.name{font-size:13px; font-weight:700; letter-spacing:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .2s var(--ease-spring), text-shadow .2s ease, transform .2s var(--ease-bounce); transform:translateX(0);} +.name{font-size:13px; font-weight:600; letter-spacing:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .2s var(--ease-spring), text-shadow .2s ease, transform .2s var(--ease-bounce); transform:translateX(0);} .row:hover .name{color:rgba(235,240,252,.96); text-shadow:0 0 20px rgba(136,164,196,.08); transform:translateX(4px);} .small{margin-top:4px; font-size:11px; color:var(--textMuted); font-family:var(--mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .2s ease;} -.row:hover .small{color:rgba(170,182,210,.75);} +.row:hover .small{color:var(--textMuted);} .tag{flex:0 0 auto; display:inline-flex; align-items:center; padding:5px 9px; border-radius:var(--r2); border:none; background:var(--surface-2); color:var(--textMuted); font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; margin-top:2px; transition:all .2s var(--ease-bounce);} .row:hover .tag{transform:scale(1.05);} @@ -105,7 +105,7 @@ .empty{padding:10px 12px; color:var(--textMuted); font-size:13px; line-height:1.4;} -.playlistHeader{font-family:var(--brand); font-weight:800; letter-spacing:-.01em; font-size:14px; cursor:help; display:flex; align-items:center; gap:10px; transition:color .2s ease;} +.playlistHeader{font-family:var(--brand); font-weight:600; letter-spacing:-.02em; font-size:15px; line-height:1.2; cursor:help; display:flex; align-items:center; gap:10px; transition:color .2s ease;} .playlistHeader:hover{color:rgba(235,240,252,.98);} .playlistHeader .fa{color:var(--iconStrong)!important; opacity:.92; transition:transform .3s var(--ease-bounce), opacity .2s ease;} .playlistHeader:hover .fa{transform:rotate(-8deg) scale(1.12); opacity:1;} diff --git a/src/ui.ts b/src/ui.ts index 2aa0de3..140fc92 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -27,6 +27,7 @@ let recentMenu: HTMLElement; let refreshBtn: HTMLElement; let resetProgBtn: HTMLElement; let notesBox: HTMLTextAreaElement; +let notesBackdrop: HTMLElement; let notesSaved: HTMLElement; let nowTitle: HTMLElement; let nowSub: HTMLElement; @@ -93,6 +94,32 @@ export function initUI(): void { resetProgBtn = document.getElementById('resetProgBtn')!; notesBox = document.getElementById('notesBox') as HTMLTextAreaElement; notesSaved = document.getElementById('notesSaved')!; + + // Create notes highlight backdrop for timestamp coloring + notesBackdrop = document.createElement('div'); + notesBackdrop.className = 'notesBackdrop'; + notesBackdrop.setAttribute('aria-hidden', 'true'); + notesBox.parentElement!.insertBefore(notesBackdrop, notesBox); + + // Scroll sync between textarea and backdrop + notesBox.addEventListener('scroll', () => { + notesBackdrop.scrollTop = notesBox.scrollTop; + notesBackdrop.scrollLeft = notesBox.scrollLeft; + }); + + // Forward hover/focus state to backdrop for visual effects + notesBox.addEventListener('mouseenter', () => notesBackdrop.classList.add('hover')); + notesBox.addEventListener('mouseleave', () => notesBackdrop.classList.remove('hover')); + notesBox.addEventListener('focus', () => notesBackdrop.classList.add('focus')); + notesBox.addEventListener('blur', () => notesBackdrop.classList.remove('focus')); + + // Show pointer cursor when hovering over timestamps + notesBox.addEventListener('mousemove', (e) => { + notesBox.style.pointerEvents = 'none'; + const el = document.elementFromPoint(e.clientX, e.clientY); + notesBox.style.pointerEvents = ''; + notesBox.style.cursor = (el && el.classList.contains('tsLink')) ? 'pointer' : ''; + }); nowTitle = document.getElementById('nowTitle')!; nowSub = document.getElementById('nowSub')!; overallBar = document.getElementById('overallBar')!; @@ -348,6 +375,7 @@ export function initUI(): void { // --- Notes --- notesBox.addEventListener('input', () => { + updateNotesHighlight(); if (!library) return; const it = currentItem(); if (!it) return; @@ -385,6 +413,37 @@ export function initUI(): void { }; } + // --- Clickable timestamps in notes --- + notesBox.addEventListener('click', () => { + const pos = notesBox.selectionStart; + if (pos === null || pos === undefined) return; + // Only act on single click without selection + if (notesBox.selectionStart !== notesBox.selectionEnd) return; + const text = notesBox.value; + // Match [H:MM:SS] or [M:SS] patterns + const re = /\[(\d{1,3}):(\d{2})(?::(\d{2}))?\]/g; + let match; + while ((match = re.exec(text)) !== null) { + const start = match.index; + const end = start + match[0].length; + if (pos >= start && pos <= end) { + let totalSeconds: number; + if (match[3] !== undefined) { + // [H:MM:SS] + totalSeconds = parseInt(match[1], 10) * 3600 + parseInt(match[2], 10) * 60 + parseInt(match[3], 10); + } else { + // [M:SS] + totalSeconds = parseInt(match[1], 10) * 60 + parseInt(match[2], 10); + } + if (player && Number.isFinite(player.duration) && player.duration > 0) { + player.currentTime = clamp(totalSeconds, 0, player.duration); + cb.notify?.(`Jumped to ${match[0]}`); + } + break; + } + } + }); + // --- Collapsible dock panes --- const notesHeader = document.getElementById('notesHeader'); const infoHeader = document.getElementById('infoHeader'); @@ -578,11 +637,12 @@ export async function refreshCurrentVideoMeta(): Promise { export async function loadNoteForCurrent(): Promise { const it = currentItem(); - if (!it) { notesBox.value = ''; return; } + if (!it) { notesBox.value = ''; updateNotesHighlight(); return; } try { const res = await api.getNote(it.fid); notesBox.value = (res && res.ok) ? (res.note || '') : ''; } catch (_) { notesBox.value = ''; } + updateNotesHighlight(); } export function setOnTopChecked(v: boolean): void { onTopChk.checked = v; } @@ -726,6 +786,21 @@ function toggleDockPane(header: HTMLElement, prefKey: string): void { savePrefsPatch({ [prefKey]: isCollapsed }); } +function updateNotesHighlight(): void { + if (!notesBackdrop) return; + const text = notesBox.value; + const escaped = text + .replace(/&/g, '&') + .replace(//g, '>'); + const highlighted = escaped.replace( + /\[(\d{1,3}:\d{2}(?::\d{2})?)\]/g, + '[$1]' + ); + // Trailing newline matches textarea's extra line space + notesBackdrop.innerHTML = highlighted + '\n'; +} + function collapsePane(header: HTMLElement, collapsed: boolean): void { const pane = header.closest('.dockPane'); if (!pane) return;