0.10.2-19 Frontend

This commit is contained in:
2026-03-17 11:58:04 +00:00
parent 7796587f16
commit 46bd235afc
13 changed files with 227 additions and 100 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-18", "version": "0.10.2-19",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-18", "version": "0.10.2-19",
"dependencies": { "dependencies": {
"archiver": "^7.0.1", "archiver": "^7.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-18", "version": "0.10.2-19",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {
+8
View File
@@ -967,6 +967,14 @@ async function migrateSettingsSchemaMetadata(db) {
VALUES ('auto_eject_after_rip', 'Laufwerk', 'Laufwerk nach Rip auswerfen', 'boolean', 1, 'Wenn aktiv, wird das Laufwerk nach erfolgreichem Abschluss eines Rip-Vorgangs automatisch ausgeworfen (eject).', 'false', '[]', '{}', 45)` VALUES ('auto_eject_after_rip', 'Laufwerk', 'Laufwerk nach Rip auswerfen', 'boolean', 1, 'Wenn aktiv, wird das Laufwerk nach erfolgreichem Abschluss eines Rip-Vorgangs automatisch ausgeworfen (eject).', 'false', '[]', '{}', 45)`
); );
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('auto_eject_after_rip', 'false')`); await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('auto_eject_after_rip', 'false')`);
await db.run(`
CREATE TABLE IF NOT EXISTS user_prefs (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`);
} }
async function getDb() { async function getDb() {
+28
View File
@@ -11,6 +11,8 @@ const userPresetService = require('../services/userPresetService');
const activationBytesService = require('../services/activationBytesService'); const activationBytesService = require('../services/activationBytesService');
const logger = require('../services/logger').child('SETTINGS_ROUTE'); const logger = require('../services/logger').child('SETTINGS_ROUTE');
const { getDb } = require('../db/database');
const router = express.Router(); const router = express.Router();
function isSensitiveSettingKey(key) { function isSensitiveSettingKey(key) {
@@ -400,4 +402,30 @@ router.post(
}) })
); );
// User preferences (UI state, persisted per-installation)
router.get(
'/prefs/:key',
asyncHandler(async (req, res) => {
const { key } = req.params;
const db = await getDb();
const row = await db.get('SELECT value FROM user_prefs WHERE key = ?', [key]);
res.json({ key, value: row ? row.value : null });
})
);
router.put(
'/prefs/:key',
asyncHandler(async (req, res) => {
const { key } = req.params;
const { value } = req.body || {};
const db = await getDb();
await db.run(
`INSERT INTO user_prefs (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
[key, value ?? null]
);
res.json({ key, value: value ?? null });
})
);
module.exports = router; module.exports = router;
+6
View File
@@ -171,6 +171,12 @@ CREATE TABLE IF NOT EXISTS aax_activation_bytes (
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TABLE IF NOT EXISTS user_prefs (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- ============================================================================= -- =============================================================================
-- Default Settings Seed -- Default Settings Seed
-- ============================================================================= -- =============================================================================
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-18", "version": "0.10.2-19",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-18", "version": "0.10.2-19",
"dependencies": { "dependencies": {
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.9.2", "primereact": "^10.9.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-18", "version": "0.10.2-19",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+9
View File
@@ -415,6 +415,15 @@ export const api = {
method: 'POST' method: 'POST'
}); });
}, },
async getPref(key) {
return request(`/settings/prefs/${encodeURIComponent(key)}`);
},
async setPref(key, value) {
return request(`/settings/prefs/${encodeURIComponent(key)}`, {
method: 'PUT',
body: JSON.stringify({ value })
});
},
async updateSetting(key, value) { async updateSetting(key, value) {
const result = await request(`/settings/${encodeURIComponent(key)}`, { const result = await request(`/settings/${encodeURIComponent(key)}`, {
method: 'PUT', method: 'PUT',
+42 -29
View File
@@ -878,8 +878,8 @@ export default function JobDetailDialog({
) : null} ) : null}
<h4>Aktionen</h4> <h4>Aktionen</h4>
<div className="actions-row">
{queueLocked ? ( {queueLocked ? (
<div className="actions-row">
<Button <Button
label="Aus Queue löschen" label="Aus Queue löschen"
icon="pi pi-times" icon="pi pi-times"
@@ -890,30 +890,14 @@ export default function JobDetailDialog({
loading={actionBusy} loading={actionBusy}
disabled={typeof onRemoveFromQueue !== 'function'} disabled={typeof onRemoveFromQueue !== 'function'}
/> />
</div>
) : ( ) : (
<> <div className="actions-section">
{!isCd && !isAudiobook ? ( {!isCd ? (
<Button <div className="actions-group">
label="OMDb neu zuordnen" <div className="actions-group-label"><i className="pi pi-play-circle" /> Kodierung</div>
icon="pi pi-search" {canResumeReady ? (
severity="secondary" <div className="action-item">
size="small"
onClick={() => onAssignOmdb?.(job)}
loading={omdbAssignBusy}
disabled={running || typeof onAssignOmdb !== 'function'}
/>
) : (
<Button
label="MusicBrainz neu zuordnen"
icon="pi pi-search"
severity="secondary"
size="small"
onClick={() => onAssignCdMetadata?.(job)}
loading={cdMetadataAssignBusy}
disabled={running || typeof onAssignCdMetadata !== 'function'}
/>
)}
{!isCd && canResumeReady ? (
<Button <Button
label="Im Dashboard öffnen" label="Im Dashboard öffnen"
icon="pi pi-window-maximize" icon="pi pi-window-maximize"
@@ -923,8 +907,11 @@ export default function JobDetailDialog({
onClick={() => onResumeReady?.(job)} onClick={() => onResumeReady?.(job)}
loading={actionBusy} loading={actionBusy}
/> />
<span className="action-desc">Öffnet den wartenden Job im Dashboard zur Weiterverarbeitung.</span>
</div>
) : null} ) : null}
{!isCd && typeof onRestartEncode === 'function' ? ( {typeof onRestartEncode === 'function' ? (
<div className="action-item">
<Button <Button
label="Encode neu starten" label="Encode neu starten"
icon="pi pi-play" icon="pi pi-play"
@@ -934,8 +921,11 @@ export default function JobDetailDialog({
loading={actionBusy} loading={actionBusy}
disabled={!canRestartEncode} disabled={!canRestartEncode}
/> />
<span className="action-desc">Startet {isAudiobook ? 'FFmpeg' : 'HandBrake'} mit den zuletzt gespeicherten Einstellungen direkt neu ohne Review.</span>
</div>
) : null} ) : null}
{!isCd && !isAudiobook && typeof onRestartReview === 'function' ? ( {!isAudiobook && typeof onRestartReview === 'function' ? (
<div className="action-item">
<Button <Button
label="Review neu starten" label="Review neu starten"
icon="pi pi-refresh" icon="pi pi-refresh"
@@ -946,8 +936,11 @@ export default function JobDetailDialog({
loading={actionBusy} loading={actionBusy}
disabled={!canRestartReview} disabled={!canRestartReview}
/> />
<span className="action-desc">Zeigt den Review-Screen erneut (Spur- und Preset-Auswahl), um Einstellungen vor dem Encode zu ändern.</span>
</div>
) : null} ) : null}
{!isCd && !isAudiobook ? ( {!isAudiobook ? (
<div className="action-item">
<Button <Button
label="RAW neu encodieren" label="RAW neu encodieren"
icon="pi pi-cog" icon="pi pi-cog"
@@ -957,7 +950,15 @@ export default function JobDetailDialog({
loading={reencodeBusy} loading={reencodeBusy}
disabled={!canReencode || typeof onReencode !== 'function'} disabled={!canReencode || typeof onReencode !== 'function'}
/> />
<span className="action-desc">Analysiert die Quelldatei neu (MediaInfo, Playlist-Check) und startet dann den Review. Unterschied zu Review neu starten": der Analyse-Schritt wird komplett wiederholt — sinnvoll wenn sich der RAW-Inhalt geändert hat.</span>
</div>
) : null} ) : null}
</div>
) : null}
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-trash" /> Dateien löschen</div>
<div className="action-item">
<Button <Button
label="RAW löschen" label="RAW löschen"
icon="pi pi-trash" icon="pi pi-trash"
@@ -968,6 +969,9 @@ export default function JobDetailDialog({
loading={actionBusy} loading={actionBusy}
disabled={!job.rawStatus?.exists || typeof onDeleteFiles !== 'function'} disabled={!job.rawStatus?.exists || typeof onDeleteFiles !== 'function'}
/> />
<span className="action-desc">Löscht die Quelldateien nach dem Rip ({isCd ? 'Audio-Rohdaten' : isAudiobook ? 'AAX/MP3-Quelldatei' : 'MKV-Datei von MakeMKV'}).</span>
</div>
<div className="action-item">
<Button <Button
label={isCd ? 'Audio löschen' : (isAudiobook ? 'Ausgabe löschen' : 'Movie löschen')} label={isCd ? 'Audio löschen' : (isAudiobook ? 'Ausgabe löschen' : 'Movie löschen')}
icon="pi pi-trash" icon="pi pi-trash"
@@ -978,6 +982,9 @@ export default function JobDetailDialog({
loading={actionBusy} loading={actionBusy}
disabled={!job.outputStatus?.exists || typeof onDeleteFiles !== 'function'} disabled={!job.outputStatus?.exists || typeof onDeleteFiles !== 'function'}
/> />
<span className="action-desc">Löscht {isCd ? 'die fertig gerippten Audiodateien' : isAudiobook ? 'die fertig konvertierte Audiobook-Ausgabe' : 'die fertig encodierte Filmdatei'}.</span>
</div>
<div className="action-item">
<Button <Button
label="Beides löschen" label="Beides löschen"
icon="pi pi-times" icon="pi pi-times"
@@ -987,6 +994,12 @@ export default function JobDetailDialog({
loading={actionBusy} loading={actionBusy}
disabled={(!job.rawStatus?.exists && !job.outputStatus?.exists) || typeof onDeleteFiles !== 'function'} disabled={(!job.rawStatus?.exists && !job.outputStatus?.exists) || typeof onDeleteFiles !== 'function'}
/> />
<span className="action-desc">Löscht RAW-Quelldateien und Ausgabe gemeinsam.</span>
</div>
</div>
<div className="action-delete-entry">
<span className="action-desc">Entfernt diesen Eintrag aus der Verlaufsliste — Dateien auf der Festplatte bleiben erhalten.</span>
<Button <Button
label="Historieneintrag löschen" label="Historieneintrag löschen"
icon="pi pi-trash" icon="pi pi-trash"
@@ -997,9 +1010,9 @@ export default function JobDetailDialog({
loading={deleteEntryBusy} loading={deleteEntryBusy}
disabled={!canDeleteEntry} disabled={!canDeleteEntry}
/> />
</>
)}
</div> </div>
</div>
)}
<h4>Log</h4> <h4>Log</h4>
{showFinalLog ? ( {showFinalLog ? (
+13 -2
View File
@@ -354,7 +354,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [status, setStatus] = useState(''); const [status, setStatus] = useState('');
const [mediumFilter, setMediumFilter] = useState(''); const [mediumFilter, setMediumFilter] = useState('');
const [layout, setLayout] = useState('list'); const [layout, setLayout] = useState('grid');
const [sortKey, setSortKey] = useState('!start_time'); const [sortKey, setSortKey] = useState('!start_time');
const [sortField, setSortField] = useState('start_time'); const [sortField, setSortField] = useState('start_time');
const [sortOrder, setSortOrder] = useState(-1); const [sortOrder, setSortOrder] = useState(-1);
@@ -432,6 +432,14 @@ export default function HistoryPage({ refreshToken = 0 }) {
} }
}; };
useEffect(() => {
api.getPref('history_layout').then((res) => {
if (res?.value === 'list' || res?.value === 'grid') {
setLayout(res.value);
}
}).catch(() => {});
}, []);
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
load(); load();
@@ -1169,7 +1177,10 @@ export default function HistoryPage({ refreshToken = 0 }) {
<Button label="Neu laden" icon="pi pi-refresh" onClick={load} loading={loading} /> <Button label="Neu laden" icon="pi pi-refresh" onClick={load} loading={loading} />
<div className="history-dv-layout-toggle"> <div className="history-dv-layout-toggle">
<DataViewLayoutOptions layout={layout} onChange={(event) => setLayout(event.value)} /> <DataViewLayoutOptions layout={layout} onChange={(event) => {
setLayout(event.value);
api.setPref('history_layout', event.value).catch(() => {});
}} />
</div> </div>
</div> </div>
); );
+52
View File
@@ -367,6 +367,58 @@ body {
flex-wrap: wrap; flex-wrap: wrap;
} }
.actions-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.actions-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.actions-group-label {
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-color-secondary);
margin-bottom: 0.1rem;
}
.action-item {
display: flex;
align-items: center;
gap: 0.75rem;
}
.action-item .p-button {
flex-shrink: 0;
min-width: 11rem;
}
.action-desc {
font-size: 0.8rem;
color: var(--text-color-secondary);
line-height: 1.35;
}
.action-delete-entry {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
border-top: 1px solid var(--surface-border);
padding-top: 0.75rem;
margin-top: 0.25rem;
}
.action-delete-entry .action-desc {
text-align: right;
}
.dashboard-drive-state { .dashboard-drive-state {
margin: 0.5rem 0; margin: 0.5rem 0;
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster", "name": "ripster",
"version": "0.10.2-18", "version": "0.10.2-19",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster", "name": "ripster",
"version": "0.10.2-18", "version": "0.10.2-19",
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.2" "concurrently": "^9.1.2"
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "ripster", "name": "ripster",
"private": true, "private": true,
"version": "0.10.2-18", "version": "0.10.2-19",
"scripts": { "scripts": {
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"", "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
"dev:backend": "npm run dev --prefix backend", "dev:backend": "npm run dev --prefix backend",