diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index e2416c03..20808e0f 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -99,7 +99,10 @@ class ScanInfo(BaseModel): number_of_images: int -DOWNLOADABLE_SCAN_FILES = ("images.zip",) +DOWNLOADABLE_SCAN_FILES = ( + "images.zip", + "stitched_thumbnail.jpg", +) JPEGBlob = blob_type("image/jpeg") ZipBlob = blob_type("application/zip") @@ -714,6 +717,29 @@ class SmartScanThing(Thing): except SubprocessError as e: self._scan_logger.error(f"Stitching failed: {e}", exc_info=e) + @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 def _capture_and_save( self, @@ -861,11 +887,12 @@ class SmartScanThing(Thing): number_of_images = len(os.listdir(images_folder)) else: number_of_images = 0 + modified = max(os.stat(root).st_mtime for root, _, _ in os.walk(path)) scans.append( ScanInfo( name=f, created=os.path.getctime(path), - modified=os.path.getmtime(path), + modified=modified, number_of_images=number_of_images, ) ) diff --git a/webapp/public/titleiconpink.svg b/webapp/public/titleiconpink.svg new file mode 100644 index 00000000..5ef0d260 --- /dev/null +++ b/webapp/public/titleiconpink.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/webapp/src/App.vue b/webapp/src/App.vue index e6a4df35..fad015e5 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -441,6 +441,12 @@ html { height: 100%; } +.thumbnail-fit { + max-height: 120px; + object-fit: contain; + overflow-y: hidden; +} + // Style tour .v-tour__target--highlighted { box-shadow: 0px 40px 200px 30px rgba(0, 0, 0, 0.5), diff --git a/webapp/src/assets/less/theme.less b/webapp/src/assets/less/theme.less index b5b18ca1..4a7d9134 100644 --- a/webapp/src/assets/less/theme.less +++ b/webapp/src/assets/less/theme.less @@ -201,8 +201,18 @@ h4, .uk-h4 { box-shadow: @small-shadow; } +.uk-card-title { + margin: 0; + width: 240px; +} + +.uk-card-body { + padding: 20px 16px 8px 16px;; +} + .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 { diff --git a/webapp/src/components/tabContentComponents/scanListContent.vue b/webapp/src/components/tabContentComponents/scanListContent.vue index 6c95f653..7bbb4a8a 100644 --- a/webapp/src/components/tabContentComponents/scanListContent.vue +++ b/webapp/src/components/tabContentComponents/scanListContent.vue @@ -42,10 +42,29 @@ uk-lightbox="toggle: .lightbox-link" > +