Skip to content

Commit 9ea1373

Browse files
committed
Fix card layout consistency and add library usage display
- Make download cards use flexbox with actions pinned to bottom for consistent height across the grid - Show video and music library usage side-by-side with cache usage on desktop, stacked on mobile - API returns both cache and library usage in a single request
1 parent b0d2bcf commit 9ea1373

4 files changed

Lines changed: 116 additions & 23 deletions

File tree

src/lib/components/download/DownloadCard.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@
323323
transition: all var(--transition-normal);
324324
flex-shrink: 0;
325325
position: relative;
326+
display: flex;
327+
flex-direction: column;
326328
}
327329
328330
.download-card:hover {
@@ -375,6 +377,9 @@
375377
376378
.content {
377379
padding: var(--spacing-lg);
380+
flex: 1;
381+
display: flex;
382+
flex-direction: column;
378383
}
379384
380385
.header {
@@ -570,6 +575,7 @@
570575
.actions {
571576
display: flex;
572577
gap: var(--spacing-sm);
578+
margin-top: auto;
573579
}
574580
575581
@media (max-width: 768px) {

src/lib/server/services/library.service.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,41 @@ class LibraryService {
243243
};
244244
}
245245

246+
async getLibraryUsage(): Promise<{ video: { usedBytes: string; count: number } | null; music: { usedBytes: string; count: number } | null }> {
247+
const settings = await this.getSettings();
248+
249+
const videoResult = settings.libraryPath ? await prisma.download.aggregate({
250+
where: {
251+
storagePool: 'library',
252+
status: DownloadStatus.COMPLETED,
253+
profile: { audioOnly: false },
254+
},
255+
_sum: { filesize: true },
256+
_count: true,
257+
}) : null;
258+
259+
const musicResult = settings.musicLibraryPath ? await prisma.download.aggregate({
260+
where: {
261+
storagePool: 'library',
262+
status: DownloadStatus.COMPLETED,
263+
profile: { audioOnly: true },
264+
},
265+
_sum: { filesize: true },
266+
_count: true,
267+
}) : null;
268+
269+
return {
270+
video: videoResult ? {
271+
usedBytes: (videoResult._sum.filesize ?? BigInt(0)).toString(),
272+
count: videoResult._count,
273+
} : null,
274+
music: musicResult ? {
275+
usedBytes: (musicResult._sum.filesize ?? BigInt(0)).toString(),
276+
count: musicResult._count,
277+
} : null,
278+
};
279+
}
280+
246281
async clearCache(): Promise<number> {
247282
const candidates = await prisma.download.findMany({
248283
where: {

src/routes/+page.svelte

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
let libraryConfigured = $state(false);
138138
let jellyfinUrl = $state('');
139139
let cacheUsage = $state<{ usedBytes: string; quotaBytes: string; percentage: number } | null>(null);
140+
let libraryUsage = $state<{ video: { usedBytes: string; count: number } | null; music: { usedBytes: string; count: number } | null } | null>(null);
140141
let clearingCache = $state(false);
141142
142143
function buildOptionsFlags(opts: { sponsorblock: boolean; subtitles: boolean; metadata: boolean }, saveToLibrary = false): string[] {
@@ -249,10 +250,12 @@
249250
try {
250251
const res = await fetch('/api/library/usage');
251252
if (res.ok) {
252-
cacheUsage = await res.json();
253+
const data = await res.json();
254+
cacheUsage = data.cache;
255+
libraryUsage = data.library;
253256
}
254257
} catch (e) {
255-
console.error('Failed to load cache usage:', e);
258+
console.error('Failed to load usage:', e);
256259
}
257260
}
258261
@@ -734,25 +737,55 @@
734737
</div>
735738
</div>
736739

737-
{#if cacheUsage}
738-
<div class="cache-usage">
739-
<div class="cache-usage-header">
740-
<div class="cache-usage-left">
741-
<span class="cache-usage-label">Cache Usage</span>
742-
<span class="cache-usage-tooltip" data-tooltip="Downloads are stored in a temporary cache. When the cache fills up, the oldest downloads are automatically removed to free space. Save to Library to keep downloads permanently.">?</span>
740+
{#if cacheUsage || libraryUsage}
741+
<div class="storage-row">
742+
{#if cacheUsage}
743+
<div class="storage-box">
744+
<div class="cache-usage-header">
745+
<div class="cache-usage-left">
746+
<span class="cache-usage-label">Cache</span>
747+
<span class="cache-usage-tooltip" data-tooltip="Downloads are stored in a temporary cache. When the cache fills up, the oldest downloads are automatically removed to free space. Save to Library to keep downloads permanently.">?</span>
748+
</div>
749+
<div class="cache-usage-right">
750+
<span class="cache-usage-value">{formatBytes(cacheUsage.usedBytes)} / {formatBytes(cacheUsage.quotaBytes)}</span>
751+
{#if Number(cacheUsage.usedBytes) > 0}
752+
<button class="btn btn-sm btn-secondary cache-clear-btn" onclick={clearCache} disabled={clearingCache}>
753+
{clearingCache ? 'Clearing...' : 'Clear'}
754+
</button>
755+
{/if}
756+
</div>
757+
</div>
758+
<div class="cache-usage-bar">
759+
<div class="cache-usage-fill" class:warning={cacheUsage.percentage > 80} class:critical={cacheUsage.percentage > 95} style="width: max({cacheUsage.percentage}%, {cacheUsage.percentage > 0 ? '4px' : '0px'})"></div>
760+
</div>
743761
</div>
744-
<div class="cache-usage-right">
745-
<span class="cache-usage-value">{formatBytes(cacheUsage.usedBytes)} / {formatBytes(cacheUsage.quotaBytes)}</span>
746-
{#if Number(cacheUsage.usedBytes) > 0}
747-
<button class="btn btn-sm btn-secondary cache-clear-btn" onclick={clearCache} disabled={clearingCache}>
748-
{clearingCache ? 'Clearing...' : 'Clear'}
749-
</button>
750-
{/if}
762+
{/if}
763+
{#if libraryUsage?.video}
764+
<div class="storage-box">
765+
<div class="cache-usage-header">
766+
<div class="cache-usage-left">
767+
<span class="cache-usage-label">Video Library</span>
768+
</div>
769+
<div class="cache-usage-right">
770+
<span class="cache-usage-value">{formatBytes(libraryUsage.video.usedBytes)}</span>
771+
<span class="storage-count">{libraryUsage.video.count} file{libraryUsage.video.count !== 1 ? 's' : ''}</span>
772+
</div>
773+
</div>
751774
</div>
752-
</div>
753-
<div class="cache-usage-bar">
754-
<div class="cache-usage-fill" class:warning={cacheUsage.percentage > 80} class:critical={cacheUsage.percentage > 95} style="width: max({cacheUsage.percentage}%, {cacheUsage.percentage > 0 ? '4px' : '0px'})"></div>
755-
</div>
775+
{/if}
776+
{#if libraryUsage?.music}
777+
<div class="storage-box">
778+
<div class="cache-usage-header">
779+
<div class="cache-usage-left">
780+
<span class="cache-usage-label">Music Library</span>
781+
</div>
782+
<div class="cache-usage-right">
783+
<span class="cache-usage-value">{formatBytes(libraryUsage.music.usedBytes)}</span>
784+
<span class="storage-count">{libraryUsage.music.count} file{libraryUsage.music.count !== 1 ? 's' : ''}</span>
785+
</div>
786+
</div>
787+
</div>
788+
{/if}
756789
</div>
757790
{/if}
758791

@@ -1933,12 +1966,24 @@
19331966
margin-bottom: var(--spacing-lg);
19341967
}
19351968
1936-
.cache-usage {
1969+
.storage-row {
1970+
display: flex;
1971+
gap: var(--spacing-md);
1972+
margin-bottom: var(--spacing-lg);
1973+
}
1974+
1975+
.storage-box {
19371976
background: var(--bg-secondary);
19381977
border: 1px solid var(--border);
19391978
border-radius: var(--radius-lg);
19401979
padding: var(--spacing-md) var(--spacing-lg);
1941-
margin-bottom: var(--spacing-lg);
1980+
flex: 1;
1981+
min-width: 0;
1982+
}
1983+
1984+
.storage-count {
1985+
font-size: 0.75rem;
1986+
color: var(--text-tertiary);
19421987
}
19431988
19441989
.cache-usage-header {
@@ -2145,6 +2190,10 @@
21452190
width: 100%;
21462191
}
21472192
2193+
.storage-row {
2194+
flex-direction: column;
2195+
}
2196+
21482197
.downloads-layout {
21492198
grid-template-columns: 1fr;
21502199
gap: var(--spacing-lg);

src/routes/api/library/usage/+server.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ export const GET: RequestHandler = async ({ locals }) => {
88
throw error(401, 'Authentication required');
99
}
1010

11-
const usage = await libraryService.getCacheUsage();
12-
return json(usage);
11+
const [cache, library] = await Promise.all([
12+
libraryService.getCacheUsage(),
13+
libraryService.getLibraryUsage(),
14+
]);
15+
return json({ cache, library });
1316
} catch (e: any) {
1417
console.error('Failed to get cache usage:', e);
1518
if (e.status) throw e;

0 commit comments

Comments
 (0)