Merge pull request 'v26.05.23.3: dogfood UX polish + accurate active-batch stats' (#5) from dev into main
This commit was merged in pull request #5.
This commit is contained in:
@@ -141,11 +141,23 @@ async def system_stats():
|
||||
).all()
|
||||
integrity_counts = {row[0]: row[1] for row in integrity_rows}
|
||||
|
||||
# Active batch (most recent running)
|
||||
# Active batch = running batch that still has outstanding work.
|
||||
# Plain "most recent running" picks a freshly-created scan that
|
||||
# enqueued zero new files and hides the older batch that's
|
||||
# actually being processed; the EXISTS clause filters those
|
||||
# empty batches out.
|
||||
active_batch_row = (
|
||||
await session.execute(
|
||||
select(ImportBatch)
|
||||
.where(ImportBatch.status == "running")
|
||||
.where(
|
||||
ImportBatch.status == "running",
|
||||
select(ImportTask.id)
|
||||
.where(
|
||||
ImportTask.batch_id == ImportBatch.id,
|
||||
ImportTask.status.in_(["pending", "queued", "processing"]),
|
||||
)
|
||||
.exists(),
|
||||
)
|
||||
.order_by(ImportBatch.started_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
@@ -95,6 +95,15 @@ def scan_directory(self, triggered_by: str = "manual",
|
||||
files_seen += 1
|
||||
|
||||
batch.total_files = files_seen
|
||||
# If the walk enqueued nothing (every file was already on a
|
||||
# non-failed ImportTask from a prior scan), there's no
|
||||
# import_media_file message that would ever flip this batch to
|
||||
# 'complete' — finalize it now so the active-batch query in
|
||||
# /api/system/stats doesn't get stuck reporting all-zero
|
||||
# counters from an empty scan.
|
||||
if files_seen == 0:
|
||||
batch.status = "complete"
|
||||
batch.finished_at = datetime.now(UTC)
|
||||
session.commit()
|
||||
|
||||
# Now enqueue import_media_file for each pending task.
|
||||
|
||||
@@ -55,7 +55,13 @@ const health = computed(() => {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
/* Mirrored side columns (1fr / auto / 1fr) keep the link block dead-
|
||||
centered no matter what the teleport-slot on the right is rendering
|
||||
for the active view (Gallery: Select, Showcase: Shuffle, others: ∅).
|
||||
With a plain flex layout the links shifted left as soon as actions
|
||||
appeared. */
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
@@ -87,7 +93,6 @@ const health = computed(() => {
|
||||
}
|
||||
|
||||
.fc-links {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
@@ -118,6 +123,7 @@ const health = computed(() => {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
justify-self: start;
|
||||
}
|
||||
.fc-health {
|
||||
display: flex;
|
||||
@@ -129,5 +135,6 @@ const health = computed(() => {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
justify-self: end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<div
|
||||
ref="canvasEl"
|
||||
class="fc-canvas"
|
||||
:class="{ 'fc-canvas--zoomed': panZoom.state.scale > 1 }"
|
||||
@wheel="panZoom.handlers.onWheel"
|
||||
@pointerdown="panZoom.handlers.onPointerDown"
|
||||
@pointermove="panZoom.handlers.onPointerMove"
|
||||
@pointerup="panZoom.handlers.onPointerUp"
|
||||
@click="panZoom.handlers.onClick"
|
||||
@click="onCanvasClick"
|
||||
>
|
||||
<img
|
||||
ref="imgEl"
|
||||
:src="src" :alt="alt"
|
||||
:style="{
|
||||
transform: `translate(${panZoom.state.x}px, ${panZoom.state.y}px) scale(${panZoom.state.scale})`
|
||||
@@ -19,21 +21,51 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { usePanZoom } from '../../composables/usePanZoom.js'
|
||||
|
||||
const props = defineProps({ src: String, alt: String })
|
||||
const emit = defineEmits(['close-request'])
|
||||
const panZoom = usePanZoom()
|
||||
|
||||
const canvasEl = ref(null)
|
||||
const imgEl = ref(null)
|
||||
|
||||
// Reset zoom when the src changes (prev/next nav).
|
||||
watch(() => props.src, () => panZoom.reset())
|
||||
|
||||
// Click on the image → toggle zoom (IR/expected behavior). Click on
|
||||
// the haze area around the image → request close. We use the image's
|
||||
// own bounding rect rather than @click.self because the <img> sets
|
||||
// pointer-events: none for the pan-drag pathway, so the click always
|
||||
// targets the canvas div regardless of where the cursor was.
|
||||
function onCanvasClick(ev) {
|
||||
if (panZoom.state.scale > 1) {
|
||||
// When zoomed, any click resets zoom — preserve old behavior.
|
||||
panZoom.handlers.onClick(ev)
|
||||
return
|
||||
}
|
||||
const img = imgEl.value
|
||||
if (!img) return
|
||||
const r = img.getBoundingClientRect()
|
||||
const inside =
|
||||
ev.clientX >= r.left && ev.clientX <= r.right &&
|
||||
ev.clientY >= r.top && ev.clientY <= r.bottom
|
||||
if (inside) {
|
||||
panZoom.handlers.onClick(ev)
|
||||
} else {
|
||||
emit('close-request')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-canvas {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: rgb(var(--v-theme-background));
|
||||
/* Transparent so the viewer's haze shows through to the image area
|
||||
instead of a solid panel sitting on top of it. */
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
cursor: zoom-in;
|
||||
user-select: none;
|
||||
|
||||
@@ -39,9 +39,11 @@
|
||||
<template v-else-if="modal.current">
|
||||
<ImageCanvas
|
||||
v-if="!isVideo" :src="modal.current.image_url" :alt="`Image ${modal.current.id}`"
|
||||
@close-request="$emit('close')"
|
||||
/>
|
||||
<VideoCanvas
|
||||
v-else :src="modal.current.image_url" :mime="modal.current.mime"
|
||||
@close-request="$emit('close')"
|
||||
/>
|
||||
</template>
|
||||
<v-alert v-else-if="modal.error" type="error" variant="tonal">
|
||||
@@ -123,7 +125,11 @@ function isTextEntry(el) {
|
||||
<style scoped>
|
||||
.fc-viewer {
|
||||
position: fixed; inset: 0; z-index: 2000;
|
||||
background: rgba(20, 23, 26, 0.96);
|
||||
/* Obsidian haze (#14171A = 20,23,26) — same palette as TopNav,
|
||||
mid-opacity + blur so the page behind shows through faintly. */
|
||||
background: rgba(20, 23, 26, 0.65);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
outline: none;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
@@ -152,8 +158,12 @@ function isTextEntry(el) {
|
||||
flex: 1; display: flex; min-height: 0;
|
||||
}
|
||||
.fc-viewer__media {
|
||||
flex: 1; display: flex; align-items: center; justify-content: center;
|
||||
min-width: 0;
|
||||
/* Let the canvas fill us; the canvas does its own centering. Both
|
||||
min-* are needed so this child can shrink inside its flex parents
|
||||
(without them, max-height/max-width on the <img> have nothing to
|
||||
bound against and the image overflows the viewport). */
|
||||
flex: 1; display: flex;
|
||||
min-width: 0; min-height: 0;
|
||||
}
|
||||
.fc-viewer__side {
|
||||
width: 320px; flex-shrink: 0;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div class="fc-canvas fc-canvas--video">
|
||||
<div class="fc-canvas fc-canvas--video" @click.self="$emit('close-request')">
|
||||
<video
|
||||
v-if="playable" :src="src" controls playsinline preload="metadata"
|
||||
class="fc-canvas__video"
|
||||
@click.stop
|
||||
/>
|
||||
<div v-else class="fc-canvas__unsupported">
|
||||
<div v-else class="fc-canvas__unsupported" @click.stop>
|
||||
<v-icon size="56" icon="mdi-video-off-outline" />
|
||||
<h3 class="fc-canvas__unsupported-title">Format not browser-playable</h3>
|
||||
<p>
|
||||
@@ -23,6 +24,7 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
defineEmits(['close-request'])
|
||||
const props = defineProps({ src: String, mime: String })
|
||||
|
||||
const BROWSER_PLAYABLE = new Set([
|
||||
@@ -34,7 +36,9 @@ const playable = computed(() => BROWSER_PLAYABLE.has(props.mime))
|
||||
<style scoped>
|
||||
.fc-canvas {
|
||||
flex: 1;
|
||||
background: rgb(var(--v-theme-background));
|
||||
/* Transparent so the viewer's haze shows through (parallels
|
||||
ImageCanvas — both sit inside ImageViewer's blurred backdrop). */
|
||||
background: transparent;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.fc-canvas__video { max-width: 100%; max-height: 100%; }
|
||||
|
||||
@@ -57,7 +57,12 @@ function startPolling() {
|
||||
pollId = setInterval(() => {
|
||||
if (!document.hidden) {
|
||||
system.refreshStats()
|
||||
if (tab.value === 'import') importStore.refreshStatus()
|
||||
if (tab.value === 'import') {
|
||||
importStore.refreshStatus()
|
||||
// Refresh the task list while a batch is in flight so the UI
|
||||
// doesn't sit stale next to a ticking imported/skipped counter.
|
||||
if (importStore.activeBatch) importStore.loadTasks(true)
|
||||
}
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user