diff --git a/.gitignore b/.gitignore index 34c8ec3..65d6508 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ release/ tmp_e2e_full/ tmp_bugtest/ tmp_dl/ +tmp_ui_shots/ # Dev-Scripts ohne Token (Stub, nicht produktiv) codeberg_api_upload.sh diff --git a/package-lock.json b/package-lock.json index bbf543a..ab9650e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "twitch-vod-manager", - "version": "5.0.12", + "version": "5.0.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twitch-vod-manager", - "version": "5.0.12", + "version": "5.0.13", "license": "MIT", "dependencies": { "axios": "^1.16.1", diff --git a/package.json b/package.json index 4494ddd..50097b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "twitch-vod-manager", - "version": "5.0.12", + "version": "5.0.13", "description": "Twitch VOD Manager - Download Twitch VODs easily", "main": "dist/main.js", "author": "xRangerDE", diff --git a/scripts/ui-screenshot.js b/scripts/ui-screenshot.js new file mode 100644 index 0000000..aae720a --- /dev/null +++ b/scripts/ui-screenshot.js @@ -0,0 +1,119 @@ +// UI-Screenshot-Harness — laedt die App, navigiert durch Tabs + Streamer +// + Themes und schreibt PNGs nach tmp_ui_shots/. Kein Test-Assert, nur +// visuelle Forensik fuer UI-Polishing. +// +// Run: node scripts/ui-screenshot.js +const { _electron: electron } = require('playwright'); +const fs = require('fs'); +const path = require('path'); + +const OUT_DIR = path.join(process.cwd(), 'tmp_ui_shots'); + +async function run() { + fs.rmSync(OUT_DIR, { recursive: true, force: true }); + fs.mkdirSync(OUT_DIR, { recursive: true }); + + const electronPath = require('electron'); + const app = await electron.launch({ + executablePath: electronPath, + args: ['.'], + cwd: process.cwd() + }); + const win = await app.firstWindow(); + await win.setViewportSize({ width: 1280, height: 900 }); + await win.waitForTimeout(2500); + + const shot = async (name) => { + const file = path.join(OUT_DIR, `${name}.png`); + try { + await win.screenshot({ path: file, animations: 'disabled', timeout: 8000 }); + process.stdout.write(`shot: ${name}\n`); + } catch (e) { + process.stdout.write(`shot FAILED: ${name} — ${String(e).split('\n')[0]}\n`); + } + }; + + // Misst die Y-Position aller .vod-actions relativ zu ihrer Grid-Reihe. + // Buttons in derselben Reihe muessen die gleiche bottom-Y haben. + const checkButtonAlignment = async (label) => { + const rows = await win.evaluate(() => { + const cards = Array.from(document.querySelectorAll('.vod-card')); + const byRow = {}; + for (const card of cards) { + const actions = card.querySelector('.vod-actions'); + if (!actions) continue; + const cardRect = card.getBoundingClientRect(); + const actRect = actions.getBoundingClientRect(); + const rowKey = Math.round(cardRect.top / 10) * 10; // Reihe nach top gruppieren + (byRow[rowKey] = byRow[rowKey] || []).push(Math.round(actRect.top)); + } + // Pro Reihe: max-min der button-tops (sollte ~0 sein wenn aligned) + const result = []; + for (const [row, tops] of Object.entries(byRow)) { + if (tops.length < 2) continue; + result.push({ row: Number(row), spread: Math.max(...tops) - Math.min(...tops), count: tops.length }); + } + return result; + }); + const maxSpread = rows.reduce((m, r) => Math.max(m, r.spread), 0); + process.stdout.write(` [align ${label}] rows=${rows.length} maxButtonTopSpread=${maxSpread}px ${maxSpread <= 2 ? 'OK' : 'MISALIGNED'}\n`); + return maxSpread; + }; + + // 1. Settings tab (alle Inputs/Selects/Checkboxes sichtbar) + await win.evaluate(() => window.showTab('settings')); + await win.waitForTimeout(600); + await shot('01-settings'); + + // 2. Streamer mit vielen VODs + variablen Titel-Laengen + const streamers = ['xqc', 'papaplatte', 'xrohat']; + for (const s of streamers) { + await win.evaluate(() => window.showTab('vods')); + await win.evaluate(async (name) => { await window.selectStreamer(name); }, s); + await win.waitForTimeout(4500); + const count = await win.locator('.vod-card').count(); + process.stdout.write(` ${s}: ${count} vod-cards\n`); + await checkButtonAlignment(s); + await shot(`02-vods-${s}`); + } + + // 3. Queue (mit einem item drin) + await win.evaluate(() => window.showTab('vods')); + const vodCount = await win.locator('.vod-card').count(); + if (vodCount > 0) { + await win.locator('.vod-card .vod-btn.primary').first().click().catch(() => {}); + await win.waitForTimeout(800); + } + await win.evaluate(() => window.showTab('queue')); + await win.waitForTimeout(500); + await shot('03-queue'); + + // 4. Clips / Cutter / Merge / Stats / Archive tabs + for (const tab of ['clips', 'cutter', 'merge', 'stats', 'archive']) { + await win.evaluate((t) => window.showTab(t), tab); + await win.waitForTimeout(600); + await shot(`04-tab-${tab}`); + } + + // 5. Themes durchschalten — auf VODs-Tab mit xqc geladen + await win.evaluate(() => window.showTab('vods')); + await win.evaluate(async () => { await window.selectStreamer('xqc'); }); + await win.waitForTimeout(3500); + for (const theme of ['discord', 'youtube', 'apple', 'light', 'twitch']) { + await win.evaluate((t) => window.changeTheme(t), theme); + await win.waitForTimeout(500); + await shot(`05-theme-${theme}`); + } + + // 6. Settings im Light-Theme (Input-Backgrounds pruefen) + await win.evaluate(() => window.changeTheme('light')); + await win.evaluate(() => window.showTab('settings')); + await win.waitForTimeout(600); + await shot('06-settings-light'); + await win.evaluate(() => window.changeTheme('twitch')); + + await app.close(); + process.stdout.write(`\nDone. PNGs in ${OUT_DIR}\n`); +} + +run().catch((e) => { process.stderr.write(String(e) + '\n'); process.exit(1); }); diff --git a/src/index.html b/src/index.html index f953560..1adeedf 100644 --- a/src/index.html +++ b/src/index.html @@ -330,7 +330,7 @@
-

