diff --git a/backend/package-lock.json b/backend/package-lock.json
index eef3cbd..94ca1a1 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ripster-backend",
- "version": "0.10.2-18",
+ "version": "0.10.2-19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster-backend",
- "version": "0.10.2-18",
+ "version": "0.10.2-19",
"dependencies": {
"archiver": "^7.0.1",
"cors": "^2.8.5",
diff --git a/backend/package.json b/backend/package.json
index 870b052..7a41ff7 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "ripster-backend",
- "version": "0.10.2-18",
+ "version": "0.10.2-19",
"private": true,
"type": "commonjs",
"scripts": {
diff --git a/backend/src/db/database.js b/backend/src/db/database.js
index 0477029..7d76cc5 100644
--- a/backend/src/db/database.js
+++ b/backend/src/db/database.js
@@ -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)`
);
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() {
diff --git a/backend/src/routes/settingsRoutes.js b/backend/src/routes/settingsRoutes.js
index 83147ea..d4b4993 100644
--- a/backend/src/routes/settingsRoutes.js
+++ b/backend/src/routes/settingsRoutes.js
@@ -11,6 +11,8 @@ const userPresetService = require('../services/userPresetService');
const activationBytesService = require('../services/activationBytesService');
const logger = require('../services/logger').child('SETTINGS_ROUTE');
+const { getDb } = require('../db/database');
+
const router = express.Router();
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;
diff --git a/db/schema.sql b/db/schema.sql
index 1b8f659..771c983 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -171,6 +171,12 @@ CREATE TABLE IF NOT EXISTS aax_activation_bytes (
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
-- =============================================================================
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 79f01eb..013a69a 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ripster-frontend",
- "version": "0.10.2-18",
+ "version": "0.10.2-19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster-frontend",
- "version": "0.10.2-18",
+ "version": "0.10.2-19",
"dependencies": {
"primeicons": "^7.0.0",
"primereact": "^10.9.2",
diff --git a/frontend/package.json b/frontend/package.json
index 56783e3..78c37bd 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "ripster-frontend",
- "version": "0.10.2-18",
+ "version": "0.10.2-19",
"private": true,
"type": "module",
"scripts": {
diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js
index e3fda71..83e6983 100644
--- a/frontend/src/api/client.js
+++ b/frontend/src/api/client.js
@@ -415,6 +415,15 @@ export const api = {
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) {
const result = await request(`/settings/${encodeURIComponent(key)}`, {
method: 'PUT',
diff --git a/frontend/src/components/JobDetailDialog.jsx b/frontend/src/components/JobDetailDialog.jsx
index db01dd0..ec65c70 100644
--- a/frontend/src/components/JobDetailDialog.jsx
+++ b/frontend/src/components/JobDetailDialog.jsx
@@ -878,8 +878,8 @@ export default function JobDetailDialog({
) : null}
Aktionen
-
- {queueLocked ? (
+ {queueLocked ? (
+
- ) : (
- <>
- {!isCd && !isAudiobook ? (
+
+ ) : (
+
+ {!isCd ? (
+
+
Kodierung
+ {canResumeReady ? (
+
+
+ ) : null}
+ {typeof onRestartEncode === 'function' ? (
+
+
+ ) : null}
+ {!isAudiobook && typeof onRestartReview === 'function' ? (
+
+
+ ) : null}
+ {!isAudiobook ? (
+
+
+ ) : null}
+
+ ) : null}
+
+
+
Dateien löschen
+
+
onRestartEncode?.(job)}
- loading={actionBusy}
- disabled={!canRestartEncode}
- />
- ) : null}
- {!isCd && !isAudiobook && typeof onRestartReview === 'function' ? (
- onRestartReview?.(job)}
+ onClick={() => onDeleteFiles?.(job, 'movie')}
loading={actionBusy}
- disabled={!canRestartReview}
+ disabled={!job.outputStatus?.exists || typeof onDeleteFiles !== 'function'}
/>
- ) : null}
- {!isCd && !isAudiobook ? (
+ Löscht {isCd ? 'die fertig gerippten Audiodateien' : isAudiobook ? 'die fertig konvertierte Audiobook-Ausgabe' : 'die fertig encodierte Filmdatei'}.
+
+
onReencode?.(job)}
- loading={reencodeBusy}
- disabled={!canReencode || typeof onReencode !== 'function'}
+ onClick={() => onDeleteFiles?.(job, 'both')}
+ loading={actionBusy}
+ disabled={(!job.rawStatus?.exists && !job.outputStatus?.exists) || typeof onDeleteFiles !== 'function'}
/>
- ) : null}
- onDeleteFiles?.(job, 'raw')}
- loading={actionBusy}
- disabled={!job.rawStatus?.exists || typeof onDeleteFiles !== 'function'}
- />
- onDeleteFiles?.(job, 'movie')}
- loading={actionBusy}
- disabled={!job.outputStatus?.exists || typeof onDeleteFiles !== 'function'}
- />
- onDeleteFiles?.(job, 'both')}
- loading={actionBusy}
- disabled={(!job.rawStatus?.exists && !job.outputStatus?.exists) || typeof onDeleteFiles !== 'function'}
- />
+ Löscht RAW-Quelldateien und Ausgabe gemeinsam.
+
+
+
+
+ Entfernt diesen Eintrag aus der Verlaufsliste — Dateien auf der Festplatte bleiben erhalten.
- >
- )}
-
+
+
+ )}
Log
{showFinalLog ? (
diff --git a/frontend/src/pages/HistoryPage.jsx b/frontend/src/pages/HistoryPage.jsx
index 4c02a32..0f55247 100644
--- a/frontend/src/pages/HistoryPage.jsx
+++ b/frontend/src/pages/HistoryPage.jsx
@@ -354,7 +354,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
const [search, setSearch] = useState('');
const [status, setStatus] = useState('');
const [mediumFilter, setMediumFilter] = useState('');
- const [layout, setLayout] = useState('list');
+ const [layout, setLayout] = useState('grid');
const [sortKey, setSortKey] = useState('!start_time');
const [sortField, setSortField] = useState('start_time');
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(() => {
const timer = setTimeout(() => {
load();
@@ -1169,7 +1177,10 @@ export default function HistoryPage({ refreshToken = 0 }) {
- setLayout(event.value)} />
+ {
+ setLayout(event.value);
+ api.setPref('history_layout', event.value).catch(() => {});
+ }} />
);
diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css
index 10463d0..f187474 100644
--- a/frontend/src/styles/app.css
+++ b/frontend/src/styles/app.css
@@ -367,6 +367,58 @@ body {
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 {
margin: 0.5rem 0;
}
diff --git a/package-lock.json b/package-lock.json
index 4d109a5..b349ecc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ripster",
- "version": "0.10.2-18",
+ "version": "0.10.2-19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster",
- "version": "0.10.2-18",
+ "version": "0.10.2-19",
"devDependencies": {
"concurrently": "^9.1.2"
}
diff --git a/package.json b/package.json
index 218bae3..9910b63 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "ripster",
"private": true,
- "version": "0.10.2-18",
+ "version": "0.10.2-19",
"scripts": {
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
"dev:backend": "npm run dev --prefix backend",