Merge branch 'refactor/migrate_vuex_to_pinia' into 'v3'
ui_migration Migrate VUEX to Pinia Closes #731 See merge request openflexure/openflexure-microscope-server!553
This commit is contained in:
commit
964a907edc
38 changed files with 2127 additions and 1548 deletions
|
|
@ -18,7 +18,7 @@ include:
|
||||||
.javascript_webapp:
|
.javascript_webapp:
|
||||||
extends: .javascript
|
extends: .javascript
|
||||||
variables:
|
variables:
|
||||||
JS_IMAGE: node:18
|
JS_IMAGE: node:20
|
||||||
JS_PATH: webapp
|
JS_PATH: webapp
|
||||||
JS_CACHE_PATH: webapp/node_modules
|
JS_CACHE_PATH: webapp/node_modules
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,30 @@
|
||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import vue from "eslint-plugin-vue";
|
import vue from "eslint-plugin-vue";
|
||||||
import vueParser from "vue-eslint-parser";
|
|
||||||
import babelParser from "@babel/eslint-parser";
|
|
||||||
import prettierPlugin from "eslint-plugin-prettier";
|
import prettierPlugin from "eslint-plugin-prettier";
|
||||||
import prettierConfig from "eslint-config-prettier";
|
import prettierConfig from "eslint-config-prettier";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
|
import pinia from "eslint-plugin-pinia";
|
||||||
|
|
||||||
const isProd = process.env.NODE_ENV === "production";
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
languageOptions: {
|
ignores: ["dist/**/*", "lib/**/*", "**/*.min.js", "tools/architecture_dashboard/**/*"],
|
||||||
globals: {
|
|
||||||
...globals.node,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
|
|
||||||
...vue.configs["flat/recommended"],
|
...vue.configs["flat/recommended"],
|
||||||
|
|
||||||
prettierConfig,
|
|
||||||
|
|
||||||
{
|
{
|
||||||
files: ["**/*.vue", "**/*.js", "**/*.jsx", "**/*.cjs", "**/*.mjs"],
|
files: ["**/*.vue", "**/*.js", "**/*.jsx", "**/*.cjs", "**/*.mjs"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parser: vueParser,
|
|
||||||
parserOptions: {
|
|
||||||
parser: babelParser,
|
|
||||||
requireConfigFile: false,
|
|
||||||
ecmaVersion: 2021,
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
ecmaVersion: 2021,
|
ecmaVersion: 2021,
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
globals: {
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.browser,
|
||||||
|
...globals.es2021,
|
||||||
process: "readonly",
|
process: "readonly",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -42,6 +32,7 @@ export default [
|
||||||
plugins: {
|
plugins: {
|
||||||
vue,
|
vue,
|
||||||
prettier: prettierPlugin,
|
prettier: prettierPlugin,
|
||||||
|
pinia,
|
||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
|
|
@ -76,6 +67,11 @@ export default [
|
||||||
|
|
||||||
"vue/no-unused-components": "warn",
|
"vue/no-unused-components": "warn",
|
||||||
"vue/no-unused-vars": "warn",
|
"vue/no-unused-vars": "warn",
|
||||||
|
|
||||||
|
// Pinia best practices
|
||||||
|
"pinia/prefer-use-store-naming-convention": "warn",
|
||||||
|
"pinia/require-setup-store-properties-export": "warn",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
prettierConfig,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
2444
webapp/package-lock.json
generated
2444
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,9 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "User interface for the OpenFlexure Microscope Server",
|
"description": "User interface for the OpenFlexure Microscope Server",
|
||||||
"author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)",
|
"author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gv": "genversion src/version.js",
|
"gv": "genversion src/version.js",
|
||||||
"serve": "vite serve --mode development",
|
"serve": "vite serve --mode development",
|
||||||
|
|
@ -18,24 +21,27 @@
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"openseadragon": "^5.0.0",
|
"openseadragon": "^5.0.0",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
"uikit": "^3.24.2",
|
"uikit": "^3.24.2",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0",
|
||||||
"vue-router": "4.0.0",
|
"vue-router": "4.0.0"
|
||||||
"vuex": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.28.5",
|
"@babel/eslint-parser": "^7.28.5",
|
||||||
"@vitejs/plugin-vue": "^5.2.4",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
"@vue/eslint-config-prettier": "9.0.0",
|
"@vue/eslint-config-prettier": "9.0.0",
|
||||||
"autoprefixer": "^10.4.24",
|
"autoprefixer": "^10.4.24",
|
||||||
"core-js": "^3.46.0",
|
"core-js": "^3.46.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-prettier": "^5.1.0",
|
"eslint-plugin-prettier": "^5.1.0",
|
||||||
"eslint-plugin-vue": "^9.20.0",
|
"eslint-plugin-vue": "^9.20.0",
|
||||||
|
"eslint-plugin-pinia": "^0.4.2",
|
||||||
|
"typescript-eslint": "^8.59.0",
|
||||||
"less": "^4.5.1",
|
"less": "^4.5.1",
|
||||||
"material-symbols": "^0.39.1",
|
"material-symbols": "^0.39.1",
|
||||||
"prettier": "^3.2.8",
|
"prettier": "^3.2.8",
|
||||||
"vite": "^5.4.21"
|
"vite": "^7.3.1"
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
<div id="app" class="uk-height-1-1 uk-margin-remove uk-padding-remove" :class="handleTheme">
|
<div id="app" class="uk-height-1-1 uk-margin-remove uk-padding-remove" :class="handleTheme">
|
||||||
<!-- this stops the app loading until setConnected is committed in the store, this means
|
<!-- this stops the app loading until setConnected is committed in the store, this means
|
||||||
other components will not load until we have Thing Descriptions. -->
|
other components will not load until we have Thing Descriptions. -->
|
||||||
<loadingContent v-if="!$store.getters.ready" />
|
<loadingContent v-if="!ready" />
|
||||||
<appContent v-if="$store.getters.ready" />
|
<appContent v-else />
|
||||||
<!-- Runtime modals -->
|
<!-- Runtime modals -->
|
||||||
<div id="modal-center" ref="keyboardManualModal" class="uk-flex-top" uk-modal>
|
<div id="modal-center" ref="keyboardManualModal" class="uk-flex-top" uk-modal>
|
||||||
<div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical">
|
<div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical">
|
||||||
|
|
@ -28,6 +28,9 @@ import appContent from "./components/appContent.vue";
|
||||||
import loadingContent from "./components/loadingContent.vue";
|
import loadingContent from "./components/loadingContent.vue";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { eventBus } from "./eventBus.js";
|
import { eventBus } from "./eventBus.js";
|
||||||
|
import { mapWritableState, mapState, mapActions } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
import { useWotStore } from "@/stores/wot.js";
|
||||||
|
|
||||||
const move_keys = ["up", "down", "left", "right", "pageup", "pagedown"];
|
const move_keys = ["up", "down", "left", "right", "pageup", "pagedown"];
|
||||||
|
|
||||||
|
|
@ -75,6 +78,16 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapWritableState(useSettingsStore, [
|
||||||
|
"appTheme",
|
||||||
|
"navigationStepSize",
|
||||||
|
"navigationInvert",
|
||||||
|
"microscopeHostname",
|
||||||
|
"error",
|
||||||
|
"waiting",
|
||||||
|
]),
|
||||||
|
...mapState(useSettingsStore, ["ready", "baseUri"]),
|
||||||
|
|
||||||
isSystemDark: function () {
|
isSystemDark: function () {
|
||||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -84,9 +97,9 @@ export default {
|
||||||
},
|
},
|
||||||
handleTheme: function () {
|
handleTheme: function () {
|
||||||
var isDark = false;
|
var isDark = false;
|
||||||
if (this.$store.state.appTheme == "dark") {
|
if (this.appTheme == "dark") {
|
||||||
isDark = true;
|
isDark = true;
|
||||||
} else if (this.$store.state.appTheme == "system") {
|
} else if (this.appTheme == "system") {
|
||||||
if (this.systemDark) {
|
if (this.systemDark) {
|
||||||
isDark = true;
|
isDark = true;
|
||||||
}
|
}
|
||||||
|
|
@ -98,126 +111,121 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// watch origin function refactored
|
||||||
|
watch: {
|
||||||
|
baseUri() {
|
||||||
|
this.checkConnection();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
// Query CSS dark theme preference
|
// Query CSS dark theme preference
|
||||||
var mql = window.matchMedia("(prefers-color-scheme: dark)");
|
this.mql = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
this.systemDark = this.mql.matches;
|
||||||
// Check for system dark theme when mounted
|
// Check for system dark theme when mounted
|
||||||
if (mql.matches) {
|
if (this.mql.matches) {
|
||||||
this.systemDark = true;
|
this.systemDark = true;
|
||||||
}
|
}
|
||||||
// Create a theme observer to watch for changes
|
// Create a theme observer to watch for changes
|
||||||
this.themeObserver = mql.addListener((e) => {
|
this.themeWatchdog = (e) => {
|
||||||
if (e.matches) {
|
this.systemDark = e.matches;
|
||||||
this.systemDark = true;
|
};
|
||||||
} else {
|
this.mql.addEventListener("change", this.themeWatchdog);
|
||||||
this.systemDark = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Check connection to API
|
// Check connection to API
|
||||||
this.checkConnection();
|
this.checkConnection();
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function () {
|
created: function () {
|
||||||
window.addEventListener("beforeunload", this.handleExit);
|
window.addEventListener("beforeunload", this.handleExit);
|
||||||
// Scrollwheel listener
|
this.bindHardwareInputs(); // Turn keys ON when connected
|
||||||
window.addEventListener("wheel", this.wheelMonitor);
|
|
||||||
// Watch for origin changes
|
|
||||||
this.unwatchOriginFunction = this.$store.watch(
|
|
||||||
(state, getters) => {
|
|
||||||
return getters.baseUri;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.checkConnection();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Keyboard shortcuts
|
|
||||||
Mousetrap.bind("?", () => {
|
|
||||||
this.toggleModalElement(this.$refs["keyboardManualModal"]); // Calls the mixin
|
|
||||||
});
|
|
||||||
|
|
||||||
Mousetrap.bind(
|
|
||||||
move_keys,
|
|
||||||
(event, key) => {
|
|
||||||
event.preventDefault();
|
|
||||||
this.keysDown.add(key);
|
|
||||||
this.updateJogFromKeys();
|
|
||||||
},
|
|
||||||
"keydown",
|
|
||||||
);
|
|
||||||
|
|
||||||
Mousetrap.bind(
|
|
||||||
move_keys,
|
|
||||||
(event, key) => {
|
|
||||||
event.preventDefault();
|
|
||||||
this.keysDown.delete(key);
|
|
||||||
this.updateJogFromKeys();
|
|
||||||
},
|
|
||||||
"keyup",
|
|
||||||
);
|
|
||||||
|
|
||||||
this.keyboardManual.push({
|
|
||||||
shortcut: "←↑→↓",
|
|
||||||
description: "Move the microscope stage",
|
|
||||||
});
|
|
||||||
|
|
||||||
this.keyboardManual.push({
|
|
||||||
shortcut: "pgup / pgdn",
|
|
||||||
description: "Move the microscope focus",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Capture
|
|
||||||
Mousetrap.bind("c", () => {
|
|
||||||
eventBus.emit("globalCaptureEvent", {});
|
|
||||||
});
|
|
||||||
this.keyboardManual.push({
|
|
||||||
shortcut: "c",
|
|
||||||
description: "Take a capture",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Autofocus
|
|
||||||
Mousetrap.bind("a", () => {
|
|
||||||
eventBus.emit("globalFastAutofocusEvent", {});
|
|
||||||
});
|
|
||||||
this.keyboardManual.push({
|
|
||||||
shortcut: "a",
|
|
||||||
description: "Fast autofocus",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Increment/decrement tab
|
|
||||||
Mousetrap.bind("shift+down", () => {
|
|
||||||
eventBus.emit("globalIncrementTab", {});
|
|
||||||
});
|
|
||||||
Mousetrap.bind("shift+up", () => {
|
|
||||||
eventBus.emit("globalDecrementTab", {});
|
|
||||||
});
|
|
||||||
this.keyboardManual.push({
|
|
||||||
shortcut: "shift+↑ / shift+↓",
|
|
||||||
description: "Switch tab",
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount: function () {
|
beforeUnmount: function () {
|
||||||
// Disconnect the theme observer
|
// Disconnect the theme observer
|
||||||
if (this.themeObserver) {
|
this.mql.removeEventListener("change", this.themeWatchdog);
|
||||||
this.themeObserver.disconnect();
|
|
||||||
}
|
|
||||||
// Remove scrollwheel listener
|
// Remove scrollwheel listener
|
||||||
window.removeEventListener("wheel", this.wheelMonitor);
|
this.unbindHardwareInputs();
|
||||||
// Remove origin watcher
|
|
||||||
this.unwatchOriginFunction();
|
|
||||||
// Remove key listeners
|
|
||||||
Mousetrap.reset();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions(useSettingsStore, ["setConnected"]),
|
||||||
|
|
||||||
|
bindHardwareInputs() {
|
||||||
|
window.addEventListener("wheel", this.wheelMonitor);
|
||||||
|
|
||||||
|
Mousetrap.bind("?", () => {
|
||||||
|
this.toggleModalElement(this.$refs["keyboardManualModal"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mousetrap.bind(
|
||||||
|
move_keys,
|
||||||
|
(event, key) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.keysDown.add(key);
|
||||||
|
this.updateJogFromKeys();
|
||||||
|
},
|
||||||
|
"keydown",
|
||||||
|
);
|
||||||
|
|
||||||
|
Mousetrap.bind(
|
||||||
|
move_keys,
|
||||||
|
(event, key) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.keysDown.delete(key);
|
||||||
|
this.updateJogFromKeys();
|
||||||
|
},
|
||||||
|
"keyup",
|
||||||
|
);
|
||||||
|
|
||||||
|
this.keyboardManual.push({
|
||||||
|
shortcut: "←↑→↓",
|
||||||
|
description: "Move the microscope stage",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.keyboardManual.push({
|
||||||
|
shortcut: "pgup / pgdn",
|
||||||
|
description: "Move the microscope focus",
|
||||||
|
});
|
||||||
|
|
||||||
|
Mousetrap.bind("c", () => {
|
||||||
|
eventBus.emit("globalCaptureEvent", {});
|
||||||
|
});
|
||||||
|
this.keyboardManual.push({
|
||||||
|
shortcut: "c",
|
||||||
|
description: "Take a capture",
|
||||||
|
});
|
||||||
|
|
||||||
|
Mousetrap.bind("a", () => {
|
||||||
|
eventBus.emit("globalFastAutofocusEvent", {});
|
||||||
|
});
|
||||||
|
this.keyboardManual.push({
|
||||||
|
shortcut: "a",
|
||||||
|
description: "Fast autofocus",
|
||||||
|
});
|
||||||
|
|
||||||
|
Mousetrap.bind("shift+down", () => {
|
||||||
|
eventBus.emit("globalIncrementTab", {});
|
||||||
|
});
|
||||||
|
Mousetrap.bind("shift+up", () => {
|
||||||
|
eventBus.emit("globalDecrementTab", {});
|
||||||
|
});
|
||||||
|
this.keyboardManual.push({
|
||||||
|
shortcut: "shift+↑ / shift+↓",
|
||||||
|
description: "Switch tab",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
unbindHardwareInputs() {
|
||||||
|
window.removeEventListener("wheel", this.wheelMonitor);
|
||||||
|
Mousetrap.reset();
|
||||||
|
this.keyboardManual = [];
|
||||||
|
},
|
||||||
|
|
||||||
async checkConnection() {
|
async checkConnection() {
|
||||||
var baseUri = this.$store.getters.baseUri;
|
this.waiting = true;
|
||||||
this.$store.commit("changeWaiting", true);
|
const wotStore = useWotStore();
|
||||||
// TODO: more robust check - e.g. use a microscope Thing
|
|
||||||
// TODO: should we purge existing consumedThings?
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch("wot/fetchThingDescriptions", `${baseUri}/thing_descriptions/`);
|
await wotStore.fetchThingDescriptions(`${this.baseUri}/thing_descriptions/`);
|
||||||
for (let requiredThing of ["camera", "system"]) {
|
for (let requiredThing of ["camera", "system"]) {
|
||||||
if (!this.thingAvailable(requiredThing)) {
|
if (!this.thingAvailable(requiredThing)) {
|
||||||
throw new Error(`No ${requiredThing} found, the GUI won't work without one.`);
|
throw new Error(`No ${requiredThing} found, the GUI won't work without one.`);
|
||||||
|
|
@ -225,37 +233,32 @@ export default {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let hostname = await this.readThingProperty("system", "hostname");
|
let hostname = await this.readThingProperty("system", "hostname");
|
||||||
this.$store.commit("changeMicroscopeHostname", hostname);
|
this.microscopeHostname = hostname;
|
||||||
document.title = `OpenFlexure Microscope: ${hostname}`;
|
document.title = `OpenFlexure Microscope: ${hostname}`;
|
||||||
} catch {
|
} catch {
|
||||||
this.$store.commit("changeMicroscopeHostname", null);
|
this.microscopeHostname = null;
|
||||||
}
|
}
|
||||||
// start rendering components
|
this.setConnected();
|
||||||
this.$store.commit("setConnected");
|
this.error = null;
|
||||||
this.$store.commit("setErrorMessage", null);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$store.commit("setErrorMessage", error);
|
this.error = error;
|
||||||
} finally {
|
} finally {
|
||||||
this.$store.commit("changeWaiting", false);
|
this.waiting = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleExit: function () {},
|
|
||||||
|
handleExit() {},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle global mouse wheel events to be associated with navigation
|
* Handle global mouse wheel events to be associated with navigation
|
||||||
*/
|
*/
|
||||||
wheelMonitor: function (event) {
|
wheelMonitor(event) {
|
||||||
// Only capture scroll if the event target's parent contains the "scrollTarget" class
|
|
||||||
if (
|
if (
|
||||||
event.target.parentNode.classList.contains("scrollTarget") ||
|
event.target.parentNode.classList.contains("scrollTarget") ||
|
||||||
event.target.classList.contains("scrollTarget")
|
event.target.classList.contains("scrollTarget")
|
||||||
) {
|
) {
|
||||||
const z_rel = event.deltaY / 100;
|
const z_rel = event.deltaY / 100;
|
||||||
// Emit a signal to move, acted on by panelControl.vue
|
const z = z_rel * this.navigationStepSize.z;
|
||||||
const navigationStepSize = this.$store.state.navigationStepSize;
|
|
||||||
const z = z_rel * navigationStepSize.z;
|
|
||||||
// Don't use `jog() due to variable size of jogs here and the rate limiting in
|
|
||||||
// `jog()`. No need to invert on z, as navigationInvert.z isn't exposed.
|
|
||||||
this.invokeAction("stage", "jog", { x: 0, y: 0, z: z });
|
this.invokeAction("stage", "jog", { x: 0, y: 0, z: z });
|
||||||
eventBus.emit("globalUpdatePositionEvent");
|
eventBus.emit("globalUpdatePositionEvent");
|
||||||
}
|
}
|
||||||
|
|
@ -269,18 +272,14 @@ export default {
|
||||||
* request to jog that is too recent after the last jog.
|
* request to jog that is too recent after the last jog.
|
||||||
*/
|
*/
|
||||||
jog(x, y, z) {
|
jog(x, y, z) {
|
||||||
// Manually debounce extra requests from keyboard repeat rate.
|
|
||||||
// This is used rather than an interval in case of missing a repeat.
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const navigationInvert = this.$store.state.navigationInvert;
|
|
||||||
if (now - this.lastJogTime < this.jogTime) {
|
if (now - this.lastJogTime < this.jogTime) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.lastJogTime = now;
|
this.lastJogTime = now;
|
||||||
|
|
||||||
this.invokeAction("stage", "jog", {
|
this.invokeAction("stage", "jog", {
|
||||||
x: x * this.jogDistance * (navigationInvert.x ? -1 : 1),
|
x: x * this.jogDistance * (this.navigationInvert.x ? -1 : 1),
|
||||||
y: y * this.jogDistance * (navigationInvert.y ? -1 : 1),
|
y: y * this.jogDistance * (this.navigationInvert.y ? -1 : 1),
|
||||||
z: z * this.jogDistance,
|
z: z * this.jogDistance,
|
||||||
});
|
});
|
||||||
eventBus.emit("globalUpdatePositionEvent");
|
eventBus.emit("globalUpdatePositionEvent");
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ import slideScanContent from "./tabContentComponents/slideScanContent.vue";
|
||||||
import viewContent from "./tabContentComponents/viewContent.vue";
|
import viewContent from "./tabContentComponents/viewContent.vue";
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
import { eventBus } from "../eventBus.js";
|
import { eventBus } from "../eventBus.js";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.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";
|
||||||
|
|
@ -196,6 +197,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
const store = useSettingsStore();
|
||||||
// A global signal listener to switch tab
|
// A global signal listener to switch tab
|
||||||
eventBus.on("globalSwitchTab", (tabID) => {
|
eventBus.on("globalSwitchTab", (tabID) => {
|
||||||
this.currentTab = tabID;
|
this.currentTab = tabID;
|
||||||
|
|
@ -208,7 +210,7 @@ export default {
|
||||||
eventBus.on("globalDecrementTab", () => {
|
eventBus.on("globalDecrementTab", () => {
|
||||||
this.incrementTabBy(-1);
|
this.incrementTabBy(-1);
|
||||||
});
|
});
|
||||||
if (this.$store.getters.ready) {
|
if (store.ready) {
|
||||||
this.startModals();
|
this.startModals();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useIntersectionObserver } from "@vueuse/core";
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -28,8 +30,9 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
streamImgUri: function () {
|
...mapState(useSettingsStore, ["baseUri"]),
|
||||||
return `${this.$store.getters.baseUri}/camera/mjpeg_stream`;
|
streamImgUri() {
|
||||||
|
return `${this.baseUri}/camera/mjpeg_stream`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ProgressBar",
|
name: "ProgressBar",
|
||||||
|
|
||||||
|
|
@ -13,6 +16,7 @@ export default {
|
||||||
emits: ["set-tab"],
|
emits: ["set-tab"],
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(useSettingsStore, ["ready"]),
|
||||||
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);
|
||||||
return `pos: right; title: ${title}; delay: 500`;
|
return `pos: right; title: ${title}; delay: 500`;
|
||||||
|
|
@ -21,7 +25,7 @@ export default {
|
||||||
classObject: function () {
|
classObject: function () {
|
||||||
return {
|
return {
|
||||||
"tabicon-active": this.currentTab == this.id,
|
"tabicon-active": this.currentTab == this.id,
|
||||||
"uk-disabled": this.requireConnection && !this.$store.getters.ready,
|
"uk-disabled": this.requireConnection && !this.ready,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="!(requireConnection && !$store.getters.ready)"
|
v-if="!(requireConnection && !ready)"
|
||||||
:hidden="currentTab != tabID"
|
:hidden="currentTab != tabID"
|
||||||
class="uk-width-expand uk-height-1-1"
|
class="uk-width-expand uk-height-1-1"
|
||||||
>
|
>
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "TabContent",
|
name: "TabContent",
|
||||||
|
|
||||||
|
|
@ -23,7 +26,9 @@ export default {
|
||||||
},
|
},
|
||||||
requireConnection: Boolean,
|
requireConnection: Boolean,
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {
|
||||||
|
...mapState(useSettingsStore, ["ready"]),
|
||||||
|
},
|
||||||
|
|
||||||
methods: {},
|
methods: {},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "TabIcon",
|
name: "TabIcon",
|
||||||
|
|
||||||
|
|
@ -52,6 +55,7 @@ export default {
|
||||||
emits: ["set-tab"],
|
emits: ["set-tab"],
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(useSettingsStore, ["ready"]),
|
||||||
computedTitle: function () {
|
computedTitle: function () {
|
||||||
if (this.title !== undefined) {
|
if (this.title !== undefined) {
|
||||||
return this.title;
|
return this.title;
|
||||||
|
|
@ -74,7 +78,7 @@ export default {
|
||||||
classObject: function () {
|
classObject: function () {
|
||||||
return {
|
return {
|
||||||
"tabicon-active": this.currentTab == this.tabID,
|
"tabicon-active": this.currentTab == this.tabID,
|
||||||
"uk-disabled": this.requireConnection && !this.$store.getters.ready,
|
"uk-disabled": this.requireConnection && !this.ready,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@ export default {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async checkExistingTasks() {
|
async checkExistingTasks() {
|
||||||
const ongoingTask = await this.getOngingAction(this.thing, this.action);
|
const ongoingTask = await this.getOngoingAction(this.thing, this.action);
|
||||||
if (ongoingTask) {
|
if (ongoingTask) {
|
||||||
// There is a started task
|
// There is a started task
|
||||||
this.taskStarted = true;
|
this.taskStarted = true;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
barWidthFromProgress: function () {
|
barWidthFromProgress: function () {
|
||||||
var progress = this.progress <= 100 ? this.progress : 100;
|
var progress = this.progress <= 100 ? this.progress : 100;
|
||||||
var styleString = `width: ${progress}%`;
|
var styleString = progress != null ? `width: ${progress}%` : `width: 0%`;
|
||||||
return styleString;
|
return styleString;
|
||||||
},
|
},
|
||||||
indeterminateProgressBar: function () {
|
indeterminateProgressBar: function () {
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,11 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
propertyDescription: function () {
|
propertyDescription: function () {
|
||||||
try {
|
const td = this.wotStore.thingDescriptions[this.thingName];
|
||||||
return this.thingDescription(this.thingName).properties[this.propertyName];
|
// Return `undefined` if the thing doesn't exist or has no properties
|
||||||
} catch {
|
if (!td || !td.properties) return undefined;
|
||||||
return undefined;
|
// JS returns `undefined` if this property name doesn't exist
|
||||||
}
|
return td.properties[this.propertyName];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="uk-flex uk-flex-column uk-flex-center uk-height-1-1">
|
<div class="uk-flex uk-flex-column uk-flex-center uk-height-1-1">
|
||||||
<div v-if="$store.state.waiting" class="uk-align-center" uk-spinner="ratio: 3"></div>
|
<div v-if="waiting" class="uk-align-center uk-text-center">
|
||||||
<div v-if="$store.state.waiting" class="uk-align-center">Loading...</div>
|
<div uk-spinner="ratio: 3"></div>
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
<span class="material-symbols-outlined uk-align-center error-icon">error_outline</span>
|
<span class="material-symbols-outlined uk-align-center error-icon">error_outline</span>
|
||||||
<div v-if="$store.state.error" class="uk-align-center">
|
<div v-if="error" class="uk-align-center">
|
||||||
{{ $store.state.error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-align-center">
|
<div class="uk-align-center">
|
||||||
<devTools class="uk-width-medium"></devTools>
|
<devTools class="uk-width-medium"></devTools>
|
||||||
|
|
@ -14,6 +16,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import devTools from "./tabContentComponents/aboutComponents/devTools.vue";
|
import devTools from "./tabContentComponents/aboutComponents/devTools.vue";
|
||||||
|
import { mapWritableState } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -23,8 +27,8 @@ export default {
|
||||||
devTools,
|
devTools,
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function () {
|
computed: {
|
||||||
return {};
|
...mapWritableState(useSettingsStore, ["error", "waiting"]),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@
|
||||||
<script>
|
<script>
|
||||||
import stepTemplateWithStream from "../stepTemplateWithStream.vue";
|
import stepTemplateWithStream from "../stepTemplateWithStream.vue";
|
||||||
import cameraCalibrationSettings from "../../../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue";
|
import cameraCalibrationSettings from "../../../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CameraMainCalibrationStep",
|
name: "CameraMainCalibrationStep",
|
||||||
|
|
@ -34,8 +36,9 @@ export default {
|
||||||
emits: ["awaiting-user"],
|
emits: ["awaiting-user"],
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(useSettingsStore, ["baseUri"]),
|
||||||
cameraUri: function () {
|
cameraUri: function () {
|
||||||
return `${this.$store.getters.baseUri}/camera/`;
|
return `${this.baseUri}/camera/`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<form class="uk-form-stacked" action="" method="GET" @submit="overrideAPIHost">
|
<form class="uk-form-stacked" action="" method="GET" @submit="overrideAPIHost">
|
||||||
<label class="uk-form-label">Override API origin</label>
|
<label class="uk-form-label">Override API origin</label>
|
||||||
<input v-model="newOrigin" name="overrideOrigin" class="uk-input" type="text" />
|
<input v-model="overrideOrigin" name="overrideOrigin" class="uk-input" type="text" />
|
||||||
<label class="uk-form-label">
|
<label class="uk-form-label">
|
||||||
<input v-model="reloadWhenOverridingOrigin" class="uk-checkbox" type="checkbox" />
|
<input v-model="reloadWhenOverridingOrigin" class="uk-checkbox" type="checkbox" />
|
||||||
Reload web app with new origin
|
Reload web app with new origin
|
||||||
|
|
@ -13,29 +13,26 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Export main app
|
import { mapWritableState } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "DevTools",
|
name: "DevTools",
|
||||||
|
|
||||||
components: {},
|
data() {
|
||||||
|
|
||||||
data: function () {
|
|
||||||
return {
|
return {
|
||||||
newOrigin: this.$store.state.overrideOrigin,
|
|
||||||
reloadWhenOverridingOrigin: true,
|
reloadWhenOverridingOrigin: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
computed: {
|
||||||
overrideAPIHost: function (event) {
|
...mapWritableState(useSettingsStore, ["overrideOrigin", "origin"]),
|
||||||
// Save the origin override, so that if we reload the web app, you can easily
|
},
|
||||||
this.$store.commit("changeOverrideOrigin", this.newOrigin);
|
|
||||||
|
|
||||||
// If we have elected not to reload the interface, just update the origin
|
methods: {
|
||||||
// in the store. Otherwise, the form's default action will do the job for us.
|
overrideAPIHost(event) {
|
||||||
// TODO: preserve other query parameters when reloading
|
|
||||||
if (!this.reloadWhenOverridingOrigin) {
|
if (!this.reloadWhenOverridingOrigin) {
|
||||||
this.$store.commit("changeOrigin", this.newOrigin);
|
this.origin = this.overrideOrigin;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="host-input">
|
<div class="host-input">
|
||||||
<div v-if="$store.state.available">
|
<div v-if="available">
|
||||||
<div>
|
<div>
|
||||||
<div class="uk-margin-small-bottom">
|
<div class="uk-margin-small-bottom">
|
||||||
<b>Microscope Hostname:</b>
|
<b>Microscope Hostname:</b>
|
||||||
<br />
|
<br />
|
||||||
{{ $store.state.microscopeHostname }}
|
{{ microscopeHostname }}
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-margin-small-bottom">
|
<div class="uk-margin-small-bottom">
|
||||||
<b>API Origin:</b>
|
<b>API Origin:</b>
|
||||||
<br />
|
<br />
|
||||||
{{ $store.state.origin }}
|
{{ origin }}
|
||||||
</div>
|
</div>
|
||||||
<action-button
|
<action-button
|
||||||
v-if="illuminationType"
|
v-if="illuminationType"
|
||||||
|
|
@ -56,14 +56,17 @@
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="$store.state.waiting">Loading...</div>
|
<div v-else-if="waiting">Loading...</div>
|
||||||
<div v-else-if="$store.state.error"><b>Error:</b> {{ $store.state.error }}</div>
|
<div v-else-if="error"><b>Error:</b> {{ error }}</div>
|
||||||
<div v-else>No active connection</div>
|
<div v-else>No active connection</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
||||||
|
import { mapState, mapWritableState } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
import { useWotStore } from "@/stores/wot.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "StatusPane",
|
name: "StatusPane",
|
||||||
|
|
@ -79,16 +82,20 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(useSettingsStore, ["origin", "microscopeHostname", "available", "waiting"]),
|
||||||
|
...mapState(useWotStore, ["thingDescriptions"]),
|
||||||
|
...mapWritableState(useSettingsStore, ["error"]),
|
||||||
|
|
||||||
cameraType() {
|
cameraType() {
|
||||||
// No need to check as the microscope won't start up without a camera defined
|
// No need to check as the microscope won't start up without a camera defined
|
||||||
return this.thingDescription("camera").title;
|
return this.thingDescriptions["camera"]?.title;
|
||||||
},
|
},
|
||||||
stageType() {
|
stageType() {
|
||||||
return this.thingAvailable("stage") ? this.thingDescription("stage").title : undefined;
|
return this.thingAvailable("stage") ? this.thingDescriptions["stage"]?.title : undefined;
|
||||||
},
|
},
|
||||||
illuminationType() {
|
illuminationType() {
|
||||||
return this.thingAvailable("illumination")
|
return this.thingAvailable("illumination")
|
||||||
? this.thingDescription("illumination").title
|
? this.thingDescriptions["illumination"]?.title
|
||||||
: undefined;
|
: undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ export default {
|
||||||
},
|
},
|
||||||
async checkIfStartedExternally() {
|
async checkIfStartedExternally() {
|
||||||
if (!this.taskStarted | !this.taskRunning) {
|
if (!this.taskStarted | !this.taskRunning) {
|
||||||
const ongoingTask = await this.getOngingAction(this.thing, this.action);
|
const ongoingTask = await this.getOngoingAction(this.thing, this.action);
|
||||||
if (ongoingTask) {
|
if (ongoingTask) {
|
||||||
this.resetData();
|
this.resetData();
|
||||||
const taskUrl = ongoingTask.links.find((t) => t.rel == "self").href;
|
const taskUrl = ongoingTask.links.find((t) => t.rel == "self").href;
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { eventBus } from "@/eventBus.js";
|
import { eventBus } from "@/eventBus.js";
|
||||||
|
import { mapWritableState } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "StageControlButtons",
|
name: "StageControlButtons",
|
||||||
|
|
@ -97,6 +99,11 @@ export default {
|
||||||
jogDistance: 600,
|
jogDistance: 600,
|
||||||
jogTime: 300,
|
jogTime: 300,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapWritableState(useSettingsStore, ["navigationInvert"]),
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
/**
|
/**
|
||||||
* Jog d-pad and focus buttons.
|
* Jog d-pad and focus buttons.
|
||||||
|
|
@ -116,11 +123,10 @@ export default {
|
||||||
// pointer is.
|
// pointer is.
|
||||||
pointerEvent.target.setPointerCapture(pointerEvent.pointerId);
|
pointerEvent.target.setPointerCapture(pointerEvent.pointerId);
|
||||||
|
|
||||||
const navigationInvert = this.$store.state.navigationInvert;
|
|
||||||
let invokeJog = () =>
|
let invokeJog = () =>
|
||||||
this.invokeAction("stage", "jog", {
|
this.invokeAction("stage", "jog", {
|
||||||
x: x * this.jogDistance * (navigationInvert.x ? -1 : 1),
|
x: x * this.jogDistance * (this.navigationInvert.x ? -1 : 1),
|
||||||
y: y * this.jogDistance * (navigationInvert.y ? -1 : 1),
|
y: y * this.jogDistance * (this.navigationInvert.y ? -1 : 1),
|
||||||
z: z * this.jogDistance,
|
z: z * this.jogDistance,
|
||||||
});
|
});
|
||||||
invokeJog();
|
invokeJog();
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,8 @@ import axios from "axios";
|
||||||
import PaginateLinks from "@/components/genericComponents/paginateLinks.vue";
|
import PaginateLinks from "@/components/genericComponents/paginateLinks.vue";
|
||||||
import EndpointButton from "../labThingsComponents/endpointButton.vue";
|
import EndpointButton from "../labThingsComponents/endpointButton.vue";
|
||||||
import { useIntersectionObserver } from "@vueuse/core";
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "LoggingContent",
|
name: "LoggingContent",
|
||||||
|
|
@ -114,6 +116,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(useSettingsStore, ["baseUri"]),
|
||||||
filteredLevels: function () {
|
filteredLevels: function () {
|
||||||
let cutoffIndex = this.allLevels.indexOf(this.filterLevel);
|
let cutoffIndex = this.allLevels.indexOf(this.filterLevel);
|
||||||
return this.allLevels.slice(cutoffIndex, -1);
|
return this.allLevels.slice(cutoffIndex, -1);
|
||||||
|
|
@ -130,10 +133,10 @@ export default {
|
||||||
return items;
|
return items;
|
||||||
},
|
},
|
||||||
logURI: function () {
|
logURI: function () {
|
||||||
return `${this.$store.getters.baseUri}/log/`;
|
return `${this.baseUri}/log/`;
|
||||||
},
|
},
|
||||||
logFileURI: function () {
|
logFileURI: function () {
|
||||||
return `${this.$store.getters.baseUri}/logfile/`;
|
return `${this.baseUri}/logfile/`;
|
||||||
},
|
},
|
||||||
pagedItems: function () {
|
pagedItems: function () {
|
||||||
let startIndex = (this.currentPage - 1) * this.maxitems;
|
let startIndex = (this.currentPage - 1) * this.maxitems;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { useWotStore } from "@/stores/wot.js";
|
||||||
|
import { mapActions } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PowerContent",
|
name: "PowerContent",
|
||||||
|
|
@ -52,6 +55,8 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions(useSettingsStore, ["resetState"]),
|
||||||
|
...mapActions(useWotStore, ["deleteAllThingDescriptions"]),
|
||||||
systemRequest: function (action) {
|
systemRequest: function (action) {
|
||||||
let message = "";
|
let message = "";
|
||||||
if (action == "reboot") {
|
if (action == "reboot") {
|
||||||
|
|
@ -61,10 +66,10 @@ export default {
|
||||||
}
|
}
|
||||||
this.modalConfirm(message).then(
|
this.modalConfirm(message).then(
|
||||||
() => {
|
() => {
|
||||||
this.$store.commit("resetState");
|
|
||||||
this.$store.commit("wot/deleteAllThingDescriptions");
|
|
||||||
// Post and silence errors
|
// Post and silence errors
|
||||||
axios.post(this.thingActionUrl("system", action)).catch(() => {});
|
axios.post(this.thingActionUrl("system", action)).catch(() => {});
|
||||||
|
this.resetState();
|
||||||
|
this.deleteAllThingDescriptions();
|
||||||
},
|
},
|
||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import actionButton from "../../labThingsComponents/actionButton.vue";
|
import actionButton from "../../labThingsComponents/actionButton.vue";
|
||||||
import EndpointButton from "../../labThingsComponents/endpointButton.vue";
|
import EndpointButton from "../../labThingsComponents/endpointButton.vue";
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -109,11 +111,12 @@ export default {
|
||||||
emits: ["viewer-requested", "update-requested"],
|
emits: ["viewer-requested", "update-requested"],
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(useSettingsStore, ["baseUri"]),
|
||||||
downloadStitchFile() {
|
downloadStitchFile() {
|
||||||
return `${this.$store.getters.baseUri}/smart_scan/get_stitch/${this.scanData.name}`;
|
return `${this.baseUri}/smart_scan/get_stitch/${this.scanData.name}`;
|
||||||
},
|
},
|
||||||
thumbnailPath() {
|
thumbnailPath() {
|
||||||
return `${this.$store.getters.baseUri}/smart_scan/scans/stitched_thumbnail.jpg?scan_name=${this.scanData.name}&modified=${this.scanData.modified}`;
|
return `${this.baseUri}/smart_scan/scans/stitched_thumbnail.jpg?scan_name=${this.scanData.name}&modified=${this.scanData.modified}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,10 +67,6 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
baseUri: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,10 @@
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<ScanViewerModal
|
<ScanViewerModal ref="scanViewer" :selected-scan="selectedScan" :base-uri="baseUri" />
|
||||||
ref="scanViewer"
|
|
||||||
:selected-scan="selectedScan"
|
|
||||||
:base-uri="$store.getters.baseUri"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Gallery -->
|
<!-- Gallery -->
|
||||||
<div
|
<div v-if="ready" class="uk-padding-remove-top" uk-lightbox="toggle: .lightbox-link">
|
||||||
v-if="$store.getters.ready"
|
|
||||||
class="uk-padding-remove-top"
|
|
||||||
uk-lightbox="toggle: .lightbox-link"
|
|
||||||
>
|
|
||||||
<!-- Gallery capture cards -->
|
<!-- Gallery capture cards -->
|
||||||
<div class="gallery-grid uk-grid-match" uk-grid>
|
<div class="gallery-grid uk-grid-match" uk-grid>
|
||||||
<div v-if="scansEmpty">
|
<div v-if="scansEmpty">
|
||||||
|
|
@ -86,6 +78,9 @@ import scanCard from "./scanListComponents/scanCard.vue";
|
||||||
import ScanViewerModal from "./scanListComponents/scanViewer.vue";
|
import ScanViewerModal from "./scanListComponents/scanViewer.vue";
|
||||||
import { eventBus } from "../../eventBus.js";
|
import { eventBus } from "../../eventBus.js";
|
||||||
import { useIntersectionObserver } from "@vueuse/core";
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
import { mapState, storeToRefs } from "pinia";
|
||||||
|
import { watch } from "vue";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -111,6 +106,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(useSettingsStore, ["baseUri", "ready"]),
|
||||||
scansUri() {
|
scansUri() {
|
||||||
return this.thingPropertyUrl("smart_scan", "scans");
|
return this.thingPropertyUrl("smart_scan", "scans");
|
||||||
},
|
},
|
||||||
|
|
@ -119,7 +115,7 @@ export default {
|
||||||
},
|
},
|
||||||
selectedScanDZI() {
|
selectedScanDZI() {
|
||||||
if (this.selectedScan && this.selectedScan.dzi != "") {
|
if (this.selectedScan && this.selectedScan.dzi != "") {
|
||||||
return `${this.$store.getters.baseUri}/data/smart_scan/${this.selectedScan.name}/images/${this.selectedScan.dzi}`;
|
return `${this.baseUri}/data/smart_scan/${this.selectedScan.name}/images/${this.selectedScan.dzi}`;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +130,15 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
const store = useSettingsStore();
|
||||||
|
const { ready } = storeToRefs(store);
|
||||||
|
watch(ready, (isReady) => {
|
||||||
|
if (isReady) {
|
||||||
|
this.updateScans();
|
||||||
|
} else {
|
||||||
|
this.scans = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
useIntersectionObserver(
|
useIntersectionObserver(
|
||||||
this.$refs.galleryDisplay,
|
this.$refs.galleryDisplay,
|
||||||
([{ isIntersecting }]) => {
|
([{ isIntersecting }]) => {
|
||||||
|
|
@ -155,24 +160,6 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function () {
|
|
||||||
// Watch for host 'ready', then update status
|
|
||||||
this.unwatchStoreFunction = this.$store.watch(
|
|
||||||
(state, getters) => {
|
|
||||||
return getters.ready;
|
|
||||||
},
|
|
||||||
(ready) => {
|
|
||||||
if (ready) {
|
|
||||||
// If the connection is now ready, update capture list
|
|
||||||
this.updateScans();
|
|
||||||
} else {
|
|
||||||
// If the connection is now disconnected, empty capture list
|
|
||||||
this.captures = {};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
// Remove global signal listener to perform a gallery refresh
|
// Remove global signal listener to perform a gallery refresh
|
||||||
eventBus.off("globalUpdateScans", this.updateScans);
|
eventBus.off("globalUpdateScans", this.updateScans);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@
|
||||||
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
|
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
|
||||||
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
||||||
import ServerSpecifiedPropertyControl from "../../labThingsComponents/serverSpecifiedPropertyControl.vue";
|
import ServerSpecifiedPropertyControl from "../../labThingsComponents/serverSpecifiedPropertyControl.vue";
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -43,8 +45,9 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
cameraUri: function () {
|
...mapState(useSettingsStore, ["baseUri"]),
|
||||||
return `${this.$store.getters.baseUri}/camera/`;
|
cameraUri() {
|
||||||
|
return `${this.baseUri}/camera/`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
import { mapWritableState } from "pinia";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
name: "AppSettings",
|
name: "AppSettings",
|
||||||
|
|
@ -28,14 +31,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
appTheme: {
|
...mapWritableState(useSettingsStore, ["appTheme"]),
|
||||||
get() {
|
|
||||||
return this.$store.state.appTheme;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.$store.commit("changeAppTheme", value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapWritableState } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
name: "StreamSettings",
|
name: "StreamSettings",
|
||||||
|
|
@ -21,14 +24,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
disableStream: {
|
...mapWritableState(useSettingsStore, ["disableStream"]),
|
||||||
get() {
|
|
||||||
return this.$store.state.disableStream;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.$store.commit("changeDisableStream", value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -10,29 +10,27 @@
|
||||||
<div>
|
<div>
|
||||||
<label class="uk-form-label" for="form-stacked-text">x</label>
|
<label class="uk-form-label" for="form-stacked-text">x</label>
|
||||||
<div class="uk-form-controls">
|
<div class="uk-form-controls">
|
||||||
<input v-model="stepSize.x" class="uk-input uk-form-small" type="number" />
|
<input v-model="navigationStepSize.x" class="uk-input uk-form-small" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<label class="uk-margin-small-right">
|
<label class="uk-margin-small-right">
|
||||||
<input v-model="invert.x" class="uk-checkbox" type="checkbox" />
|
<input v-model="navigationInvert.x" class="uk-checkbox" type="checkbox" />
|
||||||
Invert x
|
Invert x
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="uk-form-label" for="form-stacked-text">y</label>
|
<label class="uk-form-label" for="form-stacked-text">y</label>
|
||||||
<div class="uk-form-controls">
|
<div class="uk-form-controls">
|
||||||
<input v-model="stepSize.y" class="uk-input uk-form-small" type="number" />
|
<input v-model="navigationStepSize.y" class="uk-input uk-form-small" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<label class="uk-margin-small-right">
|
<label class="uk-margin-small-right">
|
||||||
<input v-model="invert.y" class="uk-checkbox" type="checkbox" />
|
<input v-model="navigationInvert.y" class="uk-checkbox" type="checkbox" />
|
||||||
Invert y
|
Invert y
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="uk-form-label" for="form-stacked-text">z</label>
|
<label class="uk-form-label" for="form-stacked-text">z</label>
|
||||||
<div class="uk-form-controls">
|
<div class="uk-form-controls">
|
||||||
<input v-model="stepSize.z" class="uk-input uk-form-small" type="number" />
|
<input v-model="navigationStepSize.z" class="uk-input uk-form-small" type="number" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -40,35 +38,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "StageControlSettings",
|
name: "StageControlSettings",
|
||||||
|
|
||||||
computed: {
|
setup() {
|
||||||
// Note that as stepSize and invert are mutated (i.e. we change stepSize.x not stepSize)
|
const store = useSettingsStore();
|
||||||
// rather than directly set we cannot use get() and set() computed to interact with the
|
const { navigationStepSize, navigationInvert } = storeToRefs(store);
|
||||||
// store as changed won't be detected by set(). Instead use a deep watcher to
|
return { navigationStepSize, navigationInvert };
|
||||||
// update the store (see ``watch:`` below)
|
|
||||||
stepSize() {
|
|
||||||
return this.$store.state.navigationStepSize;
|
|
||||||
},
|
|
||||||
invert() {
|
|
||||||
return this.$store.state.navigationInvert;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
stepSize: {
|
|
||||||
deep: true,
|
|
||||||
handler(newVal) {
|
|
||||||
this.$store.commit("changeNavigationStepSize", newVal);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
invert: {
|
|
||||||
deep: true,
|
|
||||||
handler(newVal) {
|
|
||||||
this.$store.commit("changeNavigationInvert", newVal);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<propertyControl
|
<propertyControl
|
||||||
|
v-if="thingAvailable('smart_scan')"
|
||||||
thing-name="smart_scan"
|
thing-name="smart_scan"
|
||||||
property-name="stitch_tiff"
|
property-name="stitch_tiff"
|
||||||
label="When Stitching, Produce a Pyramidal TIFF"
|
label="When Stitching, Produce a Pyramidal TIFF"
|
||||||
|
|
@ -55,6 +56,7 @@ export default {
|
||||||
workflowName: undefined,
|
workflowName: undefined,
|
||||||
workflowSettings: [],
|
workflowSettings: [],
|
||||||
workflowOptions: [],
|
workflowOptions: [],
|
||||||
|
workflowReady: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -95,7 +97,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.workflowName) {
|
if (this.workflowName) {
|
||||||
this.ready = await this.readThingProperty(this.workflowName, "ready", true);
|
this.workflowReady = await this.readThingProperty(this.workflowName, "ready", true);
|
||||||
this.workflowSettings =
|
this.workflowSettings =
|
||||||
(await this.getThingEndpoint(this.workflowName, "settings_ui")) || [];
|
(await this.getThingEndpoint(this.workflowName, "settings_ui")) || [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,8 @@ import actionTab from "./actionTab.vue";
|
||||||
import slideScanControls from "./slideScanComponents/slideScanControls.vue";
|
import slideScanControls from "./slideScanComponents/slideScanControls.vue";
|
||||||
import streamDisplay from "./streamContent.vue";
|
import streamDisplay from "./streamContent.vue";
|
||||||
import ActionButton from "../labThingsComponents/actionButton.vue";
|
import ActionButton from "../labThingsComponents/actionButton.vue";
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
export default {
|
export default {
|
||||||
name: "SlideScanContent",
|
name: "SlideScanContent",
|
||||||
|
|
||||||
|
|
@ -84,6 +85,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(useSettingsStore, ["baseUri"]),
|
||||||
scanning() {
|
scanning() {
|
||||||
return this.taskId && this.taskUrl;
|
return this.taskId && this.taskUrl;
|
||||||
},
|
},
|
||||||
|
|
@ -138,7 +140,7 @@ export default {
|
||||||
// while the scan is running
|
// while the scan is running
|
||||||
let mtime = await this.readThingProperty("smart_scan", "latest_preview_stitch_time", true);
|
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.baseUri}/smart_scan/latest_preview_stitch.jpg?t=${mtime}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastScanName = await this.readThingProperty("smart_scan", "latest_scan_name", true);
|
this.lastScanName = await this.readThingProperty("smart_scan", "latest_scan_name", true);
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,11 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="!streamEnabled" class="uk-height-1-1">
|
<div v-if="!streamEnabled" class="uk-height-1-1">
|
||||||
<div v-if="$store.state.waiting" class="uk-position-center">
|
<div v-if="waiting" class="uk-position-center">
|
||||||
<div uk-spinner="ratio: 4.5"></div>
|
<div uk-spinner="ratio: 4.5"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-else-if="disableStream" class="uk-position-center position-relative text-center">
|
||||||
v-else-if="$store.state.disableStream"
|
|
||||||
class="uk-position-center position-relative text-center"
|
|
||||||
>
|
|
||||||
Stream preview disabled
|
Stream preview disabled
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -36,11 +33,20 @@
|
||||||
<script>
|
<script>
|
||||||
import { eventBus } from "../../eventBus.js";
|
import { eventBus } from "../../eventBus.js";
|
||||||
import { useIntersectionObserver } from "@vueuse/core";
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
name: "StreamDisplay",
|
name: "StreamDisplay",
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const store = useSettingsStore();
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
|
|
@ -51,15 +57,16 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(useSettingsStore, ["ready", "baseUri", "disableStream", "waiting"]),
|
||||||
streamEnabled: function () {
|
streamEnabled: function () {
|
||||||
return this.$store.getters.ready && !this.$store.state.disableStream;
|
return this.ready && !this.disableStream;
|
||||||
},
|
},
|
||||||
thisStreamOpen: function () {
|
thisStreamOpen: function () {
|
||||||
// Only a single MJPEG connection should be open at a time
|
// Only a single MJPEG connection should be open at a time
|
||||||
return !(this.displaySize[0] == 0) && !(this.displaySize[1] == 0);
|
return !(this.displaySize[0] == 0) && !(this.displaySize[1] == 0);
|
||||||
},
|
},
|
||||||
streamImgUri: function () {
|
streamImgUri: function () {
|
||||||
return `${this.$store.getters.baseUri}/camera/mjpeg_stream`;
|
return `${this.baseUri}/camera/mjpeg_stream`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -101,7 +108,7 @@ export default {
|
||||||
// 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
|
||||||
this.$store.commit("removeStream", this._uid);
|
this.store.removeStream(this.$.uid);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -154,12 +161,12 @@ export default {
|
||||||
// Handle closed stream
|
// Handle closed stream
|
||||||
if (this.displaySize[0] == 0 && this.displaySize[1] == 0) {
|
if (this.displaySize[0] == 0 && this.displaySize[1] == 0) {
|
||||||
// If this stream was previously active
|
// If this stream was previously active
|
||||||
if (this.$store.state.activeStreams[this._uid] == true) {
|
if (this.store.activeStreams[this.$.uid] == true) {
|
||||||
this.$store.commit("removeStream", this._uid);
|
this.store.removeStream(this.$.uid);
|
||||||
}
|
}
|
||||||
// If resized to anything other than zero
|
// If resized to anything other than zero
|
||||||
} else {
|
} else {
|
||||||
this.$store.commit("addStream", this._uid);
|
this.store.addStream(this.$.uid);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import store from "./store";
|
|
||||||
import UIkit from "uikit";
|
import UIkit from "uikit";
|
||||||
|
|
||||||
// Import MD icons
|
// Import MD icons
|
||||||
|
|
@ -21,14 +22,14 @@ UIkit.mixin(
|
||||||
|
|
||||||
// Create Vue app
|
// Create Vue app
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
const pinia = createPinia();
|
||||||
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
|
||||||
// Use visibility observer
|
// Use Pinia
|
||||||
//app.use(VueObserveVisibility);
|
app.use(pinia);
|
||||||
|
|
||||||
// Use global mixins
|
// Use global mixins
|
||||||
app.mixin(modalMixin);
|
app.mixin(modalMixin);
|
||||||
app.mixin(labThingsMixins);
|
app.mixin(labThingsMixins);
|
||||||
|
|
||||||
// Use Vuex store
|
|
||||||
app.use(store);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@
|
||||||
* This mixin is registered globally in `main.js` using. Do not
|
* This mixin is registered globally in `main.js` using. Do not
|
||||||
* manually import it in components.
|
* manually import it in components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { useWotStore } from "@/stores/wot.js";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
import { mapState, mapStores } from "pinia";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -15,32 +17,35 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(useSettingsStore, ["baseUri"]),
|
||||||
|
...mapStores(useWotStore),
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
thingDescription(thing) {
|
thingDescription(thing) {
|
||||||
return this.$store.getters["wot/thingDescription"](thing);
|
return this.wotStore.thingDescriptions[thing];
|
||||||
},
|
},
|
||||||
thingList() {
|
thingList() {
|
||||||
return this.$store.getters["wot/thingList"];
|
return this.wotStore.thingList();
|
||||||
},
|
},
|
||||||
thingAvailable(thing) {
|
thingAvailable(thing) {
|
||||||
return this.$store.getters["wot/thingAvailable"](thing);
|
return this.wotStore.thingAvailable(thing);
|
||||||
},
|
},
|
||||||
thingPropertyUrl(thing, property, allowUndefined = false) {
|
thingPropertyUrl(thing, property, allowUndefined = false) {
|
||||||
return this.$store.getters["wot/thingPropertyUrl"](
|
return this.wotStore.thingPropertyUrl(thing, property, "readproperty", allowUndefined);
|
||||||
thing,
|
},
|
||||||
property,
|
thingActionUrl(thing, action, allowUndefined = false) {
|
||||||
"readproperty",
|
return this.wotStore.thingActionUrl(thing, action, "invokeaction", allowUndefined);
|
||||||
allowUndefined,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
thingActionAvailable(thing, action) {
|
thingActionAvailable(thing, action) {
|
||||||
return this.$store.getters["wot/thingAffordanceAvailable"](thing, "actions", action);
|
return this.wotStore.thingAffordanceAvailable(thing, "actions", action);
|
||||||
},
|
},
|
||||||
thingPropertyAvailable(thing, property) {
|
thingPropertyAvailable(thing, property) {
|
||||||
return this.$store.getters["wot/thingAffordanceAvailable"](thing, "properties", property);
|
return this.wotStore.thingAffordanceAvailable(thing, "properties", property);
|
||||||
},
|
},
|
||||||
async readThingProperty(thing, property, silenceErrors = false) {
|
async readThingProperty(thing, property, silenceErrors = false) {
|
||||||
let url = this.$store.getters["wot/thingPropertyUrl"](thing, property, "readproperty", false);
|
let url = this.wotStore.thingPropertyUrl(thing, property, "readproperty", false);
|
||||||
try {
|
try {
|
||||||
let response = await axios.get(url);
|
let response = await axios.get(url);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|
@ -50,12 +55,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async writeThingProperty(thing, property, value) {
|
async writeThingProperty(thing, property, value) {
|
||||||
let url = this.$store.getters["wot/thingPropertyUrl"](
|
let url = this.wotStore.thingPropertyUrl(thing, property, "writeproperty", false);
|
||||||
thing,
|
|
||||||
property,
|
|
||||||
"writeproperty",
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
// `false` and 0 fail because axios turns it to ""
|
// `false` and 0 fail because axios turns it to ""
|
||||||
// Other values should not be stringified or pydantic
|
// Other values should not be stringified or pydantic
|
||||||
// can't parse them.
|
// can't parse them.
|
||||||
|
|
@ -88,7 +88,7 @@ export default {
|
||||||
let response;
|
let response;
|
||||||
let finalMethodCalled = false;
|
let finalMethodCalled = false;
|
||||||
try {
|
try {
|
||||||
response = await axios.get(taskUrl, { baseURL: this.$store.getters.baseUri });
|
response = await axios.get(taskUrl, { baseURL: this.baseUri });
|
||||||
const result = response.data.status;
|
const result = response.data.status;
|
||||||
|
|
||||||
if (result === "running" || result === "pending") {
|
if (result === "running" || result === "pending") {
|
||||||
|
|
@ -115,7 +115,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
terminateAction(taskUrl) {
|
terminateAction(taskUrl) {
|
||||||
axios.delete(taskUrl, { baseURL: this.$store.getters.baseUri });
|
axios.delete(taskUrl, { baseURL: this.baseUri });
|
||||||
},
|
},
|
||||||
async findOngoingActions(thing, action) {
|
async findOngoingActions(thing, action) {
|
||||||
let url = this.thingActionUrl(thing, action);
|
let url = this.thingActionUrl(thing, action);
|
||||||
|
|
@ -131,7 +131,7 @@ export default {
|
||||||
*
|
*
|
||||||
* Returns null if there is no ongoing action
|
* Returns null if there is no ongoing action
|
||||||
*/
|
*/
|
||||||
async getOngingAction(thing, action) {
|
async getOngoingAction(thing, action) {
|
||||||
let response = await this.findOngoingActions(thing, action);
|
let response = await this.findOngoingActions(thing, action);
|
||||||
// Exit if response is null, due to an error.
|
// Exit if response is null, due to an error.
|
||||||
if (response == null) return null;
|
if (response == null) return null;
|
||||||
|
|
@ -140,17 +140,9 @@ export default {
|
||||||
// ?? Is the "Nullish Coalescing-Operator" to turn undefined into null.
|
// ?? Is the "Nullish Coalescing-Operator" to turn undefined into null.
|
||||||
return response.data.find((t) => ["pending", "running"].includes(t.status)) ?? null;
|
return response.data.find((t) => ["pending", "running"].includes(t.status)) ?? null;
|
||||||
},
|
},
|
||||||
thingActionUrl(thing, action, allowUndefined = false) {
|
|
||||||
let url = this.$store.getters["wot/thingActionUrl"](
|
|
||||||
thing,
|
|
||||||
action,
|
|
||||||
"invokeaction",
|
|
||||||
allowUndefined,
|
|
||||||
);
|
|
||||||
return url;
|
|
||||||
},
|
|
||||||
async getThingEndpoint(thing, endpoint) {
|
async getThingEndpoint(thing, endpoint) {
|
||||||
let url = `${this.$store.getters.baseUri}/${thing}/${endpoint}`;
|
let url = `${this.baseUri}/${thing}/${endpoint}`;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(url);
|
const response = await axios.get(url);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import UIkit from "uikit";
|
import UIkit from "uikit";
|
||||||
import { eventBus } from "@/eventBus";
|
import { eventBus } from "@/eventBus";
|
||||||
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -55,9 +56,10 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
modalError: function (error) {
|
modalError(set_error) {
|
||||||
var errormsg = this.getErrorMessage(error);
|
const store = useSettingsStore();
|
||||||
this.$store.commit("setErrorMessage", errormsg);
|
var errormsg = this.getErrorMessage(set_error);
|
||||||
|
store.error = errormsg;
|
||||||
UIkit.notification({
|
UIkit.notification({
|
||||||
message: `${errormsg}`,
|
message: `${errormsg}`,
|
||||||
status: "danger",
|
status: "danger",
|
||||||
|
|
@ -78,30 +80,30 @@ export default {
|
||||||
return String(data);
|
return String(data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getErrorData: function (error) {
|
getErrorData: function (data_error) {
|
||||||
// If a response was obtained, extract the most specific message
|
// If a response was obtained, extract the most specific message
|
||||||
if (error.response) {
|
if (data_error.response) {
|
||||||
// If the response is a nicely formatted JSON response from the server
|
// If the response is a nicely formatted JSON response from the server
|
||||||
if (error.response.data.message) {
|
if (data_error.response.data.message) {
|
||||||
return error.response.data.message;
|
return data_error.response.data.message;
|
||||||
}
|
}
|
||||||
if (error.response.data.detail) {
|
if (data_error.response.data.detail) {
|
||||||
try {
|
try {
|
||||||
return error.response.data.detail[0].msg;
|
return data_error.response.data.detail[0].msg;
|
||||||
} catch {
|
} catch {
|
||||||
return error.response.data.detail;
|
return data_error.response.data.detail;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If the response is just some generic error response
|
// If the response is just some generic error response
|
||||||
if (error.response.data) {
|
if (data_error.response.data) {
|
||||||
return error.response.data;
|
return data_error.response.data;
|
||||||
}
|
}
|
||||||
return error.response;
|
return data_error.response;
|
||||||
}
|
}
|
||||||
// If we have an error object with a message, use that
|
// If we have an error object with a message, use that
|
||||||
if (error.message) return error.message;
|
if (data_error.message) return data_error.message;
|
||||||
// At this point just formatting the whole error object is the best we can do.
|
// At this point just formatting the whole error object is the best we can do.
|
||||||
return error;
|
return data_error;
|
||||||
},
|
},
|
||||||
|
|
||||||
showModalElement: function (element) {
|
showModalElement: function (element) {
|
||||||
|
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
import { createStore } from "vuex";
|
|
||||||
import wotStoreModule from "./wot-client";
|
|
||||||
|
|
||||||
function getOriginFromLocation() {
|
|
||||||
// This will default to the same origin that's serving
|
|
||||||
// the web app - but can be overridden by the URL.
|
|
||||||
// See also devTools.vue which can change the origin.
|
|
||||||
let url = new URL(window.location.href);
|
|
||||||
let origin = url.searchParams.get("overrideOrigin");
|
|
||||||
if (origin) {
|
|
||||||
return origin;
|
|
||||||
} else {
|
|
||||||
return url.origin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a Vuex state key (e.g. "appTheme") into a corresponding
|
|
||||||
* Vuex mutation name (e.g. "changeAppTheme") using the `change<Key>`
|
|
||||||
* convention.
|
|
||||||
*
|
|
||||||
* @param {string} key - The Vuex state key to convert.
|
|
||||||
* @returns {string} - The formatted mutation name.
|
|
||||||
*/
|
|
||||||
function keyToMutationName(key) {
|
|
||||||
return `change${key.charAt(0).toUpperCase() + key.slice(1)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a Vuex mutation name (e.g. "changeAppTheme") back into
|
|
||||||
* the corresponding state key (e.g. "appTheme") using the
|
|
||||||
* `change<Key>` convention.
|
|
||||||
*
|
|
||||||
* @param {string} mutationName - The Vuex mutation name to reverse.
|
|
||||||
* @returns {string|null} - The derived state key, or null if the
|
|
||||||
* mutation name doesn't match the `change<Key>` convention.
|
|
||||||
*/
|
|
||||||
function mutationToKey(mutationName) {
|
|
||||||
const prefix = "change";
|
|
||||||
if (!mutationName.startsWith(prefix)) {
|
|
||||||
return null; // Not a mutation we care about
|
|
||||||
}
|
|
||||||
const key = mutationName.slice(prefix.length);
|
|
||||||
return key.charAt(0).toLowerCase() + key.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOCALSTORAGE_KEYS = [
|
|
||||||
"appTheme",
|
|
||||||
"disableStream",
|
|
||||||
"overrideOrigin",
|
|
||||||
"navigationStepSize",
|
|
||||||
"navigationInvert",
|
|
||||||
];
|
|
||||||
|
|
||||||
export default createStore({
|
|
||||||
modules: {
|
|
||||||
wot: wotStoreModule,
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
origin: getOriginFromLocation(),
|
|
||||||
available: false,
|
|
||||||
waiting: false,
|
|
||||||
error: "",
|
|
||||||
trackWindow: true,
|
|
||||||
activeStreams: {},
|
|
||||||
microscopeHostname: "",
|
|
||||||
// Persistent items:
|
|
||||||
// The app theme (e.g. light/dark)
|
|
||||||
appTheme: "system",
|
|
||||||
disableStream: false,
|
|
||||||
// The origin to use if overriding with dev tools
|
|
||||||
overrideOrigin: "http://microscope.local:5000",
|
|
||||||
// The step sizes for navigation via control pane/keys presses
|
|
||||||
navigationStepSize: {
|
|
||||||
x: 200,
|
|
||||||
y: 200,
|
|
||||||
z: 50,
|
|
||||||
},
|
|
||||||
// The axis inversion for navigation via control pane/keys presses
|
|
||||||
navigationInvert: {
|
|
||||||
x: false,
|
|
||||||
y: false,
|
|
||||||
z: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mutations: {
|
|
||||||
changeOrigin(state, origin) {
|
|
||||||
state.origin = origin;
|
|
||||||
},
|
|
||||||
changeWaiting(state, waiting) {
|
|
||||||
state.waiting = waiting;
|
|
||||||
},
|
|
||||||
changeDisableStream(state, disabled) {
|
|
||||||
state.disableStream = disabled;
|
|
||||||
},
|
|
||||||
changeTrackWindow(state, enabled) {
|
|
||||||
state.trackWindow = enabled;
|
|
||||||
},
|
|
||||||
changeAppTheme(state, theme) {
|
|
||||||
state.appTheme = theme;
|
|
||||||
},
|
|
||||||
resetState(state) {
|
|
||||||
state.waiting = false;
|
|
||||||
state.available = false;
|
|
||||||
state.error = null;
|
|
||||||
},
|
|
||||||
setConnected(state) {
|
|
||||||
state.waiting = false;
|
|
||||||
state.available = true;
|
|
||||||
},
|
|
||||||
setErrorMessage(state, msg) {
|
|
||||||
state.error = msg;
|
|
||||||
},
|
|
||||||
addStream(state, id) {
|
|
||||||
state.activeStreams[id] = true;
|
|
||||||
},
|
|
||||||
removeStream(state, id) {
|
|
||||||
state.activeStreams[id] = false;
|
|
||||||
},
|
|
||||||
changeMicroscopeHostname(state, value) {
|
|
||||||
state.microscopeHostname = value;
|
|
||||||
},
|
|
||||||
changeOverrideOrigin(state, value) {
|
|
||||||
state.overrideOrigin = value;
|
|
||||||
},
|
|
||||||
changeNavigationStepSize(state, value) {
|
|
||||||
state.navigationStepSize = value;
|
|
||||||
},
|
|
||||||
changeNavigationInvert(state, value) {
|
|
||||||
state.navigationInvert = value;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {},
|
|
||||||
|
|
||||||
getters: {
|
|
||||||
baseUri: (state) => state.origin,
|
|
||||||
ready: (state) => state.available,
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
(store) => {
|
|
||||||
// Load initial state from localStorage
|
|
||||||
LOCALSTORAGE_KEYS.forEach((key) => {
|
|
||||||
const saved = localStorage.getItem(key);
|
|
||||||
if (saved !== null) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(saved);
|
|
||||||
const mutationName = keyToMutationName(key);
|
|
||||||
store.commit(mutationName, parsed);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Failed to parse localStorage key "${key}":`, e);
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Subscribe to mutations
|
|
||||||
store.subscribe((mutation, state) => {
|
|
||||||
const key = mutationToKey(mutation.type);
|
|
||||||
// If the mutation is chacning a local storage key then update localStorage
|
|
||||||
if (key && LOCALSTORAGE_KEYS.includes(key)) {
|
|
||||||
localStorage.setItem(key, JSON.stringify(state[key]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
110
webapp/src/stores/settings.js
Normal file
110
webapp/src/stores/settings.js
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
function getOriginFromLocation() {
|
||||||
|
// This will default to the same origin that's serving
|
||||||
|
// the web app - but can be overridden by the URL.
|
||||||
|
// See also devTools.vue which can change the origin.
|
||||||
|
let url = new URL(window.location.href);
|
||||||
|
let origin = url.searchParams.get("overrideOrigin");
|
||||||
|
if (origin) {
|
||||||
|
return origin;
|
||||||
|
} else {
|
||||||
|
return url.origin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define Pinia store
|
||||||
|
export const useSettingsStore = defineStore(
|
||||||
|
"settings",
|
||||||
|
() => {
|
||||||
|
// State
|
||||||
|
const origin = ref(getOriginFromLocation());
|
||||||
|
const available = ref(false);
|
||||||
|
const waiting = ref(false);
|
||||||
|
const error = ref("");
|
||||||
|
const trackWindow = ref(true);
|
||||||
|
const activeStreams = ref({});
|
||||||
|
const microscopeHostname = ref("");
|
||||||
|
|
||||||
|
// Persistent items:
|
||||||
|
// The app theme (e.g. light/dark)
|
||||||
|
const appTheme = ref("system");
|
||||||
|
const disableStream = ref(false);
|
||||||
|
// The origin to use if overriding with dev tools
|
||||||
|
const overrideOrigin = ref("http://microscope.local:5000");
|
||||||
|
// The step sizes for navigation via control pane/keys presses
|
||||||
|
const navigationStepSize = ref({
|
||||||
|
x: 200,
|
||||||
|
y: 200,
|
||||||
|
z: 50,
|
||||||
|
});
|
||||||
|
// The axis inversion for navigation via control pane/keys presses
|
||||||
|
const navigationInvert = ref({
|
||||||
|
x: false,
|
||||||
|
y: false,
|
||||||
|
z: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
function resetState() {
|
||||||
|
waiting.value = false;
|
||||||
|
available.value = false;
|
||||||
|
error.value = ""; // TODO: verify that "" is needed to match initial state
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConnected() {
|
||||||
|
waiting.value = false;
|
||||||
|
available.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStream(id) {
|
||||||
|
activeStreams.value[id] = true;
|
||||||
|
}
|
||||||
|
function removeStream(id) {
|
||||||
|
activeStreams.value[id] = false;
|
||||||
|
}
|
||||||
|
// Getters
|
||||||
|
const baseUri = computed(() => origin.value);
|
||||||
|
const ready = computed(() => available.value);
|
||||||
|
|
||||||
|
// Export
|
||||||
|
return {
|
||||||
|
//State
|
||||||
|
origin,
|
||||||
|
available,
|
||||||
|
waiting,
|
||||||
|
error,
|
||||||
|
trackWindow,
|
||||||
|
activeStreams,
|
||||||
|
microscopeHostname,
|
||||||
|
appTheme,
|
||||||
|
disableStream,
|
||||||
|
overrideOrigin,
|
||||||
|
navigationStepSize,
|
||||||
|
navigationInvert,
|
||||||
|
|
||||||
|
//Getters
|
||||||
|
baseUri,
|
||||||
|
ready,
|
||||||
|
|
||||||
|
//Actions
|
||||||
|
resetState,
|
||||||
|
setConnected,
|
||||||
|
addStream,
|
||||||
|
removeStream,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// PiniaPluginPersistedState will now automatically persist ONLY these specific refs to localStorage
|
||||||
|
persist: {
|
||||||
|
pick: [
|
||||||
|
"appTheme",
|
||||||
|
"overrideOrigin",
|
||||||
|
"disableStream",
|
||||||
|
"navigationStepSize",
|
||||||
|
"navigationInvert",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
127
webapp/src/stores/wot.js
Normal file
127
webapp/src/stores/wot.js
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const useWotStore = defineStore(
|
||||||
|
"wot",
|
||||||
|
() => {
|
||||||
|
// State
|
||||||
|
const thingDescriptions = ref({});
|
||||||
|
const servient = ref(null);
|
||||||
|
const helpers = ref(null);
|
||||||
|
// Actions
|
||||||
|
function addThingDescription(thingName, thingDescription) {
|
||||||
|
thingDescriptions.value[thingName] = thingDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeThingDescription(thingName) {
|
||||||
|
delete thingDescriptions.value[thingName];
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAllThingDescriptions() {
|
||||||
|
thingDescriptions.value = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchThingDescription(uri, name = null) {
|
||||||
|
// Fetch the thing description from the given URI and consume it
|
||||||
|
// NB this should only be called once, or we'll duplicate effort.
|
||||||
|
// Deduplication should be done elsewhere.
|
||||||
|
const response = await axios.get(uri);
|
||||||
|
const td = response.data;
|
||||||
|
const thingName = name || uri.replace(/\/$/, "").split("/").pop();
|
||||||
|
addThingDescription(thingName, td);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchThingDescriptions(uri) {
|
||||||
|
// Fetch thing descriptions from the given URI
|
||||||
|
const response = await axios.get(uri);
|
||||||
|
if (response.status !== 200) throw "Could not retrieve thing descriptions";
|
||||||
|
for (const k in response.data) {
|
||||||
|
let thingName = k.replace(/\/$/, "").replace(/^\//, "");
|
||||||
|
addThingDescription(thingName, response.data[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const thingList = computed(() => Object.keys(thingDescriptions.value));
|
||||||
|
|
||||||
|
const thingAvailable = computed(() => (thingName) => {
|
||||||
|
return thingName in thingDescriptions.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const thingAffordanceAvailable = computed(() => (thing, affordanceType, affordance) => {
|
||||||
|
const td = thingDescriptions.value[thing];
|
||||||
|
if (!td || !td[affordanceType]) return false;
|
||||||
|
return affordance in td[affordanceType];
|
||||||
|
});
|
||||||
|
|
||||||
|
const thingFormUrl = computed(
|
||||||
|
() =>
|
||||||
|
(thing, affordanceType, affordance, op, allowUndefined = true) => {
|
||||||
|
// Find the URL for a particular operation
|
||||||
|
const td = thingDescriptions.value[thing];
|
||||||
|
if (!td) {
|
||||||
|
if (allowUndefined) return undefined;
|
||||||
|
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
|
||||||
|
}
|
||||||
|
const 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);
|
||||||
|
if (href === undefined) {
|
||||||
|
if (allowUndefined) return undefined;
|
||||||
|
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
|
||||||
|
}
|
||||||
|
// If we've found an href, prepend the `base` URL if appropriate
|
||||||
|
if (href.startsWith("http")) return href;
|
||||||
|
if ("base" in td) {
|
||||||
|
let base = td.base;
|
||||||
|
if (href.startsWith("/")) href = href.slice(1);
|
||||||
|
if (!base.endsWith("/")) base += "/";
|
||||||
|
return base + href;
|
||||||
|
}
|
||||||
|
return href;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const thingPropertyUrl = computed(() => (thing, property, op, allowUndefined) => {
|
||||||
|
// Find the URL for a particular property
|
||||||
|
return thingFormUrl.value(thing, "properties", property, op, allowUndefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
const thingActionUrl = computed(() => (thing, action, op, allowUndefined) => {
|
||||||
|
// Find the URL for a particular action
|
||||||
|
return thingFormUrl.value(thing, "actions", action, op, allowUndefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
thingDescriptions,
|
||||||
|
servient,
|
||||||
|
helpers, // State
|
||||||
|
thingList,
|
||||||
|
thingAvailable, // Helpers
|
||||||
|
thingFormUrl,
|
||||||
|
thingPropertyUrl,
|
||||||
|
thingActionUrl,
|
||||||
|
fetchThingDescription,
|
||||||
|
fetchThingDescriptions, // Actions
|
||||||
|
addThingDescription,
|
||||||
|
removeThingDescription,
|
||||||
|
removeAllThingDescriptions,
|
||||||
|
thingAffordanceAvailable,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
persist: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export function findFormHref(affordance, op) {
|
||||||
|
// Find the form in the affordance that matches the given operation type
|
||||||
|
if (affordance === undefined) return undefined;
|
||||||
|
let forms = affordance.forms;
|
||||||
|
let matchingForm = forms.find((f) => f.op == op || f.op.includes(op));
|
||||||
|
if (matchingForm === undefined) return undefined;
|
||||||
|
return matchingForm.href;
|
||||||
|
}
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export const wotStoreModule = {
|
|
||||||
namespaced: true,
|
|
||||||
state: () => ({
|
|
||||||
thingDescriptions: {},
|
|
||||||
servient: null,
|
|
||||||
helpers: null,
|
|
||||||
}),
|
|
||||||
mutations: {
|
|
||||||
addThingDescription(state, { thingName, thingDescription }) {
|
|
||||||
state.thingDescriptions[thingName] = thingDescription;
|
|
||||||
},
|
|
||||||
removeThingDescription(state, thingName) {
|
|
||||||
delete state.thingDescriptions[thingName];
|
|
||||||
},
|
|
||||||
removeAllThingDescriptions(state) {
|
|
||||||
state.thingDescriptions = {};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
async start() {
|
|
||||||
// Set up thing client - not currently used.
|
|
||||||
},
|
|
||||||
async fetchThingDescription({ commit }, { uri, name = null }) {
|
|
||||||
// Fetch the thing description from the given URI and consume it
|
|
||||||
// NB this should only be called once, or we'll duplicate effort.
|
|
||||||
// Deduplication should be done elsewhere.
|
|
||||||
let response = await axios.get(uri);
|
|
||||||
let td = response.data;
|
|
||||||
let thing_name = name || uri.replace(/\/$/, "").split("/").pop();
|
|
||||||
commit("addThingDescription", {
|
|
||||||
thingName: thing_name,
|
|
||||||
thingDescription: td,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async fetchThingDescriptions({ commit }, uri) {
|
|
||||||
// Fetch thing descriptions from the given URI
|
|
||||||
let response = await axios.get(uri);
|
|
||||||
if (response.status !== 200) throw "Could not retrieve thing descriptions";
|
|
||||||
for (const k in response.data) {
|
|
||||||
let thing_name = k.replace(/\/$/, "").replace(/^\//, "");
|
|
||||||
commit("addThingDescription", {
|
|
||||||
thingName: thing_name,
|
|
||||||
thingDescription: response.data[k],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getters: {
|
|
||||||
thingDescriptions: (state) => {
|
|
||||||
return state.thingDescriptions;
|
|
||||||
},
|
|
||||||
thingList: (state) => {
|
|
||||||
return Object.keys(state.thingDescriptions);
|
|
||||||
},
|
|
||||||
thingDescription: (state) => (thingName) => {
|
|
||||||
return state.thingDescriptions[thingName];
|
|
||||||
},
|
|
||||||
thingAvailable: (state) => (thingName) => {
|
|
||||||
return thingName in state.thingDescriptions;
|
|
||||||
},
|
|
||||||
thingAffordanceAvailable: (state) => (thing, affordanceType, affordance) => {
|
|
||||||
let td = state.thingDescriptions[thing];
|
|
||||||
if (!td) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return affordance in td[affordanceType];
|
|
||||||
},
|
|
||||||
thingFormUrl:
|
|
||||||
(state) =>
|
|
||||||
(thing, affordanceType, affordance, op, allowUndefined = true) => {
|
|
||||||
// Find the URL for a particular operation
|
|
||||||
let td = state.thingDescriptions[thing];
|
|
||||||
if (!td) {
|
|
||||||
if (allowUndefined) return undefined;
|
|
||||||
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
if (href === undefined) {
|
|
||||||
if (allowUndefined) return undefined;
|
|
||||||
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
|
|
||||||
}
|
|
||||||
// If we've found an href, prepend the `base` URL if appropriate
|
|
||||||
if (href.startsWith("http")) return href;
|
|
||||||
if ("base" in td) {
|
|
||||||
let base = td.base;
|
|
||||||
if (href.startsWith("/")) href = href.slice(1);
|
|
||||||
if (!base.endsWith("/")) base += "/";
|
|
||||||
return base + href;
|
|
||||||
}
|
|
||||||
return href;
|
|
||||||
},
|
|
||||||
thingPropertyUrl: (_state, getters) => (thing, property, op, allowUndefined) => {
|
|
||||||
// Find the URL for a particular property
|
|
||||||
return getters.thingFormUrl(thing, "properties", property, op, allowUndefined);
|
|
||||||
},
|
|
||||||
thingActionUrl: (_state, getters) => (thing, action, op, allowUndefined) => {
|
|
||||||
// Find the URL for a particular action
|
|
||||||
return getters.thingFormUrl(thing, "actions", action, op, allowUndefined);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function findFormHref(affordance, op) {
|
|
||||||
// Find the form in the affordance that matches the given operation type
|
|
||||||
if (affordance === undefined) return undefined;
|
|
||||||
let forms = affordance.forms;
|
|
||||||
let matchingForm = forms.find((f) => f.op == op || f.op.includes(op));
|
|
||||||
if (matchingForm === undefined) return undefined;
|
|
||||||
return matchingForm.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default wotStoreModule;
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue