Merge branch 'vue2-3_goal_1' into 'v3'
Vue2-3 migration. See merge request openflexure/openflexure-microscope-server!479
This commit is contained in:
commit
11beb79a77
58 changed files with 2725 additions and 10968 deletions
|
|
@ -3,12 +3,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware # vue3 migration
|
||||||
from uvicorn.main import Server
|
from uvicorn.main import Server
|
||||||
|
|
||||||
import labthings_fastapi as lt
|
import labthings_fastapi as lt
|
||||||
|
|
@ -28,6 +30,7 @@ from .legacy_api import add_v2_endpoints
|
||||||
from .serve_static_files import add_static_files
|
from .serve_static_files import add_static_files
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
DEVELOPER_MODE = os.getenv("OFM_SERVER_DEV_MODE", "false").lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
def set_shutdown_function(shutdown_function: Callable[[], None]) -> None:
|
def set_shutdown_function(shutdown_function: Callable[[], None]) -> None:
|
||||||
|
|
@ -61,6 +64,17 @@ def customise_server(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Customise the server with additional endpoints, etc."""
|
"""Customise the server with additional endpoints, etc."""
|
||||||
configure_logging(log_folder)
|
configure_logging(log_folder)
|
||||||
|
|
||||||
|
if DEVELOPER_MODE:
|
||||||
|
# Allow CORS in developer mode for easier testing with the webapp
|
||||||
|
server.app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
add_v2_endpoints(server)
|
add_v2_endpoints(server)
|
||||||
add_static_files(server.app, scans_folder)
|
add_static_files(server.app, scans_folder)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,7 @@ def check_static_dir() -> None:
|
||||||
"web app."
|
"web app."
|
||||||
)
|
)
|
||||||
expected = [
|
expected = [
|
||||||
os.path.isdir(os.path.join(STATIC_PATH, "js")),
|
os.path.isdir(os.path.join(STATIC_PATH, "assets")),
|
||||||
os.path.isdir(os.path.join(STATIC_PATH, "css")),
|
|
||||||
os.path.isfile(os.path.join(STATIC_PATH, "index.html")),
|
os.path.isfile(os.path.join(STATIC_PATH, "index.html")),
|
||||||
]
|
]
|
||||||
if not all(expected):
|
if not all(expected):
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,7 @@ def mock_static_dir():
|
||||||
It contains enough files to be recognised as a webapp.
|
It contains enough files to be recognised as a webapp.
|
||||||
"""
|
"""
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
os.makedirs(os.path.join(tmpdir, "js"))
|
os.makedirs(os.path.join(tmpdir, "assets"))
|
||||||
os.makedirs(os.path.join(tmpdir, "css"))
|
|
||||||
with open(os.path.join(tmpdir, "index.html"), "w", encoding="utf-8") as f_obj:
|
with open(os.path.join(tmpdir, "index.html"), "w", encoding="utf-8") as f_obj:
|
||||||
f_obj.write("<mock>mock</mock>")
|
f_obj.write("<mock>mock</mock>")
|
||||||
yield tmpdir
|
yield tmpdir
|
||||||
|
|
@ -95,21 +94,21 @@ def test_check_static_dir(mock_static_dir, mocker):
|
||||||
# First run shouldn't error as the mock dir has enough files.
|
# First run shouldn't error as the mock dir has enough files.
|
||||||
serve_static_files.check_static_dir()
|
serve_static_files.check_static_dir()
|
||||||
|
|
||||||
js_path = os.path.join(mock_static_dir, "js")
|
asset_path = os.path.join(mock_static_dir, "assets")
|
||||||
index_path = os.path.join(mock_static_dir, "index.html")
|
index_path = os.path.join(mock_static_dir, "index.html")
|
||||||
|
|
||||||
# Remove javascript dir and check error.
|
# Remove asset dir and check error.
|
||||||
shutil.rmtree(js_path)
|
shutil.rmtree(asset_path)
|
||||||
with pytest.raises(FileNotFoundError):
|
with pytest.raises(FileNotFoundError):
|
||||||
serve_static_files.check_static_dir()
|
serve_static_files.check_static_dir()
|
||||||
# replace javascript dir with file, it should still error.
|
# replace asset dir with file, it should still error.
|
||||||
shutil.copyfile(index_path, js_path)
|
shutil.copyfile(index_path, asset_path)
|
||||||
with pytest.raises(FileNotFoundError):
|
with pytest.raises(FileNotFoundError):
|
||||||
serve_static_files.check_static_dir()
|
serve_static_files.check_static_dir()
|
||||||
|
|
||||||
# Return it to a folder and check everything works again
|
# Return it to a folder and check everything works again
|
||||||
os.remove(js_path)
|
os.remove(asset_path)
|
||||||
os.makedirs(js_path)
|
os.makedirs(asset_path)
|
||||||
serve_static_files.check_static_dir()
|
serve_static_files.check_static_dir()
|
||||||
|
|
||||||
# But it errors again if index.html is removed.
|
# But it errors again if index.html is removed.
|
||||||
|
|
@ -152,13 +151,11 @@ def test_add_static_files(mock_static_dir, mocker):
|
||||||
assert second_wrapped().headers["Pragma"] == "no-cache"
|
assert second_wrapped().headers["Pragma"] == "no-cache"
|
||||||
|
|
||||||
# Also should have mounted both dirs
|
# Also should have mounted both dirs
|
||||||
assert mock_app.mount.call_count == 2
|
assert mock_app.mount.call_count == 1
|
||||||
mounted_paths = [call.args[0] for call in mock_app.mount.call_args_list]
|
mounted_path = mock_app.mount.call_args.args[0]
|
||||||
mounted_dirs = [call.args[1].directory for call in mock_app.mount.call_args_list]
|
mounted_dir = mock_app.mount.call_args.args[1].directory
|
||||||
assert "/css/" in mounted_paths
|
assert "/assets/" in mounted_path
|
||||||
assert "/js/" in mounted_paths
|
assert os.path.join(mock_static_dir, "assets") in mounted_dir
|
||||||
assert os.path.join(mock_static_dir, "css") in mounted_dirs
|
|
||||||
assert os.path.join(mock_static_dir, "js") in mounted_dirs
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_static_files_with_scan_dir(mock_static_dir, mocker):
|
def test_add_static_files_with_scan_dir(mock_static_dir, mocker):
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
NODE_ENV=production
|
|
||||||
VUE_APP_MODE=production
|
VUE_APP_MODE=production
|
||||||
VUE_APP_PLATFORM=web
|
VUE_APP_PLATFORM=web
|
||||||
VUE_APP_TARGET=web
|
VUE_APP_TARGET=web
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,44 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
|
es2021: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
parser: "vue-eslint-parser",
|
parser: "vue-eslint-parser",
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: "@babel/eslint-parser",
|
parser: "@babel/eslint-parser",
|
||||||
requireConfigFile: false,
|
requireConfigFile: false,
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2021,
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
},
|
},
|
||||||
|
extends: ["plugin:vue/vue3-recommended", "eslint:recommended", "@vue/prettier"],
|
||||||
extends: ["plugin:vue/recommended", "eslint:recommended", "@vue/prettier"],
|
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||||
|
|
||||||
|
"vue/no-deprecated-slot-attribute": "warn",
|
||||||
|
"vue/no-deprecated-v-on-native-modifier": "warn",
|
||||||
|
"vue/no-deprecated-dollar-listeners-api": "warn",
|
||||||
|
"vue/no-deprecated-destroyed-lifecycle": "warn",
|
||||||
|
"vue/no-deprecated-dollar-scopedslots-api": "warn",
|
||||||
|
"vue/no-deprecated-events-api": "warn",
|
||||||
|
"vue/no-deprecated-filter": "warn",
|
||||||
|
"vue/no-deprecated-functional-template": "warn",
|
||||||
|
"vue/no-deprecated-html-element-is": "warn",
|
||||||
|
"vue/no-deprecated-inline-template": "warn",
|
||||||
|
"vue/no-deprecated-props-default-this": "warn",
|
||||||
|
"vue/no-deprecated-scope-attribute": "warn",
|
||||||
|
"vue/no-deprecated-v-bind-sync": "warn",
|
||||||
|
"vue/no-deprecated-v-is": "warn",
|
||||||
|
"vue/no-lifecycle-after-await": "warn",
|
||||||
|
"vue/no-watch-after-await": "warn",
|
||||||
|
|
||||||
|
"vue/require-explicit-emits": "warn", // Require emits declaration
|
||||||
|
"vue/multi-word-component-names": "warn", // Component naming
|
||||||
|
"vue/no-v-for-template-key-on-child": "error", // Key placement
|
||||||
|
"vue/no-v-model-argument": "off", // Allow v-model arguments (Vue 3 feature)
|
||||||
|
|
||||||
|
"vue/no-unused-components": "warn",
|
||||||
|
"vue/no-unused-vars": "warn",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
presets: ["@vue/app"],
|
|
||||||
};
|
|
||||||
27
webapp/index.html
Normal file
27
webapp/index.html
Normal 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>
|
||||||
12680
webapp/package-lock.json
generated
12680
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,41 +6,37 @@
|
||||||
"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": {
|
||||||
|
"@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",
|
||||||
|
"vue": "^3.4.0",
|
||||||
|
"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",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"@vue/cli-plugin-eslint": "^5.0.9",
|
"@vue/eslint-config-prettier": "9.0.0",
|
||||||
"@vue/cli-plugin-router": "^5.0.9",
|
"autoprefixer": "^10.4.24",
|
||||||
"@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",
|
"vite": "^5.4.21"
|
||||||
"vue": "^2.7",
|
|
||||||
"vue-template-compiler": "^2.7",
|
|
||||||
"vuejs-paginate": "^2.1",
|
|
||||||
"vuex": "^3.6"
|
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<!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="<%= BASE_URL %>favicon.ico">
|
|
||||||
<title>OpenFlexure Microscope</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but vue3 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -26,12 +26,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";
|
||||||
var Mousetrap = require("mousetrap");
|
import { eventBus } from "./eventBus.js";
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,10 +153,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 +165,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 +174,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 +183,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 +194,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 +237,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
|
||||||
|
|
@ -247,9 +247,14 @@ export default {
|
||||||
event.target.parentNode.classList.contains("scrollTarget") ||
|
event.target.parentNode.classList.contains("scrollTarget") ||
|
||||||
event.target.classList.contains("scrollTarget")
|
event.target.classList.contains("scrollTarget")
|
||||||
) {
|
) {
|
||||||
var z_rel = event.deltaY / 100;
|
const z_steps = 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_steps: 0,
|
||||||
|
y_steps: 0,
|
||||||
|
z_steps: z_steps,
|
||||||
|
absolute: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -257,7 +262,6 @@ export default {
|
||||||
// Calculate movement array
|
// Calculate movement array
|
||||||
var x_rel = 0;
|
var x_rel = 0;
|
||||||
var y_rel = 0;
|
var y_rel = 0;
|
||||||
var z_rel = 0;
|
|
||||||
// 37 corresponds to the left key
|
// 37 corresponds to the left key
|
||||||
if (37 in this.arrowKeysDown) {
|
if (37 in this.arrowKeysDown) {
|
||||||
x_rel = x_rel - 1;
|
x_rel = x_rel - 1;
|
||||||
|
|
@ -276,7 +280,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 });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app-content" class="uk-margin-remove uk-padding-remove uk-height-1-1" uk-grid>
|
<div id="app-content" class="uk-margin-remove uk-padding-remove uk-height-1-1" uk-grid>
|
||||||
<!-- Initialisation modals -->
|
<!-- Initialisation modals -->
|
||||||
<calibrationWizard ref="calibrationWizard" @onClose="enterApp()"></calibrationWizard>
|
<calibrationWizard ref="calibrationWizard" @on-close="enterApp()"></calibrationWizard>
|
||||||
<!-- Vertical tab bar -->
|
<!-- Vertical tab bar -->
|
||||||
<div id="switcher-left-container">
|
<div id="switcher-left-container">
|
||||||
<div
|
<div
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -71,7 +69,7 @@
|
||||||
:require-connection="true"
|
:require-connection="true"
|
||||||
:current-tab="currentTab"
|
:current-tab="currentTab"
|
||||||
>
|
>
|
||||||
<component :is="item.component" @scrollTop="scrollToTop"></component>
|
<component :is="item.component" @scroll-top="scrollToTop"></component>
|
||||||
</tabContent>
|
</tabContent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -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,8 @@ 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";
|
||||||
|
import { markRaw } 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";
|
||||||
|
|
@ -113,26 +113,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 +140,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 +163,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 +207,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) {
|
||||||
|
|
|
||||||
|
|
@ -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,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
name: "MiniStreamDisplay",
|
name: "MiniStreamDisplay",
|
||||||
|
|
@ -31,10 +32,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>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ export default {
|
||||||
|
|
||||||
props: {},
|
props: {},
|
||||||
|
|
||||||
|
emits: ["set-tab"],
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
tooltipOptions: function () {
|
tooltipOptions: function () {
|
||||||
var title = this.id.charAt(0).toUpperCase() + this.id.slice(1);
|
var title = this.id.charAt(0).toUpperCase() + this.id.slice(1);
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
@ -43,6 +49,8 @@ export default {
|
||||||
requireConnection: Boolean,
|
requireConnection: Boolean,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["set-tab"],
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
computedTitle: function () {
|
computedTitle: function () {
|
||||||
if (this.title !== undefined) {
|
if (this.title !== undefined) {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -20,7 +21,7 @@
|
||||||
:task-running="taskRunning"
|
:task-running="taskRunning"
|
||||||
:task-started="taskStarted"
|
:task-started="taskStarted"
|
||||||
:task-status="taskStatus"
|
:task-status="taskStatus"
|
||||||
@terminateTask="terminateTask"
|
@terminate-task="terminateTask"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -28,10 +29,16 @@
|
||||||
<script>
|
<script>
|
||||||
import ActionProgressBar from "./actionProgressBar.vue";
|
import ActionProgressBar from "./actionProgressBar.vue";
|
||||||
import ActionStatusModal from "./actionStatusModal.vue";
|
import ActionStatusModal from "./actionStatusModal.vue";
|
||||||
|
import { eventBus } from "../../eventBus.js";
|
||||||
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ActionButton",
|
name: "ActionButton",
|
||||||
components: { ActionProgressBar, ActionStatusModal },
|
|
||||||
|
components: {
|
||||||
|
ActionProgressBar,
|
||||||
|
ActionStatusModal,
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
action: {
|
action: {
|
||||||
|
|
@ -94,6 +101,22 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: [
|
||||||
|
"update:progress",
|
||||||
|
"update:taskStarted",
|
||||||
|
"update:taskRunning",
|
||||||
|
"update:log",
|
||||||
|
"update:taskStatus",
|
||||||
|
"submit",
|
||||||
|
"taskStarted",
|
||||||
|
"taskRunning",
|
||||||
|
"response",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
"finished",
|
||||||
|
"error",
|
||||||
|
],
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
taskUrl: null,
|
taskUrl: null,
|
||||||
|
|
@ -129,31 +152,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 +205,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
if (this.submitOnEvent) {
|
if (this.submitOnEvent) {
|
||||||
this.$root.$off(this.submitOnEvent);
|
eventBus.off(this.submitOnEvent);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,14 @@
|
||||||
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";
|
||||||
|
import { eventBus } from "../../eventBus.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ActionStatusModal",
|
name: "ActionStatusModal",
|
||||||
components: { ActionProgressBar, ActionLogDisplay },
|
components: {
|
||||||
|
ActionProgressBar,
|
||||||
|
ActionLogDisplay,
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
|
|
@ -65,13 +69,15 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["terminateTask"],
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
show() {
|
show() {
|
||||||
UIkit.modal(this.$refs.modal).show();
|
UIkit.modal(this.$refs.modal).show();
|
||||||
},
|
},
|
||||||
hide() {
|
hide() {
|
||||||
UIkit.modal(this.$refs.modal).hide();
|
UIkit.modal(this.$refs.modal).hide();
|
||||||
this.$root.$emit("modalClosed");
|
eventBus.emit("modalClosed");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,12 @@
|
||||||
>{{ label }}
|
>{{ label }}
|
||||||
<div class="input-and-buttons-container">
|
<div class="input-and-buttons-container">
|
||||||
<input
|
<input
|
||||||
|
ref="numericalInput"
|
||||||
v-model="internalValue"
|
v-model="internalValue"
|
||||||
class="uk-form-small numeric-setting-line-input"
|
class="uk-form-small numeric-setting-line-input"
|
||||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||||
type="number"
|
type="number"
|
||||||
@focusin="focusIn"
|
@input="grabFocus"
|
||||||
@focusout="focusOut"
|
@focusout="focusOut"
|
||||||
@keydown="keyDown"
|
@keydown="keyDown"
|
||||||
@animationend="animationEnd"
|
@animationend="animationEnd"
|
||||||
|
|
@ -39,8 +40,7 @@
|
||||||
class="uk-form-small numeric-setting-line-input"
|
class="uk-form-small numeric-setting-line-input"
|
||||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||||
type="number"
|
type="number"
|
||||||
@input="updateIsEdited"
|
@input="grabFocus"
|
||||||
@focusin="focusIn"
|
|
||||||
@focusout="focusOut"
|
@focusout="focusOut"
|
||||||
@keydown="keyDown"
|
@keydown="keyDown"
|
||||||
@animationend="animationEnd"
|
@animationend="animationEnd"
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
</label>
|
</label>
|
||||||
<label v-if="dataType == 'number_object'" class="uk-form-label"
|
<label v-if="dataType == 'number_object'" class="uk-form-label"
|
||||||
>{{ label }}
|
>{{ label }}
|
||||||
<div v-for="(val, key) in value" :key="key">
|
<div v-for="(val, key) in modelValue" :key="key">
|
||||||
<label>{{ internalLabels[key] }}</label>
|
<label>{{ internalLabels[key] }}</label>
|
||||||
<div class="input-and-buttons-container">
|
<div class="input-and-buttons-container">
|
||||||
<input
|
<input
|
||||||
|
|
@ -58,8 +58,7 @@
|
||||||
class="uk-form-small numeric-setting-line-input"
|
class="uk-form-small numeric-setting-line-input"
|
||||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||||
type="number"
|
type="number"
|
||||||
@input="updateIsEdited"
|
@input="grabFocus"
|
||||||
@focusin="focusIn"
|
|
||||||
@focusout="focusOut"
|
@focusout="focusOut"
|
||||||
@keydown="keyDown"
|
@keydown="keyDown"
|
||||||
@animationend="animationEnd"
|
@animationend="animationEnd"
|
||||||
|
|
@ -76,7 +75,6 @@
|
||||||
class="uk-form-small numeric-setting-line-input"
|
class="uk-form-small numeric-setting-line-input"
|
||||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||||
type="text"
|
type="text"
|
||||||
@focusin="focusIn"
|
|
||||||
@focusout="focusOut"
|
@focusout="focusOut"
|
||||||
@keydown="keyDown"
|
@keydown="keyDown"
|
||||||
@animationend="animationEnd"
|
@animationend="animationEnd"
|
||||||
|
|
@ -107,15 +105,15 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
syncPropertyButton,
|
syncPropertyButton,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
dataSchema: {
|
dataSchema: {
|
||||||
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,
|
||||||
|
|
@ -123,24 +121,28 @@ export default {
|
||||||
},
|
},
|
||||||
animate: {
|
animate: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["requestUpdate", "sendValue", "animationShown"],
|
||||||
|
|
||||||
|
compatConfig: { COMPONENT_V_MODEL: false },
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// Initialise with a copy to try to prevent the this.value prop being mutated if
|
// Initialise with a copy to try to prevent the this.modelValue prop being mutated if
|
||||||
// the value is an array or object. For future updates we stringify and parse
|
// the modelValue 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: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -206,14 +208,19 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
value() {
|
modelValue: {
|
||||||
// Fire updateIsEdited on both value and internal value change,
|
deep: true,
|
||||||
// as change in value may not causse internalValue to change.
|
handler() {
|
||||||
this.updateIsEdited();
|
// Fire updateIsEdited on both modelValue and internal modelValue change,
|
||||||
this.resetInternalValue();
|
// as change in modelValue may not cause internalValue to change.
|
||||||
|
this.updateIsEdited();
|
||||||
|
this.resetInternalValue();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
internalValue() {
|
internalValue: {
|
||||||
this.updateIsEdited();
|
handler() {
|
||||||
|
this.updateIsEdited();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
animate(updated) {
|
animate(updated) {
|
||||||
if (updated) {
|
if (updated) {
|
||||||
|
|
@ -223,19 +230,20 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.value !== undefined) {
|
if (this.modelValue !== undefined) {
|
||||||
this.resetInternalValue();
|
this.resetInternalValue();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
resetInternalValue: function () {
|
resetInternalValue: function () {
|
||||||
// Whenever updatirng th internal value stringify and parse as a form of deepcopy.
|
// Whenever updatirng th internal modelValue 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.modelValue 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.internalValue = this.modelValue;
|
||||||
this.$emit("requestUpdate");
|
this.$emit("requestUpdate");
|
||||||
},
|
},
|
||||||
sendValue: async function () {
|
sendValue: async function () {
|
||||||
|
|
@ -247,11 +255,18 @@ export default {
|
||||||
this.sendValue();
|
this.sendValue();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
focusIn: function (event) {
|
/** Grab the browser focus.
|
||||||
this.valueOnEnter = event.target.value;
|
*
|
||||||
|
* This is needed for numerical elements to be in focus when a spinner
|
||||||
|
* is clicked so that the data sends when focus is lost.
|
||||||
|
*/
|
||||||
|
grabFocus(event) {
|
||||||
|
event.target.focus();
|
||||||
|
// This is needed for number objects and number arrays
|
||||||
|
this.updateIsEdited();
|
||||||
},
|
},
|
||||||
focusOut: function (event) {
|
focusOut: function (event) {
|
||||||
if (this.valueOnEnter != event.target.value) {
|
if (this.modelValue != event.target.value) {
|
||||||
this.sendValue(event.target.value);
|
this.sendValue(event.target.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -262,15 +277,16 @@ 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 = null;
|
||||||
this.$emit("animationShown");
|
this.$emit("animationShown");
|
||||||
},
|
},
|
||||||
deepStringify: function (val) {
|
deepStringify: function (val) {
|
||||||
// Create a json string where all internal numbers are also JSON strings. This is
|
// Create a json string where all internal numbers are also JSON strings. This is
|
||||||
// needed to robustly check if the value is updated because the raw value may be a
|
// needed to robustly check if the modelValue is updated because the raw modelValue may be a
|
||||||
// number but anything typed in the input is a string. In the case of arrays or
|
// number but anything typed in the input is a string. In the case of arrays or
|
||||||
// objects even with JSON.stringify we end up comparing ["1", 3] with [1, 3] and
|
// objects even with JSON.stringify we end up comparing ["1", 3] with [1, 3] and
|
||||||
// find them as not equal.
|
// find them as not equal.
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<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"
|
||||||
@requestUpdate="readProperty"
|
@request-update="readProperty"
|
||||||
@sendValue="writeProperty"
|
@send-value="writeProperty"
|
||||||
@animationShown="resetAnimate"
|
@animation-shown="resetAnimate"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: undefined,
|
modelValue: undefined,
|
||||||
animate: false,
|
animate: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -74,7 +74,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 +82,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));
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["response", "finished"],
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
/**
|
/**
|
||||||
* Runs when the ActionButton's action completes successfully.
|
* Runs when the ActionButton's action completes successfully.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "SyncPropertyButton",
|
name: "SyncPropertyButton",
|
||||||
|
|
||||||
|
emits: ["click"],
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -26,7 +28,9 @@ export default {
|
||||||
|
|
||||||
.material-symbols-outlined.sync-icon {
|
.material-symbols-outlined.sync-icon {
|
||||||
color: #888;
|
color: #888;
|
||||||
transition: transform 0.3s ease, color 0.3s ease;
|
transition:
|
||||||
|
transform 0.3s ease,
|
||||||
|
color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-symbols-outlined.sync-icon:hover {
|
.material-symbols-outlined.sync-icon:hover {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ import devTools from "./tabContentComponents/aboutComponents/devTools.vue";
|
||||||
export default {
|
export default {
|
||||||
name: "LoadingContent",
|
name: "LoadingContent",
|
||||||
|
|
||||||
components: { devTools },
|
components: {
|
||||||
|
devTools,
|
||||||
|
},
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {};
|
return {};
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -24,12 +24,15 @@ import welcomeStep from "./calibrationWizardComponents/welcomeStep.vue";
|
||||||
import cameraCalibrationTask from "./calibrationWizardComponents/cameraCalibrationTask.vue";
|
import cameraCalibrationTask from "./calibrationWizardComponents/cameraCalibrationTask.vue";
|
||||||
import cameraStageMappingTask from "./calibrationWizardComponents/cameraStageMappingTask.vue";
|
import cameraStageMappingTask from "./calibrationWizardComponents/cameraStageMappingTask.vue";
|
||||||
import finalStep from "./calibrationWizardComponents/finalStep.vue";
|
import finalStep from "./calibrationWizardComponents/finalStep.vue";
|
||||||
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CalibrationWizard",
|
name: "CalibrationWizard",
|
||||||
|
|
||||||
components: {},
|
components: {},
|
||||||
|
|
||||||
|
emits: ["onClose"],
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
isNeeded: undefined,
|
isNeeded: undefined,
|
||||||
|
|
@ -103,7 +106,7 @@ export default {
|
||||||
// Optionally include the welcome screen
|
// Optionally include the welcome screen
|
||||||
if (includeWelcome) {
|
if (includeWelcome) {
|
||||||
tasks.push({
|
tasks.push({
|
||||||
component: singleStepTask,
|
component: markRaw(singleStepTask),
|
||||||
props: { stepComponent: welcomeStep },
|
props: { stepComponent: welcomeStep },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -111,12 +114,12 @@ export default {
|
||||||
// Add calibration task for each thing
|
// Add calibration task for each thing
|
||||||
for (const thing of thingsToCal) {
|
for (const thing of thingsToCal) {
|
||||||
const taskComponent = this.availableCalibrationTasks[thing];
|
const taskComponent = this.availableCalibrationTasks[thing];
|
||||||
tasks.push({ component: taskComponent });
|
tasks.push({ component: markRaw(taskComponent), props: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always include the final step
|
// Always include the final step
|
||||||
tasks.push({
|
tasks.push({
|
||||||
component: singleStepTask,
|
component: markRaw(singleStepTask),
|
||||||
props: { stepComponent: finalStep },
|
props: { stepComponent: finalStep },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
@ -66,6 +66,8 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["back", "next"],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
stepIndex: this.startOnLast ? this.steps.length - 1 : 0,
|
stepIndex: this.startOnLast ? this.steps.length - 1 : 0,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<cameraCalibrationSettings
|
<cameraCalibrationSettings
|
||||||
:show-extra-settings="false"
|
:show-extra-settings="false"
|
||||||
:camera-uri="cameraUri"
|
:camera-uri="cameraUri"
|
||||||
@actionFinished="checkCalibrationState"
|
@action-finished="checkCalibrationState"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -28,6 +28,8 @@ export default {
|
||||||
cameraCalibrationSettings,
|
cameraCalibrationSettings,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["awaiting-user"],
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
cameraUri: function () {
|
cameraUri: function () {
|
||||||
return `${this.$store.getters.baseUri}/camera/`;
|
return `${this.$store.getters.baseUri}/camera/`;
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,14 @@
|
||||||
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";
|
||||||
|
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,
|
||||||
|
|
@ -28,9 +32,14 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["next", "back"],
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
steps: [{ component: camCalibrationExplanation }, { component: cameraMainCalibrationStep }],
|
steps: [
|
||||||
|
{ component: markRaw(camCalibrationExplanation) },
|
||||||
|
{ component: markRaw(cameraMainCalibrationStep) },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,13 @@ 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";
|
||||||
|
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,
|
||||||
|
|
@ -28,10 +31,15 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ["next", "back"],
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
steps: [{ component: csmExplanation }, { component: focusStep }, { component: runCsmStep }],
|
steps: [
|
||||||
|
{ component: markRaw(csmExplanation) },
|
||||||
|
{ component: markRaw(focusStep) },
|
||||||
|
{ component: markRaw(runCsmStep) },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
<script>
|
<script>
|
||||||
import stepTemplateWithStream from "../stepTemplateWithStream.vue";
|
import stepTemplateWithStream from "../stepTemplateWithStream.vue";
|
||||||
import ActionButton from "../../../labThingsComponents/actionButton.vue";
|
import ActionButton from "../../../labThingsComponents/actionButton.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CameraMainCalibrationStep",
|
name: "CameraMainCalibrationStep",
|
||||||
|
|
||||||
|
|
@ -47,7 +48,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;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<div class="action-button-container">
|
<div class="action-button-container">
|
||||||
<CSMCalibrationSettings
|
<CSMCalibrationSettings
|
||||||
:show-extra-settings="false"
|
:show-extra-settings="false"
|
||||||
@recalibrateResponse="checkCalibrationState"
|
@recalibrate-response="checkCalibrationState"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -27,6 +27,8 @@ export default {
|
||||||
CSMCalibrationSettings,
|
CSMCalibrationSettings,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["awaiting-user"],
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.checkCalibrationState();
|
this.checkCalibrationState();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,14 @@ supplied by the wizard.
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import calibrationWizardTask from "./calibrationWizardTask.vue";
|
import calibrationWizardTask from "./calibrationWizardTask.vue";
|
||||||
|
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: {
|
||||||
|
|
@ -52,9 +56,11 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["next", "back"],
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
steps: [{ component: this.stepComponent, props: this.stepProps }],
|
steps: [{ component: markRaw(this.stepComponent), props: this.stepProps }],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,9 @@ import ActionButton from "../../labThingsComponents/actionButton.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "StatusPane",
|
name: "StatusPane",
|
||||||
components: { ActionButton },
|
components: {
|
||||||
|
ActionButton,
|
||||||
|
},
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -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,6 +47,7 @@
|
||||||
<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";
|
||||||
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -65,13 +66,35 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
this.readSettings();
|
await this.readSettings();
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
useIntersectionObserver(
|
||||||
|
this.$refs.backgroundDetectContent,
|
||||||
|
([{ isIntersecting }]) => {
|
||||||
|
this.visibilityChanged(isIntersecting);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
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() {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
:submit-label="'Autofocus'"
|
:submit-label="'Autofocus'"
|
||||||
:button-primary="true"
|
:button-primary="true"
|
||||||
:submit-on-event="'globalFastAutofocusEvent'"
|
:submit-on-event="'globalFastAutofocusEvent'"
|
||||||
@taskStarted="onAutofocus"
|
@task-started="onAutofocus"
|
||||||
@finished="afterAutofocus"
|
@finished="afterAutofocus"
|
||||||
@error="modalError"
|
@error="modalError"
|
||||||
/>
|
/>
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
||||||
|
import { eventBus } from "../../../eventBus.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "AutofocusControl",
|
name: "AutofocusControl",
|
||||||
|
|
@ -38,7 +39,7 @@ export default {
|
||||||
},
|
},
|
||||||
afterAutofocus() {
|
afterAutofocus() {
|
||||||
this.isAutofocusing = false;
|
this.isAutofocusing = false;
|
||||||
this.$root.$emit("globalUpdatePositionEvent");
|
eventBus.emit("globalUpdatePositionEvent");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ export default {
|
||||||
let imageUri = response.output.href;
|
let imageUri = response.output.href;
|
||||||
if (!imageUri) {
|
if (!imageUri) {
|
||||||
this.modalError("No image URI returned from capture task.");
|
this.modalError("No image URI returned from capture task.");
|
||||||
console.log(`Capture resulted in response ${response}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// To save the returned data, we make a virtual link and click it
|
// To save the returned data, we make a virtual link and click it
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@ 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";
|
||||||
|
import { eventBus } from "../../../eventBus.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PaneControl",
|
name: "PaneControl",
|
||||||
|
|
||||||
|
|
@ -84,40 +86,48 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
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) {
|
||||||
|
this.moveInImageCoordinatesRequest(payload.x, payload.y, payload.absolute);
|
||||||
|
},
|
||||||
|
|
||||||
|
onMoveStep(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);
|
||||||
|
},
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -134,7 +144,7 @@ export default {
|
||||||
z: this.setPosition.z + z,
|
z: this.setPosition.z + z,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
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() {
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
</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";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
@ -81,8 +81,9 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Paginate from "vuejs-paginate";
|
import Paginate from "vuejs-paginate-next";
|
||||||
import EndpointButton from "../labThingsComponents/endpointButton.vue";
|
import EndpointButton from "../labThingsComponents/endpointButton.vue";
|
||||||
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "LoggingContent",
|
name: "LoggingContent",
|
||||||
|
|
@ -92,6 +93,8 @@ export default {
|
||||||
EndpointButton,
|
EndpointButton,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["scrollTop"],
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
maxitems: 20,
|
maxitems: 20,
|
||||||
|
|
@ -133,6 +136,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");
|
||||||
|
|
@ -183,7 +198,6 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
// if there's no existing log message to append to, discard lines
|
// if there's no existing log message to append to, discard lines
|
||||||
// until we find one.
|
// until we find one.
|
||||||
console.log("Ignored non-matching lines at the start of the log file.");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<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";
|
||||||
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "OpenSeadragonViewer",
|
name: "OpenSeadragonViewer",
|
||||||
|
|
@ -54,24 +55,41 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
id="thumbnail-stitched-image"
|
id="thumbnail-stitched-image"
|
||||||
class="thumbnail-fit"
|
class="thumbnail-fit"
|
||||||
:src="thumbnailPath"
|
:src="thumbnailPath"
|
||||||
onerror="this.src='/titleiconpink.svg';"
|
onerror="this.src = '/titleiconpink.svg'"
|
||||||
@click="requestViewer"
|
@click="requestViewer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -86,7 +86,10 @@ import EndpointButton from "../../labThingsComponents/endpointButton.vue";
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
name: "ScanCard",
|
name: "ScanCard",
|
||||||
components: { actionButton, EndpointButton },
|
components: {
|
||||||
|
actionButton,
|
||||||
|
EndpointButton,
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
scanData: {
|
scanData: {
|
||||||
|
|
@ -103,6 +106,8 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["viewer-requested", "update-requested"],
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
downloadStitchFile() {
|
downloadStitchFile() {
|
||||||
return `${this.$store.getters.baseUri}/smart_scan/get_stitch/${this.scanData.name}`;
|
return `${this.$store.getters.baseUri}/smart_scan/get_stitch/${this.scanData.name}`;
|
||||||
|
|
@ -162,7 +167,6 @@ export default {
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.setAttribute("download", filename);
|
link.setAttribute("download", filename);
|
||||||
console.log(link);
|
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,9 @@ import OpenSeadragonViewer from "./openSeadragonViewer.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ScanViewerModal",
|
name: "ScanViewerModal",
|
||||||
components: { OpenSeadragonViewer },
|
components: {
|
||||||
|
OpenSeadragonViewer,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
selectedScan: {
|
selectedScan: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div ref="galleryDisplay" class="galleryDisplay uk-padding uk-padding-remove-top">
|
||||||
v-observe-visibility="visibilityChanged"
|
|
||||||
class="galleryDisplay uk-padding uk-padding-remove-top"
|
|
||||||
>
|
|
||||||
<!-- Gallery nav bar -->
|
<!-- Gallery nav bar -->
|
||||||
<nav class="gallery-navbar uk-navbar-container uk-navbar-transparent" uk-navbar="mode: click">
|
<nav class="gallery-navbar uk-navbar-container uk-navbar-transparent" uk-navbar="mode: click">
|
||||||
<!-- Right side buttons -->
|
<!-- Right side buttons -->
|
||||||
|
|
@ -100,11 +97,19 @@ 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";
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ["scrollTop"],
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
|
@ -141,13 +146,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 +185,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: {
|
||||||
|
|
|
||||||
|
|
@ -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,7 @@
|
||||||
<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";
|
||||||
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -75,6 +76,8 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["recalibrateResponse"],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
csmMatrix: undefined,
|
csmMatrix: undefined,
|
||||||
|
|
@ -93,6 +96,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) {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ["actionFinished"],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
primaryCalibrationActions: [],
|
primaryCalibrationActions: [],
|
||||||
|
|
|
||||||
|
|
@ -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,6 +29,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
||||||
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "StageSettings",
|
name: "StageSettings",
|
||||||
|
|
@ -49,6 +50,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) {
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,9 @@ 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";
|
||||||
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -87,14 +88,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 +104,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"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||||
<div class="control-component uk-padding-small">
|
<div class="control-component uk-padding-small">
|
||||||
<div v-show="!scanning" v-observe-visibility="visibilityChanged" class="uk-padding-small">
|
<div v-show="!scanning" ref="slideScanView" class="uk-padding-small">
|
||||||
<!-- Workflow Selection Dropdown -->
|
<!-- Workflow Selection Dropdown -->
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label">Workflow</label>
|
<label class="uk-form-label">Workflow</label>
|
||||||
|
|
@ -65,8 +65,8 @@
|
||||||
:submit-data="{ scan_name: scan_name }"
|
:submit-data="{ scan_name: scan_name }"
|
||||||
submit-label="Start Smart Scan"
|
submit-label="Start Smart Scan"
|
||||||
:can-terminate="true"
|
:can-terminate="true"
|
||||||
@taskStarted="startScanning"
|
@task-started="startScanning"
|
||||||
@update:taskStatus="taskStatus = $event"
|
@update:task-status="taskStatus = $event"
|
||||||
@update:progress="progress = $event"
|
@update:progress="progress = $event"
|
||||||
@update:log="log = $event"
|
@update:log="log = $event"
|
||||||
/>
|
/>
|
||||||
|
|
@ -130,6 +130,7 @@ 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";
|
||||||
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SlideScanContent",
|
name: "SlideScanContent",
|
||||||
|
|
@ -137,11 +138,11 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
streamDisplay,
|
streamDisplay,
|
||||||
propertyControl,
|
propertyControl,
|
||||||
ServerSpecifiedPropertyControl,
|
|
||||||
actionLogDisplay,
|
actionLogDisplay,
|
||||||
actionProgressBar,
|
actionProgressBar,
|
||||||
MiniStreamDisplay,
|
MiniStreamDisplay,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
ServerSpecifiedPropertyControl,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -174,7 +175,23 @@ export default {
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
this.readSettings();
|
this.readSettings();
|
||||||
this.workflowOptions = await this.readThingProperty("smart_scan", "workflow_display_names");
|
this.workflowOptions = await this.readThingProperty(
|
||||||
|
"smart_scan",
|
||||||
|
"workflow_display_names",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
useIntersectionObserver(
|
||||||
|
this.$refs.slideScanView,
|
||||||
|
([{ isIntersecting }]) => {
|
||||||
|
this.visibilityChanged(isIntersecting);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -185,12 +202,22 @@ export default {
|
||||||
},
|
},
|
||||||
async readSettings() {
|
async readSettings() {
|
||||||
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name");
|
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name");
|
||||||
|
|
||||||
|
if (!this.workflowName) {
|
||||||
|
console.warn("Could not read workflow_name, using default");
|
||||||
|
this.workflowName = "histo_scan_workflow";
|
||||||
|
}
|
||||||
|
|
||||||
if (this.workflowName) {
|
if (this.workflowName) {
|
||||||
this.ready = await this.readThingProperty(this.workflowName, "ready");
|
this.ready = await this.readThingProperty(this.workflowName, "ready", true);
|
||||||
this.workflowSettings = await this.readThingProperty(this.workflowName, "settings_ui");
|
this.workflowSettings =
|
||||||
console.log(this.workflowSettings);
|
(await this.readThingProperty(this.workflowName, "settings_ui", true)) || [];
|
||||||
this.workflowDisplayName = await this.readThingProperty(this.workflowName, "display_name");
|
this.workflowDisplayName = await this.readThingProperty(
|
||||||
this.workflowBlurb = await this.readThingProperty(this.workflowName, "ui_blurb");
|
this.workflowName,
|
||||||
|
"display_name",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.workflowBlurb = await this.readThingProperty(this.workflowName, "ui_blurb", true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onScanError: function (error) {
|
onScanError: function (error) {
|
||||||
|
|
@ -224,11 +251,12 @@ export default {
|
||||||
async pollScan() {
|
async pollScan() {
|
||||||
if (this.cancellable) {
|
if (this.cancellable) {
|
||||||
// while the scan is running
|
// while the scan is running
|
||||||
let mtime = await this.readThingProperty("smart_scan", "latest_preview_stitch_time");
|
let mtime = await this.readThingProperty("smart_scan", "latest_preview_stitch_time", true);
|
||||||
if (mtime !== null) {
|
if (mtime !== null) {
|
||||||
this.lastStitchedImage = `${this.$store.getters.baseUri}/smart_scan/latest_preview_stitch.jpg?t=${mtime}`;
|
this.lastStitchedImage = `${this.$store.getters.baseUri}/smart_scan/latest_preview_stitch.jpg?t=${mtime}`;
|
||||||
}
|
}
|
||||||
this.lastScanName = await this.readThingProperty("smart_scan", "latest_scan_name");
|
|
||||||
|
this.lastScanName = await this.readThingProperty("smart_scan", "latest_scan_name", true);
|
||||||
setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped
|
setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -244,7 +272,7 @@ export default {
|
||||||
this.modalError(err);
|
this.modalError(err);
|
||||||
|
|
||||||
// revert if server rejected
|
// revert if server rejected
|
||||||
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name");
|
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name", true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async downloadZipFile(response) {
|
async downloadZipFile(response) {
|
||||||
|
|
@ -254,7 +282,6 @@ export default {
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.setAttribute("download", filename);
|
link.setAttribute("download", filename);
|
||||||
console.log(link);
|
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { eventBus } from "../../eventBus.js";
|
||||||
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
name: "StreamDisplay",
|
name: "StreamDisplay",
|
||||||
|
|
@ -62,30 +64,42 @@ 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
|
|
||||||
this.$root.$off("globalFlashStream");
|
|
||||||
// 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 +107,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 +137,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 () {
|
||||||
|
|
|
||||||
3
webapp/src/eventBus.js
Normal file
3
webapp/src/eventBus.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import mitt from "mitt";
|
||||||
|
|
||||||
|
export const eventBus = mitt();
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// Generic functions for formatting
|
// Generic functions for formatting
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a single number into a human-readable string.
|
* Format a single number into a human-readable string.
|
||||||
* Uses exponential notation for very large/small numbers.
|
* Uses exponential notation for very large/small numbers.
|
||||||
|
|
@ -30,7 +29,7 @@ export function formatNumber(num, maxDigits = 4) {
|
||||||
*/
|
*/
|
||||||
export function formatValue(value, maxDigits = 4) {
|
export function formatValue(value, maxDigits = 4) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
const items = value.map(val => formatValue(val, maxDigits));
|
const items = value.map((val) => formatValue(val, maxDigits));
|
||||||
return `[${items.join(", ")}]`;
|
return `[${items.join(", ")}]`;
|
||||||
}
|
}
|
||||||
if (typeof value === "object" && value !== null) {
|
if (typeof value === "object" && value !== null) {
|
||||||
|
|
@ -47,4 +46,4 @@ export function formatValue(value, maxDigits = 4) {
|
||||||
}
|
}
|
||||||
// Only if the string will not coerce to a number do we output the value.
|
// Only if the string will not coerce to a number do we output the value.
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
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";
|
||||||
|
|
||||||
import VueObserveVisibility from "vue-observe-visibility";
|
|
||||||
|
|
||||||
// Import MD icons
|
// Import MD icons
|
||||||
import "material-symbols/outlined.css";
|
import "material-symbols/outlined.css";
|
||||||
|
|
||||||
import queryMixin from "@/mixins/labThingsMixins.js";
|
|
||||||
import modalMixin from "@/mixins/modalMixins.js";
|
import modalMixin from "@/mixins/modalMixins.js";
|
||||||
|
import labThingsMixins from "./mixins/labThingsMixins";
|
||||||
|
|
||||||
// UIKit overrides
|
// UIKit overrides
|
||||||
UIkit.mixin(
|
UIkit.mixin(
|
||||||
|
|
@ -21,15 +19,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(modalMixin);
|
||||||
|
app.mixin(labThingsMixins);
|
||||||
|
|
||||||
Vue.mixin(queryMixin);
|
// Use Vuex store
|
||||||
Vue.mixin(modalMixin);
|
app.use(store);
|
||||||
|
app.mount("#app");
|
||||||
new Vue({
|
|
||||||
store,
|
|
||||||
render: (h) => h(App),
|
|
||||||
}).$mount("#app");
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export default {
|
||||||
// `false` fails because axios somehow eats it!
|
// `false` fails because axios somehow eats it!
|
||||||
// Other values should not be stringified or pydantic
|
// Other values should not be stringified or pydantic
|
||||||
// can't parse them.
|
// can't parse them.
|
||||||
if ((value === false) | (value === true)) {
|
if (value === false || value === true) {
|
||||||
value = JSON.stringify(value);
|
value = JSON.stringify(value);
|
||||||
}
|
}
|
||||||
await axios.put(url, value);
|
await axios.put(url, value);
|
||||||
|
|
@ -91,7 +91,7 @@ export default {
|
||||||
response = await axios.get(taskUrl, { baseURL: this.$store.getters.baseUri });
|
response = await axios.get(taskUrl, { baseURL: this.$store.getters.baseUri });
|
||||||
const result = response.data.status;
|
const result = response.data.status;
|
||||||
|
|
||||||
if ((result == "running") | (result == "pending")) {
|
if (result === "running" || result === "pending") {
|
||||||
ongoingMethod?.(response);
|
ongoingMethod?.(response);
|
||||||
this.pollTimers[taskUrl] = setTimeout(() => {
|
this.pollTimers[taskUrl] = setTimeout(() => {
|
||||||
this.pollUntilComplete(taskUrl, ongoingMethod, finalMethod, interval);
|
this.pollUntilComplete(taskUrl, ongoingMethod, finalMethod, interval);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import Vue from "vue";
|
import { createStore } from "vuex";
|
||||||
import Vuex from "vuex";
|
|
||||||
import wotStoreModule from "./wot-client";
|
import wotStoreModule from "./wot-client";
|
||||||
|
|
||||||
Vue.use(Vuex);
|
|
||||||
|
|
||||||
function getOriginFromLocation() {
|
function getOriginFromLocation() {
|
||||||
// This will default to the same origin that's serving
|
// This will default to the same origin that's serving
|
||||||
// the web app - but can be overridden by the URL.
|
// the web app - but can be overridden by the URL.
|
||||||
|
|
@ -55,7 +52,7 @@ const LOCALSTORAGE_KEYS = [
|
||||||
"navigationInvert",
|
"navigationInvert",
|
||||||
];
|
];
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default createStore({
|
||||||
modules: {
|
modules: {
|
||||||
wot: wotStoreModule,
|
wot: wotStoreModule,
|
||||||
},
|
},
|
||||||
|
|
@ -150,7 +147,6 @@ export default new Vuex.Store({
|
||||||
(store) => {
|
(store) => {
|
||||||
// Load initial state from localStorage
|
// Load initial state from localStorage
|
||||||
LOCALSTORAGE_KEYS.forEach((key) => {
|
LOCALSTORAGE_KEYS.forEach((key) => {
|
||||||
console.log(key);
|
|
||||||
const saved = localStorage.getItem(key);
|
const saved = localStorage.getItem(key);
|
||||||
if (saved !== null) {
|
if (saved !== null) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export const wotStoreModule = {
|
||||||
// Deduplication should be done elsewhere.
|
// Deduplication should be done elsewhere.
|
||||||
let response = await axios.get(uri);
|
let response = await axios.get(uri);
|
||||||
let td = response.data;
|
let td = response.data;
|
||||||
let thing_name = name | uri.replace(/\/$/, "").split("/").pop();
|
let thing_name = name || uri.replace(/\/$/, "").split("/").pop();
|
||||||
commit("addThingDescription", {
|
commit("addThingDescription", {
|
||||||
thingName: thing_name,
|
thingName: thing_name,
|
||||||
thingDescription: td,
|
thingDescription: td,
|
||||||
|
|
@ -37,7 +37,7 @@ export const wotStoreModule = {
|
||||||
async fetchThingDescriptions({ commit }, uri) {
|
async fetchThingDescriptions({ commit }, uri) {
|
||||||
// Fetch thing descriptions from the given URI
|
// Fetch thing descriptions from the given URI
|
||||||
let response = await axios.get(uri);
|
let response = await axios.get(uri);
|
||||||
if (response.status != 200) throw "Could not retrieve thing descriptions";
|
if (response.status !== 200) throw "Could not retrieve thing descriptions";
|
||||||
for (const k in response.data) {
|
for (const k in response.data) {
|
||||||
let thing_name = k.replace(/\/$/, "").replace(/^\//, "");
|
let thing_name = k.replace(/\/$/, "").replace(/^\//, "");
|
||||||
commit("addThingDescription", {
|
commit("addThingDescription", {
|
||||||
|
|
@ -77,8 +77,14 @@ export const wotStoreModule = {
|
||||||
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
|
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
|
||||||
}
|
}
|
||||||
let affordances = td[affordanceType];
|
let affordances = td[affordanceType];
|
||||||
|
|
||||||
|
if (!affordances || !(affordance in affordances)) {
|
||||||
|
if (allowUndefined) return undefined;
|
||||||
|
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
|
||||||
|
}
|
||||||
|
|
||||||
let href = findFormHref(affordances[affordance], op);
|
let href = findFormHref(affordances[affordance], op);
|
||||||
if (href == undefined) {
|
if (href === undefined) {
|
||||||
if (allowUndefined) return undefined;
|
if (allowUndefined) return undefined;
|
||||||
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
|
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
|
||||||
}
|
}
|
||||||
|
|
@ -105,10 +111,10 @@ export const wotStoreModule = {
|
||||||
|
|
||||||
export function findFormHref(affordance, op) {
|
export function findFormHref(affordance, op) {
|
||||||
// Find the form in the affordance that matches the given operation type
|
// Find the form in the affordance that matches the given operation type
|
||||||
if (affordance == undefined) return undefined;
|
if (affordance === undefined) return undefined;
|
||||||
let forms = affordance.forms;
|
let forms = affordance.forms;
|
||||||
let matchingForm = forms.find((f) => f.op == op || f.op.includes(op));
|
let matchingForm = forms.find((f) => f.op == op || f.op.includes(op));
|
||||||
if (matchingForm == undefined) return undefined;
|
if (matchingForm === undefined) return undefined;
|
||||||
return matchingForm.href;
|
return matchingForm.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
52
webapp/vite.config.mjs
Normal file
52
webapp/vite.config.mjs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
// vite.config.mjs
|
||||||
|
|
||||||
|
// Vite replaces Vue CLI as the build tool for Vue 3.
|
||||||
|
// 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(),
|
||||||
|
],
|
||||||
|
|
||||||
|
build: {
|
||||||
|
// Output directory for production builds
|
||||||
|
outDir: "../src/openflexure_microscope_server/static",
|
||||||
|
// Clear output directory before build
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
// 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
outputDir: "../src/openflexure_microscope_server/static",
|
|
||||||
};
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue