Add filtering to gallery

This commit is contained in:
Julian Stirling 2026-06-24 10:34:09 +01:00
parent e1e8cef8af
commit a5828fbf41
6 changed files with 52 additions and 37 deletions

View file

@ -295,11 +295,6 @@ class BaseCamera(OFMThing, ABC):
# Register with gallery. # Register with gallery.
_show_data_in_gallery = True _show_data_in_gallery = True
@property
def gallery_data_name(self) -> str:
"""Name under which data shows up in gallery."""
return "Captures"
@property @property
def gallery_data_schema(self) -> type[CaptureInfo]: def gallery_data_schema(self) -> type[CaptureInfo]:
"""The schema (BaseModel) for passing data to the gallery.""" """The schema (BaseModel) for passing data to the gallery."""

View file

@ -23,14 +23,14 @@ class GalleryCompatibleThing(Protocol):
be picked up, but may throw an error when used. be picked up, but may throw an error when used.
""" """
name: str
# Ensure it is a Thing: # Ensure it is a Thing:
_thing_server_interface: lt.ThingServerInterface _thing_server_interface: lt.ThingServerInterface
# Ensure it is an OFMThing: # Ensure it is an OFMThing:
show_data_in_gallery: bool show_data_in_gallery: bool
gallery_data_name: str
gallery_data_schema: type[BaseModel] gallery_data_schema: type[BaseModel]
# Ignore D102: No docstrings for the protocol. # Ignore D102: No docstrings for the protocol.
@ -45,6 +45,7 @@ class GalleryThing(lt.Thing):
all_ofm_things: Mapping[str, OFMThing] = lt.thing_slot() all_ofm_things: Mapping[str, OFMThing] = lt.thing_slot()
_gallery_providing_things: Optional[Mapping[str, GalleryCompatibleThing]] = None _gallery_providing_things: Optional[Mapping[str, GalleryCompatibleThing]] = None
_card_types: Optional[Mapping[str, GalleryCompatibleThing]] = None
@property @property
def gallery_providing_things(self) -> Mapping[str, GalleryCompatibleThing]: def gallery_providing_things(self) -> Mapping[str, GalleryCompatibleThing]:
@ -58,6 +59,7 @@ class GalleryThing(lt.Thing):
def __enter__(self) -> Self: def __enter__(self) -> Self:
"""Check for all gallery providing things on server startup.""" """Check for all gallery providing things on server startup."""
self._set_gallery_providers() self._set_gallery_providers()
self._set_card_types()
return self return self
def _set_gallery_providers(self) -> None: def _set_gallery_providers(self) -> None:
@ -86,6 +88,29 @@ class GalleryThing(lt.Thing):
Mapping[str, GalleryCompatibleThing], gallery_providers Mapping[str, GalleryCompatibleThing], gallery_providers
) )
def _set_card_types(self) -> None:
"""Find the card types from each provider."""
card_types: list[str] = []
for thing in self.gallery_providing_things.values():
card_schema = thing.gallery_data_schema.schema()
props = card_schema["properties"]
if "card_type" not in props or "const" not in props["card_type"]:
raise TypeError(
f"Gallery data card for {thing.name} doesn't have a staticcard_type"
)
card_type = props["card_type"]["const"]
if card_type not in card_types:
card_types.append(card_type)
self._card_types = card_types
@lt.property
def card_types(self) -> list[str]:
"""Names for the card types in the gallery."""
if self._card_types is None:
raise RuntimeError("Cannot access card_types before server has started.")
return self._card_types
@lt.property @lt.property
def list_data(self) -> list[dict[str, Any]]: def list_data(self) -> list[dict[str, Any]]:
"""List the data from all registered things. """List the data from all registered things.

View file

@ -170,11 +170,6 @@ class SmartScanThing(OFMThing):
# Register with gallery. # Register with gallery.
_show_data_in_gallery = True _show_data_in_gallery = True
@property
def gallery_data_name(self) -> str:
"""Name under which data shows up in gallery."""
return "Scans"
@property @property
def gallery_data_schema(self) -> type[scan_directories.ScanInfo]: def gallery_data_schema(self) -> type[scan_directories.ScanInfo]:
"""The schema (BaseModel) for passing data to the gallery.""" """The schema (BaseModel) for passing data to the gallery."""

View file

@ -42,8 +42,6 @@ class MinimalGalleryClass:
This should fail the protocol check as it is not an OFMThing This should fail the protocol check as it is not an OFMThing
""" """
gallery_data_name: str = "Mock"
gallery_data_schema: type[BaseModel] = MockGalleryData gallery_data_schema: type[BaseModel] = MockGalleryData
def get_data_for_gallery(self) -> list[BaseModel]: def get_data_for_gallery(self) -> list[BaseModel]:
@ -66,13 +64,11 @@ class MinimalGallerySource(OFMThing, MinimalGalleryClass):
class BadGallerySource(OFMThing): class BadGallerySource(OFMThing):
"""An OFMThing that almost defines enough to show in the gallery. """An OFMThing that almost defines enough to show in the gallery.
Shouldn't match the protocol as ``gallery_data_name`` is missing. Shouldn't match the protocol as ``gallery_data_schema`` is missing.
""" """
show_data_in_gallery = True show_data_in_gallery = True
gallery_data_schema: type[BaseModel] = MockGalleryData
def get_data_for_gallery(self) -> list[BaseModel]: def get_data_for_gallery(self) -> list[BaseModel]:
"""Return a list of Mock Gallery Data.""" """Return a list of Mock Gallery Data."""
return [MockGalleryData(mock="mock", foobar="foobar")] return [MockGalleryData(mock="mock", foobar="foobar")]

