openflexure-microscope-server/webapp/src/App.vue
Richard Bowman cbd8b090ec Replace tasksubmitter with action-button
I've changed tasksubmitter to accept a "thing" and "action" rather than a single URL. This seems more in keeping with
propertyControl, and eliminates the need for loads of
computed properties.

I've also deleted (rather than just commenting out) chunks of the web app that are no longer in use. These may be
reinstated in the future - but we can get them from git
history. It's less confusing not to have vestigial
code in the repo.

This builds OK but is not, as yet, tested with hardware.
2024-02-29 21:44:39 +00:00

497 lines
13 KiB
Vue

<template>
<div
id="app"
class="uk-height-1-1 uk-margin-remove uk-padding-remove"
:class="handleTheme"
>
<loadingContent v-if="!$store.getters.ready" />
<div v-if="$store.getters.ready" id="tour-header"></div>
<appContent v-if="$store.getters.ready" />
<!-- Runtime modals -->
<div
id="modal-center"
ref="keyboardManualModal"
class="uk-flex-top"
uk-modal
>
<div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical">
<button class="uk-modal-close-default" type="button" uk-close></button>
<div
v-for="shortcut in keyboardManual"
:key="shortcut.shortcut"
class="uk-margin-small"
uk-grid
>
<div class="uk-width-small">{{ shortcut.shortcut }}</div>
<div class="uk-width-expand">{{ shortcut.description }}</div>
</div>
</div>
</div>
<v-tour
v-show="$store.getters.ready"
name="guidedTour"
:steps="tourSteps"
:callbacks="tourCallbacks"
:options="{ highlight: true }"
></v-tour>
</div>
</template>
<script>
// Import components
import appContent from "./components/appContent.vue";
import loadingContent from "./components/loadingContent.vue";
var Mousetrap = require("mousetrap");
Mousetrap.prototype.stopCallback = function(e, element) {
// if the element has the class "mousetrap" then no need to stop
if ((" " + element.className + " ").indexOf(" mousetrap ") > -1) {
return false;
}
// if we're in a lightbox, stop mousetrap
if (element.classList.contains("lightbox-link")) {
return true;
}
// stop for input, select, and textarea
return (
element.tagName == "INPUT" ||
element.tagName == "SELECT" ||
element.tagName == "TEXTAREA" ||
(element.contentEditable && element.contentEditable == "true")
);
};
// Export main app
export default {
name: "App",
components: {
appContent,
loadingContent
},
data: function() {
return {
appAvailable: false,
arrowKeysDown: {},
keyboardManual: [],
systemDark: undefined,
themeObserver: undefined,
tourCallbacks: {
onStop: () => {
this.setLocalStorageObj("completedTour", true);
}
}
};
},
computed: {
isSystemDark: function() {
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
return true;
} else {
return false;
}
},
handleTheme: function() {
var isDark = false;
if (this.$store.state.appTheme == "dark") {
isDark = true;
} else if (this.$store.state.appTheme == "system") {
if (this.systemDark) {
isDark = true;
}
}
return {
"uk-light": isDark,
"uk-background-secondary": isDark
};
},
tourSteps: function() {
let tabPopperParams = {
modifiers: {
preventOverflow: {
boundariesElement: "window"
}
},
placement: "right"
};
return [
{
target: "#tour-header", // We're using document.querySelector() under the hood
header: {
title: "Welcome to the OpenFlexure Microscope"
},
content: `Click Next to learn how to use your microscope`,
params: {
placement: "bottom"
}
},
{
target: "#gallery-tab-icon",
header: {
title: "Capture gallery"
},
content: `View and download your microscope images from the gallery tab`,
params: tabPopperParams
},
{
target: "#navigate-tab-icon",
header: {
title: "Navigate around your sample"
},
content: `Move your microscope stage and perform autofocus from the navigate tab`,
params: tabPopperParams
},
{
target: "#capture-tab-icon",
header: {
title: "Capture microscope images"
},
content: `Take images and simple tile scans from the capture tab`,
params: tabPopperParams
},
{
target: "#settings-tab-icon",
header: {
title: "Change settings"
},
content: `Change app and microscope settings, including microscope calibration, from the settings tab`,
params: tabPopperParams
},
{
target: "#extension-tab-divider",
header: {
title: "Microscope extensions"
},
content: `Extensions installed on your microscope will appear below this line`,
params: tabPopperParams
}
];
}
},
mounted() {
// Query CSS dark theme preference
var mql = window.matchMedia("(prefers-color-scheme: dark)");
// Check for system dark theme when mounted
if (mql.matches) {
this.systemDark = true;
}
// Create a theme observer to watch for changes
this.themeObserver = mql.addListener(e => {
if (e.matches) {
this.systemDark = true;
} else {
this.systemDark = false;
}
});
// Check connection to API
this.checkConnection();
// Handle guided tour
// If the user has already completed or skipped the guided tour
var completedTour = this.getLocalStorageObj("completedTour") || false;
if (!completedTour) {
this.$tours["guidedTour"].start();
}
},
created: function() {
window.addEventListener("beforeunload", this.handleExit);
// Scrollwheel listener
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
});
// Arrow keys
Mousetrap.bind(
["up", "down", "left", "right"],
event => {
this.arrowKeysDown[event.keyCode] = true; //Add key to array
this.navigateKeyHandler();
},
"keydown"
);
Mousetrap.bind(
["up", "down", "left", "right"],
event => {
delete this.arrowKeysDown[event.keyCode]; //Remove key from array
},
"keyup"
);
this.keyboardManual.push({
shortcut: "←↑→↓",
description: "Move the microscope stage"
});
// Focus keys
Mousetrap.bind("pageup", () => {
this.$root.$emit("globalMoveStepEvent", 0, 0, 1);
});
Mousetrap.bind("pagedown", () => {
this.$root.$emit("globalMoveStepEvent", 0, 0, -1);
});
this.keyboardManual.push({
shortcut: "pgup / pgdn",
description: "Move the microscope focus"
});
// Capture
Mousetrap.bind("c", () => {
this.$root.$emit("globalCaptureEvent");
});
this.keyboardManual.push({
shortcut: "c",
description: "Take a capture"
});
// Autofocus
Mousetrap.bind("a", () => {
this.$root.$emit("globalFastAutofocusEvent");
});
this.keyboardManual.push({
shortcut: "a",
description: "Fast autofocus"
});
// Increment/decrement tab
Mousetrap.bind("shift+down", () => {
this.$root.$emit("globalIncrementTab");
});
Mousetrap.bind("shift+up", () => {
this.$root.$emit("globalDecrementTab");
});
this.keyboardManual.push({
shortcut: "shift+↑ / shift+↓",
description: "Switch tab"
});
// Re-run tour
Mousetrap.bind("alt+t", () => {
this.$tours["guidedTour"].start();
});
},
beforeDestroy: function() {
// Disconnect the theme observer
if (this.themeObserver) {
this.themeObserver.disconnect();
}
// Remove scrollwheel listener
window.removeEventListener("wheel", this.wheelMonitor);
// Remove origin watcher
this.unwatchOriginFunction();
// Remove key listeners
Mousetrap.reset();
},
methods: {
async checkConnection() {
var baseUri = this.$store.getters.baseUri;
this.$store.commit("changeWaiting", true);
// TODO: more robust check - e.g. use a microscope Thing
// TODO: should we purge existing consumedThings?
try {
await this.$store.dispatch(
"wot/fetchThingDescriptions",
`${baseUri}/thing_descriptions/`
);
for (let requiredThing of ["camera", "stage"]) {
if (!this.$store.getters["wot/thingAvailable"](requiredThing)) {
throw new Error(
`No ${requiredThing} found, the GUI won't work without one.`
);
}
}
try {
let hostname = await this.readThingProperty("settings", "hostname");
this.$store.commit("changeMicroscopeHostname", hostname);
document.title = `OpenFlexure Microscope: ${hostname}`;
} catch {
this.$store.commit("changeMicroscopeHostname", null);
}
this.$store.commit("setConnected");
this.$store.commit("setErrorMessage", null);
} catch (error) {
this.$store.commit("setErrorMessage", error);
} finally {
this.$store.commit("changeWaiting", false);
}
},
handleExit: function() {
this.$root.$emit("globalTogglePreview", false);
},
// Handle global mouse wheel events to be associated with navigation
wheelMonitor: function(event) {
// Only capture scroll if the event target's parent contains the "scrollTarget" class
if (
event.target.parentNode.classList.contains("scrollTarget") ||
event.target.classList.contains("scrollTarget")
) {
var z_rel = event.deltaY / 100;
// Emit a signal to move, acted on by panelNavigate.vue
this.$root.$emit("globalMoveStepEvent", 0, 0, z_rel, false);
}
},
navigateKeyHandler: function() {
// Calculate movement array
var x_rel = 0;
var y_rel = 0;
var z_rel = 0;
if (37 in this.arrowKeysDown) {
x_rel = x_rel + 1;
}
if (39 in this.arrowKeysDown) {
x_rel = x_rel - 1;
}
if (38 in this.arrowKeysDown) {
y_rel = y_rel + 1;
}
if (40 in this.arrowKeysDown) {
y_rel = y_rel - 1;
}
// Make a position request
// Emit a signal to move, acted on by panelNavigate.vue
this.$root.$emit("globalMoveStepEvent", x_rel, y_rel, z_rel);
}
}
};
</script>
<style lang="less">
// Basic UIkit CSS
@import "../node_modules/uikit/src/less/uikit.less";
// Custom UIkit CSS modifications
@import "./assets/less/theme.less";
// We override the custom-electron-titlebar z-index
// UIKit lightbox must be able to draw over the titlebar
// as it currently always spawns at the root of the DOM
.titlebar,
.titlebar > * {
z-index: 1000 !important;
}
#app {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: left;
height: 100%;
}
body,
html {
height: 100%;
overflow: hidden;
}
.uk-disabled {
pointer-events: none;
opacity: 0.4;
}
.control-component {
overflow-y: auto;
overflow-x: hidden;
width: 300px;
height: 100%;
padding: 0;
background-color: rgba(180, 180, 180, 0.03);
border-width: 0 1px 0 0;
border-style: solid;
border-color: rgba(180, 180, 180, 0.25);
}
.view-component {
overflow-y: auto;
overflow-x: hidden;
height: 100%;
padding: 0;
}
.image-fit {
height: 80%;
width: 100%;
object-fit: contain;
overflow-y: clip;
}
.section-content {
padding: 0;
height: 100%;
}
// Style tour
.v-tour__target--highlighted {
box-shadow: 0px 40px 200px 30px rgba(0, 0, 0, 0.5),
0px 0px 0px 4px rgba(128, 128, 128, 0.5) !important;
border-radius: 5px;
opacity: 100% !important;
pointer-events: none !important;
}
.v-step {
background: @global-primary-background !important;
}
.v-step__header {
background-color: darken(@global-primary-background, 7%) !important;
}
.v-step__button {
font-size: 0.9rem !important;
}
// Change step arrow colour
// This is awful and hacky and makes me sad, but needs must
.v-step .v-step__arrow {
border-color: darken(@global-primary-background, 7%) !important;
&--dark {
border-color: darken(@global-primary-background, 7%) !important;
}
}
.v-step[x-placement^="top"] .v-step__arrow {
border-left-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
}
.v-step[x-placement^="bottom"] .v-step__arrow {
border-left-color: transparent !important;
border-right-color: transparent !important;
border-top-color: transparent !important;
}
.v-step[x-placement^="right"] .v-step__arrow {
border-left-color: transparent !important;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
}
.v-step[x-placement^="left"] .v-step__arrow {
border-top-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
}
</style>