feat(ui): bump thumbnail sizes 1.5x, make Settings fluid, add subscription_count stat
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ from quart import Blueprint, jsonify, request
|
|||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from ..extensions import get_session
|
from ..extensions import get_session
|
||||||
from ..models import AppSetting, ImageRecord, ImportBatch, ImportSettings, ImportTask, Tag
|
from ..models import AppSetting, Artist, ImageRecord, ImportBatch, ImportSettings, ImportTask, Tag
|
||||||
|
|
||||||
settings_bp = Blueprint("settings", __name__, url_prefix="/api")
|
settings_bp = Blueprint("settings", __name__, url_prefix="/api")
|
||||||
|
|
||||||
@@ -118,6 +118,9 @@ async def system_stats():
|
|||||||
storage_bytes = (
|
storage_bytes = (
|
||||||
(await session.execute(select(func.coalesce(func.sum(ImageRecord.size_bytes), 0)))).scalar_one()
|
(await session.execute(select(func.coalesce(func.sum(ImageRecord.size_bytes), 0)))).scalar_one()
|
||||||
)
|
)
|
||||||
|
subscription_count = (await session.execute(
|
||||||
|
select(func.count(Artist.id)).where(Artist.is_subscription.is_(True))
|
||||||
|
)).scalar_one()
|
||||||
|
|
||||||
# Task counts grouped by status
|
# Task counts grouped by status
|
||||||
status_rows = (
|
status_rows = (
|
||||||
@@ -163,6 +166,7 @@ async def system_stats():
|
|||||||
"total_images": total_images,
|
"total_images": total_images,
|
||||||
"total_tags": total_tags,
|
"total_tags": total_tags,
|
||||||
"storage_bytes": storage_bytes,
|
"storage_bytes": storage_bytes,
|
||||||
|
"subscription_count": int(subscription_count),
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"pending": status_counts.get("pending", 0),
|
"pending": status_counts.get("pending", 0),
|
||||||
"queued": status_counts.get("queued", 0),
|
"queued": status_counts.get("queued", 0),
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function imagesForGroup(group) {
|
|||||||
}
|
}
|
||||||
.fc-gallery-grid__items {
|
.fc-gallery-grid__items {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.fc-gallery-grid__sentinel {
|
.fc-gallery-grid__sentinel {
|
||||||
@@ -101,7 +101,7 @@ function imagesForGroup(group) {
|
|||||||
}
|
}
|
||||||
.fc-gallery-grid__skeleton {
|
.fc-gallery-grid__skeleton {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.fc-gallery-grid__skeleton-item {
|
.fc-gallery-grid__skeleton-item {
|
||||||
@@ -124,7 +124,7 @@ function imagesForGroup(group) {
|
|||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.fc-gallery-grid__items,
|
.fc-gallery-grid__items,
|
||||||
.fc-gallery-grid__skeleton {
|
.fc-gallery-grid__skeleton {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-row dense>
|
<v-row dense>
|
||||||
<v-col v-for="card in cards" :key="card.label" cols="12" sm="6" md="4" lg="2">
|
<v-col v-for="card in cards" :key="card.label" cols="12" sm="6" md="4" lg="3" xl="2">
|
||||||
<v-card class="fc-stat">
|
<v-card class="fc-stat">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div class="fc-stat__label text-caption">{{ card.label }}</div>
|
<div class="fc-stat__label text-caption">{{ card.label }}</div>
|
||||||
@@ -33,15 +33,17 @@ const cards = computed(() => {
|
|||||||
{ label: 'Total images', value: '—' },
|
{ label: 'Total images', value: '—' },
|
||||||
{ label: 'Total tags', value: '—' },
|
{ label: 'Total tags', value: '—' },
|
||||||
{ label: 'Storage used', value: '—' },
|
{ label: 'Storage used', value: '—' },
|
||||||
|
{ label: 'Subscriptions', value: '—' },
|
||||||
{ label: 'Pending tasks', value: '—' },
|
{ label: 'Pending tasks', value: '—' },
|
||||||
{ label: 'Failed tasks', value: '—' }
|
{ label: 'Failed tasks', value: '—' }
|
||||||
]
|
]
|
||||||
return [
|
return [
|
||||||
{ label: 'Total images', value: s.total_images.toLocaleString() },
|
{ label: 'Total images', value: s.total_images.toLocaleString() },
|
||||||
{ label: 'Total tags', value: s.total_tags.toLocaleString() },
|
{ label: 'Total tags', value: s.total_tags.toLocaleString() },
|
||||||
{ label: 'Storage used', value: formatBytes(s.storage_bytes) },
|
{ label: 'Storage used', value: formatBytes(s.storage_bytes) },
|
||||||
{ label: 'Pending', value: (s.tasks.pending + s.tasks.queued).toLocaleString() },
|
{ label: 'Subscriptions', value: (s.subscription_count ?? 0).toLocaleString() },
|
||||||
{ label: 'Failed', value: s.tasks.failed.toLocaleString() }
|
{ label: 'Pending', value: (s.tasks.pending + s.tasks.queued).toLocaleString() },
|
||||||
|
{ label: 'Failed', value: s.tasks.failed.toLocaleString() }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ export function distributeIntoColumns(items, columnCount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reactive column count from container width. Breakpoints mirror the
|
// Reactive column count from container width. Breakpoints mirror the
|
||||||
// gallery grid intent: ~260px target column width, min 1 column.
|
// gallery grid intent: ~390px target column width, min 1 column. Bumped
|
||||||
|
// from 260px on 2026-05-23 per dogfood UX feedback (thumbs were too small).
|
||||||
export function columnCountForWidth(width) {
|
export function columnCountForWidth(width) {
|
||||||
if (!width || width < 0) return 1
|
if (!width || width < 0) return 1
|
||||||
return Math.max(1, Math.floor(width / 260))
|
return Math.max(1, Math.floor(width / 390))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePolyMasonry(containerRef) {
|
export function usePolyMasonry(containerRef) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container class="py-6">
|
<v-container fluid class="py-6">
|
||||||
<v-tabs v-model="tab" color="accent" class="mb-4">
|
<v-tabs v-model="tab" color="accent" class="mb-4">
|
||||||
<v-tab value="overview">Overview</v-tab>
|
<v-tab value="overview">Overview</v-tab>
|
||||||
<v-tab value="import">Import</v-tab>
|
<v-tab value="import">Import</v-tab>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ async def test_system_stats_shape(client):
|
|||||||
resp = await client.get("/api/system/stats")
|
resp = await client.get("/api/system/stats")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
body = await resp.get_json()
|
body = await resp.get_json()
|
||||||
for key in ("total_images", "total_tags", "storage_bytes", "tasks", "active_batch"):
|
for key in ("total_images", "total_tags", "storage_bytes", "subscription_count", "tasks", "active_batch"):
|
||||||
assert key in body
|
assert key in body
|
||||||
for status in ("pending", "queued", "processing", "complete", "skipped", "failed"):
|
for status in ("pending", "queued", "processing", "complete", "skipped", "failed"):
|
||||||
assert status in body["tasks"]
|
assert status in body["tasks"]
|
||||||
|
|||||||
Reference in New Issue
Block a user