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
node_modules
/dist
/app/dist
/release-builds
/installers/release-builds

View file

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

View file

@ -6,6 +6,9 @@ const autoUpdater = updater.autoUpdater;
const contextMenu = require('electron-context-menu')
const path = require('path')
// Attach settings store
const { store } = require('./store')
// Auto upadater
autoUpdater.autoDownload = false;
@ -14,9 +17,7 @@ autoUpdater.setFeedURL({
url: "https://gitlab.com/openflexure/openflexure-microscope-jsclient/-/jobs/artifacts/stable/raw?job=package"
});
autoUpdater.on('checking-for-update', function () {
sendStatusToWindow('Checking for update...');
});
autoUpdater.on('checking-for-update', function () {});
autoUpdater.on('update-available', function (info) {
sendStatusToWindow('Update available.' + info)
@ -70,56 +71,76 @@ const app = electron.app
const BrowserWindow = electron.BrowserWindow
// Set the window content
let url = `file://${path.join(__dirname, '../dist/index.html')}`
let url = `file://${path.join(__dirname, '/dist/index.html')}`
let mainWindow
// Set the application menu
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() {
// Create the browser window.
mainWindow = new BrowserWindow({
frame: !store.get('drawCustomTitleBar'),
width: 1124,
height: 800,
icon: path.join(__dirname, '/icons/png/64x64.png')
})
// Make a context menu
contextMenu({
showCopyImageAddress: true,
showSaveImageAs: true,
showInspectElement: false,
});
// Load window contents
mainWindow.loadURL(url)
// Check for updates
autoUpdater.checkForUpdates();
// Emitted when the window is closed.
mainWindow.on('closed', function() {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
mainWindow = null // Dereference the window object
})
}
// 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.
app.on('ready', createWindow)
app.on('ready', () => {
createWindow()
})
// Quit when all windows are closed.
app.on('window-all-closed', function() {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
if ((process.platform !== 'darwin') && recreatingWindowInProgress != true) {
app.quit()
}
})
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) {
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
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"
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 autoUpdater = updater.autoUpdater;
const path = require('path')
const openAboutWindow = require('about-window').default
const main = require('./app')
const { store } = require('./store')
const template = [
{
label: 'Edit',
@ -26,7 +29,16 @@ const template = [
{ role: 'forcereload' },
{ role: 'toggledevtools' },
{ 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',
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",
"version": "0.2.6",
"version": "1.1.0",
"private": true,
"description": "An electron-based user client for the OpenFlexure Microscope Server",
"author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)",
@ -11,21 +11,24 @@
"license": "GNU General Public License v3.0 ",
"main": "./app/app.js",
"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:raspi": "electron-builder --linux --config app/builder-config-raspi.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:appx": "electron-builder --win --config app/builder-config-appx.yaml",
"build": "vue-cli-service build",
"electron:run": "electron .",
"electron:dev": "electron app/app.dev.js",
"serve": "vue-cli-service serve"
"release": "electron-builder --linux --config app/builder-config-raspi.yaml & electron-builder --win --linux --config app/builder-config-x86_64.yaml"
},
"dependencies": {
"about-window": "^1.12.1",
"custom-electron-titlebar": "^3.0.8",
"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": {
"@vue/cli-plugin-babel": "^3.3.0",
@ -33,10 +36,10 @@
"axios": "^0.18.0",
"css-loader": "^2.1.1",
"electron": "^4.1.4",
"electron-builder": "^20.39.0",
"electron-builder": "^21.0.1",
"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"

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>
<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>

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
@import "./highlight.less";
// Electron app theming
@import "./app.less";
// Custom OpenFlexure theming
@global-primary-background: #C32280;
@inverse-primary-muted-color: lighten(@global-primary-background, 15%);
// UIkit
// ========================================================================
@ -120,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;
/* ========================================================================
@ -134,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;
@ -238,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

@ -42,20 +42,7 @@
<a class="uk-accordion-title" href="#">Metadata</a>
<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>
</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>
<keyvalList v-model="metadata"/>
</div>
</li>
@ -63,16 +50,7 @@
<li>
<a class="uk-accordion-title" href="#">Tags</a>
<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>
</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>
<tagList v-model="tags"/>
</div>
</li>
@ -176,10 +154,19 @@
<script>
import axios from 'axios'
import tagList from "../fieldComponents/tagList"
import keyvalList from "../fieldComponents/keyvalList"
// Export main app
export default {
name: 'paneCapture',
components: {
tagList,
keyvalList
},
data: function () {
return {
filename: '',
@ -203,42 +190,14 @@ export default {
z: 5
},
resizeDims: [640, 480],
newTag: "",
tags: [],
customMetadata: {
Client: "openflexure.vue"
},
newMetadata: {
key: "",
value: ""
metadata: {
Client: `${process.env.PACKAGE.name}.${process.env.PACKAGE.version}`
}
}
},
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() {
var payload = this.basePayload
@ -336,7 +295,7 @@ export default {
}
// Additional metadata
payload.metadata = this.customMetadata
payload.metadata = this.metadata
payload.tags = this.tags
// Attach notes

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,29 +34,35 @@
<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>
</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>
@ -122,6 +127,29 @@ export default {
this.$store.dispatch('firstConnect')
.then (() => {
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 => {
this.modalError(error) // Let mixin handle error
@ -132,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) {
@ -217,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

@ -4,30 +4,31 @@
<div>
<label class="uk-form-label" for="form-stacked-text">Exposure time</label>
<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>
<label class="uk-form-label" for="form-stacked-text">Analogue gain</label>
<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>
<label class="uk-form-label" for="form-stacked-text">Digital gain</label>
<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>
<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,35 +41,42 @@
<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.picamera_settings.shutter_speed,
analogGain: this.$store.state.apiConfig.picamera_settings.analog_gain,
digitalGain: this.$store.state.apiConfig.picamera_settings.digital_gain,
shutterSpeed: this.$store.state.apiConfig.camera_settings.picamera_settings.shutter_speed,
analogGain: this.$store.state.apiConfig.camera_settings.picamera_settings.analog_gain,
digitalGain: this.$store.state.apiConfig.camera_settings.picamera_settings.digital_gain,
isCalibrating: false
}
},
methods: {
updateInputValues: function () {
this.shutterSpeed = this.$store.state.apiConfig.picamera_settings.shutter_speed
this.digitalGain = this.$store.state.apiConfig.picamera_settings.digital_gain
this.analogGain = this.$store.state.apiConfig.picamera_settings.analog_gain
this.shutterSpeed = this.$store.state.apiConfig.camera_settings.picamera_settings.shutter_speed
this.digitalGain = this.$store.state.apiConfig.camera_settings.picamera_settings.digital_gain
this.analogGain = this.$store.state.apiConfig.camera_settings.picamera_settings.analog_gain
},
applyConfigRequest: function() {
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) {
payload.picamera_settings.shutter_speed = this.shutterSpeed
payload.picamera_settings.analog_gain = this.analogGain
payload.picamera_settings.digital_gain = this.digitalGain
payload.camera_settings.picamera_settings.shutter_speed = this.shutterSpeed
payload.camera_settings.picamera_settings.analog_gain = this.analogGain
payload.camera_settings.picamera_settings.digital_gain = this.digitalGain
//};
// Send request
@ -119,6 +127,15 @@ export default {
},
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 () {
return this.$store.getters.uri + "/plugin/default/camera_calibration/recalibrate"
},

View file

@ -52,27 +52,31 @@ export default {
data: function () {
return {
microscopeName: this.$store.state.apiConfig.name,
stageBacklash: this.$store.state.apiConfig.backlash
stageBacklash: this.$store.state.apiConfig.stage_settings.backlash
}
},
methods: {
updateInputValues: function () {
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() {
var payload = {}
var payload = {
stage_settings: {}
}
if (this.microscopeName != this.$store.state.apiConfig.name) {
payload.name = this.microscopeName
};
if (this.stageBacklash != this.$store.state.apiConfig.backlash) {
payload.backlash = this.stageBacklash
if (this.stageBacklash != this.$store.state.apiConfig.stage_settings.backlash) {
payload.stage_settings.backlash = this.stageBacklash
}
console.log(payload)
// Send request to update config
axios.post(this.configApiUri, payload)
.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>
<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="Connect"></a></li>
<li v-bind:class="disableIfDisconnected"><a href="#" uk-switcher-item="navigate" uk-icon="location" uk-tooltip="Navigate"></a></li>
<li v-bind:class="disableIfDisconnected"><a href="#" uk-switcher-item="capture" uk-icon="camera" uk-tooltip="Capture"></a></li>
<li v-bind:class="disableIfDisconnected"><a href="#" uk-switcher-item="plugins" uk-icon="git-fork" uk-tooltip="Plugins"></a></li>
<li><a href="#" uk-switcher-item="settings" uk-icon="settings" uk-tooltip="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/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="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><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,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>
<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">
@ -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

@ -1,5 +1,5 @@
<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">

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,14 +25,16 @@
<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-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
v-if="'isScan' in item"
:metadata="item.metadata"

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%;
}

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 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
@ -36,8 +36,20 @@ Vue.mixin({
return new Promise(showModal)
},
modalNotify: function(message) {
UIkit.notification({message: message, status: 'success'})
modalNotify: function(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) {
@ -66,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 {

View file

@ -1,3 +1,24 @@
var webpack = require('webpack');
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: ''
}