Merge branch 'goal_1' into v3

This commit is contained in:
Antonio Anaya 2026-02-13 06:36:37 -06:00
commit f4a10ec897
53 changed files with 2534 additions and 10836 deletions

27
webapp/index.html Normal file
View file

@ -0,0 +1,27 @@
<!--
index.html
The main HTML file for the OpenFlexure Microscope web application.
Vite will use this file as a template to build the final HTML served to users.
It's at root directory to allow Vite to process it.
The previous version was located at public/index.html.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico">
<title>OpenFlexure Microscope</title>
</head>
<body>
<noscript>
<strong>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

12537
webapp/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,46 +1,43 @@
{ {
"name": "openflexure-microscope-jsclient", "name": "openflexure-microscope-jsclient",
"version": "3.0.0-alpha4", "version": "3.0.0-alpha4_vue3_dryrun",
"private": true, "private": true,
"description": "User client for the OpenFlexure Microscope Server", "description": "User client for the OpenFlexure Microscope Server",
"author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)", "author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)",
"scripts": { "scripts": {
"gv": "genversion src/version.js", "gv": "genversion src/version.js",
"serve": "vue-cli-service serve --mode development", "serve": "vite serve --mode development",
"build": "vue-cli-service build --mode production", "build": "vite build --mode production",
"lint": "vue-cli-service lint --no-fix --max-warnings 0", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-path .gitignore",
"lint:fix": "vue-cli-service lint --fix" "lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"@vue/compat": "^3.2.0",
"@vueuse/core": "^14.1.0",
"axios": "^1.7.7",
"material-design-icons": "^3.0", "material-design-icons": "^3.0",
"mitt": "^3.0.1",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"openseadragon": "^5.0.0", "openseadragon": "^5.0.0",
"vue-observe-visibility": "^0.4" "uikit": "^3.24.2",
"vite": "^5.0.0",
"vue": "^3.4.0",
"vue-observe-visibility": "^0.4",
"vue-router": "4.0.0",
"vuejs-paginate-next": "1.0.2",
"vuex": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.28.5", "@babel/eslint-parser": "^7.28.5",
"@vue/cli-plugin-babel": "^5.0.9", "@vue/eslint-config-prettier": "9.0.0",
"@vue/cli-plugin-eslint": "^5.0.9",
"@vue/cli-plugin-router": "^5.0.9",
"@vue/cli-plugin-vuex": "^5.0.9",
"@vue/cli-service": "^5.0.9",
"@vue/eslint-config-prettier": "^7.1.0",
"axios": "^1.13.1",
"core-js": "^3.46.0", "core-js": "^3.46.0",
"css-loader": "^3.6", "eslint": "^8.57.0",
"eslint": "^7.32.0", "eslint-plugin-prettier": "^5.1.0",
"eslint-config-prettier": "^8.10.2", "eslint-plugin-vue": "^9.20.0",
"eslint-plugin-prettier": "^3.4.1", "less": "^4.5.1",
"eslint-plugin-vue": "^7.20.0",
"less": "^4.4.2",
"less-loader": "^11.1.4",
"material-symbols": "^0.39.1", "material-symbols": "^0.39.1",
"prettier": "^2.8.8", "prettier": "^3.2.8"
"uikit": "^3.24.2",
"vue": "^2.7",
"vue-template-compiler": "^2.7",
"vuejs-paginate": "^2.1",
"vuex": "^3.6"
}, },
"postcss": { "postcss": {
"plugins": { "plugins": {

View file

@ -1,4 +1,4 @@
<template> <template :key="shortcut.shortcut">
<div id="app" class="uk-height-1-1 uk-margin-remove uk-padding-remove" :class="handleTheme"> <div id="app" class="uk-height-1-1 uk-margin-remove uk-padding-remove" :class="handleTheme">
<!-- this stops the app loading until setConnected is committed in the store, this means <!-- this stops the app loading until setConnected is committed in the store, this means
other components will not load until we have Thing Descriptions. --> other components will not load until we have Thing Descriptions. -->
@ -10,7 +10,6 @@
<button class="uk-modal-close-default" type="button" uk-close></button> <button class="uk-modal-close-default" type="button" uk-close></button>
<div <div
v-for="shortcut in keyboardManual" v-for="shortcut in keyboardManual"
:key="shortcut.shortcut"
class="uk-margin-small" class="uk-margin-small"
uk-grid uk-grid
> >
@ -26,10 +25,12 @@
// Import components // Import components
import appContent from "./components/appContent.vue"; import appContent from "./components/appContent.vue";
import loadingContent from "./components/loadingContent.vue"; import loadingContent from "./components/loadingContent.vue";
import MouseTrap from "mousetrap";
// vue3 migration
import { markRaw } from "vue";
import { eventBus } from "./eventBus.js";
var Mousetrap = require("mousetrap"); MouseTrap.prototype.stopCallback = function (e, element) {
Mousetrap.prototype.stopCallback = function (e, element) {
// if the element has the class "mousetrap" then no need to stop // if the element has the class "mousetrap" then no need to stop
if ((" " + element.className + " ").indexOf(" mousetrap ") > -1) { if ((" " + element.className + " ").indexOf(" mousetrap ") > -1) {
return false; return false;
@ -55,7 +56,7 @@ export default {
components: { components: {
appContent, appContent,
loadingContent, loadingContent
}, },
data: function () { data: function () {
@ -153,10 +154,10 @@ export default {
// Focus keys // Focus keys
Mousetrap.bind("pageup", () => { Mousetrap.bind("pageup", () => {
this.$root.$emit("globalMoveStepEvent", 0, 0, 1); eventBus.emit("globalMoveStepEvent", {x: 0, y: 0, z: 1});
}); });
Mousetrap.bind("pagedown", () => { Mousetrap.bind("pagedown", () => {
this.$root.$emit("globalMoveStepEvent", 0, 0, -1); eventBus.emit("globalMoveStepEvent", {x: 0, y: 0, z: -1});
}); });
this.keyboardManual.push({ this.keyboardManual.push({
shortcut: "pgup / pgdn", shortcut: "pgup / pgdn",
@ -165,7 +166,7 @@ export default {
// Capture // Capture
Mousetrap.bind("c", () => { Mousetrap.bind("c", () => {
this.$root.$emit("globalCaptureEvent"); eventBus.emit("globalCaptureEvent", {});
}); });
this.keyboardManual.push({ this.keyboardManual.push({
shortcut: "c", shortcut: "c",
@ -174,7 +175,7 @@ export default {
// Autofocus // Autofocus
Mousetrap.bind("a", () => { Mousetrap.bind("a", () => {
this.$root.$emit("globalFastAutofocusEvent"); eventBus.emit("globalFastAutofocusEvent", {});
}); });
this.keyboardManual.push({ this.keyboardManual.push({
shortcut: "a", shortcut: "a",
@ -183,10 +184,10 @@ export default {
// Increment/decrement tab // Increment/decrement tab
Mousetrap.bind("shift+down", () => { Mousetrap.bind("shift+down", () => {
this.$root.$emit("globalIncrementTab"); eventBus.emit("globalIncrementTab", {});
}); });
Mousetrap.bind("shift+up", () => { Mousetrap.bind("shift+up", () => {
this.$root.$emit("globalDecrementTab"); eventBus.emit("globalDecrementTab", {});
}); });
this.keyboardManual.push({ this.keyboardManual.push({
shortcut: "shift+↑ / shift+↓", shortcut: "shift+↑ / shift+↓",
@ -194,7 +195,7 @@ export default {
}); });
}, },
beforeDestroy: function () { beforeUnmount: function () {
// Disconnect the theme observer // Disconnect the theme observer
if (this.themeObserver) { if (this.themeObserver) {
this.themeObserver.disconnect(); this.themeObserver.disconnect();
@ -237,7 +238,7 @@ export default {
} }
}, },
handleExit: function () { handleExit: function () {
this.$root.$emit("globalTogglePreview", false); eventBus.emit("globalTogglePreview", false);
}, },
// Handle global mouse wheel events to be associated with navigation // Handle global mouse wheel events to be associated with navigation
@ -249,7 +250,7 @@ export default {
) { ) {
var z_rel = event.deltaY / 100; var z_rel = event.deltaY / 100;
// Emit a signal to move, acted on by panelControl.vue // Emit a signal to move, acted on by panelControl.vue
this.$root.$emit("globalMoveStepEvent", 0, 0, z_rel, false); eventBus.emit("globalMoveStepEvent", {x: 0, y: 0, z: z_rel, absolute: false});
} }
}, },
@ -276,7 +277,7 @@ export default {
} }
// Make a position request // Make a position request
// Emit a signal to move, acted on by panelControl.vue // Emit a signal to move, acted on by panelControl.vue
this.$root.$emit("globalMoveStepEvent", x_rel, y_rel, z_rel); eventBus.emit("globalMoveStepEvent", {x: x_rel, y: y_rel, z: 0});
}, },
}, },
}; };

View file

@ -9,11 +9,10 @@
class="uk-flex uk-flex-column uk-padding-remove uk-width-auto uk-height-1-1 uk-text-center" class="uk-flex uk-flex-column uk-padding-remove uk-width-auto uk-height-1-1 uk-text-center"
> >
<!-- For each top tab --> <!-- For each top tab -->
<template v-for="(item, index) in topTabs"> <template v-for="(item, index) in topTabs" :key="item.id + '-tab-icon'">
<!-- Render the tab icon --> <!-- Render the tab icon -->
<tabIcon <tabIcon
:id="item.id + '-tab-icon'" :id="item.id + '-tab-icon'"
:key="item.id + '-tab-icon'"
:tab-i-d="item.id" :tab-i-d="item.id"
:title="item.title" :title="item.title"
:require-connection="true" :require-connection="true"
@ -37,11 +36,10 @@
<hr id="extension-tab-divider" /> <hr id="extension-tab-divider" />
<!-- For each bottom tab --> <!-- For each bottom tab -->
<template v-for="(item, index) in bottomTabs"> <template v-for="(item, index) in bottomTabs" :key="item.id + '-tab-icon'">
<!-- Render the tab icon --> <!-- Render the tab icon -->
<tabIcon <tabIcon
:id="item.id + '-tab-icon'" :id="item.id + '-tab-icon'"
:key="item.id + '-tab-icon'"
:tab-i-d="item.id" :tab-i-d="item.id"
:title="item.title" :title="item.title"
:require-connection="true" :require-connection="true"
@ -79,8 +77,8 @@
<script> <script>
// Import generic components // Import generic components
import tabIcon from "./genericComponents/tabIcon"; import tabIcon from "./genericComponents/tabIcon.vue";
import tabContent from "./genericComponents/tabContent"; import tabContent from "./genericComponents/tabContent.vue";
// Import new content components // Import new content components
import aboutContent from "./tabContentComponents/aboutContent.vue"; import aboutContent from "./tabContentComponents/aboutContent.vue";
@ -92,6 +90,9 @@ import scanListContent from "./tabContentComponents/scanListContent.vue";
import settingsContent from "./tabContentComponents/settingsContent.vue"; import settingsContent from "./tabContentComponents/settingsContent.vue";
import slideScanContent from "./tabContentComponents/slideScanContent.vue"; import slideScanContent from "./tabContentComponents/slideScanContent.vue";
import viewContent from "./tabContentComponents/viewContent.vue"; import viewContent from "./tabContentComponents/viewContent.vue";
// vue3 migration
import { shallowRef, markRaw, ref } from 'vue'
import { eventBus } from "../eventBus.js";
// Import modal components for device initialisation // Import modal components for device initialisation
import calibrationWizard from "./modalComponents/calibrationWizard.vue"; import calibrationWizard from "./modalComponents/calibrationWizard.vue";
@ -103,7 +104,7 @@ export default {
components: { components: {
tabIcon, tabIcon,
tabContent, tabContent,
calibrationWizard, calibrationWizard
}, },
data: function () { data: function () {
return { return {
@ -113,26 +114,26 @@ export default {
id: "settings", id: "settings",
title: "Settings", title: "Settings",
icon: "settings", icon: "settings",
component: settingsContent, component: markRaw(settingsContent),
class: "uk-margin-auto-top", class: "uk-margin-auto-top",
}, },
{ {
id: "logging", id: "logging",
title: "Logging", title: "Logging",
icon: "assignment_late", icon: "assignment_late",
component: loggingContent, component: markRaw(loggingContent),
}, },
{ {
id: "about", id: "about",
title: "About", title: "About",
icon: "info", icon: "info",
component: aboutContent, component: markRaw(aboutContent),
}, },
{ {
id: "power", id: "power",
title: "Power", title: "Power",
icon: "power_settings_new", icon: "power_settings_new",
component: powerContent, component: markRaw(powerContent),
}, },
], ],
coreTopTabs: [ coreTopTabs: [
@ -140,21 +141,21 @@ export default {
id: "view", id: "view",
title: "View", title: "View",
icon: "visibility", icon: "visibility",
component: viewContent, component: markRaw(viewContent),
requiredThings: [], requiredThings: [],
}, },
{ {
id: "control", id: "control",
title: "Control", title: "Control",
icon: "gamepad", icon: "gamepad",
component: controlContent, component: markRaw(controlContent),
requiredThings: [], requiredThings: [],
}, },
{ {
id: "background-detect", id: "background-detect",
title: "Background Detect", title: "Background Detect",
icon: "background_replace", icon: "background_replace",
component: backgroundDetectContent, component: markRaw(backgroundDetectContent),
// While stage isn't needed; automatic background detect has little function // While stage isn't needed; automatic background detect has little function
// for a manual microscope. // for a manual microscope.
requiredThings: ["stage"], requiredThings: ["stage"],
@ -163,14 +164,14 @@ export default {
id: "slide-scan", id: "slide-scan",
title: "Slide Scan", title: "Slide Scan",
icon: "settings_overscan", icon: "settings_overscan",
component: slideScanContent, component: markRaw(slideScanContent),
requiredThings: ["smart_scan"], requiredThings: ["smart_scan"],
}, },
{ {
id: "scan-list", id: "scan-list",
title: "Scan List", title: "Scan List",
icon: "photo_library", icon: "photo_library",
component: scanListContent, component: markRaw(scanListContent),
requiredThings: ["smart_scan"], requiredThings: ["smart_scan"],
}, },
], ],
@ -207,15 +208,15 @@ export default {
mounted() { mounted() {
// A global signal listener to switch tab // A global signal listener to switch tab
this.$root.$on("globalSwitchTab", (tabID) => { eventBus.on("globalSwitchTab", (tabID) => {
this.currentTab = tabID; this.currentTab = tabID;
}); });
// A global signal listener to increment tab // A global signal listener to increment tab
this.$root.$on("globalIncrementTab", () => { eventBus.on("globalIncrementTab", () => {
this.incrementTabBy(1); this.incrementTabBy(1);
}); });
// A global signal listener to decrement tab // A global signal listener to decrement tab
this.$root.$on("globalDecrementTab", () => { eventBus.on("globalDecrementTab", () => {
this.incrementTabBy(-1); this.incrementTabBy(-1);
}); });
if (this.$store.getters.ready) { if (this.$store.getters.ready) {

View file

@ -2,7 +2,6 @@
<div <div
id="stream-display" id="stream-display"
ref="streamDisplay" ref="streamDisplay"
v-observe-visibility="visibilityChanged"
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget" class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
> >
<img <img
@ -16,6 +15,9 @@
</template> </template>
<script> <script>
//vue3 migration
import { useIntersectionObserver } from '@vueuse/core';
// Export main app // Export main app
export default { export default {
name: "MiniStreamDisplay", name: "MiniStreamDisplay",
@ -31,10 +33,17 @@ export default {
return `${this.$store.getters.baseUri}/camera/mjpeg_stream`; return `${this.$store.getters.baseUri}/camera/mjpeg_stream`;
}, },
}, },
methods: {
visibilityChanged(isVisible) { mounted() {
this.isVisible = isVisible; useIntersectionObserver(
}, this.$refs.streamDisplay,
([{ isIntersecting }]) => {
this.isVisible = isIntersecting;
},
{
threshold: 0.0,
}
);
}, },
}; };
</script> </script>

View file

@ -1,5 +1,11 @@
<template> <template>
<a href="#" class="uk-link" :class="classObject" :uk-tooltip="tooltipOptions" @click="setThisTab"> <a
href="#"
class="uk-link"
:class="classObject"
:uk-tooltip="tooltipOptions ? tooltipOptions : null"
@click="setThisTab"
>
<slot></slot> <slot></slot>
<div v-if="showTitle" class="tabtitle"> <div v-if="showTitle" class="tabtitle">
{{ computedTitle }} {{ computedTitle }}

View file

@ -1,6 +1,7 @@
<template> <template>
<div v-observe-visibility="visibilityChanged" class="uk-margin-remove uk-padding-remove"> <div class="uk-margin-remove uk-padding-remove">
<button <button
ref="actionButton"
type="button" type="button"
:disabled="buttonDisabled" :disabled="buttonDisabled"
class="uk-button uk-width-1-1 uk-position-relative" class="uk-button uk-width-1-1 uk-position-relative"
@ -28,10 +29,34 @@
<script> <script>
import ActionProgressBar from "./actionProgressBar.vue"; import ActionProgressBar from "./actionProgressBar.vue";
import ActionStatusModal from "./actionStatusModal.vue"; import ActionStatusModal from "./actionStatusModal.vue";
//vue3 migration
import { eventBus } from "../../eventBus.js";
import { useIntersectionObserver } from "@vueuse/core";
export default { export default {
name: "ActionButton", name: "ActionButton",
components: { ActionProgressBar, ActionStatusModal },
emits: [
'update:progress',
'update:taskStarted',
'update:taskRunning',
'update:log',
'update:taskStatus',
'submit',
'taskStarted',
'taskRunning',
'response',
'completed',
'cancelled',
'finished',
'error'
],
components: {
ActionProgressBar,
ActionStatusModal
},
props: { props: {
action: { action: {
@ -129,31 +154,52 @@ export default {
}, },
watch: { watch: {
progress(newval) { progress: {
this.$emit("update:progress", newval); handler(newval) {
this.$emit("update:progress", newval);
},
}, },
taskStarted(newval) { taskStarted: {
this.$emit("update:taskStarted", newval); handler(newval) {
this.$emit("update:taskStarted", newval);
},
}, },
taskRunning(newval) { taskRunning: {
this.$emit("update:taskRunning", newval); handler(newval) {
this.$emit("update:taskRunning", newval);
},
}, },
log(newval) { log: {
this.$emit("update:log", newval); handler(newval) {
this.$emit("update:log", newval);
},
deep: true
}, },
taskStatus(newval) { taskStatus: {
this.$emit("update:taskStatus", newval); handler(newval) {
}, this.$emit("update:taskStatus", newval);
}, },
}
},
mounted() { mounted() {
useIntersectionObserver(
this.$refs.actionButton,
([{ isIntersecting }]) => {
this.visibilityChanged(isIntersecting);
},
{
threshold: 0.0, // Adjust as needed
},
);
// Check for already running tasks // Check for already running tasks
if (this.taskStarted != true) { if (this.taskStarted != true) {
this.checkExistingTasks(); this.checkExistingTasks();
} }
// A global signal listener to perform the action // A global signal listener to perform the action
if (this.submitOnEvent) { if (this.submitOnEvent) {
this.$root.$on(this.submitOnEvent, () => { eventBus.on(this.submitOnEvent, () => {
if (this.isDisabled) return; if (this.isDisabled) return;
// Bootstrap task if button is not disabled. // Bootstrap task if button is not disabled.
this.bootstrapTask(); this.bootstrapTask();
@ -161,9 +207,9 @@ export default {
} }
}, },
beforeDestroy() { beforeUnmount() {
if (this.submitOnEvent) { if (this.submitOnEvent) {
this.$root.$off(this.submitOnEvent); eventBus.off(this.submitOnEvent);
} }
}, },
@ -222,6 +268,7 @@ export default {
async startTask() { async startTask() {
// Starts a new Action task // Starts a new Action task
console.log("Starting task with data:", this.submitData);
this.$emit("submit", this.submitData); this.$emit("submit", this.submitData);
// Send a request to start a task // Send a request to start a task
this.taskStarted = true; this.taskStarted = true;

View file

@ -60,11 +60,16 @@ export default {
}, },
watch: { watch: {
log: function () { log: {
this.scrollToBottom(); handler() {
this.scrollToBottom();
},
deep: true,
}, },
taskStatus: function () { taskStatus: {
this.scrollToBottom(); handler() {
this.scrollToBottom();
}
}, },
}, },

View file

@ -28,11 +28,15 @@
import UIkit from "uikit"; import UIkit from "uikit";
import ActionProgressBar from "./actionProgressBar.vue"; import ActionProgressBar from "./actionProgressBar.vue";
import ActionLogDisplay from "./actionLogDisplay.vue"; import ActionLogDisplay from "./actionLogDisplay.vue";
// vue3 migration
import { eventBus } from "../../eventBus.js";
export default { export default {
name: "ActionStatusModal", name: "ActionStatusModal",
components: { ActionProgressBar, ActionLogDisplay }, components: {
ActionProgressBar,
ActionLogDisplay
},
props: { props: {
title: { title: {
type: String, type: String,
@ -71,7 +75,7 @@ export default {
}, },
hide() { hide() {
UIkit.modal(this.$refs.modal).hide(); UIkit.modal(this.$refs.modal).hide();
this.$root.$emit("modalClosed"); eventBus.emit("modalClosed");
}, },
}, },
}; };

View file

@ -100,12 +100,13 @@
<script> <script>
import syncPropertyButton from "./syncPropertyButton.vue"; import syncPropertyButton from "./syncPropertyButton.vue";
// vue3 migration
export default { export default {
name: "InputFromSchema", name: "InputFromSchema",
components: { components: {
syncPropertyButton, syncPropertyButton
}, },
props: { props: {
@ -113,9 +114,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
value: { modelValue: {
type: null, type: null,
required: true, required: false,
default: undefined,
}, },
label: { label: {
type: String, type: String,
@ -133,11 +135,11 @@ export default {
// the value is an array or object. For future updates we stringify and parse // the value is an array or object. For future updates we stringify and parse
// (see resetInternalValue). If we do this here there is a chance we get errors // (see resetInternalValue). If we do this here there is a chance we get errors
// as internalValue is still null when rendering starts. // as internalValue is still null when rendering starts.
internalValue: Array.isArray(this.value) internalValue: Array.isArray(this.modelValue)
? [...this.value] ? [...this.modelValue]
: typeof this.value === "object" : typeof this.modelValue === "object"
? { ...this.value } ? { ...this.modelValue }
: this.value, : this.modelValue,
// Is edited can't be computed as we mutate internalValue // Is edited can't be computed as we mutate internalValue
isEdited: false, isEdited: false,
animateUpdate: false, animateUpdate: false,
@ -206,14 +208,18 @@ export default {
}, },
watch: { watch: {
value() { modelValue: {
// Fire updateIsEdited on both value and internal value change, handler() {
// as change in value may not causse internalValue to change. // Fire updateIsEdited on both value and internal value change,
this.updateIsEdited(); // as change in value may not causse internalValue to change.
this.resetInternalValue(); this.updateIsEdited();
this.resetInternalValue();
},
}, },
internalValue() { internalValue: {
this.updateIsEdited(); handler() {
this.updateIsEdited();
},
}, },
animate(updated) { animate(updated) {
if (updated) { if (updated) {
@ -223,7 +229,7 @@ export default {
}, },
mounted() { mounted() {
if (this.value !== undefined) { if (this.modelValue !== undefined) {
this.resetInternalValue(); this.resetInternalValue();
} }
}, },
@ -233,7 +239,7 @@ export default {
// Whenever updatirng th internal value stringify and parse as a form of deepcopy. // Whenever updatirng th internal value stringify and parse as a form of deepcopy.
// This ensure that the this.value prop is not mutated for when elements of arrays // This ensure that the this.value prop is not mutated for when elements of arrays
// or objects are updated. // or objects are updated.
this.internalValue = JSON.parse(JSON.stringify(this.value)); this.internalValue = JSON.parse(JSON.stringify(this.modelValue));
}, },
requestUpdate: async function () { requestUpdate: async function () {
this.$emit("requestUpdate"); this.$emit("requestUpdate");
@ -262,7 +268,7 @@ export default {
} }
}, },
updateIsEdited: function () { updateIsEdited: function () {
this.isEdited = this.deepStringify(this.internalValue) !== this.deepStringify(this.value); this.isEdited = this.deepStringify(this.internalValue) !== this.deepStringify(this.modelValue);
}, },
animationEnd: function () { animationEnd: function () {
this.animateUpdate = false; this.animateUpdate = false;

View file

@ -1,6 +1,6 @@
<template> <template>
<input-from-schema <input-from-schema
v-model="value" v-model="modelValue"
:data-schema="propertyDescription" :data-schema="propertyDescription"
:label="label" :label="label"
:animate="animate" :animate="animate"
@ -13,12 +13,14 @@
<script> <script>
import { formatValue } from "@/js_utils/formatter.mjs"; import { formatValue } from "@/js_utils/formatter.mjs";
import InputFromSchema from "./inputFromSchema.vue"; import InputFromSchema from "./inputFromSchema.vue";
// vue3 migration
export default { export default {
name: "PropertyControl", name: "PropertyControl",
components: { components: {
InputFromSchema, InputFromSchema
}, },
props: { props: {
@ -48,7 +50,7 @@ export default {
data() { data() {
return { return {
value: undefined, modelValue: undefined,
animate: false, animate: false,
}; };
}, },
@ -74,7 +76,7 @@ export default {
// Read the property when we're mounted - usually this won't // Read the property when we're mounted - usually this won't
// work because the URL isn't set yet. However, it's helpful if // work because the URL isn't set yet. However, it's helpful if
// the app is reloaded (e.g. from a dev server). // the app is reloaded (e.g. from a dev server).
if (this.value == undefined) { if (this.modelValue == undefined) {
this.readProperty(); this.readProperty();
} }
}, },
@ -82,12 +84,12 @@ export default {
methods: { methods: {
readProperty: async function () { readProperty: async function () {
let data = await this.readThingProperty(this.thingName, this.propertyName); let data = await this.readThingProperty(this.thingName, this.propertyName);
this.value = data; this.modelValue = data;
return data; return data;
}, },
writeProperty: async function (requestedValue) { writeProperty: async function (requestedValue) {
try { try {
this.value = requestedValue; this.modelValue = requestedValue;
await this.writeThingProperty(this.thingName, this.propertyName, requestedValue); await this.writeThingProperty(this.thingName, this.propertyName, requestedValue);
if (this.readBack) { if (this.readBack) {
await new Promise((r) => setTimeout(r, this.readBackDelay)); await new Promise((r) => setTimeout(r, this.readBackDelay));

View file

@ -17,13 +17,14 @@
<script> <script>
import ActionButton from "./actionButton.vue"; import ActionButton from "./actionButton.vue";
// vue3 migration
// Export main app // Export main app
export default { export default {
name: "ServerSpecifiedActionButton", name: "ServerSpecifiedActionButton",
components: { components: {
ActionButton, ActionButton
}, },
props: { props: {

View file

@ -11,13 +11,14 @@
<script> <script>
import PropertyControl from "./propertyControl.vue"; import PropertyControl from "./propertyControl.vue";
// vue3 migration
// Export main app // Export main app
export default { export default {
name: "ServerSpecifiedPropertyControl", name: "ServerSpecifiedPropertyControl",
components: { components: {
PropertyControl, PropertyControl
}, },
props: { props: {

View file

@ -14,12 +14,16 @@
<script> <script>
import devTools from "./tabContentComponents/aboutComponents/devTools.vue"; import devTools from "./tabContentComponents/aboutComponents/devTools.vue";
// vue3 migration
import { markRaw } from "vue";
// Export main app // Export main app
export default { export default {
name: "LoadingContent", name: "LoadingContent",
components: { devTools }, components: {
devTools
},
data: function () { data: function () {
return {}; return {};

View file

@ -4,10 +4,10 @@
<h2 class="uk-modal-title">Microscope Calibration</h2> <h2 class="uk-modal-title">Microscope Calibration</h2>
<component <component
v-bind="currentTask.props"
:is="currentTask.component" :is="currentTask.component"
v-if="currentTask" v-if="currentTask"
:key="taskIndex" :key="taskIndex"
v-bind="currentTask.props"
:first="isFirstTask" :first="isFirstTask"
:final="isFinalTask" :final="isFinalTask"
:start-on-last="movingBackward" :start-on-last="movingBackward"
@ -28,7 +28,13 @@ import finalStep from "./calibrationWizardComponents/finalStep.vue";
export default { export default {
name: "CalibrationWizard", name: "CalibrationWizard",
components: {}, components: {
singleStepTask,
welcomeStep,
cameraCalibrationTask,
cameraStageMappingTask,
finalStep
},
data: function () { data: function () {
return { return {

View file

@ -23,10 +23,10 @@ gets very confusing.
<div> <div>
<h3 v-if="title">{{ title }}</h3> <h3 v-if="title">{{ title }}</h3>
<component <component
v-bind="currentStep.props"
:is="currentStep.component" :is="currentStep.component"
v-if="currentStep" v-if="currentStep"
:key="stepIndex" :key="stepIndex"
v-bind="currentStep.props"
@awaiting-user="handleAwaitingUser" @awaiting-user="handleAwaitingUser"
/> />
<p class="uk-text-right"> <p class="uk-text-right">

View file

@ -19,13 +19,14 @@
<script> <script>
import stepTemplateWithStream from "../stepTemplateWithStream.vue"; import stepTemplateWithStream from "../stepTemplateWithStream.vue";
import cameraCalibrationSettings from "../../../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue"; import cameraCalibrationSettings from "../../../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue";
// vue3 migration
export default { export default {
name: "CameraMainCalibrationStep", name: "CameraMainCalibrationStep",
components: { components: {
stepTemplateWithStream, stepTemplateWithStream,
cameraCalibrationSettings, cameraCalibrationSettings
}, },
computed: { computed: {

View file

@ -14,10 +14,13 @@
import calibrationWizardTask from "./calibrationWizardTask.vue"; import calibrationWizardTask from "./calibrationWizardTask.vue";
import camCalibrationExplanation from "./cameraCalibrationSteps/camCalibrationExplanation.vue"; import camCalibrationExplanation from "./cameraCalibrationSteps/camCalibrationExplanation.vue";
import cameraMainCalibrationStep from "./cameraCalibrationSteps/cameraMainCalibrationStep.vue"; import cameraMainCalibrationStep from "./cameraCalibrationSteps/cameraMainCalibrationStep.vue";
// vue3 migration
import { markRaw } from "vue";
export default { export default {
name: "CameraCalibrationTask", name: "CameraCalibrationTask",
components: { calibrationWizardTask }, components: {
calibrationWizardTask},
props: { props: {
// Standard calibrationWizardTask props below: // Standard calibrationWizardTask props below:
first: Boolean, first: Boolean,
@ -30,7 +33,7 @@ export default {
data: function () { data: function () {
return { return {
steps: [{ component: camCalibrationExplanation }, { component: cameraMainCalibrationStep }], steps: [{ component: markRaw(camCalibrationExplanation) }, { component: markRaw(cameraMainCalibrationStep)}],
}; };
}, },
}; };

View file

@ -15,10 +15,14 @@ import calibrationWizardTask from "./calibrationWizardTask.vue";
import csmExplanation from "./csmSteps/csmExplanation.vue"; import csmExplanation from "./csmSteps/csmExplanation.vue";
import focusStep from "./csmSteps/focusStep.vue"; import focusStep from "./csmSteps/focusStep.vue";
import runCsmStep from "./csmSteps/runCsmStep.vue"; import runCsmStep from "./csmSteps/runCsmStep.vue";
// vue3 migration
import { markRaw } from "vue";
export default { export default {
name: "CameraCalibrationTask", name: "CameraCalibrationTask",
components: { calibrationWizardTask }, components: {
calibrationWizardTask
},
props: { props: {
// Standard calibrationWizardTask props below: // Standard calibrationWizardTask props below:
first: Boolean, first: Boolean,
@ -31,7 +35,7 @@ export default {
data: function () { data: function () {
return { return {
steps: [{ component: csmExplanation }, { component: focusStep }, { component: runCsmStep }], steps: [{ component: markRaw(csmExplanation) }, { component: markRaw(focusStep) }, { component: markRaw(runCsmStep) }],
}; };
}, },
}; };

View file

@ -30,12 +30,14 @@
<script> <script>
import stepTemplateWithStream from "../stepTemplateWithStream.vue"; import stepTemplateWithStream from "../stepTemplateWithStream.vue";
import ActionButton from "../../../labThingsComponents/actionButton.vue"; import ActionButton from "../../../labThingsComponents/actionButton.vue";
// vue3 migration
export default { export default {
name: "CameraMainCalibrationStep", name: "CameraMainCalibrationStep",
components: { components: {
stepTemplateWithStream, stepTemplateWithStream,
ActionButton, ActionButton
}, },
}; };
</script> </script>
@ -47,7 +49,7 @@ export default {
gap: 8px; /* Small space between buttons */ gap: 8px; /* Small space between buttons */
margin-top: 4px; /* Gap from image */ margin-top: 4px; /* Gap from image */
} }
.moveZ >>> .uk-button.uk-width-1-1 { .moveZ :deep(.uk-button.uk-width-1-1) {
line-height: 50px; line-height: 50px;
font-size: 50px !important; font-size: 50px !important;
height: 60px; height: 60px;

View file

@ -18,6 +18,7 @@
<script> <script>
import stepTemplateWithStream from "../stepTemplateWithStream.vue"; import stepTemplateWithStream from "../stepTemplateWithStream.vue";
import CSMCalibrationSettings from "../../../tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue"; import CSMCalibrationSettings from "../../../tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue";
// vue3 migration
export default { export default {
name: "CameraMainCalibrationStep", name: "CameraMainCalibrationStep",

View file

@ -24,10 +24,14 @@ supplied by the wizard.
<script> <script>
import calibrationWizardTask from "./calibrationWizardTask.vue"; import calibrationWizardTask from "./calibrationWizardTask.vue";
// vue3 migration
import { markRaw } from "vue";
export default { export default {
name: "SingleStepTask", name: "SingleStepTask",
components: { calibrationWizardTask }, components: {
calibrationWizardTask
},
props: { props: {
// This must be sent // This must be sent
stepComponent: { stepComponent: {
@ -54,7 +58,7 @@ export default {
data: function () { data: function () {
return { return {
steps: [{ component: this.stepComponent, props: this.stepProps }], steps: [{ component: markRaw(this.stepComponent), props: this.stepProps }],
}; };
}, },
}; };

View file

@ -8,12 +8,13 @@
<script> <script>
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue"; import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
// vue3 migration
export default { export default {
name: "StepTemplateWithStream", name: "StepTemplateWithStream",
components: { components: {
miniStreamDisplay, miniStreamDisplay
}, },
}; };
</script> </script>

View file

@ -64,10 +64,13 @@
<script> <script>
import ActionButton from "../../labThingsComponents/actionButton.vue"; import ActionButton from "../../labThingsComponents/actionButton.vue";
// vue3 migration
export default { export default {
name: "StatusPane", name: "StatusPane",
components: { ActionButton }, components: {
ActionButton
},
data: function () { data: function () {
return { return {

View file

@ -24,13 +24,14 @@
<script> <script>
import devTools from "./aboutComponents/devTools.vue"; import devTools from "./aboutComponents/devTools.vue";
import statusPane from "./aboutComponents/statusPane.vue"; import statusPane from "./aboutComponents/statusPane.vue";
//vue3 migration
export default { export default {
name: "AboutContent", name: "AboutContent",
components: { components: {
devTools, devTools,
statusPane, statusPane
}, },
}; };
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div v-observe-visibility="visibilityChanged" class="uk-padding-small"> <div ref="backgroundDetectContent" class="uk-padding-small">
<div> <div>
<ul uk-accordion="multiple: true"> <ul uk-accordion="multiple: true">
<li> <li>
@ -47,11 +47,13 @@
<script> <script>
import ActionButton from "../../labThingsComponents/actionButton.vue"; import ActionButton from "../../labThingsComponents/actionButton.vue";
import ServerSpecifiedPropertyControl from "../../labThingsComponents/serverSpecifiedPropertyControl.vue"; import ServerSpecifiedPropertyControl from "../../labThingsComponents/serverSpecifiedPropertyControl.vue";
// vue3 migration
import { useIntersectionObserver } from "@vueuse/core";
export default { export default {
components: { components: {
ActionButton, ActionButton,
ServerSpecifiedPropertyControl, ServerSpecifiedPropertyControl
}, },
data() { data() {
@ -64,14 +66,20 @@ export default {
}; };
}, },
async created() {
this.readSettings();
},
methods: { methods: {
async safeReadSettings() {
if (!this.$store.state.connected) return;
try {
await this.readSettings();
} catch (error) {
console.error("Error reading background detector settings:", error);
}
},
visibilityChanged(isVisible) { visibilityChanged(isVisible) {
if (isVisible) { if (isVisible) {
this.readSettings(); this.safeReadSettings();
} }
}, },
alertBackgroundSet() { alertBackgroundSet() {
@ -99,6 +107,18 @@ export default {
} }
}, },
}, },
mounted() {
useIntersectionObserver(
this.$refs.backgroundDetectContent,
([{ isIntersecting }]) => {
this.visibilityChanged(isIntersecting);
},
{
threshold: 0.0,
},
);
},
}; };
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View file

@ -13,13 +13,14 @@
<script> <script>
import paneBackgroundDetect from "./backgroundDetectComponents/paneBackgroundDetect"; import paneBackgroundDetect from "./backgroundDetectComponents/paneBackgroundDetect";
import streamDisplay from "./streamContent.vue"; import streamDisplay from "./streamContent.vue";
// vue3 migration
export default { export default {
name: "BackgroundDetectContent", name: "BackgroundDetectContent",
components: { components: {
paneBackgroundDetect, paneBackgroundDetect,
streamDisplay, streamDisplay
}, },
}; };
</script> </script>

View file

@ -18,12 +18,14 @@
</template> </template>
<script> <script>
import ActionButton from "../../labThingsComponents/actionButton.vue"; import ActionButton from "../../labThingsComponents/actionButton.vue";
// vue3 migration
import { eventBus } from "../../../eventBus.js";
export default { export default {
name: "AutofocusControl", name: "AutofocusControl",
components: { components: {
ActionButton, ActionButton
}, },
data: function () { data: function () {
@ -38,7 +40,7 @@ export default {
}, },
afterAutofocus() { afterAutofocus() {
this.isAutofocusing = false; this.isAutofocusing = false;
this.$root.$emit("globalUpdatePositionEvent"); eventBus.emit("globalUpdatePositionEvent");
}, },
}, },
}; };

View file

@ -34,6 +34,7 @@ import axios from "axios";
import ActionButton from "../../labThingsComponents/actionButton.vue"; import ActionButton from "../../labThingsComponents/actionButton.vue";
import positionControl from "./positionControl.vue"; import positionControl from "./positionControl.vue";
import autofocusControl from "./autofocusControl.vue"; import autofocusControl from "./autofocusControl.vue";
// vue3 migration
export default { export default {
name: "PaneControl", name: "PaneControl",
@ -41,7 +42,7 @@ export default {
components: { components: {
ActionButton, ActionButton,
positionControl, positionControl,
autofocusControl, autofocusControl
}, },
computed: { computed: {

View file

@ -15,8 +15,8 @@ and zero position buttons. It also includes the d-pad.
<!-- Text boxes to set and view position --> <!-- Text boxes to set and view position -->
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
<input <input
v-for="(_v, key) in setPosition"
:key="`setPosition_${key}`" :key="`setPosition_${key}`"
v-for="(_v, key) in setPosition"
v-model="setPosition[key]" v-model="setPosition[key]"
class="uk-form-small numeric-setting-line-input" class="uk-form-small numeric-setting-line-input"
type="number" type="number"
@ -61,13 +61,16 @@ and zero position buttons. It also includes the d-pad.
import ActionButton from "../../labThingsComponents/actionButton.vue"; import ActionButton from "../../labThingsComponents/actionButton.vue";
import syncPropertyButton from "../../labThingsComponents/syncPropertyButton.vue"; import syncPropertyButton from "../../labThingsComponents/syncPropertyButton.vue";
import stageControlButtons from "./stageControlButtons.vue"; import stageControlButtons from "./stageControlButtons.vue";
// vue3 migration
import { eventBus } from "../../../eventBus.js";
export default { export default {
name: "PaneControl", name: "PaneControl",
components: { components: {
ActionButton, ActionButton,
syncPropertyButton, syncPropertyButton,
stageControlButtons, stageControlButtons
}, },
data: function () { data: function () {
@ -79,45 +82,57 @@ export default {
computed: { computed: {
positionStatusUri: function () { positionStatusUri: function () {
console.log("Computing position status URI", this.thingActionUrl("stage", "position"));
return this.thingActionUrl("stage", "position"); return this.thingActionUrl("stage", "position");
}, },
}, },
async mounted() { async mounted() {
let self = this;
// A global signal listener to perform a move action // A global signal listener to perform a move action
this.$root.$on("globalMoveEvent", self.move); eventBus.on("globalMoveEvent", this.move);
this.$root.$on("globalUpdatePositionEvent", self.updatePosition); // A global signal listener to update position text boxes
eventBus.on("globalUpdatePositionEvent", this.updatePosition);
// A global signal listener to perform a move action in pixels // A global signal listener to perform a move action in pixels
this.$root.$on("globalMoveInImageCoordinatesEvent", (x, y, absolute) => { eventBus.on("globalMoveInImageCoordinatesEvent", this.onMoveImage);
this.moveInImageCoordinatesRequest(x, y, absolute);
});
// A global signal listener to perform a move in multiples of a step size // A global signal listener to perform a move in multiples of a step size
this.$root.$on("globalMoveStepEvent", (x_steps, y_steps, z_steps) => { eventBus.on("globalMoveStepEvent", this.onMoveStep);
const navigationStepSize = this.$store.state.navigationStepSize;
const navigationInvert = this.$store.state.navigationInvert;
const x = x_steps * navigationStepSize.x * (navigationInvert.x ? -1 : 1);
const y = y_steps * navigationStepSize.y * (navigationInvert.y ? -1 : 1);
const z = z_steps * navigationStepSize.z * (navigationInvert.z ? -1 : 1);
this.$root.$emit("globalMoveEvent", x, y, z, false);
});
// Update the current position in text boxes // Update the current position in text boxes
await this.updatePosition(); await this.updatePosition();
}, },
beforeDestroy() { beforeUnmount() {
// Remove global signal listener to perform a move action // Remove global signal listener to perform a move action
this.$root.$off("globalMoveEvent"); eventBus.off("globalMoveEvent", this.move);
this.$root.$off("globalMoveInImageCoordinatesEvent"); eventBus.off("globalUpdatePositionEvent", this.updatePosition);
this.$root.$off("globalMoveStepEvent"); eventBus.off("globalMoveInImageCoordinatesEvent", this.onMoveImage);
this.$root.$off("globalUpdatePositionEvent"); eventBus.off("globalMoveStepEvent", this.onMoveStep);
}, },
methods: { methods: {
timeout(ms) { timeout(ms) {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
}, },
async move(x, y, z, absolute) {
onMoveImage(payload) {
console.log("Received move in image coordinates:", payload);
this.moveInImageCoordinatesRequest(payload.x, payload.y, payload.absolute);
},
onMoveStep(payload) {
console.log("Received key move step event:", payload);
const { x: x_steps, y: y_steps, z: z_steps } = payload;
const navigationStepSize = this.$store.state.navigationStepSize;
const navigationInvert = this.$store.state.navigationInvert;
const x = x_steps * navigationStepSize.x * (navigationInvert.x ? -1 : 1);
const y = y_steps * navigationStepSize.y * (navigationInvert.y ? -1 : 1);
const z = z_steps * navigationStepSize.z * (navigationInvert.z ? -1 : 1);
const movePayload = { x, y, z, absolute: false };
eventBus.emit("globalMoveEvent", movePayload);
console.log("Emitted global move event with:", movePayload);
},
async move(payload) {
const { x, y, z, absolute } = payload;
// Move the stage, by updating the controls and starting a move task // Move the stage, by updating the controls and starting a move task
// This is equivalent to clicking the "move" button. // This is equivalent to clicking the "move" button.
if (this.moveLock) return; // Discard move requests if we're already moving if (this.moveLock) return; // Discard move requests if we're already moving
@ -133,15 +148,17 @@ export default {
y: this.setPosition.y + y, y: this.setPosition.y + y,
z: this.setPosition.z + z, z: this.setPosition.z + z,
}; };
console.log("Updated setPosition for relative move:", this.setPosition);
} }
await this.timeout(1); // Wait for Vue to update the position await this.$nextTick(); // Wait for Vue to update the position
await this.startMoveTask(); await this.startMoveTask();
}, },
async startMoveTask() { async startMoveTask() {
this.moveLock = true; //this.moveLock = true;
await this.$refs.moveButton.startTask(); await this.$refs.moveButton.startTask();
}, },
moveComplete() { moveComplete() {
console.log("Move completed.");
this.updatePosition(); this.updatePosition();
this.moveLock = false; this.moveLock = false;
}, },

View file

@ -37,11 +37,13 @@
</template> </template>
<script> <script>
import { eventBus } from "../../../eventBus.js";
export default { export default {
name: "StageControlButtons", name: "StageControlButtons",
methods: { methods: {
move(x, y, z) { move(x, y, z) {
this.$root.$emit("globalMoveStepEvent", x, y, z); eventBus.emit("globalMoveStepEvent", {x, y, z, absolute: false});
}, },
}, },
}; };

View file

@ -11,15 +11,16 @@
</template> </template>
<script> <script>
import paneControl from "./controlComponents/paneControl"; import paneControl from "./controlComponents/paneControl.vue";
import streamDisplay from "./streamContent.vue"; import streamDisplay from "./streamContent.vue";
// vue3 migration
export default { export default {
name: "ControlContent", name: "ControlContent",
components: { components: {
paneControl, paneControl,
streamDisplay, streamDisplay
}, },
}; };
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div v-observe-visibility="visibilityChanged" class="uk-padding uk-padding-remove-top"> <div ref="loggingDisplay" class="uk-padding uk-padding-remove-top">
<!-- Logging nav bar --> <!-- Logging nav bar -->
<nav class="logging-navbar uk-navbar-container uk-navbar-transparent" uk-navbar="mode: click"> <nav class="logging-navbar uk-navbar-container uk-navbar-transparent" uk-navbar="mode: click">
<!-- Left side controls --> <!-- Left side controls -->
@ -83,13 +83,15 @@
import axios from "axios"; import axios from "axios";
import Paginate from "vuejs-paginate"; import Paginate from "vuejs-paginate";
import EndpointButton from "../labThingsComponents/endpointButton.vue"; import EndpointButton from "../labThingsComponents/endpointButton.vue";
//vue3 migration
import { useIntersectionObserver } from "@vueuse/core";
export default { export default {
name: "LoggingContent", name: "LoggingContent",
components: { components: {
Paginate, Paginate,
EndpointButton, EndpointButton
}, },
data: function () { data: function () {
@ -133,6 +135,18 @@ export default {
}, },
}, },
mounted() {
useIntersectionObserver(
this.$refs.loggingDisplay,
([{ isIntersecting }]) => {
this.visibilityChanged(isIntersecting);
},
{
threshold: 0.0,
},
);
},
methods: { methods: {
scrollToTop() { scrollToTop() {
this.$emit("scrollTop"); this.$emit("scrollTop");

View file

@ -1,11 +1,13 @@
<template> <template>
<div v-observe-visibility="visibilityChanged"> <div ref="osdViewerContainer" class="osd-viewer-container uk-height-1-1">
<div id="openseadragon" ref="osdContainer"></div> <div id="openseadragon" ref="osdContainer"></div>
</div> </div>
</template> </template>
<script> <script>
import OpenSeaDragon from "openseadragon"; import OpenSeaDragon from "openseadragon";
// vue3 migration
import { useIntersectionObserver } from "@vueuse/core";
export default { export default {
name: "OpenSeadragonViewer", name: "OpenSeadragonViewer",
@ -54,24 +56,43 @@ export default {
}, },
async mounted() { async mounted() {
useIntersectionObserver(
this.$refs.osdViewerContainer,
([{ isIntersecting }]) => {
this.visibilityChanged(isIntersecting);
},
{ threshold: 0.0 }
);
if (this.src) { if (this.src) {
this.loadOpenSeaDragon(this.src); this.loadOpenSeaDragon(this.src);
} }
}, },
beforeDestroy() { beforeUnmount() {
// Remove global signal listener to perform a gallery refresh // Remove global signal listener to perform a gallery refresh
this.osdViewer.destroy(); if (this.osdViewer) {
this.osdViewer.destroy();
}
}, },
methods: { methods: {
visibilityChanged(isVisible) { visibilityChanged(isVisible) {
// adding this check to avoid error when the viewer is not yet initialized.
if (isVisible) { if (isVisible) {
this.loadOpenSeaDragon();
if (!this.osdViewer && this.src) {
this.loadOpenSeaDragon(this.src);
}
} else { } else {
this.osdViewer.destroy();
if (this.osdViewer) {
this.osdViewer.destroy();
this.osdViewer = null;
}
} }
}, },
async loadOpenSeaDragon() { async loadOpenSeaDragon() {
if (this.osdViewer) { if (this.osdViewer) {
this.osdViewer.destroy(); this.osdViewer.destroy();

View file

@ -82,11 +82,15 @@
import axios from "axios"; import axios from "axios";
import actionButton from "../../labThingsComponents/actionButton.vue"; import actionButton from "../../labThingsComponents/actionButton.vue";
import EndpointButton from "../../labThingsComponents/endpointButton.vue"; import EndpointButton from "../../labThingsComponents/endpointButton.vue";
// vue3 migration
// Export main app // Export main app
export default { export default {
name: "ScanCard", name: "ScanCard",
components: { actionButton, EndpointButton }, components: {
actionButton,
EndpointButton
},
props: { props: {
scanData: { scanData: {

View file

@ -55,10 +55,13 @@
<script> <script>
import UIkit from "uikit"; import UIkit from "uikit";
import OpenSeadragonViewer from "./openSeadragonViewer.vue"; import OpenSeadragonViewer from "./openSeadragonViewer.vue";
// vue3 migration
export default { export default {
name: "ScanViewerModal", name: "ScanViewerModal",
components: { OpenSeadragonViewer }, components: {
OpenSeadragonViewer
},
props: { props: {
selectedScan: { selectedScan: {
type: Object, type: Object,

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
v-observe-visibility="visibilityChanged" ref="galleryDisplay"
class="galleryDisplay uk-padding uk-padding-remove-top" class="galleryDisplay uk-padding uk-padding-remove-top"
> >
<!-- Gallery nav bar --> <!-- Gallery nav bar -->
@ -100,11 +100,18 @@ import axios from "axios";
import actionButton from "../labThingsComponents/actionButton.vue"; import actionButton from "../labThingsComponents/actionButton.vue";
import scanCard from "./scanListComponents/scanCard.vue"; import scanCard from "./scanListComponents/scanCard.vue";
import ScanViewerModal from "./scanListComponents/scanViewer.vue"; import ScanViewerModal from "./scanListComponents/scanViewer.vue";
// vue3 migration
import { eventBus } from "../../eventBus.js";
import { useIntersectionObserver } from "@vueuse/core";
// Export main app // Export main app
export default { export default {
name: "ScanListContent", name: "ScanListContent",
components: { actionButton, scanCard, ScanViewerModal }, components: {
actionButton,
scanCard,
ScanViewerModal
},
data: function () { data: function () {
return { return {
@ -141,13 +148,22 @@ export default {
}, },
async mounted() { async mounted() {
useIntersectionObserver(
this.$refs.galleryDisplay,
([{ isIntersecting }]) => {
this.visibilityChanged(isIntersecting);
},
{
threshold: 0.0, // Adjust as needed
},
);
// Update on mount (does nothing if not connected) // Update on mount (does nothing if not connected)
await this.updateScans(); await this.updateScans();
// A global signal listener to perform a gallery refresh // A global signal listener to perform a gallery refresh
this.$root.$on("globalUpdateScans", () => { eventBus.on("globalUpdateScans", () => {
this.updateScans(); this.updateScans();
}); });
this.$root.$on("modalClosed", () => { eventBus.on("modalClosed", () => {
// Handle the modal closed event here // Handle the modal closed event here
this.updateScans(); this.updateScans();
}); });
@ -171,15 +187,15 @@ export default {
); );
}, },
beforeDestroy() { beforeUnmount() {
// Remove global signal listener to perform a gallery refresh // Remove global signal listener to perform a gallery refresh
this.$root.$off("globalUpdateScans"); eventBus.off("globalUpdateScans", this.updateScans);
// Then we call that function here to unwatch // Then we call that function here to unwatch
if (this.unwatchStoreFunction) { if (this.unwatchStoreFunction) {
this.unwatchStoreFunction(); this.unwatchStoreFunction();
this.unwatchStoreFunction = null; this.unwatchStoreFunction = null;
} }
this.$root.$off("modalClosed"); // Clean up event listener eventBus.off("modalClosed", this.updateScans); // Clean up event listener
}, },
methods: { methods: {

View file

@ -17,6 +17,7 @@
<script> <script>
import CSMCalibrationSettings from "./CSMSettingsComponents/CSMCalibrationSettings.vue"; import CSMCalibrationSettings from "./CSMSettingsComponents/CSMCalibrationSettings.vue";
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue"; import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
// vue3 migration
// Export main app // Export main app
export default { export default {
@ -24,7 +25,7 @@ export default {
components: { components: {
CSMCalibrationSettings, CSMCalibrationSettings,
miniStreamDisplay, miniStreamDisplay
}, },
}; };
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div id="CSMCalibrationSettings" v-observe-visibility="visibilityChanged"> <div id="CSMCalibrationSettings" ref="CSMCalibrationSettingsContainer" class="uk-width-large">
<!--Show auto calibrate if default plugin is enabled--> <!--Show auto calibrate if default plugin is enabled-->
<div v-if="'calibrate_xy' in actions" class="uk-margin-small"> <div v-if="'calibrate_xy' in actions" class="uk-margin-small">
<action-button <action-button
@ -57,6 +57,8 @@
<script> <script>
import ActionButton from "@/components/labThingsComponents/actionButton.vue"; import ActionButton from "@/components/labThingsComponents/actionButton.vue";
import matrixDisplay from "@/components/ui/matrixDisplay.vue"; import matrixDisplay from "@/components/ui/matrixDisplay.vue";
// vue3 migration
import { useIntersectionObserver } from "@vueuse/core";
// Export main app // Export main app
export default { export default {
@ -64,7 +66,7 @@ export default {
components: { components: {
ActionButton, ActionButton,
matrixDisplay, matrixDisplay
}, },
props: { props: {
@ -93,6 +95,16 @@ export default {
}, },
}, },
mounted() {
useIntersectionObserver(
this.$refs.CSMCalibrationSettingsContainer,
([{ isIntersecting }]) => {
this.visibilityChanged(isIntersecting);
},
{ threshold: 0.0 }
);
},
methods: { methods: {
visibilityChanged(isVisible) { visibilityChanged(isVisible) {
if (isVisible) { if (isVisible) {

View file

@ -25,6 +25,7 @@
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue"; import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue"; import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
import ServerSpecifiedPropertyControl from "../../labThingsComponents/serverSpecifiedPropertyControl.vue"; import ServerSpecifiedPropertyControl from "../../labThingsComponents/serverSpecifiedPropertyControl.vue";
// vue3 migration
// Export main app // Export main app
export default { export default {
@ -33,7 +34,7 @@ export default {
components: { components: {
cameraCalibrationSettings, cameraCalibrationSettings,
miniStreamDisplay, miniStreamDisplay,
ServerSpecifiedPropertyControl, ServerSpecifiedPropertyControl
}, },
data() { data() {

View file

@ -22,13 +22,15 @@
<script> <script>
import ServerSpecifiedActionButton from "../../../labThingsComponents/serverSpecifiedActionButton.vue"; import ServerSpecifiedActionButton from "../../../labThingsComponents/serverSpecifiedActionButton.vue";
// vue3 migration
import { markRaw } from "vue";
// Export main app // Export main app
export default { export default {
name: "CameraCalibrationSettings", name: "CameraCalibrationSettings",
components: { components: {
ServerSpecifiedActionButton, ServerSpecifiedActionButton
}, },
props: { props: {

View file

@ -15,7 +15,7 @@ export default {
components: { components: {
streamSettings, streamSettings,
appSettings, appSettings
}, },
}; };
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div id="stageSettings" v-observe-visibility="visibilityChanged" class="uk-width-large"> <div id="stageSettings" ref="stageSettingsContainer" class="uk-width-large">
The microscope stage is a <b>{{ stageType }}</b> The microscope stage is a <b>{{ stageType }}</b>
<div> <div>
<div class="uk-margin"> <div class="uk-margin">
@ -29,12 +29,14 @@
<script> <script>
import ActionButton from "../../labThingsComponents/actionButton.vue"; import ActionButton from "../../labThingsComponents/actionButton.vue";
// vue3 migration
import { useIntersectionObserver } from "@vueuse/core";
export default { export default {
name: "StageSettings", name: "StageSettings",
components: { components: {
ActionButton, ActionButton
}, },
data: function () { data: function () {
@ -49,6 +51,16 @@ export default {
}, },
}, },
mounted() {
useIntersectionObserver(
this.$refs.stageSettingsContainer,
([{ isIntersecting }]) => {
this.visibilityChanged(isIntersecting);
},
{ threshold: 0.0 }
);
},
methods: { methods: {
visibilityChanged(isVisible) { visibilityChanged(isVisible) {
if (isVisible) { if (isVisible) {

View file

@ -65,8 +65,11 @@ import cameraSettings from "./settingsComponents/cameraSettings.vue";
import CSMSettings from "./settingsComponents/CSMSettings.vue"; import CSMSettings from "./settingsComponents/CSMSettings.vue";
import stageSettings from "./settingsComponents/stageSettings.vue"; import stageSettings from "./settingsComponents/stageSettings.vue";
// Import generic components // Import generic components
import tabIcon from "../genericComponents/tabIcon"; import tabIcon from "../genericComponents/tabIcon.vue";
import tabContent from "../genericComponents/tabContent"; import tabContent from "../genericComponents/tabContent.vue";
// vue3 migration
import { markRaw } from "vue";
// Export main app // Export main app
export default { export default {
@ -75,7 +78,7 @@ export default {
components: { components: {
tabIcon, tabIcon,
tabContent, tabContent,
calibrationWizard, calibrationWizard
}, },
data: function () { data: function () {
@ -87,14 +90,14 @@ export default {
id: "display", id: "display",
title: "Display", title: "Display",
requireConnection: false, requireConnection: false,
component: displaySettings, component: markRaw(displaySettings),
requiredThings: [], requiredThings: [],
}, },
{ {
id: "stage-control", id: "stage-control",
title: "Stage Control Preferences", title: "Stage Control Preferences",
requireConnection: false, requireConnection: false,
component: stageControlSettings, component: markRaw(stageControlSettings),
requiredThings: ["stage"], requiredThings: ["stage"],
}, },
], ],
@ -103,21 +106,21 @@ export default {
id: "camera", id: "camera",
title: "Camera", title: "Camera",
requireConnection: true, requireConnection: true,
component: cameraSettings, component: markRaw(cameraSettings),
requiredThings: [], requiredThings: [],
}, },
{ {
id: "stage", id: "stage",
title: "Stage", title: "Stage",
requireConnection: true, requireConnection: true,
component: stageSettings, component: markRaw(stageSettings),
requiredThings: ["stage"], requiredThings: ["stage"],
}, },
{ {
id: "mapping", id: "mapping",
title: "Camera to Stage Mapping", title: "Camera to Stage Mapping",
requireConnection: true, requireConnection: true,
component: CSMSettings, component: markRaw(CSMSettings),
requiredThings: ["camera_stage_mapping"], requiredThings: ["camera_stage_mapping"],
}, },
], ],

View file

@ -66,9 +66,9 @@
submit-label="Start Smart Scan" submit-label="Start Smart Scan"
:can-terminate="true" :can-terminate="true"
@taskStarted="startScanning" @taskStarted="startScanning"
@update:taskStatus="taskStatus = $event" @beforeUpdate:taskStatus="taskStatus = $event"
@update:progress="progress = $event" @beforeUpdate:progress="progress = $event"
@update:log="log = $event" @beforeUpdate:log="log = $event"
/> />
</div> </div>
</div> </div>
@ -130,6 +130,8 @@ import actionLogDisplay from "../labThingsComponents/actionLogDisplay.vue";
import actionProgressBar from "../labThingsComponents/actionProgressBar.vue"; import actionProgressBar from "../labThingsComponents/actionProgressBar.vue";
import MiniStreamDisplay from "../genericComponents/miniStreamDisplay.vue"; import MiniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
import ActionButton from "../labThingsComponents/actionButton.vue"; import ActionButton from "../labThingsComponents/actionButton.vue";
// vue3 migration
import { useIntersectionObserver } from "@vueuse/core";
export default { export default {
name: "SlideScanContent", name: "SlideScanContent",
@ -137,11 +139,11 @@ export default {
components: { components: {
streamDisplay, streamDisplay,
propertyControl, propertyControl,
ServerSpecifiedPropertyControl,
actionLogDisplay, actionLogDisplay,
actionProgressBar, actionProgressBar,
MiniStreamDisplay, MiniStreamDisplay,
ActionButton, ActionButton,
ServerSpecifiedPropertyControl
}, },
data() { data() {
@ -177,6 +179,18 @@ export default {
this.workflowOptions = await this.readThingProperty("smart_scan", "workflow_display_names"); this.workflowOptions = await this.readThingProperty("smart_scan", "workflow_display_names");
}, },
mounted() {
useIntersectionObserver(
this.$refs.slideScanContent,
([{ isIntersecting }]) => {
this.visibilityChanged(isIntersecting);
},
{
threshold: 0.0, // Adjust as needed
},
);
},
methods: { methods: {
visibilityChanged(isVisible) { visibilityChanged(isVisible) {
if (isVisible) { if (isVisible) {

View file

@ -2,7 +2,6 @@
<div <div
id="stream-display" id="stream-display"
ref="streamDisplay" ref="streamDisplay"
v-observe-visibility="visibilityChanged"
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget" class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
> >
<img <img
@ -35,6 +34,10 @@
</template> </template>
<script> <script>
// vue3 migration
import { eventBus } from "../../eventBus.js";
import { useIntersectionObserver } from "@vueuse/core";
// Export main app // Export main app
export default { export default {
name: "StreamDisplay", name: "StreamDisplay",
@ -47,7 +50,7 @@ export default {
resizeTimeoutId: setTimeout(this.doneResizing, 500), resizeTimeoutId: setTimeout(this.doneResizing, 500),
}; };
}, },
computed: { computed: {
streamEnabled: function () { streamEnabled: function () {
return this.$store.getters.ready && !this.$store.state.disableStream; return this.$store.getters.ready && !this.$store.state.disableStream;
@ -62,30 +65,44 @@ export default {
}, },
mounted() { mounted() {
//set up an intersection observer
useIntersectionObserver(
this.$refs.streamDisplay,
([{ isIntersecting }]) => {
this.isVisible = isIntersecting;
},
{
threshold: 0.0,
}
);
// A global signal listener to flash the stream element // A global signal listener to flash the stream element
this.$root.$on("globalFlashStream", () => { this.onFlashStream = () => {
this.flashStream(); this.flashStream();
}); };
eventBus.on("globalFlashStream", this.onFlashStream);
// Mutation observer // Mutation observer
this.sizeObserver = new ResizeObserver(() => { this.sizeObserver = new ResizeObserver(() => {
this.handleResize(); // For any element attached to the observer, run handleResize() on change this.handleResize(); // For any element attached to the observer, run handleResize() on change
}); });
// Fetch streamDisplay component by ref // Fetch streamDisplay component by ref
const streamDisplayElement = this.$refs.streamDisplay.parentNode; if (this.$refs.streamDisplay && this.$refs.streamDisplay.parentNode) {
// Attach streamDisplay to the size observer const streamDisplayElement = this.$refs.streamDisplay.parentNode;
this.sizeObserver.observe(streamDisplayElement); // Attach streamDisplay to the size observer
this.sizeObserver.observe(streamDisplayElement);
}
}, },
created: function () { created: function () {
// Do nothing: preview stream now runs all the time // Do nothing: preview stream now runs all the time
}, },
beforeDestroy: function () { beforeUnmount: function () {
// Remove global signal listener to change the GPU preview state // Remove global signal listener to change the GPU preview state
this.$root.$off("globalTogglePreview"); //eventBus.off("globalTogglePreview", true);
// Remove global signal listener to flash the stream element // Remove global signal listener to flash the stream element
this.$root.$off("globalFlashStream"); eventBus.off("globalFlashStream", this.onFlashStream);
// Disconnect the size observer // Disconnect the size observer
this.sizeObserver.disconnect(); this.sizeObserver.disconnect();
// Remove from the array of active streams // Remove from the array of active streams
@ -93,9 +110,6 @@ export default {
}, },
methods: { methods: {
visibilityChanged(isVisible) {
this.isVisible = isVisible;
},
flashStream: function () { flashStream: function () {
// Run an animation that flashes the stream (for capture feedback) // Run an animation that flashes the stream (for capture feedback)
let element = this.$refs.streamDisplay; let element = this.$refs.streamDisplay;
@ -126,7 +140,10 @@ export default {
let yRelative = (0.5 * event.target.offsetHeight - yCoordinate) * scale; let yRelative = (0.5 * event.target.offsetHeight - yCoordinate) * scale;
// Emit a signal to move, acted on by paneControl.vue // Emit a signal to move, acted on by paneControl.vue
this.$root.$emit("globalMoveInImageCoordinatesEvent", -xRelative, -yRelative); eventBus.emit("globalMoveInImageCoordinatesEvent", {
x:-xRelative,
y:-yRelative
});
}, },
handleResize: function () { handleResize: function () {

View file

@ -9,6 +9,8 @@
<script> <script>
import streamDisplay from "./streamContent.vue"; import streamDisplay from "./streamContent.vue";
// vue3 migration
import { markRaw } from "vue";
export default { export default {
name: "ViewContent", name: "ViewContent",

3
webapp/src/eventBus.js Normal file
View file

@ -0,0 +1,3 @@
import mitt from "mitt";
export const eventBus = mitt();

View file

@ -1,4 +1,4 @@
import Vue from "vue"; import { createApp } from 'vue';
import App from "./App.vue"; import App from "./App.vue";
import store from "./store"; import store from "./store";
import UIkit from "uikit"; import UIkit from "uikit";
@ -21,15 +21,16 @@ UIkit.mixin(
"accordion", "accordion",
); );
// Create Vue app
const app = createApp(App);
// Use visibility observer // Use visibility observer
Vue.use(VueObserveVisibility); app.use(VueObserveVisibility);
Vue.config.productionTip = false; // Use global mixins
app.mixin(queryMixin);
app.mixin(modalMixin);
Vue.mixin(queryMixin); // Use Vuex store
Vue.mixin(modalMixin); app.use(store);
app.mount("#app");
new Vue({
store,
render: (h) => h(App),
}).$mount("#app");

View file

@ -7,6 +7,7 @@
*/ */
import UIkit from "uikit"; import UIkit from "uikit";
import { eventBus } from "@/eventBus";
export default { export default {
methods: { methods: {
@ -14,7 +15,7 @@ export default {
var context = this; var context = this;
// Stop GPU preview to show modal // Stop GPU preview to show modal
context.$root.$emit("globalTogglePreview", false); eventBus.emit("globalTogglePreview", false);
// force OK to be capitalised // force OK to be capitalised
UIkit.modal.i18n = { ok: "OK", cancel: "Cancel" }; UIkit.modal.i18n = { ok: "OK", cancel: "Cancel" };
@ -33,9 +34,9 @@ export default {
.finally(function () { .finally(function () {
// Re-enable the GPU preview, if it was active before the modal // Re-enable the GPU preview, if it was active before the modal
if (context.$store.state.autoGpuPreview) { if (context.$store.state.autoGpuPreview) {
context.$root.$emit("globalTogglePreview", true); eventBus.emit("globalTogglePreview", true);
} }
context.$root.$emit("modalClosed"); eventBus.emit("modalClosed");
}); });
}; };
return new Promise(showModal); return new Promise(showModal);

57
webapp/vite.config.mjs Normal file
View file

@ -0,0 +1,57 @@
// vite.config.mjs
// Vite configuration file for a Vue 2 webapp using the Vue 2 compatibility build.
// Vite replaces Vue CLI as the build tool. As it is designed for Vue 3, we use the
// @vue/compat package to enable Vue 2 maintaining compatibility syntax and features.
// Important: This is a preliminary setup, for vue2 to vue3 migration purposes.
// Note: Comments are generated to explain relevant configuration options.
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// Enable Vue 2 compatibility mode.
compatConfig: {
MODE: 2
}
}
}
}),
],
resolve: {
alias: {
// Use Vue 2 compatible build.
'vue': '@vue/compat',
// Setup path alias for src directory.
// This allows importing modules using '@/path/to/module'.
'@': fileURLToPath(new URL('./src', import.meta.url))
},
// Recognize these file extensions for module resolution.
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
},
server: {
host: true,
// Set the development server port to 8080.
// OFM uses port 5000, so we avoid conflicts. run, and override by using the address:
// http://microscope.local:8080/?overrideOrigin=http://microscope.local:5000#
port: 8080,
strictPort: true,
},
css: {
preprocessorOptions: {
less: {
// Always include math in Less files.
math: 'always',
// Enable relative URLs in Less files.
relativeUrls: true,
// Enable JavaScript in Less files
javascriptEnabled: true,
}
}
}
})