"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
"dev": true
},
+ "clone-deep": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+ "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ }
+ },
"coa": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
}
}
},
+ "interpret": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
+ "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
+ "dev": true
+ },
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"readable-stream": "^2.0.2"
}
},
+ "rechoir": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+ "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
+ "dev": true,
+ "requires": {
+ "resolve": "^1.1.6"
+ }
+ },
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
+ "sass": {
+ "version": "1.23.7",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.23.7.tgz",
+ "integrity": "sha512-cYgc0fanwIpi0rXisGxl+/wadVQ/HX3RhpdRcjLdj2o2ye/sxUTpAxIhbmJy3PLQgRFbf6Pn8Jsrta2vdXcoOQ==",
+ "dev": true,
+ "requires": {
+ "chokidar": ">=2.0.0 <4.0.0"
+ }
+ },
+ "sass-loader": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.0.tgz",
+ "integrity": "sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==",
+ "dev": true,
+ "requires": {
+ "clone-deep": "^4.0.1",
+ "loader-utils": "^1.2.3",
+ "neo-async": "^2.6.1",
+ "schema-utils": "^2.1.0",
+ "semver": "^6.3.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ }
+ }
+ },
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"safe-buffer": "^5.0.1"
}
},
+ "shallow-clone": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+ "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.2"
+ }
+ },
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"dev": true
},
+ "shelljs": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz",
+ "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.0.0",
+ "interpret": "^1.0.0",
+ "rechoir": "^0.6.2"
+ }
+ },
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
"integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
},
+ "vue-cli-plugin-vuetify": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.2.tgz",
+ "integrity": "sha512-OJ1YUSfDlQibj111QMGv4a44atmtWdrkynk4voiEuivUqLcZGCJyF/A8ae0VojpMU2jlvZydz0nXjmZmcg+Nqw==",
+ "dev": true,
+ "requires": {
+ "semver": "^6.0.0",
+ "shelljs": "^0.8.3"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ }
+ }
+ },
"vue-hot-reload-api": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
+ "vuetify": {
+ "version": "2.1.14",
+ "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.1.14.tgz",
+ "integrity": "sha512-DZ1Jq2+PPEXYeh08FGJktB6St0ClZlcwzveCvJmkW16pYkUnimFlW6E3AQxSqxRpcGfZDsE53XkvZ2BGhy+m8Q=="
+ },
+ "vuetify-loader": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/vuetify-loader/-/vuetify-loader-1.4.3.tgz",
+ "integrity": "sha512-fS0wRil682Ebsj2as+eruBoMPKaQYDhu/fDAndnTItzSY4RK4LOEIsssVL4vD6QY8dvUgoGL84SUQ6vGr777CA==",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^1.2.0"
+ }
+ },
"watchpack": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
{
- "name": "garnod-pwa.git",
- "version": "0.1.0",
- "private": true,
+ "name": "garnod-pwa",
+ "version": "1.0.0",
+ "description": "A garage door monitoring application (client).",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
+ "author": "ebelcrom",
+ "license": "GPL-2.0-or-later",
"dependencies": {
"core-js": "^3.4.3",
"register-service-worker": "^1.6.2",
"vue": "^2.6.10",
- "vue-router": "^3.1.3"
+ "vue-router": "^3.1.3",
+ "vuetify": "^2.1.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0",
"@vue/cli-plugin-pwa": "^4.1.0",
"@vue/cli-plugin-router": "^4.1.0",
"@vue/cli-service": "^4.1.0",
- "vue-template-compiler": "^2.6.10"
+ "sass": "^1.19.0",
+ "sass-loader": "^8.0.0",
+ "vue-cli-plugin-vuetify": "^2.0.2",
+ "vue-template-compiler": "^2.6.10",
+ "vuetify-loader": "^1.3.0"
}
}
<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>garnod-pwa.git</title>
+ <title>Garage Node</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
</head>
<body>
<noscript>
- <strong>We're sorry but garnod-pwa.git doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+ <strong>We're sorry but Garage Node doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<template>
- <div id="app">
- <div id="nav">
- <router-link to="/">Home</router-link> |
- <router-link to="/about">About</router-link>
- </div>
- <router-view/>
- </div>
-</template>
+ <v-app>
+
+ <Navbar v-on:openAboutEvent="openAboutDialog" />
-<style>
-#app {
- font-family: 'Avenir', Helvetica, Arial, sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- text-align: center;
- color: #2c3e50;
-}
+ <v-content>
+ <router-view></router-view>
+ </v-content>
-#nav {
- padding: 30px;
-}
+ <AboutDialog :showAboutDialog="showAboutDialog"/>
+
+ </v-app>
+</template>
-#nav a {
- font-weight: bold;
- color: #2c3e50;
-}
+<script>
+import Navbar from '@/components/Navbar';
+import AboutDialog from '@/components/About';
-#nav a.router-link-exact-active {
- color: #42b983;
-}
-</style>
+export default {
+ name: 'App',
+ components: {
+ Navbar,
+ AboutDialog,
+ },
+ data() {
+ return {
+ showAboutDialog: null,
+ };
+ },
+ methods: {
+ openAboutDialog() {
+ this.showAboutDialog = ['showDialog'];
+ },
+ },
+};
+</script>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 19.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->\r
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"\r
+ viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">\r
+<path fill="#E8EAF6" d="M24,8L4,17.9V42h40V17.9L24,8z"/>\r
+<path fill="#C5CAE9" d="M44,42H4v-5h40V42z M4.9,21.8L24,12.2l19.1,9.6l0.9,0.5V22v-2L24,10L4,20v2v0.3L4.9,21.8z"/>\r
+<rect x="9" y="24" fill="#607D8B" width="30" height="18"/>\r
+<polygon fill="#546E7A" points="37,24 11,24 9,24 9,26 9,42 11,42 11,26 37,26 37,42 39,42 39,26 39,24 "/>\r
+<polygon fill="#D32F2F" points="2,17 24,6 46,17 46,20 46,21 24,10 2,21 2,20 "/>\r
+<rect x="21" y="17" fill="#01579B" width="6" height="4"/>\r
+<path fill="#90A4AE" d="M39,32H9v-2h30V32z M39,34H9v2h30V34z M39,38H9v2h30V38z M39,26H9v2h30V26z"/>\r
+<path fill="#78909C" d="M39,28h-2v-2h2V28z M39,30h-2v2h2V30z M39,34h-2v2h2V34z M39,38h-2v2h2V38z M11,26H9v2h2V26z M11,30H9v2h2\r
+ V30z M11,34H9v2h2V34z M11,38H9v2h2V38z"/>\r
+</svg>\r
--- /dev/null
+<svg width="48" height="48" xmlns="http://www.w3.org/2000/svg">
+
+ <g>
+ <title>background</title>
+ <rect fill="none" id="canvas_background" height="402" width="582" y="-1" x="-1"/>
+ </g>
+ <g>
+ <title>Layer 1</title>
+ <path id="svg_1" d="m24,8l-20,9.9l0,24.1l40,0l0,-24.1l-20,-9.9z" fill="#E8EAF6"/>
+ <path id="svg_2" d="m44,42l-40,0l0,-5l40,0l0,5zm-39.1,-20.2l19.1,-9.6l19.1,9.6l0.9,0.5l0,-0.3l0,-2l-20,-10l-20,10l0,2l0,0.3l0.9,-0.5z" fill="#C5CAE9"/>
+ <rect id="svg_3" height="18" width="30" fill="#607D8B" y="24" x="9"/>
+ <polygon id="svg_4" points="37,24 11,24 9,24 9,26 9,42 11,42 11,26 37,26 37,42 39,42 39,26 39,24 " fill="#546E7A"/>
+ <polygon id="svg_5" points="2,17 24,6 46,17 46,20 46,21 24,10 2,21 2,20 " fill="#D32F2F"/>
+ <rect id="svg_6" height="4" width="6" fill="#01579B" y="17" x="21"/>
+ <path id="svg_7" d="m39,32l-30,0l0,-2l30,0l0,2zm0,2l-30,0l0,2l30,0l0,-2zm0,4l-30,0l0,2l30,0l0,-2zm0,-12l-30,0l0,2l30,0l0,-2z" fill="#90A4AE"/>
+ <path id="svg_8" d="m39,28l-2,0l0,-2l2,0l0,2zm0,2l-2,0l0,2l2,0l0,-2zm0,4l-2,0l0,2l2,0l0,-2zm0,4l-2,0l0,2l2,0l0,-2zm-28,-12l-2,0l0,2l2,0l0,-2zm0,4l-2,0l0,2l2,0l0,-2zm0,4l-2,0l0,2l2,0l0,-2zm0,4l-2,0l0,2l2,0l0,-2z" fill="#78909C"/>
+ <path stroke="#000" transform="rotate(-90 24.00000000000001,32.98158264160157) " id="svg_10" d="m16.312502,32.981592l4.457476,-3.687508l0,1.843749l6.460048,0l0,-1.843749l4.457474,3.687508l-4.457474,3.687489l0,-1.843745l-6.460048,0l0,1.843745l-4.457476,-3.687489z" stroke-opacity="null" fill="#FFF093"/>
+ </g>
+</svg>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 19.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->\r
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"\r
+ viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">\r
+<path fill="#E8EAF6" d="M24,7.4L4,17.9V42h40V17.9L24,7.4z"/>\r
+<polygon fill="#D32F2F" points="2,17 24,6 46,17 46,20 46,21 24,10 2,21 2,20 "/>\r
+<path fill="#C5CAE9" d="M44,42H4v-5h40V42z M24,12.2l20,9.9V22v-2L24,10L4,20v2v0.2L24,12.2z"/>\r
+<rect x="9" y="24" fill="#546E7A" width="30" height="2"/>\r
+<rect x="9" y="26" fill="#455A64" width="30" height="16"/>\r
+<rect x="9" y="26" fill="#90A4AE" width="30" height="2"/>\r
+<path fill="#78909C" d="M39,28h-2v-2h2V28z M11,26H9v2h2V26z"/>\r
+<rect x="21" y="17" fill="#01579B" width="6" height="4"/>\r
+<polygon fill="#263238" points="9,28 9,37 9,42 14,37 34,37 39,42 39,37 39,28 "/>\r
+</svg>\r
--- /dev/null
+<svg width="40" height="30" xmlns="http://www.w3.org/2000/svg">
+ <g>
+ <title>background</title>
+ <rect x="-1" y="-1" width="42" height="32" id="canvas_background" fill="none"/>
+ </g>
+
+ <g>
+ <title>Layer 1</title>
+ <path opacity="0.6" stroke="#C74343" d="m20,1.311108c-7.594386,0 -13.751391,6.129021 -13.751391,13.688892s6.157005,13.688892 13.751391,13.688892s13.751391,-6.129021 13.751391,-13.688892s-6.157005,-13.688892 -13.751391,-13.688892zm-10.132604,13.688892c0,-5.570659 4.536512,-10.086552 10.132604,-10.086552c2.144493,0 4.12976,0.667154 5.766899,1.798288l-14.093005,14.028952c-1.136299,-1.629699 -1.806499,-3.605942 -1.806499,-5.740689l0.000001,0.000001zm10.132604,10.086552c-2.144493,0 -4.12976,-0.667154 -5.766899,-1.798288l14.093005,-14.028952c1.136299,1.629699 1.806499,3.605942 1.806499,5.740689c0,5.570659 -4.536512,10.086552 -10.132604,10.086552l-0.000001,-0.000001z" stroke-miterlimit="10" fill="#F78F8F" id="svg_1"/>
+ </g>
+</svg>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 19.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->\r
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"\r
+ viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">\r
+<path fill="#E8EAF6" d="M24,8L4,17.9V42h40V17.9L24,8z"/>\r
+<path fill="#C5CAE9" d="M44,42H4v-5h40V42z M4.9,21.8L24,12.2l19.1,9.6l0.9,0.5V22v-2L24,10L4,20v2v0.3L4.9,21.8z"/>\r
+<rect x="9" y="24" fill="#607D8B" width="30" height="18"/>\r
+<polygon fill="#546E7A" points="37,24 11,24 9,24 9,26 9,42 11,42 11,26 37,26 37,42 39,42 39,26 39,24 "/>\r
+<polygon fill="#D32F2F" points="2,17 24,6 46,17 46,20 46,21 24,10 2,21 2,20 "/>\r
+<rect x="21" y="17" fill="#01579B" width="6" height="4"/>\r
+<path fill="#90A4AE" d="M39,32H9v-2h30V32z M39,34H9v2h30V34z M39,38H9v2h30V38z M39,26H9v2h30V26z"/>\r
+<path fill="#78909C" d="M39,28h-2v-2h2V28z M39,30h-2v2h2V30z M39,34h-2v2h2V34z M39,38h-2v2h2V38z M11,26H9v2h2V26z M11,30H9v2h2\r
+ V30z M11,34H9v2h2V34z M11,38H9v2h2V38z"/>\r
+</svg>\r
--- /dev/null
+<template>
+ <v-row justify="center">
+ <v-dialog v-model="aboutDialog" persistent>
+ <v-card>
+ <v-card-title>Garage Node</v-card-title>
+ <v-card-subtitle>
+ ©2019 ebelcrom
+ </v-card-subtitle>
+ <v-card-text>
+ App for push notifications about or checking the garage
+ door status and performing a move action.
+ </v-card-text>
+ <v-card-actions>
+ <v-spacer />
+ <v-btn color="primary" @click="aboutDialog = false">Close</v-btn>
+ </v-card-actions>
+ </v-card>
+ </v-dialog>
+ </v-row>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ aboutDialog: false,
+ };
+ },
+ props: ['showAboutDialog'],
+ watch: {
+ 'showAboutDialog': function(args) {
+ this.aboutDialog = true;
+ }
+ },
+};
+</script>
--- /dev/null
+<template>
+ <v-row justify="center">
+ <v-dialog v-model="apiKeyDialog" persistent>
+ <v-card>
+ <v-card-title>Production API-Key</v-card-title>
+ <v-card-text>
+ Set your API-Key below to get access to production functions.
+ <v-input>
+ <v-text-field
+ v-model="apiKey"
+ label="API-Key"
+ :rules="[validation]"
+ />
+ </v-input>
+ </v-card-text>
+ <v-card-actions>
+ <v-spacer />
+ <v-btn @click="cancelAction">Cancel</v-btn>
+ <v-btn :disabled="disabled" color="primary" @click="okAction">Ok</v-btn>
+ </v-card-actions>
+ </v-card>
+ </v-dialog>
+ </v-row>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ apiKeyDialog: false,
+ disabled: true,
+ apiKey: '',
+ };
+ },
+ props: ['showApiKeyDialog'],
+ watch: {
+ 'showApiKeyDialog': function(args) {
+ this.apiKeyDialog = true;
+ }
+ },
+ methods: {
+ validation(value) {
+ if (!!value) {
+ this.disabled = false;
+ return !!value;
+ } else {
+ this.disabled = true;
+ return 'Input required';
+ }
+ },
+ okAction() {
+ if (this.apiKey !== '') {
+ this.apiKeyDialog = false;
+ this.$emit('apiKeySetEvent', this.apiKey);
+ }
+ },
+ cancelAction() {
+ this.apiKeyDialog = false;
+ this.$emit('apiKeySetEvent', this.apiKey);
+ },
+ },
+};
+</script>
+++ /dev/null
-<template>
- <div class="hello">
- <h1>{{ msg }}</h1>
- <p>
- For a guide and recipes on how to configure / customize this project,<br>
- check out the
- <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
- </p>
- <h3>Installed CLI Plugins</h3>
- <ul>
- <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
- <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank" rel="noopener">pwa</a></li>
- <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
- </ul>
- <h3>Essential Links</h3>
- <ul>
- <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
- <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
- <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
- <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
- <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
- </ul>
- <h3>Ecosystem</h3>
- <ul>
- <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
- <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
- <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
- <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
- <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
- </ul>
- </div>
-</template>
-
-<script>
-export default {
- name: 'HelloWorld',
- props: {
- msg: String
- }
-}
-</script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped>
-h3 {
- margin: 40px 0 0;
-}
-ul {
- list-style-type: none;
- padding: 0;
-}
-li {
- display: inline-block;
- margin: 0 10px;
-}
-a {
- color: #42b983;
-}
-</style>
--- /dev/null
+<template>
+ <nav>
+ <div v-if="showSettingsView">
+ <v-app-bar dense />
+
+ <v-app-bar fixed dark color="primary">
+ <v-btn icon v-on:click="goHome()">
+ <v-icon>mdi-arrow-left</v-icon>
+ </v-btn>
+ <v-toolbar-title>Settings</v-toolbar-title>
+ </v-app-bar>
+ </div>
+ <div v-else>
+ <v-app-bar dense />
+
+ <v-app-bar fixed dark color="primary">
+ <v-app-bar-nav-icon @click.stop="navbarDrawer = !navbarDrawer" />
+ <v-toolbar-title>Garage Node</v-toolbar-title>
+ </v-app-bar>
+
+ <v-navigation-drawer v-model="navbarDrawer" app temporary>
+ <v-list-item>
+ <v-list-item-content>
+ <v-list-item-title class="title">
+ DemoApp
+ </v-list-item-title>
+ </v-list-item-content>
+ </v-list-item>
+
+ <v-divider />
+
+ <v-list>
+ <v-list-item
+ v-for="item in navbarItems"
+ :key="item.title"
+ :to="item.link"
+ v-on:click="wrapToolbar(item.title)"
+ link
+ >
+ <v-list-item-icon>
+ <v-icon>{{ item.icon }}</v-icon>
+ </v-list-item-icon>
+ <v-list-item-content>
+ <v-list-item-title>{{ item.title }}</v-list-item-title>
+ </v-list-item-content>
+ </v-list-item>
+ <v-list-item v-on:click="openAboutDialog">
+ <v-list-item-icon>
+ <v-icon>{{ navbarItem.icon }}</v-icon>
+ </v-list-item-icon>
+ <v-list-item-content>
+ <v-list-item-title>{{ navbarItem.title }}</v-list-item-title>
+ </v-list-item-content>
+ </v-list-item>
+ </v-list>
+ </v-navigation-drawer>
+ </div>
+ </nav>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ navbarItems: [
+ { title: 'Home', icon: 'mdi-home', link: '/' },
+ { title: 'Settings', icon: 'mdi-settings', link: '/settings' },
+ ],
+ navbarItem: { title: 'About', icon: 'mdi-information', },
+ navbarDrawer: false,
+ showSettingsView: false,
+ };
+ },
+ methods: {
+ wrapToolbar(item) {
+ switch (item) {
+ case 'Home':
+ this.navbarDrawer = false;
+ this.showSettingsView = false;
+ break;
+ case 'Settings':
+ this.showSettingsView = true;
+ break;
+ };
+ },
+ goHome() {
+ this.showSettingsView = false;
+ this.$router.push('/');
+ },
+ openAboutDialog() {
+ this.navbarDrawer = false;
+ this.$emit('openAboutEvent');
+ },
+ },
+};
+</script>
--- /dev/null
+function setItem(key, value) {
+ if (key === null) {
+ return;
+ } else {
+ localStorage.setItem(key, JSON.stringify(value));
+ }
+}
+
+function getItem(key) {
+ if (key === null) {
+ return null;
+ } else {
+ if (localStorage.getItem(key) === '') {
+ return '';
+ } else {
+ return JSON.parse(localStorage.getItem(key));
+ }
+ }
+}
+
+function hasItem(key) {
+ if (key === null) {
+ return false;
+ } else {
+ return localStorage.getItem(key) !== null;
+ }
+}
+
+export const storage = {
+ setItem: setItem,
+ getItem: getItem,
+ hasItem: hasItem,
+};
+
+export const notification = {
+};
+
+const url = 'https://binomiant.duckdns.org/mVk7Yr3k/v1';
+const prodStatus = '/status';
+const prodEvents = '/events';
+const prodControl = '/control';
+const testStatus = '/test/status';
+const testEvents = '/test/events';
+const testControl = '/test/control';
+
+async function sendRequest(data) {
+ var init = {
+ method: data.method,
+ headers: {},
+ }
+ if (data.requestContentType != null) {
+ init.headers['Content-Type'] = 'application/json';
+ }
+ if (data.apiKey == null) {
+ init.headers['X-API-Key-Test'] = '2TTqCD4mNNny';
+ } else {
+ init.headers['X-API-Key'] = data.apiKey;
+ }
+
+ const response = await fetch(url + data.path + data.query, init);
+ if (response.status >= 200 && response.status <= 399) {
+ if (response.headers.get('Content-Type') != null) {
+ const result = await response.json();
+ return result;
+ } else {
+ return {};
+ }
+ } else {
+ return null;
+ }
+}
+
+function getStatus() {
+ const testMode = getItem('testMode');
+ var path = '',
+ apiKey = null;
+ var query = new URLSearchParams('');
+
+ if (testMode) {
+ path = testStatus + '?';
+ } else {
+ path = prodStatus + '?';
+ apiKey = getItem('apiKey');
+ }
+ query.append('image', getItem('imageTransmission'));
+
+ var data = {
+ path: path,
+ query: query.toString(),
+ method: 'GET',
+ apiKey: apiKey,
+ requestContentType: null,
+ responseContentType: 'json',
+ }
+
+ return sendRequest(data);
+}
+
+function getEvents() {
+ const testMode = getItem('testMode');
+ var path = '',
+ apiKey = null;
+ var query = new URLSearchParams('');
+
+ if (testMode) {
+ path = testEvents + '?';
+ } else {
+ path = prodEvents + '?';
+ apiKey = getItem('apiKey');
+ }
+ query.append('image', getItem('imageTransmission'));
+ query.append('timeout', 30);
+
+ var data = {
+ path: path,
+ query: query.toString(),
+ method: 'GET',
+ apiKey: apiKey,
+ requestContentType: null,
+ responseContentType: 'json',
+ }
+
+ return new Promise((resolve, reject) => {
+ sendRequest(data)
+ .then(response => {
+ if (response !== null) {
+ if (Object.entries(response).length === 0) {
+ // 304, request again
+ sendRequest(data)
+ .then(response => {
+ if (response !== null) {
+ if (Object.entries(response).length === 0) {
+ // 304, request again
+ sendRequest(data)
+ .then(response => {
+ if (response !== null) {
+ if (Object.entries(response).length === 0) {
+ // 304, abort
+ reject(null);
+ return;
+ } else {
+ resolve(response);
+ return;
+ }
+ } else {
+ throw new Error('server response not 2xx nor 3xx');
+ }
+ })
+ .catch(err => {
+ reject(null);
+ return;
+ });
+ } else {
+ resolve(response);
+ return;
+ }
+ } else {
+ throw new Error('server response not 2xx nor 3xx');
+ }
+ })
+ .catch(err => {
+ reject(null);
+ return;
+ });
+ } else {
+ resolve(response);
+ return;
+ }
+ } else {
+ throw new Error('server response not 2xx nor 3xx');
+ }
+ })
+ .catch(err => {
+ reject(null);
+ return;
+ });
+ });
+}
+
+function postControl() {
+ const testMode = getItem('testMode');
+ var path = '',
+ apiKey = null;
+ var query = new URLSearchParams('');
+
+ if (testMode) {
+ path = testControl + '?';
+ } else {
+ path = prodControl + '?';
+ apiKey = getItem('apiKey');
+ }
+ query.append('command', 'move');
+
+ var data = {
+ path: path,
+ query: query.toString(),
+ method: 'POST',
+ apiKey: apiKey,
+ requestContentType: null,
+ responseContentType: null,
+ }
+
+ return sendRequest(data);
+}
+
+export const server = {
+ getStatus: getStatus,
+ getEvents: getEvents,
+ postControl: postControl,
+}
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
+import vuetify from './plugins/vuetify';
Vue.config.productionTip = false
new Vue({
router,
+ vuetify,
render: h => h(App)
}).$mount('#app')
--- /dev/null
+import Vue from 'vue';
+import Vuetify from 'vuetify/lib';
+
+Vue.use(Vuetify);
+
+export default new Vuetify({
+});
import { register } from 'register-service-worker'
-if (process.env.NODE_ENV === 'production') {
+//if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
console.error('Error during service worker registration:', error)
}
})
-}
+//}
-import Vue from 'vue'
-import VueRouter from 'vue-router'
-import Home from '../views/Home.vue'
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import Home from '@/views/Home';
+import Settings from '@/views/Settings';
-Vue.use(VueRouter)
+Vue.use(VueRouter);
const routes = [
{
component: Home
},
{
- path: '/about',
- name: 'about',
- // route level code-splitting
- // this generates a separate chunk (about.[hash].js) for this route
- // which is lazy-loaded when the route is visited.
- component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
+ path: '/settings',
+ name: 'settings',
+ component: Settings
}
]
const router = new VueRouter({
routes
-})
+});
-export default router
+export default router;
+++ /dev/null
-<template>
- <div class="about">
- <h1>This is an about page</h1>
- </div>
-</template>
<template>
- <div class="home">
- <img alt="Vue logo" src="../assets/logo.png">
- <HelloWorld msg="Welcome to Your Vue.js App"/>
- </div>
+ <v-container>
+ <v-row dense>
+ <v-col>
+ <v-card>
+ <v-list-item three-line>
+ <v-list-item-content>
+ <div class="overline mb-4">DOOR STATUS</div>
+ <v-list-item-title class="headline mb-1">
+ {{status.state}}
+ </v-list-item-title>
+ <v-list-item-subtitle>
+ {{status.text}}
+ </v-list-item-subtitle>
+ </v-list-item-content>
+ <v-list-item-avatar tile size="80">
+ <v-img
+ :src="icon"
+ />
+ </v-list-item-avatar>
+ </v-list-item>
+ <v-card-actions>
+ <v-btn
+ color="primary"
+ :disabled="buttons.action.disabled"
+ :loading="buttons.action.loading"
+ @click="action"
+ >
+ {{buttons.action.name}}
+ </v-btn>
+ <v-spacer />
+ <v-btn
+ color="secondary"
+ :disabled="buttons.refresh.disabled"
+ :loading="buttons.refresh.loading"
+ @click="refresh"
+ >
+ {{buttons.refresh.name}}
+ </v-btn>
+ </v-card-actions>
+ </v-card>
+ </v-col>
+
+ <v-col>
+ <v-card>
+ <v-list-item>
+ <v-list-item-content>
+ <div class="overline mb-4">LIVE IMAGE</div>
+ </v-list-item-content>
+ </v-list-item>
+ <v-list-item>
+ <v-list-item-content>
+ <v-img
+ :src="image"
+ :aspect-ratio="4/3"
+ min-width="150"
+ />
+ </v-list-item-content>
+ </v-list-item>
+ </v-card>
+ </v-col>
+ </v-row>
+
+ <v-snackbar
+ v-model="snackbar.show"
+ :timeout="snackbar.timeout"
+ :color="snackbar.color"
+ bottom
+ >
+ {{snackbar.text}}
+ </v-snackbar>
+ </v-container>
</template>
<script>
-// @ is an alias to /src
-import HelloWorld from '@/components/HelloWorld.vue'
+import { storage, server } from '@/lib/lib.js'
+
+const status = {
+ unknown: {
+ state: 'Unknown',
+ text: 'Door state is unknown.',
+ },
+ closed: {
+ state: 'Closed',
+ text: 'The garage door is closed.',
+ },
+ open: {
+ state: 'Open',
+ text: 'The garage door is open.',
+ },
+ moving: {
+ state: 'Moving',
+ text: 'The garage door is moving.',
+ },
+};
+
+const messages = {
+ checkSettings: {
+ text: 'Check settings',
+ color: 'warning',
+ },
+ refreshFailed: {
+ text: 'Refresh failed',
+ color: 'error',
+ },
+ closeFailed: {
+ text: 'Close failed',
+ color: 'error',
+ },
+};
export default {
- name: 'home',
- components: {
- HelloWorld
- }
-}
+ data() {
+ return {
+ buttons: {
+ action: {
+ name: 'Close door',
+ disabled: true,
+ loading: false,
+ },
+ refresh: {
+ name: 'Refresh',
+ disabled: true,
+ loading: false,
+ },
+ },
+ status: {
+ state: status.unknown.state,
+ text: status.unknown.text,
+ },
+ icon: require('@/assets/images/unavailable.svg'),
+ image: require('@/assets/images/unavailable.svg'),
+ snackbar: {
+ show: false,
+ timeout: 5000,
+ color: messages.checkSettings.color,
+ text: messages.checkSettings.text,
+ },
+ };
+ },
+ created() {
+ if (storage.hasItem('testMode')) {
+ if (storage.getItem('testMode') || storage.getItem('apiKey') !== '') {
+ this.buttons.refresh.disabled = false;
+ } else {
+ this.buttons.refresh.disabled = true;
+ this.snackbar.show = true;
+ }
+ } else {
+ this.buttons.refresh.disabled = true;
+ this.snackbar.show = true;
+ }
+ },
+ methods: {
+ action() {
+ this.buttons.action.loading = true;
+ this.buttons.refresh.loading = true;
+ server.postControl()
+ .then(response => {
+ if (response === null) {
+ this.stateError(err);
+ } else {
+ this.stateMoving(response);
+ }
+ })
+ .catch(err => {
+ this.stateError(err);
+ })
+ .then(response => {
+ server.getEvents()
+ .then(response => {;
+ if (response === null) {
+ this.stateError(err);
+ } else {
+ switch (response.state) {
+ case 'closed':
+ this.stateClosed(response);
+ break;
+ default:
+ break;
+ }
+ }
+ })
+ .catch(err => {
+ this.stateError(err);
+ });
+ });
+ },
+ refresh() {
+ this.buttons.refresh.loading = true;
+ server.getStatus()
+ .then(response => {
+ if (response === null) {
+ this.stateError(err);
+ } else {
+ switch (response.state) {
+ case 'closed':
+ this.stateClosed(response);
+ break;
+ case 'open':
+ this.stateOpen(response);
+ break;
+ case 'moving':
+ this.stateMoving(response);
+ break;
+ default:
+ break;
+ }
+ }
+ })
+ .catch(err => {
+ this.stateError(err);
+ });
+ },
+
+ stateClosed(response) {
+ var image;
+
+ if (typeof response.image != 'undefined') {
+ image = true;
+ } else {
+ image = false;
+ }
+
+ this.buttons.action.loading = false;
+ this.buttons.action.disabled = true;
+ this.buttons.refresh.loading = false;
+ this.buttons.refresh.disabled = false;
+
+ this.status.state = status.closed.state;
+ this.status.text = status.closed.text;
+ this.icon = require('@/assets/images/closed.svg');
+
+ if (image) {
+ this.image = 'data:image/jpeg;base64,' + response.image;
+ } else {
+ this.image = require('@/assets/images/closed.svg');
+ }
+ },
+ stateOpen(response) {
+ var image;
+
+ if (typeof response.image != 'undefined') {
+ image = true;
+ } else {
+ image = false;
+ }
+
+ this.buttons.action.loading = false;
+ this.buttons.action.disabled = false;
+ this.buttons.refresh.loading = false;
+ this.buttons.refresh.disabled = false;
+
+ this.status.state = status.open.state;
+ this.status.text = status.open.text;
+ this.icon = require('@/assets/images/open.svg');
+
+ if (image) {
+ this.image = 'data:image/jpeg;base64,' + response.image;
+ } else {
+ this.image = require('@/assets/images/open.svg');
+ }
+ },
+ stateMoving(response) {
+ var image;
+
+ if (typeof response.image != 'undefined') {
+ image = true;
+ } else {
+ image = false;
+ }
+
+ this.buttons.action.loading = true;
+ this.buttons.action.disabled = true;
+ this.buttons.refresh.loading = true;
+ this.buttons.refresh.disabled = true;
+
+ this.status.state = status.moving.state;
+ this.status.text = status.moving.text;
+ this.icon = require('@/assets/images/moving.svg');
+
+ if (image) {
+ this.image = 'data:image/jpeg;base64,' + response.image;
+ } else {
+ this.image = require('@/assets/images/moving.svg');
+ }
+ },
+ stateError(err) {
+ this.buttons.action.loading = false;
+ this.buttons.refresh.loading = false;
+ this.snackbar.text = messages.refreshFailed.text;
+ this.snackbar.color = messages.refreshFailed.color;
+ this.snackbar.show = true;
+ },
+ },
+};
</script>
--- /dev/null
+<template>
+ <v-content>
+ <v-card flat>
+ <v-list-item three-line>
+ <v-list-item-content>
+ <v-list-item-subtitle>
+ Either Production API-Key or Test mode must be activated.
+ </v-list-item-subtitle>
+ </v-list-item-content>
+ </v-list-item>
+ </v-card>
+
+ <v-list
+ three-line
+ flat
+ >
+ <v-list-item-group
+ v-model="settings"
+ multiple
+ >
+ <v-divider />
+ <template v-for="item in listItems">
+ <v-divider v-if="item.title === null" />
+ <v-list-item v-else v-on:change="item.itemAction">
+ <template>
+ <v-list-item-action>
+ <v-switch
+ v-model="item.value"
+ color="primary"
+ v-on:change="item.switchAction"
+ />
+ </v-list-item-action>
+ <v-list-item-content>
+ <v-list-item-title>
+ {{item.title}}
+ </v-list-item-title>
+ <v-list-item-subtitle>
+ {{item.subtitle}}
+ </v-list-item-subtitle>
+ </v-list-item-content>
+ </template>
+ </v-list-item>
+ </template>
+ </v-list-item-group>
+ </v-list>
+
+ <ApiKeyDialog
+ :showApiKeyDialog="showApiKeyDialog"
+ v-on:apiKeySetEvent="setApiKey"
+ />
+ </v-content>
+</template>
+
+<script>
+import ApiKeyDialog from '@/components/ApiKey'
+import { storage } from '@/lib/lib.js'
+
+export default {
+ components: {
+ ApiKeyDialog,
+ },
+ data() {
+ return {
+ listItems: [
+ {
+ value: false,
+ itemAction: this.setApiKeyInItem,
+ switchAction: this.setApiKeyInSwitch,
+ title: 'Production API-Key',
+ subtitle: 'Allows access on pruduction features',
+ },
+ {
+ value: false,
+ itemAction: this.setPushNotificationInItem,
+ switchAction: this.setPushNotificationInSwitch,
+ title: 'Push notification',
+ subtitle: 'Allows access on production features',
+ },
+ {
+ value: false,
+ itemAction: this.setImageTransmissionInItem,
+ switchAction: this.setImageTransmissionInSwitch,
+ title: 'Transmit live image',
+ subtitle: 'Gets web cam image of garage door every time it\'s refreshed',
+ },
+ {
+ title: null,
+ },
+ {
+ value: false,
+ itemAction: this.setTestModeInItem,
+ switchAction: this.setTestModeInSwitch,
+ title: 'Test mode',
+ subtitle: 'Enables simulation of production features',
+ },
+ ],
+ settings: [
+ {
+ key: 'apiKey',
+ value: '',
+ },
+ {
+ key: 'pushNotification',
+ value: false,
+ },
+ {
+ key: 'imageTransmission',
+ value: false,
+ },
+ {
+ },
+ {
+ key: 'testMode',
+ value: false,
+ },
+ ],
+ showApiKeyDialog: false,
+ };
+ },
+ created() {
+ if (storage.hasItem('testMode')) {
+ this.listItems[0].value = storage.getItem('apiKey');
+ this.listItems[1].value = storage.getItem('pushNotification');
+ this.listItems[2].value = storage.getItem('imageTransmission');
+ this.listItems[4].value = storage.getItem('testMode');
+ } else {
+ storage.setItem('apiKey', '');
+ storage.setItem('pushNotification', false);
+ storage.setItem('imageTransmission', false);
+ storage.setItem('testMode', false);
+ }
+ },
+ methods: {
+ setApiKeyInItem() {
+ this.listItems[0].value = !this.listItems[0].value;
+ if (this.listItems[0].value) {
+ this.showApiKeyDialog = ['showDialog'];
+ } else {
+ this.setApiKey('');
+ }
+ },
+ setApiKeyInSwitch() {
+ this.listItems[0].value = !this.listItems[0].value;
+ },
+ setApiKey(value) {
+ value === ''
+ ? this.listItems[0].value = false
+ : this.listItems[0].value = true;
+ this.settings[0].value = value;
+ storage.setItem('apiKey', this.settings[0].value);
+ },
+ setPushNotificationInItem() {
+ this.listItems[1].value = !this.listItems[1].value;
+ this.settings[1].value = this.listItems[1].value;
+ storage.setItem('pushNotification', this.settings[1].value);
+ },
+ setPushNotificationInSwitch() {
+ this.listItems[1].value = !this.listItems[1].value;
+ },
+ setImageTransmissionInItem() {
+ this.listItems[2].value = !this.listItems[2].value;
+ this.settings[2].value = this.listItems[2].value;
+ storage.setItem('imageTransmission', this.settings[2].value);
+ },
+ setImageTransmissionInSwitch() {
+ this.listItems[2].value = !this.listItems[2].value;
+ },
+ setTestModeInItem() {
+ this.listItems[4].value = !this.listItems[4].value;
+ this.settings[4].value = this.listItems[4].value;
+ storage.setItem('testMode', this.settings[4].value);
+ },
+ setTestModeInSwitch() {
+ this.listItems[4].value = !this.listItems[4].value;
+ },
+ },
+};
+</script>
--- /dev/null
+module.exports = {
+ "transpileDependencies": [
+ "vuetify"
+ ],
+
+ pwa: {
+ name: 'Garage Node',
+ themeColor: '#1976D2',
+ msTileColor: '#424242'
+ },
+
+ publicPath: ''
+}