Merge branch 'gallery-views' into 'v3'

Gallery views

See merge request openflexure/openflexure-microscope-server!646
This commit is contained in:
Joe Knapper 2026-07-07 13:36:16 +00:00
commit ece2eaa514
4 changed files with 183 additions and 81 deletions

View file

@ -222,9 +222,18 @@
border-width: 0 0 1px; border-width: 0 0 1px;
border-style: solid; border-style: solid;
border-color: rgba(180, 180, 180, 0.25); border-color: rgba(180, 180, 180, 0.25);
padding-right: 20px; padding-right: 15px;
background: #fff; background: #fff;
} }
// Make the gap increase for larger screens, this means that the right padding
// always matches the gap in uk-navbar-right
@media (width >= 960px) {
.ofm-tab-navbar {
padding-right: 30px;
}
}
.hook-inverse() { .hook-inverse() {
.ofm-tab-navbar { .ofm-tab-navbar {
background: #252525; background: #252525;
@ -363,6 +372,22 @@
} }
} }
.ofm-top-nav-button {
margin-top: 4px;
height: 38px;
}
.ofm-top-nav-square-button {
margin-top: 4px;
height: 38px;
aspect-ratio: 1;
}
.ofm-top-nav-square-button.material-icon {
// Material icons are 24x24 , set padding to centre
padding: 6px !important;
}
// A button styled like text. For use in menus // A button styled like text. For use in menus
.ofm-text-button { .ofm-text-button {
display: block; display: block;

View file

@ -1,9 +1,21 @@
<template> <template>
<div ref="mini-menu" class="mini-menu"> <div ref="mini-menu" class="mini-menu">
<a class="icon" @click.prevent="isOpen = !isOpen"> <a v-if="!withButton" class="icon" @click.prevent="isOpen = !isOpen">
<span class="material-symbols-outlined"> more_vert </span> <span class="material-symbols-outlined"> {{ icon }} </span>
</a> </a>
<div v-if="isOpen" class="dropdown-menu ofm-close-pars ofm-opaque-element"> <button
v-if="withButton"
class="ofm-top-nav-square-button material-icon uk-button uk-button-default"
type="button"
@click="isOpen = !isOpen"
>
<span class="material-symbols-outlined"> {{ icon }} </span>
</button>
<div
v-if="isOpen"
class="dropdown-menu ofm-close-pars ofm-opaque-element"
:class="{ 'with-button': withButton }"
>
<slot :close="close"></slot> <slot :close="close"></slot>
</div> </div>
</div> </div>
@ -13,7 +25,18 @@
export default { export default {
name: "MiniMenu", name: "MiniMenu",
props: {}, props: {
icon: {
type: String,
default: "more_vert",
required: false,
},
withButton: {
type: Boolean,
default: false,
required: false,
},
},
data() { data() {
return { return {
@ -62,6 +85,10 @@ export default {
padding-top: 5px; padding-top: 5px;
} }
.dropdown-menu.with-button {
top: 40px;
}
.icon { .icon {
user-select: none; user-select: none;
color: #888; color: #888;

View file

@ -1,11 +1,14 @@
<template> <template>
<div class="uk-card"> <div class="uk-card">
<div class="uk-card-body"> <div class="uk-card-body" :class="{ 'thumbnail-only-card': thumbnailOnly }">
<div class="uk-card-media-top"> <div
<div class="view-image uk-padding-remove uk-height-1-1"> class="uk-card-media-top uk-flex uk-flex-center uk-flex-middle"
:class="{ 'thumbnail-only-container': thumbnailOnly }"
>
<div class="uk-padding-remove">
<img <img
id="thumbnail-stitched-image" id="thumbnail-stitched-image"
:class="[viewerAvailable ? 'thumbnail-fit clickable' : 'thumbnail-fit disabled']" :class="[viewerAvailable ? 'clickable' : 'disabled', thumbnailOnly && 'thumbnail-only']"
class="thumbnail-fit" class="thumbnail-fit"
:src="thumbnailPath" :src="thumbnailPath"
onerror="this.src = '/titleiconpink.svg'" onerror="this.src = '/titleiconpink.svg'"
@ -13,54 +16,60 @@
/> />
</div> </div>
</div> </div>
<h3 class="uk-card-title gallery-card-title">{{ itemData.name }}</h3> <template v-if="!thumbnailOnly">
<div class="button-container"> <h3 class="uk-card-title gallery-card-title">{{ itemData.name }}</h3>
<div v-if="itemData.card_type === 'Scan'" class="uk-button-group gallery-card-buttons"> <div class="button-container">
<div v-if="itemData.card_type === 'Scan'" class="uk-button-group gallery-card-buttons">
<action-button
class="uk-width-1-2"
thing="smart_scan"
action="download_zip"
submit-label="Download All"
:can-terminate="false"
:submit-data="{ scan_name: itemData.name }"
:button-primary="true"
@response="downloadZipFile"
@error="modalError"
/>
<EndpointButton
class="uk-width-1-2"
:button-primary="true"
:is-disabled="!itemData.stitch_available"
:url="downloadStitchFile"
button-label="Download JPEG"
/>
</div>
<button class="uk-button uk-button-default uk-width-1-1" @click="deleteScan">
Delete
</button>
<action-button <action-button
class="uk-width-1-2" v-if="itemData.can_stitch | (itemData.stitch_available & !itemData.dzi)"
submit-label="Stitch Images"
thing="smart_scan" thing="smart_scan"
action="download_zip" action="stitch_scan"
submit-label="Download All" :can-terminate="true"
:can-terminate="false"
:submit-data="{ scan_name: itemData.name }" :submit-data="{ scan_name: itemData.name }"
:button-primary="true" :button-primary="false"
@response="downloadZipFile" :modal-progress="true"
@error="modalError" @error="modalError"
/> />
<EndpointButton <button
class="uk-width-1-2" v-if="itemData.dzi"
:button-primary="true" class="uk-button uk-button-default uk-width-1-1"
:is-disabled="!itemData.stitch_available" @click="requestViewer"
:url="downloadStitchFile" >
button-label="Download JPEG" Show Stitched Scan
/> </button>
</div> </div>
<button class="uk-button uk-button-default uk-width-1-1" @click="deleteScan">Delete</button> <div v-if="itemData.card_type === 'Scan'" class="item-info">
<action-button <ul>
v-if="itemData.can_stitch | (itemData.stitch_available & !itemData.dzi)" <li>{{ itemData.number_of_images }} images</li>
submit-label="Stitch Images" <li>Created: {{ formatDate(itemData.created) }}</li>
thing="smart_scan" <li>Duration: {{ formatDuration(itemData.duration) }}</li>
action="stitch_scan" </ul>
:can-terminate="true" </div>
:submit-data="{ scan_name: itemData.name }" </template>
:button-primary="false" <div :class="{ 'thumbnail-only-warning': thumbnailOnly }">
:modal-progress="true"
@error="modalError"
/>
<button
v-if="itemData.dzi"
class="uk-button uk-button-default uk-width-1-1"
@click="requestViewer"
>
Show Stitched Scan
</button>
</div>
<div v-if="itemData.card_type === 'Scan'" class="item-info">
<ul>
<li>{{ itemData.number_of_images }} images</li>
<li>Created: {{ formatDate(itemData.created) }}</li>
<li>Duration: {{ formatDuration(itemData.duration) }}</li>
</ul>
<ul> <ul>
<li v-if="itemData.number_of_images < 2" class="warning-msg"> <li v-if="itemData.number_of_images < 2" class="warning-msg">
Not enough images to stitch Not enough images to stitch
@ -97,6 +106,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
thumbnailOnly: {
type: Boolean,
default: false,
required: false,
},
}, },
emits: ["viewer-requested", "update-requested"], emits: ["viewer-requested", "update-requested"],
@ -210,6 +224,19 @@ ul {
text-align: center; text-align: center;
} }
.thumbnail-only-card {
width: auto;
}
.thumbnail-only-container {
height: 150px;
width: 150px;
}
.thumbnail-only-warning {
width: 150px;
}
.thumbnail-fit { .thumbnail-fit {
max-height: 120px; max-height: 120px;
max-width: 240px; max-width: 240px;
@ -217,6 +244,11 @@ ul {
overflow-y: hidden; overflow-y: hidden;
} }
.thumbnail-fit.thumbnail-only {
max-height: 150px;
max-width: 150px;
}
.clickable { .clickable {
cursor: pointer; cursor: pointer;
} }

View file

@ -4,26 +4,45 @@
<nav class="ofm-tab-navbar"> <nav class="ofm-tab-navbar">
<!-- Right side buttons --> <!-- Right side buttons -->
<div class="uk-navbar-right"> <div class="uk-navbar-right">
<div class="uk-grid"> <mini-menu v-slot="{ close }" icon="mobile_layout" :with-button="true">
<div> <p>
<button <icon-button
class="uk-button uk-button-default uk-width-1-1 gallery-button" icon="tile_small"
type="button" :show-label="true"
@click="refreshGallery()" label="Detailed cards"
> @click="
Refresh thumbnailOnly = false;
</button> close();
</div> "
<div> />
<button </p>
class="sidebar-toggle uk-button uk-button-default uk-width-1-1 gallery-button" <p>
type="button" <icon-button
@click="sidebarOpen = !sidebarOpen" icon="view_compact"
> :show-label="true"
<div class="sidebar-toggle-text" :class="{ open: sidebarOpen }"></div> label="Thumbnail only"
</button> @click="
</div> thumbnailOnly = true;
</div> close();
"
/>
</p>
</mini-menu>
<button
class="uk-button uk-button-default ofm-top-nav-button"
type="button"
@click="refreshGallery()"
>
Refresh
</button>
<button
class="ofm-top-nav-square-button uk-button uk-button-default"
type="button"
@click="sidebarOpen = !sidebarOpen"
>
<div class="sidebar-toggle-text" :class="{ open: sidebarOpen }"></div>
</button>
</div> </div>
</nav> </nav>
@ -46,6 +65,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"
:thumbnail-only="thumbnailOnly"
@viewer-requested="showItem" @viewer-requested="showItem"
@update-requested="refreshGallery" @update-requested="refreshGallery"
/> />
@ -98,6 +118,8 @@ import PaginateLinks from "@/components/genericComponents/paginateLinks.vue";
import MultiSelectDropdown from "@/components/genericComponents/multiSelectDropdown.vue"; import MultiSelectDropdown from "@/components/genericComponents/multiSelectDropdown.vue";
import actionButton from "@/components/labThingsComponents/actionButton.vue"; import actionButton from "@/components/labThingsComponents/actionButton.vue";
import ServerSpecifiedActionButton from "@/components/labThingsComponents/serverSpecifiedActionButton.vue"; import ServerSpecifiedActionButton from "@/components/labThingsComponents/serverSpecifiedActionButton.vue";
import MiniMenu from "@/components/genericComponents/miniMenu.vue";
import iconButton from "@/components/genericComponents/iconButton.vue";
import galleryCard from "./galleryComponents/galleryCard.vue"; import galleryCard from "./galleryComponents/galleryCard.vue";
import galleryModal from "./galleryComponents/galleryViewer.vue"; import galleryModal from "./galleryComponents/galleryViewer.vue";
import { eventBus } from "../../eventBus.js"; import { eventBus } from "../../eventBus.js";
@ -115,6 +137,8 @@ export default {
PaginateLinks, PaginateLinks,
MultiSelectDropdown, MultiSelectDropdown,
ServerSpecifiedActionButton, ServerSpecifiedActionButton,
MiniMenu,
iconButton,
}, },
emits: ["scrollTop"], emits: ["scrollTop"],
@ -125,11 +149,11 @@ export default {
selectedItem: null, selectedItem: null,
osdViewer: null, osdViewer: null,
currentPage: 1, currentPage: 1,
itemsPerPage: 18,
selectedCardTypes: [], selectedCardTypes: [],
allCardTypes: [], allCardTypes: [],
bulkActions: [], bulkActions: [],
sidebarOpen: false, sidebarOpen: false,
thumbnailOnly: false,
}; };
}, },
@ -141,6 +165,9 @@ export default {
noItems() { noItems() {
return !this.filtered_items || this.filtered_items?.length === 0; return !this.filtered_items || this.filtered_items?.length === 0;
}, },
itemsPerPage() {
return this.thumbnailOnly ? 25 : 18;
},
totalPages() { totalPages() {
return Math.ceil((this.filtered_items?.length || 0) / this.itemsPerPage); return Math.ceil((this.filtered_items?.length || 0) / this.itemsPerPage);
}, },
@ -268,15 +295,6 @@ export default {
padding-top: 20px; padding-top: 20px;
} }
.gallery-button {
margin-top: 5px;
margin-bottom: 2px;
}
.sidebar-toggle {
width: 40px;
}
.sidebar-toggle-text { .sidebar-toggle-text {
font-size: 24px; font-size: 24px;
transition: transform 0.25s ease; transition: transform 0.25s ease;