Start showing images in gallery (requires tidy)

This commit is contained in:
Julian Stirling 2026-06-04 17:30:28 +01:00
parent 1e12900506
commit 5a07721fc2
6 changed files with 123 additions and 29 deletions

View file

@ -8,7 +8,7 @@ import shutil
import threading
import zipfile
from datetime import datetime, timedelta
from typing import Any, Mapping, Optional, Self
from typing import Any, Literal, Mapping, Optional, Self
from pydantic import (
BaseModel,
@ -55,6 +55,8 @@ class ScanInfo(BaseModel):
number_of_images: int
stitch_available: bool
dzi: Optional[str]
thing: str = "smart_scan"
card_type: Literal["Scan"] = "Scan"
class BaseScanData(BaseModel):

View file

@ -20,7 +20,7 @@ from typing import Any, Literal, Mapping, Optional, Self
import numpy as np
import piexif
from fastapi import Response
from fastapi import HTTPException, Response
from PIL import Image
from pydantic import BaseModel
@ -199,6 +199,16 @@ class CaptureMode(BaseModel):
"""The resolution to save the image. Use None to save as captured."""
class CaptureInfo(BaseModel):
"""Summary information for the UI about an image."""
name: str
created: float
modified: float
thing: str
card_type: Literal["Capture"] = "Capture"
class BaseCamera(OFMThing, ABC):
"""The base class for all cameras. All cameras must directly inherit from this class.
@ -275,6 +285,65 @@ class BaseCamera(OFMThing, ABC):
jpeg_data = await self.lores_mjpeg_stream.grab_frame()
return Response(content=jpeg_data, media_type="image/jpeg")
# Register with gallery.
_show_data_in_gallery = True
@property
def gallery_data_name(self) -> str:
"""Name under which data shows up in gallery."""
return "Captures"
@property
def gallery_data_schema(self) -> type[CaptureInfo]:
"""The schema (BaseModel) for passing data to the gallery."""
return CaptureInfo
def get_data_for_gallery(self) -> list[CaptureInfo]:
"""Return all the information about the saved captures."""
files = os.listdir(self._data_dir)
captures = []
for filename in files:
full_path = os.path.join(self.data_dir, filename)
if os.path.isfile(full_path) and (
BASE_IMAGE_FORMATS["jpeg"].path_matches(filename)
or BASE_IMAGE_FORMATS["png"].path_matches(filename)
):
captures.append(
CaptureInfo(
name=filename,
created=os.path.getctime(full_path),
modified=os.path.getmtime(full_path),
thing=self.name,
)
)
return captures
@lt.endpoint(
"delete",
"capture/{name}",
responses={
200: {"description": "Successfully deleted capture"},
400: {"description": "An error occurred while trying to delete capture"},
},
)
def delete_capture(self, name: str) -> None:
"""Delete the specified capture.
This endpoint allows captures to be deleted from disk.
:param name: The name of the capture to delete
"""
full_path = os.path.join(self.data_dir, name)
if not os.path.exists(full_path):
self.logger.warning(f"Cannot find a capture of name {name}")
raise HTTPException(400, "Capture not found")
try:
os.remove(full_path)
except IOError as e:
raise HTTPException(
400, "Couldn't delete capture, check log for details"
) from e
@property
def focus_fom(self) -> int:
"""Return the focus figure of merit.

View file

@ -5,17 +5,17 @@
<div class="view-image uk-padding-remove uk-height-1-1">
<img
id="thumbnail-stitched-image"
:class="[itemData?.dzi ? 'thumbnail-fit clickable' : 'thumbnail-fit disabled']"
:class="[viewerAvailable ? 'thumbnail-fit clickable' : 'thumbnail-fit disabled']"
class="thumbnail-fit"
:src="thumbnailPath"
onerror="this.src = '/titleiconpink.svg'"
@click="itemData?.dzi && requestViewer()"
@click="viewerAvailable && requestViewer()"
/>
</div>
</div>
<h3 class="uk-card-title gallery-card-title">{{ itemData.name }}</h3>
<div class="button-container">
<div class="uk-button-group gallery-card-buttons">
<div v-if="itemData.card_type === 'Scan'" class="uk-button-group gallery-card-buttons">
<action-button
class="uk-width-1-2"
thing="smart_scan"
@ -55,7 +55,7 @@
Show Stitched Scan
</button>
</div>
<div class="item-info">
<div v-if="itemData.card_type === 'Scan'" class="item-info">
<ul>
<li>{{ itemData.number_of_images }} images</li>
<li>Created: {{ formatDate(itemData.created) }}</li>
@ -107,7 +107,14 @@ export default {
return `${this.baseUri}/smart_scan/get_stitch/${this.itemData.name}`;
},
thumbnailPath() {
return `${this.baseUri}/smart_scan/scans/stitched_thumbnail.jpg?scan_name=${this.itemData.name}&modified=${this.itemData.modified}`;
if (this.itemData.card_type === "Scan") {
return `${this.baseUri}/smart_scan/scans/stitched_thumbnail.jpg?scan_name=${this.itemData.name}&modified=${this.itemData.modified}`;
}
return `${this.baseUri}/data/${this.itemData.thing}/${this.itemData.name}`;
},
viewerAvailable() {
if (this.itemData.card_type === "Scan") return Boolean(this.itemData?.dzi);
return true;
},
},
@ -144,7 +151,13 @@ export default {
async deleteScan() {
try {
await this.modalConfirm(`Are you sure you want to delete ${this.itemData.name}?`);
await axios.delete(`${this.baseUri}/smart_scan/scans/${this.itemData.name}`);
if (this.itemData.card_type === "Scan") {
await axios.delete(`${this.baseUri}/smart_scan/scans/${this.itemData.name}`);
} else {
await axios.delete(
`${this.baseUri}/${this.itemData.thing}/capture/${this.itemData.name}`,
);
}
this.$emit("update-requested");
this.modalNotify(`Deleted ${this.itemData.name}`);
} catch (e) {

View file

@ -13,7 +13,7 @@ export default {
props: {
src: {
type: String,
type: [String, Object],
required: true,
},
brightness: {

View file

@ -1,8 +1,8 @@
<template>
<div id="scan-modal" ref="scanModal" uk-modal>
<div v-if="selectedScan" id="scan-modal-body" class="uk-modal-dialog uk-modal-body">
<div v-if="selectedItem" id="scan-modal-body" class="uk-modal-dialog uk-modal-body">
<h2 id="scan-modal-title" class="uk-modal-title">
{{ selectedScan.name }}
{{ selectedItem.name }}
<button class="uk-modal-close uk-float-right" type="button">
<span class="material-symbols-outlined">close</span>
</button>
@ -12,11 +12,11 @@
</h2>
<!-- Viewer -->
<div v-if="selectedScanDZI" id="viewer_container" class="viewer_container">
<div v-if="imageSource" id="viewer_container" class="viewer_container">
<OpenSeadragonViewer
id="openseadragon"
ref="openseadragon"
:src="selectedScanDZI"
:src="imageSource"
:brightness="brightness"
:contrast="contrast"
:saturation="saturation"
@ -25,7 +25,7 @@
</div>
<!-- Controls -->
<div v-if="selectedScanDZI" class="viewer-controls">
<div v-if="imageSource" class="viewer-controls">
<div class="controlsContainer">
<label>
Brightness
@ -63,7 +63,7 @@ export default {
OpenSeadragonViewer,
},
props: {
selectedScan: {
selectedItem: {
type: Object,
default: null,
},
@ -79,9 +79,14 @@ export default {
};
},
computed: {
selectedScanDZI() {
if (this.selectedScan && this.selectedScan.dzi) {
return `${this.baseUri}/data/smart_scan/${this.selectedScan.name}/images/${this.selectedScan.dzi}`;
imageSource() {
if (this.selectedItem?.card_type === "Scan" && this.selectedItem?.dzi) {
return `${this.baseUri}/data/smart_scan/${this.selectedItem.name}/images/${this.selectedItem.dzi}`;
} else if (this.selectedItem?.card_type === "Capture") {
return {
type: "image",
url: `${this.baseUri}/data/${this.selectedItem.thing}/${this.selectedItem.name}`,
};
}
return null;
},

View file

@ -41,7 +41,7 @@
</div>
</nav>
<ScanViewerModal ref="scanViewer" :selected-scan="selectedScan" :base-uri="baseUri" />
<ScanViewerModal ref="scanViewer" :selected-item="selectedItem" :base-uri="baseUri" />
<!-- Gallery -->
<div v-if="ready" class="uk-padding-remove-top" uk-lightbox="toggle: .lightbox-link">
@ -54,7 +54,7 @@
<div v-for="itemData in paginatedItems" :key="itemData.id">
<gallery-card
:item-data="itemData"
@viewer-requested="showScan"
@viewer-requested="showItem"
@update-requested="refreshGallery"
/>
</div>
@ -95,7 +95,7 @@ export default {
data: function () {
return {
all_items: [],
selectedScan: null,
selectedItem: null,
osdViewer: null,
currentPage: 1,
itemsPerPage: 18,
@ -112,9 +112,9 @@ export default {
noItems() {
return !this.all_items || this.all_items?.length === 0;
},
selectedScanDZI() {
if (this.selectedScan && this.selectedScan.dzi != "") {
return `${this.baseUri}/data/smart_scan/${this.selectedScan.name}/images/${this.selectedScan.dzi}`;
selectedItemDZI() {
if (this.selectedItem && this.selectedItem.dzi != "") {
return `${this.baseUri}/data/smart_scan/${this.selectedItem.name}/images/${this.selectedItem.dzi}`;
} else {
return null;
}
@ -209,12 +209,17 @@ export default {
if (e) this.modalError(e);
}
},
showScan(scan) {
if (scan.dzi) {
this.selectedScan = scan;
this.$refs.scanViewer.show();
showItem(itemData) {
if (itemData.card_type === "Scan") {
if (itemData.dzi) {
this.selectedItem = itemData;
this.$refs.scanViewer.show();
} else {
this.modalError("Scan not stitched for viewing in webapp, please download or stitch");
}
} else {
this.modalError("Scan not stitched for viewing in webapp, please download or stitch");
this.selectedItem = itemData;
this.$refs.scanViewer.show();
}
},
changePage(page) {