Merge branch 'master' into 'stable'

v1.1.0

See merge request openflexure/openflexure-microscope-jsclient!34
This commit is contained in:
Joel Collins 2019-07-04 10:30:23 +00:00
commit be881c482e
48 changed files with 2437 additions and 897 deletions

3
.env.development.web Normal file
View file

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

3
.env.production.app Normal file
View file

@ -0,0 +1,3 @@
NODE_ENV=production
VUE_APP_PLATFORM=web
VUE_APP_TARGET=electron-renderer

3
.env.production.web Normal file
View file

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

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
.DS_Store .DS_Store
node_modules node_modules
/dist /dist
/app/dist
/release-builds /release-builds
/installers/release-builds /installers/release-builds

View file

@ -1,6 +1,5 @@
stages: stages:
- build - build
- package
- meta - meta
# Cache modules in between jobs # Cache modules in between jobs
@ -9,13 +8,13 @@ cache:
paths: paths:
- node_modules/ - node_modules/
# Build basic webapp # Basic webapp build
build: build:
stage: build stage: build
image: electronuserland/builder:wine image: electronuserland/builder:wine
script: script:
- npm install - npm install
- npm run build - npm run build:web
artifacts: artifacts:
name: "build" name: "build"
paths: paths:
@ -25,20 +24,20 @@ build:
- tags - tags
- web - web
# Platform-specific Electron builds # Electron app build
package: package:
stage: package stage: build
dependencies:
- build
image: electronuserland/builder:wine image: electronuserland/builder:wine
retry: 2
script: script:
- npm install
- npm run build:app
- npm run release - npm run release
- shopt -s extglob
- mv ./release-builds/*.AppImage . - mv ./release-builds/*.AppImage .
- mv ./release-builds/*.deb . - mv ./release-builds/*.deb .
- mv ./release-builds/*.exe . - mv ./release-builds/*.exe .
- mv ./release-builds/*.exe.blockmap . - mv ./release-builds/*.exe.blockmap .
- mv ./release-builds/latest*.yml . - mv ./release-builds/?(beta*.yml|latest*.yml) .
artifacts: artifacts:
name: "dist" name: "dist"
paths: paths:
@ -47,6 +46,7 @@ package:
- "*.exe" - "*.exe"
- "*.exe.blockmap" - "*.exe.blockmap"
- "latest*.yml" - "latest*.yml"
- "beta*.yml"
only: only:
- stable - stable
- tags - tags
@ -74,6 +74,4 @@ meta:choco:
- tools/chocolateyInstall.ps1 - tools/chocolateyInstall.ps1
- ./*.nupkg - ./*.nupkg
only: only:
- stable
- tags - tags
- web

View file

@ -6,6 +6,9 @@ const autoUpdater = updater.autoUpdater;
const contextMenu = require('electron-context-menu') const contextMenu = require('electron-context-menu')
const path = require('path') const path = require('path')
// Attach settings store
const { store } = require('./store')
// Auto upadater // Auto upadater
autoUpdater.autoDownload = false; autoUpdater.autoDownload = false;
@ -14,9 +17,7 @@ autoUpdater.setFeedURL({
url: "https://gitlab.com/openflexure/openflexure-microscope-jsclient/-/jobs/artifacts/stable/raw?job=package" url: "https://gitlab.com/openflexure/openflexure-microscope-jsclient/-/jobs/artifacts/stable/raw?job=package"
}); });
autoUpdater.on('checking-for-update', function () { autoUpdater.on('checking-for-update', function () {});
sendStatusToWindow('Checking for update...');
});
autoUpdater.on('update-available', function (info) { autoUpdater.on('update-available', function (info) {
sendStatusToWindow('Update available.' + info) sendStatusToWindow('Update available.' + info)
@ -70,56 +71,76 @@ const app = electron.app
const BrowserWindow = electron.BrowserWindow const BrowserWindow = electron.BrowserWindow
// Set the window content // Set the window content
let url = `file://${path.join(__dirname, '../dist/index.html')}` let url = `file://${path.join(__dirname, '/dist/index.html')}`
let mainWindow let mainWindow
// Set the application menu // Set the application menu
require('./menu.js') require('./menu.js')
// Handle redrawing the mainWindow
let recreatingWindowInProgress = false;
function toggleCustomTitleBar () {
// Mark window as being recreated, to prevent stopping the application
recreatingWindowInProgress = true
// Invert the drawCustomTitleBar setting
store.set('drawCustomTitleBar', !store.get('drawCustomTitleBar'))
// Destroy old window
mainWindow.destroy()
// Create new window
createWindow()
// Mark window as no longer being recreated
recreatingWindowInProgress = false
}
function createWindow() { function createWindow() {
// Create the browser window. // Create the browser window.
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
frame: !store.get('drawCustomTitleBar'),
width: 1124, width: 1124,
height: 800, height: 800,
icon: path.join(__dirname, '/icons/png/64x64.png') icon: path.join(__dirname, '/icons/png/64x64.png')
}) })
// Make a context menu
contextMenu({ contextMenu({
showCopyImageAddress: true, showCopyImageAddress: true,
showSaveImageAs: true, showSaveImageAs: true,
showInspectElement: false, showInspectElement: false,
}); });
// Load window contents
mainWindow.loadURL(url) mainWindow.loadURL(url)
// Check for updates
autoUpdater.checkForUpdates(); autoUpdater.checkForUpdates();
// Emitted when the window is closed. // Emitted when the window is closed.
mainWindow.on('closed', function() { mainWindow.on('closed', function() {
// Dereference the window object, usually you would store windows mainWindow = null // Dereference the window object
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
}) })
} }
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
app.on('ready', createWindow) app.on('ready', () => {
createWindow()
})
// Quit when all windows are closed. // Quit when all windows are closed.
app.on('window-all-closed', function() { app.on('window-all-closed', function() {
// On OS X it is common for applications and their menu bar if ((process.platform !== 'darwin') && recreatingWindowInProgress != true) {
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit() app.quit()
} }
}) })
app.on('activate', function() { app.on('activate', function() {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) { if (mainWindow === null) {
createWindow() createWindow()
} }
}) })
// Export toggleCustomTitleBar for use in ./menu.js
module.exports.toggleCustomTitleBar = toggleCustomTitleBar

View file

@ -41,7 +41,7 @@ echo "Nupkg version: $version"
# Build installer URL # Build installer URL
pipelineURL="$CI_PROJECT_URL/-/jobs/artifacts/$CI_COMMIT_REF_NAME/raw" pipelineURL="$CI_PROJECT_URL/-/jobs/artifacts/$CI_COMMIT_REF_NAME/raw"
instURL="$pipelineURL/openflexure-ev-win.exe?job=package:win32" instURL="$pipelineURL/openflexure-ev-win.exe?job=package"
nuspecURL="$CI_JOB_URL/artifacts/browse" nuspecURL="$CI_JOB_URL/artifacts/browse"
echo "$instURL" echo "$instURL"

View file

@ -1,10 +1,13 @@
const { app, Menu } = require('electron') const { app, shell, Menu } = require('electron')
const updater = require("electron-updater"); const updater = require("electron-updater");
const autoUpdater = updater.autoUpdater; const autoUpdater = updater.autoUpdater;
const path = require('path') const path = require('path')
const openAboutWindow = require('about-window').default const openAboutWindow = require('about-window').default
const main = require('./app')
const { store } = require('./store')
const template = [ const template = [
{ {
label: 'Edit', label: 'Edit',
@ -26,7 +29,16 @@ const template = [
{ role: 'forcereload' }, { role: 'forcereload' },
{ role: 'toggledevtools' }, { role: 'toggledevtools' },
{ type: 'separator' }, { type: 'separator' },
{ role: 'togglefullscreen' } { role: 'togglefullscreen' },
{ type: 'separator' },
{
type: 'checkbox',
checked: store.get('drawCustomTitleBar'),
label: 'Custom titlebar',
click () {
main.toggleCustomTitleBar()
}
}
] ]
}, },
{ {
@ -50,6 +62,13 @@ const template = [
}) })
} }
}, },
{
label: 'Homepage',
click () {
shell.openExternal('https://openflexure.org')
}
},
{ type: 'separator' },
{ {
label: 'Check for Updates', label: 'Check for Updates',
click () { click () {

9
app/store.js Normal file
View file

@ -0,0 +1,9 @@
const Store = require('electron-store');
const store = new Store();
if (store.has('drawCustomTitleBar') !== true) {
// Default to false if on MacOS, otherwise true
store.set('drawCustomTitleBar', (process.platform !== 'darwin') ? true : false)
}
module.exports.store = store

1144
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "openflexure-ev", "name": "openflexure-ev",
"version": "0.2.6", "version": "1.1.0",
"private": true, "private": true,
"description": "An electron-based user client for the OpenFlexure Microscope Server", "description": "An electron-based user client for the OpenFlexure Microscope Server",
"author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)", "author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)",
@ -11,21 +11,24 @@
"license": "GNU General Public License v3.0 ", "license": "GNU General Public License v3.0 ",
"main": "./app/app.js", "main": "./app/app.js",
"scripts": { "scripts": {
"release": "electron-builder --linux --config app/builder-config-raspi.yaml & electron-builder --win --linux --config app/builder-config-x86_64.yaml", "build:web": "vue-cli-service build --mode production.web",
"build:app": "vue-cli-service build --mode production.app",
"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:linux": "electron-builder --linux --config app/builder-config-x86_64.yaml",
"dist:raspi": "electron-builder --linux --config app/builder-config-raspi.yaml", "dist:raspi": "electron-builder --linux --config app/builder-config-raspi.yaml",
"dist:win": "electron-builder --win --config app/builder-config-x86_64.yaml", "dist:win": "electron-builder --win --config app/builder-config-x86_64.yaml",
"dist:dmg": "electron-builder --mac --config app/builder-config-x86_64.yaml", "dist:dmg": "electron-builder --mac --config app/builder-config-x86_64.yaml",
"dist:appx": "electron-builder --win --config app/builder-config-appx.yaml", "dist:appx": "electron-builder --win --config app/builder-config-appx.yaml",
"build": "vue-cli-service build", "release": "electron-builder --linux --config app/builder-config-raspi.yaml & electron-builder --win --linux --config app/builder-config-x86_64.yaml"
"electron:run": "electron .",
"electron:dev": "electron app/app.dev.js",
"serve": "vue-cli-service serve"
}, },
"dependencies": { "dependencies": {
"about-window": "^1.12.1", "about-window": "^1.12.1",
"custom-electron-titlebar": "^3.0.8",
"electron-context-menu": "^0.11.0", "electron-context-menu": "^0.11.0",
"electron-updater": "^4.0.6" "electron-store": "^3.2.0",
"electron-updater": "^4.0.6",
"material-design-icons": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^3.3.0", "@vue/cli-plugin-babel": "^3.3.0",
@ -33,10 +36,10 @@
"axios": "^0.18.0", "axios": "^0.18.0",
"css-loader": "^2.1.1", "css-loader": "^2.1.1",
"electron": "^4.1.4", "electron": "^4.1.4",
"electron-builder": "^20.39.0", "electron-builder": "^21.0.1",
"less": "^3.9.0", "less": "^3.9.0",
"less-loader": "^4.1.0", "less-loader": "^4.1.0",
"uikit": "3.1.1", "uikit": "^3.1.1",
"vue": "^2.5.21", "vue": "^2.5.21",
"vue-template-compiler": "^2.6.10", "vue-template-compiler": "^2.6.10",
"vuex": "^3.0.1" "vuex": "^3.0.1"

9
public/titleicon.svg Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 90 56" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;">
<g transform="matrix(1,0,0,1,-30.245,-47.0254)">
<g id="path3773" transform="matrix(0.12596,0,0,0.12596,28.1368,19.1335)">
<path d="M212.85,344.652C234.052,276.913 297.347,227.702 372.047,227.702C446.747,227.702 510.042,276.913 531.245,344.652L531.245,344.652L531.246,344.656C531.789,346.391 532.304,348.138 532.791,349.896C556.452,421.618 655.593,393.868 655.593,393.868L721.09,563.056L472.356,659.347L430.306,550.728L430.129,550.794L406.853,490.142C408.621,489.464 410.361,488.736 412.072,487.962C419.736,484.232 426.404,480.109 432.198,475.703C447.995,463.209 459.586,446.041 465.257,426.69C473.38,395.32 463.384,366.691 463.101,365.891C449.309,328.959 413.54,302.366 372.047,302.366C330.554,302.366 294.785,328.959 280.994,365.891C280.711,366.689 270.714,395.319 278.838,426.69C284.509,446.041 296.099,463.209 311.897,475.703C317.69,480.109 324.359,484.232 332.023,487.962C333.734,488.736 335.474,489.464 337.242,490.142L337.166,490.34C337.166,490.34 313.965,550.794 313.965,550.794L313.788,550.728L271.739,659.347L23.004,563.056L88.501,393.868C88.501,393.868 187.642,421.618 211.303,349.896C211.79,348.138 212.306,346.391 212.849,344.656L212.85,344.652L212.85,344.652Z" style="fill:white;fill-rule:nonzero;stroke:white;stroke-width:12.54px;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,82 +1,41 @@
<template> <template>
<div id="app" v-bind:class="handleTheme"> <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 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"> <panelLeft/>
<div id="overlay-toggle"> <panelRight/>
<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>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
// Import axios for HTTP requests // Import axios for HTTP requests
import axios from 'axios' import axios from 'axios'
// Import basic UIkit
import UIkit from 'uikit';
// Import components // Import components
import panelLeft from './components/panelLeft.vue' import panelLeft from './components/panelLeft.vue'
import panelDisplay from './components/panelDisplay.vue' import panelRight from './components/panelRight.vue'
// Export main app // Export main app
export default { export default {
name: 'app', name: 'app',
components: { components: {
panelLeft, panelRight,
panelDisplay panelLeft
}, },
data: function () { data: function () {
return { return {}
window: {
width: 0,
height: 0
}
}
}, },
created: function () { 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) window.addEventListener('beforeunload', this.handleExit)
}, },
beforeDestroy: function () {
window.removeEventListener('resize', this.handleResize)
},
methods: { methods: {
handleResize: function(event) {
this.window.width = window.innerWidth;
this.window.height = window.innerHeight;
},
handleExit: function(event) { handleExit: function(event) {
console.log("Triggered beforeunload") console.log("Triggered beforeunload")
this.$root.$emit('globalTogglePreview', false) this.$root.$emit('globalTogglePreview', false)
@ -114,38 +73,9 @@ body, html {
overflow-x: hidden; 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 { .uk-disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.4;
} }
</style> </style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

17
src/assets/less/app.less Normal file
View file

@ -0,0 +1,17 @@
// Theming specific to the electron app
.menubar-menu-container {
box-shadow: 0.5px 1px 4px 0px rgba(0,0,0,0.3) !important;
}
.menubar-menu-container > * {
box-shadow: none !important;
}
.menubar-menu-container .action-menu-item, .menubar-menu-container .action-label {
cursor: default
}
.menubar-menu-container a:hover {
text-decoration: none;
}

View file

@ -4,9 +4,14 @@
// Highlight.js // Highlight.js
@import "./highlight.less"; @import "./highlight.less";
// Electron app theming
@import "./app.less";
// Custom OpenFlexure theming // Custom OpenFlexure theming
@global-primary-background: #C32280; @global-primary-background: #C32280;
@inverse-primary-muted-color: lighten(@global-primary-background, 15%);
// UIkit // UIkit
// ======================================================================== // ========================================================================
@ -120,6 +125,10 @@
@inverse-global-color: fade(@global-inverse-color, 80%); @inverse-global-color: fade(@global-inverse-color, 80%);
@inverse-global-muted-color: fade(@global-inverse-color, 60%); @inverse-global-muted-color: fade(@global-inverse-color, 60%);
//
// Paper
//
@paper-border-radius: 4px;
/* ======================================================================== /* ========================================================================
@ -134,103 +143,14 @@
@sidebar-right-left-xl: 60px; @sidebar-right-left-xl: 60px;
/* HTML
========================================================================== */
/* /* UIkit modifiers
* 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
========================================================================== */ ========================================================================== */
/* /*
* Navbar * 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 // Line Mode
@navbar-nav-item-line-margin-vertical: 20px; @navbar-nav-item-line-margin-vertical: 20px;
@navbar-nav-item-line-margin-horizontal: @navbar-nav-item-padding-horizontal; @navbar-nav-item-line-margin-horizontal: @navbar-nav-item-padding-horizontal;
@ -238,271 +158,115 @@ body { overflow: hidden; }
@navbar-nav-item-line-background: currentColor; @navbar-nav-item-line-background: currentColor;
@navbar-nav-item-line-transition-duration: 0.3s; @navbar-nav-item-line-transition-duration: 0.3s;
.tm-navbar-container .uk-navbar-nav > li > a { .uk-navbar-dropdown.uk-open {
position: relative; border-radius: @paper-border-radius;
&::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;
} }
/* /*
* Subnav * Cards
*/ */
/* Tablet landscape and bigger */ .uk-card {
@media (min-width: @breakpoint-medium) { border-radius: @paper-border-radius;
.tm-subnav { margin-left: -50px; }
.tm-subnav > * { padding-left: 50px; }
} }
.tm-subnav > * > :first-child { text-transform: capitalize; } .uk-card-media-top img {
border-radius: @paper-border-radius @paper-border-radius 0 0;
/*
* Label (Changelog()
*/
.tm-label-changelog {
width: 80px;
margin-top: 3px;
} }
/* .uk-card-default .uk-card-footer {
* Button border-top: 1px solid rgba(180, 180, 180, 0.25);
*/ }
.uk-button {
border-radius: 2px;
}
.tm-button-default, .uk-card-primary .uk-card-footer {
.tm-button-primary { border-radius: 500px; } border-top: 1px solid rgba(0, 0, 0, 0.075);
}
.tm-button-default {}
.tm-button-large { line-height: 48px; }
.tm-button-primary {}
.hook-inverse() { .hook-inverse() {
// Override background colour in dark mode
.tm-button-default { .uk-card-default {
color: @inverse-global-color; background-color: #2a2a2a !important;
border-color: @inverse-global-muted-color;
} }
.tm-button-primary { // Lighten on hover to show depth in dark mode
background: #fff !important; .uk-card-default:hover {
color: @global-primary-background !important; 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 * Modal
*/ */
.uk-modal-dialoge {
.tm-modal-dialog { background: @global-muted-background; } border-radius: @paper-border-radius;
box-shadow: 0 14px 25px rgba(0, 0, 0, 0.16);
.tm-modal-header { padding: 20px 30px; } transition: 0.3s ease !important;
.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; }
} }
/* 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
*/ */
.uk-notification-message {
.tm-heading-fragment > a { border-radius: @paper-border-radius;
color: inherit; box-shadow: 0 14px 25px rgba(0, 0, 0, 0.16);
text-decoration: none;
} }
/* Phone landscape and bigger */ .uk-notification-message-danger {
@media (min-width: @breakpoint-small) { border: 1px solid @notification-message-danger-color
}
h2.tm-heading-fragment > a:before { .uk-notification-message-warning {
content: "#"; border: 1px solid @notification-message-warning-color
position: absolute; }
width: 25px;
margin-left: -25px; .uk-notification-message-success {
color: @global-muted-color; 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;
} }
} .uk-button-primary:hover {
background-color: @inverse-primary-muted-color;
color: #fff;
/* 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;
} }
} }
/* Tablet landscape and bigger */ .uk-icon-button, .uk-icon-button:hover {
@media (min-width: @breakpoint-medium) { text-decoration: none
}
.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; }
}

View file

@ -42,20 +42,7 @@
<a class="uk-accordion-title" href="#">Metadata</a> <a class="uk-accordion-title" href="#">Metadata</a>
<div class="uk-accordion-content"> <div class="uk-accordion-content">
<form @submit.prevent="handleMetadataSubmit"> <keyvalList v-model="metadata"/>
<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>
</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 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>
</div>
</div> </div>
</li> </li>
@ -63,16 +50,7 @@
<li> <li>
<a class="uk-accordion-title" href="#">Tags</a> <a class="uk-accordion-title" href="#">Tags</a>
<div class="uk-accordion-content"> <div class="uk-accordion-content">
<tagList v-model="tags"/>
<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>
</form>
<span v-for="tag in tags" :key="tag" v-on:click="delTag(tag)" class="uk-label uk-margin-small-right deletable-label"> {{ tag }} </span>
</div> </div>
</li> </li>
@ -176,10 +154,19 @@
<script> <script>
import axios from 'axios' import axios from 'axios'
import tagList from "../fieldComponents/tagList"
import keyvalList from "../fieldComponents/keyvalList"
// Export main app // Export main app
export default { export default {
name: 'paneCapture', name: 'paneCapture',
components: {
tagList,
keyvalList
},
data: function () { data: function () {
return { return {
filename: '', filename: '',
@ -203,42 +190,14 @@ export default {
z: 5 z: 5
}, },
resizeDims: [640, 480], resizeDims: [640, 480],
newTag: "",
tags: [], tags: [],
customMetadata: { metadata: {
Client: "openflexure.vue" Client: `${process.env.PACKAGE.name}.${process.env.PACKAGE.version}`
},
newMetadata: {
key: "",
value: ""
} }
} }
}, },
methods: { methods: {
handleMetadataSubmit: function () {
console.log("Adding metadata");
this.customMetadata[this.newMetadata.key] = this.newMetadata.value
this.newMetadata.key = "";
this.newMetadata.value = "";
},
delMetadataKey: function (key) {
this.$delete(this.customMetadata, key)
},
handleTagSubmit: function () {
this.tags.push(this.newTag)
this.newTag = "";
},
delTag: function (tag) {
console.log(tag)
var index = this.tags.indexOf(tag);
if (index > -1) {
this.tags.splice(index, 1);
}
},
handleCapture: function() { handleCapture: function() {
var payload = this.basePayload var payload = this.basePayload
@ -336,7 +295,7 @@ export default {
} }
// Additional metadata // Additional metadata
payload.metadata = this.customMetadata payload.metadata = this.metadata
payload.tags = this.tags payload.tags = this.tags
// Attach notes // Attach notes

View file

@ -2,8 +2,6 @@
<div class="host-input"> <div class="host-input">
<div class="uk-margin"> <div class="uk-margin">
<h3>Connect</h3>
<form @submit.prevent="handleSubmit"> <form @submit.prevent="handleSubmit">
<div class="uk-form-controls uk-margin"> <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> <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 v-if="!localMode">
<div class="uk-inline uk-width-1-1"> <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"> <input v-model="hostname" v-bind:class="IpFormClasses" class="uk-input uk-form-small" type="text" placeholder="Hostname or IP address">
</div> </div>
</div> </div>
@ -23,10 +21,11 @@
<div class="uk-accordion-content"> <div class="uk-accordion-content">
<label class="uk-form-label" for="form-stacked-text">Port</label> <label class="uk-form-label" for="form-stacked-text">Port</label>
<div class="uk-form-controls"> <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>
</div> </div>
</li> </li>
<li> <li>
<a class="uk-accordion-title" href="#">Status</a> <a class="uk-accordion-title" href="#">Status</a>
<div class="uk-accordion-content"> <div class="uk-accordion-content">
@ -35,29 +34,35 @@
<p><b>Base URI:</b> {{ $store.getters.uri }}</p> <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> <p v-if="$store.state.apiConfig.name"><b>Device name:</b> {{ $store.state.apiConfig.name }}</p>
</div> </div>
<div v-else-if="$store.state.waiting"><div uk-spinner></div></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-if="$store.state.error"><b>Error:</b> {{ $store.state.error }}</div>
<div v-else>No active connection</div> <div v-else>No active connection</div>
</div> </div>
</li> </li>
<li v-if="!localMode" class="uk-open"> <li v-if="!localMode" class="uk-open">
<a class="uk-accordion-title" href="#">Saved hosts</a> <a class="uk-accordion-title" href="#">Saved hosts</a>
<div class="uk-accordion-content"> <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> <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-flex uk-flex-middle">
<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-link uk-padding-remove uk-width-expand host-description"><b>{{ host.name }}</b> ({{ host.hostname }}:{{ host.port }})</a>
<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 uk-width-auto host-delete"><i class="material-icons">delete</i></a>
<a href="#" v-on:click="delSavedHost(host)" class="uk-icon-link uk-width-auto" uk-icon="trash"></a>
</div> </div>
</div> </div>
</li> </li>
</ul> </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> <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>
</form> </form>
<button
v-if="$store.getters.ready"
v-on:click="$store.commit('resetState')"
class="uk-button uk-button-danger uk-form-small uk-float-right uk-margin uk-margin-remove-top uk-width-1-1">
Disconnect
</button>
</div> </div>
</div> </div>
@ -122,6 +127,29 @@ export default {
this.$store.dispatch('firstConnect') this.$store.dispatch('firstConnect')
.then (() => { .then (() => {
console.log("Connected!") console.log("Connected!")
this.checkServerVersion()
})
.catch(error => {
this.modalError(error) // Let mixin handle error
})
},
checkServerVersion: function () {
this.$store.dispatch('updateState')
.then (() => {
var clientVersion = process.env.PACKAGE.version
var clientVersionMajor = clientVersion.substring(0, 3)
console.log(clientVersionMajor)
var serverVersion = this.$store.state.apiState.version
if (serverVersion != undefined) {
var serverVersionMajor = serverVersion.substring(0, 3)
}
console.log(serverVersionMajor)
if ((serverVersion == undefined) || (serverVersionMajor != clientVersionMajor)) {
this.modalDialog("Version mismatch", "Client and microscope versions do not match. Consider updating your microscope software. Some functionality may currently be broken.", status='warning')
}
}) })
.catch(error => { .catch(error => {
this.modalError(error) // Let mixin handle error this.modalError(error) // Let mixin handle error
@ -132,7 +160,7 @@ export default {
this.$store.commit("changeSetting", ['trackWindow', state]); this.$store.commit("changeSetting", ['trackWindow', state]);
this.$store.commit("changeSetting", ['disableStream', state]); this.$store.commit("changeSetting", ['disableStream', state]);
this.$store.commit("changeSetting", ['autoGpuPreview', state]); this.$store.commit("changeSetting", ['autoGpuPreview', state]);
this.$root.$emit('globalTogglePreview', state) //this.$root.$emit('globalTogglePreview', state)
}, },
autofillHost: function (host) { autofillHost: function (host) {
@ -217,4 +245,14 @@ export default {
.host-input { .host-input {
text-align: left; text-align: left;
} }
.host-description {
text-overflow: ellipsis;
overflow: hidden;
}
.host-delete {
padding: 0 2px 0 10px;
}
</style> </style>

View file

@ -64,26 +64,23 @@
<li class="uk-open"> <li class="uk-open">
<a class="uk-accordion-title" href="#">Autofocus</a> <a class="uk-accordion-title" href="#">Autofocus</a>
<div class="uk-text-center uk-container" v-if="isAutofocusing"> <div class="uk-accordion-content">
<div class="center-spinner" uk-spinner></div>
</div>
<div class="uk-accordion-content" v-else>
<div class="uk-grid-small uk-child-width-1-3" uk-grid> <div v-if="isAutofocusing">
<progressBar/>
<div> </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 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>
<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> </div>
</li> </li>
@ -94,6 +91,7 @@
<script> <script>
import axios from 'axios' import axios from 'axios'
import progressBar from "../genericComponents/progressBar"
// Key Codes // Key Codes
const keyCodes = { const keyCodes = {
@ -111,6 +109,10 @@ const keyCodes = {
export default { export default {
name: 'paneNavigate', name: 'paneNavigate',
components: {
progressBar
},
data: function () { data: function () {
return { return {
keysDown: {}, keysDown: {},

View file

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

View file

@ -4,30 +4,31 @@
<div> <div>
<label class="uk-form-label" for="form-stacked-text">Exposure time</label> <label class="uk-form-label" for="form-stacked-text">Exposure time</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
<input v-model="shutterSpeed" class="uk-input uk-form-small" type="number"> <input v-model="displayShutterSpeed" class="uk-input uk-form-small" type="number">
</div> </div>
</div> </div>
<div> <div>
<label class="uk-form-label" for="form-stacked-text">Analogue gain</label> <label class="uk-form-label" for="form-stacked-text">Analogue gain</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
<input v-model="analogGain" class="uk-input uk-form-small" type="number"> <input v-model="displayAnalogGain" class="uk-input uk-form-small" type="number" step="0.01">
</div> </div>
</div> </div>
<div> <div>
<label class="uk-form-label" for="form-stacked-text">Digital gain</label> <label class="uk-form-label" for="form-stacked-text">Digital gain</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
<input v-model="digitalGain" class="uk-input uk-form-small" type="number"> <input v-model="displayDigitalGain" class="uk-input uk-form-small" type="number" step="0.01">
</div> </div>
</div> </div>
<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> <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 v-if="isCalibrating">
<div class="center-spinner" uk-spinner></div> <progressBar/>
</div> </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> <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> </div>
@ -40,35 +41,42 @@
<script> <script>
import axios from 'axios' import axios from 'axios'
import progressBar from "../../genericComponents/progressBar"
// Export main app // Export main app
export default { export default {
name: 'microscopeSettings', name: 'microscopeSettings',
components: {
progressBar
},
data: function () { data: function () {
return { return {
shutterSpeed: this.$store.state.apiConfig.picamera_settings.shutter_speed, shutterSpeed: this.$store.state.apiConfig.camera_settings.picamera_settings.shutter_speed,
analogGain: this.$store.state.apiConfig.picamera_settings.analog_gain, analogGain: this.$store.state.apiConfig.camera_settings.picamera_settings.analog_gain,
digitalGain: this.$store.state.apiConfig.picamera_settings.digital_gain, digitalGain: this.$store.state.apiConfig.camera_settings.picamera_settings.digital_gain,
isCalibrating: false isCalibrating: false
} }
}, },
methods: { methods: {
updateInputValues: function () { updateInputValues: function () {
this.shutterSpeed = this.$store.state.apiConfig.picamera_settings.shutter_speed this.shutterSpeed = this.$store.state.apiConfig.camera_settings.picamera_settings.shutter_speed
this.digitalGain = this.$store.state.apiConfig.picamera_settings.digital_gain this.digitalGain = this.$store.state.apiConfig.camera_settings.picamera_settings.digital_gain
this.analogGain = this.$store.state.apiConfig.picamera_settings.analog_gain this.analogGain = this.$store.state.apiConfig.camera_settings.picamera_settings.analog_gain
}, },
applyConfigRequest: function() { applyConfigRequest: function() {
console.log("Applying config to the microscope") console.log("Applying config to the microscope")
var payload = {picamera_settings:{}} var payload = {
picamera_settings: {}
}
//if (this.shutterSpeed != this.$store.state.apiConfig.picamera_settings.shutter_speed) { //if (this.shutterSpeed != this.$store.state.apiConfig.picamera_settings.shutter_speed) {
payload.picamera_settings.shutter_speed = this.shutterSpeed payload.camera_settings.picamera_settings.shutter_speed = this.shutterSpeed
payload.picamera_settings.analog_gain = this.analogGain payload.camera_settings.picamera_settings.analog_gain = this.analogGain
payload.picamera_settings.digital_gain = this.digitalGain payload.camera_settings.picamera_settings.digital_gain = this.digitalGain
//}; //};
// Send request // Send request
@ -119,6 +127,15 @@ export default {
}, },
computed: { computed: {
displayDigitalGain: function () {
return Number(this.digitalGain).toFixed(2)
},
displayAnalogGain: function () {
return Number(this.analogGain).toFixed(2)
},
displayShutterSpeed: function () {
return (this.shutterSpeed != 0) ? this.shutterSpeed : "auto"
},
recalibrateApiUri: function () { recalibrateApiUri: function () {
return this.$store.getters.uri + "/plugin/default/camera_calibration/recalibrate" return this.$store.getters.uri + "/plugin/default/camera_calibration/recalibrate"
}, },

View file

@ -52,27 +52,31 @@ export default {
data: function () { data: function () {
return { return {
microscopeName: this.$store.state.apiConfig.name, microscopeName: this.$store.state.apiConfig.name,
stageBacklash: this.$store.state.apiConfig.backlash stageBacklash: this.$store.state.apiConfig.stage_settings.backlash
} }
}, },
methods: { methods: {
updateInputValues: function () { updateInputValues: function () {
this.microscopeName = this.$store.state.apiConfig.name; this.microscopeName = this.$store.state.apiConfig.name;
this.stageBacklash = this.$store.state.apiConfig.backlash this.stageBacklash = this.$store.state.apiConfig.stage_settings.backlash
}, },
applyConfigRequest: function() { applyConfigRequest: function() {
var payload = {} var payload = {
stage_settings: {}
}
if (this.microscopeName != this.$store.state.apiConfig.name) { if (this.microscopeName != this.$store.state.apiConfig.name) {
payload.name = this.microscopeName payload.name = this.microscopeName
}; };
if (this.stageBacklash != this.$store.state.apiConfig.backlash) { if (this.stageBacklash != this.$store.state.apiConfig.stage_settings.backlash) {
payload.backlash = this.stageBacklash payload.stage_settings.backlash = this.stageBacklash
} }
console.log(payload)
// Send request to update config // Send request to update config
axios.post(this.configApiUri, payload) axios.post(this.configApiUri, payload)
.then(response => { .then(response => {

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,84 @@
<template>
<div>
<form @submit.prevent="handleMetadataSubmit">
<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="(val, key) in value" :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">
<labelInput :label="key" :value="value[key]" @input="value[key]=$event"/>
</div>
<a href="#" v-on:click="delMetadataKey(key)" class="uk-icon uk-width-auto"><i class="material-icons">delete</i></a>
</div>
</div>
</template>
<script>
import labelInput from "../fieldComponents/labelInput"
export default {
name: 'keyvalList',
components: {
labelInput
},
data: function () {
return {
newMetadata: {
key: "",
value: ""
}
}
},
props: [
'value'
],
methods: {
handleMetadataSubmit: function () {
var newSelected = {}
if (this.value != null) {
Object.assign(newSelected, this.value)
}
newSelected[this.newMetadata.key] = this.newMetadata.value
this.newMetadata.key = "";
this.newMetadata.value = "";
this.$emit('input', newSelected)
},
delMetadataKey: function (key) {
var newSelected = {}
if (this.value != null) {
Object.assign(newSelected, this.value)
}
this.$delete(newSelected, key)
this.$emit('input', newSelected)
},
modifyValue: function (e, v) {
console.log(e)
console.log(v)
}
}
}
</script>
<style scoped></style>

View file

@ -0,0 +1,58 @@
<template>
<div>
<label class="uk-form-label uk-text-bold">{{label}}</label>
<div @click="setEditing(true)" uk-tooltip="title: Click to edit value; delay: 250">
<div v-show="editing == false">
<label> {{value}} </label>
</div>
<input
v-show="editing == true"
class="uk-input uk-form-small"
type="text"
v-bind:name="name"
v-bind:value="value"
ref="textinput"
v-on:blur= "setEditing(false)"
@keyup.enter = "setEditing(false)"
@input="$emit('input', $event.target.value)"
autofocus
>
</div>
</div>
</template>
<script>
export default {
name: 'labelInput',
data: function () {
return {
editing: false
}
},
props: [
'name',
'label',
'value'
],
methods: {
setEditing(editing) {
this.editing = editing
if (editing == true) {
this.$nextTick(() => this.$refs.textinput.focus())
}
}
},
}
</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,63 @@
<template>
<div>
<form @submit.prevent="handleTagSubmit">
<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>
<span v-for="tag in value" :key="tag" v-on:click="delTag(tag)" class="uk-label uk-margin-small-right deletable-label"> {{ tag }} </span>
</div>
</template>
<script>
export default {
name: 'tagList',
data: function () {
return {
newTag: ""
}
},
props: [
'value'
],
methods: {
handleTagSubmit: function () {
var newSelected = this.value != null ? [...this.value] : [] // Clone value array
newSelected.push(this.newTag)
this.newTag = "";
this.$emit('input', newSelected)
},
delTag: function (tag) {
var newSelected = this.value != null ? [...this.value] : [] // Clone value array
if (newSelected.includes(tag)) {
var newSelected = newSelected.filter(function(value, index, arr){
return value != tag;
})
}
this.$emit('input', newSelected)
}
}
}
</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

@ -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> <template>
<div id="panelLeft" class="uk-flex uk-flex-column uk-margin-remove uk-padding-remove uk-height-1-1"> <div id="panel-left" class="uk-margin-remove uk-padding-remove uk-height-1-1" uk-grid>
<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="Connect"></a></li> <!-- Vertical tab bar -->
<li v-bind:class="disableIfDisconnected"><a href="#" uk-switcher-item="navigate" uk-icon="location" uk-tooltip="Navigate"></a></li> <div id="switcher-left" class="uk-flex uk-flex-column uk-padding-remove uk-width-auto uk-height-1-1">
<li v-bind:class="disableIfDisconnected"><a href="#" uk-switcher-item="capture" uk-icon="camera" uk-tooltip="Capture"></a></li> <tabIcon id="connect" :requireConnection="false" :currentTab="currentTab" @set-tab="setTab">
<li v-bind:class="disableIfDisconnected"><a href="#" uk-switcher-item="plugins" uk-icon="git-fork" uk-tooltip="Plugins"></a></li> <i class="material-icons">settings_ethernet</i>
<li><a href="#" uk-switcher-item="settings" uk-icon="settings" uk-tooltip="Settings"></a></li> </tabIcon>
</ul> <tabIcon id="navigate" :requireConnection="true" :currentTab="currentTab" @set-tab="setTab">
<ul class="uk-switcher uk-padding-small uk-flex uk-flex-1 panel-content"> <i class="material-icons">gamepad</i>
<li class="uk-width-expand"><paneConnect/></li> </tabIcon>
<li class="uk-width-expand"><div v-if="$store.getters.ready"><paneNavigate/></div></li> <tabIcon id="capture" :requireConnection="true" :currentTab="currentTab" @set-tab="setTab">
<li class="uk-width-expand"><div v-if="$store.getters.ready"><paneCapture/></div></li> <i class="material-icons">camera_alt</i>
<li class="uk-width-expand"><div v-if="$store.getters.ready"><panePlugins/></div></li> </tabIcon>
<li class="uk-width-expand"><paneSettings/></li> <tabIcon id="settings" :requireConnection="false" :currentTab="currentTab" @set-tab="setTab">
</ul> <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> </div>
</template> </template>
<script> <script>
// Import components // Import axios for HTTP requests
import paneConnect from './paneConnect.vue' import axios from 'axios'
import paneNavigate from './paneNavigate.vue'
import paneCapture from './paneCapture.vue' // Import generic components
import panePlugins from './panePlugins.vue' import tabIcon from './genericComponents/tabIcon'
import paneSettings from './paneSettings.vue' 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/JsonForm'
// Export main app // Export main app
export default { export default {
name: 'panelLeft', name: 'panelLeft',
components: { components: {
tabIcon,
tabContent,
paneConnect, paneConnect,
paneNavigate, paneNavigate,
paneCapture, 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: { computed: {
disableIfDisconnected: function () { pluginApiUri: function () {
return { return this.$store.getters.uri + "/plugin"
'uk-disabled': !this.$store.getters.ready },
}
}
} }
} }
</script> </script>
<style lang="less"> <style scoped lang="less">
.uk-tab {
padding-left: 0; #component-left {
}
.panel-content {
width: 300px; 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> </style>

View file

@ -1,30 +1,34 @@
<template> <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"> <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="Live"></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" uk-icon="image" uk-tooltip="Captures"></a></li> <li v-bind:class="{'uk-disabled': !this.$store.getters.ready}"><a href="#" uk-switcher-item="gallery">Gallery</a></li>
</ul> </ul>
<ul class="uk-switcher uk-flex uk-flex-1"> <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 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 uk-overflow-auto" id="galleryDisplayTab"><galleryDisplay/></li>
</ul> </ul>
</div> </div>
</template> </template>
<script> <script>
// Import axios for HTTP requests
import axios from 'axios'
// Import basic UIkit
import UIkit from 'uikit'; import UIkit from 'uikit';
// Import components // Import components
import streamDisplay from './paneDisplayComponents/streamDisplay.vue' import streamDisplay from './viewComponents/streamDisplay.vue'
import galleryDisplay from './paneDisplayComponents/galleryDisplay.vue' import galleryDisplay from './viewComponents/galleryDisplay.vue'
// Export main app // Export main app
export default { export default {
name: 'panelDisplay', name: 'panelRight',
components: { components: {
streamDisplay, streamDisplay,
galleryDisplay galleryDisplay,
}, },
mounted() { mounted() {
@ -54,4 +58,9 @@ export default {
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.uk-tab {
padding-left: 0;
}
</style> </style>

View file

@ -0,0 +1,233 @@
<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 tagList from "../fieldComponents/tagList"
import keyvalList from "../fieldComponents/keyvalList"
import progressBar from "../genericComponents/progressBar"
export default {
name: 'JsonForm',
components: {
numberInput,
selectList,
textInput,
htmlBlock,
radioList,
checkList,
tagList,
keyvalList,
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

@ -1,5 +1,5 @@
<template> <template>
<div class="captureCard uk-card uk-card-default uk-card-hover uk-padding-remove uk-width-medium uk-margin-right" v-bind:class="{ 'uk-card-secondary': $store.state.globalSettings.darkMode }"> <div class="captureCard uk-card uk-card-default uk-card-hover uk-padding-remove uk-width-medium" v-bind:class="{ 'uk-card-secondary': $store.state.globalSettings.darkMode }">
<div class="uk-card-media-top"> <div class="uk-card-media-top">
@ -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-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-expand">{{ metadata.filename }}</div>
<div class="uk-margin-remove-top uk-padding-remove uk-width-auto"> <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>
</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"> <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"> <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"> <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> <button class="uk-button uk-button-default uk-margin-left uk-form-small uk-modal-close" type="button">Cancel</button>

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="captureCard uk-card uk-card-primary uk-card-hover uk-padding-remove uk-width-medium uk-margin-right"> <div class="captureCard uk-card uk-card-primary uk-card-hover uk-padding-remove uk-width-medium">
<div class="uk-card-media-top"> <div class="uk-card-media-top">

View file

@ -4,8 +4,8 @@
<nav class="uk-navbar-container uk-navbar-transparent navbar" uk-navbar="mode: click"> <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"> <div class="uk-navbar-left uk-padding-remove-top uk-padding-remove-bottom">
<ul class="uk-navbar-nav"> <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=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-link" href="#" uk-icon="icon: arrow-up"></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> <li>
<a href="#">Filter</a> <a href="#">Filter</a>
<div class="uk-navbar-dropdown" v-bind:class="{ 'uk-light uk-background-secondary': $store.state.globalSettings.darkMode }"> <div class="uk-navbar-dropdown" v-bind:class="{ 'uk-light uk-background-secondary': $store.state.globalSettings.darkMode }">
@ -25,14 +25,16 @@
<div v-if="$store.getters.ready" class="uk-padding-remove-top" uk-lightbox="toggle: .lightbox-link"> <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"> <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-button" uk-icon="arrow-left"></a> <a href="#" v-on:click="galleryFolder=''" class="uk-icon uk-margin-remove"><i class="material-icons">arrow_back</i></a>
<h3 class="uk-inline uk-float-right uk-margin-remove"><b>SCAN</b> {{ allScans[galleryFolder].metadata.filename }}</h3> <div class="uk-margin-left">
<h3 class="uk-margin-remove uk-margin-left"><b>SCAN</b> {{ allScans[galleryFolder].metadata.filename }}</h3>
</div>
</div> </div>
<div class="uk-grid-medium uk-padding uk-padding-remove-right uk-grid-match" uk-grid> <div class="uk-grid-medium uk-grid-match uk-margin-top" uk-grid>
<div v-for="item in sortedItems" :key="item.metadata.id" class="uk-remove-padding-remove-left" > <div v-for="item in sortedItems" :key="item.metadata.id">
<scanCard <scanCard
v-if="'isScan' in item" v-if="'isScan' in item"
:metadata="item.metadata" :metadata="item.metadata"

View file

@ -1,5 +1,5 @@
<template> <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"> <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 main app
export default { export default {
name: 'streamDisplay', name: 'stream-display',
data: function () { data: function () {
return { return {
displaySize: [0, 0], displaySize: [0, 0],
displayPosition: [0, 0], displayPosition: [0, 0],
GpuPreviewActive: false, GpuPreviewActive: false,
resizeTeimoutId: setTimeout(this.doneResizing, 500) resizeTimeoutId: setTimeout(this.doneResizing, 500)
} }
}, },
mounted() { 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 // A global signal listener to change the GPU preview state
this.$root.$on('globalTogglePreview', (state) => { this.$root.$on('globalTogglePreview', (state) => {
console.log(`Toggling preview to ${state}`) console.log(`Toggling preview to ${state}`)
this.previewRequest(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 () { created: function () {
// Add resize listener
window.addEventListener('resize', this.handleResize);
// Watch for host 'ready' // Watch for host 'ready'
this.$store.watch( this.$store.watch(
(state)=>{ (state)=>{
@ -57,29 +61,27 @@ export default {
}, },
(newValue, oldValue)=>{ (newValue, oldValue)=>{
// 'ready' changed, so do something // 'ready' changed, so do something
console.log(oldValue)
console.log(newValue)
this.previewRequest(this.$store.state.globalSettings.autoGpuPreview) this.previewRequest(this.$store.state.globalSettings.autoGpuPreview)
} }
) )
}, },
beforeDestroy: function () { beforeDestroy: function () {
// Remove resize listener // Disconnect the size observer
window.removeEventListener('resize', this.handleResize) this.sizeObserver.disconnect()
}, },
methods: { methods: {
clickmonitor: function(event) { clickmonitor: function(event) {
// Calculate steps from event coordinates and store config FOV // Calculate steps from event coordinates and store config FOV
var xCoordinate = event.offsetX; let xCoordinate = event.offsetX;
var yCoordinate = event.offsetY; let yCoordinate = event.offsetY;
var xRelative = (0.5*event.target.offsetWidth - xCoordinate)/event.target.offsetWidth; let xRelative = (0.5*event.target.offsetWidth - xCoordinate)/event.target.offsetWidth;
var yRelative = (0.5*event.target.offsetHeight - yCoordinate)/event.target.offsetHeight; let yRelative = (0.5*event.target.offsetHeight - yCoordinate)/event.target.offsetHeight;
var xSteps = xRelative * this.$store.state.apiConfig.fov[0]; let xSteps = xRelative * this.$store.state.apiConfig.fov[0];
var ySteps = yRelative * this.$store.state.apiConfig.fov[1]; let ySteps = yRelative * this.$store.state.apiConfig.fov[1];
// Emit a signal to move, acted on by panelNavigate.vue // Emit a signal to move, acted on by panelNavigate.vue
this.$root.$emit('globalMoveEvent', xSteps, ySteps, 0, false) this.$root.$emit('globalMoveEvent', xSteps, ySteps, 0, false)
@ -87,12 +89,13 @@ export default {
handleResize: function(event) { handleResize: function(event) {
// Only fires resize event after no resize in 500ms (prevents resize event spam) // Only fires resize event after no resize in 500ms (prevents resize event spam)
clearTimeout(this.resizeTeimoutId); clearTimeout(this.resizeTimeoutId);
this.resizeTeimoutId = setTimeout(this.handleDoneResize, 500) this.resizeTimeoutId = setTimeout(this.handleDoneResize, 250)
}, },
handleDoneResize: function() { handleDoneResize: function() {
// Recalculate size // Recalculate size
console.log("Recalculating frame size")
this.recalculateSize(); this.recalculateSize();
if (this.$store.state.globalSettings.autoGpuPreview == true && this.GpuPreviewActive == true) { if (this.$store.state.globalSettings.autoGpuPreview == true && this.GpuPreviewActive == true) {
// Reload preview // Reload preview
@ -101,23 +104,24 @@ export default {
}, },
recalculateSize: function () { recalculateSize: function () {
console.log("Recalculating window dimensions...") let element = this.$refs.streamDisplay.parentNode;
// Stacking parentNode is a hacky fix let bound = element.getBoundingClientRect()
// 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 size = [element.clientWidth, element.clientHeight]; let elementSize = [bound.width, bound.height]
let elem_pos = [element.getBoundingClientRect().left, element.getBoundingClientRect().top]; let elementPositionOnWindow = [bound.left, bound.top]
let wind_pos = [window.screenX, window.screenY]; let windowPositionOnDisplay = [window.screenX, window.screenY]
let navHeight = window.outerHeight - window.innerHeight; let windowChromeHeight = 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 elementPositionOnDisplay = [
Math.max(0, windowPositionOnDisplay[0] + elementPositionOnWindow[0]),
Math.max(0, windowPositionOnDisplay[1] + elementPositionOnWindow[1] + windowChromeHeight)
]
this.displaySize = size; this.displaySize = elementSize
this.displayPosition = position; this.displayPosition = elementPositionOnDisplay
console.log(`Size: ${this.displaySize}`)
console.log(`Position: ${this.displayPosition}`)
}, },
previewRequest: function(state) { previewRequest: function(state) {
@ -147,7 +151,7 @@ export default {
} }
} }
else { else {
var letpayload = {} var payload = {}
} }
// Send preview request // Send preview request
@ -182,13 +186,13 @@ export default {
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.streamDisplay img { .stream-display img {
height: 100%; height: 100%;
text-align: center; text-align: center;
object-fit: contain object-fit: contain
} }
.streamDisplay { .stream-display {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

15
src/main.app.js Normal file
View file

@ -0,0 +1,15 @@
// Load settings store from main process
const { store } = require('electron').remote.require('./store')
// Load extra modules we'll be using
const { Titlebar, Color } = require('custom-electron-titlebar');
// Only show custom menubar if current window has no frame
if (store.get('drawCustomTitleBar') == true) {
new Titlebar({
backgroundColor: Color.fromHex('#c5247f'),
icon: './titleicon.svg'
});
}
console.log("Loaded main.app.js for electron-renderer functionality")

View file

@ -3,9 +3,9 @@ import App from './App.vue'
import store from './store' import store from './store'
import UIkit from 'uikit'; import UIkit from 'uikit';
// Import UIkit icon set
import Icons from 'uikit/dist/js/uikit-icons'; // Import MD icons
UIkit.use(Icons); import 'material-design-icons/iconfont/material-icons.css'
Vue.config.productionTip = false Vue.config.productionTip = false
@ -36,8 +36,20 @@ Vue.mixin({
return new Promise(showModal) return new Promise(showModal)
}, },
modalNotify: function(message) { modalNotify: function(message, status = 'success') {
UIkit.notification({message: message, status: 'success'}) UIkit.notification({message: message, status: status})
},
modalDialog: function(title, message) {
UIkit.modal.dialog(`
<button class="uk-modal-close-default" type="button" uk-close></button>
<div class="uk-modal-header">
<h2 class="uk-modal-title">${title}</h2>
</div>
<div class="uk-modal-body">
<p>${message}</p>
</div>
`);
}, },
modalError: function(error) { modalError: function(error) {
@ -66,7 +78,7 @@ Vue.mixin({
console.log(errormsg) console.log(errormsg)
} }
this.$store.commit('setError', 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) { getLocalStorageObj: function(keyName) {

View file

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

View file

@ -1,3 +1,24 @@
var webpack = require('webpack');
module.exports = { module.exports = {
configureWebpack: config => {
config.target = process.env.VUE_APP_TARGET || 'web',
config.entry = (process.env.VUE_APP_TARGET == 'electron-renderer')
? {
main: './src/main.js',
app: './src/main.app.js'
}
:{
main: './src/main.js',
},
config.plugins.push(
new webpack.DefinePlugin({
'process.env': {
PACKAGE: JSON.stringify(require('./package.json'))
}
})
)
},
outputDir: (process.env.VUE_APP_TARGET == 'electron-renderer') ? './app/dist' : './dist',
publicPath: '' publicPath: ''
} }