Merged vue into master
This commit is contained in:
commit
ef6c224342
32 changed files with 13140 additions and 679 deletions
25
.gitignore
vendored
25
.gitignore
vendored
|
|
@ -1,4 +1,21 @@
|
|||
.vscode/
|
||||
.idea/
|
||||
dist/
|
||||
node_modules/
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
|
|
|
|||
42
README.md
Normal file
42
README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# OpenFlexure.vue
|
||||
|
||||
## To-do
|
||||
* ~~Connect~~
|
||||
* ~~Connect to user-input IP~~
|
||||
* ~~Store connection in vuex store~~
|
||||
|
||||
* ~~Move~~
|
||||
* ~~Read absolute position~~
|
||||
* ~~Change absolute position~~
|
||||
* ~~Keyboard movement~~
|
||||
* ~~Mouse scroll focus~~
|
||||
* ~~Double-click to centre~~
|
||||
|
||||
* Gallery
|
||||
* ~~Basic card for each capture~~
|
||||
* ~~Thumbnails~~
|
||||
* ~~Reload button~~
|
||||
* Download buttons
|
||||
* ~~Lightbox~~
|
||||
* Save to disk
|
||||
* ~~Basic info/metadata modal~~
|
||||
* ~~Tags~~
|
||||
* ~~Read tag list~~
|
||||
* ~~Add tag button~~
|
||||
* ~~Delete tag button~~
|
||||
* Tag filter
|
||||
|
||||
* ~~Capture~~
|
||||
* ~~UI elements for capture options~~
|
||||
* ~~Big, friendly capture button~~
|
||||
* ~~Metadata for captures~~
|
||||
* ~~Tags for captures~~
|
||||
|
||||
* Plugins
|
||||
* ~~Placeholder for other plugins~~
|
||||
* Accordion for:
|
||||
* Autofocus
|
||||
* Recalibrate
|
||||
|
||||
* Settings
|
||||
* Metadata keys to automatically add (e.g. userID, patientID)
|
||||
5
babel.config.js
Normal file
5
babel.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
||||
128
index.html
128
index.html
|
|
@ -1,128 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>OpenFlexure Microscope API Demo</title>
|
||||
|
||||
<link rel= "stylesheet" type= "text/css" href="./static/style.css">
|
||||
<script type="text/javascript" src="./static/main.js"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui, maximum-scale=1.0, user-scalable=0">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="row">
|
||||
<div class="column left" id="left-sb">
|
||||
<h2>Settings</h2>
|
||||
Host: <input type="text" id="microscopeHostInput" value="192.168.1.126"><br>
|
||||
Port: <input type="number" id="microscopePortInput" value=5000><br>
|
||||
<button type="button" onclick="setHostFromInput();">Connect</button>
|
||||
|
||||
<h3>Position</h3>
|
||||
<p>
|
||||
x: <input type="number" id="x_abs"><br>
|
||||
y: <input type="number" id="y_abs"><br>
|
||||
z: <input type="number" id="z_abs"><br>
|
||||
</p>
|
||||
<p>
|
||||
<button type="button" onclick="setStagePositionsFromInput();">GO</button>
|
||||
<button type="button" onclick="getStagePositions();">Update</button>
|
||||
</p>
|
||||
<p>
|
||||
Autofocus:
|
||||
<button type="button" onclick='doAutofocus({"dz":[-200, -100, 0, 100, 200]});'>Coarse</button>
|
||||
<button type="button" onclick='doAutofocus({"dz":[-60, -30, 0, 30, 60]});'>Medium</button>
|
||||
<button type="button" onclick='doAutofocus({"dz":[-20, -10, 0, 10, 20]});'>Fine</button>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Doubleclick to position: <input type="checkbox" id="clickPositionCheck" checked><br>
|
||||
<div class="flexbox">
|
||||
<div class="flexgrow">FOV width:</div>
|
||||
<div>
|
||||
<input type="number" id="fovXText" onchange="fovX = Number(this.value); updateTextBoxes();" style="width: 7em" >
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flexbox">
|
||||
<div class="flexgrow">FOV height:</div>
|
||||
<div>
|
||||
<input type="number" id="fovYText" onchange="fovY = Number(this.value); updateTextBoxes();" style="width: 7em" >
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
|
||||
<h3>Move</h3>
|
||||
|
||||
<p>
|
||||
<b>Up/down/left/right:</b> <br>
|
||||
Move stage in x-y. <br>
|
||||
<b>PgUp/PgDn: </b> <br>
|
||||
Move stage in z/focus.
|
||||
</p>
|
||||
|
||||
<h4>Velocity:</h4>
|
||||
<div class="flexbox">
|
||||
<div class="flexgrow">x-y:</div>
|
||||
<div>
|
||||
<input type="range" id="stageVelocityInput" min="50" max="200" oninput="stageVelocity = Number(this.value); updateTextBoxes();">
|
||||
<input type="text" id="stageVelocityText" size="3" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flexbox">
|
||||
<div class="flexgrow">z:</div>
|
||||
<div>
|
||||
<input type="range" id="focusVelocityInput" min="10" max="100" oninput="focusVelocity = Number(this.value); updateTextBoxes();">
|
||||
<input type="text" id="focusVelocityText" size="3" disabled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<p>Scroll to focus: <input type="checkbox" id="scrollFocusCheck" checked></p>
|
||||
|
||||
<h4>Camera:</h4>
|
||||
<div class="flexbox">
|
||||
<button type="button" onclick="calibrateCamera();">Recalibrate Camera</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="column middle">
|
||||
<div id="left-sb-btn" class="sb-btn">
|
||||
<button type="button" onclick="toggleClassToDiv('left-sb', 'hidden');">
|
||||
<img src="./static/icons/baseline-settings-20px.svg">
|
||||
</button>
|
||||
</div>
|
||||
<div id="right-sb-btn" class="sb-btn">
|
||||
<button type="button" onclick="toggleClassToDiv('right-sb', 'hidden');">
|
||||
<img src="./static/icons/baseline-camera_alt-24px.svg">
|
||||
</button>
|
||||
</div>
|
||||
<div class="scrolltarget" id="streambox">No active stream</div>
|
||||
</div>
|
||||
|
||||
<div class="column right" id="right-sb">
|
||||
<h2>Capture</h2>
|
||||
Filename: <br>
|
||||
<input type="text" id="captureFilenameInput" placeholder="Leave blank for default"><br>
|
||||
Store on Pi: <input type="checkbox" id="captureKeepOnDiskCheck"><br>
|
||||
Full resolution: <input type="checkbox" id="captureFullResolutionCheck"><br>
|
||||
Keep raw data: <input type="checkbox" id="captureBayerCheck"><br>
|
||||
Resize capture: <input type="checkbox" id="captureResizeCheck" onclick="newCaptureResizeToggle(this);"><br>
|
||||
<input type="number" id="captureWidthInput" value="640" style="width: 7em" disabled>
|
||||
<input type="number" id="captureHeightInput" value="480" style="width: 7em" disabled><br>
|
||||
|
||||
<button type="button" onclick="newCaptureFromInput();">Capture</button>
|
||||
|
||||
<h2>Captures</h2>
|
||||
<button type="button" onclick="getCaptures();">Update</button>
|
||||
<button type="button" onclick="deleteAllCaptures();">Delete all</button>
|
||||
<div id="captures"></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
10792
package-lock.json
generated
Normal file
10792
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
37
package.json
Normal file
37
package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "openflexure-microscope-vue",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"dev": "set NODE_ENV=DEV&& electron src/app.js",
|
||||
"run": "vue-cli-service build && electron src/app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"electron": "^4.0.5",
|
||||
"electron-context-menu": "^0.11.0",
|
||||
"uikit": "^3.0.2",
|
||||
"vue": "^2.5.21",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.3.0",
|
||||
"@vue/cli-service": "^3.3.0",
|
||||
"css-loader": "^2.1.0",
|
||||
"less": "^3.9.0",
|
||||
"less-loader": "^4.1.0",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
]
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
17
public/index.html
Normal file
17
public/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>OpenFlexure Microscope</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but vue3 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
120
src/App.vue
Normal file
120
src/App.vue
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove" margin=0>
|
||||
<div id="sidebar-container" v-bind:class="responsivePanelLeft" 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: .toggle-hidden; animation: uk-animation-slide-left-small, uk-animation-slide-left-small" ></a>
|
||||
</div>
|
||||
<div id="left-panel-container" class="toggle-hidden uk-padding-remove uk-card uk-card-default uk-width-auto uk-height-1-1">
|
||||
<panelLeft/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-panel-container" class="uk-padding-remove uk-height-1-1 uk-width-expand">
|
||||
<panelDisplay/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Import axios for HTTP requests
|
||||
import axios from 'axios'
|
||||
// Import basic UIkit
|
||||
import UIkit from 'uikit';
|
||||
// Import UIkit icon set
|
||||
import Icons from 'uikit/dist/js/uikit-icons';
|
||||
UIkit.use(Icons);
|
||||
|
||||
// Import components
|
||||
import panelLeft from './components/panelLeft.vue'
|
||||
import panelDisplay from './components/panelDisplay.vue'
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'app',
|
||||
|
||||
components: {
|
||||
panelLeft,
|
||||
panelDisplay
|
||||
},
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
window: {
|
||||
width: 0,
|
||||
height: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
this.handleResize();
|
||||
},
|
||||
|
||||
beforeDestroy: function () {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleResize: function(event) {
|
||||
this.window.width = window.innerWidth;
|
||||
this.window.height = window.innerHeight;
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Stylises the hostname input box based on connection status
|
||||
responsivePanelLeft: function () {
|
||||
return {
|
||||
//'uk-hidden': this.window.width<850,
|
||||
'overlay-panel': this.window.width<850
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
// Basic UIkit CSS
|
||||
@import "../node_modules/uikit/src/less/uikit.less";
|
||||
// Custom UIkit CSS modifications
|
||||
@import "./assets/less/theme.less";
|
||||
|
||||
#app {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: left;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
body, html {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.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-disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
</style>
|
||||
31
src/app.js
Normal file
31
src/app.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const electron = require('electron')
|
||||
const app = electron.app
|
||||
const BrowserWindow = electron.BrowserWindow
|
||||
const contextMenu = require('electron-context-menu');
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'DEV';
|
||||
var isWin = process.platform === "win32";
|
||||
|
||||
|
||||
let url
|
||||
console.log(process.env.NODE_ENV)
|
||||
if (isDev) {
|
||||
console.log("Loading from localhost")
|
||||
url = 'http://localhost:8080/'
|
||||
} else {
|
||||
console.log("Loading from dist")
|
||||
url = `file://${process.cwd()}/dist/index.html`
|
||||
}
|
||||
|
||||
app.on('ready', () => {
|
||||
let window = new BrowserWindow({width: 1124, height: 800})
|
||||
window.setMenu(null);
|
||||
|
||||
contextMenu({
|
||||
showCopyImageAddress: true,
|
||||
showSaveImageAs: true,
|
||||
showInspectElement: isDev,
|
||||
});
|
||||
|
||||
window.loadURL(url)
|
||||
})
|
||||
BIN
src/assets/images/logo.png
Normal file
BIN
src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
51
src/assets/less/highlight.less
Normal file
51
src/assets/less/highlight.less
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* UIkit Theme for Highlight.js
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-meta { color: #969896; }
|
||||
|
||||
.hljs-string,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-strong,
|
||||
.hljs-emphasis,
|
||||
.hljs-quote { color: @global-danger-background; }
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type { color: #1f34aa; }
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-attribute { color: #0086b3; }
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name { color: #63a35c; }
|
||||
|
||||
.hljs-tag { color: #333; }
|
||||
|
||||
.hljs-title,
|
||||
.hljs-attr,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo { color: #795da3; }
|
||||
|
||||
.hljs-addition {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
|
||||
.hljs-link { text-decoration: underline; }
|
||||
507
src/assets/less/theme.less
Normal file
507
src/assets/less/theme.less
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
// UIkit Core
|
||||
@import "../../../node_modules/uikit/src/less/uikit.theme.less";
|
||||
|
||||
// Highlight.js
|
||||
@import "./highlight.less";
|
||||
|
||||
|
||||
// UIkit
|
||||
// ========================================================================
|
||||
|
||||
@global-font-family: ProximaNova, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
@global-font-size: 15px;
|
||||
|
||||
@global-xxlarge-font-size: 38px;
|
||||
@global-xlarge-font-size: 30px;
|
||||
@global-large-font-size: 24px;
|
||||
@global-medium-font-size: 20px;
|
||||
@global-small-font-size: 14px;
|
||||
|
||||
@global-emphasis-color: #222;
|
||||
|
||||
//
|
||||
// Base
|
||||
//
|
||||
|
||||
@base-code-font-family: 'Roboto Mono', monospace;
|
||||
@base-code-font-size: 12px;
|
||||
|
||||
@base-heading-font-weight: 300;
|
||||
|
||||
@base-pre-font-size: 12px;
|
||||
@base-pre-padding: 25px;
|
||||
@base-pre-background: @global-muted-background;
|
||||
@base-pre-border-width: 0;
|
||||
@base-pre-border-radius: 0;
|
||||
|
||||
.hook-base-body() {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
//
|
||||
// Container
|
||||
//
|
||||
|
||||
@container-max-width: 1380px;
|
||||
@container-small-max-width: 650px;
|
||||
|
||||
//
|
||||
// Navbar
|
||||
//
|
||||
|
||||
@inverse-navbar-nav-item-color: @inverse-global-color;
|
||||
@inverse-navbar-nav-item-hover-color: @inverse-global-emphasis-color;
|
||||
|
||||
//
|
||||
// Nav
|
||||
//
|
||||
|
||||
@nav-header-font-size: 12px;
|
||||
|
||||
//
|
||||
// Subnav
|
||||
//
|
||||
|
||||
@inverse-subnav-item-color: @inverse-global-color;
|
||||
@inverse-subnav-item-hover-color: @inverse-global-emphasis-color;
|
||||
|
||||
//
|
||||
// Tab
|
||||
//
|
||||
|
||||
@tab-item-padding-horizontal: 10px;
|
||||
@tab-item-padding-vertical: 9px;
|
||||
@tab-item-border-width: 2px;
|
||||
@tab-item-font-size: 12px;
|
||||
|
||||
.hook-tab-item() { line-height: 20px; }
|
||||
|
||||
//
|
||||
// Table
|
||||
//
|
||||
|
||||
@table-header-cell-font-size: 12px;
|
||||
|
||||
//
|
||||
// Label
|
||||
//
|
||||
|
||||
@label-font-size: 12px;
|
||||
|
||||
//
|
||||
// Text
|
||||
//
|
||||
|
||||
.hook-text-lead() { font-weight: 300; }
|
||||
.hook-text-large() { font-weight: 300; }
|
||||
|
||||
//
|
||||
// Utility
|
||||
//
|
||||
|
||||
@inverse-logo-color: @inverse-global-emphasis-color;
|
||||
@inverse-logo-hover-color: @inverse-global-emphasis-color;
|
||||
|
||||
//
|
||||
// Off-canvas
|
||||
//
|
||||
|
||||
@offcanvas-bar-background: #fff;
|
||||
@offcanvas-bar-color-mode: dark;
|
||||
|
||||
//
|
||||
// Inverse
|
||||
//
|
||||
|
||||
@inverse-global-color: fade(@global-inverse-color, 80%);
|
||||
@inverse-global-muted-color: fade(@global-inverse-color, 60%);
|
||||
|
||||
|
||||
|
||||
/* ========================================================================
|
||||
Theme
|
||||
========================================================================== */
|
||||
|
||||
@sidebar-left-width: 240px;
|
||||
@sidebar-left-width-xl: 300px;
|
||||
|
||||
@sidebar-right-width: 200px;
|
||||
@sidebar-right-left: 0px;
|
||||
@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
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* 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;
|
||||
@navbar-nav-item-line-height: 1px;
|
||||
@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;
|
||||
}
|
||||
|
||||
/*
|
||||
* Subnav
|
||||
*/
|
||||
|
||||
/* Tablet landscape and bigger */
|
||||
@media (min-width: @breakpoint-medium) {
|
||||
|
||||
.tm-subnav { margin-left: -50px; }
|
||||
.tm-subnav > * { padding-left: 50px; }
|
||||
|
||||
}
|
||||
|
||||
.tm-subnav > * > :first-child { text-transform: capitalize; }
|
||||
|
||||
/*
|
||||
* Label (Changelog()
|
||||
*/
|
||||
|
||||
.tm-label-changelog {
|
||||
width: 80px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Button
|
||||
*/
|
||||
|
||||
.tm-button-default,
|
||||
.tm-button-primary { border-radius: 500px; }
|
||||
|
||||
.tm-button-default {}
|
||||
|
||||
.tm-button-large { line-height: 48px; }
|
||||
|
||||
.tm-button-primary {}
|
||||
|
||||
.hook-inverse() {
|
||||
|
||||
.tm-button-default {
|
||||
color: @inverse-global-color;
|
||||
border-color: @inverse-global-muted-color;
|
||||
}
|
||||
|
||||
.tm-button-primary {
|
||||
background: #fff !important;
|
||||
color: @global-primary-background !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; }
|
||||
|
||||
}
|
||||
|
||||
/* 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
|
||||
*/
|
||||
|
||||
.tm-heading-fragment > a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Phone landscape and bigger */
|
||||
@media (min-width: @breakpoint-small) {
|
||||
|
||||
h2.tm-heading-fragment > a:before {
|
||||
content: "#";
|
||||
position: absolute;
|
||||
width: 25px;
|
||||
margin-left: -25px;
|
||||
color: @global-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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 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; }
|
||||
|
||||
}
|
||||
197
src/components/paneCapture.vue
Normal file
197
src/components/paneCapture.vue
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
<template>
|
||||
<div id="paneCapture">
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">Filename</label>
|
||||
<input v-model="filename" class="uk-input uk-width-1-1 uk-form-small" name="inputFilename" placeholder="Leave blank for default">
|
||||
</div>
|
||||
|
||||
<p uk-tooltip="title: Capture will be removed automatically; delay: 500"><label><input v-model="temporary" class="uk-checkbox" type="checkbox"> Temporary</label></p>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="uk-child-width-1-2" uk-grid>
|
||||
<p><label><input v-model="fullResolution" class="uk-checkbox" type="checkbox"> Full resolution</label></p>
|
||||
<p><label><input v-model="storeBayer" class="uk-checkbox" type="checkbox"> Store raw data</label></p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p><label><input v-model="resizeCapture" class="uk-checkbox" type="checkbox"> Resize capture</label></p>
|
||||
|
||||
<div class="uk-child-width-1-2" uk-grid>
|
||||
<div>
|
||||
<input v-bind:class="resizeClass" v-model="resizeDims[0]" class="uk-input uk-form-width-medium uk-form-small" type="number" name="inputResizeW">
|
||||
</div>
|
||||
<div>
|
||||
<input v-bind:class="resizeClass" v-model="resizeDims[1]" class="uk-input uk-form-width-medium uk-form-small" type="number" name="inputResizeH">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul uk-accordion="multiple: true">
|
||||
<li>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
<button v-on:click="handleCapture()" class="uk-button uk-button-default uk-form-small uk-float-right uk-width-1-1">Capture</button>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'paneCapture',
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
filename: '',
|
||||
temporary: false,
|
||||
fullResolution: false,
|
||||
storeBayer: false,
|
||||
resizeCapture: false,
|
||||
resizeDims: [640, 480],
|
||||
newTag: "",
|
||||
tags: [],
|
||||
customMetadata: {
|
||||
Client: "openflexure.vue"
|
||||
},
|
||||
newMetadata: {
|
||||
key: "",
|
||||
value: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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 = {}
|
||||
|
||||
// Filename
|
||||
if (Boolean(this.filename)) {
|
||||
payload.filename = this.filename
|
||||
}
|
||||
|
||||
// Basic boolean params
|
||||
payload.temporary = this.temporary;
|
||||
payload.use_video_port = !this.fullResolution;
|
||||
payload.bayer = this.storeBayer;
|
||||
|
||||
// Resizing
|
||||
if (this.resizeCapture) {
|
||||
payload.size = {
|
||||
width: this.resizeDims[0],
|
||||
height: this.resizeDims[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Additional metadata
|
||||
payload.metadata = this.customMetadata
|
||||
payload.tags = this.tags
|
||||
|
||||
// Do capture
|
||||
this.newCaptureRequest(payload)
|
||||
},
|
||||
|
||||
newCaptureRequest: function(params) {
|
||||
// Send move request
|
||||
axios.post(this.captureApiUri, params)
|
||||
.then(response => {
|
||||
this.$root.$emit('globalUpdateCaptureList')
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.dispatch('handleHTTPError', error); // Let store handle error
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
resizeClass: function () {
|
||||
return {
|
||||
'uk-disabled': !this.resizeCapture
|
||||
}
|
||||
},
|
||||
captureApiUri: function () {
|
||||
return this.$store.getters.uri + "/camera/capture"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.deletable-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deletable-label:hover {
|
||||
background-color: #f0506e;
|
||||
}
|
||||
|
||||
</style>
|
||||
185
src/components/paneConnect.vue
Normal file
185
src/components/paneConnect.vue
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
<template>
|
||||
<div class="host-input">
|
||||
<div class="uk-margin">
|
||||
|
||||
<h3>Connect</h3>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="uk-inline">
|
||||
<span class="uk-form-icon" uk-icon="icon: server"></span>
|
||||
<input v-model="hostname" v-bind:class="IpFormClasses" class="uk-input uk-form-width-medium uk-form-small" type="text" name="flavor" placeholder="localhost">
|
||||
</div>
|
||||
<button class="uk-button uk-button-default uk-form-small uk-float-right">Connect</button>
|
||||
|
||||
<ul uk-accordion>
|
||||
<li>
|
||||
<a class="uk-accordion-title" href="#">Advanced</a>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
<div class="host-display">
|
||||
|
||||
<h3>Status</h3>
|
||||
|
||||
<div v-if="$store.getters.ready">
|
||||
<p><b>Host:</b> {{ $store.state.host }}</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>
|
||||
|
||||
<button v-on:click="saveHost()" class="uk-button uk-button-default uk-form-small uk-float-right uk-margin-small uk-width-1-1">Save Host</button>
|
||||
|
||||
</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>
|
||||
|
||||
<h3>Saved hosts</h3>
|
||||
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'paneConnect',
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
hostname: "localhost",
|
||||
port: 5000,
|
||||
selectedHost: "",
|
||||
savedHosts: [
|
||||
{
|
||||
name: "local",
|
||||
hostname: "localhost",
|
||||
port: 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Try loading and parsing savedHosts from localStorage
|
||||
if (localStorage.getItem('savedHosts')) {
|
||||
try {
|
||||
this.savedHosts = JSON.parse(localStorage.getItem('savedHosts'));
|
||||
} catch(e) {
|
||||
localStorage.removeItem('savedHosts');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// When savedHosts changes, serialise to JSON and save to localStorage
|
||||
watch: {
|
||||
savedHosts(newSavedHosts) {
|
||||
const parsed = JSON.stringify(this.savedHosts);
|
||||
localStorage.setItem('savedHosts', parsed);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleSubmit: function(event) {
|
||||
if (this.hostname.includes(':')) {
|
||||
this.port = this.computedPort
|
||||
this.hostname = this.hostname.split(':')[0];
|
||||
}
|
||||
// Commit the hostname and port to store
|
||||
this.$store.commit('changeHost', [
|
||||
this.hostname,
|
||||
this.port
|
||||
]);
|
||||
// Try to get config and state JSON from the newly submitted host
|
||||
this.$store.dispatch('firstConnect')
|
||||
},
|
||||
|
||||
autofillHost: function (host) {
|
||||
this.hostname = host.hostname;
|
||||
this.port = host.port
|
||||
},
|
||||
|
||||
delSavedHost: function (host) {
|
||||
console.log(host)
|
||||
var index = this.savedHosts.indexOf(host);
|
||||
if (index > -1) {
|
||||
this.savedHosts.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
saveHost: function() {
|
||||
this.savedHosts.push(
|
||||
{
|
||||
name: this.$store.state.apiConfig.name,
|
||||
hostname: this.$store.state.host,
|
||||
port: this.$store.state.port
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Stylises the hostname input box based on connection status
|
||||
IpFormClasses: function () {
|
||||
return {
|
||||
'uk-form-danger': !this.$store.state.available,
|
||||
'uk-form-success': this.$store.getters.ready
|
||||
}
|
||||
},
|
||||
|
||||
computedPort: {
|
||||
get: function() {
|
||||
if (this.hostname.includes(':')) {
|
||||
var port = parseInt(this.hostname.split(':')[1]);
|
||||
return !isNaN(port) ? port : this.port
|
||||
}
|
||||
else {
|
||||
return this.port
|
||||
}
|
||||
},
|
||||
set: function(newPort) {
|
||||
this.port = newPort
|
||||
if (this.hostname.includes(':')) {
|
||||
this.hostname = this.hostname.split(':')[0] + ":" + this.port
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computedSelectedHost: {
|
||||
get: function() {
|
||||
return this.selectedHost
|
||||
},
|
||||
set: function(host) {
|
||||
this.selectedHost = host;
|
||||
this.hostname = host.hostname;
|
||||
this.port = host.port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="less">
|
||||
.host-input {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
<template>
|
||||
<div class="captureCard uk-card uk-card-default uk-card-hover uk-padding-remove uk-width-medium uk-margin-right">
|
||||
|
||||
<div class="uk-card-media-top">
|
||||
|
||||
<a class="lightbox-link" v-bind:href="imgURL" v-bind:data-caption="metadata.filename">
|
||||
<img class="uk-width-1-1" v-bind:src="thumbURL" v-bind:alt="metadata.id" uk-img>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="uk-card-body uk-padding-small">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="uk-text-meta uk-margin-remove-top uk-padding-remove uk-width-expand"><time>{{ betterTimestring }}</time></div>
|
||||
<div class="uk-text-meta uk-margin-remove-top uk-padding-remove uk-width-auto">
|
||||
<a v-bind:href="metadataModalTarget" uk-toggle>More...</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="uk-card-footer uk-padding-small">
|
||||
<span v-if="temporary" class="uk-label uk-label-danger uk-margin-small-right" uk-tooltip="title: Capture will be removed automatically; delay: 500">Temporary</span>
|
||||
|
||||
<span v-for="tag in tags" :key="tag" v-on:click="delTagConfirm(tag)" class="uk-label uk-margin-small-right deletable-label"> {{ tag }} </span>
|
||||
|
||||
<a v-bind:href="tagModalTarget" uk-toggle>
|
||||
<span class="uk-label uk-label-success uk-margin-small-right">Add</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-bind:id="metadataModalID" uk-modal>
|
||||
|
||||
<div class="uk-modal-dialog uk-modal-body">
|
||||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||
<h2 class="uk-modal-title">{{ metadata.filename }}</h2>
|
||||
<p><b>Time: </b>{{ betterTimestring }}</p>
|
||||
<p><b>ID: </b>{{ metadata.id }}</p>
|
||||
<p><b>Format: </b>{{ metadata.format }}</p>
|
||||
<p><b>Path: </b>{{ metadata.path }}</p>
|
||||
|
||||
<div v-for="(value, key) in metadata.custom" :key="key" >
|
||||
<p><b>{{ key }}: </b>{{ value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-bind:id="tagModalID" uk-modal>
|
||||
|
||||
<form class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical" @submit.prevent="handleTagSubmit">
|
||||
|
||||
<div class="uk-inline">
|
||||
<span class="uk-form-icon" uk-icon="icon: tag"></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>
|
||||
<button type="submit" class="uk-button uk-button-primary uk-margin-left uk-form-small">Save</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UIkit from 'uikit';
|
||||
import axios from 'axios'
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'captureCard',
|
||||
|
||||
props: {
|
||||
temporary: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
metadata: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
tags: [],
|
||||
newtag: "",
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleTagSubmit: function(event) {
|
||||
console.log(this.tagURL);
|
||||
console.log(this.newtag);
|
||||
this.newTagRequest(this.newtag);
|
||||
this.newtag = "";
|
||||
UIkit.modal(event.target.parentNode).hide();
|
||||
},
|
||||
|
||||
delCaptureConfirm: function(tag_string) {
|
||||
var self = this;
|
||||
UIkit.modal.confirm('Permanantly delete capture?').then(function() {
|
||||
self.delCaptureRequest()
|
||||
}, function () {
|
||||
console.log('Rejected.')
|
||||
});
|
||||
},
|
||||
|
||||
delCaptureRequest: function() {
|
||||
// Send tag DELETE request
|
||||
axios.delete(this.captureURL)
|
||||
.then(response => {
|
||||
// Emit signal to update capture list
|
||||
this.$root.$emit('globalUpdateCaptureList')
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.dispatch('handleHTTPError', error); // Let store handle error
|
||||
})
|
||||
},
|
||||
|
||||
newTagRequest: function(tag_string) {
|
||||
// Send tag PUT request
|
||||
axios.put(this.tagURL, [tag_string])
|
||||
.then(response => {
|
||||
// Update tag array
|
||||
this.getTagRequest()
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.dispatch('handleHTTPError', error); // Let store handle error
|
||||
})
|
||||
},
|
||||
|
||||
delTagConfirm: function(tag_string) {
|
||||
var self = this;
|
||||
UIkit.modal.confirm(`Remove tag '${tag_string}'?`).then(function() {
|
||||
self.delTagRequest(tag_string)
|
||||
}, function () {
|
||||
console.log('Rejected.')
|
||||
});
|
||||
},
|
||||
|
||||
delTagRequest: function(tag_string) {
|
||||
console.log(tag_string)
|
||||
// Send tag DELETE request
|
||||
axios.delete(this.tagURL, {data: [tag_string]})
|
||||
.then(response => {
|
||||
// Update tag array
|
||||
this.getTagRequest()
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.dispatch('handleHTTPError', error); // Let store handle error
|
||||
})
|
||||
},
|
||||
|
||||
getTagRequest: function() {
|
||||
// Send tag request
|
||||
axios.get(this.tagURL)
|
||||
.then(response => {
|
||||
this.tags = response.data.metadata.tags
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.dispatch('handleHTTPError', error); // Let store handle error
|
||||
})
|
||||
},
|
||||
|
||||
makeModalName: function(prefix) {
|
||||
return prefix + this.metadata.id
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.getTagRequest()
|
||||
},
|
||||
|
||||
computed: {
|
||||
tagModalID: function () {
|
||||
return this.makeModalName("tag-modal-")
|
||||
},
|
||||
tagModalTarget: function () {
|
||||
return "#" + this.tagModalID
|
||||
},
|
||||
metadataModalID: function () {
|
||||
return this.makeModalName("metadata-modal-")
|
||||
},
|
||||
metadataModalTarget: function () {
|
||||
return "#" + this.metadataModalID
|
||||
},
|
||||
thumbURL: function () {
|
||||
return this.captureURL + "/download?thumbnail=true"
|
||||
},
|
||||
imgURL: function () {
|
||||
return this.captureURL + "/download/" + this.metadata.filename
|
||||
},
|
||||
tagURL: function () {
|
||||
return this.captureURL + "/tags"
|
||||
},
|
||||
captureURL: function () {
|
||||
return this.$store.getters.uri + "/camera/capture/" + this.metadata.id
|
||||
},
|
||||
betterTimestring: function () {
|
||||
var dtSplit = this.metadata.time.split("_");
|
||||
var date = dtSplit[0]
|
||||
var time = dtSplit[1].replace(/-/g, ":")
|
||||
return date + " " + time
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
79
src/components/paneDisplayComponents/galleryDisplay.vue
Normal file
79
src/components/paneDisplayComponents/galleryDisplay.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<div class="galleryDisplay uk-padding uk-padding-remove-right">
|
||||
|
||||
<div uk-lightbox="toggle: .lightbox-link">
|
||||
<div class="uk-grid-medium uk-padding uk-padding-remove-right uk-grid-match" uk-grid>
|
||||
|
||||
<captureCard
|
||||
v-for="capture in captureList"
|
||||
:key="capture.metadata.id"
|
||||
:metadata="capture.metadata"
|
||||
:temporary="capture.temporary"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import captureCard from './galleryComponents/captureCard.vue'
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'galleryDisplay',
|
||||
|
||||
components: {
|
||||
captureCard
|
||||
},
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
captureList: []
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// A global signal listener to perform a gallery refresh
|
||||
this.$root.$on('globalUpdateCaptureList', () => {
|
||||
this.updateCaptureList()
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateCaptureList: function() {
|
||||
console.log("Updating capture list...")
|
||||
// Send move request
|
||||
axios.get(this.captureApiUri)
|
||||
.then(response => {
|
||||
this.$store.dispatch('updateState'); // Update store state for good measure
|
||||
this.captureList = response.data; // Update boxes from response
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.dispatch('handleHTTPError', error); // Let store handle error
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.updateCaptureList()
|
||||
},
|
||||
|
||||
computed: {
|
||||
captureApiUri: function () {
|
||||
return this.$store.getters.uri + "/camera/capture"
|
||||
},
|
||||
orderedcaptureList: function () {
|
||||
return _.orderBy(this.captureList, 'metadata_path', 'desc')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
70
src/components/paneDisplayComponents/streamDisplay.vue
Normal file
70
src/components/paneDisplayComponents/streamDisplay.vue
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div class="streamDisplay scrollTarget">
|
||||
|
||||
<img class="uk-align-center uk-margin-remove-bottom" v-on:dblclick="clickmonitor" v-if="showStream" v-bind:src="streamImgUri" alt="Stream">
|
||||
|
||||
<div v-else-if="$store.state.waiting" class="uk-position-center">
|
||||
<div uk-spinner="ratio: 4.5" ></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="$store.state.settings.disableStream" class="uk-position-center position-relative">
|
||||
Stream preview disabled
|
||||
</div>
|
||||
|
||||
<div v-else class="uk-position-center position-relative">
|
||||
No active connection
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'streamDisplay',
|
||||
|
||||
methods: {
|
||||
clickmonitor: function(event) {
|
||||
// Calculate steps from event coordinates and store config FOV
|
||||
var xCoordinate = event.offsetX;
|
||||
var 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;
|
||||
|
||||
var xSteps = xRelative * this.$store.state.apiConfig.fov[0];
|
||||
var 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)
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
showStream: function () {
|
||||
return this.$store.getters.ready && !this.$store.state.settings.disableStream
|
||||
},
|
||||
streamImgUri: function () {
|
||||
return this.$store.getters.uri + "/stream"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.streamDisplay img {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
object-fit: contain
|
||||
}
|
||||
|
||||
.streamDisplay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative !important;
|
||||
}
|
||||
</style>
|
||||
228
src/components/paneNavigate.vue
Normal file
228
src/components/paneNavigate.vue
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
<template>
|
||||
<div id="paneNavigate">
|
||||
|
||||
<ul uk-accordion="multiple: true">
|
||||
<li>
|
||||
<a class="uk-accordion-title" href="#">Configure</a>
|
||||
<div class="uk-accordion-content">
|
||||
|
||||
<div class="uk-child-width-1-2" uk-grid>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">x-y step size</label>
|
||||
<div class="uk-form-controls">
|
||||
<input v-model="stepXy" class="uk-input uk-form-width-medium uk-form-small" type="number" name="inputStepXy">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">z step size</label>
|
||||
<div class="uk-form-controls">
|
||||
<input v-model="stepZz" class="uk-input uk-form-width-medium uk-form-small" type="number" name="inputStepZz">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="uk-open">
|
||||
<a class="uk-accordion-title" href="#">Move-to</a>
|
||||
<div class="uk-accordion-content">
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">x</label>
|
||||
<div class="uk-form-controls">
|
||||
<input v-model="setPosition.x" class="uk-input uk-form-small" type="number" name="inputPositionX">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">y</label>
|
||||
<div class="uk-form-controls">
|
||||
<input v-model="setPosition.y" class="uk-input uk-form-small" type="number" name="inputPositionY">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">z</label>
|
||||
<div class="uk-form-controls">
|
||||
<input v-model="setPosition.z" class="uk-input uk-form-small" type="number" name="inputPositionZx">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<button class="uk-button uk-button-default uk-form-small uk-float-right uk-width-1-1">Move</button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="uk-accordion-title" href="#">Autofocus</a>
|
||||
<div class="uk-accordion-content">
|
||||
|
||||
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
|
||||
|
||||
<div>
|
||||
<button class="uk-button uk-button-default uk-form-small uk-float-right uk-width-1-1 uk-disabled">Coarse</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="uk-button uk-button-default uk-form-small uk-float-right uk-width-1-1 uk-disabled">Medium</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="uk-button uk-button-default uk-form-small uk-float-right uk-width-1-1 uk-disabled">Fine</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
// Key Codes
|
||||
const keyCodes = {
|
||||
pgup: 33,
|
||||
pgdn: 34,
|
||||
left: 37,
|
||||
up: 38,
|
||||
right: 39,
|
||||
down: 40,
|
||||
enter: 13,
|
||||
esc: 27
|
||||
}
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'paneNavigate',
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
keysDown: {},
|
||||
stepXy: 200,
|
||||
stepZz: 50,
|
||||
setPosition: this.$store.state.apiState.stage.position
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// A global signal listener to perform a move action
|
||||
this.$root.$on('globalMoveEvent', (x, y, z, absolute) => {
|
||||
this.moveRequest(x, y, z, absolute)
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
// Handle global mouse wheel events to be associated with navigation
|
||||
wheelMonitor: function(event) {
|
||||
// Only capture scroll if the event target's parent contains the "scrollTarget" class
|
||||
if (event.target.parentNode.classList.contains("scrollTarget")) {
|
||||
var z_rel = (event.deltaY)/100 * this.stepZz
|
||||
this.moveRequest(0, 0, z_rel, false)
|
||||
}
|
||||
},
|
||||
|
||||
// Handle global key press events to be associated with navigation
|
||||
keyDownMonitor: function(event) {
|
||||
this.keysDown[event.keyCode] = true; //Add key to array
|
||||
|
||||
// Convert keyCode dict into a list of key codes
|
||||
var keyCodeList = Object.keys(keyCodes).map(function(key){return keyCodes[key];});
|
||||
|
||||
if (!(event.target instanceof HTMLInputElement) && !(event.target.classList.contains('lightbox-link')) && keyCodeList.includes(event.keyCode)) {
|
||||
console.log(this.keysDown)
|
||||
// Calculate movement array
|
||||
var x_rel = 0;
|
||||
var y_rel = 0;
|
||||
var z_rel = 0;
|
||||
if (keyCodes.left in this.keysDown) {
|
||||
x_rel = x_rel + this.stepXy;
|
||||
}
|
||||
if (keyCodes.right in this.keysDown) {
|
||||
x_rel = x_rel - this.stepXy;
|
||||
}
|
||||
if (keyCodes.up in this.keysDown) {
|
||||
y_rel = y_rel + this.stepXy;
|
||||
}
|
||||
if (keyCodes.down in this.keysDown) {
|
||||
y_rel = y_rel - this.stepXy;
|
||||
}
|
||||
if (keyCodes.pgup in this.keysDown) {
|
||||
z_rel = z_rel - this.stepZz;
|
||||
}
|
||||
if (keyCodes.pgdn in this.keysDown) {
|
||||
z_rel = z_rel + this.stepZz;
|
||||
}
|
||||
|
||||
// Make a position request
|
||||
this.moveRequest(x_rel, y_rel, z_rel, false)
|
||||
}
|
||||
},
|
||||
|
||||
keyUpMonitor: function(event) {
|
||||
delete this.keysDown[event.keyCode]; //Remove key from array
|
||||
},
|
||||
|
||||
handleSubmit: function(event) {
|
||||
this.moveRequest(
|
||||
this.setPosition.x,
|
||||
this.setPosition.y,
|
||||
this.setPosition.z,
|
||||
true,
|
||||
)
|
||||
},
|
||||
|
||||
moveRequest: function(x, y, z, absolute) {
|
||||
// If not movement-locked
|
||||
if (!this.$store.state.moveLock) {
|
||||
// Lock move requests
|
||||
this.$store.commit('changeMoveLock', true)
|
||||
|
||||
// Send move request
|
||||
axios.post(this.positionApiUri, {
|
||||
x: x,
|
||||
y: y,
|
||||
z: z,
|
||||
absolute: absolute
|
||||
})
|
||||
.then(response => {
|
||||
this.$store.dispatch('updateState'); // Update store state
|
||||
this.setPosition = response.data.stage.position; // Update boxes from response
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.dispatch('handleHTTPError', error); // Let store handle error
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.commit('changeMoveLock', false) // Release the move lock
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
created: function () {
|
||||
window.addEventListener('keydown', this.keyDownMonitor);
|
||||
window.addEventListener("keyup", this.keyUpMonitor);
|
||||
window.addEventListener('wheel', this.wheelMonitor);
|
||||
},
|
||||
|
||||
computed: {
|
||||
positionApiUri: function () {
|
||||
return this.$store.getters.uri + "/stage/position"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
17
src/components/panePlugins.vue
Normal file
17
src/components/panePlugins.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<div id="panePlugins">
|
||||
<h3>Plugins</h3>
|
||||
<div class="uk-placeholder uk-text-center">Plugin support coming soon...</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'panePlugins',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
</style>
|
||||
37
src/components/paneSettings.vue
Normal file
37
src/components/paneSettings.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div id="paneSettings">
|
||||
|
||||
<ul uk-accordion="multiple: true">
|
||||
<li>
|
||||
<a class="uk-accordion-title" href="#">App settings</a>
|
||||
<div class="uk-accordion-content"><appSettings/></div>
|
||||
</li>
|
||||
|
||||
<li v-if="$store.getters.ready">
|
||||
<a class="uk-accordion-title" href="#">Microscope settings</a>
|
||||
<div class="uk-accordion-content"><microscopeSettings/></div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appSettings from './paneSettingsComponents/appSettings.vue'
|
||||
import microscopeSettings from './paneSettingsComponents/microscopeSettings.vue'
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'paneSettings',
|
||||
|
||||
components: {
|
||||
appSettings,
|
||||
microscopeSettings
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
</style>
|
||||
41
src/components/paneSettingsComponents/appSettings.vue
Normal file
41
src/components/paneSettingsComponents/appSettings.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div id="appSettings">
|
||||
|
||||
<div class="uk-child-width-1-2" uk-grid>
|
||||
<p><label><input v-model="disableStream" class="uk-checkbox" type="checkbox"> Disable stream</label></p>
|
||||
<p><label class="uk-disabled"><input v-model="autoGpuPreview" class="uk-checkbox" type="checkbox"> GPU preview</label></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'appSettings',
|
||||
|
||||
computed: {
|
||||
disableStream: {
|
||||
get() {
|
||||
return this.$store.state.settings.disableStream;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeSetting", ['disableStream', value]);
|
||||
}
|
||||
},
|
||||
autoGpuPreview: {
|
||||
get() {
|
||||
return this.$store.state.settings.autoGpuPreview;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeSetting", ['autoGpuPreview', value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
</style>
|
||||
149
src/components/paneSettingsComponents/microscopeSettings.vue
Normal file
149
src/components/paneSettingsComponents/microscopeSettings.vue
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<template>
|
||||
<div id="microscopeSettings">
|
||||
<form @submit.prevent="applyConfigRequest">
|
||||
<h4>Camera</h4>
|
||||
<div class="uk-text-center uk-container" v-if="isCalibrating">
|
||||
<div class="center-spinner" uk-spinner></div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<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>
|
||||
|
||||
<h4>Stage</h4>
|
||||
|
||||
<label class="uk-form-label" for="form-stacked-text">Backlash compensation</label>
|
||||
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">x</label>
|
||||
<div class="uk-form-controls">
|
||||
<input v-model="stageBacklash.x" class="uk-input uk-form-small" type="number">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">y</label>
|
||||
<div class="uk-form-controls">
|
||||
<input v-model="stageBacklash.y" class="uk-input uk-form-small" type="number">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">z</label>
|
||||
<div class="uk-form-controls">
|
||||
<input v-model="stageBacklash.z" class="uk-input uk-form-small" type="number">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Microscope</h4>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">Microscope name</label>
|
||||
<input v-model="microscopeName" class="uk-input uk-width-1-1 uk-form-small" name="inputFilename" placeholder="Leave blank for default">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="uk-button uk-button-default uk-form-small uk-float-right uk-margin-small uk-width-1-1">Apply Settings</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import UIkit from 'uikit';
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'microscopeSettings',
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
microscopeName: this.$store.state.apiConfig.name,
|
||||
stageBacklash: this.$store.state.apiConfig.backlash,
|
||||
isCalibrating: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateInputValues: function () {
|
||||
this.microscopeName = this.$store.state.apiConfig.name;
|
||||
this.stageBacklash = this.$store.state.apiConfig.backlash
|
||||
},
|
||||
|
||||
recalibrateConfirm: function() {
|
||||
var self = this;
|
||||
UIkit.modal.confirm('Start recalibration? This may take a while, and the microscope will be locked during this time.').then(function() {
|
||||
self.recalibrateRequest()
|
||||
}, function () {
|
||||
console.log('Rejected.')
|
||||
});
|
||||
},
|
||||
|
||||
recalibrateRequest: function() {
|
||||
// Send move request
|
||||
axios.post(this.recalibrateApiUri)
|
||||
.then(response => {
|
||||
console.log("Task ID: " + response.data[0].id)
|
||||
this.isCalibrating = true
|
||||
// Start the store polling TaskId for success
|
||||
self = this;
|
||||
this.$store.dispatch('pollTask', [response.data[0].id, null, null])
|
||||
.then(function() {
|
||||
self.isCalibrating = false
|
||||
UIkit.notification({message: "Finished recalibration.", status: 'success'})
|
||||
})
|
||||
.catch(error => {
|
||||
self.isCalibrating = false
|
||||
UIkit.notification({message: `<span uk-icon=\'icon: warning\'></span> ${error}`, status: 'danger'})
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.dispatch('handleHTTPError', error); // Let store handle error
|
||||
})
|
||||
},
|
||||
|
||||
applyConfigRequest: function() {
|
||||
var payload = {}
|
||||
|
||||
if (this.microscopeName != this.$store.state.apiConfig.name) {
|
||||
payload.name = this.microscopeName
|
||||
};
|
||||
|
||||
if (this.stageBacklash != this.$store.state.apiConfig.backlash) {
|
||||
payload.backlash = this.stageBacklash
|
||||
}
|
||||
|
||||
// Send move request
|
||||
axios.post(this.configApiUri, payload)
|
||||
.then(response => {
|
||||
this.$store.dispatch('updateConfig');
|
||||
this.updateInputValues
|
||||
UIkit.notification({message: "Microscope config applied.", status: 'success'})
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.dispatch('handleHTTPError', error); // Let store handle error
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
recalibrateApiUri: function () {
|
||||
return this.$store.getters.uri + "/plugin/default/camera_calibration/recalibrate"
|
||||
},
|
||||
configApiUri: function () {
|
||||
return this.$store.getters.uri + "/config"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.center-spinner {
|
||||
margin-left: auto;
|
||||
margin-right: auto
|
||||
}
|
||||
</style>
|
||||
47
src/components/panelDisplay.vue
Normal file
47
src/components/panelDisplay.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div id="panelDisplay" 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-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="disableIfDisconnected"><a href="#" v-on:click="updateCaptureList()" uk-switcher-item="gallery" uk-icon="image" uk-tooltip="Captures"></a></li>
|
||||
</ul>
|
||||
<ul class="uk-switcher uk-flex uk-flex-1">
|
||||
<li class="uk-height-1-1 uk-width-1-1 "><streamDisplay/></li>
|
||||
<li v-if="$store.getters.ready" class="uk-height-1-1 uk-width-1-1 uk-overflow-auto "><galleryDisplay/></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Import components
|
||||
import streamDisplay from './paneDisplayComponents/streamDisplay.vue'
|
||||
import galleryDisplay from './paneDisplayComponents/galleryDisplay.vue'
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'panelDisplay',
|
||||
|
||||
components: {
|
||||
streamDisplay,
|
||||
galleryDisplay
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateCaptureList: function () {
|
||||
this.$root.$emit('globalUpdateCaptureList')
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
disableIfDisconnected: function () {
|
||||
return {
|
||||
'uk-disabled': !this.$store.getters.ready
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
62
src/components/panelLeft.vue
Normal file
62
src/components/panelLeft.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<div id="panelLeft" class="uk-flex uk-flex-column uk-margin-remove uk-padding-remove uk-height-1-1">
|
||||
<div class="uk-flex-none uk-card-header">
|
||||
<h3 class="uk-card-title uk-align-center"><b>OpenFlexure</b> Microscope</h3>
|
||||
</div>
|
||||
<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>
|
||||
</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'
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: 'panelLeft',
|
||||
|
||||
components: {
|
||||
paneConnect,
|
||||
paneNavigate,
|
||||
paneCapture,
|
||||
panePlugins,
|
||||
paneSettings
|
||||
},
|
||||
|
||||
computed: {
|
||||
disableIfDisconnected: function () {
|
||||
return {
|
||||
'uk-disabled': !this.$store.getters.ready
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.uk-tab {
|
||||
padding-left: 0;
|
||||
}
|
||||
.panel-content {
|
||||
width: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
10
src/main.js
Normal file
10
src/main.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import store from './store'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
171
src/store.js
Normal file
171
src/store.js
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import axios from 'axios'
|
||||
import UIkit from 'uikit';
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
|
||||
state: {
|
||||
host: '',
|
||||
port: 5000,
|
||||
apiVer: 'v1',
|
||||
available: true,
|
||||
waiting: false,
|
||||
error: '',
|
||||
apiConfig: {},
|
||||
apiState: {},
|
||||
moveLock: false,
|
||||
settings: {
|
||||
disableStream: false,
|
||||
autoGpuPreview: false
|
||||
}
|
||||
},
|
||||
|
||||
mutations: {
|
||||
changeHost(state, [host, port, apiVer='v1']) {
|
||||
state.host = host;
|
||||
state.port = port;
|
||||
state.apiVer = apiVer;
|
||||
},
|
||||
changeAvailable(state, available) {
|
||||
state.available = available
|
||||
},
|
||||
changeWaiting(state, waiting) {
|
||||
state.waiting = waiting
|
||||
},
|
||||
changeMoveLock(state, lock) {
|
||||
state.moveLock = lock
|
||||
},
|
||||
commitError(state, errorString) {
|
||||
state.error = errorString;
|
||||
},
|
||||
commitConfig(state, configData) {
|
||||
state.apiConfig = configData;
|
||||
},
|
||||
commitState(state, stateData) {
|
||||
state.apiState = stateData;
|
||||
},
|
||||
changeSetting(state, [key, value]) {
|
||||
state.settings[key] = value;
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
firstConnect(context) {
|
||||
// Reset the state when reconnecting starts
|
||||
context.dispatch('resetState');
|
||||
// Mark as loading
|
||||
context.commit('changeWaiting', true);
|
||||
// Do requests
|
||||
context.dispatch('updateConfig')
|
||||
context.dispatch('updateState')
|
||||
},
|
||||
|
||||
updateConfig(context, uri=`${context.getters.uri}/config`) {
|
||||
context.dispatch('waitingState')
|
||||
|
||||
axios.get(uri)
|
||||
.then(response => {
|
||||
context.commit('commitConfig', response.data);
|
||||
context.dispatch('connectedState')
|
||||
})
|
||||
.catch(error => {
|
||||
context.dispatch('handleHTTPError', error);
|
||||
})
|
||||
},
|
||||
|
||||
updateState(context, uri=`${context.getters.uri}/state`) {
|
||||
context.dispatch('waitingState')
|
||||
|
||||
axios.get(uri)
|
||||
.then(response => {
|
||||
context.commit('commitState', response.data);
|
||||
context.dispatch('connectedState')
|
||||
})
|
||||
.catch(error => {
|
||||
context.dispatch('handleHTTPError', error);
|
||||
})
|
||||
},
|
||||
|
||||
pollTask(context, [taskId, timeout, interval]) {
|
||||
var endTime = Number(new Date()) + (timeout || 30000);
|
||||
interval = interval || 500;
|
||||
|
||||
var checkCondition = function(resolve, reject) {
|
||||
// If the condition is met, we're done!
|
||||
axios.get(`${context.getters.uri}/task/${taskId}`)
|
||||
.then(response => {
|
||||
console.log(response.data.status)
|
||||
var result = response.data.status;
|
||||
// If the task ends with success
|
||||
if(result == 'success') {
|
||||
resolve(response.data.return);
|
||||
}
|
||||
// If task ends with an error
|
||||
else if (result == 'error') {
|
||||
reject(new Error(response.data.return));
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
// Didn't match and too much time, reject!
|
||||
else {
|
||||
reject(new Error('Polling timed out'));
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
return new Promise(checkCondition);
|
||||
|
||||
},
|
||||
|
||||
handleHTTPError(context, error) {
|
||||
var errormsg = '';
|
||||
if (error.response) {
|
||||
errormsg = `${error.response.status}: ${error.response.data}`
|
||||
console.log(errormsg)
|
||||
} else if (error.request) {
|
||||
errormsg = `${error.message}`
|
||||
console.log(errormsg)
|
||||
} else {
|
||||
errormsg = `${error.message}`
|
||||
console.log(errormsg)
|
||||
}
|
||||
context.dispatch('errorState', errormsg);
|
||||
},
|
||||
|
||||
resetState(context) {
|
||||
context.commit('changeWaiting', false);
|
||||
context.commit('changeAvailable', true);
|
||||
context.commit('commitError', null);
|
||||
context.commit('commitConfig', {});
|
||||
context.commit('commitState', {});
|
||||
},
|
||||
|
||||
waitingState(context) {
|
||||
context.commit('changeWaiting', true);
|
||||
},
|
||||
|
||||
connectedState(context) {
|
||||
context.commit('changeWaiting', false);
|
||||
context.commit('changeAvailable', true);
|
||||
},
|
||||
|
||||
errorState(context, msg) {
|
||||
context.commit('changeWaiting', false);
|
||||
context.commit('changeAvailable', false);
|
||||
context.commit('commitError', msg);
|
||||
UIkit.notification({message: `<span uk-icon=\'icon: warning\'></span> ${msg}`, status: 'danger'})
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
uri: state => `http://${state.host}:${state.port}/api/${state.apiVer}`,
|
||||
ready: state => ((Object.keys(state.apiConfig).length !== 0) && (Object.keys(state.apiState).length !== 0))
|
||||
}
|
||||
|
||||
})
|
||||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M9 2L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2H9zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||
|
Before Width: | Height: | Size: 324 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="none" d="M0 0h20v20H0V0z"/><path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/></svg>
|
||||
|
Before Width: | Height: | Size: 774 B |
410
static/main.js
410
static/main.js
|
|
@ -1,410 +0,0 @@
|
|||
var apiVersion = "v1"
|
||||
var hostName
|
||||
var hostPort
|
||||
var baseURI
|
||||
|
||||
// Key IDs
|
||||
var pgupKeyID = 33;
|
||||
var pgdnKeyID = 34;
|
||||
|
||||
var leftKeyID = 37;
|
||||
var upKeyID = 38;
|
||||
var rightKeyID = 39;
|
||||
var downKeyID = 40;
|
||||
|
||||
var enterKeyID = 13;
|
||||
var escKeyID = 27;
|
||||
|
||||
var keysDown = {}; //Array of keys down
|
||||
|
||||
var stageVelocity = 50;
|
||||
var focusVelocity = 20;
|
||||
|
||||
var requestLock = false;
|
||||
|
||||
// Microscope client-side callibration
|
||||
//TODO: Move to serverside microscope property
|
||||
var fovX = 4100;
|
||||
var fovY = 3146;
|
||||
|
||||
window.onload = function() {
|
||||
setHostFromInput()
|
||||
|
||||
// TODO: Use getConfig as a connection check. Handle connection timeout.
|
||||
getConfig(updateConfigValues)
|
||||
getState(updateStateValues)
|
||||
updateTextBoxes()
|
||||
getCaptures()
|
||||
}
|
||||
|
||||
|
||||
// Cosmetic methods
|
||||
function toggleClassToDiv(div_id, class_name) {
|
||||
document.getElementById(div_id).classList.toggle(class_name)
|
||||
}
|
||||
|
||||
// Setup methods
|
||||
function setHostFromInput() {
|
||||
hostName = document.getElementById('microscopeHostInput').value;
|
||||
hostPort = document.getElementById('microscopePortInput').value;
|
||||
|
||||
baseURI = `http://${hostName}:${hostPort}/api/${apiVersion}`
|
||||
|
||||
streamContent = `<img src="${baseURI}/stream" ondblclick="clickHotspotImage(event);">`
|
||||
document.getElementById("streambox").innerHTML = streamContent;
|
||||
}
|
||||
|
||||
// Microscope methods
|
||||
|
||||
function deleteCapture(capture_id) {
|
||||
function deleteCaptureCallback(response, status) {
|
||||
console.log(status);
|
||||
getCaptures();
|
||||
}
|
||||
var r = confirm("Warning! This will delete all copies of this capture from the Raspberry Pi. Click OK to proceed.");
|
||||
if (r == true) {
|
||||
safeRequest("DELETE", baseURI+"/camera/capture/"+capture_id+"/", null, deleteCaptureCallback, false)
|
||||
}
|
||||
}
|
||||
|
||||
function deleteAllCaptures() {
|
||||
function deleteAllCapturesCallback(response, status) {
|
||||
console.log(status);
|
||||
getCaptures();
|
||||
}
|
||||
var r = confirm("Warning! This will delete all copies of all captures from the Raspberry Pi. Click OK to proceed.");
|
||||
if (r == true) {
|
||||
safeRequest("DELETE", baseURI+"/camera/capture/", null, deleteAllCapturesCallback, false)
|
||||
}
|
||||
}
|
||||
|
||||
function getCaptures() {
|
||||
function updateCapturesCallback(response, status) {
|
||||
console.log(status);
|
||||
updateCaptureList(response);
|
||||
}
|
||||
safeRequest("GET", baseURI+"/camera/capture", null, updateCapturesCallback, false)
|
||||
}
|
||||
|
||||
function updateCaptureList(response) {
|
||||
// Clear captures list
|
||||
var capturesNode = document.getElementById("captures");
|
||||
while (capturesNode.firstChild) {
|
||||
capturesNode.removeChild(capturesNode.firstChild);
|
||||
}
|
||||
|
||||
// For each capture in the response array
|
||||
response.forEach(function(element) {
|
||||
// Store the capture object URI
|
||||
element_uri = `${baseURI}/camera/capture/${element.metadata.id}`
|
||||
|
||||
// Generate inner HTML from capture object
|
||||
html = `
|
||||
<div class="capture-thumb"><img src="${element_uri}/download?thumbnail=true"></div>
|
||||
<div class="capture-actions">
|
||||
<div class='capture-heading'>${element.metadata.filename}</div>
|
||||
<br>
|
||||
<button type="button" class="capture-button" onclick="window.open('${element_uri}/download');">Image</button>
|
||||
<button type="button" class="capture-button" onclick="window.open('${element_uri}/metadata');">Metadata</button>
|
||||
<br>
|
||||
<button type="button" class="capture-button" onclick="window.open('${element_uri}/', '_blank');">JSON</button>
|
||||
<button type="button" class="capture-button" onclick="deleteCapture('${element.metadata.id}');">Delete</button>
|
||||
<br>
|
||||
</div>
|
||||
`
|
||||
|
||||
// Create a new capture div, and append HTML
|
||||
var div = document.createElement("div");
|
||||
div.className = "capture";
|
||||
div.innerHTML = html;
|
||||
capturesNode.appendChild(div);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function updateTextBoxes() {
|
||||
document.getElementById('stageVelocityText').value = stageVelocity;
|
||||
document.getElementById('stageVelocityInput').value = stageVelocity;
|
||||
document.getElementById('focusVelocityText').value = focusVelocity;
|
||||
document.getElementById('focusVelocityInput').value = focusVelocity;
|
||||
}
|
||||
|
||||
function updatePositionValues(state) {
|
||||
document.getElementById('x_abs').value = state.stage.position.x;
|
||||
document.getElementById('y_abs').value = state.stage.position.y;
|
||||
document.getElementById('z_abs').value = state.stage.position.z;
|
||||
}
|
||||
|
||||
function updateFovValues(config) {
|
||||
document.getElementById('fovXText').value = config.fov.x;
|
||||
document.getElementById('fovYText').value = config.fov.y;
|
||||
}
|
||||
|
||||
function updateStateValues(state){
|
||||
updatePositionValues(state);
|
||||
}
|
||||
|
||||
function updateConfigValues(config){
|
||||
updateFovValues(config)
|
||||
}
|
||||
|
||||
function getState(callback) {
|
||||
function getStateCallback(response, status) {
|
||||
callback(response);
|
||||
}
|
||||
safeRequest("GET", baseURI+"/state", null, getStateCallback, false)
|
||||
}
|
||||
|
||||
function getConfig(callback) {
|
||||
function getConfigCallback(response, status) {
|
||||
callback(response);
|
||||
}
|
||||
safeRequest("GET", baseURI+"/config", null, getConfigCallback, false)
|
||||
}
|
||||
|
||||
// Capture methods
|
||||
// TODO: Explicit callback. Roll into class.
|
||||
function newCapture(filename, keep_on_disk, use_video_port, bayer, resizeWidth=null, resizeHeight=null) {
|
||||
// Make a position request
|
||||
function newCaptureCallback(response, status) {
|
||||
if (status != 200) {
|
||||
alert("Capture failed.");
|
||||
}
|
||||
console.log(status, response);
|
||||
getCaptures(); // Update full list of captures when done
|
||||
}
|
||||
|
||||
payload = {
|
||||
"filename": filename,
|
||||
"keep_on_disk": keep_on_disk,
|
||||
"use_video_port": use_video_port,
|
||||
"bayer": bayer
|
||||
}
|
||||
|
||||
if ((resizeWidth) && (resizeHeight)) {
|
||||
payload.size = {
|
||||
"width": resizeWidth,
|
||||
"height": resizeHeight
|
||||
}
|
||||
}
|
||||
|
||||
console.log(payload);
|
||||
safeRequest("POST", baseURI+"/camera/capture/", payload, newCaptureCallback);
|
||||
}
|
||||
|
||||
function newCaptureFromInput() {
|
||||
captureFilenameInput = document.getElementById('captureFilenameInput');
|
||||
captureKeepOnDiskCheck = document.getElementById('captureKeepOnDiskCheck');
|
||||
captureFullResolutionCheck = document.getElementById('captureFullResolutionCheck');
|
||||
captureBayerCheck = document.getElementById('captureBayerCheck')
|
||||
|
||||
captureResizeCheck = document.getElementById('captureResizeCheck');
|
||||
captureWidthInput = document.getElementById('captureWidthInput')
|
||||
captureHeightInput = document.getElementById('captureHeightInput')
|
||||
|
||||
// Get filename if one is given
|
||||
if (captureFilenameInput.value === "") {
|
||||
filename = null;
|
||||
}
|
||||
else {
|
||||
filename = captureFilenameInput.value;
|
||||
}
|
||||
|
||||
if (captureResizeCheck.checked) {
|
||||
resizeWidth = Number(captureWidthInput.value);
|
||||
resizeHeight = Number(captureHeightInput.value);
|
||||
}
|
||||
else {
|
||||
resizeWidth = null;
|
||||
resizeHeight = null;
|
||||
}
|
||||
|
||||
newCapture(
|
||||
filename,
|
||||
captureKeepOnDiskCheck.checked,
|
||||
!(captureFullResolutionCheck.checked),
|
||||
captureBayerCheck.checked,
|
||||
resizeWidth,
|
||||
resizeHeight
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function newCaptureResizeToggle(checkbox) {
|
||||
captureWidthInput = document.getElementById('captureWidthInput')
|
||||
captureHeightInput = document.getElementById('captureHeightInput')
|
||||
|
||||
captureWidthInput.disabled = !(checkbox.checked)
|
||||
captureHeightInput.disabled = !(checkbox.checked)
|
||||
}
|
||||
|
||||
// Move stage methods
|
||||
// TODO: Combine setStagePositions and moveStagePositions, with bool argument for 'absolute'
|
||||
|
||||
function setStagePositions(x_abs, y_abs, z_abs) {
|
||||
// Make a position request
|
||||
function absMoveCallback(response, status) {
|
||||
if (status != 200) {
|
||||
alert("Error moving stage.");
|
||||
}
|
||||
console.log(status, response);
|
||||
updatePositionValues(response);
|
||||
}
|
||||
safeRequest("POST", baseURI+"/stage/position", { "absolute": true, "x": x_abs, "y": y_abs, "z": z_abs}, absMoveCallback)
|
||||
}
|
||||
|
||||
function moveStagePositions(x_rel, y_rel, z_rel) {
|
||||
// Make a position request
|
||||
function relMoveCallback(response, status) {
|
||||
if (status != 200) {
|
||||
alert("Error moving stage.");
|
||||
}
|
||||
console.log(status, response);
|
||||
updatePositionValues(response);
|
||||
}
|
||||
safeRequest("POST", baseURI+"/stage/position", { "absolute": false, "x": x_rel, "y": y_rel, "z": z_rel}, relMoveCallback)
|
||||
}
|
||||
|
||||
function setStagePositionsFromInput() {
|
||||
x_abs = Number(document.getElementById('x_abs').value);
|
||||
y_abs = Number(document.getElementById('y_abs').value);
|
||||
z_abs = Number(document.getElementById('z_abs').value);
|
||||
setStagePositions(x_abs, y_abs, z_abs)
|
||||
}
|
||||
|
||||
// Standard callback for user-controller movement
|
||||
function keyMoveCallback(response, status) {
|
||||
console.log(status, response);
|
||||
updatePositionValues(response);
|
||||
}
|
||||
|
||||
// Methods for input events
|
||||
|
||||
// Click-to-move
|
||||
function clickHotspotImage(event) {
|
||||
if (document.getElementById("clickPositionCheck").checked == true) {
|
||||
xCoordinate = event.offsetX;
|
||||
yCoordinate = event.offsetY;
|
||||
console.log(xCoordinate, yCoordinate)
|
||||
xRelative = (0.5*event.target.offsetWidth - xCoordinate)/event.target.offsetWidth;
|
||||
yRelative = (0.5*event.target.offsetHeight - yCoordinate)/event.target.offsetHeight;
|
||||
console.log(xRelative, yRelative)
|
||||
xSteps = xRelative * fovX;
|
||||
ySteps = yRelative * fovY;
|
||||
console.log(xSteps, ySteps, 0)
|
||||
// Make a position request
|
||||
moveStagePositions(xSteps, ySteps, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll-to-focus
|
||||
window.addEventListener('wheel', function(e) {
|
||||
multiplier = 1/100;
|
||||
z_delta = e.deltaY * multiplier * focusVelocity;
|
||||
if ((document.getElementById("scrollFocusCheck").checked == true) && (e.target.parentNode.classList.value == "scrolltarget")) {
|
||||
console.log(z_delta);
|
||||
console.log(e.target.parentNode.classList.value);
|
||||
// Make a position request
|
||||
safeRequest("POST", baseURI+"/stage/position", { "absolute": false, "z": z_delta}, keyMoveCallback)
|
||||
}
|
||||
else {
|
||||
console.log("Scroll-to-focus inactive")
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard to move
|
||||
addEventListener("keydown", function (e) {
|
||||
keysDown[e.keyCode] = true; //Add key to array
|
||||
|
||||
// If not currently in an input box
|
||||
if (!(e.target instanceof HTMLInputElement)) {
|
||||
|
||||
// TODO: Remove this if condition? Pointless?
|
||||
// If stage movement keys are pressed
|
||||
if ((leftKeyID in keysDown) || (rightKeyID in keysDown) || (upKeyID in keysDown) || (downKeyID in keysDown) || (pgupKeyID in keysDown) || (pgdnKeyID in keysDown)) {
|
||||
// Calculate movement array
|
||||
x_rel = 0;
|
||||
y_rel = 0;
|
||||
z_rel = 0;
|
||||
if (leftKeyID in keysDown) {
|
||||
x_rel = x_rel + stageVelocity;
|
||||
}
|
||||
if (rightKeyID in keysDown) {
|
||||
x_rel = x_rel - stageVelocity;
|
||||
}
|
||||
if (upKeyID in keysDown) {
|
||||
y_rel = y_rel + stageVelocity;
|
||||
}
|
||||
if (downKeyID in keysDown) {
|
||||
y_rel = y_rel - stageVelocity;
|
||||
}
|
||||
if (pgupKeyID in keysDown) {
|
||||
z_rel = z_rel - focusVelocity;
|
||||
}
|
||||
if (pgdnKeyID in keysDown) {
|
||||
z_rel = z_rel + focusVelocity;
|
||||
}
|
||||
|
||||
// Make a position request
|
||||
moveStagePositions(x_rel, y_rel, z_rel)
|
||||
}
|
||||
}
|
||||
|
||||
}, false);
|
||||
|
||||
addEventListener("keyup", function (e) {
|
||||
delete keysDown[e.keyCode]; //Remove key from array
|
||||
}, false);
|
||||
|
||||
function ignoreCallback(value=null, code=0){
|
||||
console.log("Intentionally ignoring a callback: ", value, code);
|
||||
}
|
||||
|
||||
function doAutofocus(payload=null, callback=ignoreCallback){
|
||||
safeRequest("POST", baseURI+"/plugin/default/autofocus/autofocus", payload, callback);
|
||||
}
|
||||
function calibrateCamera(callback=ignoreCallback){
|
||||
safeRequest("POST", baseURI+"/plugin/default/camera_calibration/recalibrate/", null, callback);
|
||||
}
|
||||
|
||||
// Methods for making requests
|
||||
function safeRequest(method, url, payload, callback, lock=true, force=false) {
|
||||
if (force == true) {
|
||||
allowRequest = true;
|
||||
}
|
||||
else {
|
||||
allowRequest = !(requestLock);
|
||||
}
|
||||
if (allowRequest == true) {
|
||||
var xhr = new XMLHttpRequest(); // new HttpRequest instance
|
||||
xhr.open(method, url);
|
||||
|
||||
if (lock == true) { // If request is using the lock
|
||||
requestLock = true; // Hold the lock
|
||||
}
|
||||
|
||||
xhr.onload = function (e) {
|
||||
if (xhr.readyState === 4) {
|
||||
if (lock == true) { // If request is using the lock
|
||||
requestLock = false; // Release the lock
|
||||
}
|
||||
|
||||
if (xhr.status !== 200) {
|
||||
console.error(xhr.statusText);
|
||||
}
|
||||
callback(JSON.parse(xhr.responseText), xhr.status);
|
||||
}
|
||||
};
|
||||
|
||||
if (method == "POST" || method == "PUT") {
|
||||
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
||||
}
|
||||
|
||||
xhr.send(JSON.stringify(payload));
|
||||
|
||||
}
|
||||
else {
|
||||
console.log("Cannot start the following request while the previous requets is pending:")
|
||||
console.log(method, url, payload)
|
||||
}
|
||||
}
|
||||
135
static/style.css
135
static/style.css
|
|
@ -1,135 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Preview sizing */
|
||||
.middle img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain
|
||||
}
|
||||
|
||||
/* Create columns that float next to each other */
|
||||
.row {
|
||||
height: 100%;
|
||||
}
|
||||
.column {
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.left, .right {
|
||||
background-color: #f4f4f4;
|
||||
overflow-y: scroll;
|
||||
flex: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 250px;
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.left{
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right{
|
||||
float: right;
|
||||
}
|
||||
|
||||
.hidden{
|
||||
flex-basis: 0;
|
||||
padding:0
|
||||
}
|
||||
|
||||
.middle {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
background-color: #d3d3d3;
|
||||
}
|
||||
|
||||
#left-sb-btn {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
#right-sb-btn {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.sb-btn {
|
||||
top: 20px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.sb-btn button{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
padding:5px;
|
||||
}
|
||||
|
||||
/* Clear floats after the columns */
|
||||
.row{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Flexboxes */
|
||||
.flexbox {
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.flexgrow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Data divs */
|
||||
|
||||
#captures {
|
||||
font-size:12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.capture {
|
||||
height: 75px;
|
||||
width: 240px;
|
||||
margin-bottom: 5px;
|
||||
background-color: #d3d3d3;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.capture-thumb {
|
||||
width: 80px;
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
.capture-heading {
|
||||
font-weight: bold;
|
||||
margin: 5px 0 5px 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.capture-actions {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.capture img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.capture-button {
|
||||
width: 75px;
|
||||
margin-bottom: 3px;
|
||||
height: 22px;
|
||||
}
|
||||
3
vue.config.js
Normal file
3
vue.config.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
publicPath: ''
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue