release: 5.0.13 — VOD-Card Button-Alignment + UI-Polish-Pass
Hauptfix (User-Report): in der VOD-Grid sass der Trim/Queue-Button bei Cards mit 1-zeiligem Titel hoeher als bei 2-zeiligen Nachbarn. Ursache: .vod-card war ein Block, Buttons flossen mit dem Content. Grid streckt zwar alle Cards einer Reihe gleich hoch, aber der Leerraum landete unten. Fix: .vod-card = flex column, .vod-actions = margin-top:auto -> Buttons docken am Boden an. Verifiziert per Playwright ueber xqc (16 Reihen), papaplatte (6), xrohat (12): maxButtonTopSpread = 0px in allen. Weitere Funde aus dem Screenshot-Pass (scripts/ui-screenshot.js): - Globale Basis-Dark-Theme-Regel fuer alle text-Inputs + textarea, damit bare Inputs ohne .form-group/.form-stack Wrapper nie OS-weiss durchkommen (#cutterFilePath war weiss im Dark-Theme). - Cutter-Preview-Placeholder 'Video auswahlen um Vorschau zu sehen' war hardcoded Deutsch ohne id -> id + Locale-Key + Wiring (zeigt jetzt 'Select a video to see a preview' im EN-Mode). - Clips-Button '#btnClip' wurde nie lokalisiert (zeigte immer 'Clip herunterladen') -> setText-Wiring ergaenzt, nutzt existierenden clips.downloadButton key. scripts/ui-screenshot.js: neues Harness das die App startet, durch Tabs/Streamer/Themes navigiert, Screenshots macht + Button-Alignment programmatisch misst. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7bc7ef84a2
commit
951158fe5a
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
119
scripts/ui-screenshot.js
Normal file
119
scripts/ui-screenshot.js
Normal file
@ -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); });
|
||||
@ -330,7 +330,7 @@
|
||||
<div class="video-preview" id="cutterPreview">
|
||||
<div class="placeholder">
|
||||
<svg aria-hidden="true" width="64" height="64" viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 8l7 4-7 4V8z"/></svg>
|
||||
<p>Video auswahlen um Vorschau zu sehen</p>
|
||||
<p id="cutterPreviewPlaceholder">Video auswaehlen um Vorschau zu sehen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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.',
|
||||
|
||||
@ -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.',
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user