Thumbnails and accurate durations in scan-list tab

This commit is contained in:
jaknapper 2025-04-22 19:02:29 +01:00
parent 257d700333
commit 91057f164a
4 changed files with 117 additions and 15 deletions

View file

@ -123,7 +123,10 @@ class ScanInfo(BaseModel):
number_of_images: int number_of_images: int
DOWNLOADABLE_SCAN_FILES = ("images.zip",) DOWNLOADABLE_SCAN_FILES = (
"images.zip",
"stitched_thumbnail.jpg",
)
JPEGBlob = blob_type("image/jpeg") JPEGBlob = blob_type("image/jpeg")
ZipBlob = blob_type("application/zip") ZipBlob = blob_type("application/zip")
@ -767,6 +770,50 @@ class SmartScanThing(Thing):
except SubprocessError as e: except SubprocessError as e:
self._scan_logger.error(f"Stitching failed: {e}", exc_info=e) self._scan_logger.error(f"Stitching failed: {e}", exc_info=e)
def update_thumbnail(self):
target_size = (400, 400)
images_folder = self._ongoing_scan_images_dir
file_path = os.path.join(images_folder, "stitched.jpg")
if os.path.isfile(file_path):
stitched_image = Image.open(file_path)
else:
file_path = os.path.join(images_folder, "stitched_from_stage.jpg")
if os.path.isfile(file_path):
stitched_image = Image.open(file_path)
else:
return None
try:
stitched_image.thumbnail(target_size)
stitched_image.save(os.path.join(images_folder, "stitched_thumbnail.jpg"))
except Exception:
return None
@fastapi_endpoint(
"get",
"scans/stitched_thumbnail.jpg",
responses={
200: {
"description": "A thumbnail-quality stitched image",
"content": {"image/jpeg": {}},
},
404: {"description": "File not found"},
},
)
def get_scan_thumbnail(self, scan_name: str) -> FileResponse:
"""Retrieve a file from a scan.
This endpoint allows files to be downloaded from a scan.
"""
path = os.path.join(
self.images_dir_for_scan(scan_name), "stitched_thumbnail.jpg"
)
if not os.path.isfile(path):
raise HTTPException(404, "File not found")
return FileResponse(path)
@_scan_running @_scan_running
def _capture_and_save( def _capture_and_save(
self, self,
@ -914,11 +961,12 @@ class SmartScanThing(Thing):
number_of_images = len(os.listdir(images_folder)) number_of_images = len(os.listdir(images_folder))
else: else:
number_of_images = 0 number_of_images = 0
modified = max(os.stat(root).st_mtime for root, _, _ in os.walk(path))
scans.append( scans.append(
ScanInfo( ScanInfo(
name=f, name=f,
created=os.path.getctime(path), created=os.path.getctime(path),
modified=os.path.getmtime(path), modified=modified,
number_of_images=number_of_images, number_of_images=number_of_images,
) )
) )
@ -1053,6 +1101,7 @@ class SmartScanThing(Thing):
self._ongoing_scan_images_dir, self._ongoing_scan_images_dir,
] ]
) )
self.update_thumbnail()
@_scan_running @_scan_running
def _preview_stitch_running(self) -> bool: def _preview_stitch_running(self) -> bool:

View file

@ -441,6 +441,12 @@ html {
height: 100%; height: 100%;
} }
.thumbnail-fit {
max-height: 120px;
object-fit: contain;
overflow-y: hidden;
}
// Style tour // Style tour
.v-tour__target--highlighted { .v-tour__target--highlighted {
box-shadow: 0px 40px 200px 30px rgba(0, 0, 0, 0.5), box-shadow: 0px 40px 200px 30px rgba(0, 0, 0, 0.5),

View file

@ -201,8 +201,18 @@ h4, .uk-h4 {
box-shadow: @small-shadow; box-shadow: @small-shadow;
} }
.uk-card-title {
margin: 0;
width: 240px;
}
.uk-card-body {
padding: 20px 16px 8px 16px;;
}
.uk-card-media-top img { .uk-card-media-top img {
border-radius: @paper-border-radius @paper-border-radius 0 0; width: 240px;
margin: 0 auto;
} }
.uk-card-default .uk-card-footer { .uk-card-default .uk-card-footer {

View file

@ -42,10 +42,29 @@
uk-lightbox="toggle: .lightbox-link" uk-lightbox="toggle: .lightbox-link"
> >
<!-- Gallery capture cards --> <!-- Gallery capture cards -->
<div class="gallery-grid uk-grid-match" uk-grid> <div class="gallery-grid uk-grid-match" uk-grid>
<div v-if="scansEmpty">
<h2>No scans available</h2>
<p>
There are no scans available to show.
</p>
</div>
<div v-for="item in scans" :key="item.id"> <div v-for="item in scans" :key="item.id">
<div class="uk-card"> <div class="uk-card">
<div class="uk-card-body"> <div class="uk-card-body">
<div class="uk-card-media-top">
<div
class="view-image uk-padding-remove uk-height-1-1"
>
<img
id="thumbnail-stitched-image"
class="thumbnail-fit"
:src="thumbnailPath(item.name)"
onerror="this.src='/titleicon.svg';"
/>
</div>
</div>
<h3 class="uk-card-title">{{ item.name }}</h3> <h3 class="uk-card-title">{{ item.name }}</h3>
<action-button <action-button
thing="smart_scan" thing="smart_scan"
@ -57,7 +76,10 @@
@response="downloadZipFile" @response="downloadZipFile"
@error="modalError" @error="modalError"
/> />
<button class="uk-button uk-button-default uk-width-1-1" @click="deleteScan(item.name)"> <button
class="uk-button uk-button-default uk-width-1-1"
@click="deleteScan(item.name)"
>
Delete Delete
</button> </button>
<action-button <action-button
@ -112,6 +134,9 @@ export default {
"readproperty", "readproperty",
true true
); );
},
scansEmpty() {
return this.scans.length == 0;
} }
}, },
@ -153,24 +178,36 @@ export default {
}, },
methods: { methods: {
thumbnailPath(scan_name) {
return (
`${this.$store.getters.baseUri}/smart_scan/scans/stitched_thumbnail.jpg?scan_name=` +
scan_name
);
},
visibilityChanged(isVisible) { visibilityChanged(isVisible) {
if (isVisible) { if (isVisible) {
this.updateScans(); this.updateScans();
} }
}, },
async updateScans() { async updateScans() {
let scans = await this.readThingProperty("smart_scan", "scans"); try {
if (!scans | (scans.length == 0)) { let scans = await this.readThingProperty("smart_scan", "scans");
if (!scans | (scans.length == 0)) {
this.scans = scans;
}
scans.forEach(scan => {
scan.modified = Date.parse(scan.modified);
scan.created = Date.parse(scan.created);
});
scans.sort((a, b) => {
return b.modified - a.modified;
});
this.scans = scans; this.scans = scans;
} catch (err) {
console.log("Failed to refresh scans");
console.log(err);
this.scans = [];
} }
scans.forEach(scan => {
scan.modified = Date.parse(scan.modified);
scan.created = Date.parse(scan.created);
});
scans.sort((a, b) => {
return b.modified - a.modified;
});
this.scans = scans;
}, },
formatDate(timestamp) { formatDate(timestamp) {
let d = new Date(timestamp); let d = new Date(timestamp);