View file

@ -297,7 +297,6 @@ def test_gallery_responses(camera, mocker, caplog):
with tempfile.TemporaryDirectory() as tmpdir, caplog.at_level(logging.INFO): with tempfile.TemporaryDirectory() as tmpdir, caplog.at_level(logging.INFO):
camera._data_dir = tmpdir camera._data_dir = tmpdir
assert camera.gallery_data_name == "Captures"
assert camera.gallery_data_schema == CaptureInfo assert camera.gallery_data_schema == CaptureInfo
# When we start there are no captures # When we start there are no captures

View file

@ -7,8 +7,8 @@
<div class="uk-grid"> <div class="uk-grid">
<div class="gallery-button"> <div class="gallery-button">
<multi-select-dropdown <multi-select-dropdown
v-model="selectedFilter" v-model="selectedCardTypes"
:options="filterOptions" :options="allCardTypes"
title="Filter Gallery" title="Filter Gallery"
/> />
</div> </div>
@ -61,7 +61,7 @@
<div class="gallery-grid uk-grid-match" uk-grid> <div class="gallery-grid uk-grid-match" uk-grid>
<div v-if="noItems"> <div v-if="noItems">
<h2>Nothing to show</h2> <h2>Nothing to show</h2>
<p>There is no captured data to show..</p> <p>There is no captured data to show.</p>
</div> </div>
<div v-for="itemData in paginatedItems" :key="itemData.id"> <div v-for="itemData in paginatedItems" :key="itemData.id">
<gallery-card <gallery-card
@ -89,8 +89,7 @@ import galleryModal from "./galleryComponents/galleryViewer.vue";
import { eventBus } from "../../eventBus.js"; import { eventBus } from "../../eventBus.js";
import { useIntersectionObserver } from "@vueuse/core"; import { useIntersectionObserver } from "@vueuse/core";
import { useSettingsStore } from "@/stores/settings.js"; import { useSettingsStore } from "@/stores/settings.js";
import { mapState, storeToRefs } from "pinia"; import { mapState } from "pinia";
import { watch } from "vue";
// Export main app // Export main app
export default { export default {
@ -112,35 +111,39 @@ export default {
osdViewer: null, osdViewer: null,
currentPage: 1, currentPage: 1,
itemsPerPage: 18, itemsPerPage: 18,
selectedFilter: ["Scans", "Captures"], selectedCardTypes: [],
filterOptions: ["Scans", "Captures"], allCardTypes: [],
}; };
}, },
computed: { computed: {
...mapState(useSettingsStore, ["baseUri", "ready"]), ...mapState(useSettingsStore, ["baseUri", "ready"]),
filtered_items() {
return this.all_items.filter((item) => this.selectedCardTypes.includes(item.card_type));
},
noItems() { noItems() {
return !this.all_items || this.all_items?.length === 0; return !this.filtered_items || this.filtered_items?.length === 0;
}, },
totalPages() { totalPages() {
return Math.ceil((this.all_items?.length || 0) / this.itemsPerPage); return Math.ceil((this.filtered_items?.length || 0) / this.itemsPerPage);
}, },
paginatedItems() { paginatedItems() {
const start = (this.currentPage - 1) * this.itemsPerPage; const start = (this.currentPage - 1) * this.itemsPerPage;
return (this.all_items || []).slice(start, start + this.itemsPerPage); return (this.filtered_items || []).slice(start, start + this.itemsPerPage);
},
},
watch: {
totalPages(newPageCount) {
if (this.currentPage > newPageCount) {
this.currentPage = Math.max(1, newPageCount);
} else if (this.currentPage < 1) {
this.currentPage == 1;
}
}, },
}, },
async mounted() { async mounted() {
const store = useSettingsStore();
const { ready } = storeToRefs(store);
this.unwatchStoreFunction = watch(ready, (isReady) => {
if (isReady) {
this.refreshGallery();
} else {
this.all_items = [];
}
});
useIntersectionObserver( useIntersectionObserver(
this.$refs.galleryDisplay, this.$refs.galleryDisplay,
([{ isIntersecting }]) => { ([{ isIntersecting }]) => {
@ -150,6 +153,8 @@ export default {
threshold: 0.0, // Adjust as needed threshold: 0.0, // Adjust as needed
}, },
); );
this.allCardTypes = await this.readThingProperty("gallery", "card_types");
this.selectedCardTypes = this.allCardTypes;
// Update on mount (does nothing if not connected) // Update on mount (does nothing if not connected)
await this.refreshGallery(); await this.refreshGallery();
// A global signal listener to perform a gallery refresh // A global signal listener to perform a gallery refresh