Compare commits
55 Commits
11cf67b594
...
master
Author | SHA1 | Date | |
---|---|---|---|
4e7fa44301 | |||
|
104d39b4c6 | ||
|
88e62bdf48 | ||
|
b9c8b6c1e6 | ||
66f86827ff | |||
|
8ca4d55045 | ||
|
2b495779d3 | ||
|
ce46f006d5 | ||
|
5f8aad7355 | ||
|
c975432b73 | ||
be140b4c22 | |||
|
3c5855ecd3 | ||
|
1499ee5736 | ||
|
a07d4469c0 | ||
|
5e565f84c2 | ||
|
b2cc6f4723 | ||
|
6720816a97 | ||
|
5c15c8fcf5 | ||
738e688c51 | |||
19352bf409 | |||
a632034751 | |||
|
fa6809b507 | ||
|
c29a64b1cc | ||
d1486b61df | |||
ecb8c1f815 | |||
ec8128d96a | |||
|
15023b0e24 | ||
|
534a3fe632 | ||
|
f338e14577 | ||
d5cfbafbde | |||
b213f4cb18 | |||
612f47d551 | |||
|
14395bdbdd | ||
|
be284b9acb | ||
|
9fe790ed9c | ||
|
91e26fe9fc | ||
|
0760c93ce4 | ||
b1aaffa4db | |||
0f0f6b1f7f | |||
e8822a8d3a | |||
|
b0aec2cfd1 | ||
|
1e8d655d1d | ||
|
181a2bbb74 | ||
|
963939d511 | ||
|
d08a6e77f4 | ||
|
d1e5b0310b | ||
026913013a | |||
ba63fcaf01 | |||
d1f527c93b | |||
23f15aed34 | |||
2c0c838bdb | |||
c3d83be3c4 | |||
7cc29f6b9f | |||
bb9e9e6a11 | |||
a436ff0ad7 |
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Tanner and Elijah
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
4
client/.gitignore
vendored
@@ -9,7 +9,9 @@
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
# /build
|
||||
|
||||
client/settings.ts
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
@@ -2,6 +2,30 @@
|
||||
|
||||
# Requirements (pages)
|
||||
|
||||
# Routes
|
||||
|
||||
Client Datatype
|
||||
|
||||
```ts
|
||||
type Client = {
|
||||
name: string
|
||||
email: string
|
||||
phone: number
|
||||
photos: string[]
|
||||
}
|
||||
```
|
||||
|
||||
post /api/clients -> create new client
|
||||
get /api/clients/:id -> get client
|
||||
|
||||
post /api/clients/:id/session -> begin capture
|
||||
delete /api/clients/:id/session -> delete all current photos (for new capture)
|
||||
|
||||
### Note Needed
|
||||
|
||||
get /api/clients -> get client list
|
||||
get /api/clients/:id/session -> get active sesion (list of preview photo locations)
|
||||
|
||||
## Create Session
|
||||
|
||||
Information gathering
|
||||
|
BIN
client/build/1.jpg
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
client/build/2.jpg
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
client/build/3.jpg
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
client/build/4.jpg
Normal file
After Width: | Height: | Size: 3.2 KiB |
25
client/build/asset-manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.895ab0f5.chunk.css",
|
||||
"main.js": "/static/js/main.4388b0cf.chunk.js",
|
||||
"main.js.map": "/static/js/main.4388b0cf.chunk.js.map",
|
||||
"runtime-main.js": "/static/js/runtime-main.8d7962eb.js",
|
||||
"runtime-main.js.map": "/static/js/runtime-main.8d7962eb.js.map",
|
||||
"static/css/2.015dc0ab.chunk.css": "/static/css/2.015dc0ab.chunk.css",
|
||||
"static/js/2.6d12aa16.chunk.js": "/static/js/2.6d12aa16.chunk.js",
|
||||
"static/js/2.6d12aa16.chunk.js.map": "/static/js/2.6d12aa16.chunk.js.map",
|
||||
"static/js/3.8e9312c3.chunk.js": "/static/js/3.8e9312c3.chunk.js",
|
||||
"static/js/3.8e9312c3.chunk.js.map": "/static/js/3.8e9312c3.chunk.js.map",
|
||||
"index.html": "/index.html",
|
||||
"static/css/2.015dc0ab.chunk.css.map": "/static/css/2.015dc0ab.chunk.css.map",
|
||||
"static/css/main.895ab0f5.chunk.css.map": "/static/css/main.895ab0f5.chunk.css.map",
|
||||
"static/js/2.6d12aa16.chunk.js.LICENSE.txt": "/static/js/2.6d12aa16.chunk.js.LICENSE.txt"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/js/runtime-main.8d7962eb.js",
|
||||
"static/css/2.015dc0ab.chunk.css",
|
||||
"static/js/2.6d12aa16.chunk.js",
|
||||
"static/css/main.895ab0f5.chunk.css",
|
||||
"static/js/main.4388b0cf.chunk.js"
|
||||
]
|
||||
}
|
BIN
client/build/favicon.ico
Normal file
After Width: | Height: | Size: 3.8 KiB |
5
client/build/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<text y=".9em" font-size="90">
|
||||
💩
|
||||
</text>
|
||||
</svg>
|
After Width: | Height: | Size: 110 B |
1
client/build/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="3D Shock by Tanner and Elijah enterprises"/><link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡</text></svg>"><link rel="manifest" href="/manifest.json"/><title>3D Shock!</title><link href="/static/css/2.015dc0ab.chunk.css" rel="stylesheet"><link href="/static/css/main.895ab0f5.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function t(t){for(var n,i,a=t[0],c=t[1],l=t[2],s=0,p=[];s<a.length;s++)i=a[s],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&p.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(t);p.length;)p.shift()();return u.push.apply(u,l||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,a=1;a<r.length;a++){var c=r[a];0!==o[c]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.e=function(e){var t=[],r=o[e];if(0!==r)if(r)t.push(r[2]);else{var n=new Promise((function(t,n){r=o[e]=[t,n]}));t.push(r[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+"static/js/"+({}[e]||e)+"."+{3:"8e9312c3"}[e]+".chunk.js"}(e);var c=new Error;u=function(t){a.onerror=a.onload=null,clearTimeout(l);var r=o[e];if(0!==r){if(r){var n=t&&("load"===t.type?"missing":t.type),u=t&&t.target&&t.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,r[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(t)},i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/",i.oe=function(e){throw console.error(e),e};var a=this.webpackJsonpclient=this.webpackJsonpclient||[],c=a.push.bind(a);a.push=t,a=a.slice();for(var l=0;l<a.length;l++)t(a[l]);var f=c;r()}([])</script><script src="/static/js/2.6d12aa16.chunk.js"></script><script src="/static/js/main.4388b0cf.chunk.js"></script></body></html>
|
BIN
client/build/logo192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
client/build/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
client/build/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
client/build/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
10
client/build/static/css/2.015dc0ab.chunk.css
Normal file
1
client/build/static/css/2.015dc0ab.chunk.css.map
Normal file
2
client/build/static/css/main.895ab0f5.chunk.css
Normal file
@@ -0,0 +1,2 @@
|
||||
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,"Courier New",monospace}form{max-width:500px;-webkit-flex-direction:column;flex-direction:column}form,form label{display:-webkit-flex;display:flex;margin:auto}form label{width:100%;-webkit-flex-direction:row;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}:root{--color-primary:#282c34}.App-header{background-color:#282c34;background-color:var(--color-primary);min-height:100vh;display:-webkit-flex;display:flex;-webkit-flex-direction:column;flex-direction:column;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center;font-size:calc(10px + 2vmin);color:#fff}.App-link{color:#61dafb}@-webkit-keyframes App-logo-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes App-logo-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.error{color:red}.photo-wall{display:-webkit-flex;display:flex;-webkit-justify-content:center;justify-content:center;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:2rem}.dashboard-form{margin:auto}.ant-card{margin:1rem}.ant-card img{max-width:250px}.loading-bar-container{background:#282c34;background:var(--color-primary);width:100%;height:30px;min-height:30px}.loading-bar{transition:all .5s linear;background:#6f6;height:100%}.page-head{background:#282c34;background:var(--color-primary);color:#fff!important;margin-top:0;padding:1rem}.session-toolbar button{margin:0 .5rem;padding:.5rem 1rem;height:100%;font-weight:700;text-transform:uppercase;letter-spacing:.05rem}.ant-modal-content{border-radius:1rem;padding:0!important}.ant-modal-body img{border-radius:.5rem}.ant-modal-close{font-weight:700}.ant-card{border:1px solid #282c34;border:1px solid var(--color-primary);border-radius:.2rem}.ant-card-head{background:#282c34;background:var(--color-primary);color:#fff;font-weight:700;text-transform:uppercase;letter-spacing:.2rem}.session-toolbar{-webkit-justify-content:center;justify-content:center;width:60%;margin:1rem auto}.session-toolbar h3{font-weight:700;color:#fff;margin-right:1rem}.slider{width:400px;margin:auto 1rem}.toolbar{padding:.5rem;background:#282c34;background:var(--color-primary)}.client-info{-webkit-justify-content:center;justify-content:center;width:60%;margin:1rem auto}.client-info span{margin:auto 1rem}
|
||||
/*# sourceMappingURL=main.895ab0f5.chunk.css.map */
|
1
client/build/static/css/main.895ab0f5.chunk.css.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack://src/index.css","webpack://src/App.css"],"names":[],"mappings":"AAAA,KACE,QAAS,CACT,mJAEY,CACZ,kCAAmC,CACnC,iCACF,CAEA,KACE,yEAEF,CAEA,KAEE,eAAgB,CAEhB,6BAAsB,CAAtB,qBACF,CAEA,gBANE,oBAAa,CAAb,YAAa,CAEb,WAUF,CANA,WACE,UAAW,CAEX,0BAAmB,CAAnB,kBAAmB,CACnB,qCAA8B,CAA9B,6BAEF,CC3BA,MACE,uBACF,CAEA,YACE,wBAAsC,CAAtC,qCAAsC,CACtC,gBAAiB,CACjB,oBAAa,CAAb,YAAa,CACb,6BAAsB,CAAtB,qBAAsB,CACtB,0BAAmB,CAAnB,kBAAmB,CACnB,8BAAuB,CAAvB,sBAAuB,CACvB,4BAA6B,CAC7B,UACF,CAEA,UACE,aACF,CAEA,iCACE,GACE,8BAAuB,CAAvB,sBACF,CACA,GACE,+BAAyB,CAAzB,uBACF,CACF,CAPA,yBACE,GACE,8BAAuB,CAAvB,sBACF,CACA,GACE,+BAAyB,CAAzB,uBACF,CACF,CAEA,OACE,SACF,CAEA,YACE,oBAAa,CAAb,YAAa,CACb,8BAAuB,CAAvB,sBAAuB,CACvB,sBAAe,CAAf,cAAe,CACf,WACF,CAEA,gBACE,WACF,CAEA,UACE,WACF,CAEA,cACE,eACF,CAEA,uBACE,kBAAgC,CAAhC,+BAAgC,CAChC,UAAW,CACX,WAAY,CACZ,eACF,CAEA,aACE,yBAA2B,CAC3B,eAAmB,CACnB,WACF,CAEA,WACE,kBAAgC,CAAhC,+BAAgC,CAChC,oBAAuB,CACvB,YAAa,CACb,YACF,CAEA,wBACE,cAAgB,CAChB,kBAAoB,CACpB,WAAY,CACZ,eAAiB,CACjB,wBAAyB,CACzB,qBACF,CAEA,mBACE,kBAAmB,CACnB,mBACF,CAEA,oBACE,mBACF,CAEA,iBACE,eACF,CAEA,UACE,wBAAsC,CAAtC,qCAAsC,CACtC,mBACF,CAEA,eACE,kBAAgC,CAAhC,+BAAgC,CAChC,UAAY,CACZ,eAAiB,CACjB,wBAAyB,CACzB,oBACF,CAEA,iBACE,8BAAuB,CAAvB,sBAAuB,CACvB,SAAU,CACV,gBACF,CAEA,oBACE,eAAiB,CACjB,UAAY,CACZ,iBACF,CAEA,QACE,WAAY,CACZ,gBACF,CAEA,SACE,aAAe,CACf,kBAAgC,CAAhC,+BACF,CAEA,aACE,8BAAuB,CAAvB,sBAAuB,CACvB,SAAU,CACV,gBACF,CAEA,kBACE,gBACF","file":"main.895ab0f5.chunk.css","sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\",\n \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, \"Courier New\",\n monospace;\n}\n\nform {\n display: flex;\n max-width: 500px;\n margin: auto;\n flex-direction: column;\n}\n\nform label {\n width: 100%; \n display: flex;\n flex-direction: row;\n justify-content: space-between;\n margin: auto;\n}\n\n",":root {\n --color-primary: #282c34;\n}\n\n.App-header {\n background-color: var(--color-primary);\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n font-size: calc(10px + 2vmin);\n color: white;\n}\n\n.App-link {\n color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\n.error {\n color: red;\n}\n\n.photo-wall {\n display: flex;\n justify-content: center;\n flex-wrap: wrap;\n margin: 2rem;\n}\n\n.dashboard-form {\n margin: auto;\n}\n\n.ant-card {\n margin: 1rem;\n}\n\n.ant-card img {\n max-width: 250px;\n}\n\n.loading-bar-container {\n background: var(--color-primary);\n width: 100%;\n height: 30px;\n min-height: 30px;\n}\n\n.loading-bar {\n transition: all 0.5s linear;\n background: #66ff66;\n height: 100%;\n}\n\n.page-head {\n background: var(--color-primary);\n color: white !important;\n margin-top: 0;\n padding: 1rem;\n}\n\n.session-toolbar button {\n margin: 0 0.5rem;\n padding: 0.5rem 1rem;\n height: 100%;\n font-weight: bold;\n text-transform: uppercase;\n letter-spacing: 0.05rem;\n}\n\n.ant-modal-content {\n border-radius: 1rem;\n padding: 0 !important;\n}\n\n.ant-modal-body img {\n border-radius: 0.5rem;\n}\n\n.ant-modal-close {\n font-weight: bold;\n}\n\n.ant-card {\n border: 1px solid var(--color-primary);\n border-radius: 0.2rem;\n}\n\n.ant-card-head {\n background: var(--color-primary);\n color: white;\n font-weight: bold;\n text-transform: uppercase;\n letter-spacing: 0.2rem;\n}\n\n.session-toolbar {\n justify-content: center;\n width: 60%;\n margin: 1rem auto;\n}\n\n.session-toolbar h3 {\n font-weight: bold;\n color: white;\n margin-right: 1rem;\n}\n\n.slider {\n width: 400px;\n margin: auto 1rem;\n}\n\n.toolbar {\n padding: 0.5rem;\n background: var(--color-primary);\n}\n\n.client-info {\n justify-content: center;\n width: 60%;\n margin: 1rem auto;\n}\n\n.client-info span {\n margin: auto 1rem;\n}\n"]}
|
3
client/build/static/js/2.6d12aa16.chunk.js
Normal file
56
client/build/static/js/2.6d12aa16.chunk.js.LICENSE.txt
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
object-assign
|
||||
(c) Sindre Sorhus
|
||||
@license MIT
|
||||
*/
|
||||
|
||||
/*!
|
||||
Copyright (c) 2017 Jed Watson.
|
||||
Licensed under the MIT License (MIT), see
|
||||
http://jedwatson.github.io/classnames
|
||||
*/
|
||||
|
||||
/** @license React v0.20.1
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.13.1
|
||||
* react-is.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v17.0.1
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v17.0.1
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v17.0.1
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
1
client/build/static/js/2.6d12aa16.chunk.js.map
Normal file
2
client/build/static/js/3.8e9312c3.chunk.js
Normal file
@@ -0,0 +1,2 @@
|
||||
(this.webpackJsonpclient=this.webpackJsonpclient||[]).push([[3],{399:function(t,n,e){"use strict";e.r(n),e.d(n,"getCLS",(function(){return v})),e.d(n,"getFCP",(function(){return y})),e.d(n,"getFID",(function(){return k})),e.d(n,"getLCP",(function(){return C})),e.d(n,"getTTFB",(function(){return P}));var i,a,r,o,c=function(t,n){return{name:t,value:void 0===n?-1:n,delta:0,entries:[],id:"v1-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},u=function(t,n){try{if(PerformanceObserver.supportedEntryTypes.includes(t)){var e=new PerformanceObserver((function(t){return t.getEntries().map(n)}));return e.observe({type:t,buffered:!0}),e}}catch(t){}},f=!1,s=function(t,n){f||"undefined"!=typeof InstallTrigger||(addEventListener("beforeunload",(function(){})),f=!0),addEventListener("visibilitychange",(function e(i){"hidden"===document.visibilityState&&(t(i),n&&removeEventListener("visibilitychange",e,!0))}),!0)},d=function(t){addEventListener("pageshow",(function(n){n.persisted&&t(n)}),!0)},m="function"==typeof WeakSet?new WeakSet:new Set,p=function(t,n,e){var i;return function(){n.value>=0&&(e||m.has(n)||"hidden"===document.visibilityState)&&(n.delta=n.value-(i||0),(n.delta||void 0===i)&&(i=n.value,t(n)))}},v=function(t,n){var e,i=c("CLS",0),a=function(t){t.hadRecentInput||(i.value+=t.value,i.entries.push(t),e())},r=u("layout-shift",a);r&&(e=p(t,i,n),s((function(){r.takeRecords().map(a),e()})),d((function(){i=c("CLS",0),e=p(t,i,n)})))},l=-1,h=function(){return"hidden"===document.visibilityState?0:1/0},S=function(){s((function(t){var n=t.timeStamp;l=n}),!0)},g=function(){return l<0&&(l=h(),S(),d((function(){setTimeout((function(){l=h(),S()}),0)}))),{get timeStamp(){return l}}},y=function(t,n){var e,i=g(),a=c("FCP"),r=u("paint",(function(t){"first-contentful-paint"===t.name&&(r&&r.disconnect(),t.startTime<i.timeStamp&&(a.value=t.startTime,a.entries.push(t),m.add(a),e()))}));r&&(e=p(t,a,n),d((function(i){a=c("FCP"),e=p(t,a,n),requestAnimationFrame((function(){requestAnimationFrame((function(){a.value=performance.now()-i.timeStamp,m.add(a),e()}))}))})))},w={passive:!0,capture:!0},E=new Date,L=function(t,n){i||(i=n,a=t,r=new Date,F(removeEventListener),T())},T=function(){if(a>=0&&a<r-E){var t={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+a};o.forEach((function(n){n(t)})),o=[]}},b=function(t){if(t.cancelable){var n=(t.timeStamp>1e12?new Date:performance.now())-t.timeStamp;"pointerdown"==t.type?function(t,n){var e=function(){L(t,n),a()},i=function(){a()},a=function(){removeEventListener("pointerup",e,w),removeEventListener("pointercancel",i,w)};addEventListener("pointerup",e,w),addEventListener("pointercancel",i,w)}(n,t):L(n,t)}},F=function(t){["mousedown","keydown","touchstart","pointerdown"].forEach((function(n){return t(n,b,w)}))},k=function(t,n){var e,r=g(),f=c("FID"),v=function(t){t.startTime<r.timeStamp&&(f.value=t.processingStart-t.startTime,f.entries.push(t),m.add(f),e())},l=u("first-input",v);e=p(t,f,n),l&&s((function(){l.takeRecords().map(v),l.disconnect()}),!0),l&&d((function(){var r;f=c("FID"),e=p(t,f,n),o=[],a=-1,i=null,F(addEventListener),r=v,o.push(r),T()}))},C=function(t,n){var e,i=g(),a=c("LCP"),r=function(t){var n=t.startTime;n<i.timeStamp&&(a.value=n,a.entries.push(t)),e()},o=u("largest-contentful-paint",r);if(o){e=p(t,a,n);var f=function(){m.has(a)||(o.takeRecords().map(r),o.disconnect(),m.add(a),e())};["keydown","click"].forEach((function(t){addEventListener(t,f,{once:!0,capture:!0})})),s(f,!0),d((function(i){a=c("LCP"),e=p(t,a,n),requestAnimationFrame((function(){requestAnimationFrame((function(){a.value=performance.now()-i.timeStamp,m.add(a),e()}))}))}))}},P=function(t){var n,e=c("TTFB");n=function(){try{var n=performance.getEntriesByType("navigation")[0]||function(){var t=performance.timing,n={entryType:"navigation",startTime:0};for(var e in t)"navigationStart"!==e&&"toJSON"!==e&&(n[e]=Math.max(t[e]-t.navigationStart,0));return n}();e.value=e.delta=n.responseStart,e.entries=[n],t(e)}catch(t){}},"complete"===document.readyState?setTimeout(n,0):addEventListener("pageshow",n)}}}]);
|
||||
//# sourceMappingURL=3.8e9312c3.chunk.js.map
|
1
client/build/static/js/3.8e9312c3.chunk.js.map
Normal file
2
client/build/static/js/main.4388b0cf.chunk.js
Normal file
1
client/build/static/js/main.4388b0cf.chunk.js.map
Normal file
2
client/build/static/js/runtime-main.8d7962eb.js
Normal file
@@ -0,0 +1,2 @@
|
||||
!function(e){function t(t){for(var n,i,a=t[0],c=t[1],l=t[2],s=0,p=[];s<a.length;s++)i=a[s],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&p.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(t);p.length;)p.shift()();return u.push.apply(u,l||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,a=1;a<r.length;a++){var c=r[a];0!==o[c]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.e=function(e){var t=[],r=o[e];if(0!==r)if(r)t.push(r[2]);else{var n=new Promise((function(t,n){r=o[e]=[t,n]}));t.push(r[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+"static/js/"+({}[e]||e)+"."+{3:"8e9312c3"}[e]+".chunk.js"}(e);var c=new Error;u=function(t){a.onerror=a.onload=null,clearTimeout(l);var r=o[e];if(0!==r){if(r){var n=t&&("load"===t.type?"missing":t.type),u=t&&t.target&&t.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,r[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(t)},i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/",i.oe=function(e){throw console.error(e),e};var a=this.webpackJsonpclient=this.webpackJsonpclient||[],c=a.push.bind(a);a.push=t,a=a.slice();for(var l=0;l<a.length;l++)t(a[l]);var f=c;r()}([]);
|
||||
//# sourceMappingURL=runtime-main.8d7962eb.js.map
|
1
client/build/static/js/runtime-main.8d7962eb.js.map
Normal file
19831
client/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"antd": "^4.13.1",
|
||||
"axios": "^0.21.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
|
BIN
client/public/1.jpg
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
client/public/2.jpg
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
client/public/3.jpg
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
client/public/4.jpg
Normal file
After Width: | Height: | Size: 3.2 KiB |
5
client/public/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<text y=".9em" font-size="90">
|
||||
💩
|
||||
</text>
|
||||
</svg>
|
After Width: | Height: | Size: 110 B |
@@ -2,14 +2,13 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
content="3D Shock by Tanner and Elijah enterprises"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡</text></svg>">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
@@ -24,7 +23,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>3D Shock!</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
@@ -1,20 +1,9 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
:root {
|
||||
--color-primary: #282c34;
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
background-color: var(--color-primary);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -36,3 +25,113 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.photo-wall {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-form {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.ant-card img {
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.loading-bar-container {
|
||||
background: var(--color-primary);
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
transition: all 0.5s linear;
|
||||
background: #66ff66;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
background: var(--color-primary);
|
||||
color: white !important;
|
||||
margin-top: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.session-toolbar button {
|
||||
margin: 0 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
height: 100%;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05rem;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: 1rem;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-modal-body img {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.ant-card-head {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2rem;
|
||||
}
|
||||
|
||||
.session-toolbar {
|
||||
justify-content: center;
|
||||
width: 60%;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.session-toolbar h3 {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 400px;
|
||||
margin: auto 1rem;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 0.5rem;
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.client-info {
|
||||
justify-content: center;
|
||||
width: 60%;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.client-info span {
|
||||
margin: auto 1rem;
|
||||
}
|
||||
|
@@ -5,15 +5,15 @@ import { BrowserRouter, Switch, Route } from 'react-router-dom'
|
||||
import { Dashboard } from './pages/Dashboard'
|
||||
import { Session } from './pages/Session'
|
||||
|
||||
console.log('ENV', process.env.NODE_ENV)
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div className="App">
|
||||
<Switch>
|
||||
<Route path="/" component={Dashboard} />
|
||||
<Route path="/sessions/:clientId" component={Session} />
|
||||
<p>landing page</p>
|
||||
<p>session</p>
|
||||
<Route exact path="/" component={Dashboard} />
|
||||
</Switch>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
|
@@ -1,33 +1,72 @@
|
||||
import settings from '../settings'
|
||||
import { Client } from '../types'
|
||||
import { Client, Timings } from '../types'
|
||||
import axios from 'axios'
|
||||
import { message } from 'antd'
|
||||
import { Status } from '../components/StatusChip'
|
||||
|
||||
const { apiUrl } = settings
|
||||
const dev = process.env.NODE_ENV === 'development'
|
||||
|
||||
export const createClient = async (body: Client) => {
|
||||
await axios.post(`${apiUrl}/clients`, body)
|
||||
if (dev) {
|
||||
const host = 'http://192.168.1.114:5000'
|
||||
axios.defaults.baseURL = host
|
||||
}
|
||||
|
||||
const mock = false
|
||||
|
||||
export const createClient = async (body: Omit<Client, 'has_photos'>) => {
|
||||
if (mock) return 'test'
|
||||
const res = await axios.post<{ client_id: string }>(`/api/clients`, body)
|
||||
return res.data.client_id
|
||||
}
|
||||
|
||||
export const getClient = async (id: string): Promise<Client> => {
|
||||
if (mock)
|
||||
return {
|
||||
name: 'Test Client',
|
||||
has_photos: false,
|
||||
email: 'test@test.test',
|
||||
phone: 1234567890,
|
||||
}
|
||||
const res = await axios.get<Client>(`/api/clients/${id}`)
|
||||
return res.data
|
||||
}
|
||||
export const startSession = async (clientId: string, timings: Timings) => {
|
||||
try {
|
||||
const res = await axios.post(`/api/clients/${clientId}/session`, timings)
|
||||
return res.data
|
||||
} catch (err) {
|
||||
message.error('Something went wrong, check connection with the machine')
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
export const getSession = async (clientId: string) => {
|
||||
const res = await axios.get<{ photos: string[] }>(
|
||||
`/api/clients/${clientId}/session`,
|
||||
)
|
||||
return res.data // session data
|
||||
}
|
||||
|
||||
export const killSession = async (clientId: string) => {
|
||||
await axios.delete(`/api/clients/${clientId}/session`)
|
||||
}
|
||||
|
||||
export const restartSession = async (clientId: string, timings: Timings) => {
|
||||
await killSession(clientId)
|
||||
await startSession(clientId, timings)
|
||||
}
|
||||
|
||||
// TOOD: Get status
|
||||
|
||||
export const getStatus = async (): Promise<Status> => {
|
||||
const res = await axios.get<{ status: Status }>('/api/status')
|
||||
return res.data.status
|
||||
}
|
||||
|
||||
// Someday
|
||||
|
||||
export const getClients = async (): Promise<Client[]> => {
|
||||
const res = await axios.post<Client[]>(`${apiUrl}/clients`)
|
||||
return res.data as Client[]
|
||||
}
|
||||
|
||||
export const beginCapture = async (clientId: string) => {
|
||||
const res = await axios.post(`${apiUrl}/clients/${clientId}/session`)
|
||||
return res.data as string // capture id
|
||||
}
|
||||
|
||||
export const getCapture = async (
|
||||
clientId: string,
|
||||
): Promise<null | string[]> => {
|
||||
const res = await axios.get(`${apiUrl}/clients/${clientId}/session`)
|
||||
return res.data as null | string[]
|
||||
}
|
||||
|
||||
export const retryCapture = async (clientId: string) => {
|
||||
await axios.delete(`${apiUrl}/clients/${clientId}/session`)
|
||||
await axios.post(`${apiUrl}/clients/${clientId}/session`)
|
||||
const res = await axios.get<Client[]>(`/api/clients`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export const cleanup = () => {
|
||||
|
23
client/src/components/ScrollToTop.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Button } from 'antd'
|
||||
import React from 'react'
|
||||
|
||||
export const ScrollToTop = () => {
|
||||
const handleClick = () => {
|
||||
window.scrollTo(0, 0)
|
||||
console.log('')
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
⤴️ Scroll To Top
|
||||
</Button>
|
||||
)
|
||||
}
|
131
client/src/components/SessionPictures.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Card, Modal, Row, Select, Typography } from 'antd'
|
||||
import { getSession } from '../api'
|
||||
import { ScrollToTop } from './ScrollToTop'
|
||||
|
||||
type Props = {
|
||||
clientId: string
|
||||
}
|
||||
|
||||
export const SessionPictures = ({ clientId }: Props) => {
|
||||
const [urls, setUrls] = useState<string[] | null>(null)
|
||||
const [activeUrl, setActiveUrl] = useState<string | null>(null)
|
||||
const [focusPhotos, setFocusPhotos] = useState<string[]>(
|
||||
JSON.parse(window.localStorage.getItem('focusPhotos') || '[]'),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const get = async () => {
|
||||
if (urls && urls.length >= 89 * 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const { photos } = await getSession(clientId)
|
||||
if (photos.length) setUrls(photos)
|
||||
}
|
||||
|
||||
const interval = setInterval(get, 250)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [clientId, urls])
|
||||
|
||||
const closeModal = () => setActiveUrl(null)
|
||||
|
||||
const host =
|
||||
process.env.NODE_ENV === 'development' ? 'http://192.168.1.107:5000' : ''
|
||||
|
||||
if (!urls?.length) return null
|
||||
|
||||
const photos = urls.sort((a, b) =>
|
||||
a.split('_')[0].localeCompare(b.split('_')[0]),
|
||||
)
|
||||
|
||||
const u = urls.length / 89
|
||||
|
||||
const handleSelect = (v: string[]) => {
|
||||
console.log('SEelcted', v)
|
||||
window.localStorage.setItem('focusPhotos', JSON.stringify(v))
|
||||
setFocusPhotos(v)
|
||||
}
|
||||
|
||||
const filteredPhotos = photos.filter((name) => {
|
||||
const num = name.split('_')[0]
|
||||
return focusPhotos.includes(num)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
visible={!!activeUrl}
|
||||
onOk={closeModal}
|
||||
footer={null}
|
||||
onCancel={closeModal}
|
||||
width="50%"
|
||||
>
|
||||
<img
|
||||
width="100%"
|
||||
onClick={closeModal}
|
||||
src={`${host}/output/${clientId}/${activeUrl}`}
|
||||
alt="large modal"
|
||||
></img>
|
||||
</Modal>
|
||||
<Row
|
||||
align="middle"
|
||||
justify="space-around"
|
||||
style={{ display: 'flex', width: '100%' }}
|
||||
>
|
||||
<Typography.Title style={{ margin: '0.5rem 1rem 0.7rem' }} level={3}>
|
||||
Session Pictures
|
||||
</Typography.Title>
|
||||
<Typography.Text>{urls.length}/ 89 loaded</Typography.Text>
|
||||
<Typography.Text>Select Featured Photos:</Typography.Text>
|
||||
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder="Please select featured"
|
||||
style={{ width: '35%' }}
|
||||
defaultValue={focusPhotos}
|
||||
value={focusPhotos}
|
||||
onChange={handleSelect}
|
||||
>
|
||||
{photos.map((name) => {
|
||||
const val = name.split('_')[0]
|
||||
return <Select.Option value={val}>{val}</Select.Option>
|
||||
})}
|
||||
</Select>
|
||||
</Row>
|
||||
|
||||
<div className="loading-bar-container">
|
||||
<div
|
||||
className="loading-bar"
|
||||
style={{
|
||||
width: `${u * 100}%`,
|
||||
background: `hsl(${Math.floor(u * 120)}, 90%, 70%)`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="featured-photos" style={{ marginTop: '2rem' }}>
|
||||
{filteredPhotos.map((src) => (
|
||||
<img
|
||||
onClick={() => setActiveUrl(src)}
|
||||
src={`${host}/output/${clientId}/${src}`}
|
||||
alt="lol"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="photo-wall">
|
||||
{photos.map((src) => (
|
||||
<Card key={src} className="photo" title={src.split('_')[0]}>
|
||||
<img
|
||||
onClick={() => setActiveUrl(src)}
|
||||
src={`${host}/output/${clientId}/${src}`}
|
||||
alt="lol"
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<ScrollToTop />
|
||||
</>
|
||||
)
|
||||
}
|
47
client/src/components/StatusChip.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Tag } from 'antd'
|
||||
import { PresetColorType } from 'antd/lib/_util/colors'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getStatus } from '../api'
|
||||
|
||||
export enum Status {
|
||||
'Standing By...',
|
||||
'Warming Up...',
|
||||
'Capturing Photo',
|
||||
'Capturing Grid',
|
||||
'Writing To Disk',
|
||||
'Downloading!',
|
||||
}
|
||||
|
||||
const colors: Partial<PresetColorType>[] = [
|
||||
'lime',
|
||||
'gold',
|
||||
'volcano',
|
||||
'magenta',
|
||||
'geekblue',
|
||||
]
|
||||
|
||||
type Props = {
|
||||
poll: boolean
|
||||
}
|
||||
|
||||
export const StatusChip = ({ poll }: Props) => {
|
||||
const [status, setStatus] = useState<Status>(Status['Standing By...'])
|
||||
|
||||
useEffect(() => {
|
||||
const get = async () => {
|
||||
if (!poll) return
|
||||
const status = await getStatus()
|
||||
setStatus(status)
|
||||
}
|
||||
|
||||
const interval = setInterval(get, 1000 / 4)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [poll])
|
||||
|
||||
return (
|
||||
<Tag color={colors[status]} style={{ display: 'flex' }}>
|
||||
<span style={{ margin: 'auto' }}>{Status[status]}</span>
|
||||
</Tag>
|
||||
)
|
||||
}
|
21
client/src/data/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Client } from '../types'
|
||||
|
||||
export const clients: Client[] = [
|
||||
{
|
||||
name: 'Elijah',
|
||||
email: 'elijah@elijah.com',
|
||||
phone: 4039876543,
|
||||
has_photos: false,
|
||||
},
|
||||
{
|
||||
name: 'Tanner',
|
||||
email: 'tanner@tanner.com',
|
||||
phone: 4031234567,
|
||||
has_photos: true,
|
||||
},
|
||||
]
|
||||
export const client: Client = clients[0]
|
||||
|
||||
export const session: { photos: string[] } = {
|
||||
photos: ['/images/1.jpg', '/images/2.jpg', '/images/3.jpg', '/images/4.jpg'],
|
||||
}
|
@@ -11,3 +11,19 @@ code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
form label {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
|
@@ -1,17 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import './index.css'
|
||||
import 'antd/dist/antd.css'
|
||||
import App from './App'
|
||||
import reportWebVitals from './reportWebVitals'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
ReactDOM.render(<App />, document.getElementById('root'))
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
reportWebVitals()
|
||||
|
@@ -1,45 +1,78 @@
|
||||
import React from 'react'
|
||||
import { Button, Divider, Form, Input, message, Row, Typography } from 'antd'
|
||||
import FormItem from 'antd/lib/form/FormItem'
|
||||
import { Content } from 'antd/lib/layout/layout'
|
||||
import { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { createClient } from '../api'
|
||||
|
||||
type FormData = {
|
||||
name: string
|
||||
email: string
|
||||
phone: string
|
||||
}
|
||||
|
||||
export const Dashboard = () => {
|
||||
const history = useHistory()
|
||||
const [name, setName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [phone, setPhone] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [form] = Form.useForm<FormData>()
|
||||
|
||||
const handleReset = () => {
|
||||
//
|
||||
form.resetFields()
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// phone number is stripped for numbers
|
||||
const handleSubmit = async (values: FormData) => {
|
||||
if (values.phone.length < 10) {
|
||||
// helpful message
|
||||
message.error('Check all fields!')
|
||||
setError('Phone number needs to be a length of at least 10')
|
||||
return
|
||||
}
|
||||
|
||||
await createClient({ name, email, phone })
|
||||
history.push(`/sessions/${phone}`)
|
||||
const client_id = await createClient({
|
||||
name: values.name,
|
||||
email: values.email,
|
||||
phone: parseInt(values.phone.replace(/\D/g, '')),
|
||||
})
|
||||
|
||||
history.push(`/sessions/${client_id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label htmlFor="name">
|
||||
Name:
|
||||
<input type="text" name="name" />
|
||||
</label>
|
||||
<label htmlFor="email">
|
||||
Email:
|
||||
<input type="email" name="email" />
|
||||
</label>
|
||||
<label htmlFor="phone">
|
||||
Phone:
|
||||
<input type="phone" name="phone" />
|
||||
</label>
|
||||
<button type="submit">Start Session</button>
|
||||
<button onClick={handleReset}>Reset</button>
|
||||
</form>
|
||||
<div>TODO: List of past sessions for review?</div>
|
||||
</div>
|
||||
<Content>
|
||||
<Typography.Title className="page-head" level={3}>
|
||||
Dashboard
|
||||
</Typography.Title>
|
||||
<Divider />
|
||||
<Form
|
||||
form={form}
|
||||
className="dashboard-form"
|
||||
onFinish={handleSubmit}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
>
|
||||
<Typography.Paragraph style={{ textAlign: 'center' }}>
|
||||
Enter the name, email and phone number of the subject
|
||||
</Typography.Paragraph>
|
||||
<FormItem label="name" name="name">
|
||||
<Input minLength={3} />
|
||||
</FormItem>
|
||||
<FormItem label="email" name="email">
|
||||
<Input type="email" />
|
||||
</FormItem>
|
||||
<FormItem label="phone" name="phone">
|
||||
<Input type="tel" minLength={10} />
|
||||
</FormItem>
|
||||
<Row justify="space-between">
|
||||
<Button danger onClick={handleReset}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button htmlType="submit" type="primary">
|
||||
Start Session
|
||||
</Button>
|
||||
</Row>
|
||||
{error && <p className="error">{error}</p>}
|
||||
</Form>
|
||||
</Content>
|
||||
)
|
||||
}
|
||||
|
@@ -1,15 +1,142 @@
|
||||
import React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { RouteComponentProps, useHistory } from 'react-router-dom'
|
||||
import { getClient, killSession, restartSession, startSession } from '../api'
|
||||
import { SessionPictures } from '../components/SessionPictures'
|
||||
import { StatusChip } from '../components/StatusChip'
|
||||
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { Content } from 'antd/lib/layout/layout'
|
||||
import { Client } from '../types'
|
||||
|
||||
type Props = RouteComponentProps<{ clientId: string }>
|
||||
|
||||
export const Session = (props: Props) => {
|
||||
const history = useHistory()
|
||||
const { clientId } = props.match.params
|
||||
const [client, setClient] = useState<Client | null>(null)
|
||||
const [active, setActive] = useState(false)
|
||||
const [lightTime, setLightTime] = useState(
|
||||
parseInt(window.localStorage.getItem('lightTime') || '5000'),
|
||||
)
|
||||
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const handleTimingUpdate = (n: number) => {
|
||||
window.localStorage.setItem('lightTime', n.toString())
|
||||
setLightTime(n)
|
||||
}
|
||||
|
||||
useEffect(() => {})
|
||||
const handleStartSession = async () => {
|
||||
message.loading('Photo sequence starting! Stand by...')
|
||||
await startSession(clientId, { light_time: lightTime })
|
||||
setActive(true)
|
||||
}
|
||||
|
||||
return <div>Session {clientId}</div>
|
||||
const handleRestartSession = async () => {
|
||||
setActive(false)
|
||||
message.loading(
|
||||
'Deleting photos & restarting capture sequence! Stand by...',
|
||||
)
|
||||
await restartSession(clientId, { light_time: lightTime })
|
||||
setActive(true)
|
||||
}
|
||||
|
||||
const handleExit = async () => {
|
||||
history.push('/')
|
||||
}
|
||||
|
||||
const handleNuke = async () => {
|
||||
await killSession(clientId)
|
||||
message.success('Photos Deleted! Going back to dashboard')
|
||||
history.push('/')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const get = async () => {
|
||||
const client = await getClient(clientId)
|
||||
setClient(client)
|
||||
if (client.has_photos) setActive(true)
|
||||
}
|
||||
|
||||
get()
|
||||
}, [clientId])
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<Typography.Title className="page-head" level={3}>
|
||||
Session View
|
||||
</Typography.Title>
|
||||
|
||||
<Row className="client-info">
|
||||
<Typography.Text>
|
||||
<strong>Name:</strong> {client?.name}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<strong>Email:</strong> {client?.email}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<strong>Phone:</strong> {client?.phone}
|
||||
</Typography.Text>
|
||||
</Row>
|
||||
<div className="toolbar">
|
||||
<Row justify="center" className="session-toolbar">
|
||||
<Button key="finish" onClick={handleExit}>
|
||||
Back To Dashboard
|
||||
</Button>
|
||||
<Button
|
||||
key="startsession"
|
||||
disabled={active}
|
||||
type="primary"
|
||||
onClick={handleStartSession}
|
||||
>
|
||||
Capture
|
||||
</Button>
|
||||
<Popconfirm
|
||||
disabled={!active}
|
||||
key="retry"
|
||||
title="Re-capture set?"
|
||||
onConfirm={handleRestartSession}
|
||||
>
|
||||
<Button type="default" disabled={!active}>
|
||||
Retry Capture
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
key="nuke"
|
||||
disabled={!active}
|
||||
title="Delete all photos and return to dashboard?"
|
||||
onConfirm={handleNuke}
|
||||
>
|
||||
<Button danger disabled={!active}>
|
||||
Abort Session
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
||||
<StatusChip poll={true} />
|
||||
</Row>
|
||||
<Row className="session-toolbar">
|
||||
<h3>Light Duration (ms)</h3>
|
||||
<InputNumber value={lightTime} onChange={handleTimingUpdate} />
|
||||
<Input
|
||||
className="slider"
|
||||
type="range"
|
||||
onChange={(e) => handleTimingUpdate(parseInt(e.target.value))}
|
||||
value={lightTime}
|
||||
min={500}
|
||||
max={10000}
|
||||
step={500}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
<Row className="controls">
|
||||
{active && <SessionPictures clientId={clientId} />}
|
||||
</Row>
|
||||
</Content>
|
||||
)
|
||||
}
|
||||
|
@@ -1,4 +0,0 @@
|
||||
export default {
|
||||
apiUrl: "http://localhost/api",
|
||||
port: 4442,
|
||||
};
|
@@ -1,9 +1,14 @@
|
||||
export type Client = {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: number;
|
||||
};
|
||||
name: string
|
||||
email: string
|
||||
phone: number
|
||||
has_photos: boolean
|
||||
}
|
||||
|
||||
export type Session = {
|
||||
timestamp: number;
|
||||
};
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export type Timings = {
|
||||
light_time: number // 5000
|
||||
}
|
||||
|
18913
client/yarn.lock
BIN
network-settings.png
Normal file
After Width: | Height: | Size: 30 KiB |
106
server/.gitignore
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# Editor
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
paramiko.log
|
||||
output/
|
34
server/capture.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
# make sure multicast is being routed to the right interface ie.
|
||||
# sudo route add -net 224.0.0.0 netmask 240.0.0.0 dev enx00249b649e67
|
||||
|
||||
def trigger_capture():
|
||||
charid = 1
|
||||
unitid = 1
|
||||
groupid = 1
|
||||
|
||||
gtdate = time.gmtime()
|
||||
now = str(gtdate.tm_year) + str(gtdate.tm_mon) + str(gtdate.tm_mday) + str(gtdate.tm_hour) + str(gtdate.tm_min) + str(gtdate.tm_sec)
|
||||
|
||||
SDATA = str(now)
|
||||
|
||||
print('Sending: ' + SDATA)
|
||||
MCAST_GRP = '224.1.1.1'
|
||||
MCAST_PORT = 5007
|
||||
SCMD = chr(charid)
|
||||
SUNIT = chr(unitid)
|
||||
SGROUP = chr(groupid)
|
||||
SEND = SCMD+SUNIT+SGROUP+SDATA
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
dev = 'eth0' + '\0'
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
||||
sock.sendto(SEND.encode('utf-8'), (MCAST_GRP, MCAST_PORT))
|
||||
sock.close()
|
||||
print('Sent.')
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('Triggering test capture...')
|
||||
trigger_capture()
|
56
server/download.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
import settings
|
||||
import threading
|
||||
import paramiko
|
||||
import time
|
||||
paramiko.util.log_to_file('paramiko.log')
|
||||
|
||||
def fake_download(ip, dest):
|
||||
import requests, random
|
||||
num = ip.split('.')[3]
|
||||
r = requests.get('https://picsum.photos/400/300?random=' + num)
|
||||
time.sleep(random.uniform(2, 10))
|
||||
with open(dest / (num + '_420.jpg'), 'wb') as f:
|
||||
f.write(r.content)
|
||||
|
||||
def download(ip, dest):
|
||||
if settings.DEBUG:
|
||||
fake_download(ip, dest)
|
||||
|
||||
print('Downloading from', ip)
|
||||
|
||||
port = 22
|
||||
transport = paramiko.Transport((ip, port))
|
||||
transport.connect(None, settings.RASPBERRY_USER, settings.RASPBERRY_PASS)
|
||||
|
||||
sftp = paramiko.SFTPClient.from_transport(transport)
|
||||
|
||||
files = sftp.listdir('/3dscan/')
|
||||
|
||||
for f in files:
|
||||
source_file = '/3dscan/' + f
|
||||
dest_file = dest / (f + '.tmp')
|
||||
print('Grabbing file', source_file)
|
||||
sftp.get(source_file, dest_file)
|
||||
sftp.remove(source_file)
|
||||
done_file = dest / f
|
||||
dest_file.rename(done_file)
|
||||
|
||||
if sftp: sftp.close()
|
||||
if transport: transport.close()
|
||||
|
||||
print('Finished downloading from', ip)
|
||||
|
||||
def download_all_photos(dest):
|
||||
if not dest.exists():
|
||||
raise Exception('Destination does not exist')
|
||||
|
||||
print('Downloading all photos to', dest)
|
||||
|
||||
for ip in settings.RASPBERRY_IPS:
|
||||
t = threading.Thread(target=download, args=(ip, dest))
|
||||
t.start()
|
||||
|
||||
if __name__ == '__main__':
|
||||
from pathlib import Path
|
||||
download_all_photos(Path('test/'))
|
168
server/main.py
Normal file
@@ -0,0 +1,168 @@
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from flask import Flask, request, abort, send_from_directory
|
||||
from flask_cors import CORS
|
||||
|
||||
import power, capture, download, settings
|
||||
|
||||
import logging
|
||||
log = logging.getLogger('werkzeug')
|
||||
if not settings.DEBUG:
|
||||
log.setLevel(logging.ERROR)
|
||||
|
||||
build_folder = Path('../client/build')
|
||||
output_folder = Path('./output')
|
||||
app = Flask(__name__, static_folder=str(build_folder), static_url_path='')
|
||||
CORS(app)
|
||||
|
||||
STANDBY = 0
|
||||
WARMUP = 1
|
||||
CAPTURING_PHOTO = 2
|
||||
CAPTURING_GRID = 3
|
||||
WRITING = 4
|
||||
DOWNLOADING = 5
|
||||
|
||||
status = STANDBY
|
||||
|
||||
@app.route('/api/status', methods=['GET'])
|
||||
def status_get():
|
||||
return {'status': status}
|
||||
|
||||
@app.route('/api/clients', methods=['POST'])
|
||||
def clients_post():
|
||||
content = request.json
|
||||
|
||||
phone = str(content['phone'])
|
||||
|
||||
for i in range(1, 100):
|
||||
suffix = str(i).zfill(2)
|
||||
folder = phone + '_' + suffix
|
||||
path = output_folder / folder
|
||||
if not path.exists():
|
||||
break
|
||||
|
||||
content['date'] = datetime.now().strftime('%Y-%m-%d')
|
||||
content['time'] = datetime.now().strftime('%H:%M:%S')
|
||||
|
||||
path.mkdir()
|
||||
info_file = path / 'info.txt'
|
||||
info_file.touch()
|
||||
info_file.write_text(json.dumps(content, indent=4))
|
||||
|
||||
client_id = folder
|
||||
|
||||
print('POST client:', content, 'cid:', client_id)
|
||||
return {'client_id': client_id}
|
||||
|
||||
@app.route('/api/clients/<cid>', methods=['GET'])
|
||||
def clients_get(cid):
|
||||
folder = cid
|
||||
path = output_folder / cid
|
||||
|
||||
if not path.exists():
|
||||
abort(404)
|
||||
|
||||
info_file = path / 'info.txt'
|
||||
info_text = info_file.read_text()
|
||||
res = json.loads(info_text)
|
||||
photo_glob = path.glob('*.jpg')
|
||||
res['has_photos'] = bool(list(photo_glob))
|
||||
|
||||
print('GET client:', cid, 'res:', res)
|
||||
return res
|
||||
|
||||
@app.route('/api/clients/<cid>/session', methods=['GET'])
|
||||
def session_get(cid):
|
||||
folder = cid
|
||||
path = output_folder / cid
|
||||
|
||||
if not path.exists():
|
||||
abort(404)
|
||||
|
||||
photo_glob = path.glob('*.jpg')
|
||||
res = {}
|
||||
res['photos'] = sorted([x.name for x in photo_glob])
|
||||
|
||||
return res
|
||||
|
||||
@app.route('/api/clients/<cid>/session', methods=['DELETE'])
|
||||
def session_delete(cid):
|
||||
folder = cid
|
||||
path = output_folder / cid
|
||||
|
||||
if not path.exists():
|
||||
abort(404)
|
||||
|
||||
photo_glob = path.glob('*.jpg')
|
||||
|
||||
for p in photo_glob:
|
||||
p.unlink()
|
||||
|
||||
print('DELETE session:', cid)
|
||||
return ''
|
||||
|
||||
@app.route('/api/clients/<cid>/session', methods=['POST'])
|
||||
def session_post(cid):
|
||||
content = request.json
|
||||
light_time = content.get('light_time', 5000)
|
||||
|
||||
global status
|
||||
|
||||
print('POST session:', cid)
|
||||
folder = cid
|
||||
path = output_folder / cid
|
||||
|
||||
if not path.exists():
|
||||
abort(404)
|
||||
|
||||
# go through the photo taking process
|
||||
|
||||
try:
|
||||
# warmup
|
||||
status = WARMUP
|
||||
power.lights_on()
|
||||
time.sleep(2)
|
||||
power.lights_off()
|
||||
time.sleep(1)
|
||||
except BaseException as e:
|
||||
print('Problem with lights: {} - {}'.format(e.__class__.__name__, str(e)))
|
||||
print()
|
||||
print('Are you sure the system is connected?')
|
||||
print()
|
||||
abort(500)
|
||||
|
||||
# capture
|
||||
status = CAPTURING_PHOTO
|
||||
power.lights_on()
|
||||
time.sleep(0.1)
|
||||
capture.trigger_capture()
|
||||
time.sleep(light_time / 1000)
|
||||
power.lights_off()
|
||||
|
||||
status = WRITING
|
||||
time.sleep(max(5 - light_time / 1000, 1))
|
||||
|
||||
status = DOWNLOADING
|
||||
download.download_all_photos(path)
|
||||
time.sleep(3)
|
||||
|
||||
status = STANDBY
|
||||
print('Finished.')
|
||||
return ''
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return app.send_static_file('index.html')
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return app.send_static_file('index.html')
|
||||
|
||||
@app.route('/output/<path:filename>')
|
||||
def output(filename):
|
||||
return send_from_directory('output/', filename)
|
||||
|
||||
app.run(host='0.0.0.0')
|
0
server/output/.gitkeep
Normal file
53
server/power.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import requests
|
||||
import settings
|
||||
import time
|
||||
|
||||
def np_02B_api(ip, username, password, is_on):
|
||||
if settings.DEBUG: return
|
||||
|
||||
if is_on:
|
||||
endpoint = '/cmd.cgi?grp=0'
|
||||
else:
|
||||
endpoint = '/cmd.cgi?grp=30'
|
||||
|
||||
url = 'http://' + ip + endpoint
|
||||
r = requests.get(url, auth=(username, password), timeout=4)
|
||||
r.raise_for_status()
|
||||
|
||||
def lights_on():
|
||||
np_02B_api(settings.LIGHT_IP, settings.LIGHT_USER, settings.LIGHT_PASS, True)
|
||||
|
||||
def lights_off():
|
||||
np_02B_api(settings.LIGHT_IP, settings.LIGHT_USER, settings.LIGHT_PASS, False)
|
||||
|
||||
def grid_on():
|
||||
np_02B_api(settings.GRID_IP, settings.GRID_USER, settings.GRID_PASS, True)
|
||||
|
||||
def grid_off():
|
||||
np_02B_api(settings.GRID_IP, settings.GRID_USER, settings.GRID_PASS, False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
print('Turning lights on...')
|
||||
lights_on()
|
||||
|
||||
print('Waiting three seconds...')
|
||||
time.sleep(3)
|
||||
|
||||
print('Turning lights off...')
|
||||
lights_off()
|
||||
except BaseException as e:
|
||||
print('Problem with lights: {} - {}'.format(e.__class__.__name__, str(e)))
|
||||
|
||||
try:
|
||||
print('Turning grid on...')
|
||||
grid_on()
|
||||
|
||||
print('Waiting three seconds...')
|
||||
time.sleep(3)
|
||||
|
||||
print('Turning grid off...')
|
||||
grid_off()
|
||||
except BaseException as e:
|
||||
print('Problem with grid: {} - {}'.format(e.__class__.__name__, str(e)))
|
36
server/requirements.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
appdirs==1.4.3
|
||||
bcrypt==3.2.0
|
||||
CacheControl==0.12.6
|
||||
certifi==2019.11.28
|
||||
cffi==1.14.5
|
||||
chardet==3.0.4
|
||||
click==7.1.2
|
||||
colorama==0.4.3
|
||||
contextlib2==0.6.0
|
||||
cryptography==3.4.6
|
||||
distlib==0.3.0
|
||||
distro==1.4.0
|
||||
Flask==1.1.2
|
||||
Flask-Cors==3.0.10
|
||||
html5lib==1.0.1
|
||||
idna==2.8
|
||||
ipaddr==2.2.0
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.11.3
|
||||
lockfile==0.12.2
|
||||
MarkupSafe==1.1.1
|
||||
msgpack==0.6.2
|
||||
packaging==20.3
|
||||
paramiko==2.7.2
|
||||
pep517==0.8.2
|
||||
progress==1.5
|
||||
pycparser==2.20
|
||||
PyNaCl==1.4.0
|
||||
pyparsing==2.4.6
|
||||
pytoml==0.1.21
|
||||
requests==2.22.0
|
||||
retrying==1.3.3
|
||||
six==1.14.0
|
||||
urllib3==1.25.8
|
||||
webencodings==0.5.1
|
||||
Werkzeug==1.0.1
|
104
server/settings.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import os
|
||||
DEBUG = os.environ.get('FLASK_ENV', None) == 'development'
|
||||
|
||||
LIGHT_IP = '192.168.99.25'
|
||||
LIGHT_USER = 'admin'
|
||||
LIGHT_PASS = 'admin'
|
||||
|
||||
GRID_IP = '192.168.99.20'
|
||||
GRID_USER = 'admin'
|
||||
GRID_PASS = 'admin'
|
||||
|
||||
RASPBERRY_USER = 'root'
|
||||
RASPBERRY_PASS = '3dscan'
|
||||
RASPBERRY_IPS = [
|
||||
'192.168.99.101',
|
||||
'192.168.99.103',
|
||||
'192.168.99.104',
|
||||
'192.168.99.105',
|
||||
'192.168.99.106',
|
||||
'192.168.99.107',
|
||||
'192.168.99.108',
|
||||
'192.168.99.109',
|
||||
'192.168.99.110',
|
||||
'192.168.99.111',
|
||||
'192.168.99.112',
|
||||
'192.168.99.113',
|
||||
'192.168.99.114',
|
||||
'192.168.99.115',
|
||||
'192.168.99.116',
|
||||
'192.168.99.117',
|
||||
'192.168.99.118',
|
||||
'192.168.99.119',
|
||||
'192.168.99.120',
|
||||
'192.168.99.121',
|
||||
'192.168.99.122',
|
||||
'192.168.99.123',
|
||||
'192.168.99.124',
|
||||
'192.168.99.125',
|
||||
'192.168.99.126',
|
||||
'192.168.99.127',
|
||||
'192.168.99.128',
|
||||
'192.168.99.129',
|
||||
'192.168.99.130',
|
||||
'192.168.99.131',
|
||||
'192.168.99.132',
|
||||
'192.168.99.133',
|
||||
'192.168.99.134',
|
||||
'192.168.99.135',
|
||||
'192.168.99.136',
|
||||
'192.168.99.137',
|
||||
'192.168.99.138',
|
||||
'192.168.99.139',
|
||||
'192.168.99.140',
|
||||
'192.168.99.141',
|
||||
'192.168.99.142',
|
||||
'192.168.99.143',
|
||||
'192.168.99.144',
|
||||
'192.168.99.145',
|
||||
'192.168.99.146',
|
||||
'192.168.99.147',
|
||||
'192.168.99.148',
|
||||
'192.168.99.149',
|
||||
'192.168.99.150',
|
||||
'192.168.99.151',
|
||||
'192.168.99.152',
|
||||
'192.168.99.153',
|
||||
'192.168.99.154',
|
||||
'192.168.99.155',
|
||||
'192.168.99.156',
|
||||
'192.168.99.157',
|
||||
'192.168.99.158',
|
||||
'192.168.99.159',
|
||||
'192.168.99.160',
|
||||
'192.168.99.161',
|
||||
'192.168.99.162',
|
||||
'192.168.99.163',
|
||||
'192.168.99.164',
|
||||
'192.168.99.165',
|
||||
'192.168.99.166',
|
||||
'192.168.99.167',
|
||||
'192.168.99.168',
|
||||
'192.168.99.169',
|
||||
'192.168.99.170',
|
||||
'192.168.99.171',
|
||||
'192.168.99.172',
|
||||
'192.168.99.173',
|
||||
'192.168.99.174',
|
||||
'192.168.99.175',
|
||||
'192.168.99.176',
|
||||
'192.168.99.177',
|
||||
'192.168.99.178',
|
||||
'192.168.99.179',
|
||||
'192.168.99.180',
|
||||
'192.168.99.181',
|
||||
'192.168.99.182',
|
||||
'192.168.99.183',
|
||||
'192.168.99.184',
|
||||
'192.168.99.185',
|
||||
'192.168.99.191',
|
||||
'192.168.99.192',
|
||||
'192.168.99.193',
|
||||
'192.168.99.194',
|
||||
'192.168.99.195',
|
||||
]
|
@@ -1,3 +0,0 @@
|
||||
import socket
|
||||
import sys
|
||||
import time
|