Merge branch 'ui-plugin-system-testing' into 'master'

GUI plugin system

See merge request openflexure/openflexure-microscope-jsclient!33
This commit is contained in:
Joel Collins 2019-06-21 12:12:27 +00:00
commit 3f458f220c
32 changed files with 1128 additions and 564 deletions

3
.env.development.web Normal file
View file

@ -0,0 +1,3 @@
NODE_ENV=development
VUE_APP_PLATFORM=web
VUE_APP_TARGET=web

48
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "openflexure-ev",
"version": "1.1.0-beta.1",
"version": "1.1.0-beta.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -5187,7 +5187,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -5208,12 +5209,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -5228,17 +5231,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -5355,7 +5361,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -5367,6 +5374,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -5381,6 +5389,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -5388,12 +5397,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -5412,6 +5423,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -5492,7 +5504,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -5504,6 +5517,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -5589,7 +5603,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -5625,6 +5640,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -5644,6 +5660,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -5687,12 +5704,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},
@ -7288,6 +7307,11 @@
"object-visit": "^1.0.0"
}
},
"material-design-icons": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz",
"integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78="
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "openflexure-ev",
"version": "1.1.0-beta.2",
"version": "1.1.0-beta.4",
"private": true,
"description": "An electron-based user client for the OpenFlexure Microscope Server",
"author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)",
@ -13,7 +13,7 @@
"scripts": {
"build:web": "vue-cli-service build --mode production.web",
"build:app": "vue-cli-service build --mode production.app",
"serve": "vue-cli-service serve",
"serve": "vue-cli-service serve --mode development.web",
"start": "npm run build:app && electron app/app.js",
"dist:linux": "electron-builder --linux --config app/builder-config-x86_64.yaml",
"dist:raspi": "electron-builder --linux --config app/builder-config-raspi.yaml",
@ -27,7 +27,8 @@
"custom-electron-titlebar": "^3.0.8",
"electron-context-menu": "^0.11.0",
"electron-store": "^3.2.0",
"electron-updater": "^4.0.6"
"electron-updater": "^4.0.6",
"material-design-icons": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.3.0",
@ -38,7 +39,7 @@
"electron-builder": "^20.39.0",
"less": "^3.9.0",
"less-loader": "^4.1.0",
"uikit": "3.1.1",
"uikit": "^3.1.1",
"vue": "^2.5.21",
"vue-template-compiler": "^2.6.10",
"vuex": "^3.0.1"

View file

@ -1,82 +1,41 @@
<template>
<div id="app" v-bind:class="handleTheme">
<!-- Grid managing whole app -->
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove" margin=0>
<div id="sidebar-container" v-bind:class="{ 'overlay-panel': this.window.width<850 }" class="uk-padding-remove uk-first-column uk-inline uk-height-1-1">
<div id="overlay-toggle">
<a href="" class="uk-icon-button uk-box-shadow-small uk-box-shadow-hover-medium action-btn-outline" uk-icon="menu" uk-toggle="target: #left-panel-container; animation: uk-animation-slide-left-small, uk-animation-slide-left-small" ></a>
</div>
<div id="left-panel-container" class="uk-padding-remove uk-card uk-card-default uk-width-auto uk-height-1-1" v-bind:class="{ 'uk-card-secondary': $store.state.globalSettings.darkMode }">
<panelLeft/>
</div>
</div>
<div id="main-panel-container" class="uk-padding-remove uk-height-1-1 uk-width-expand">
<panelDisplay/>
</div>
<panelLeft/>
<panelRight/>
</div>
</div>
</template>
<script>
// Import axios for HTTP requests
import axios from 'axios'
// Import basic UIkit
import UIkit from 'uikit';
// Import components
import panelLeft from './components/panelLeft.vue'
import panelDisplay from './components/panelDisplay.vue'
import panelRight from './components/panelRight.vue'
// Export main app
export default {
name: 'app',
components: {
panelLeft,
panelDisplay
panelRight,
panelLeft
},
data: function () {
return {
window: {
width: 0,
height: 0
}
}
return {}
},
created: function () {
var context = this
function handleSidebarEvent(context, event){
if (event.target.id == 'left-panel-container') {
console.log("Sidebar hidden")
context.$root.$emit('globalResizePreview')
}
}
UIkit.util.on(document, 'hidden', '#left-panel-container', function (e) {
handleSidebarEvent(context, e)
})
UIkit.util.on(document, 'shown', '#left-panel-container', function (e) {
handleSidebarEvent(context, e)
})
window.addEventListener('resize', this.handleResize)
this.handleResize();
window.addEventListener('beforeunload', this.handleExit)
},
beforeDestroy: function () {
window.removeEventListener('resize', this.handleResize)
},
methods: {
handleResize: function(event) {
this.window.width = window.innerWidth;
this.window.height = window.innerHeight;
},
handleExit: function(event) {
console.log("Triggered beforeunload")
this.$root.$emit('globalTogglePreview', false)
@ -114,38 +73,9 @@ body, html {
overflow-x: hidden;
}
.overlay-panel {
position: fixed;
z-index: 99;
}
#overlay-toggle {
width: 0px;
height: 30px;
z-index: 999;
position: absolute;
right: -20px;
top: 24px;
}
.action-btn-outline {
border: 1px solid lightgray;
}
.uk-light .uk-icon-button {
background-color: rgb(52, 52, 52);
}
.uk-light .uk-icon-button:hover, .uk-light .uk-icon-button:focus {
background-color: rgb(70, 70, 70);
}
.uk-light .uk-card-default {
background: #222
}
.uk-disabled {
pointer-events: none;
opacity: 0.5;
pointer-events: none;
opacity: 0.4;
}
</style>

View file

@ -10,6 +10,8 @@
// Custom OpenFlexure theming
@global-primary-background: #C32280;
@inverse-primary-muted-color: lighten(@global-primary-background, 15%);
// UIkit
// ========================================================================
@ -123,6 +125,10 @@
@inverse-global-color: fade(@global-inverse-color, 80%);
@inverse-global-muted-color: fade(@global-inverse-color, 60%);
//
// Paper
//
@paper-border-radius: 4px;
/* ========================================================================
@ -137,103 +143,14 @@
@sidebar-right-left-xl: 60px;
/* HTML
========================================================================== */
/*
* Force vertical scrollbar
* 1. `overflow-x` has to be on the `body` element for Safari to prevent horizontal scrolling on touch
This causes a random bug showing a vertical scrollbar in combination with some fonts like `Poppins`
This is why `overflow-y` must also be set to `hidden`
*/
html { overflow-y: scroll; }
/* 1 */
body { overflow: hidden; }
/* Documentation Sidebars
========================================================================== */
/*
* Sidebar Left
*/
.tm-sidebar-left {
position: fixed;
top: @navbar-nav-item-height;
bottom: 0;
box-sizing: border-box;
width: @sidebar-left-width !important;
padding: 40px 40px 60px 40px;
border-right: 1px @global-border solid;
overflow: auto;
}
/*
* Sidebar Right
*/
.tm-sidebar-right {
position: absolute;
top: 0;
left: ~'calc(100% + @{sidebar-right-left})';
width: @sidebar-right-width;
}
/* Tablet landscape and bigger */
@media (min-width: @breakpoint-medium) {
.tm-sidebar-left + .tm-main { padding-left: @sidebar-left-width; }
}
/* Desktop and bigger */
@media (min-width: @breakpoint-large) {
.tm-sidebar-right { left: ~'calc(100% + @{sidebar-right-left})'; }
.tm-sidebar-left + .tm-main { padding-left: @sidebar-left-width - (@sidebar-right-width + @sidebar-right-left); }
}
/* Large screen and bigger */
@media (min-width: 1400px) {
.tm-sidebar-left {
width: @sidebar-left-width-xl !important;
padding: 45px 45px 60px 45px;
}
.tm-sidebar-right { left: ~'calc(100% + @{sidebar-right-left-xl})'; }
.tm-sidebar-left + .tm-main { padding-left: @sidebar-left-width-xl - (@sidebar-right-width + @sidebar-right-left-xl); }
}
/* UIkit modifier
/* UIkit modifiers
========================================================================== */
/*
* Navbar
*/
.tm-navbar-container:not(.uk-navbar-transparent) {
background: linear-gradient(to left, #28a5f5, @global-primary-background);
}
.tm-navbar-container .uk-navbar-nav > li > a,
.tm-navbar-container .uk-navbar-item:not(.uk-logo),
.tm-navbar-container .uk-navbar-toggle {
height: 70px;
font-size: 13px;
}
// Color Mode
.tm-navbar-container:not(.uk-navbar-transparent):extend(.uk-light all) {}
// Line Mode
@navbar-nav-item-line-margin-vertical: 20px;
@navbar-nav-item-line-margin-horizontal: @navbar-nav-item-padding-horizontal;
@ -241,271 +158,115 @@ body { overflow: hidden; }
@navbar-nav-item-line-background: currentColor;
@navbar-nav-item-line-transition-duration: 0.3s;
.tm-navbar-container .uk-navbar-nav > li > a {
position: relative;
&::before {
content: '';
display: block;
position: absolute;
left: @navbar-nav-item-line-margin-horizontal;
right: ~'calc(102% - @{navbar-nav-item-line-margin-horizontal})';
bottom: @navbar-nav-item-line-margin-vertical;
height: @navbar-nav-item-line-height;
background-color: @navbar-nav-item-line-background;
transition: @navbar-nav-item-line-transition-duration ease-in-out;
transition-property: right;
}
}
.tm-navbar-container .uk-navbar-nav > li > a:hover::before { right: @navbar-nav-item-line-margin-horizontal; }
/*
* Nav
*/
.tm-nav > li.uk-active > a { position: relative; }
.tm-nav > li.uk-active > a:before {
content: "";
position: absolute;
top: 15px;
left: -25px;
width: 15px;
border-top: 1px solid @global-primary-background;
}
.tm-nav .uk-nav-header {
padding: 8px 0;
border-bottom: 1px solid @global-border;
.uk-navbar-dropdown.uk-open {
border-radius: @paper-border-radius;
}
/*
* Subnav
* Cards
*/
/* Tablet landscape and bigger */
@media (min-width: @breakpoint-medium) {
.tm-subnav { margin-left: -50px; }
.tm-subnav > * { padding-left: 50px; }
.uk-card {
border-radius: @paper-border-radius;
}
.tm-subnav > * > :first-child { text-transform: capitalize; }
/*
* Label (Changelog()
*/
.tm-label-changelog {
width: 80px;
margin-top: 3px;
.uk-card-media-top img {
border-radius: @paper-border-radius @paper-border-radius 0 0;
}
/*
* Button
*/
.uk-button {
border-radius: 2px;
}
.uk-card-default .uk-card-footer {
border-top: 1px solid rgba(180, 180, 180, 0.25);
}
.tm-button-default,
.tm-button-primary { border-radius: 500px; }
.tm-button-default {}
.tm-button-large { line-height: 48px; }
.tm-button-primary {}
.uk-card-primary .uk-card-footer {
border-top: 1px solid rgba(0, 0, 0, 0.075);
}
.hook-inverse() {
.tm-button-default {
color: @inverse-global-color;
border-color: @inverse-global-muted-color;
// Override background colour in dark mode
.uk-card-default {
background-color: #2a2a2a !important;
}
.tm-button-primary {
background: #fff !important;
color: @global-primary-background !important;
// Lighten on hover to show depth in dark mode
.uk-card-default:hover {
transition: background-color @animation-fast-duration ease;
background-color: #333 !important;
}
.tm-button-primary:hover { box-shadow: 0 10px 40px rgba(30,135,240,1); }
}
/*
* Section
*/
.tm-section-primary {
background: linear-gradient(to left top, #28a5f5, @global-primary-background) 0 0 no-repeat;
}
.tm-section-intro {
background: linear-gradient(to left top, #28a5f5, @global-primary-background) 0 0 no-repeat,
#fff;
background-size: 100% 75%;
}
/*
* Heading
*/
.tm-h4,
.tm-h5,
.tm-h6 {
font-family: Montserrat;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
color: @global-muted-color;
}
.tm-h4 { font-size: 14px; }
.tm-h5 { font-size: 12px; }
.tm-h6 { font-size: 11px; }
.hook-inverse() {
.tm-h4,
.tm-h5,
.tm-h6 { color: rgba(255, 255, 255, 0.7); }
}
/*
* Text
*/
.tm-text-large { font-size: 18px; }
/*
* Box shadow
*/
.tm-box-shadow-medium {
box-shadow: 2px 12px 35px rgba(0,0,0,0.1),
0 1px 6px rgba(0,0,0,0.05);
}
.tm-box-shadow-large {
box-shadow: -40px 40px 160px 0 rgba(0,0,0,0.08),
-8px 8px 15px 0 rgba(120,120,120,0.04),
3px 3px 30px 0 rgba(0,0,0,0.04);
}
.tm-box-shadow-modal { box-shadow: 3px 3px 40px 0 rgba(0,0,0,0.06); }
/*
* Overlay
*/
.tm-overlay-default { background: fade(@global-background, 50%); }
/*
* Modal
*/
.tm-modal-dialog { background: @global-muted-background; }
.tm-modal-header { padding: 20px 30px; }
.tm-modal-body { padding: 0; }
/* Phone landscape and bigger */
@media (min-width: @breakpoint-small) {
.tm-modal-header { padding: 25px 50px; }
.tm-modal-body { padding: 0 50px; }
.uk-modal-dialoge {
border-radius: @paper-border-radius;
box-shadow: 0 14px 25px rgba(0, 0, 0, 0.16);
transition: 0.3s ease !important;
}
/* Desktop and bigger */
@media (min-width: @breakpoint-xlarge) {
.tm-modal-header { padding: 50px 120px; }
.tm-modal-body { padding: 0 120px; }
}
.tm-modal-close-full {
background: transparent;
padding: 30px;
}
/* Phone landscape and bigger */
@media (min-width: @breakpoint-small) {
.tm-modal-close-full { padding: @global-medium-margin; }
}
/* Utilities
========================================================================== */
/*
* Heading fragment
* Notifications
*/
.tm-heading-fragment > a {
color: inherit;
text-decoration: none;
.uk-notification-message {
border-radius: @paper-border-radius;
box-shadow: 0 14px 25px rgba(0, 0, 0, 0.16);
}
/* Phone landscape and bigger */
@media (min-width: @breakpoint-small) {
.uk-notification-message-danger {
border: 1px solid @notification-message-danger-color
}
h2.tm-heading-fragment > a:before {
content: "#";
position: absolute;
width: 25px;
margin-left: -25px;
color: @global-muted-color;
.uk-notification-message-warning {
border: 1px solid @notification-message-warning-color
}
.uk-notification-message-success {
border: 1px solid @notification-message-success-color
}
/*
* Links
*/
.uk-link, .uk-button-link {
color: @global-muted-color;
}
.uk-link:hover, .uk-button-link:hover, a:hover {
color: @global-primary-background;
}
.hook-inverse() {
.uk-link:hover, .uk-button-link:hover, a:hover {
color: @inverse-primary-muted-color;
}
}
/*
* Buttons
*/
.uk-button {
border-radius: 2px;
}
.uk-button-danger {
background-color: transparent;
color: #f0506e;
border: 1px solid #f0506e;
}
.hook-inverse() {
.uk-button-primary{
background-color: rgba(180, 180, 180, 0.15);
color: @inverse-primary-muted-color;
border-color: @inverse-primary-muted-color;
}
}
/* Reset container for docs
========================================================================== */
.tm-main .uk-container {
padding-left: @container-padding-horizontal;
padding-right: @container-padding-horizontal;
}
/* Phone landscape and bigger */
@media (min-width: @breakpoint-small) {
.tm-main .uk-container {
padding-left: @container-padding-horizontal-s;
padding-right: @container-padding-horizontal-s;
.uk-button-primary:hover {
background-color: @inverse-primary-muted-color;
color: #fff;
}
}
/* Tablet landscape and bigger */
@media (min-width: @breakpoint-medium) {
.tm-main .uk-container {
padding-left: @container-padding-horizontal-m;
padding-right: @container-padding-horizontal-m;
}
}
/* Pro
========================================================================== */
/* Desktop and bigger */
@media (min-width: @breakpoint-large) {
.tm-intro-text { margin-top: 30px; }
}
/* Large screen and bigger */
@media (min-width: @breakpoint-xlarge) {
.tm-intro-text { margin-top: 90px; }
.tm-intro-image { margin-right: -75px; }
}
.uk-icon-button, .uk-icon-button:hover {
text-decoration: none
}

View file

@ -43,18 +43,20 @@
<div class="uk-accordion-content">
<form @submit.prevent="handleMetadataSubmit">
<div class="uk-margin-small uk-grid-small" uk-grid>
<div class="uk-margin-remove-top uk-width-2-5"><input v-model="newMetadata.key" class="uk-input uk-form-width-small uk-form-small" type="text" name="flavor" placeholder="Key"></div>
<div class="uk-margin-remove-top uk-width-2-5"><input v-model="newMetadata.value" class="uk-input uk-form-width-small uk-form-small" type="text" name="flavor" placeholder="Value"></div>
<div class="uk-margin-remove-top uk-width-1-5"><button class="uk-button uk-button-default uk-form-small uk-float-right" uk-icon="plus"></button></div>
<div class="uk-margin-remove uk-flex uk-flex-middle">
<div class="uk-margin-remove-top uk-padding-remove uk-grid-small uk-width-expand" uk-grid>
<div class="uk-margin-remove uk-width-1-2"><input v-model="newMetadata.key" class="uk-input uk-form-width-small uk-form-small" type="text" name="flavor" placeholder="Key"></div>
<div class="uk-margin-remove uk-width-1-2"><input v-model="newMetadata.value" class="uk-input uk-form-width-small uk-form-small" type="text" name="flavor" placeholder="Value"></div>
</div>
<a href="#" v-on:click="handleMetadataSubmit()" class="uk-icon uk-margin-left"><i class="material-icons">add_circle</i></a>
</div>
</form>
<div v-for="(value, key) in customMetadata" :key="key" class="uk-width-1-1 uk-margin-small uk-margin-remove-left uk-margin-remove-right" uk-grid>
<div v-for="(value, key) in customMetadata" :key="key" class="uk-width-1-1 uk-margin-small uk-margin-remove-left uk-margin-remove-right uk-flex uk-flex-middle">
<div class="uk-margin-remove-top uk-padding-remove uk-width-expand"><b>{{ key }}: </b>{{ value }}</div>
<div class="uk-margin-remove-top uk-padding-remove uk-width-auto">
<a href="#" v-on:click="delMetadataKey(key)" class="uk-icon-link" uk-icon="trash"></a>
</div>
<a href="#" v-on:click="delMetadataKey(key)" class="uk-icon uk-width-auto"><i class="material-icons">delete</i></a>
</div>
</div>
@ -65,9 +67,16 @@
<div class="uk-accordion-content">
<form @submit.prevent="handleTagSubmit">
<div class="uk-margin-small uk-grid-small" uk-grid>
<div class="uk-margin-remove-top uk-width-4-5"><input v-model="newTag" class="uk-input uk-form-width-small uk-form-small" type="text" name="flavor" placeholder="Tag"></div>
<div class="uk-margin-remove-top uk-width-1-5"><button class="uk-button uk-button-default uk-form-small uk-float-right" uk-icon="plus"></button></div>
<div class="uk-margin-small uk-flex uk-flex-middle">
<div class="uk-margin-remove-top uk-width-expand">
<div class="uk-inline uk-width-1-1">
<span class="uk-form-icon"><i class="material-icons">label</i></span>
<input v-model="newTag" class="uk-input uk-form-small" type="text" name="flavor" placeholder="Tag">
</div>
</div>
<a href="#" v-on:click="handleTagSubmit()" class="uk-icon uk-margin-left"><i class="material-icons">add_circle</i></a>
</div>
</form>

View file

@ -2,8 +2,6 @@
<div class="host-input">
<div class="uk-margin">
<h3>Connect</h3>
<form @submit.prevent="handleSubmit">
<div class="uk-form-controls uk-margin">
<label><input class="uk-radio" type="radio" name="radio_local" v-model="computedLocalMode" v-bind:value="true"> Connect locally</label><br>
@ -12,7 +10,7 @@
<div v-if="!localMode">
<div class="uk-inline uk-width-1-1">
<span class="uk-form-icon" uk-icon="icon: server"></span>
<span class="uk-form-icon"><i class="material-icons">dns</i></span>
<input v-model="hostname" v-bind:class="IpFormClasses" class="uk-input uk-form-small" type="text" placeholder="Hostname or IP address">
</div>
</div>
@ -23,10 +21,11 @@
<div class="uk-accordion-content">
<label class="uk-form-label" for="form-stacked-text">Port</label>
<div class="uk-form-controls">
<input v-model="computedPort" class="uk-input uk-form-width-medium uk-form-small" id="form-stacked-text" type="number" value=5000>
<input v-model="computedPort" class="uk-input uk-form-small" id="form-stacked-text" type="number" value=5000>
</div>
</div>
</li>
<li>
<a class="uk-accordion-title" href="#">Status</a>
<div class="uk-accordion-content">
@ -35,23 +34,23 @@
<p><b>Base URI:</b> {{ $store.getters.uri }}</p>
<p v-if="$store.state.apiConfig.name"><b>Device name:</b> {{ $store.state.apiConfig.name }}</p>
</div>
<div v-else-if="$store.state.waiting"><div uk-spinner></div></div>
<div v-else-if="$store.state.error"><b>Error:</b> {{ $store.state.error }}</div>
<div v-else>No active connection</div>
</div>
</li>
<li v-if="!localMode" class="uk-open">
<a class="uk-accordion-title" href="#">Saved hosts</a>
<div class="uk-accordion-content">
<button v-if="$store.getters.ready" v-on:click="saveHost()" class="uk-button uk-button-default uk-form-small uk-margin-small uk-width-1-1">Save Current</button>
<div v-for="host in savedHosts" :key="host.name" class="uk-margin-small uk-margin-remove-left uk-margin-remove-right uk-grid">
<a href="#" v-on:click="autofillHost(host)" class="uk-icon-link uk-padding-remove uk-width-expand"><b>{{ host.name }}</b> ({{ host.hostname }}:{{ host.port }})</a>
<a href="#" v-on:click="delSavedHost(host)" class="uk-icon-link uk-width-auto" uk-icon="trash"></a>
<div v-for="host in savedHosts" :key="host.name" class="uk-margin-small uk-margin-remove-left uk-margin-remove-right uk-flex uk-flex-middle">
<a href="#" v-on:click="autofillHost(host)" class="uk-link uk-padding-remove uk-width-expand host-description"><b>{{ host.name }}</b> ({{ host.hostname }}:{{ host.port }})</a>
<a href="#" v-on:click="delSavedHost(host)" class="uk-icon uk-width-auto host-delete"><i class="material-icons">delete</i></a>
</div>
</div>
</li>
</ul>
<button class="uk-button uk-button-primary uk-form-small uk-float-right uk-margin uk-margin-remove-top uk-width-1-1">Connect</button>
@ -161,7 +160,7 @@ export default {
this.$store.commit("changeSetting", ['trackWindow', state]);
this.$store.commit("changeSetting", ['disableStream', state]);
this.$store.commit("changeSetting", ['autoGpuPreview', state]);
this.$root.$emit('globalTogglePreview', state)
//this.$root.$emit('globalTogglePreview', state)
},
autofillHost: function (host) {
@ -246,4 +245,14 @@ export default {
.host-input {
text-align: left;
}
.host-description {
text-overflow: ellipsis;
overflow: hidden;
}
.host-delete {
padding: 0 2px 0 10px;
}
</style>

View file

@ -64,26 +64,23 @@
<li class="uk-open">
<a class="uk-accordion-title" href="#">Autofocus</a>
<div class="uk-text-center uk-container" v-if="isAutofocusing">
<div class="center-spinner" uk-spinner></div>
</div>
<div class="uk-accordion-content" v-else>
<div class="uk-accordion-content">
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
<div>
<button v-on:click="runFastAutofocus(2000, 300);" class="uk-button uk-button-default uk-form-small uk-float-right uk-width-1-1">Fast</button>
</div>
<div>
<button v-on:click="runAutofocus([-60,-30,0,30,60]);" class="uk-button uk-button-default uk-form-small uk-float-right uk-width-1-1">Medium</button>
</div>
<div>
<button v-on:click="runAutofocus([-20,-10,0,10,20]);" class="uk-button uk-button-default uk-form-small uk-float-right uk-width-1-1">Fine</button>
</div>
<div v-if="isAutofocusing">
<progressBar/>
</div>
<div class="uk-grid-small uk-child-width-1-3" v-bind:hidden="isAutofocusing" uk-grid>
<div>
<button v-on:click="runFastAutofocus(2000, 300);" class="uk-button uk-button-default uk-form-small uk-float-right uk-width-1-1">Fast</button>
</div>
<div>
<button v-on:click="runAutofocus([-60,-30,0,30,60]);" class="uk-button uk-button-default uk-form-small uk-float-right uk-width-1-1">Medium</button>
</div>
<div>
<button v-on:click="runAutofocus([-20,-10,0,10,20]);" class="uk-button uk-button-default uk-form-small uk-float-right uk-width-1-1">Fine</button>
</div>
</div>
</div>
</li>
@ -94,6 +91,7 @@
<script>
import axios from 'axios'
import progressBar from "../genericComponents/progressBar"
// Key Codes
const keyCodes = {
@ -111,6 +109,10 @@ const keyCodes = {
export default {
name: 'paneNavigate',
components: {
progressBar
},
data: function () {
return {
keysDown: {},

View file

@ -1,7 +1,6 @@
<template>
<div id="paneSettings">
<h3>Settings</h3>
<appSettings/>
<ul uk-accordion="multiple: true">
@ -26,10 +25,10 @@
</template>
<script>
import streamSettings from './paneSettingsComponents/streamSettings.vue'
import microscopeSettings from './paneSettingsComponents/microscopeSettings.vue'
import cameraSettings from './paneSettingsComponents/cameraSettings.vue'
import appSettings from './paneSettingsComponents/appSettings.vue'
import streamSettings from './settingsComponents/streamSettings.vue'
import microscopeSettings from './settingsComponents/microscopeSettings.vue'
import cameraSettings from './settingsComponents/cameraSettings.vue'
import appSettings from './settingsComponents/appSettings.vue'
// Export main app
export default {

View file

@ -24,10 +24,11 @@
<button type="submit" class="uk-button uk-button-primary uk-form-small uk-float-right uk-margin-small uk-width-1-1">Apply Settings</button>
<div class="uk-text-center uk-container" v-if="isCalibrating">
<div class="center-spinner" uk-spinner></div>
<div v-if="isCalibrating">
<progressBar/>
</div>
<div v-else>
<div v-bind:hidden="isCalibrating">
<button type="button" v-on:click="recalibrateConfirm()" class="uk-button uk-button-default uk-form-small uk-float-right uk-margin-small uk-width-1-1">Auto-Calibrate</button>
</div>
@ -40,11 +41,16 @@
<script>
import axios from 'axios'
import progressBar from "../../genericComponents/progressBar"
// Export main app
export default {
name: 'microscopeSettings',
components: {
progressBar
},
data: function () {
return {
shutterSpeed: this.$store.state.apiConfig.camera_settings.picamera_settings.shutter_speed,

View file

@ -0,0 +1,142 @@
<template>
<div class="progress uk-margin-top uk-margin-horizontal-remove uk-padding-remove">
<div class="indeterminate"></div>
</div>
</template>
<script>
export default {
name: 'progressBar',
props: {},
methods: {
setThisTab(event, value) {
this.$emit('set-tab', event, this.id);
}
},
computed: {
tooltipOptions: function () {
var title = this.id.charAt(0).toUpperCase() + this.id.slice(1);
return `pos: right; title: ${title}; delay: 500`
},
classObject: function () {
return {
'tabicon-active': this.currentTab == this.id,
'uk-disabled': (this.requireConnection && !this.$store.getters.ready)
}
}
}
}
</script>
<style lang="less" scoped>
@import "../../assets/less/theme.less";
.progress {
position: relative;
height: 5px;
display: block;
width: 100%;
background-color: rgba(180, 180, 180, 0.15);
border-radius: 2px;
background-clip: padding-box;
margin: 0.5rem 0 1rem 0;
overflow: hidden;
}
.progress .determinate {
position: absolute;
background-color: inherit;
top: 0;
bottom: 0;
transition: width .3s linear;
}
.progress .indeterminate, .progress .determinate {
background-color: @global-primary-background;
}
.hook-inverse() {
.progress .indeterminate, .progress .determinate{
background-color: @inverse-primary-muted-color;
}
}
.progress .indeterminate:before {
content: '';
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
}
.progress .indeterminate:after {
content: '';
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
-webkit-animation-delay: 1.15s;
animation-delay: 1.15s;
}
@-webkit-keyframes indeterminate {
0% {
left: -35%;
right: 100%; }
60% {
left: 100%;
right: -90%; }
100% {
left: 100%;
right: -90%; }
}
@keyframes indeterminate {
0% {
left: -35%;
right: 100%; }
60% {
left: 100%;
right: -90%; }
100% {
left: 100%;
right: -90%; }
}
@-webkit-keyframes indeterminate-short {
0% {
left: -200%;
right: 100%; }
60% {
left: 107%;
right: -8%; }
100% {
left: 107%;
right: -8%; }
}
@keyframes indeterminate-short {
0% {
left: -200%;
right: 100%; }
60% {
left: 107%;
right: -8%; }
100% {
left: 107%;
right: -8%; }
}
</style>

View file

@ -0,0 +1,46 @@
<template>
<div
v-if="!(this.requireConnection && !this.$store.getters.ready)"
v-bind:hidden="currentTab!=id"
class="uk-width-expand"
>
<div class="section-heading">{{ id }}</div>
<div class="section-content"><slot></slot></div>
</div>
</template>
<script>
export default {
name: 'tabContent',
props: {
id: String,
currentTab: String,
requireConnection: Boolean,
},
methods: {},
computed: {}
}
</script>
<style lang="less" scoped>
.section-heading {
display: block;
font-size: 12px;
text-transform: uppercase;
line-height: 20px;
cursor: default;
}
.section-content, .section-heading {
padding: 9px 10px;
}
</style>

View file

@ -0,0 +1,62 @@
<template>
<a
href="#"
class="uk-link"
:class="classObject"
:uk-tooltip="tooltipOptions"
@click="setThisTab">
<slot></slot>
</a>
</template>
<script>
export default {
name: 'tabIcon',
props: {
id: String,
currentTab: String,
requireConnection: Boolean
},
methods: {
setThisTab(event, value) {
this.$emit('set-tab', event, this.id);
}
},
computed: {
tooltipOptions: function () {
var title = this.id.charAt(0).toUpperCase() + this.id.slice(1);
return `pos: right; title: ${title}; delay: 500`
},
classObject: function () {
return {
'tabicon-active': this.currentTab == this.id,
'uk-disabled': (this.requireConnection && !this.$store.getters.ready)
}
}
}
}
</script>
<style lang="less" scoped>
// Custom UIkit CSS modifications
@import "../../assets/less/theme.less";
.tabicon-active {
color: @global-primary-background !important;
}
.hook-inverse() {
.tabicon-active {
color: @inverse-primary-muted-color !important;
}
}
</style>

View file

@ -1,59 +1,160 @@
<template>
<div id="panelLeft" class="uk-flex uk-flex-column uk-margin-remove uk-padding-remove uk-height-1-1">
<ul class="uk-flex-none uk-flex-center uk-margin-remove-top uk-margin-remove-bottom" uk-tab="swiping: false">
<li><a href="#" uk-switcher-item="connect" uk-icon="server" uk-tooltip="pos: bottom; title: Connect"></a></li>
<li v-bind:class="disableIfDisconnected"><a href="#" uk-switcher-item="navigate" uk-icon="location" uk-tooltip="pos: bottom; title: Navigate"></a></li>
<li v-bind:class="disableIfDisconnected"><a href="#" uk-switcher-item="capture" uk-icon="camera" uk-tooltip="pos: bottom; title: Capture"></a></li>
<li v-bind:class="disableIfDisconnected"><a href="#" uk-switcher-item="plugins" uk-icon="git-fork" uk-tooltip="pos: bottom; title: Plugins"></a></li>
<li><a href="#" uk-switcher-item="settings" uk-icon="settings" uk-tooltip="pos: bottom; title: Settings"></a></li>
</ul>
<ul class="uk-switcher uk-padding-small uk-flex uk-flex-1 panel-content">
<li class="uk-width-expand"><paneConnect/></li>
<li class="uk-width-expand"><div v-if="$store.getters.ready"><paneNavigate/></div></li>
<li class="uk-width-expand"><div v-if="$store.getters.ready"><paneCapture/></div></li>
<li class="uk-width-expand"><div v-if="$store.getters.ready"><panePlugins/></div></li>
<li class="uk-width-expand"><paneSettings/></li>
</ul>
<div id="panel-left" class="uk-margin-remove uk-padding-remove uk-height-1-1" uk-grid>
<!-- Vertical tab bar -->
<div id="switcher-left" class="uk-flex uk-flex-column uk-padding-remove uk-width-auto uk-height-1-1">
<tabIcon id="connect" :requireConnection="false" :currentTab="currentTab" @set-tab="setTab">
<i class="material-icons">settings_ethernet</i>
</tabIcon>
<tabIcon id="navigate" :requireConnection="true" :currentTab="currentTab" @set-tab="setTab">
<i class="material-icons">gamepad</i>
</tabIcon>
<tabIcon id="capture" :requireConnection="true" :currentTab="currentTab" @set-tab="setTab">
<i class="material-icons">camera_alt</i>
</tabIcon>
<tabIcon id="settings" :requireConnection="false" :currentTab="currentTab" @set-tab="setTab">
<i class="material-icons">settings</i>
</tabIcon>
<hr>
<tabIcon v-for="plugin in $store.state.apiPlugins" @set-tab="setTab"
:key="plugin.id"
:id="plugin.id"
:requireConnection="plugin.requiresConnection"
:currentTab="currentTab">
<i class="material-icons">{{ plugin.icon || "extension" }}</i>
</tabIcon>
</div>
<!-- Corresponding vertical tab content -->
<div v-bind:hidden="!showControlBar" id="container-left" class="uk-padding-remove uk-height-1-1 uk-width-expand">
<div id="component-left" class="uk-padding-remove uk-flex uk-flex-1 panel-content">
<tabContent id="connect" :requireConnection="false" :currentTab="currentTab">
<paneConnect/>
</tabContent>
<tabContent id="navigate" :requireConnection="true" :currentTab="currentTab">
<paneNavigate/>
</tabContent>
<tabContent id="capture" :requireConnection="true" :currentTab="currentTab">
<paneCapture/>
</tabContent>
<tabContent id="settings" :requireConnection="false" :currentTab="currentTab">
<paneSettings/>
</tabContent>
<tabContent v-for="plugin in $store.state.apiPlugins"
:key="plugin.id"
:id="plugin.id"
:requireConnection="plugin.requiresConnection"
:currentTab="currentTab">
<div class="uk-flex uk-flex-column" v-for="form in plugin.forms" :key="`${form.route}/${form.name}`.replace(/\s+/g, '-').toLowerCase()" >
<JsonForm
:name="form.name"
:route="form.route"
:isTask="form.isTask"
:submitLabel="form.submitLabel"
:selfUpdate="form.selfUpdate"
:schema="form.schema"/>
<hr>
</div>
</tabContent>
</div>
</div>
</div>
</template>
<script>
// Import components
import paneConnect from './paneConnect.vue'
import paneNavigate from './paneNavigate.vue'
import paneCapture from './paneCapture.vue'
import panePlugins from './panePlugins.vue'
import paneSettings from './paneSettings.vue'
// Import axios for HTTP requests
import axios from 'axios'
// Import generic components
import tabIcon from './genericComponents/tabIcon'
import tabContent from './genericComponents/tabContent'
// Import pane components
import paneConnect from './controlComponents/paneConnect'
import paneNavigate from './controlComponents/paneNavigate'
import paneCapture from './controlComponents/paneCapture'
import paneSettings from './controlComponents/paneSettings'
// Import plugin components
import JsonForm from './pluginComponents/formComponents/JsonForm'
// Export main app
export default {
name: 'panelLeft',
components: {
tabIcon,
tabContent,
paneConnect,
paneNavigate,
paneCapture,
panePlugins,
paneSettings
paneSettings,
JsonForm
},
data: function () {
return {
currentTab: 'connect',
showControlBar: true
}
},
methods: {
setTab: function(event, tab) {
if (this.currentTab == tab) {
this.showControlBar = !this.showControlBar
this.currentTab = 'none'
}
else {
this.showControlBar = true
this.currentTab = tab
}
},
},
computed: {
disableIfDisconnected: function () {
return {
'uk-disabled': !this.$store.getters.ready
}
}
pluginApiUri: function () {
return this.$store.getters.uri + "/plugin"
},
}
}
</script>
<style lang="less">
.uk-tab {
padding-left: 0;
}
.panel-content {
<style scoped lang="less">
#component-left {
width: 300px;
overflow: auto;
}
#container-left {
overflow: hidden auto;
background-color: rgba(180, 180, 180, 0.025);
}
#container-left, #switcher-left {
border-width: 0 1px 0 0;
border-style: solid;
border-color: rgba(180, 180, 180, 0.25)
}
#switcher-left a{
padding: 10px 16px;
}
#switcher-left{
background-color: rgba(180, 180, 180, 0.1);
padding-top: 2px !important;
}
</style>

View file

@ -1,30 +1,34 @@
<template>
<div id="panelDisplay" class="uk-flex uk-flex-column uk-margin-remove uk-padding-remove uk-height-1-1">
<!-- Tabbed panel for gallery and live views -->
<div id="panel-right" class="uk-flex uk-flex-column uk-margin-remove uk-padding-remove uk-width-expand uk-height-1-1">
<ul class="uk-flex-none uk-flex-center uk-margin-remove-bottom uk-text-center" uk-tab="swiping: false">
<li><a href="#" uk-switcher-item="preview" uk-icon="play-circle" uk-tooltip="pos: bottom; title: Live"></a></li>
<li v-bind:class="{'uk-disabled': !this.$store.getters.ready}"><a href="#" uk-switcher-item="gallery" uk-icon="image" uk-tooltip="pos: bottom; title: Captures"></a></li>
<li><a href="#" uk-switcher-item="preview">Live</a></li>
<li v-bind:class="{'uk-disabled': !this.$store.getters.ready}"><a href="#" uk-switcher-item="gallery">Gallery</a></li>
</ul>
<ul class="uk-switcher uk-flex uk-flex-1">
<li class="uk-height-1-1 uk-width-1-1 clickableTab" id="streamDisplayTab"><streamDisplay/></li>
<li class="uk-height-1-1 uk-width-1-1 uk-overflow-auto" id="galleryDisplayTab"><galleryDisplay/></li>
<li class="uk-height-1-1 uk-width-1-1 clickableTab" id="streamDisplayTab"><streamDisplay/></li>
<li class="uk-height-1-1 uk-width-1-1 uk-overflow-auto" id="galleryDisplayTab"><galleryDisplay/></li>
</ul>
</div>
</template>
<script>
// Import axios for HTTP requests
import axios from 'axios'
// Import basic UIkit
import UIkit from 'uikit';
// Import components
import streamDisplay from './paneDisplayComponents/streamDisplay.vue'
import galleryDisplay from './paneDisplayComponents/galleryDisplay.vue'
import streamDisplay from './viewComponents/streamDisplay.vue'
import galleryDisplay from './viewComponents/galleryDisplay.vue'
// Export main app
export default {
name: 'panelDisplay',
name: 'panelRight',
components: {
streamDisplay,
galleryDisplay
galleryDisplay,
},
mounted() {
@ -54,4 +58,9 @@ export default {
</script>
<style scoped lang="less">
.uk-tab {
padding-left: 0;
}
</style>

View file

@ -0,0 +1,229 @@
<template>
<div>
<div class="uk-flex">
<div class="uk-text-bold uk-text-uppercase uk-width-expand">{{ name }}</div>
<a href="#" v-if="selfUpdate" v-on:click="getFormData()" class="uk-icon uk-width-auto"><i class="material-icons">cached</i></a>
</div>
<form @submit.prevent="submitForm" class="uk-form-stacked">
<div v-for="(field, index) in schema" :key="index">
<div v-if="Array.isArray(field)" class="uk-grid-small uk-width-1-1 uk-child-width-expand" uk-grid>
<div v-for="(subfield, subindex) in field" :key="subindex">
<component
:is="subfield.fieldType"
v-model="formData[subfield.name]"
v-bind="subfield">
</component>
</div>
</div>
<component
:is="field.fieldType"
v-model="formData[field.name]"
v-bind="field">
</component>
</div>
<div v-if="taskRunning">
<progressBar/>
</div>
<button v-bind:hidden="taskRunning" class="uk-button uk-button-primary uk-form-small uk-float-right uk-margin-small uk-width-1-1">{{ submitLabel }}</button>
</form>
</div>
</template>
<script>
import axios from 'axios'
import numberInput from "./fieldComponents/numberInput";
import selectList from "./fieldComponents/selectList";
import textInput from "./fieldComponents/textInput";
import htmlBlock from "./fieldComponents/htmlBlock";
import radioList from "./fieldComponents/radioList";
import checkList from "./fieldComponents/checkList"
import progressBar from "../../genericComponents/progressBar"
export default {
name: 'JsonForm',
components: {
numberInput,
selectList,
textInput,
htmlBlock,
radioList,
checkList,
progressBar
},
props: {
'name': {
type: String,
required: false,
default: "Plugin"
},
'schema': {
type: Array,
required: true
},
'route': {
type: String,
required: true
},
'isTask': {
type: Boolean,
required: false,
default: false
},
'submitLabel': {
type: String,
required: false,
default: "Submit"
},
'selfUpdate': {
type: Boolean,
required: false,
default: false
},
},
data: function () {
return {
formData: {},
taskRunning: false
}
},
created() {
this.initialiseFormData()
if (this.selfUpdate) {
this.getFormData()
}
},
methods: {
initialiseFormData() {
/*
This function initialises the form data.
Limitations in Vue mean that newly created formData properties are not reactive.
GETting formData values from the server creates the properties, but the form components
don't get updated because of this limitation.
Here, we step through the form schema, and properly create reactive properties for each component.
*/
for (const field of this.schema) {
if (Array.isArray(field)) {
for (const subfield of field) {
console.log(subfield.name)
this.$set(this.formData, subfield.name, null)
}
}
else {
console.log(field.name)
this.$set(this.formData, field.name, null)
}
}
},
updateForm(fieldName, value) {
this.$set(this.formData, fieldName, value);
this.$emit('input', this.formData)
},
submitForm() {
if (this.isTask == true) {
this.newLongRequest(this.formData)
}
else {
this.newQuickRequest(this.formData)
}
},
getFormData: function() {
// Send a quick request
axios.get(this.submitApiUri)
.then(response => {
console.log(response.data)
Object.assign(this.formData, response.data)
})
.catch(error => {
this.modalError(error) // Let mixin handle error
})
},
newQuickRequest: function(params) {
console.log(this.submitApiUri)
console.log(params)
// Send a quick request
axios.post(this.submitApiUri, params)
.then(response => {
// Do something with the response
console.log(response)
// Update the form data if we're self-updating
if (this.selfUpdate) {
this.getFormData()
}
})
.catch(error => {
this.modalError(error) // Let mixin handle error
})
},
newLongRequest: function(params) {
this.taskRunning = true
axios.post(this.submitApiUri, params)
.then(response => {
console.log("Task ID: " + response.data.id)
// Start the store polling TaskId for success
return this.$store.dispatch('pollTask', [response.data.id, null, null])
})
.then(response => {
// Do something with the response
console.log(response)
})
.catch(error => {
this.modalError(error) // Let mixin handle error
})
.finally(() => {
console.log("Cleaning up after task.")
this.taskRunning = false
// Update the form data if we're self-updating
if (this.selfUpdate) {
this.getFormData()
}
})
},
},
computed: {
pluginApiUri: function () {
return this.$store.getters.uri + "/plugin"
},
submitApiUri: function () {
return this.pluginApiUri + this.route
},
}
}
</script>
<style scoped>
.flex-container {
display: flex;
}
.flex-container > div {
flex-basis: 100%
}
</style>

View file

@ -0,0 +1,52 @@
<template>
<div>
<label>{{label}}</label>
<div class="uk-form-controls">
<div v-for="option in options" :key="option">
<label>
<input class="uk-checkbox" type="checkbox" v-bind:value="option" v-bind:checked="(value && value.includes(option))" @change="updateValue($event.target)">
{{ option }}
</label>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'checkList',
props: [
'options',
'name',
'label',
'value'
],
methods: {
updateValue(target) {
var newSelected = this.value != null ? [...this.value] : [] // Clone value array
if (target.checked) {
if (!newSelected.includes(target.value)) {
newSelected.push(target.value)
}
}
else {
if (newSelected.includes(target.value)) {
var newSelected = newSelected.filter(function(value, index, arr){
return value != target.value;
})
}
}
this.$emit('input', newSelected)
}
}
}
</script>
<style scoped></style>

View file

@ -0,0 +1,19 @@
<template>
<div>
<p v-html="content"></p>
</div>
</template>
<script>
export default {
name: 'htmlBlock',
props: [
'label',
'name',
'content'
]
}
</script>
<style scoped></style>

View file

@ -0,0 +1,33 @@
<template>
<div>
<label class="uk-form-label">{{label}}</label>
<input
class="uk-input uk-form-small"
type="number"
:name="name"
:value="value"
@input="$emit('input', $event.target.value)"
:placeholder="placeholder"
>
</div>
</template>
<script>
export default {
name: 'numberInput',
props: [
'placeholder',
'label',
'name',
'value'
]
}
</script>
<style scoped></style>

View file

@ -0,0 +1,29 @@
<template>
<div>
<label>{{label}}</label>
<div class="uk-form-controls">
<div v-for="option in options" :key="option">
<label><input class="uk-radio" type="radio" :name="name" :value="option" :checked="value==option" @input="$emit('input', $event.target.value)"> {{ option }}</label>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'radioList',
props: [
'options',
'name',
'label',
'value'
]
}
</script>
<style scoped></style>

View file

@ -0,0 +1,33 @@
<template>
<div>
<label class="uk-form-label">{{label}}</label>
<select
class="uk-select uk-form-small"
:multiple="multi"
:value="value"
@input="$emit('input', $event.target.value)"
>
<option v-for="option in options" :key="option">
{{option}}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'selectList',
props: [
'multi',
'options',
'name',
'label',
'value'
]
}
</script>
<style scoped></style>

View file

@ -0,0 +1,30 @@
<template>
<div>
<label class="uk-form-label">{{label}}</label>
<input
class="uk-input uk-form-small"
type="text"
v-bind:name="name"
v-bind:value="value"
v-bind:placeholder="placeholder"
@input="$emit('input',$event.target.value)"
>
</div>
</template>
<script>
export default {
name: 'textInput',
props: [
'placeholder',
'label',
'name',
'value'
]
}
</script>
<style scoped></style>

View file

@ -13,7 +13,9 @@
<div class="uk-width-1-1 uk-margin-small uk-margin-remove-left uk-margin-remove-right" uk-grid>
<div class="uk-margin-remove-top uk-padding-remove uk-width-expand">{{ metadata.filename }}</div>
<div class="uk-margin-remove-top uk-padding-remove uk-width-auto">
<a href="#" v-on:click="delCaptureConfirm()" class="uk-icon-link" uk-icon="trash"></a>
<a href="#" v-on:click="delCaptureConfirm()" class="uk-icon">
<i class="material-icons">delete</i>
</a>
</div>
</div>
@ -59,7 +61,7 @@
<form class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical" v-bind:class="{ 'uk-light uk-background-secondary': $store.state.globalSettings.darkMode }" @submit.prevent="handleTagSubmit">
<div class="uk-inline">
<span class="uk-form-icon" uk-icon="icon: tag"></span>
<span class="uk-form-icon"><i class="material-icons">label</i></span>
<input v-model="newtag" class="uk-input uk-form-width-medium uk-form-small" type="text" name="tagname" placeholder="tag">
<button class="uk-button uk-button-default uk-margin-left uk-form-small uk-modal-close" type="button">Cancel</button>

View file

@ -4,8 +4,8 @@
<nav class="uk-navbar-container uk-navbar-transparent navbar" uk-navbar="mode: click">
<div class="uk-navbar-left uk-padding-remove-top uk-padding-remove-bottom">
<ul class="uk-navbar-nav">
<li v-bind:class="[sortDescending ? 'uk-active' : '']"><a v-on:click="sortDescending=true;" class="uk-icon-link" href="#" uk-icon="icon: arrow-down"></a></li>
<li v-bind:class="[!sortDescending ? 'uk-active' : '']"><a v-on:click="sortDescending=false;" class="uk-icon-link" href="#" uk-icon="icon: arrow-up"></a></li>
<li v-bind:class="[sortDescending ? 'uk-active' : '']"><a v-on:click="sortDescending=true;" class="uk-icon" href="#"><i class="material-icons">keyboard_arrow_down</i></a></li>
<li v-bind:class="[!sortDescending ? 'uk-active' : '']"><a v-on:click="sortDescending=false;" class="uk-icon" href="#"><i class="material-icons">keyboard_arrow_up</i></a></li>
<li>
<a href="#">Filter</a>
<div class="uk-navbar-dropdown" v-bind:class="{ 'uk-light uk-background-secondary': $store.state.globalSettings.darkMode }">
@ -25,9 +25,11 @@
<div v-if="$store.getters.ready" class="uk-padding-remove-top" uk-lightbox="toggle: .lightbox-link">
<div v-if="(galleryFolder)" class="uk-padding uk-padding-remove-right uk-padding-remove-bottom">
<a href="#" v-on:click="galleryFolder=''" class="uk-icon-button" uk-icon="arrow-left"></a>
<h3 class="uk-inline uk-float-right uk-margin-remove"><b>SCAN</b> {{ allScans[galleryFolder].metadata.filename }}</h3>
<div v-if="(galleryFolder)" class="uk-flex uk-flex-middle uk-padding uk-padding-remove-horizontal uk-padding-remove-bottom">
<a href="#" v-on:click="galleryFolder=''" class="uk-icon uk-margin-remove"><i class="material-icons">arrow_back</i></a>
<div class="uk-margin-left">
<h3 class="uk-margin-remove uk-margin-left"><b>SCAN</b> {{ allScans[galleryFolder].metadata.filename }}</h3>
</div>
</div>
<div class="uk-grid-medium uk-grid-match uk-margin-top" uk-grid>

View file

@ -1,5 +1,5 @@
<template>
<div class="streamDisplay uk-width-1-1 uk-height-1-1 scrollTarget" id="streamDisplay" ref="streamDisplay">
<div ref="streamDisplay" class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget" id="stream-display">
<img class="uk-align-center uk-margin-remove-bottom" v-on:dblclick="clickmonitor" v-if="showStream" v-bind:src="streamImgUri" alt="Stream">
@ -23,33 +23,37 @@ import axios from 'axios'
// Export main app
export default {
name: 'streamDisplay',
name: 'stream-display',
data: function () {
return {
displaySize: [0, 0],
displayPosition: [0, 0],
GpuPreviewActive: false,
resizeTeimoutId: setTimeout(this.doneResizing, 500)
resizeTimeoutId: setTimeout(this.doneResizing, 500)
}
},
mounted() {
// A global signal listener to change the GPU preview state
this.$root.$on('globalResizePreview', (state) => {
this.handleDoneResize()
})
// A global signal listener to change the GPU preview state
this.$root.$on('globalTogglePreview', (state) => {
console.log(`Toggling preview to ${state}`)
this.previewRequest(state)
})
// Mutation observer
this.sizeObserver = new ResizeObserver(entries => {
this.handleResize() // For any element attached to the observer, run handleResize() on change
entries.forEach(entry => {}) // Optional: Run something per entry
});
// Fetch streamDisplay by ref
const streamDisplayElement = this.$refs.streamDisplay.parentNode;
// Attach streamDisplay to the size observer
this.sizeObserver.observe(streamDisplayElement);
},
created: function () {
// Add resize listener
window.addEventListener('resize', this.handleResize);
// Watch for host 'ready'
this.$store.watch(
(state)=>{
@ -57,29 +61,27 @@ export default {
},
(newValue, oldValue)=>{
// 'ready' changed, so do something
console.log(oldValue)
console.log(newValue)
this.previewRequest(this.$store.state.globalSettings.autoGpuPreview)
}
)
},
beforeDestroy: function () {
// Remove resize listener
window.removeEventListener('resize', this.handleResize)
// Disconnect the size observer
this.sizeObserver.disconnect()
},
methods: {
clickmonitor: function(event) {
// Calculate steps from event coordinates and store config FOV
var xCoordinate = event.offsetX;
var yCoordinate = event.offsetY;
let xCoordinate = event.offsetX;
let yCoordinate = event.offsetY;
var xRelative = (0.5*event.target.offsetWidth - xCoordinate)/event.target.offsetWidth;
var yRelative = (0.5*event.target.offsetHeight - yCoordinate)/event.target.offsetHeight;
let xRelative = (0.5*event.target.offsetWidth - xCoordinate)/event.target.offsetWidth;
let yRelative = (0.5*event.target.offsetHeight - yCoordinate)/event.target.offsetHeight;
var xSteps = xRelative * this.$store.state.apiConfig.fov[0];
var ySteps = yRelative * this.$store.state.apiConfig.fov[1];
let xSteps = xRelative * this.$store.state.apiConfig.fov[0];
let ySteps = yRelative * this.$store.state.apiConfig.fov[1];
// Emit a signal to move, acted on by panelNavigate.vue
this.$root.$emit('globalMoveEvent', xSteps, ySteps, 0, false)
@ -87,12 +89,13 @@ export default {
handleResize: function(event) {
// Only fires resize event after no resize in 500ms (prevents resize event spam)
clearTimeout(this.resizeTeimoutId);
this.resizeTeimoutId = setTimeout(this.handleDoneResize, 500)
clearTimeout(this.resizeTimeoutId);
this.resizeTimeoutId = setTimeout(this.handleDoneResize, 250)
},
handleDoneResize: function() {
// Recalculate size
console.log("Recalculating frame size")
this.recalculateSize();
if (this.$store.state.globalSettings.autoGpuPreview == true && this.GpuPreviewActive == true) {
// Reload preview
@ -101,23 +104,24 @@ export default {
},
recalculateSize: function () {
console.log("Recalculating window dimensions...")
// Stacking parentNode is a hacky fix
// For some reason, when switching tabs, width was always half what it should be,
// until the size was recalculated at some later time. Probably something to do
// with tab transition. This parentNode stuff instead reads the size of the tab
// container, irrespective of WHICH tab is selected. It's nasty, but works.
let element = this.$refs.streamDisplay.parentNode.parentNode;
let element = this.$refs.streamDisplay.parentNode;
let bound = element.getBoundingClientRect()
let size = [element.clientWidth, element.clientHeight];
let elementSize = [bound.width, bound.height]
let elem_pos = [element.getBoundingClientRect().left, element.getBoundingClientRect().top];
let wind_pos = [window.screenX, window.screenY];
let navHeight = window.outerHeight - window.innerHeight;
let position = [Math.max(0, wind_pos[0] + elem_pos[0]), Math.max(0, wind_pos[1] + elem_pos[1] + navHeight)];
let elementPositionOnWindow = [bound.left, bound.top]
let windowPositionOnDisplay = [window.screenX, window.screenY]
let windowChromeHeight = window.outerHeight - window.innerHeight
let elementPositionOnDisplay = [
Math.max(0, windowPositionOnDisplay[0] + elementPositionOnWindow[0]),
Math.max(0, windowPositionOnDisplay[1] + elementPositionOnWindow[1] + windowChromeHeight)
]
this.displaySize = size;
this.displayPosition = position;
this.displaySize = elementSize
this.displayPosition = elementPositionOnDisplay
console.log(`Size: ${this.displaySize}`)
console.log(`Position: ${this.displayPosition}`)
},
previewRequest: function(state) {
@ -147,7 +151,7 @@ export default {
}
}
else {
var letpayload = {}
var payload = {}
}
// Send preview request
@ -182,13 +186,13 @@ export default {
</script>
<style scoped lang="less">
.streamDisplay img {
.stream-display img {
height: 100%;
text-align: center;
object-fit: contain
}
.streamDisplay {
.stream-display {
width: 100%;
height: 100%;
}

View file

@ -3,9 +3,9 @@ import App from './App.vue'
import store from './store'
import UIkit from 'uikit';
// Import UIkit icon set
import Icons from 'uikit/dist/js/uikit-icons';
UIkit.use(Icons);
// Import MD icons
import 'material-design-icons/iconfont/material-icons.css'
Vue.config.productionTip = false
@ -78,7 +78,7 @@ Vue.mixin({
console.log(errormsg)
}
this.$store.commit('setError', errormsg);
UIkit.notification({message: `<span uk-icon=\'icon: warning\'></span> ${errormsg}`, status: 'danger'})
UIkit.notification({message: `${errormsg}`, status: 'danger'})
},
getLocalStorageObj: function(keyName) {

View file

@ -15,6 +15,7 @@ export default new Vuex.Store({
error: '',
apiConfig: {},
apiState: {},
apiPlugins: [],
globalSettings: {
disableStream: false,
autoGpuPreview: false,
@ -38,6 +39,9 @@ export default new Vuex.Store({
commitState(state, stateData) {
state.apiState = stateData;
},
commitPlugins(state, pluginData) {
state.apiPlugins = pluginData;
},
changeSetting(state, [key, value]) {
state.globalSettings[key] = value;
},
@ -47,6 +51,7 @@ export default new Vuex.Store({
state.error = null
state.apiConfig = {}
state.apiState = {}
state.apiPlugins = []
},
setConnected(state) {
state.waiting = false
@ -72,6 +77,9 @@ export default new Vuex.Store({
.then (() => {
context.dispatch('updateState')
})
.then (() => {
context.dispatch('updatePlugins')
})
.then (() => {
resolve()
})
@ -116,6 +124,25 @@ export default new Vuex.Store({
return new Promise(sendRequest)
},
updatePlugins(context, uri=`${context.getters.uri}/plugin`) {
context.commit('changeWaiting', true)
var sendRequest = function(resolve, reject) {
axios.get(uri)
.then(response => {
console.log("PLUGINS")
console.log(response.data)
context.commit('commitPlugins', response.data)
context.commit('setConnected')
resolve()
})
.catch(error => {
reject(new Error(error))
})
}
return new Promise(sendRequest)
},
pollTask(context, [taskId, timeout, interval]) {
var endTime = Number(new Date()) + (timeout*1000 || 30000);
interval = interval*1000 || 500;
@ -128,7 +155,7 @@ export default new Vuex.Store({
var result = response.data.status
// If the task ends with success
if(result == 'success') {
resolve(response.data.return)
resolve(response.data)
}
// If task ends with an error
else if (result == 'error') {
@ -136,7 +163,7 @@ export default new Vuex.Store({
}
// If the condition isn't met but the timeout hasn't elapsed, go again
else if (Number(new Date()) < endTime) {
setTimeout(checkCondition, interval, resolve, reject)
setTimeout(checkCondition, interval, resolve, reject)
}
// Didn't match and too much time, reject!
else {