0.11.0-5 multi lw
This commit is contained in:
@@ -16,7 +16,13 @@
|
||||
"Bash(node --check /home/michael/ripster/backend/src/services/settingsService.js)",
|
||||
"Bash(node --check /home/michael/ripster/backend/src/services/pipelineService.js)",
|
||||
"Bash(node --check /home/michael/ripster/backend/src/db/database.js)",
|
||||
"Bash(node --check /home/michael/ripster/backend/src/routes/settingsRoutes.js)"
|
||||
"Bash(node --check /home/michael/ripster/backend/src/routes/settingsRoutes.js)",
|
||||
"Bash(sqlite3 /home/michael/ripster/backend/ripster.db \"SELECT key, value FROM settings_values WHERE key LIKE ''%drive%'' OR key LIKE ''%disc%'' ORDER BY key;\")",
|
||||
"Bash(sqlite3 /home/michael/ripster/backend/data/ripster.db \"SELECT key, value FROM settings_values WHERE key LIKE ''%drive%'' OR key LIKE ''%disc%'' ORDER BY key;\")",
|
||||
"Bash(lsblk -J -o NAME,PATH,TYPE,MOUNTPOINT,FSTYPE,LABEL,MODEL)",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); [print\\(e\\) for e in d.get\\(''''blockdevices'''',[]\\)]\")",
|
||||
"Bash(lsblk -o NAME,TYPE,MODEL)",
|
||||
"Bash(node --check /home/michael/ripster/backend/src/routes/pipelineRoutes.js)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.11.0-4",
|
||||
"version": "0.11.0-5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-backend",
|
||||
"version": "0.11.0-4",
|
||||
"version": "0.11.0-5",
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.11.0-4",
|
||||
"version": "0.11.0-5",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
||||
@@ -54,6 +54,21 @@ router.post(
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/rescan-drive',
|
||||
asyncHandler(async (req, res) => {
|
||||
const devicePath = String(req.body?.devicePath || '').trim();
|
||||
logger.info('post:rescan-drive', { reqId: req.reqId, devicePath });
|
||||
if (!devicePath) {
|
||||
const err = new Error('devicePath ist erforderlich');
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
const result = await diskDetectionService.rescanDriveAndEmit(devicePath);
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/omdb/search',
|
||||
asyncHandler(async (req, res) => {
|
||||
|
||||
@@ -464,7 +464,65 @@ class DiskDetectionService extends EventEmitter {
|
||||
return results.filter(Boolean);
|
||||
}
|
||||
|
||||
return this.detectAllAuto();
|
||||
// Auto mode: scan all ROM drives via lsblk
|
||||
const autoResults = await this.detectAllAuto();
|
||||
|
||||
// Fallback: if lsblk found no ROM drives but drive_device is configured, try it directly
|
||||
if (autoResults.length === 0) {
|
||||
const legacy = String(settingsMap.drive_device || '').trim();
|
||||
if (legacy) {
|
||||
logger.debug('detect:auto:lsblk-empty-fallback-explicit', { legacy });
|
||||
const legacyResult = await this.detectExplicit(legacy);
|
||||
if (legacyResult) {
|
||||
return [legacyResult];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return autoResults;
|
||||
}
|
||||
|
||||
// Rescan a single specific drive and emit the appropriate event
|
||||
async rescanDriveAndEmit(devicePath) {
|
||||
const normalized = this.normalizeDevicePath(devicePath);
|
||||
if (!normalized) {
|
||||
return { present: false, emitted: 'none', device: null };
|
||||
}
|
||||
try {
|
||||
logger.info('rescan-drive:requested', { devicePath: normalized });
|
||||
const detected = await this.detectExplicit(normalized);
|
||||
const previouslyTracked = this.detectedDiscs.has(normalized);
|
||||
|
||||
if (detected) {
|
||||
const previous = this.detectedDiscs.get(normalized);
|
||||
const changed = previous ? buildSignature(previous) !== buildSignature(detected) : false;
|
||||
this.detectedDiscs.set(normalized, detected);
|
||||
this.lastDetected = detected;
|
||||
this.lastPresent = true;
|
||||
logger.info('rescan-drive:inserted', { devicePath: normalized, changed });
|
||||
this.emit('discInserted', detected);
|
||||
return { present: true, emitted: 'discInserted', device: detected };
|
||||
}
|
||||
|
||||
// No disc found
|
||||
if (previouslyTracked) {
|
||||
const old = this.detectedDiscs.get(normalized);
|
||||
this.detectedDiscs.delete(normalized);
|
||||
if (this.detectedDiscs.size === 0) {
|
||||
this.lastDetected = null;
|
||||
this.lastPresent = false;
|
||||
}
|
||||
logger.info('rescan-drive:removed', { devicePath: normalized });
|
||||
this.emit('discRemoved', old || { path: normalized });
|
||||
return { present: false, emitted: 'discRemoved', device: null };
|
||||
}
|
||||
|
||||
logger.info('rescan-drive:empty', { devicePath: normalized });
|
||||
return { present: false, emitted: 'none', device: null };
|
||||
} catch (error) {
|
||||
logger.error('rescan-drive:error', { devicePath: normalized, error: errorToMeta(error) });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-drive: tracks per-device state and emits insert/remove events for each
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.11.0-4",
|
||||
"version": "0.11.0-5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.11.0-4",
|
||||
"version": "0.11.0-5",
|
||||
"dependencies": {
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.11.0-4",
|
||||
"version": "0.11.0-5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -489,6 +489,13 @@ export const api = {
|
||||
afterMutationInvalidate(['/history', '/pipeline/queue']);
|
||||
return result;
|
||||
},
|
||||
async rescanDrive(devicePath) {
|
||||
const result = await request('/pipeline/rescan-drive', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ devicePath })
|
||||
});
|
||||
return result;
|
||||
},
|
||||
searchOmdb(q) {
|
||||
return request(`/pipeline/omdb/search?q=${encodeURIComponent(q)}`);
|
||||
},
|
||||
|
||||
@@ -870,6 +870,8 @@ export default function DashboardPage({
|
||||
const [activationBytesBusy, setActivationBytesBusy] = useState(false);
|
||||
const [pendingActivationJobIds, setPendingActivationJobIds] = useState(() => new Set());
|
||||
const [cpuCoresExpanded, setCpuCoresExpanded] = useState(false);
|
||||
const [knownDrives, setKnownDrives] = useState([]);
|
||||
const [driveRescanBusy, setDriveRescanBusy] = useState(() => new Set());
|
||||
const [expandedQueueScriptKeys, setExpandedQueueScriptKeys] = useState(() => new Set());
|
||||
const [queueCatalog, setQueueCatalog] = useState({ scripts: [], chains: [] });
|
||||
const [insertWaitSeconds, setInsertWaitSeconds] = useState(30);
|
||||
@@ -1062,6 +1064,12 @@ export default function DashboardPage({
|
||||
void loadDashboardJobs();
|
||||
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, jobsRefreshToken]);
|
||||
|
||||
useEffect(() => {
|
||||
api.getDetectedDrives()
|
||||
.then((res) => setKnownDrives(Array.isArray(res?.drives) ? res.drives : []))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const requestedJobId = normalizeJobId(pendingExpandedJobId);
|
||||
if (!requestedJobId) {
|
||||
@@ -1327,20 +1335,29 @@ export default function DashboardPage({
|
||||
}
|
||||
};
|
||||
|
||||
const refreshKnownDrives = () => {
|
||||
api.getDetectedDrives()
|
||||
.then((res) => setKnownDrives(Array.isArray(res?.drives) ? res.drives : []))
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const handleRescan = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
const response = await api.rescanDisc();
|
||||
refreshKnownDrives();
|
||||
const allDetected = response?.result?.allDetected || [];
|
||||
const emitted = response?.result?.emitted || 'none';
|
||||
const count = allDetected.length;
|
||||
toastRef.current?.show({
|
||||
severity: emitted === 'discInserted' ? 'success' : 'info',
|
||||
summary: 'Laufwerk neu gelesen',
|
||||
detail:
|
||||
emitted === 'discInserted'
|
||||
? 'Disk-Event wurde neu ausgelöst.'
|
||||
: 'Kein Medium erkannt.',
|
||||
severity: count > 0 ? 'success' : 'info',
|
||||
summary: 'Laufwerke neu gelesen',
|
||||
detail: count > 0
|
||||
? `${count} Medium${count > 1 ? 'en' : ''} erkannt.`
|
||||
: 'Kein Medium erkannt.',
|
||||
life: 2800
|
||||
});
|
||||
void emitted; // used implicitly via count
|
||||
await refreshPipeline();
|
||||
await loadDashboardJobs();
|
||||
} catch (error) {
|
||||
@@ -1350,6 +1367,32 @@ export default function DashboardPage({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRescanDrive = async (devicePath) => {
|
||||
setDriveRescanBusy((prev) => new Set([...prev, devicePath]));
|
||||
try {
|
||||
const response = await api.rescanDrive(devicePath);
|
||||
refreshKnownDrives();
|
||||
const emitted = response?.result?.emitted || 'none';
|
||||
toastRef.current?.show({
|
||||
severity: emitted === 'discInserted' ? 'success' : 'info',
|
||||
summary: devicePath,
|
||||
detail: emitted === 'discInserted'
|
||||
? 'Medium erkannt.'
|
||||
: emitted === 'discRemoved' ? 'Medium entfernt.' : 'Leer.',
|
||||
life: 2500
|
||||
});
|
||||
await refreshPipeline();
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} finally {
|
||||
setDriveRescanBusy((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(devicePath);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = async (jobId = null, jobState = null) => {
|
||||
const cancelledJobId = normalizeJobId(jobId) || currentPipelineJobId;
|
||||
const cancelledJob = dashboardJobs.find((item) => normalizeJobId(item?.id) === cancelledJobId) || null;
|
||||
@@ -1987,6 +2030,23 @@ export default function DashboardPage({
|
||||
const contextDevice = pipeline?.context?.device;
|
||||
const device = lastNonCdDiscEvent || (contextDevice?.mediaProfile !== 'cd' ? contextDevice : null);
|
||||
const cdDriveEntries = Object.entries(pipeline?.cdDrives || {});
|
||||
|
||||
// Unified list of all known drives: merge lsblk drives + CD state + non-CD pipeline state
|
||||
const allDrives = useMemo(() => {
|
||||
const driveMap = new Map();
|
||||
for (const d of knownDrives) {
|
||||
driveMap.set(d.path, { path: d.path, model: d.model, discArg: d.discArg });
|
||||
}
|
||||
for (const [drivePath, drive] of Object.entries(pipeline?.cdDrives || {})) {
|
||||
const base = driveMap.get(drivePath) || { path: drivePath };
|
||||
driveMap.set(drivePath, { ...base, cdDrive: drive });
|
||||
}
|
||||
if (device?.path) {
|
||||
const base = driveMap.get(device.path) || { path: device.path, model: device.model };
|
||||
driveMap.set(device.path, { ...base, pipelineDevice: device, pipelineState: state });
|
||||
}
|
||||
return Array.from(driveMap.values()).sort((a, b) => (a.path || '').localeCompare(b.path || ''));
|
||||
}, [knownDrives, pipeline?.cdDrives, device, state]);
|
||||
const isDriveActive = driveActiveStates.includes(state);
|
||||
const canRescan = !isDriveActive;
|
||||
const canReanalyze = !isDriveActive && (state === 'ENCODING' ? Boolean(device) : !processingStates.includes(state));
|
||||
@@ -2229,9 +2289,10 @@ export default function DashboardPage({
|
||||
</Card>
|
||||
|
||||
<Card title="Disk-Information">
|
||||
{/* Global action buttons (non-CD / DVD / Bluray) */}
|
||||
<div className="actions-row">
|
||||
<Button
|
||||
label="Laufwerk neu lesen"
|
||||
label="Alle neu lesen"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
onClick={handleRescan}
|
||||
@@ -2254,106 +2315,125 @@ export default function DashboardPage({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Per-drive CD sections */}
|
||||
{cdDriveEntries.length > 0 && (
|
||||
<div className="cd-drives-section">
|
||||
{cdDriveEntries.map(([drivePath, drive]) => {
|
||||
const driveState = String(drive?.state || '').trim().toUpperCase();
|
||||
const driveDevice = drive?.device || {};
|
||||
const driveJobId = normalizeJobId(drive?.jobId);
|
||||
const driveCtx = drive?.context && typeof drive.context === 'object' ? drive.context : {};
|
||||
const cdStateSeverity = driveState === 'FINISHED' ? 'success'
|
||||
: driveState === 'ERROR' || driveState === 'CANCELLED' ? 'danger'
|
||||
: ['CD_RIPPING', 'CD_ENCODING'].includes(driveState) ? 'warning'
|
||||
: 'info';
|
||||
const driveProgress = Number(drive?.progress ?? 0);
|
||||
const driveStatusText = drive?.statusText || null;
|
||||
const isActiveRip = ['CD_RIPPING', 'CD_ENCODING', 'CD_ANALYZING'].includes(driveState);
|
||||
{/* Compact per-drive list */}
|
||||
{allDrives.length > 0 ? (
|
||||
<div className="drive-list">
|
||||
{allDrives.map((drv) => {
|
||||
const drivePath = drv.path;
|
||||
const cdDrive = drv.cdDrive;
|
||||
const pipelineDevice = drv.pipelineDevice;
|
||||
const isRescanBusy = driveRescanBusy.has(drivePath);
|
||||
|
||||
// Determine display state
|
||||
let stateLabel = null;
|
||||
let stateSeverity = 'secondary';
|
||||
let discInfo = null;
|
||||
let showProgress = false;
|
||||
let progressValue = 0;
|
||||
let statusText = null;
|
||||
|
||||
if (cdDrive) {
|
||||
const s = String(cdDrive.state || '').toUpperCase();
|
||||
stateLabel = s;
|
||||
stateSeverity = s === 'FINISHED' ? 'success'
|
||||
: s === 'ERROR' || s === 'CANCELLED' ? 'danger'
|
||||
: ['CD_RIPPING', 'CD_ENCODING'].includes(s) ? 'warning'
|
||||
: 'info';
|
||||
const cd = cdDrive.device || {};
|
||||
discInfo = cd.discLabel || cd.model || null;
|
||||
showProgress = ['CD_RIPPING', 'CD_ENCODING', 'CD_ANALYZING'].includes(s);
|
||||
progressValue = Number(cdDrive.progress ?? 0);
|
||||
statusText = cdDrive.statusText || null;
|
||||
} else if (pipelineDevice) {
|
||||
const s = String(drv.pipelineState || '').toUpperCase();
|
||||
stateLabel = s || 'DISC_DETECTED';
|
||||
stateSeverity = s === 'FINISHED' ? 'success'
|
||||
: s === 'ERROR' ? 'danger'
|
||||
: processingStates.includes(s) ? 'warning'
|
||||
: 'info';
|
||||
discInfo = pipelineDevice.discLabel || pipelineDevice.label || null;
|
||||
} else {
|
||||
stateLabel = 'LEER';
|
||||
stateSeverity = 'secondary';
|
||||
}
|
||||
|
||||
const driveJobId = normalizeJobId(cdDrive?.jobId);
|
||||
const driveCtx = cdDrive?.context && typeof cdDrive.context === 'object' ? cdDrive.context : {};
|
||||
const cdState = cdDrive ? String(cdDrive.state || '').toUpperCase() : null;
|
||||
|
||||
return (
|
||||
<div key={drivePath} className="cd-drive-item">
|
||||
<div className="cd-drive-header">
|
||||
<code className="cd-drive-path">{drivePath}</code>
|
||||
<Tag value={driveState} severity={cdStateSeverity} />
|
||||
</div>
|
||||
{(driveDevice.discLabel || driveDevice.model) && (
|
||||
<div className="cd-drive-disc-label">
|
||||
{driveDevice.discLabel || driveDevice.model}
|
||||
<div key={drivePath} className="drive-list-item">
|
||||
<div className="drive-list-row">
|
||||
<div className="drive-list-info">
|
||||
<code className="drive-list-path">{drivePath}</code>
|
||||
{drv.model && <span className="drive-list-model">{drv.model}</span>}
|
||||
{drv.discArg && <span className="drive-list-disc-arg">{drv.discArg}</span>}
|
||||
</div>
|
||||
)}
|
||||
{isActiveRip && (
|
||||
<ProgressBar value={driveProgress} style={{ height: '6px', margin: '0.2rem 0' }} showValue={false} />
|
||||
)}
|
||||
{isActiveRip && driveStatusText && (
|
||||
<div className="cd-drive-status-text">{driveStatusText}</div>
|
||||
)}
|
||||
<div className="cd-drive-actions">
|
||||
{driveState === 'DISC_DETECTED' && (
|
||||
<div className="drive-list-state">
|
||||
{stateLabel && <Tag value={stateLabel} severity={stateSeverity} />}
|
||||
</div>
|
||||
<div className="drive-list-actions">
|
||||
<Button
|
||||
label="CD analysieren"
|
||||
icon="pi pi-search"
|
||||
size="small"
|
||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||
loading={busy}
|
||||
disabled={busy}
|
||||
/>
|
||||
)}
|
||||
{driveState === 'CD_METADATA_SELECTION' && (
|
||||
<Button
|
||||
label="Metadaten auswählen"
|
||||
icon="pi pi-list"
|
||||
size="small"
|
||||
severity="info"
|
||||
onClick={() => {
|
||||
setCdMetadataDialogContext({ ...driveCtx, jobId: driveJobId });
|
||||
setCdMetadataDialogVisible(true);
|
||||
}}
|
||||
disabled={!driveJobId}
|
||||
/>
|
||||
)}
|
||||
{(driveState === 'ERROR' || driveState === 'CANCELLED') && driveJobId && (
|
||||
<Button
|
||||
label="Erneut analysieren"
|
||||
icon="pi pi-refresh"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="warning"
|
||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||
loading={busy}
|
||||
disabled={busy}
|
||||
severity="secondary"
|
||||
loading={isRescanBusy}
|
||||
disabled={isRescanBusy || isDriveActive}
|
||||
onClick={() => handleRescanDrive(drivePath)}
|
||||
title="Laufwerk neu lesen"
|
||||
aria-label="Laufwerk neu lesen"
|
||||
/>
|
||||
)}
|
||||
{cdState === 'DISC_DETECTED' && (
|
||||
<Button
|
||||
label="Analysieren"
|
||||
icon="pi pi-search"
|
||||
size="small"
|
||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||
loading={busy}
|
||||
disabled={busy}
|
||||
/>
|
||||
)}
|
||||
{cdState === 'CD_METADATA_SELECTION' && (
|
||||
<Button
|
||||
label="Metadaten"
|
||||
icon="pi pi-list"
|
||||
size="small"
|
||||
severity="info"
|
||||
onClick={() => {
|
||||
setCdMetadataDialogContext({ ...driveCtx, jobId: driveJobId });
|
||||
setCdMetadataDialogVisible(true);
|
||||
}}
|
||||
disabled={!driveJobId}
|
||||
/>
|
||||
)}
|
||||
{(cdState === 'ERROR' || cdState === 'CANCELLED') && driveJobId && (
|
||||
<Button
|
||||
label="Retry"
|
||||
icon="pi pi-replay"
|
||||
size="small"
|
||||
severity="warning"
|
||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||
loading={busy}
|
||||
disabled={busy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{discInfo && <div className="drive-list-disc-label">{discInfo}</div>}
|
||||
{showProgress && (
|
||||
<ProgressBar value={progressValue} style={{ height: '5px', margin: '0.2rem 0 0' }} showValue={false} />
|
||||
)}
|
||||
{showProgress && statusText && (
|
||||
<div className="drive-list-status-text">{statusText}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Non-CD / legacy single-drive display */}
|
||||
<div className="dashboard-drive-state">
|
||||
{device
|
||||
? <Tag value="Disk eingelegt" severity="success" icon="pi pi-circle-fill" />
|
||||
: <Tag value="Laufwerk leer" severity="secondary" icon="pi pi-circle" />}
|
||||
</div>
|
||||
{device ? (
|
||||
<div className="device-meta">
|
||||
<div>
|
||||
<strong>Pfad:</strong> {device.path || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Modell:</strong> {device.model || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Disk-Label:</strong> {device.discLabel || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Laufwerks-Label:</strong> {device.label || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Mount:</strong> {device.mountpoint || '-'}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
cdDriveEntries.length === 0 ? <p>Aktuell keine Disk erkannt.</p> : null
|
||||
<p className="drive-list-empty">Keine Laufwerke erkannt.</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -1802,6 +1802,90 @@ body {
|
||||
}
|
||||
|
||||
/* Per-drive CD sections in Dashboard */
|
||||
/* Unified per-drive list (Disk-Information card) */
|
||||
.drive-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.drive-list-empty {
|
||||
font-size: 0.85rem;
|
||||
color: var(--rip-muted);
|
||||
margin: 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.drive-list-item {
|
||||
padding: 0.4rem 0.6rem;
|
||||
border: 1px solid var(--rip-border);
|
||||
border-radius: 0.4rem;
|
||||
background: var(--rip-panel-soft);
|
||||
}
|
||||
|
||||
.drive-list-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.drive-list-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.drive-list-path {
|
||||
font-size: 0.8rem;
|
||||
color: var(--rip-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.drive-list-model {
|
||||
font-size: 0.75rem;
|
||||
color: var(--rip-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.drive-list-disc-arg {
|
||||
font-size: 0.7rem;
|
||||
color: var(--rip-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.drive-list-state {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.drive-list-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.drive-list-disc-label {
|
||||
font-size: 0.82rem;
|
||||
margin-top: 0.2rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.drive-list-status-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--rip-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Legacy per-CD-drive section (kept for compat) */
|
||||
.cd-drives-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"version": "0.11.0-4",
|
||||
"version": "0.11.0-5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster",
|
||||
"version": "0.11.0-4",
|
||||
"version": "0.11.0-5",
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"private": true,
|
||||
"version": "0.11.0-4",
|
||||
"version": "0.11.0-5",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||
"dev:backend": "npm run dev --prefix backend",
|
||||
|
||||
Reference in New Issue
Block a user