Start showing images in gallery (requires tidy)
This commit is contained in:
parent
1e12900506
commit
5a07721fc2
6 changed files with 123 additions and 29 deletions
|
|
@ -8,7 +8,7 @@ import shutil
|
||||||
import threading
|
import threading
|
||||||
import zipfile
|
import zipfile
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Mapping, Optional, Self
|
from typing import Any, Literal, Mapping, Optional, Self
|
||||||
|
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
BaseModel,
|
BaseModel,
|
||||||
|
|
@ -55,6 +55,8 @@ class ScanInfo(BaseModel):
|
||||||
number_of_images: int
|
number_of_images: int
|
||||||
stitch_available: bool
|
stitch_available: bool
|
||||||
dzi: Optional[str]
|
dzi: Optional[str]
|
||||||
|
thing: str = "smart_scan"
|
||||||
|
card_type: Literal["Scan"] = "Scan"
|
||||||
|
|
||||||
|
|
||||||
class BaseScanData(BaseModel):
|
class BaseScanData(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from typing import Any, Literal, Mapping, Optional, Self
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import piexif
|
import piexif
|
||||||
from fastapi import Response
|
from fastapi import HTTPException, Response
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
@ -199,6 +199,16 @@ class CaptureMode(BaseModel):
|
||||||
"""The resolution to save the image. Use None to save as captured."""
|
"""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):
|
class BaseCamera(OFMThing, ABC):
|
||||||
"""The base class for all cameras. All cameras must directly inherit from this class.
|
"""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()
|
jpeg_data = await self.lores_mjpeg_stream.grab_frame()
|
||||||
return Response(content=jpeg_data, media_type="image/jpeg")
|
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
|
@property
|
||||||
def focus_fom(self) -> int:
|
def focus_fom(self) -> int:
|
||||||
"""Return the focus figure of merit.
|
"""Return the focus figure of merit.
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@
|
||||||
<div class="view-image uk-padding-remove uk-height-1-1">
|
<div class="view-image uk-padding-remove uk-height-1-1">
|
||||||
<img
|
<img
|
||||||
id="thumbnail-stitched-image"
|
id="thumbnail-stitched-image"
|
||||||
:class="[itemData?.dzi ? 'thumbnail-fit clickable' : 'thumbnail-fit disabled']"
|
:class="[viewerAvailable ? 'thumbnail-fit clickable' : 'thumbnail-fit disabled']"
|
||||||
class="thumbnail-fit"
|
class="thumbnail-fit"
|
||||||
:src="thumbnailPath"
|
:src="thumbnailPath"
|
||||||
onerror="this.src = '/titleiconpink.svg'"
|
onerror="this.src = '/titleiconpink.svg'"
|
||||||
@click="itemData?.dzi && requestViewer()"
|
@click="viewerAvailable && requestViewer()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="uk-card-title gallery-card-title">{{ itemData.name }}</h3>
|
<h3 class="uk-card-title gallery-card-title">{{ itemData.name }}</h3>
|
||||||
<div class="button-container">
|
<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
|
<action-button
|
||||||
class="uk-width-1-2"
|
class="uk-width-1-2"
|
||||||
thing="smart_scan"
|
thing="smart_scan"
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
Show Stitched Scan
|
Show Stitched Scan
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-info">
|
<div v-if="itemData.card_type === 'Scan'" class="item-info">
|
||||||
<ul>
|
<ul>
|
||||||
<li>{{ itemData.number_of_images }} images</li>
|
<li>{{ itemData.number_of_images }} images</li>
|
||||||
<li>Created: {{ formatDate(itemData.created) }}</li>
|
<li>Created: {{ formatDate(itemData.created) }}</li>
|
||||||
|
|
@ -107,7 +107,14 @@ export default {
|
||||||
return `${this.baseUri}/smart_scan/get_stitch/${this.itemData.name}`;
|
return `${this.baseUri}/smart_scan/get_stitch/${this.itemData.name}`;
|
||||||
},
|
},
|
||||||
thumbnailPath() {
|
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() {
|
async deleteScan() {
|
||||||
try {
|
try {
|
||||||
await this.modalConfirm(`Are you sure you want to delete ${this.itemData.name}?`);
|
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.$emit("update-requested");
|
||||||
this.modalNotify(`Deleted ${this.itemData.name}`);
|
this.modalNotify(`Deleted ${this.itemData.name}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default {
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
src: {
|
src: {
|
||||||
type: String,
|
type: [String, Object],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
brightness: {
|
brightness: {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="scan-modal" ref="scanModal" uk-modal>
|
<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">
|
<h2 id="scan-modal-title" class="uk-modal-title">
|
||||||
{{ selectedScan.name }}
|
{{ selectedItem.name }}
|
||||||
<button class="uk-modal-close uk-float-right" type="button">
|
<button class="uk-modal-close uk-float-right" type="button">
|
||||||
<span class="material-symbols-outlined">close</span>
|
<span class="material-symbols-outlined">close</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -12,11 +12,11 @@
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Viewer -->
|
<!-- Viewer -->
|
||||||
<div v-if="selectedScanDZI" id="viewer_container" class="viewer_container">
|
<div v-if="imageSource" id="viewer_container" class="viewer_container">
|
||||||
<OpenSeadragonViewer
|
<OpenSeadragonViewer
|
||||||
id="openseadragon"
|
id="openseadragon"
|
||||||
ref="openseadragon"
|
ref="openseadragon"
|
||||||
:src="selectedScanDZI"
|
:src="imageSource"
|
||||||
:brightness="brightness"
|
:brightness="brightness"
|
||||||
:contrast="contrast"
|
:contrast="contrast"
|
||||||
:saturation="saturation"
|
:saturation="saturation"
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Controls -->
|
<!-- Controls -->
|
||||||
<div v-if="selectedScanDZI" class="viewer-controls">
|
<div v-if="imageSource" class="viewer-controls">
|
||||||
<div class="controlsContainer">
|
<div class="controlsContainer">
|
||||||
<label>
|
<label>
|
||||||
Brightness
|
Brightness
|
||||||
|
|
@ -63,7 +63,7 @@ export default {
|
||||||
OpenSeadragonViewer,
|
OpenSeadragonViewer,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
selectedScan: {
|
selectedItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
|
@ -79,9 +79,14 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
selectedScanDZI() {
|
imageSource() {
|
||||||
if (this.selectedScan && this.selectedScan.dzi) {
|
if (this.selectedItem?.card_type === "Scan" && this.selectedItem?.dzi) {
|
||||||
return `${this.baseUri}/data/smart_scan/${this.selectedScan.name}/images/${this.selectedScan.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;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<ScanViewerModal ref="scanViewer" :selected-scan="selectedScan" :base-uri="baseUri" />
|
<ScanViewerModal ref="scanViewer" :selected-item="selectedItem" :base-uri="baseUri" />
|
||||||
|
|
||||||
<!-- Gallery -->
|
<!-- Gallery -->
|
||||||
<div v-if="ready" class="uk-padding-remove-top" uk-lightbox="toggle: .lightbox-link">
|
<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">
|
<div v-for="itemData in paginatedItems" :key="itemData.id">
|
||||||
<gallery-card
|
<gallery-card
|
||||||
:item-data="itemData"
|
:item-data="itemData"
|
||||||
@viewer-requested="showScan"
|
@viewer-requested="showItem"
|
||||||
@update-requested="refreshGallery"
|
@update-requested="refreshGallery"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -95,7 +95,7 @@ export default {
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
all_items: [],
|
all_items: [],
|
||||||
selectedScan: null,
|
selectedItem: null,
|
||||||
osdViewer: null,
|
osdViewer: null,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
itemsPerPage: 18,
|
itemsPerPage: 18,
|
||||||
|
|
@ -112,9 +112,9 @@ export default {
|
||||||
noItems() {
|
noItems() {
|
||||||
return !this.all_items || this.all_items?.length === 0;
|
return !this.all_items || this.all_items?.length === 0;
|
||||||
},
|
},
|
||||||
selectedScanDZI() {
|
selectedItemDZI() {
|
||||||
if (this.selectedScan && this.selectedScan.dzi != "") {
|
if (this.selectedItem && this.selectedItem.dzi != "") {
|
||||||
return `${this.baseUri}/data/smart_scan/${this.selectedScan.name}/images/${this.selectedScan.dzi}`;
|
return `${this.baseUri}/data/smart_scan/${this.selectedItem.name}/images/${this.selectedItem.dzi}`;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -209,12 +209,17 @@ export default {
|
||||||
if (e) this.modalError(e);
|
if (e) this.modalError(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showScan(scan) {
|
showItem(itemData) {
|
||||||
if (scan.dzi) {
|
if (itemData.card_type === "Scan") {
|
||||||
this.selectedScan = scan;
|
if (itemData.dzi) {
|
||||||
this.$refs.scanViewer.show();
|
this.selectedItem = itemData;
|
||||||
|
this.$refs.scanViewer.show();
|
||||||
|
} else {
|
||||||
|
this.modalError("Scan not stitched for viewing in webapp, please download or stitch");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.modalError("Scan not stitched for viewing in webapp, please download or stitch");
|
this.selectedItem = itemData;
|
||||||
|
this.$refs.scanViewer.show();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changePage(page) {
|
changePage(page) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue