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-style: solid;
border-color: rgba(180, 180, 180, 0.25);
padding-right: 20px;
padding-right: 15px;
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() {
.ofm-tab-navbar {
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
.ofm-text-button {
display: block;

View file

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

View file

@ -1,11 +1,14 @@
<template>
<div class="uk-card">
<div class="uk-card-body">
<div class="uk-card-media-top">
<div class="view-image uk-padding-remove uk-height-1-1">
<div class="uk-card-body" :class="{ 'thumbnail-only-card': thumbnailOnly }">
<div
class="uk-card-media-top uk-flex uk-flex-center uk-flex-middle"
:class="{ 'thumbnail-only-container': thumbnailOnly }"
>
<div class="uk-padding-remove">
<img
id="thumbnail-stitched-image"
:class="[viewerAvailable ? 'thumbnail-fit clickable' : 'thumbnail-fit disabled']"
:class="[viewerAvailable ? 'clickable' : 'disabled', thumbnailOnly && 'thumbnail-only']"
class="thumbnail-fit"
:src="thumbnailPath"
onerror="this.src = '/titleiconpink.svg'"
@ -13,6 +16,7 @@
/>
</div>
</div>
<template v-if="!thumbnailOnly">
<h3 class="uk-card-title gallery-card-title">{{ itemData.name }}</h3>
<div class="button-container">
<div v-if="itemData.card_type === 'Scan'" class="uk-button-group gallery-card-buttons">
@ -35,7 +39,9 @@
button-label="Download JPEG"
/>
</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
v-if="itemData.can_stitch | (itemData.stitch_available & !itemData.dzi)"
submit-label="Stitch Images"
@ -61,6 +67,9 @@
<li>Created: {{ formatDate(itemData.created) }}</li>
<li>Duration: {{ formatDuration(itemData.duration) }}</li>
</ul>
</div>
</template>
<div :class="{ 'thumbnail-only-warning': thumbnailOnly }">
<ul>
<li v-if="itemData.number_of_images < 2" class="warning-msg">
Not enough images to stitch
@ -97,6 +106,11 @@ export default {
type: Object,
required: true,
},
thumbnailOnly: {
type: Boolean,
default: false,
required: false,
},
},
emits: ["viewer-requested", "update-requested"],
@ -210,6 +224,19 @@ ul {
text-align: center;
}
.thumbnail-only-card {
width: auto;
}
.thumbnail-only-container {
height: 150px;
width: 150px;
}
.thumbnail-only-warning {
width: 150px;
}
.thumbnail-fit {
max-height: 120px;
max-width: 240px;
@ -217,6 +244,11 @@ ul {
overflow-y: hidden;
}
.thumbnail-fit.thumbnail-only {
max-height: 150px;
max-width: 150px;
}
.clickable {
cursor: pointer;
}

View file

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