0.10.2-19 Frontend
This commit is contained in:
Generated
+2
-2
@@ -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,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": {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
|
|||||||
Generated
+2
-2
@@ -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,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": {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2
-2
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user