Add filtering to gallery
This commit is contained in:
parent
e1e8cef8af
commit
a5828fbf41
6 changed files with 52 additions and 37 deletions
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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")]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue