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,6 +16,7 @@
/> />
</div> </div>
</div> </div>
<template v-if="!thumbnailOnly">
<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 v-if="itemData.card_type === 'Scan'" class="uk-button-group gallery-card-buttons"> <div v-if="itemData.card_type === 'Scan'" class="uk-button-group gallery-card-buttons">
@ -35,7 +39,9 @@
button-label="Download JPEG" button-label="Download JPEG"
/> />
</div> </div>
<button class="uk-button uk-button-default uk-width-1-1" @click="deleteScan">Delete</button> <button class="uk-button uk-button-default uk-width-1-1" @click="deleteScan">
Delete
</button>
<action-button <action-button
v-if="itemData.can_stitch | (itemData.stitch_available & !itemData.dzi)" v-if="itemData.can_stitch | (itemData.stitch_available & !itemData.dzi)"
submit-label="Stitch Images" submit-label="Stitch Images"
@ -61,6 +67,9 @@
<li>Created: {{ formatDate(itemData.created) }}</li> <li>Created: {{ formatDate(itemData.created) }}</li>
<li>Duration: {{ formatDuration(itemData.duration) }}</li> <li>Duration: {{ formatDuration(itemData.duration) }}</li>
</ul> </ul>
</div>
</template>
<div :class="{ 'thumbnail-only-warning': thumbnailOnly }">
<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,27 +4,46 @@
<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>
<icon-button
icon="tile_small"
:show-label="true"
label="Detailed cards"
@click="
thumbnailOnly = false;
close();
"
/>
</p>
<p>
<icon-button
icon="view_compact"
:show-label="true"
label="Thumbnail only"
@click="
thumbnailOnly = true;
close();
"
/>
</p>
</mini-menu>
<button <button
class="uk-button uk-button-default uk-width-1-1 gallery-button" class="uk-button uk-button-default ofm-top-nav-button"
type="button" type="button"
@click="refreshGallery()" @click="refreshGallery()"
> >
Refresh Refresh
</button> </button>
</div>
<div>
<button <button
class="sidebar-toggle uk-button uk-button-default uk-width-1-1 gallery-button" class="ofm-top-nav-square-button uk-button uk-button-default"
type="button" type="button"
@click="sidebarOpen = !sidebarOpen" @click="sidebarOpen = !sidebarOpen"
> >
<div class="sidebar-toggle-text" :class="{ open: sidebarOpen }"></div> <div class="sidebar-toggle-text" :class="{ open: sidebarOpen }"></div>
</button> </button>
</div> </div>
</div>
</div>
</nav> </nav>
<gallery-modal ref="viewerModal" :selected-item="selectedItem" :base-uri="baseUri" /> <gallery-modal ref="viewerModal" :selected-item="selectedItem" :base-uri="baseUri" />
@ -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;