Video auswahlen um Vorschau zu sehen

+

Video auswaehlen um Vorschau zu sehen

diff --git a/src/renderer-locale-de.ts b/src/renderer-locale-de.ts index 7c0783c..10814a8 100644 --- a/src/renderer-locale-de.ts +++ b/src/renderer-locale-de.ts @@ -20,6 +20,7 @@ const UI_TEXT_DE = { clipsInfoTitle: 'Info', clipsInfoText: 'Unterstutzte Formate:\n- https://clips.twitch.tv/ClipName\n- https://www.twitch.tv/streamer/clip/ClipName\n\nClips werden im Download-Ordner unter "Clips/StreamerName/" gespeichert.', cutterSelectTitle: 'Video auswahlen', + cutterPreviewPlaceholder: 'Video auswahlen um Vorschau zu sehen', cutterBrowse: 'Durchsuchen', mergeTitle: 'Videos zusammenfugen', mergeDesc: 'Wahle mehrere Videos aus, um sie zu einem Video zusammenzufugen. Die Reihenfolge kann geandert werden.', diff --git a/src/renderer-locale-en.ts b/src/renderer-locale-en.ts index 7ba575f..ec960a7 100644 --- a/src/renderer-locale-en.ts +++ b/src/renderer-locale-en.ts @@ -20,6 +20,7 @@ const UI_TEXT_EN = { clipsInfoTitle: 'Info', clipsInfoText: 'Supported formats:\n- https://clips.twitch.tv/ClipName\n- https://www.twitch.tv/streamer/clip/ClipName\n\nClips are saved in your download folder under "Clips/StreamerName/".', cutterSelectTitle: 'Select video', + cutterPreviewPlaceholder: 'Select a video to see a preview', cutterBrowse: 'Browse', mergeTitle: 'Merge videos', mergeDesc: 'Select multiple videos to merge into one file. You can change the order before merging.', diff --git a/src/renderer-texts.ts b/src/renderer-texts.ts index 32faa7b..f52130b 100644 --- a/src/renderer-texts.ts +++ b/src/renderer-texts.ts @@ -114,9 +114,11 @@ function applyLanguageToStaticUI(): void { setText('clipDialogFormatLabel', UI_TEXT.clips.dialogFormatLabel); setText('clipDialogConfirmBtn', UI_TEXT.clips.dialogConfirm); setPlaceholder('clipUrl', UI_TEXT.clips.urlPlaceholder); + setText('btnClip', UI_TEXT.clips.downloadButton); setPlaceholder('clipStartPart', UI_TEXT.clips.startPartPlaceholder); setPlaceholder('cutterFilePath', UI_TEXT.cutter.filePathPlaceholder); setText('cutterSelectTitle', UI_TEXT.static.cutterSelectTitle); + setText('cutterPreviewPlaceholder', UI_TEXT.static.cutterPreviewPlaceholder); setText('cutterBrowseBtn', UI_TEXT.static.cutterBrowse); setText('cutterInfoDurationLabel', UI_TEXT.cutter.infoDuration); setText('cutterInfoResolutionLabel', UI_TEXT.cutter.infoResolution); diff --git a/src/styles.css b/src/styles.css index a8efffd..9d678ea 100644 --- a/src/styles.css +++ b/src/styles.css @@ -693,6 +693,32 @@ body { /* ============================================ GLOBAL TEXT-INPUT POLISH — focus ring + smooth transitions ============================================ */ +/* Basis-Dark-Theme fuer ALLE text-artigen Inputs + Selects, damit nie ein + OS-default-weisses Feld durchkommt wenn ein Input mal nicht in einem + .form-group / .form-stack / .header-search etc. Wrapper sitzt (z.B. + #cutterFilePath in .settings-card > .form-row). Per-Container-Regeln + ueberschreiben Groesse/Padding spaeter — Farbe/Border kommen von hier. */ +input[type="text"], +input[type="search"], +input[type="number"], +input[type="password"], +input[type="email"], +textarea { + background-color: var(--bg-main); + color: var(--text); + border: 1px solid var(--border-soft); + border-radius: 4px; +} + +input[type="text"]::placeholder, +input[type="search"]::placeholder, +input[type="number"]::placeholder, +input[type="password"]::placeholder, +input[type="email"]::placeholder, +textarea::placeholder { + color: var(--text-secondary); +} + input[type="text"], input[type="search"], input[type="number"], @@ -1443,6 +1469,12 @@ select option { cursor: pointer; position: relative; border: 1px solid transparent; + /* Flex-Column + stretch (grid default) macht alle Cards einer Reihe + gleich hoch. Die Actions unten kriegen margin-top:auto und docken + damit am Boden an — egal ob der Titel 1 oder 2 Zeilen hat. Vorher + sass der Button bei 1-Zeilen-Titeln hoeher als bei 2-Zeilen-Nachbarn. */ + display: flex; + flex-direction: column; } .vod-card:hover { @@ -1665,6 +1697,9 @@ select option { padding: 10px 15px 15px; display: flex; gap: 8px; + /* Dockt am Card-Boden an, sodass Trim/Queue-Buttons ueber alle Cards + einer Reihe auf gleicher Hoehe liegen — unabhaengig von Titel-Zeilen. */ + margin-top: auto; } .vod-btn {