Big update to deploy scripts, and deploy prosecss
No files matched your search
@@ -1,31 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Push built release files to production server
|
|
||||||
#
|
|
||||||
|
|
||||||
echo -e "\e[32m\nStarting Build. \n\e[0m"
|
|
||||||
|
|
||||||
# Build out new release
|
|
||||||
cd client
|
|
||||||
npm run build
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# only compress client/dist and server with node_modules
|
|
||||||
echo -e "\e[32m\nCompressing client and server code... \n\e[0m"
|
|
||||||
tar -czf release.tar.gz server node_modules client/dist staticFiles/assets
|
|
||||||
|
|
||||||
#send compressed release to remote machine
|
|
||||||
echo -e "\e[32m\nMoving compressed release to production... \n\e[0m"
|
|
||||||
rsync -e 'ssh -p 13328' -havzC --update release.tar.gz [email protected]:/home/mab/pi/
|
|
||||||
|
|
||||||
# Remove Release from local after its been uploaded
|
|
||||||
rm release.tar.gz
|
|
||||||
|
|
||||||
#uncompress release on server
|
|
||||||
echo -e "\e[32m\nExtracting release on production... \n\e[0m"
|
|
||||||
ssh [email protected] -p 13328 "cd /home/mab/pi/; rm -r server node_modules client; tar -xzf *.tar.gz --overwrite; rm *.tar.gz; pm2 reload all"
|
|
||||||
|
|
||||||
#Congratulate how awesome you are
|
|
||||||
echo -e "\e[32m\nRelease Complete! Nice Work! \n\e[0m"
|
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
.DS_Store
|
# Logs
|
||||||
node_modules
|
logs
|
||||||
/dist
|
*.log
|
||||||
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
*.pem
|
|
||||||
*.crt
|
|
||||||
*.key
|
|
||||||
|
|
||||||
# Log files
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.DS_Store
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Local dev TLS certs (mkcert)
|
||||||
|
*.pem
|
||||||
|
certs/
|
||||||
@@ -1,19 +1,5 @@
|
|||||||
# client
|
# Vue 3 + Vite
|
||||||
|
|
||||||
## Project setup
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
```
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||||
```
|
|
||||||
npm run serve
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and minifies for production
|
|
||||||
```
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Customize configuration
|
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"module": "esnext",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lib": [
|
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
{
|
{
|
||||||
"name": "solidscribe",
|
"name": "solidscribe-client",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "vite",
|
||||||
"build": "vue-cli-service build"
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"axios": "^1.1.3",
|
"axios": "^1.1.3",
|
||||||
"core-js": "^3.6.5",
|
"chart.js": "^4.6.1",
|
||||||
"es6-promise": "^4.2.8",
|
"diff-match-patch": "^1.0.5",
|
||||||
"fomantic-ui-css": "^2.9.0",
|
"fomantic-ui-css": "^2.9.0",
|
||||||
"vue": "^2.6.11",
|
"mitt": "^3.0.1",
|
||||||
"vue-chartjs": "^5.0.1",
|
"socket.io-client": "^2.5.0",
|
||||||
"vue-router": "^3.2.0",
|
"tailwindcss": "^4.1.13",
|
||||||
"vuedraggable": "^2.24.3",
|
"vue": "^3.5.18",
|
||||||
"vuex": "^3.4.0"
|
"vue-chartjs": "^5.3.4",
|
||||||
|
"vue-router": "^4.0.0",
|
||||||
|
"vuedraggable": "^4.1.0",
|
||||||
|
"vuex": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^5.0.8",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/cli-plugin-router": "^5.0.8",
|
"vite": "^7.1.2",
|
||||||
"@vue/cli-plugin-vuex": "^5.0.8",
|
"vite-plugin-pwa": "^2.0.0",
|
||||||
"@vue/cli-service": "^5.0.8",
|
"workbox-window": "^7.4.1"
|
||||||
"vue-template-compiler": "^2.6.11"
|
}
|
||||||
},
|
|
||||||
"browserslist": [
|
|
||||||
"> 1%",
|
|
||||||
"last 2 versions",
|
|
||||||
"not dead"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 799 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 132.29166 132.29167" width="512" height="512">
|
||||||
|
<rect x="0" y="0" width="132.29166" height="132.29167" fill="#ffffff"/>
|
||||||
|
<g transform="translate(13.229166,13.229167) scale(0.8)">
|
||||||
|
<g transform="translate(0,-164.70832)">
|
||||||
|
<path id="path3813-4" d="m 56.22733,165.36641 -55.56249926,15.875 8e-7,63.5 47.62499846,11.90625 v 27.78125 l -47.76066333,-13.9757 0.13566407,10.00695 55.56249926,15.875 v -47.625 l -47.6249985,-11.90625 -8e-7,-47.625 47.7606633,-13.94121 c 0.135664,-2.30629 -0.135664,-9.87129 -0.135664,-9.87129 z" style="fill:#0f7425;fill-opacity:1;stroke:none;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
|
||||||
|
<path id="path4563" d="m 20.508581,220.92891 c 15.265814,-14.23899 27.809717,-7.68002 39.687499,3.96875 v -7.9375 C 51.75093,200.8366 37.512584,206.01499 20.508581,205.05391 Z" style="fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
|
||||||
|
<path id="path4563-6" d="m 111.78985,220.92891 c -15.265834,-14.23899 -27.809737,-7.68002 -39.68752,3.96875 v -7.9375 c 8.445151,-16.12356 22.683497,-10.94517 39.68752,-11.90625 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
|
||||||
|
<path id="path3813-4-2" d="m 76.07108,165.36641 55.5625,15.875 v 63.5 l -47.625,11.90625 v 27.78125 l 47.76067,-13.9757 -0.13567,10.00695 -55.5625,15.875 v -47.625 l 47.625,-11.90626 V 189.17891 L 75.93542,175.2377 c -0.13567,-2.30629 0.13566,-9.87129 0.13566,-9.87129 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 132.29166 132.29167" width="512" height="512">
|
||||||
|
<g transform="translate(0,-164.70832)">
|
||||||
|
<path id="path3813-4" d="m 56.22733,165.36641 -55.56249926,15.875 8e-7,63.5 47.62499846,11.90625 v 27.78125 l -47.76066333,-13.9757 0.13566407,10.00695 55.56249926,15.875 v -47.625 l -47.6249985,-11.90625 -8e-7,-47.625 47.7606633,-13.94121 c 0.135664,-2.30629 -0.135664,-9.87129 -0.135664,-9.87129 z" style="fill:#0f7425;fill-opacity:1;stroke:none;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
|
||||||
|
<path id="path4563" d="m 20.508581,220.92891 c 15.265814,-14.23899 27.809717,-7.68002 39.687499,3.96875 v -7.9375 C 51.75093,200.8366 37.512584,206.01499 20.508581,205.05391 Z" style="fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
|
||||||
|
<path id="path4563-6" d="m 111.78985,220.92891 c -15.265834,-14.23899 -27.809737,-7.68002 -39.68752,3.96875 v -7.9375 c 8.445151,-16.12356 22.683497,-10.94517 39.68752,-11.90625 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
|
||||||
|
<path id="path3813-4-2" d="m 76.07108,165.36641 55.5625,15.875 v 63.5 l -47.625,11.90625 v 27.78125 l 47.76067,-13.9757 -0.13567,10.00695 -55.5625,15.875 v -47.625 l 47.625,-11.90626 V 189.17891 L 75.93542,175.2377 c -0.13567,-2.30629 0.13566,-9.87129 0.13566,-9.87129 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 215 B |
@@ -1,70 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
|
|
||||||
<link rel="icon" href="/api/static/assets/favicon.ico" type="image/ico"/>
|
|
||||||
<link rel="shortcut icon" href="/api/static/assets/favicon.ico" type="image/x-icon"/>
|
|
||||||
|
|
||||||
<meta name="theme-color" content="#000" />
|
|
||||||
<!-- Manifest is generated per page -->
|
|
||||||
<!-- <link rel="manifest" href="/api/static/assets/manifest.json"> -->
|
|
||||||
|
|
||||||
<title>Solid Scribe - An easy, encrypted Note App</title>
|
|
||||||
<!-- <title><%= htmlWebpackPlugin.options.title %></title> -->
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but Solid Scribe doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app">
|
|
||||||
<!-- placeholder data for scrapers with no JS -->
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background-color: #212221;
|
|
||||||
color: #aeaeae;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.centered {
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
text-align: center;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
.logo {
|
|
||||||
width: 200px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
.scrape-info {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="centered">
|
|
||||||
<img class="logo" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo - if you can read this your connection is really slow">
|
|
||||||
<h1>Solid Scribe</h1>
|
|
||||||
<h3>An easy, encrypted Note App</h3>
|
|
||||||
<h4>Loading...</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="scrape-info">
|
|
||||||
<h1>Solid Scribe</h1>
|
|
||||||
<h2>A note application that respects your privacy.</h2>
|
|
||||||
<p>Take notes with a clean editor that works on desktop or mobile.</p>
|
|
||||||
<p>Search notes, links and files to find what you need.</p>
|
|
||||||
<p>Accessable everywhere.</p>
|
|
||||||
<p>Categorize notes with tags.</p>
|
|
||||||
<p>Share data with fellow users.</p>
|
|
||||||
<p>Encrypt notes for additional security.</p>
|
|
||||||
<b>This site requires Javascipt to run.</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"id": "/",
|
||||||
|
"name": "Solid Scribe",
|
||||||
|
"short_name": "Scribe",
|
||||||
|
"description": "Encrypted notes and scratch pad",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"display_override": [
|
||||||
|
"standalone",
|
||||||
|
"minimal-ui"
|
||||||
|
],
|
||||||
|
"orientation": "any",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#000000",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/img/icons/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/icons/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/icons/maskable-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/icons/maskable-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "Scratch Pad",
|
||||||
|
"short_name": "Scratch",
|
||||||
|
"url": "/quick",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/img/icons/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Files & Links",
|
||||||
|
"short_name": "Files",
|
||||||
|
"url": "/attachments",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/img/icons/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"share_target": {
|
||||||
|
"action": "/share-target",
|
||||||
|
"method": "GET",
|
||||||
|
"enctype": "application/x-www-form-urlencoded",
|
||||||
|
"params": {
|
||||||
|
"title": "title",
|
||||||
|
"text": "text",
|
||||||
|
"url": "url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
@@ -51,19 +51,24 @@
|
|||||||
|
|
||||||
<global-notification />
|
<global-notification />
|
||||||
|
|
||||||
|
<update-prompt />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
// import io from 'socket.io-client'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import GlobalSiteMenu from '@/components/GlobalSiteMenu.vue'
|
||||||
|
import GlobalNotification from '@/components/GlobalNotificationComponent.vue'
|
||||||
|
import UpdatePrompt from '@/components/UpdatePrompt.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
'global-site-menu': require('@/components/GlobalSiteMenu.vue').default,
|
'global-site-menu': GlobalSiteMenu,
|
||||||
'global-notification':require('@/components/GlobalNotificationComponent.vue').default,
|
'global-notification': GlobalNotification,
|
||||||
|
'update-prompt': UpdatePrompt,
|
||||||
},
|
},
|
||||||
data: function(){
|
data: function(){
|
||||||
return {
|
return {
|
||||||
@@ -82,74 +87,6 @@ export default {
|
|||||||
// - Gets new session tokens from server and uses them in app
|
// - Gets new session tokens from server and uses them in app
|
||||||
beforeCreate: function(){
|
beforeCreate: function(){
|
||||||
|
|
||||||
const root = 'https://www.solidscribe.com'
|
|
||||||
// const root = 'https://192.168.1.164'
|
|
||||||
const manifest = {
|
|
||||||
"theme_color":"#000",
|
|
||||||
"background_color": "#000",
|
|
||||||
"description": "Take Notes",
|
|
||||||
"display_override": ["fullscreen", "minimal-ui"],
|
|
||||||
"display": "standalone",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": root+"/api/static/assets/logo.png",
|
|
||||||
"sizes": "495x495",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": root+"/api/static/assets/maskable_icon.png",
|
|
||||||
"sizes": "826x826",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"screenshots" : [
|
|
||||||
{
|
|
||||||
"src": root+"/api/static/assets/lightmode.png",
|
|
||||||
"sizes": "1000x1080",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": root+"/api/static/assets/darkmode.png",
|
|
||||||
"sizes": "1000x1081",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"shortcuts" : [
|
|
||||||
{
|
|
||||||
"name": "Files & Links",
|
|
||||||
"url": " /attachments",
|
|
||||||
"description": "All Note files and links",
|
|
||||||
"icons": [{
|
|
||||||
"src": root+"/api/static/assets/icon1.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image/png"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Metric Track",
|
|
||||||
"url": "/metrictrack",
|
|
||||||
"description": "Metric Tracking",
|
|
||||||
"icons": [{
|
|
||||||
"src": root+"/api/static/assets/icon2.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image/png"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "Solid Scribe",
|
|
||||||
"short_name": "Solid Scribe",
|
|
||||||
"author": "Max",
|
|
||||||
"scope": root,
|
|
||||||
"start_url": window.location.href
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = document.createElement('link')
|
|
||||||
link.rel = 'manifest'
|
|
||||||
link.href = `data:application/json;base64,${btoa(JSON.stringify(manifest))}`
|
|
||||||
document.head.appendChild(link)
|
|
||||||
|
|
||||||
//Before all requests going out
|
//Before all requests going out
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
@@ -240,7 +177,9 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted: function(){
|
mounted: function(){
|
||||||
|
|
||||||
const isDev = process.env['NODE_ENV'] == 'development'
|
// Vite does not shim node's `process` into the browser bundle the way
|
||||||
|
// webpack/vue-cli did; import.meta.env.DEV is the equivalent.
|
||||||
|
const isDev = import.meta.env.DEV
|
||||||
// if(window.location.hostname.toLowerCase().replace('www.','') != "solidscribe.com" && !isDev){
|
// if(window.location.hostname.toLowerCase().replace('www.','') != "solidscribe.com" && !isDev){
|
||||||
// this.showFakeSite = true
|
// this.showFakeSite = true
|
||||||
// setInterval(() => {
|
// setInterval(() => {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import Vue from 'vue'
|
import mitt from 'mitt'
|
||||||
const EventBus = new Vue()
|
|
||||||
|
|
||||||
Object.defineProperties(Vue.prototype, {
|
const emitter = mitt()
|
||||||
$bus: {
|
|
||||||
get: () => EventBus
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default EventBus;
|
// Preserves the Vue 2 $on/$emit/$off API used throughout the codebase
|
||||||
|
const EventBus = {
|
||||||
|
$on: (...args) => emitter.on(...args),
|
||||||
|
$off: (...args) => emitter.off(...args),
|
||||||
|
$emit: (...args) => emitter.emit(...args),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventBus
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
const helpers = {}
|
const helpers = {}
|
||||||
|
|
||||||
helpers.timeAgo = (time) => {
|
helpers.timeAgo = (time) => {
|
||||||
@@ -62,10 +60,9 @@ helpers.timeAgo = (time) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const plugin = {
|
const plugin = {
|
||||||
install () {
|
install(app) {
|
||||||
Vue.helpers = helpers
|
app.config.globalProperties.$helpers = helpers
|
||||||
Vue.prototype.$helpers = helpers
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.use(plugin)
|
export default plugin
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// Launch page preference.
|
||||||
|
//
|
||||||
|
// The app used to inject its manifest at runtime with
|
||||||
|
// `start_url: window.location.href`, so installing from /metrictrack made the
|
||||||
|
// installed app always open there. That mattered: not everyone wants the notes
|
||||||
|
// page as their home.
|
||||||
|
//
|
||||||
|
// A static manifest can't carry a per-install start_url, and we need a static
|
||||||
|
// same-origin manifest for share_target to register at all (a data: URI
|
||||||
|
// manifest has an opaque origin, so scope resolution fails). So the preference
|
||||||
|
// moves in-app: the manifest's start_url is "/", and we redirect from there.
|
||||||
|
//
|
||||||
|
// This is also strictly better than the old behaviour -- the old one froze the
|
||||||
|
// home page at install time and could only be changed by reinstalling. This one
|
||||||
|
// is editable in Settings.
|
||||||
|
//
|
||||||
|
|
||||||
|
const LAUNCH_KEY = 'launchPage'
|
||||||
|
const DEFAULT_LAUNCH = '/notes'
|
||||||
|
|
||||||
|
// Pages that make sense as a home. Anything else (a specific note, the share
|
||||||
|
// target, login) would be a bad place to land every time.
|
||||||
|
export const LAUNCH_OPTIONS = [
|
||||||
|
{ path: '/notes', label: 'Notes' },
|
||||||
|
{ path: '/overview', label: 'Overview' },
|
||||||
|
{ path: '/quick', label: 'Scratch Pad' },
|
||||||
|
{ path: '/metrictrack', label: 'Metric Tracking' },
|
||||||
|
{ path: '/attachments', label: 'Files & Links' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function getLaunchPage(){
|
||||||
|
const stored = localStorage.getItem(LAUNCH_KEY)
|
||||||
|
const valid = LAUNCH_OPTIONS.some(option => option.path === stored)
|
||||||
|
return valid ? stored : DEFAULT_LAUNCH
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLaunchPage(path){
|
||||||
|
if(!LAUNCH_OPTIONS.some(option => option.path === path)){ return }
|
||||||
|
localStorage.setItem(LAUNCH_KEY, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStandalone(){
|
||||||
|
return window.matchMedia('(display-mode: standalone)').matches
|
||||||
|
|| window.navigator.standalone === true
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Capture the page the user installed from, matching the old behaviour: if they
|
||||||
|
// install while on /metrictrack, that becomes their launch page. Only set as a
|
||||||
|
// default -- an explicit choice in Settings is never overwritten.
|
||||||
|
//
|
||||||
|
export function watchForInstall(){
|
||||||
|
window.addEventListener('appinstalled', () => {
|
||||||
|
if(localStorage.getItem(LAUNCH_KEY)){ return }
|
||||||
|
|
||||||
|
const path = window.location.pathname
|
||||||
|
if(LAUNCH_OPTIONS.some(option => option.path === path)){
|
||||||
|
localStorage.setItem(LAUNCH_KEY, path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// "/" is a shim for installed launches. Redirect only when running standalone,
|
||||||
|
// so the marketing home page still works normally in a browser tab.
|
||||||
|
//
|
||||||
|
export function installLaunchRedirect(router){
|
||||||
|
|
||||||
|
watchForInstall()
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
|
||||||
|
// Only the very first navigation of a standalone launch
|
||||||
|
if(to.path === '/' && from.matched.length === 0 && isStandalone()){
|
||||||
|
return next(getLaunchPage())
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1001,3 +1001,8 @@ padding-right: 10px;
|
|||||||
background-color: rgba(0,0,0,0.7);
|
background-color: rgba(0,0,0,0.7);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
i.outline {
|
||||||
|
outline-style: none;
|
||||||
|
}
|
||||||
@@ -18,12 +18,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
export default {
|
export default {
|
||||||
name: 'FileUploadButton',
|
name: 'FileUploadButton',
|
||||||
props: [ 'noteId' ],
|
props: [ 'noteId' ],
|
||||||
components: {
|
components: {
|
||||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
'nm-button':defineAsyncComponent(() => import('@/components/NoteMenuButtonComponent.vue'))
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -369,14 +369,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
'search-input': require('@/components/SearchInput.vue').default,
|
'search-input': defineAsyncComponent(() => import('@/components/SearchInput.vue')),
|
||||||
'counter':require('@/components/AnimatedCounterComponent.vue').default,
|
'counter':defineAsyncComponent(() => import('@/components/AnimatedCounterComponent.vue')),
|
||||||
'logo':require('@/components/LogoComponent.vue').default,
|
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||||
},
|
},
|
||||||
data: function(){
|
data: function(){
|
||||||
return {
|
return {
|
||||||
@@ -393,7 +394,7 @@
|
|||||||
beforeMount(){
|
beforeMount(){
|
||||||
window.addEventListener('resize', this.resizeEventHandler)
|
window.addEventListener('resize', this.resizeEventHandler)
|
||||||
},
|
},
|
||||||
beforeDestroy(){
|
beforeUnmount(){
|
||||||
window.removeEventListener('resize', this.resizeEventHandler)
|
window.removeEventListener('resize', this.resizeEventHandler)
|
||||||
},
|
},
|
||||||
mounted: function(){
|
mounted: function(){
|
||||||
|
|||||||
@@ -153,8 +153,10 @@
|
|||||||
this.$io.emit('user_connect', token)
|
this.$io.emit('user_connect', token)
|
||||||
localStorage.setItem('loginToken', token)
|
localStorage.setItem('loginToken', token)
|
||||||
|
|
||||||
//Redirect user to notes section after login
|
//Redirect user to notes section after login, or back to
|
||||||
this.$router.push('/notes')
|
//wherever they were headed (e.g. an Android share waiting to
|
||||||
|
//be saved -- see ShareTargetPage.vue)
|
||||||
|
this.$router.push(this.$route.query.redirect || '/notes')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
register(){
|
register(){
|
||||||
|
|||||||
@@ -12,18 +12,6 @@
|
|||||||
width="500">
|
width="500">
|
||||||
<defs
|
<defs
|
||||||
id="defs2" />
|
id="defs2" />
|
||||||
<metadata
|
|
||||||
id="metadata5">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
<g
|
||||||
style="display:inline"
|
style="display:inline"
|
||||||
transform="translate(0,-164.70832)"
|
transform="translate(0,-164.70832)"
|
||||||
|
|||||||
@@ -43,12 +43,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sixteen wide column">
|
<div class="sixteen wide column">
|
||||||
<dix class="ui basic segment" v-if="!editGraphs">
|
<div class="ui basic segment" v-if="!editGraphs">
|
||||||
<div class="ui button" v-on:click="toggleEditGraphs">
|
<div class="ui button" v-on:click="toggleEditGraphs">
|
||||||
<i class="edit icon"></i>
|
<i class="edit icon"></i>
|
||||||
<span>Add/Edit Graphs</span>
|
<span>Add/Edit Graphs</span>
|
||||||
</div>
|
</div>
|
||||||
</dix>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div v-if="editGraphs">
|
<div v-if="editGraphs">
|
||||||
@@ -64,7 +64,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="(graph, index) in graphs" :class="`ui not-padded ${editGraphs?'active ':'inactive '}segment animate-height`">
|
<div class="ui basic center aligned segment" v-if="!graphs || graphs.length == 0">
|
||||||
|
<h4 class="ui header">No graphs yet</h4>
|
||||||
|
<p>Choose <b>Add/Edit Graphs</b>, then <b>New Graph</b>, and pick which metrics to plot.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(graph, index) in graphs" :class="`ui not-padded ${editGraphs?'active ':'inactive '}segment`">
|
||||||
|
|
||||||
<!-- Edit options -->
|
<!-- Edit options -->
|
||||||
<div class="ui small header" v-if="editGraphs">
|
<div class="ui small header" v-if="editGraphs">
|
||||||
@@ -126,15 +131,20 @@
|
|||||||
<span v-if="!editGraphs">Click "Edit Graphs" then,</span>
|
<span v-if="!editGraphs">Click "Edit Graphs" then,</span>
|
||||||
Select Graph type and Metrics to display
|
Select Graph type and Metrics to display
|
||||||
</div>
|
</div>
|
||||||
<div v-if="graph?.type == undefined && graph.fieldIds && graph.fieldIds.length > 0">
|
<div v-if="isLineGraph(graph) && graph.fieldIds && graph.fieldIds.length > 0">
|
||||||
<div :id="`graphdiv${index}`" style="width: 100%; min-height: 320px;"></div>
|
<LineGraph
|
||||||
|
:graph="graph"
|
||||||
|
:userFields="userFields"
|
||||||
|
:cycleData="cycleData"
|
||||||
|
:tempChartDays="tempChartDays"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui segment" v-if="editGraphs">
|
<div class="ui segment" v-if="editGraphs">
|
||||||
|
|
||||||
<!-- change graph type -->
|
<!-- change graph type -->
|
||||||
<div v-for="(graphType, graphId) in graphTypesDef" class="ui buttons">
|
<div v-for="(graphType, graphId) in graphTypesDef" class="ui buttons">
|
||||||
<div class="ui tiny button" v-on:click="changeGraphType(index, graphId)" :class="{'green':(String(graphId) == String(graph?.type))}">
|
<div class="ui tiny button" v-on:click="changeGraphType(index, graphId)" :class="{'green':isGraphTypeActive(graphId, graph)}">
|
||||||
{{ graphType }}
|
{{ graphType }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,7 +156,7 @@
|
|||||||
<span v-else v-on:click="toggleGraphField(fieldId, index)">
|
<span v-else v-on:click="toggleGraphField(fieldId, index)">
|
||||||
<i class="square outline icon"></i>
|
<i class="square outline icon"></i>
|
||||||
</span>
|
</span>
|
||||||
<i :class="`${$parent.getFieldColor(fieldId)} ${$parent.getFieldIcon(fieldId)} icon`"></i>
|
<i :class="`${getFieldColor(fieldId)} ${getFieldIcon(fieldId)} icon`"></i>
|
||||||
<b>{{ userFields[fieldId]?.label }}</b>
|
<b>{{ userFields[fieldId]?.label }}</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,12 +170,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sixteen wide column">
|
<div class="sixteen wide column">
|
||||||
<dix class="ui basic segment" v-if="!editGraphs">
|
<div class="ui basic segment" v-if="!editGraphs">
|
||||||
<div class="ui button" v-on:click="toggleEditGraphs">
|
<div class="ui button" v-on:click="toggleEditGraphs">
|
||||||
<i class="edit icon"></i>
|
<i class="edit icon"></i>
|
||||||
<span>Add/Edit Graphs</span>
|
<span>Add/Edit Graphs</span>
|
||||||
</div>
|
</div>
|
||||||
</dix>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div v-if="editGraphs">
|
<div v-if="editGraphs">
|
||||||
@@ -189,11 +199,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
const PILL_CALENDAR = 'pillCalendar'
|
const PILL_CALENDAR = 'pillCalendar'
|
||||||
const LAST_DONE = 'lastDone'
|
const LAST_DONE = 'lastDone'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
inject: ['getFieldColor', 'getFieldIcon'],
|
||||||
name: 'MetricTrackingGraphs',
|
name: 'MetricTrackingGraphs',
|
||||||
props: [
|
props: [
|
||||||
'tempChartDays', // Number of days to display
|
'tempChartDays', // Number of days to display
|
||||||
@@ -205,7 +217,8 @@
|
|||||||
'editGraphs' // boolean for edit or not edit graphs
|
'editGraphs' // boolean for edit or not edit graphs
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
'PillCalendarGraph':require('@/components/Metrictracking/PillCalendarGraph.vue').default,
|
'PillCalendarGraph':defineAsyncComponent(() => import('@/components/Metrictracking/PillCalendarGraph.vue')),
|
||||||
|
'LineGraph':defineAsyncComponent(() => import('@/components/Metrictracking/LineGraph.vue')),
|
||||||
},
|
},
|
||||||
data: function(){
|
data: function(){
|
||||||
return {
|
return {
|
||||||
@@ -221,33 +234,9 @@
|
|||||||
// Constants
|
// Constants
|
||||||
this.PILL_CALENDAR = PILL_CALENDAR
|
this.PILL_CALENDAR = PILL_CALENDAR
|
||||||
this.LAST_DONE = LAST_DONE
|
this.LAST_DONE = LAST_DONE
|
||||||
|
|
||||||
// Include JS libraries
|
|
||||||
let graphsScript = document.createElement('script')
|
|
||||||
graphsScript.setAttribute('src', '//cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js')
|
|
||||||
document.head.appendChild(graphsScript)
|
|
||||||
},
|
},
|
||||||
mounted(){
|
mounted(){
|
||||||
this.localGraphData = this.graphs
|
this.localGraphData = this.graphs
|
||||||
|
|
||||||
this.graphCurrentData()
|
|
||||||
},
|
|
||||||
updated(){
|
|
||||||
// update graphs here? Or watch graphs prop
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
// whenever question changes, this function will run
|
|
||||||
userFields(newFields, oldFields) {
|
|
||||||
// console.log([newFields, oldFields])
|
|
||||||
if( JSON.stringify(oldFields) == "{}" ){
|
|
||||||
this.graphCurrentData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tempChartDays(newDays, oldDays){
|
|
||||||
if( newDays != oldDays ){
|
|
||||||
this.graphCurrentData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
saveGraphs(){
|
saveGraphs(){
|
||||||
@@ -267,10 +256,35 @@
|
|||||||
this.$emit('toggleEditGraphs')
|
this.$emit('toggleEditGraphs')
|
||||||
},
|
},
|
||||||
changeGraphType(index, newType){
|
changeGraphType(index, newType){
|
||||||
console.log(index + ' change to ' + newType)
|
|
||||||
this.localGraphData[index]['type'] = newType
|
// graphTypesDef is an object, so its keys are strings and the
|
||||||
|
// default line graph arrives here as the STRING 'undefined'.
|
||||||
|
// Saved graphs represent the line graph as an absent type, so
|
||||||
|
// remove the key rather than storing a string that no render
|
||||||
|
// test can match.
|
||||||
|
if(this.isLineGraphType(newType)){
|
||||||
|
delete this.localGraphData[index].type
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.localGraphData[index].type = newType
|
||||||
|
}
|
||||||
|
|
||||||
this.saveGraphs()
|
this.saveGraphs()
|
||||||
},
|
},
|
||||||
|
// A line graph is the default: an absent type. Accept the string
|
||||||
|
// form too so graphs saved by the older code still render.
|
||||||
|
isLineGraphType(type){
|
||||||
|
return type === undefined || type === null || type === '' || type === 'undefined'
|
||||||
|
},
|
||||||
|
isLineGraph(graph){
|
||||||
|
return this.isLineGraphType(graph?.type)
|
||||||
|
},
|
||||||
|
isGraphTypeActive(graphId, graph){
|
||||||
|
if(this.isLineGraphType(graphId)){
|
||||||
|
return this.isLineGraph(graph)
|
||||||
|
}
|
||||||
|
return String(graphId) === String(graph?.type)
|
||||||
|
},
|
||||||
addGraph(){
|
addGraph(){
|
||||||
this.localGraphData.push({})
|
this.localGraphData.push({})
|
||||||
this.saveGraphs()
|
this.saveGraphs()
|
||||||
@@ -329,103 +343,6 @@
|
|||||||
|
|
||||||
return title
|
return title
|
||||||
},
|
},
|
||||||
graphCurrentData(){
|
|
||||||
|
|
||||||
// try again if dygraphs isn't loaded
|
|
||||||
if( typeof(window.Dygraph) != 'function' ){
|
|
||||||
setTimeout(() => {
|
|
||||||
this.graphCurrentData()
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const graphOptions = {
|
|
||||||
interactionModel: {},
|
|
||||||
// pointClickCallback: function(e, pt){
|
|
||||||
// console.log(e)
|
|
||||||
// console.log(pt)
|
|
||||||
// console.log(this.getValue(pt.idx, 0))
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Excel date format YYYYMMDD
|
|
||||||
const convertToExcelDate = (dateCode) => {
|
|
||||||
return dateCode
|
|
||||||
.split('.')
|
|
||||||
.reverse()
|
|
||||||
.map(item => String(item).padStart(2,0))
|
|
||||||
.join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate set of keys for graph length
|
|
||||||
let dataKeys = Object.keys(this.cycleData)
|
|
||||||
dataKeys = dataKeys.splice(0, this.tempChartDays)
|
|
||||||
console.log(dataKeys)
|
|
||||||
|
|
||||||
|
|
||||||
// build CSV data for each graph
|
|
||||||
this.graphs.forEach((graph,index) => {
|
|
||||||
|
|
||||||
// only chart line graphs with dygraphs
|
|
||||||
if( graph.type != undefined ){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if( !graph.fieldIds ){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSV or path to a CSV file.
|
|
||||||
let dataString = ""
|
|
||||||
|
|
||||||
// Lookup graph field titles
|
|
||||||
let graphLabels = ['Date']
|
|
||||||
graph.fieldIds.forEach(fieldId => {
|
|
||||||
const graphLabel = this.userFields[fieldId]?.label
|
|
||||||
const escapedLabel = graphLabel.replaceAll(',','')
|
|
||||||
graphLabels.push(escapedLabel)
|
|
||||||
})
|
|
||||||
dataString += graphLabels.join(',') + '\n'
|
|
||||||
|
|
||||||
|
|
||||||
// build each row, for each day
|
|
||||||
for (var i = 0; i < dataKeys.length; i++) {
|
|
||||||
|
|
||||||
let nextFragment = []
|
|
||||||
// push date code to first column
|
|
||||||
nextFragment.push(convertToExcelDate(dataKeys[i]))
|
|
||||||
|
|
||||||
graph.fieldIds.forEach(fieldId => {
|
|
||||||
|
|
||||||
const currentEntry = this.cycleData[dataKeys[i]]
|
|
||||||
let currentValue = currentEntry[fieldId]
|
|
||||||
|
|
||||||
// setup correct float graphing
|
|
||||||
if(fieldId == 'BT'){
|
|
||||||
// parse temp to fixed length float 00.00
|
|
||||||
currentValue = parseFloat(currentValue).toFixed(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if( currentValue == undefined ){
|
|
||||||
currentValue = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
nextFragment.push(currentValue)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
dataString += nextFragment.join(',') + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let graphDiv = document.getElementById("graphdiv"+index)
|
|
||||||
const g = new Dygraph(graphDiv, dataString ,graphOptions)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -81,6 +81,13 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.days-since {
|
||||||
|
padding: 0.75em 0.5em 0.25em 0.5em;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
.days-since-row {
|
||||||
|
padding: 0.15em 0;
|
||||||
|
}
|
||||||
.pill-container {
|
.pill-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -229,36 +236,47 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="pill-container" v-for="(entry, dateCode) in getChartData" v-if="dateCode == `${day}.${calendar.month}.${calendar.year}`">
|
<template v-for="(entry, dateCode) in getChartData" :key="dateCode">
|
||||||
|
<span class="pill-container" v-if="dateCode == `${day}.${calendar.month}.${calendar.year}`">
|
||||||
|
<template v-for="(dayData, fieldId) in entry" :key="fieldId">
|
||||||
<span
|
<span
|
||||||
v-for="(dayData, fieldId) in entry"
|
|
||||||
v-if="showZeroValuesCheck(dayData.value, fieldId)"
|
v-if="showZeroValuesCheck(dayData.value, fieldId)"
|
||||||
class="pill"
|
class="pill"
|
||||||
:class="[$parent.$parent.getFieldColor(fieldId), {
|
:class="[getFieldColor(fieldId), {
|
||||||
'did-next':dayData.didNext,
|
'did-next':dayData.didNext,
|
||||||
'did-last':dayData.didLast,
|
'did-last':dayData.didLast,
|
||||||
'last-high':dayData.lastHigh,
|
'last-high':dayData.lastHigh,
|
||||||
'next-high':dayData.nextHigh,
|
'next-high':dayData.nextHigh,
|
||||||
}]">
|
}]">
|
||||||
<!-- 'zero-day':dayData.value == lowestGraphValue, -->
|
<!-- 'zero-day':dayData.value == lowestGraphValue, -->
|
||||||
<!-- <i v-if="dayData.value != 0" :class="`tiny ${$parent.$parent.getFieldColor(fieldId)} ${$parent.$parent.getFieldIcon(fieldId)} icon`"></i> -->
|
<!-- <i v-if="dayData.value != 0" :class="`tiny ${getFieldColor(fieldId)} ${getFieldIcon(fieldId)} icon`"></i> -->
|
||||||
<!-- <span v-else class="icon-spacer"></span>
|
<!-- <span v-else class="icon-spacer"></span>
|
||||||
:style="{height:(Math.round(dayData.value*5)+'px')}"
|
:style="{height:(Math.round(dayData.value*5)+'px')}"
|
||||||
|
|
||||||
-->
|
-->
|
||||||
<span v-if="dayData.value > lowestGraphValue-1" class="big-day">
|
<span v-if="dayData.value > lowestGraphValue-1" class="big-day">
|
||||||
<i v-if="!hideIcons" :class="`tiny white ${$parent.$parent.getFieldIcon(fieldId)} icon`"></i>
|
<i v-if="!hideIcons" :class="`tiny white ${getFieldIcon(fieldId)} icon`"></i>
|
||||||
<span v-if="!hideValues">
|
<span v-if="!hideValues">
|
||||||
{{ getDayValue(fieldId, dayData.value) }}
|
{{ getDayValue(fieldId, dayData.value) }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
|
</template>
|
||||||
<!-- <span v-for="fieldId in graph.fieldIds"></span> -->
|
<!-- <span v-for="fieldId in graph.fieldIds"></span> -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- time since each metric was last recorded -->
|
||||||
|
<div class="days-since" v-if="daysSince.length > 0">
|
||||||
|
<div class="days-since-row" v-for="entry in daysSince" :key="entry.fieldId">
|
||||||
|
<i :class="`${entry.color} ${entry.icon} icon`"></i>
|
||||||
|
<span v-if="entry.days === null">Days Since {{ entry.label }} - no entries yet</span>
|
||||||
|
<span v-else>Days Since {{ entry.label }} - {{ entry.days }} {{ entry.days === 1 ? 'day' : 'days' }}.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -269,6 +287,7 @@
|
|||||||
// let chartData = {}
|
// let chartData = {}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
inject: ['getFieldColor', 'getFieldIcon', 'getFieldLabel'],
|
||||||
props: [
|
props: [
|
||||||
'graph', // options associated with this graph
|
'graph', // options associated with this graph
|
||||||
'userFields', // all field attributes
|
'userFields', // all field attributes
|
||||||
@@ -309,6 +328,53 @@
|
|||||||
this.setupCalendar(new Date())
|
this.setupCalendar(new Date())
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
// "Days Since <metric> - <n> days" for every field on this graph.
|
||||||
|
// Counts a day as recorded only if it passes the same
|
||||||
|
// showZeroValuesCheck the pills use, so the "Show/Hide Lowest Value"
|
||||||
|
// toggle moves these numbers too.
|
||||||
|
daysSince(){
|
||||||
|
|
||||||
|
// showZeroValuesCheck reads lowestGraphValue, which getChartData
|
||||||
|
// assigns as a side effect -- touch it so the threshold is current
|
||||||
|
// and so this recomputes whenever the chart data does.
|
||||||
|
void this.getChartData
|
||||||
|
|
||||||
|
const dayMs = 86400000
|
||||||
|
const midnight = (time) => {
|
||||||
|
const date = new Date(time)
|
||||||
|
date.setHours(0, 0, 0, 0)
|
||||||
|
return date.getTime()
|
||||||
|
}
|
||||||
|
const today = midnight(Date.now())
|
||||||
|
|
||||||
|
return (this.graph?.fieldIds || []).map(fieldId => {
|
||||||
|
|
||||||
|
let latest = null
|
||||||
|
|
||||||
|
Object.keys(this.cycleData || {}).forEach(dateCode => {
|
||||||
|
|
||||||
|
const value = this.cycleData[dateCode]?.[fieldId]
|
||||||
|
if(value === undefined || value === null || value === ''){ return }
|
||||||
|
|
||||||
|
// honour the Lowest Value toggle
|
||||||
|
if(!this.showZeroValuesCheck(value, fieldId)){ return }
|
||||||
|
|
||||||
|
const time = this.dateCodeToTime(dateCode)
|
||||||
|
// ignore unparseable codes and anything dated ahead of today
|
||||||
|
if(time === null || time > today){ return }
|
||||||
|
|
||||||
|
if(latest === null || time > latest){ latest = time }
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldId,
|
||||||
|
label: this.getFieldLabel(fieldId),
|
||||||
|
icon: this.getFieldIcon(fieldId),
|
||||||
|
color: this.getFieldColor(fieldId),
|
||||||
|
days: latest === null ? null : Math.round((today - latest) / dayMs),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
getChartData(){
|
getChartData(){
|
||||||
|
|
||||||
let chartData = {}
|
let chartData = {}
|
||||||
@@ -389,11 +455,26 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// Date codes are stored unpadded as D.M.YYYY
|
||||||
|
dateCodeToTime(dateCode){
|
||||||
|
|
||||||
|
const parts = String(dateCode).split('.').map(Number)
|
||||||
|
if(parts.length !== 3 || parts.some(isNaN)){ return null }
|
||||||
|
|
||||||
|
const [day, month, year] = parts
|
||||||
|
const date = new Date(year, month - 1, day)
|
||||||
|
date.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
return isNaN(date.getTime()) ? null : date.getTime()
|
||||||
|
},
|
||||||
showZeroValuesCheck(dayValue, fieldId){
|
showZeroValuesCheck(dayValue, fieldId){
|
||||||
|
|
||||||
|
// Optional chaining: a graph can reference a default field (BT,
|
||||||
|
// CM, ...) that was never customised, so it has no userFields
|
||||||
|
// entry and the bare lookup threw.
|
||||||
// if graph type is boolean or there are two options
|
// if graph type is boolean or there are two options
|
||||||
let isBooleanField = this.userFields[fieldId].type == 'boolean'
|
let isBooleanField = this.userFields[fieldId]?.type == 'boolean'
|
||||||
if(this.userFields[fieldId].customOptions){
|
if(this.userFields[fieldId]?.customOptions){
|
||||||
let options = this.userFields[fieldId].customOptions
|
let options = this.userFields[fieldId].customOptions
|
||||||
|
|
||||||
isBooleanField = options.split(',').length == 2
|
isBooleanField = options.split(',').length == 2
|
||||||
@@ -411,6 +492,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// lowestGraphValue comes from Math.min of the charted values, so
|
||||||
|
// it is Infinity when the window holds no data -- without the
|
||||||
|
// finite guard nothing would ever clear the threshold.
|
||||||
|
if(!isFinite(this.lowestGraphValue)){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return this.showZeroValues || dayValue > this.lowestGraphValue
|
return this.showZeroValues || dayValue > this.lowestGraphValue
|
||||||
},
|
},
|
||||||
getDayValue(fieldId, value){
|
getDayValue(fieldId, value){
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
<div class="menu-bottom-half" :class="{ 'hide-text':(openNotes > 1) }">
|
<div class="menu-bottom-half" :class="{ 'hide-text':(openNotes > 1) }">
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/table`)" data-tooltip="Insert Table" data-position="bottom center">
|
<div class="edit-button" v-on:click="openMenuRoute('table')" data-tooltip="Insert Table" data-position="bottom center">
|
||||||
<i class="border all icon"></i>
|
<i class="border all icon"></i>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
@@ -87,13 +87,13 @@
|
|||||||
<div class="edit-divide"></div>
|
<div class="edit-divide"></div>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/colors`)" data-tooltip="Note Color" data-position="top center" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
|
<div class="edit-button" v-on:click="openMenuRoute('colors')" data-tooltip="Note Color" data-position="top center" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
|
||||||
<i class="paint brush icon"></i>
|
<i class="paint brush icon"></i>
|
||||||
</div> -->
|
</div> -->
|
||||||
<!-- <div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/tags`)" data-tooltip="Tags" data-position="top center">
|
<!-- <div class="edit-button" v-on:click="openMenuRoute('tags')" data-tooltip="Tags" data-position="top center">
|
||||||
<i class="tags icon"></i>
|
<i class="tags icon"></i>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/images`)" data-tooltip="Images" data-position="top center">
|
<div class="edit-button" v-on:click="openMenuRoute('images')" data-tooltip="Images" data-position="top center">
|
||||||
<i class="image icon"></i>
|
<i class="image icon"></i>
|
||||||
<span>Images</span>
|
<span>Images</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
|
|
||||||
<div class="edit-divide"></div>
|
<div class="edit-divide"></div>
|
||||||
|
|
||||||
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/options`)" data-tooltip="More Options" data-position="top center">
|
<div class="edit-button" v-on:click="openMenuRoute('options')" data-tooltip="More Options" data-position="top center">
|
||||||
|
|
||||||
<i class="ellipsis horizontal icon"></i>
|
<i class="ellipsis horizontal icon"></i>
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
|
|
||||||
<!-- tags on the side, only show on desktop -->
|
<!-- tags on the side, only show on desktop -->
|
||||||
<div class="note-mini-tag-area"
|
<div class="note-mini-tag-area"
|
||||||
v-on:click="$router.push(`/notes/open/${noteid}/menu/tags`)"
|
v-on:click="openMenuRoute('tags')"
|
||||||
:style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText'] }">
|
:style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText'] }">
|
||||||
|
|
||||||
<span class="add-mini-tag" v-if="allTags.length == 0">
|
<span class="add-mini-tag" v-if="allTags.length == 0">
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
|
|
||||||
<!-- Squire Box -->
|
<!-- Squire Box -->
|
||||||
<div
|
<div
|
||||||
id="squire-id"
|
:id="`squire-id-${noteid}`"
|
||||||
class="squire-box"
|
class="squire-box"
|
||||||
ref="squirebox"
|
ref="squirebox"
|
||||||
:style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText'] }"
|
:style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText'] }"
|
||||||
@@ -359,33 +359,32 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
const Squire = require('../assets/squire.js')
|
import '../assets/squire.js' // side-effect: sets window.Squire
|
||||||
|
const Squire = window.Squire
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
// const crypto = require('crypto')
|
import DiffMatchPatch from 'diff-match-patch'
|
||||||
const DiffMatchPatch = require('../../../server/helpers/DiffMatchPatch')
|
const dmp = new DiffMatchPatch()
|
||||||
const dmp = new DiffMatchPatch.diff_match_patch()
|
|
||||||
|
|
||||||
import SquireButtonFunctions from '@/mixins/SquireButtonFunctions.js'
|
import SquireButtonFunctions from '@/mixins/SquireButtonFunctions.js'
|
||||||
|
|
||||||
let rawNoteText = '' // Used for comparing and generating diffs
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NoteInputPanel',
|
name: 'NoteInputPanel',
|
||||||
props: [ 'noteid', 'position', 'openMenu', 'urlData', 'openNotes'],
|
props: [ 'noteid', 'position', 'openMenu', 'urlData', 'openNotes'],
|
||||||
components:{
|
components:{
|
||||||
'note-tag-edit': () => import('@/components/NoteTagEdit.vue'),
|
'note-tag-edit': defineAsyncComponent(() => import('@/components/NoteTagEdit.vue')),
|
||||||
'color-picker': () => import('@/components/ColorPicker.vue'),
|
'color-picker': defineAsyncComponent(() => import('@/components/ColorPicker.vue')),
|
||||||
'file-upload-button': () => import('@/components/FileUploadButton.vue'),
|
'file-upload-button': defineAsyncComponent(() => import('@/components/FileUploadButton.vue')),
|
||||||
// 'delete-button': () => import('@/components/NoteDeleteButtonComponent.vue'),
|
// 'delete-button': defineAsyncComponent(() => import('@/components/NoteDeleteButtonComponent.vue')),
|
||||||
'side-slide-menu': () => import('@/components/SideSlideMenuComponent.vue'),
|
'side-slide-menu': defineAsyncComponent(() => import('@/components/SideSlideMenuComponent.vue')),
|
||||||
'simple-attachment-note': () => import('@/components/SimpleAttachmentNoteComponent.vue'),
|
'simple-attachment-note': defineAsyncComponent(() => import('@/components/SimpleAttachmentNoteComponent.vue')),
|
||||||
'share-note-component': () => import('@/components/ShareNoteComponent.vue'),
|
'share-note-component': defineAsyncComponent(() => import('@/components/ShareNoteComponent.vue')),
|
||||||
|
|
||||||
'color-tooltip':require('@/components/TextColorTooltipComponent.vue').default,
|
'color-tooltip':defineAsyncComponent(() => import('@/components/TextColorTooltipComponent.vue')),
|
||||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default,
|
'nm-button':defineAsyncComponent(() => import('@/components/NoteMenuButtonComponent.vue')),
|
||||||
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
|
'loading-icon':defineAsyncComponent(() => import('@/components/LoadingIconComponent.vue')),
|
||||||
},
|
},
|
||||||
mixins:[ SquireButtonFunctions ],
|
mixins:[ SquireButtonFunctions ],
|
||||||
data(){
|
data(){
|
||||||
@@ -419,6 +418,9 @@
|
|||||||
|
|
||||||
//All the squire settings
|
//All the squire settings
|
||||||
editor: null,
|
editor: null,
|
||||||
|
// Baseline for diff generation. Per-instance: as a module-scoped
|
||||||
|
// variable this was shared by every open note's panel.
|
||||||
|
rawNoteText: '',
|
||||||
usersOnNote: 0,
|
usersOnNote: 0,
|
||||||
|
|
||||||
sideMenuOpen: false,
|
sideMenuOpen: false,
|
||||||
@@ -475,25 +477,17 @@
|
|||||||
},
|
},
|
||||||
beforeMount(){
|
beforeMount(){
|
||||||
|
|
||||||
this.$bus.$on('new_file_upload', ({noteId, imageCode}) => {
|
// Named handlers so beforeUnmount can remove only THIS panel's.
|
||||||
if(this.noteid == noteId && this.editor){
|
// mitt's off(type) with no handler removes every panel's.
|
||||||
this.editor.moveCursorToEnd()
|
this.$bus.$on('new_file_upload', this.onFileUpload)
|
||||||
this.editor.insertHTML(imageCode)
|
this.$bus.$on('close_note_by_id', this.onCloseNoteById)
|
||||||
this.save()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$bus.$on('close_note_by_id', (noteId) => {
|
|
||||||
if(noteId == this.noteid){
|
|
||||||
this.closeButtonAction()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
beforeDestroy(){
|
beforeUnmount(){
|
||||||
|
|
||||||
this.$io.emit('leave_room', this.rawTextId)
|
this.$io.emit('leave_room', { noteId: this.noteid })
|
||||||
|
|
||||||
this.$bus.$off('new_file_upload')
|
this.$bus.$off('new_file_upload', this.onFileUpload)
|
||||||
|
this.$bus.$off('close_note_by_id', this.onCloseNoteById)
|
||||||
|
|
||||||
this.destroyAttachmentStyles()
|
this.destroyAttachmentStyles()
|
||||||
|
|
||||||
@@ -533,13 +527,13 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
testModify(){
|
testModify(){
|
||||||
const text = document.getElementById('squire-id').children[0].innerHTML
|
const text = this.$refs.squirebox.children[0].innerHTML
|
||||||
const prepended = 'Hello-> ' + text
|
const prepended = 'Hello-> ' + text
|
||||||
document.getElementById('squire-id').children[0].innerHTML = prepended
|
this.$refs.squirebox.children[0].innerHTML = prepended
|
||||||
|
|
||||||
const text1 = document.getElementById('squire-id').children[2].innerHTML
|
const text1 = this.$refs.squirebox.children[2].innerHTML
|
||||||
const prepended1 = 'Hey-> ' + text1
|
const prepended1 = 'Hey-> ' + text1
|
||||||
document.getElementById('squire-id').children[2].innerHTML = prepended1
|
this.$refs.squirebox.children[2].innerHTML = prepended1
|
||||||
},
|
},
|
||||||
simulateTyping(index = 0){
|
simulateTyping(index = 0){
|
||||||
|
|
||||||
@@ -593,7 +587,7 @@
|
|||||||
this.setText(this.noteText)
|
this.setText(this.noteText)
|
||||||
|
|
||||||
// Use squire box HTML for diff/patch changes
|
// Use squire box HTML for diff/patch changes
|
||||||
rawNoteText = document.getElementById('squire-id').innerHTML
|
this.rawNoteText = this.$refs.squirebox.innerHTML
|
||||||
|
|
||||||
//focus on open, not on mobile, it causes the keyboard to pop up, thats annoying
|
//focus on open, not on mobile, it causes the keyboard to pop up, thats annoying
|
||||||
if(!this.$store.getters.getIsUserOnMobile){
|
if(!this.$store.getters.getIsUserOnMobile){
|
||||||
@@ -867,6 +861,8 @@
|
|||||||
},
|
},
|
||||||
//Called on squire event for keyup
|
//Called on squire event for keyup
|
||||||
diffText(event){
|
diffText(event){
|
||||||
|
console.log('Skipping diff text function')
|
||||||
|
return
|
||||||
// console.log(event.type)
|
// console.log(event.type)
|
||||||
|
|
||||||
const diffEvents = ['keyup','pathChange', 'click']
|
const diffEvents = ['keyup','pathChange', 'click']
|
||||||
@@ -880,17 +876,17 @@
|
|||||||
this.diffTextTimeout = setTimeout(() => {
|
this.diffTextTimeout = setTimeout(() => {
|
||||||
|
|
||||||
// Current Editor Text
|
// Current Editor Text
|
||||||
const liveEditorElm = document.getElementById('squire-id')
|
const liveEditorElm = this.$refs.squirebox
|
||||||
|
|
||||||
// virtual element for selecting div
|
// virtual element for selecting div
|
||||||
let virtualEditorElm = document.createElement('div')
|
let virtualEditorElm = document.createElement('div')
|
||||||
virtualEditorElm.innerHTML = rawNoteText
|
virtualEditorElm.innerHTML = this.rawNoteText
|
||||||
|
|
||||||
// element at cursor
|
// element at cursor
|
||||||
const elmAtCaret = window.getSelection().getRangeAt(0).startContainer.parentNode
|
const elmAtCaret = window.getSelection().getRangeAt(0).startContainer.parentNode
|
||||||
|
|
||||||
// Remove beginngin selector from path, make it more generic
|
// Remove beginngin selector from path, make it more generic
|
||||||
const path = this.generateSelector(elmAtCaret).replace('div#squire-id > ','')
|
const path = this.generateSelector(elmAtCaret).replace(`div#squire-id-${this.noteid} > `,'')
|
||||||
let workingPath = ''
|
let workingPath = ''
|
||||||
|
|
||||||
// default to entire note text, select down if path
|
// default to entire note text, select down if path
|
||||||
@@ -938,10 +934,10 @@
|
|||||||
const patch_text = dmp.patch_toText(patch_list)
|
const patch_text = dmp.patch_toText(patch_list)
|
||||||
|
|
||||||
// save raw text for future diffs
|
// save raw text for future diffs
|
||||||
rawNoteText = liveEditorElm.innerHTML
|
this.rawNoteText = liveEditorElm.innerHTML
|
||||||
|
|
||||||
let newPatch = {
|
let newPatch = {
|
||||||
id: this.rawTextId,
|
noteId: this.noteid,
|
||||||
diff: patch_text,
|
diff: patch_text,
|
||||||
path: path,
|
path: path,
|
||||||
// testing metrics
|
// testing metrics
|
||||||
@@ -961,7 +957,10 @@
|
|||||||
// console.log('incoming patches ', incomingPatchs)
|
// console.log('incoming patches ', incomingPatchs)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
const editorElement = document.getElementById('squire-id')
|
const editorElement = this.$refs.squirebox
|
||||||
|
|
||||||
|
// Socket-driven: this panel may not be rendered yet, or may be gone
|
||||||
|
if(!editorElement){ return resolve(false) }
|
||||||
|
|
||||||
// iterate over incoming patches because they apply to specific divs
|
// iterate over incoming patches because they apply to specific divs
|
||||||
incomingPatchs.forEach(patch => {
|
incomingPatchs.forEach(patch => {
|
||||||
@@ -988,10 +987,10 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
// save editor HTML after change for future comparisons
|
// save editor HTML after change for future comparisons
|
||||||
rawNoteText = editorElement.innerHTML
|
this.rawNoteText = editorElement.innerHTML
|
||||||
|
|
||||||
// update hash on patch
|
// update hash on patch
|
||||||
this.lastNoteHash = this.hashString( rawNoteText )
|
this.lastNoteHash = this.hashString( this.rawNoteText )
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
return resolve(true)
|
return resolve(true)
|
||||||
@@ -1041,7 +1040,7 @@
|
|||||||
|
|
||||||
//tell websockets to truncate history at this save
|
//tell websockets to truncate history at this save
|
||||||
this.lastNoteHash = currentHash //Update last saved note hash
|
this.lastNoteHash = currentHash //Update last saved note hash
|
||||||
this.$io.emit('truncate_diffs_at_save', {'rawTextId':this.rawTextId, 'hash':currentHash })
|
this.$io.emit('truncate_diffs_at_save', {'noteId':this.noteid, 'hash':currentHash })
|
||||||
|
|
||||||
const postData = {
|
const postData = {
|
||||||
'noteId': this.currentNoteId,
|
'noteId': this.currentNoteId,
|
||||||
@@ -1093,7 +1092,8 @@
|
|||||||
this.setupLoadedNoteData(response)
|
this.setupLoadedNoteData(response)
|
||||||
|
|
||||||
//Manually set squire text
|
//Manually set squire text
|
||||||
const editorElement = document.getElementById('squire-id')
|
const editorElement = this.$refs.squirebox
|
||||||
|
if(!editorElement){ return }
|
||||||
editorElement.innerHTML = response.data.text
|
editorElement.innerHTML = response.data.text
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -1146,6 +1146,15 @@
|
|||||||
|
|
||||||
return hash;
|
return hash;
|
||||||
},
|
},
|
||||||
|
// Menu routes are pushed (not replaced) because the history entry is
|
||||||
|
// load-bearing: every menu closes via $router.go(-1). Forwarding the
|
||||||
|
// current query keeps ?open= alive across the menu and the pop back.
|
||||||
|
openMenuRoute(menu){
|
||||||
|
this.$router.push({
|
||||||
|
path: `/notes/open/${this.noteid}/menu/${menu}`,
|
||||||
|
query: this.$route.query,
|
||||||
|
})
|
||||||
|
},
|
||||||
closeButtonAction(playAnimation = false){
|
closeButtonAction(playAnimation = false){
|
||||||
this.sizeDown = playAnimation
|
this.sizeDown = playAnimation
|
||||||
const animationTimeout = (playAnimation ? 300 : 0)
|
const animationTimeout = (playAnimation ? 300 : 0)
|
||||||
@@ -1181,47 +1190,75 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
destroyWebSockets(){
|
destroyWebSockets(){
|
||||||
this.$io.removeListener('past_diffs')
|
|
||||||
this.$io.removeListener('update_user_count')
|
// By reference: removeListener(event) alone would remove the other
|
||||||
this.$io.removeListener('incoming_diff')
|
// open panels' handlers too. new_note_text_saved was never removed
|
||||||
this.$io.removeListener('update_note_attachments')
|
// at all and leaked a handler on every open/close.
|
||||||
|
this.$io.removeListener('update_user_count', this.onUpdateUserCount)
|
||||||
|
this.$io.removeListener('past_diffs', this.onPastDiffs)
|
||||||
|
this.$io.removeListener('incoming_diff', this.onIncomingDiff)
|
||||||
|
this.$io.removeListener('new_note_text_saved', this.onNewNoteTextSaved)
|
||||||
|
this.$io.removeListener('update_note_attachments', this.onUpdateNoteAttachments)
|
||||||
|
|
||||||
clearTimeout(this.idleNoteTimeout)
|
clearTimeout(this.idleNoteTimeout)
|
||||||
},
|
},
|
||||||
initWebsocketEvents(){
|
// Socket handlers are named methods rather than inline closures so that
|
||||||
|
// destroyWebSockets() can remove THIS panel's handler by reference.
|
||||||
//Tell server to push this note into a room
|
// removeListener(event) with no handler drops every open panel's.
|
||||||
this.$io.emit('join_room', this.rawTextId )
|
onFileUpload({noteId, imageCode}){
|
||||||
|
if(this.noteid == noteId && this.editor){
|
||||||
this.$io.on('update_user_count', userCount => {
|
this.editor.moveCursorToEnd()
|
||||||
|
this.editor.insertHTML(imageCode)
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCloseNoteById(noteId){
|
||||||
|
if(noteId == this.noteid){
|
||||||
|
this.closeButtonAction()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUpdateUserCount(userCount){
|
||||||
this.usersOnNote = userCount
|
this.usersOnNote = userCount
|
||||||
})
|
},
|
||||||
|
|
||||||
//Apply all diffs since last save
|
//Apply all diffs since last save
|
||||||
this.$io.on('past_diffs', diffSinceLastUpdate => {
|
onPastDiffs(diffSinceLastUpdate){
|
||||||
|
|
||||||
|
// Every open panel shares one socket, so filter to this note's room
|
||||||
|
if(!this.isDiffForThisNote(diffSinceLastUpdate)){ return }
|
||||||
|
|
||||||
if(diffSinceLastUpdate != null){
|
if(diffSinceLastUpdate != null){
|
||||||
this.diffsApplied = diffSinceLastUpdate.length
|
this.diffsApplied = diffSinceLastUpdate.length
|
||||||
// console.log('Got Diffs Total -> ', diffSinceLastUpdate)
|
|
||||||
}
|
}
|
||||||
console.log(diffSinceLastUpdate)
|
|
||||||
this.patchText(diffSinceLastUpdate)
|
this.patchText(diffSinceLastUpdate)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.restoreCaretPosition()
|
this.restoreCaretPosition()
|
||||||
.catch(e => console.log(e) )
|
.catch(e => console.log(e) )
|
||||||
})
|
})
|
||||||
})
|
},
|
||||||
|
onIncomingDiff(incomingDiff){
|
||||||
|
|
||||||
this.$io.on('incoming_diff', incomingDiff => {
|
if(!this.isDiffForThisNote([incomingDiff])){ return }
|
||||||
|
|
||||||
this.patchText([incomingDiff])
|
this.patchText([incomingDiff])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.restoreCaretPosition()
|
this.restoreCaretPosition()
|
||||||
.catch(e => console.log(e) )
|
.catch(e => console.log(e) )
|
||||||
})
|
})
|
||||||
})
|
},
|
||||||
|
// A diff payload carries the raw-text id it belongs to. Anything without
|
||||||
|
// one is dropped rather than guessed at -- applying a patch to the wrong
|
||||||
|
// note is how note bodies got clobbered.
|
||||||
|
isDiffForThisNote(diffs){
|
||||||
|
|
||||||
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
|
if(diffs == null){ return false }
|
||||||
|
|
||||||
|
const list = [].concat(diffs).filter(diff => diff != null)
|
||||||
|
if(list.length == 0){ return false }
|
||||||
|
|
||||||
|
return list.every(diff => String(diff.rawTextId) === String(this.rawTextId))
|
||||||
|
},
|
||||||
|
onNewNoteTextSaved({noteId, hash}){
|
||||||
|
|
||||||
if(this.statusText != 'saved'){
|
if(this.statusText != 'saved'){
|
||||||
return
|
return
|
||||||
@@ -1234,13 +1271,25 @@
|
|||||||
if(sameIdCheck && differentHashCheck){
|
if(sameIdCheck && differentHashCheck){
|
||||||
this.loadNoteNextFromServer()
|
this.loadNoteNextFromServer()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
})
|
onUpdateNoteAttachments(){
|
||||||
|
|
||||||
this.$io.on('update_note_attachments', () => {
|
|
||||||
this.getAttachments()
|
this.getAttachments()
|
||||||
|
},
|
||||||
|
initWebsocketEvents(){
|
||||||
|
|
||||||
|
//Tell server to push this note into a room
|
||||||
|
// Ask for a note, not a room. The server authorises against the
|
||||||
|
// token and derives the room itself.
|
||||||
|
this.$io.emit('join_room', {
|
||||||
|
noteId: this.noteid,
|
||||||
|
token: localStorage.getItem('loginToken'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.$io.on('update_user_count', this.onUpdateUserCount)
|
||||||
|
this.$io.on('past_diffs', this.onPastDiffs)
|
||||||
|
this.$io.on('incoming_diff', this.onIncomingDiff)
|
||||||
|
this.$io.on('new_note_text_saved', this.onNewNoteTextSaved)
|
||||||
|
this.$io.on('update_note_attachments', this.onUpdateNoteAttachments)
|
||||||
},
|
},
|
||||||
logNoteInteraction(){
|
logNoteInteraction(){
|
||||||
|
|
||||||
@@ -1253,6 +1302,8 @@
|
|||||||
},
|
},
|
||||||
saveCaretPosition(event){
|
saveCaretPosition(event){
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
//Find index of child element based on past range
|
//Find index of child element based on past range
|
||||||
const element = window.getSelection().getRangeAt(0).startContainer.parentNode
|
const element = window.getSelection().getRangeAt(0).startContainer.parentNode
|
||||||
|
|
||||||
@@ -1271,7 +1322,9 @@
|
|||||||
|
|
||||||
if(!this.lastRange){ return resolve(true) }
|
if(!this.lastRange){ return resolve(true) }
|
||||||
|
|
||||||
const editorElement = document.getElementById('squire-id')
|
const editorElement = this.$refs.squirebox
|
||||||
|
if(!editorElement){ return resolve(true) }
|
||||||
|
|
||||||
const lastElement = editorElement.querySelector(this.lastRange)
|
const lastElement = editorElement.querySelector(this.lastRange)
|
||||||
|
|
||||||
if( !lastElement ){ return resolve(true) }
|
if( !lastElement ){ return resolve(true) }
|
||||||
|
|||||||
@@ -184,6 +184,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
@@ -191,9 +192,9 @@
|
|||||||
name: 'NoteTitleDisplayCard',
|
name: 'NoteTitleDisplayCard',
|
||||||
props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults', 'titleView' ],
|
props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults', 'titleView' ],
|
||||||
components: {
|
components: {
|
||||||
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
|
'delete-button': defineAsyncComponent(() => import('@/components/NoteDeleteButtonComponent.vue')),
|
||||||
'note-tag-edit': require('@/components/NoteTagEdit.vue').default,
|
'note-tag-edit': defineAsyncComponent(() => import('@/components/NoteTagEdit.vue')),
|
||||||
'side-slide-menu': require('@/components/SideSlideMenuComponent.vue').default,
|
'side-slide-menu': defineAsyncComponent(() => import('@/components/SideSlideMenuComponent.vue')),
|
||||||
},
|
},
|
||||||
methods:{
|
methods:{
|
||||||
cardClicked(){
|
cardClicked(){
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -104,7 +103,7 @@
|
|||||||
this.tagSuggestions = []
|
this.tagSuggestions = []
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
beforeDestroy(){
|
beforeUnmount(){
|
||||||
this.$bus.$off('reset_fast_filters')
|
this.$bus.$off('reset_fast_filters')
|
||||||
},
|
},
|
||||||
mounted: function(){
|
mounted: function(){
|
||||||
|
|||||||
@@ -107,11 +107,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
export default {
|
export default {
|
||||||
name: 'SideSlideMenu',
|
name: 'SideSlideMenu',
|
||||||
props: [ 'name', 'styleObject', 'fullShadow', 'skipHistory' ],
|
props: [ 'name', 'styleObject', 'fullShadow', 'skipHistory' ],
|
||||||
components: {
|
components: {
|
||||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
'nm-button':defineAsyncComponent(() => import('@/components/NoteMenuButtonComponent.vue'))
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -129,7 +130,7 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
beforeDestroy(){
|
beforeUnmount(){
|
||||||
},
|
},
|
||||||
mounted(){
|
mounted(){
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style type="text/css" scoped>
|
<style type="text/css" scoped>
|
||||||
.svg-holder >>> svg {
|
.svg-holder :deep(svg) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,11 +86,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components:{
|
components:{
|
||||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
'nm-button':defineAsyncComponent(() => import('@/components/NoteMenuButtonComponent.vue'))
|
||||||
},
|
},
|
||||||
props: [ 'lastUsedColor' ],
|
props: [ 'lastUsedColor' ],
|
||||||
data: function(){
|
data: function(){
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div class="update-prompt" v-if="visible">
|
||||||
|
<span class="update-text">A new version is ready.</span>
|
||||||
|
<div class="ui tiny green button" v-on:click="apply()">Update</div>
|
||||||
|
<div class="ui tiny basic button" v-on:click="dismiss()">Later</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'UpdatePrompt',
|
||||||
|
data(){
|
||||||
|
return { visible: false }
|
||||||
|
},
|
||||||
|
// The service worker is configured with skipWaiting:false, so a new build
|
||||||
|
// sits waiting until the user accepts it here rather than swapping assets
|
||||||
|
// out from under an open note mid-edit.
|
||||||
|
beforeMount(){
|
||||||
|
this.$bus.$on('sw-update-available', this.onUpdateAvailable)
|
||||||
|
},
|
||||||
|
beforeUnmount(){
|
||||||
|
this.$bus.$off('sw-update-available', this.onUpdateAvailable)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onUpdateAvailable(){
|
||||||
|
this.visible = true
|
||||||
|
},
|
||||||
|
apply(){
|
||||||
|
this.visible = false
|
||||||
|
// Imported lazily: this component mounts with the app, but the
|
||||||
|
// worker registers after mount.
|
||||||
|
import('@/registerServiceWorker').then(({ updateSW }) => updateSW(true))
|
||||||
|
},
|
||||||
|
dismiss(){
|
||||||
|
// Stays waiting; the hourly update check will offer it again.
|
||||||
|
this.visible = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.update-prompt {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 1em;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75em;
|
||||||
|
padding: 0.75em 1em;
|
||||||
|
border: 1px solid var(--border_color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--small_element_bg_color);
|
||||||
|
color: var(--text_color);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.update-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,71 +1,51 @@
|
|||||||
// The Vue build version to load with the `import` command
|
import { createApp } from 'vue'
|
||||||
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
|
import App from './App.vue'
|
||||||
|
|
||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
import Vuex from 'vuex'
|
|
||||||
import store from './stores/mainStore';
|
|
||||||
|
|
||||||
import App from './App'
|
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import store from './stores/mainStore'
|
||||||
|
|
||||||
//Include entire fomantic ui library
|
|
||||||
// import 'fomantic-ui-css/semantic.css';
|
|
||||||
|
|
||||||
//Required site and reset CSS
|
|
||||||
import 'fomantic-ui-css/components/reset.min.css'
|
import 'fomantic-ui-css/components/reset.min.css'
|
||||||
import 'fomantic-ui-css/components/site.css' //modified to remove included LATO fonts
|
import 'fomantic-ui-css/components/site.css'
|
||||||
|
|
||||||
//Only include parts that are used
|
|
||||||
import 'fomantic-ui-css/components/button.min.css'
|
import 'fomantic-ui-css/components/button.min.css'
|
||||||
import 'fomantic-ui-css/components/container.min.css'
|
import 'fomantic-ui-css/components/container.min.css'
|
||||||
import 'fomantic-ui-css/components/form.min.css'
|
import 'fomantic-ui-css/components/form.min.css'
|
||||||
import 'fomantic-ui-css/components/grid.min.css'
|
import 'fomantic-ui-css/components/grid.min.css'
|
||||||
import 'fomantic-ui-css/components/header.min.css'
|
import 'fomantic-ui-css/components/header.min.css'
|
||||||
import 'fomantic-ui-css/components/icon.css' //Modified to remove brand icons
|
import 'fomantic-ui-css/components/icon.css'
|
||||||
import 'fomantic-ui-css/components/input.min.css'
|
import 'fomantic-ui-css/components/input.min.css'
|
||||||
import 'fomantic-ui-css/components/segment.min.css'
|
import 'fomantic-ui-css/components/segment.min.css'
|
||||||
import 'fomantic-ui-css/components/label.min.css'
|
import 'fomantic-ui-css/components/label.min.css'
|
||||||
|
|
||||||
|
import './assets/semantic-helper.css'
|
||||||
|
|
||||||
//Overwrite and site styles and themes and good stuff
|
// Last, so Tailwind utilities win specificity ties against Fomantic
|
||||||
require('./assets/semantic-helper.css')
|
import './tailwind.css'
|
||||||
// Fonts
|
|
||||||
require('./assets/roboto-latin.woff2')
|
|
||||||
require('./assets/roboto-latin-bold.woff2')
|
|
||||||
|
|
||||||
|
import io from 'socket.io-client'
|
||||||
|
|
||||||
|
|
||||||
//Import socket io, init using nginx configured socket path
|
|
||||||
import io from 'socket.io-client';
|
|
||||||
const socket = io({ path:'/socket' });
|
|
||||||
|
|
||||||
//integrate connected socket into vue instance
|
|
||||||
Object.defineProperties(Vue.prototype, {
|
|
||||||
$io: {
|
|
||||||
get: () => socket
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// This callback runs before every route change, including on page load.
|
|
||||||
// Sets the title of the page using vue router
|
|
||||||
router.beforeEach((to, from, next) => {
|
|
||||||
document.title = to.meta.title + ' - Solid Scribe';
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
//Attach event bus to main vue object, all components will inherit event bus
|
|
||||||
import EventBus from './EventBus'
|
import EventBus from './EventBus'
|
||||||
import Helpers from './Helpers'
|
import Helpers from './Helpers'
|
||||||
|
import { installLaunchRedirect } from './LaunchPage'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
const socket = io({ path: '/socket' })
|
||||||
Vue.config.productionTip = false
|
|
||||||
|
|
||||||
new Vue({
|
router.beforeEach((to, from, next) => {
|
||||||
router,
|
document.title = to.meta.title + ' - Solid Scribe'
|
||||||
store,
|
next()
|
||||||
render: h => h(App),
|
})
|
||||||
}).$mount('#app')
|
|
||||||
|
// Installed launches land on "/" and are redirected to the user's chosen
|
||||||
|
// launch page -- see LaunchPage.js
|
||||||
|
installLaunchRedirect(router)
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.config.globalProperties.$io = socket
|
||||||
|
app.config.globalProperties.$bus = EventBus
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.use(store)
|
||||||
|
app.use(Helpers)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
|
||||||
|
// After mount, so a failed registration can never block the app rendering
|
||||||
|
import('./registerServiceWorker')
|
||||||
@@ -171,7 +171,7 @@ const SquireButtonFunctions = {
|
|||||||
//
|
//
|
||||||
|
|
||||||
//Fetch the container
|
//Fetch the container
|
||||||
let container = document.getElementById('squire-id')
|
let container = this.$refs.squirebox
|
||||||
this.$router.go(-1)
|
this.$router.go(-1)
|
||||||
|
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
@@ -189,7 +189,7 @@ const SquireButtonFunctions = {
|
|||||||
//
|
//
|
||||||
|
|
||||||
//Fetch the container
|
//Fetch the container
|
||||||
let container = document.getElementById('squire-id')
|
let container = this.$refs.squirebox
|
||||||
|
|
||||||
//Close menu if user is on mobile, then sort list
|
//Close menu if user is on mobile, then sort list
|
||||||
this.$router.go(-1)
|
this.$router.go(-1)
|
||||||
@@ -249,7 +249,7 @@ const SquireButtonFunctions = {
|
|||||||
//
|
//
|
||||||
|
|
||||||
//Fetch the container
|
//Fetch the container
|
||||||
let container = document.getElementById('squire-id')
|
let container = this.$refs.squirebox
|
||||||
|
|
||||||
//Close menu if user is on mobile
|
//Close menu if user is on mobile
|
||||||
this.$router.go(-1)
|
this.$router.go(-1)
|
||||||
@@ -317,7 +317,7 @@ const SquireButtonFunctions = {
|
|||||||
//
|
//
|
||||||
|
|
||||||
//Fetch the container
|
//Fetch the container
|
||||||
let container = document.getElementById('squire-id')
|
let container = this.$refs.squirebox
|
||||||
|
|
||||||
//Close menu if user is on mobile, then sort list
|
//Close menu if user is on mobile, then sort list
|
||||||
this.$router.go(-1)
|
this.$router.go(-1)
|
||||||
@@ -377,7 +377,7 @@ const SquireButtonFunctions = {
|
|||||||
// this.diffNoteText = this.editor._getHTML()
|
// this.diffNoteText = this.editor._getHTML()
|
||||||
|
|
||||||
//Make sure all list items have draggable property
|
//Make sure all list items have draggable property
|
||||||
// let container = document.getElementById('squire-id')
|
// let container = this.$refs.squirebox
|
||||||
// let listItems = container.getElementsByTagName('li')
|
// let listItems = container.getElementsByTagName('li')
|
||||||
// for(let itemIndex in listItems){
|
// for(let itemIndex in listItems){
|
||||||
// // console.log(listItems[itemIndex])
|
// // console.log(listItems[itemIndex])
|
||||||
|
|||||||
@@ -102,13 +102,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
'attachment-display': require('@/components/AttachmentDisplayCard').default,
|
'attachment-display': defineAsyncComponent(() => import('@/components/AttachmentDisplayCard.vue')),
|
||||||
'counter':require('@/components/AnimatedCounterComponent.vue').default,
|
'counter':defineAsyncComponent(() => import('@/components/AnimatedCounterComponent.vue')),
|
||||||
},
|
},
|
||||||
data: function(){
|
data: function(){
|
||||||
return {
|
return {
|
||||||
@@ -124,7 +125,7 @@
|
|||||||
//
|
//
|
||||||
// Perform Login check
|
// Perform Login check
|
||||||
//
|
//
|
||||||
this.$parent.loginGateway()
|
this.$root.loginGateway()
|
||||||
|
|
||||||
},
|
},
|
||||||
mounted: function(){
|
mounted: function(){
|
||||||
@@ -140,7 +141,7 @@
|
|||||||
//Mount notes on load if note ID is set
|
//Mount notes on load if note ID is set
|
||||||
this.searchAttachments()
|
this.searchAttachments()
|
||||||
},
|
},
|
||||||
beforeDestroy(){
|
beforeUnmount(){
|
||||||
|
|
||||||
//Remove scroll event on destroy
|
//Remove scroll event on destroy
|
||||||
window.removeEventListener('scroll', this.onScroll)
|
window.removeEventListener('scroll', this.onScroll)
|
||||||
|
|||||||
@@ -40,13 +40,13 @@
|
|||||||
},
|
},
|
||||||
beforeCreate: function(){
|
beforeCreate: function(){
|
||||||
// Perform Login check
|
// Perform Login check
|
||||||
this.$parent.loginGateway()
|
this.$root.loginGateway()
|
||||||
|
|
||||||
},
|
},
|
||||||
mounted: function(){
|
mounted: function(){
|
||||||
this.getBookmarklet()
|
this.getBookmarklet()
|
||||||
},
|
},
|
||||||
beforeDestroy(){
|
beforeUnmount(){
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -100,13 +100,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CycleTracking',
|
name: 'CycleTracking',
|
||||||
components: {
|
components: {
|
||||||
'logo':require('@/components/LogoComponent.vue').default,
|
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -133,7 +134,7 @@
|
|||||||
beforeCreate() {
|
beforeCreate() {
|
||||||
|
|
||||||
// Perform Login check
|
// Perform Login check
|
||||||
this.$parent.loginGateway()
|
this.$root.loginGateway()
|
||||||
|
|
||||||
},
|
},
|
||||||
mounted(){
|
mounted(){
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<div class="ui grid">
|
<div class="ui grid">
|
||||||
<div class="center aligned sixteen wide column">
|
<div class="center aligned sixteen wide column">
|
||||||
<router-link to="/terms"></i>Solid Scribe Terms of Use</router-link>
|
<router-link to="/terms">Solid Scribe Terms of Use</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Go to notes button -->
|
<!-- Go to notes button -->
|
||||||
<div class="row" v-if="$parent.loggedIn">
|
<div class="row" v-if="$root.loggedIn">
|
||||||
<div class="sixteen wide middle algined center aligned column">
|
<div class="sixteen wide middle algined center aligned column">
|
||||||
<h3>You are already logged in</h3>
|
<h3>You are already logged in</h3>
|
||||||
<router-link class="ui huge green labeled icon button" to="/notes">
|
<router-link class="ui huge green labeled icon button" to="/notes">
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Small login form -->
|
<!-- Small login form -->
|
||||||
<div class="row" v-if="!$parent.loggedIn">
|
<div class="row" v-if="!$root.loggedIn">
|
||||||
<div class="sixteen wide middle algined column">
|
<div class="sixteen wide middle algined column">
|
||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
<h2>
|
<h2>
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
<h2 style="color: var(--main-accent);">
|
<h2 style="color: var(--main-accent);">
|
||||||
Pick your theme
|
Pick your theme
|
||||||
</h2>
|
</h2>
|
||||||
<h3 v-if="$parent.loggedIn">Go to settings to change theme</h3>
|
<h3 v-if="$root.loggedIn">Go to settings to change theme</h3>
|
||||||
<div
|
<div
|
||||||
v-for="color in themeColors"
|
v-for="color in themeColors"
|
||||||
v-bind:key="color"
|
v-bind:key="color"
|
||||||
@@ -532,12 +532,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
export default {
|
export default {
|
||||||
name: 'WelcomePage',
|
name: 'WelcomePage',
|
||||||
components: {
|
components: {
|
||||||
'login-form':require('@/components/LoginFormComponent.vue').default,
|
'login-form':defineAsyncComponent(() => import('@/components/LoginFormComponent.vue')),
|
||||||
'logo':require('@/components/LogoComponent.vue').default,
|
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||||
'svg-displayer':require('@/components/SvgDisplayer.vue').default,
|
'svg-displayer':defineAsyncComponent(() => import('@/components/SvgDisplayer.vue')),
|
||||||
},
|
},
|
||||||
data(){
|
data(){
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -33,13 +33,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
components: {
|
components: {
|
||||||
'login-form':require('@/components/LoginFormComponent.vue').default,
|
'login-form':defineAsyncComponent(() => import('@/components/LoginFormComponent.vue')),
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -355,9 +355,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui form" :class="{'loading-day':loadingDay}">
|
<div class="ui form" :class="{'loading-day':loadingDay}">
|
||||||
<draggable v-model="fields" class="ui compact grid" ghost-class="ghost" @end="onDragEnd" handle=".draggable">
|
<draggable v-model="fields" class="ui compact grid" ghost-class="ghost" @end="onDragEnd" handle=".draggable" :item-key="field => field">
|
||||||
<div v-for="field in fields" :key="field"
|
<template #item="{ element: field }">
|
||||||
:class="userFields[field]?.width ? userFields[field]?.width+' wide stretched column':'eight wide stretched column'">
|
<div :class="userFields[field]?.width ? userFields[field]?.width+' wide stretched column':'eight wide stretched column'">
|
||||||
|
|
||||||
<div class="ui very compact grid input-grid">
|
<div class="ui very compact grid input-grid">
|
||||||
<!-- field label display -->
|
<!-- field label display -->
|
||||||
@@ -443,6 +443,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -599,7 +600,6 @@
|
|||||||
<div :class="{'green':(tempChartDays == 7)}" v-on:click="tempChartDays = 7; " class="ui button">7</div>
|
<div :class="{'green':(tempChartDays == 7)}" v-on:click="tempChartDays = 7; " class="ui button">7</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div id="graphdiv" style="width: 100%; min-height: 320px;"></div> -->
|
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
|
|
||||||
<MetricGraphs
|
<MetricGraphs
|
||||||
@@ -686,8 +686,9 @@
|
|||||||
<div class="ui small dividing header">
|
<div class="ui small dividing header">
|
||||||
Active Fields
|
Active Fields
|
||||||
</div>
|
</div>
|
||||||
<draggable v-model="fields" class="ui very compact internally celled grid" ghost-class="ghost" @end="onDragEnd" handle=".draggable">
|
<draggable v-model="fields" class="ui very compact internally celled grid" ghost-class="ghost" @end="onDragEnd" handle=".draggable" :item-key="fieldId => fieldId">
|
||||||
<div v-for="fieldId in fields" :key="fieldId" class="row">
|
<template #item="{ element: fieldId }">
|
||||||
|
<div class="row">
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
<i class="draggable grip lines icon"></i>
|
<i class="draggable grip lines icon"></i>
|
||||||
{{ userFields[fieldId]?.label }}
|
{{ userFields[fieldId]?.label }}
|
||||||
@@ -708,13 +709,14 @@
|
|||||||
<!-- {{ openDay[fieldId] }} -->
|
<!-- {{ openDay[fieldId] }} -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
|
|
||||||
<div class="ui small dividing header">
|
<div class="ui small dividing header">
|
||||||
Other Fields
|
Other Fields
|
||||||
</div>
|
</div>
|
||||||
<draggable class="ui very compact internally celled grid">
|
<div class="ui very compact internally celled grid">
|
||||||
<div class="ui row" v-for="(field,fieldId) in userFields" v-if="!fields.includes(fieldId)">
|
<div class="ui row" v-for="(field,fieldId) in unusedUserFields" :key="fieldId">
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
<i class="draggable grip lines icon"></i>
|
<i class="draggable grip lines icon"></i>
|
||||||
{{ userFields[fieldId]?.label }}
|
{{ userFields[fieldId]?.label }}
|
||||||
@@ -726,8 +728,7 @@
|
|||||||
<div class="ui tiny compact button" v-on:click="startUsingCustomField(fieldId)">Show</div>
|
<div class="ui tiny compact button" v-on:click="startUsingCustomField(fieldId)">Show</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui row" v-for="(field,fieldId) in defaultFields"
|
<div class="ui row" v-for="(field,fieldId) in unusedDefaultFields" :key="'default-'+fieldId">
|
||||||
v-if="!fields.includes(fieldId) && !Object.keys(userFields).includes(fieldId)">
|
|
||||||
|
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
<i class="draggable grip lines icon"></i>
|
<i class="draggable grip lines icon"></i>
|
||||||
@@ -740,7 +741,7 @@
|
|||||||
<div class="ui tiny compact button" v-on:click="startUsingCustomField(fieldId)">Show</div>
|
<div class="ui tiny compact button" v-on:click="startUsingCustomField(fieldId)">Show</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</draggable>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -817,7 +818,8 @@
|
|||||||
<div class="ui small dividing header">
|
<div class="ui small dividing header">
|
||||||
Add/Edit Field
|
Add/Edit Field
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(entry, key) in fieldDefinition" class="row" v-if="!['id'].includes(key)">
|
<template v-for="(entry, key) in fieldDefinition" :key="key">
|
||||||
|
<div class="row" v-if="!['id'].includes(key)">
|
||||||
|
|
||||||
<div v-if="fieldDefinitionOptionsDef[key]" class="sixteen wide column">
|
<div v-if="fieldDefinitionOptionsDef[key]" class="sixteen wide column">
|
||||||
{{ mapFormTerm(key) }}
|
{{ mapFormTerm(key) }}
|
||||||
@@ -861,6 +863,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
<div class="ui fluid green button" v-on:click="saveCustomField(editFieldId)" v-if="validateCustomFieldsForm()">
|
<div class="ui fluid green button" v-on:click="saveCustomField(editFieldId)" v-if="validateCustomFieldsForm()">
|
||||||
@@ -880,8 +883,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
|
|
||||||
<!-- may or may not need digraph CSS, maybe disect -->
|
|
||||||
<!-- <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.css" /> -->
|
|
||||||
|
|
||||||
<div class="ui basic segment"></div>
|
<div class="ui basic segment"></div>
|
||||||
|
|
||||||
@@ -890,6 +891,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Important Notes:
|
Important Notes:
|
||||||
@@ -906,12 +908,22 @@
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MetricTracking',
|
name: 'MetricTracking',
|
||||||
|
// Descendant graph components need these; injecting avoids the
|
||||||
|
// $parent / $parent.$parent chains, which break whenever an
|
||||||
|
// intermediate instance is added (async wrappers, router-view).
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
getFieldColor: field => this.getFieldColor(field),
|
||||||
|
getFieldIcon: field => this.getFieldIcon(field),
|
||||||
|
getFieldLabel: field => this.getFieldLabel(field),
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
'logo':require('@/components/LogoComponent.vue').default,
|
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||||
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
|
'delete-button': defineAsyncComponent(() => import('@/components/NoteDeleteButtonComponent.vue')),
|
||||||
'modal': require('@/components/ModalComponent.vue').default,
|
'modal': defineAsyncComponent(() => import('@/components/ModalComponent.vue')),
|
||||||
draggable,
|
draggable,
|
||||||
'MetricGraphs':require('@/components/Metrictracking/MetricGraphsComponent.vue').default,
|
'MetricGraphs':defineAsyncComponent(() => import('@/components/Metrictracking/MetricGraphsComponent.vue')),
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -991,7 +1003,7 @@
|
|||||||
beforeCreate() {
|
beforeCreate() {
|
||||||
|
|
||||||
// Perform Login check
|
// Perform Login check
|
||||||
this.$parent.loginGateway()
|
this.$root.loginGateway()
|
||||||
|
|
||||||
},
|
},
|
||||||
mounted(){
|
mounted(){
|
||||||
@@ -1020,6 +1032,21 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
// vuedraggable's computeNodes walks the default slot's vnodes directly
|
||||||
|
// and cannot handle the fragments that <template v-for> produces, so
|
||||||
|
// the filtering happens here instead of with a v-if in the slot.
|
||||||
|
unusedUserFields(){
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(this.userFields).filter(([fieldId]) => !this.fields.includes(fieldId))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
unusedDefaultFields(){
|
||||||
|
const userFieldIds = Object.keys(this.userFields)
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(this.defaultFields).filter(([fieldId]) =>
|
||||||
|
!this.fields.includes(fieldId) && !userFieldIds.includes(fieldId))
|
||||||
|
)
|
||||||
|
},
|
||||||
getToday(){
|
getToday(){
|
||||||
return this.generateDateCode(new Date())
|
return this.generateDateCode(new Date())
|
||||||
},
|
},
|
||||||
@@ -1099,6 +1126,27 @@
|
|||||||
return color
|
return color
|
||||||
// userFields[field]?.icon
|
// userFields[field]?.icon
|
||||||
},
|
},
|
||||||
|
getFieldLabel(field){
|
||||||
|
|
||||||
|
let label = null
|
||||||
|
|
||||||
|
// user defined
|
||||||
|
if(this.userFields[field]?.label){
|
||||||
|
label = this.userFields[field].label
|
||||||
|
}
|
||||||
|
|
||||||
|
// default field value
|
||||||
|
if(!label && this.defaultFields[field]?.label){
|
||||||
|
label = this.defaultFields[field]?.label
|
||||||
|
}
|
||||||
|
|
||||||
|
// fall back to the raw id
|
||||||
|
if(!label){
|
||||||
|
label = field
|
||||||
|
}
|
||||||
|
|
||||||
|
return label
|
||||||
|
},
|
||||||
getFieldIcon(field){
|
getFieldIcon(field){
|
||||||
|
|
||||||
let icon = null
|
let icon = null
|
||||||
@@ -1218,7 +1266,7 @@
|
|||||||
if(event?.target?.value){
|
if(event?.target?.value){
|
||||||
option = event.target.value
|
option = event.target.value
|
||||||
}
|
}
|
||||||
this.$set(this.editFieldObject, key, option)
|
this.editFieldObject[key] = option
|
||||||
},
|
},
|
||||||
mapFormTerm(term){
|
mapFormTerm(term){
|
||||||
|
|
||||||
@@ -1371,7 +1419,11 @@
|
|||||||
if(this.openDay[fieldId] === value){ return }
|
if(this.openDay[fieldId] === value){ return }
|
||||||
|
|
||||||
// update field to be reactive
|
// update field to be reactive
|
||||||
this.$set(this.openDay, fieldId, value)
|
this.openDay[fieldId] = value
|
||||||
|
|
||||||
|
// Push straight into cycleData so the graphs redraw immediately;
|
||||||
|
// the server write below stays debounced.
|
||||||
|
this.applyOpenDayToCycleData()
|
||||||
|
|
||||||
// remove debounce and set to modified
|
// remove debounce and set to modified
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -1401,12 +1453,14 @@
|
|||||||
// open day has all fields defined, just set values
|
// open day has all fields defined, just set values
|
||||||
let currentDay = this.cycleData[dateCode] || {}
|
let currentDay = this.cycleData[dateCode] || {}
|
||||||
Object.keys(this.openDay).forEach(fieldId => {
|
Object.keys(this.openDay).forEach(fieldId => {
|
||||||
this.$set(this.openDay, fieldId, currentDay[fieldId])
|
this.openDay[fieldId] = currentDay[fieldId]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
saveDayData(){
|
// Local-only: fold the open day's values into cycleData. Cheap and
|
||||||
|
// synchronous, so callers can run it on every keystroke.
|
||||||
|
applyOpenDayToCycleData(){
|
||||||
|
|
||||||
// remove empty keys
|
// remove empty keys
|
||||||
let cleanDayData = {}
|
let cleanDayData = {}
|
||||||
@@ -1416,12 +1470,15 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Only save entry if there is data
|
// Only keep entry if there is data
|
||||||
delete this.cycleData[this.calendar.dateCode]
|
delete this.cycleData[this.calendar.dateCode]
|
||||||
if(Object.keys(cleanDayData).length > 0){
|
if(Object.keys(cleanDayData).length > 0){
|
||||||
this.cycleData[this.calendar.dateCode] = cleanDayData
|
this.cycleData[this.calendar.dateCode] = cleanDayData
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
saveDayData(){
|
||||||
|
|
||||||
|
this.applyOpenDayToCycleData()
|
||||||
this.saveCycleData()
|
this.saveCycleData()
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -1547,7 +1604,7 @@
|
|||||||
|
|
||||||
// set up reactive open day object
|
// set up reactive open day object
|
||||||
Object.keys(this.userFields).forEach(fieldId => {
|
Object.keys(this.userFields).forEach(fieldId => {
|
||||||
this.$set(this.openDay, fieldId, '')
|
this.openDay[fieldId] = ''
|
||||||
})
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -117,7 +117,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- render each section based on notes in set -->
|
<!-- render each section based on notes in set -->
|
||||||
<div v-for="section,index in noteSections" v-if="section.length > 0" class="note-card-section">
|
<template v-for="section,index in noteSections" :key="index">
|
||||||
|
<div v-if="section.length > 0" class="note-card-section">
|
||||||
<h5 class="ui tiny dividing header"><i :class="`green ${sectionData[index][0]} icon`"></i>{{ sectionData[index][1] }}</h5>
|
<h5 class="ui tiny dividing header"><i :class="`green ${sectionData[index][0]} icon`"></i>{{ sectionData[index][1] }}</h5>
|
||||||
|
|
||||||
<div class="note-card-display-area">
|
<div class="note-card-display-area">
|
||||||
@@ -133,6 +134,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<div class="loading-section" v-if="showLoading">
|
<div class="loading-section" v-if="showLoading">
|
||||||
@@ -163,20 +165,21 @@
|
|||||||
|
|
||||||
<!-- flexbox note container evenly spaces open notes -->
|
<!-- flexbox note container evenly spaces open notes -->
|
||||||
<div class="note-panel-container" :class="{ 'note-panel-fullwidth':collapseFloatingList}" v-if="openNotes.length">
|
<div class="note-panel-container" :class="{ 'note-panel-fullwidth':collapseFloatingList}" v-if="openNotes.length">
|
||||||
|
<template v-for="noteId in openNotes" :key="noteId">
|
||||||
<note-input-panel
|
<note-input-panel
|
||||||
v-for="noteId in openNotes"
|
|
||||||
v-if="noteId != null"
|
v-if="noteId != null"
|
||||||
:key="noteId"
|
|
||||||
:noteid="noteId"
|
:noteid="noteId"
|
||||||
:url-data="$route.params"
|
:url-data="$route.params"
|
||||||
:open-notes="openNotes.length"
|
:open-notes="openNotes.length"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
@@ -184,15 +187,15 @@
|
|||||||
name: 'NotesPage',
|
name: 'NotesPage',
|
||||||
components: {
|
components: {
|
||||||
|
|
||||||
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
|
'note-input-panel': defineAsyncComponent(() => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue')),
|
||||||
|
|
||||||
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
|
'note-title-display-card': defineAsyncComponent(() => import('@/components/NoteTitleDisplayCard.vue')),
|
||||||
// 'fast-filters': require('@/components/FastFilters.vue').default,
|
// 'fast-filters': defineAsyncComponent(() => import('@/components/FastFilters.vue')),
|
||||||
'search-input': require('@/components/SearchInput.vue').default,
|
'search-input': defineAsyncComponent(() => import('@/components/SearchInput.vue')),
|
||||||
'attachment-display': require('@/components/AttachmentDisplayCard').default,
|
'attachment-display': defineAsyncComponent(() => import('@/components/AttachmentDisplayCard.vue')),
|
||||||
'tag-display':require('@/components/TagDisplayComponent.vue').default,
|
'tag-display':defineAsyncComponent(() => import('@/components/TagDisplayComponent.vue')),
|
||||||
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
|
'loading-icon':defineAsyncComponent(() => import('@/components/LoadingIconComponent.vue')),
|
||||||
'paste-button':require('@/components/PasteButton.vue').default,
|
'paste-button':defineAsyncComponent(() => import('@/components/PasteButton.vue')),
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -203,6 +206,9 @@
|
|||||||
searchTags: [],
|
searchTags: [],
|
||||||
notes: [],
|
notes: [],
|
||||||
openNotes: [],
|
openNotes: [],
|
||||||
|
// fullPath of our own last URL write, so the $route watcher can
|
||||||
|
// recognise it and not treat it as an inbound navigation
|
||||||
|
lastSyncedFullPath: null,
|
||||||
collapseFloatingList: false,
|
collapseFloatingList: false,
|
||||||
highlights: [],
|
highlights: [],
|
||||||
searchDebounce: null,
|
searchDebounce: null,
|
||||||
@@ -258,7 +264,7 @@
|
|||||||
},
|
},
|
||||||
beforeMount(){
|
beforeMount(){
|
||||||
|
|
||||||
this.$parent.loginGateway()
|
this.$root.loginGateway()
|
||||||
|
|
||||||
//If user is on title view,
|
//If user is on title view,
|
||||||
this.titleView = this.$store.getters.getIsUserOnMobile
|
this.titleView = this.$store.getters.getIsUserOnMobile
|
||||||
@@ -363,7 +369,7 @@
|
|||||||
// document.addEventListener('visibilitychange', this.visibiltyChangeAction);
|
// document.addEventListener('visibilitychange', this.visibiltyChangeAction);
|
||||||
|
|
||||||
},
|
},
|
||||||
beforeDestroy(){
|
beforeUnmount(){
|
||||||
window.removeEventListener('scroll', this.onScroll)
|
window.removeEventListener('scroll', this.onScroll)
|
||||||
// document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
|
// document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
|
||||||
|
|
||||||
@@ -380,11 +386,26 @@
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
||||||
//Open note on PAGE LOAD if ID is set
|
// Restore every note named in ?open=
|
||||||
if(this.$route.params.id > 1){
|
const ids = this.parseOpenQuery(this.$route.query.open)
|
||||||
this.openNote(this.$route.params.id)
|
|
||||||
|
// Fold the legacy /notes/open/:id deep link in, first position
|
||||||
|
const paramId = parseInt(this.$route.params.id, 10)
|
||||||
|
if(Number.isInteger(paramId) && paramId > 1 && !ids.includes(paramId)){
|
||||||
|
ids.unshift(paramId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push directly rather than via openNote(), which would write the URL
|
||||||
|
// once per note; one normalising write follows instead.
|
||||||
|
ids.forEach(id => {
|
||||||
|
if(!this.openNotes.includes(id)){
|
||||||
|
this.openNotes.push(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Unconditional, so a junk ?open=abc gets scrubbed too
|
||||||
|
this.syncOpenNotesToUrl()
|
||||||
|
|
||||||
//Loads initial batch and tags
|
//Loads initial batch and tags
|
||||||
this.reset()
|
this.reset()
|
||||||
|
|
||||||
@@ -395,17 +416,31 @@
|
|||||||
},
|
},
|
||||||
'$route' (to, from) {
|
'$route' (to, from) {
|
||||||
|
|
||||||
|
// Our own URL write, not an inbound navigation
|
||||||
|
if(to.fullPath === this.lastSyncedFullPath){ return }
|
||||||
|
|
||||||
// Reload the notes if returning to this page
|
// The discriminator is the absence of ?open=, NOT the path. A menu's
|
||||||
if(to.fullPath == '/notes' && !from.fullPath.includes('/notes/open/')){
|
// $router.go(-1) lands on /notes?open=5 with `from` containing
|
||||||
|
// '/notes/open/', so a path comparison here would close every note
|
||||||
|
// every time a side menu is dismissed.
|
||||||
|
const landingOnBareNotes = (to.path === '/notes' && !to.query.open)
|
||||||
|
|
||||||
|
// Reload the notes if arriving at an empty /notes from elsewhere
|
||||||
|
if(landingOnBareNotes && !from.path.startsWith('/notes/open/') && from.path !== '/notes'){
|
||||||
this.reset()
|
this.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close all notes if returning to /notes page
|
// Explicit navigation to a note-less /notes closes everything
|
||||||
if(to.fullPath == '/notes' && from.fullPath.includes('/notes/open/')){
|
if(landingOnBareNotes && this.openNotes.length > 0){
|
||||||
this.closeAllNotes()
|
this.closeAllNotes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stale history entry (e.g. go(-1) after another note was closed):
|
||||||
|
// rewrite the URL from the array. Never read it.
|
||||||
|
if(to.path === '/notes' && to.query.open && to.query.open !== this.openNotes.join(',')){
|
||||||
|
this.syncOpenNotesToUrl()
|
||||||
|
}
|
||||||
|
|
||||||
//Lookup tags set in URL
|
//Lookup tags set in URL
|
||||||
if(to.params.tag && this.$store.getters.totals && this.$store.getters.totals['tags'][to.params.tag]){
|
if(to.params.tag && this.$store.getters.totals && this.$store.getters.totals['tags'][to.params.tag]){
|
||||||
|
|
||||||
@@ -432,81 +467,121 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openNote(id, event = null){
|
// Where the open-note URL should point. A menu or a tag search owns the
|
||||||
|
// path, so only a plain notes view gets normalised back to /notes.
|
||||||
|
openNotesTargetPath(){
|
||||||
|
|
||||||
//
|
// A side menu is open; its close button is a $router.go(-1), so the
|
||||||
|
// path has to stay put or there is nowhere sane to land.
|
||||||
|
if(this.$route.params.openMenu){ return this.$route.path }
|
||||||
|
|
||||||
const intId = parseInt(id)
|
// Preserve a tag search
|
||||||
if(this.openNotes.includes(intId)){
|
if(this.$route.params.tag){ return this.$route.path }
|
||||||
|
|
||||||
console.log('Open already open note?')
|
return '/notes'
|
||||||
|
},
|
||||||
|
// Mirror openNotes into ?open=. This is the ONLY place the open-note
|
||||||
|
// URL is written.
|
||||||
|
syncOpenNotesToUrl(){
|
||||||
|
|
||||||
// const openIndex = this.openNotes.indexOf(intId)
|
const query = Object.assign({}, this.$route.query)
|
||||||
// if(openIndex != -1){
|
|
||||||
// console.log('Open note and remove it ', intId + ' on index ' + openIndex)
|
if(this.openNotes.length > 0){
|
||||||
// this.openNotes.splice(openIndex, 1)
|
query.open = this.openNotes.join(',')
|
||||||
// }
|
}
|
||||||
// this.$bus.$emit('close_note_by_id', intId)
|
else {
|
||||||
|
delete query.open
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = { path: this.openNotesTargetPath(), query }
|
||||||
|
const targetFullPath = this.$router.resolve(target).fullPath
|
||||||
|
|
||||||
|
// Already correct -- don't navigate at all
|
||||||
|
if(targetFullPath === this.$route.fullPath){
|
||||||
|
this.lastSyncedFullPath = targetFullPath
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set before navigating so the watcher sees it
|
||||||
|
this.lastSyncedFullPath = targetFullPath
|
||||||
|
|
||||||
|
this.$router.replace(target).catch(() => {})
|
||||||
|
},
|
||||||
|
// Tolerant parse of ?open=. Handles 'abc', '1,,2,2', '-4' and the
|
||||||
|
// repeated form ?open=1&open=2 (which arrives as an array).
|
||||||
|
parseOpenQuery(raw){
|
||||||
|
|
||||||
|
if(raw === undefined || raw === null){ return [] }
|
||||||
|
|
||||||
|
// Cap: a hand-crafted URL shouldn't be able to fire 50 note fetches
|
||||||
|
const MAX_RESTORE = 8
|
||||||
|
|
||||||
|
return [].concat(raw)
|
||||||
|
.join(',')
|
||||||
|
.split(',')
|
||||||
|
.map(value => parseInt(value, 10))
|
||||||
|
.filter(value => Number.isInteger(value) && value > 0)
|
||||||
|
.filter((value, index, all) => all.indexOf(value) === index)
|
||||||
|
.slice(0, MAX_RESTORE)
|
||||||
|
},
|
||||||
|
openNote(id, event = null){
|
||||||
|
|
||||||
|
const intId = parseInt(id, 10)
|
||||||
|
|
||||||
|
// Also absorbs the undefined the '$route.params.id' watcher now gets
|
||||||
|
// when we normalise /notes/open/5 into /notes?open=5
|
||||||
|
if(!Number.isInteger(intId) || intId < 1){ return }
|
||||||
|
|
||||||
//Don't open note if a link is clicked in display card
|
//Don't open note if a link is clicked in display card
|
||||||
if(event && event.target && event.target.nodeName){
|
if(event && event.target && event.target.nodeName){
|
||||||
const nodeClick = event.target.nodeName
|
const nodeClick = event.target.nodeName
|
||||||
if(nodeClick == 'A'){ return }
|
if(nodeClick == 'A'){ return }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push note to stack if not open
|
if(this.openNotes.includes(intId)){
|
||||||
if(Number.isInteger(intId) && !this.openNotes.includes(intId)){
|
// Already open, but an external push('/notes/open/5') still needs
|
||||||
this.openNotes.push(intId)
|
// the URL normalised back to the full set
|
||||||
}
|
this.syncOpenNotesToUrl()
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
// change route if open ID is not the same as current ID
|
|
||||||
if(this.$route.params.id != id){
|
|
||||||
console.log('Open note, change route -> route id ' + this.$route.params.id + ' note id ->' + id + ', ' +(this.$route.params.id == id))
|
|
||||||
this.$router.push('/notes/open/'+id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openNotes.push(intId)
|
||||||
|
this.syncOpenNotesToUrl()
|
||||||
},
|
},
|
||||||
closeNote(noteId, modified){
|
closeNote(noteId, modified, syncUrl = true){
|
||||||
|
|
||||||
console.log('close note', this.$route.fullPath)
|
// parseInt: indexOf is strict, and ids arriving from the URL are
|
||||||
|
// strings. Without this the splice silently misses.
|
||||||
|
const intId = parseInt(noteId, 10)
|
||||||
|
const openIndex = this.openNotes.indexOf(intId)
|
||||||
|
const didClose = (openIndex !== -1)
|
||||||
|
|
||||||
const openIndex = this.openNotes.indexOf(noteId)
|
if(didClose){
|
||||||
if(openIndex != -1){
|
|
||||||
console.log('Removing note id ', noteId + ' on index ' + openIndex)
|
|
||||||
this.openNotes.splice(openIndex, 1)
|
this.openNotes.splice(openIndex, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// //A note has been closed
|
// Only write the URL when the array actually changed. NoteInputPanel
|
||||||
// if(this.$route.fullPath != '/notes'){
|
// emits close_active_note twice per note (closeButtonAction -> close(),
|
||||||
// this.$router.push('/notes')
|
// then beforeUnmount -> close()); the second arrives after the splice.
|
||||||
// }
|
if(didClose && syncUrl){
|
||||||
if(this.openNotes.length == 0 && this.$route.fullPath != '/notes'){
|
this.syncOpenNotesToUrl()
|
||||||
this.$router.push('/notes')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(modified){
|
if(modified){
|
||||||
console.log('Just closed Note -> ' + noteId + ', modified -> ', modified)
|
|
||||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
//Focus and animate if modified
|
//Focus and animate if modified
|
||||||
this.updateSingleNote(noteId, modified)
|
this.updateSingleNote(noteId, modified)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('closeNote(): Open notes length ', this.openNotes.length)
|
|
||||||
},
|
},
|
||||||
closeAllNotes(){
|
closeAllNotes(){
|
||||||
console.log('Close all notes ------------')
|
|
||||||
|
// Backwards, so the in-loop splice can't skip an entry. URL writes are
|
||||||
|
// deferred so this is one navigation rather than N.
|
||||||
for (let i = this.openNotes.length - 1; i >= 0; i--) {
|
for (let i = this.openNotes.length - 1; i >= 0; i--) {
|
||||||
console.log('Close all notes -> ' + this.openNotes[i])
|
this.closeNote(this.openNotes[i], false, false)
|
||||||
this.closeNote(this.openNotes[i])
|
|
||||||
}
|
}
|
||||||
console.log('----------------')
|
|
||||||
|
this.syncOpenNotesToUrl()
|
||||||
},
|
},
|
||||||
toggleTagFilter(tagId){
|
toggleTagFilter(tagId){
|
||||||
|
|
||||||
@@ -741,7 +816,10 @@
|
|||||||
|
|
||||||
return resolve(true)
|
return resolve(true)
|
||||||
})
|
})
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
|
.catch(error => {
|
||||||
|
console.error('[NotesPage] search failed:', error)
|
||||||
|
this.$bus.$emit('notification', 'Failed to Search Notes')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
rebuildNoteCategorise(){
|
rebuildNoteCategorise(){
|
||||||
|
|||||||
@@ -106,7 +106,8 @@
|
|||||||
<div :class="{'one-column':( showOneColumn() )}">
|
<div :class="{'one-column':( showOneColumn() )}">
|
||||||
|
|
||||||
<!-- render each section based on notes in set -->
|
<!-- render each section based on notes in set -->
|
||||||
<div v-for="section,index in noteSections" v-if="section.length > 0" class="note-card-section">
|
<template v-for="section,index in noteSections" :key="index">
|
||||||
|
<div v-if="section.length > 0" class="note-card-section">
|
||||||
<h5 class="ui tiny dividing header"><i :class="`green ${sectionData[index][0]} icon`"></i>{{ sectionData[index][1] }}</h5>
|
<h5 class="ui tiny dividing header"><i :class="`green ${sectionData[index][0]} icon`"></i>{{ sectionData[index][1] }}</h5>
|
||||||
|
|
||||||
<div class="note-card-display-area">
|
<div class="note-card-display-area">
|
||||||
@@ -122,6 +123,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<loading-icon v-if="loadingInProgress" message="Decrypting Notes" />
|
<loading-icon v-if="loadingInProgress" message="Decrypting Notes" />
|
||||||
@@ -143,6 +145,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
@@ -150,15 +153,15 @@
|
|||||||
name: 'SearchBar',
|
name: 'SearchBar',
|
||||||
components: {
|
components: {
|
||||||
|
|
||||||
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
|
'note-input-panel': defineAsyncComponent(() => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue')),
|
||||||
|
|
||||||
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
|
'note-title-display-card': defineAsyncComponent(() => import('@/components/NoteTitleDisplayCard.vue')),
|
||||||
// 'fast-filters': require('@/components/FastFilters.vue').default,
|
// 'fast-filters': defineAsyncComponent(() => import('@/components/FastFilters.vue')),
|
||||||
'search-input': require('@/components/SearchInput.vue').default,
|
'search-input': defineAsyncComponent(() => import('@/components/SearchInput.vue')),
|
||||||
'attachment-display': require('@/components/AttachmentDisplayCard').default,
|
'attachment-display': defineAsyncComponent(() => import('@/components/AttachmentDisplayCard.vue')),
|
||||||
'counter':require('@/components/AnimatedCounterComponent.vue').default,
|
'counter':defineAsyncComponent(() => import('@/components/AnimatedCounterComponent.vue')),
|
||||||
'tag-display':require('@/components/TagDisplayComponent.vue').default,
|
'tag-display':defineAsyncComponent(() => import('@/components/TagDisplayComponent.vue')),
|
||||||
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
|
'loading-icon':defineAsyncComponent(() => import('@/components/LoadingIconComponent.vue')),
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -222,7 +225,7 @@
|
|||||||
},
|
},
|
||||||
beforeMount(){
|
beforeMount(){
|
||||||
|
|
||||||
this.$parent.loginGateway()
|
this.$root.loginGateway()
|
||||||
|
|
||||||
this.$io.on('new_note_created', noteId => {
|
this.$io.on('new_note_created', noteId => {
|
||||||
|
|
||||||
@@ -335,7 +338,7 @@
|
|||||||
// document.addEventListener('visibilitychange', this.visibiltyChangeAction);
|
// document.addEventListener('visibilitychange', this.visibiltyChangeAction);
|
||||||
|
|
||||||
},
|
},
|
||||||
beforeDestroy(){
|
beforeUnmount(){
|
||||||
window.removeEventListener('scroll', this.onScroll)
|
window.removeEventListener('scroll', this.onScroll)
|
||||||
// document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
|
// document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
//
|
//
|
||||||
// Perform Login check
|
// Perform Login check
|
||||||
//
|
//
|
||||||
this.$parent.loginGateway()
|
this.$root.loginGateway()
|
||||||
},
|
},
|
||||||
beforeMount(){
|
beforeMount(){
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- App / PWA -->
|
||||||
|
<h4 class="ui header">
|
||||||
|
App
|
||||||
|
</h4>
|
||||||
|
<div class="ui segment">
|
||||||
|
<div class="ui doubling grid">
|
||||||
|
<div class="sixteen wide column">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Which page the installed app opens to. Set automatically to
|
||||||
|
whichever page you installed from, and changeable here.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-for="option in launchOptions"
|
||||||
|
:key="option.path"
|
||||||
|
class="ui compact basic button"
|
||||||
|
:class="{'green':(launchPage == option.path)}"
|
||||||
|
v-on:click="chooseLaunchPage(option.path)">
|
||||||
|
{{ option.label }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If the app gets stuck on an old version, this clears the stored
|
||||||
|
copies of its files and reloads. Your notes are not affected.
|
||||||
|
</p>
|
||||||
|
<div class="ui compact basic button" v-on:click="clearAppCache()">
|
||||||
|
<i class="redo icon"></i>
|
||||||
|
Reset App Cache
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Enable Two Factor -->
|
<!-- Enable Two Factor -->
|
||||||
<h4>Two Factor Authentication</h4>
|
<h4>Two Factor Authentication</h4>
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
@@ -152,7 +188,7 @@
|
|||||||
|
|
||||||
<div class="ui grid">
|
<div class="ui grid">
|
||||||
<div class="center aligned sixteen wide column">
|
<div class="center aligned sixteen wide column">
|
||||||
<router-link to="/terms"></i>Solid Scribe Terms of Use</router-link>
|
<router-link to="/terms">Solid Scribe Terms of Use</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -161,13 +197,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { LAUNCH_OPTIONS, getLaunchPage, setLaunchPage } from '@/LaunchPage'
|
||||||
|
import { resetAppCache } from '@/registerServiceWorker'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SettingsPage',
|
name: 'SettingsPage',
|
||||||
components: {
|
components: {
|
||||||
'logo':require('@/components/LogoComponent.vue').default,
|
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -176,6 +216,8 @@
|
|||||||
verificationToken: '',
|
verificationToken: '',
|
||||||
showNewNoteConfirm: false,
|
showNewNoteConfirm: false,
|
||||||
|
|
||||||
|
launchOptions: LAUNCH_OPTIONS,
|
||||||
|
launchPage: getLaunchPage(),
|
||||||
themeColors: [
|
themeColors: [
|
||||||
'#21BA45', //Green
|
'#21BA45', //Green
|
||||||
'#b5cc18', //Lime
|
'#b5cc18', //Lime
|
||||||
@@ -197,6 +239,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
chooseLaunchPage(path){
|
||||||
|
setLaunchPage(path)
|
||||||
|
this.launchPage = getLaunchPage()
|
||||||
|
this.$bus.$emit('notification', 'App will open to ' + path)
|
||||||
|
},
|
||||||
|
clearAppCache(){
|
||||||
|
resetAppCache()
|
||||||
|
},
|
||||||
newQuickNote(){
|
newQuickNote(){
|
||||||
|
|
||||||
this.showNewNoteConfirm = true
|
this.showNewNoteConfirm = true
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ui container share-target">
|
||||||
|
|
||||||
|
<div class="ui basic center aligned segment">
|
||||||
|
|
||||||
|
<div v-if="status === 'working'">
|
||||||
|
<loading-icon message="Saving to Scratch Pad" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="status === 'error'">
|
||||||
|
<h3 class="ui header">
|
||||||
|
<i class="exclamation triangle icon"></i>
|
||||||
|
Couldn't save that
|
||||||
|
</h3>
|
||||||
|
<p>You may be offline. Your text is safe below — retry when you have a connection.</p>
|
||||||
|
|
||||||
|
<textarea class="shared-text" rows="8" readonly :value="pushText"></textarea>
|
||||||
|
|
||||||
|
<div class="ui buttons">
|
||||||
|
<div class="ui green button" v-on:click="save()">
|
||||||
|
<i class="redo icon"></i> Retry
|
||||||
|
</div>
|
||||||
|
<div class="ui basic button" v-on:click="discard()">
|
||||||
|
Discard
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<loading-icon message="Working" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// Shared text is parked here when the share arrives while logged out. The
|
||||||
|
// share's query string does not survive the bounce through /login, so a
|
||||||
|
// redirect param alone would lose it.
|
||||||
|
const STASH_KEY = 'pendingShareText'
|
||||||
|
|
||||||
|
// Guard against a pathological share wedging localStorage
|
||||||
|
const MAX_STASH = 100 * 1024
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ShareTargetPage',
|
||||||
|
components: {
|
||||||
|
'loading-icon': defineAsyncComponent(() => import('@/components/LoadingIconComponent.vue')),
|
||||||
|
},
|
||||||
|
data(){
|
||||||
|
return {
|
||||||
|
status: 'working',
|
||||||
|
pushText: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Deliberately NOT calling $root.loginGateway() the way other pages do --
|
||||||
|
// it would bounce away and discard the shared payload before we read it.
|
||||||
|
mounted(){
|
||||||
|
|
||||||
|
const text = this.composeSharedText()
|
||||||
|
|
||||||
|
if(!text){
|
||||||
|
this.$router.replace('/quick')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pushText = text
|
||||||
|
|
||||||
|
// getLoggedIn is reliable here: App.vue's beforeCreate populates the
|
||||||
|
// username from localStorage before any route component mounts.
|
||||||
|
if(!this.$store.getters.getLoggedIn){
|
||||||
|
this.stash(text)
|
||||||
|
this.$router.replace({ path: '/login', query: { redirect: '/share-target' } })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// Android routinely duplicates content across these fields -- the URL
|
||||||
|
// lands in both `text` and `url`, the page title in both `title` and
|
||||||
|
// `text`. Without the dedupe every shared link is saved twice.
|
||||||
|
composeSharedText(){
|
||||||
|
|
||||||
|
const query = this.$route.query
|
||||||
|
|
||||||
|
const parts = ['title', 'text', 'url']
|
||||||
|
.map(key => String(query[key] ?? '').trim())
|
||||||
|
.filter(value => value.length > 0)
|
||||||
|
.filter((value, index, all) => all.indexOf(value) === index)
|
||||||
|
|
||||||
|
if(parts.length > 0){ return parts.join('\n') }
|
||||||
|
|
||||||
|
// Nothing in the URL: this is the replay after logging in
|
||||||
|
return localStorage.getItem(STASH_KEY) || ''
|
||||||
|
},
|
||||||
|
stash(text){
|
||||||
|
if(text.length > MAX_STASH){ return }
|
||||||
|
localStorage.setItem(STASH_KEY, text)
|
||||||
|
},
|
||||||
|
save(){
|
||||||
|
|
||||||
|
this.status = 'working'
|
||||||
|
|
||||||
|
// Same call the paste button makes. The server prepends to the
|
||||||
|
// scratch pad, strips HTML and auto-links URLs, so nothing needs
|
||||||
|
// formatting here.
|
||||||
|
axios.post('/api/quick-note/update', { 'pushText': this.pushText })
|
||||||
|
.then(() => {
|
||||||
|
localStorage.removeItem(STASH_KEY)
|
||||||
|
this.$bus.$emit('notification', 'Saved To Scratch Pad')
|
||||||
|
this.$router.replace('/quick')
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Keep the stash: offline is the likely cause and the service
|
||||||
|
// worker means the app boots without a connection.
|
||||||
|
console.error('[ShareTarget] save failed:', error)
|
||||||
|
this.stash(this.pushText)
|
||||||
|
this.status = 'error'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
discard(){
|
||||||
|
localStorage.removeItem(STASH_KEY)
|
||||||
|
this.$router.replace('/quick')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.share-target {
|
||||||
|
padding-top: 3em;
|
||||||
|
}
|
||||||
|
.shared-text {
|
||||||
|
width: 100%;
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.75em;
|
||||||
|
border: 1px solid var(--border_color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--small_element_bg_color);
|
||||||
|
color: var(--text_color);
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
|
import EventBus from './EventBus'
|
||||||
|
|
||||||
|
// How often to ask the server whether a new build exists
|
||||||
|
const UPDATE_INTERVAL = 60 * 60 * 1000
|
||||||
|
|
||||||
|
//
|
||||||
|
// The worker precaches static build output only -- see the SECURITY INVARIANT
|
||||||
|
// comment in vite.config.js. Nothing user-derived ever enters Cache Storage,
|
||||||
|
// which is why caches are deliberately NOT cleared on logout: doing so would
|
||||||
|
// only destroy the offline shell.
|
||||||
|
//
|
||||||
|
export const updateSW = registerSW({
|
||||||
|
onNeedRefresh(){
|
||||||
|
// A new build is waiting. Let the user apply it rather than swapping
|
||||||
|
// assets underneath them mid-edit.
|
||||||
|
EventBus.$emit('sw-update-available')
|
||||||
|
},
|
||||||
|
onOfflineReady(){
|
||||||
|
EventBus.$emit('notification', 'Ready to work offline')
|
||||||
|
},
|
||||||
|
onRegisteredSW(swUrl, registration){
|
||||||
|
if(!registration){ return }
|
||||||
|
setInterval(() => registration.update(), UPDATE_INTERVAL)
|
||||||
|
},
|
||||||
|
onRegisterError(error){
|
||||||
|
console.error('[SW] registration failed:', error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Escape hatch. A wedged service worker is very hard to talk someone through
|
||||||
|
// remotely, so this is surfaced as a button in Settings.
|
||||||
|
//
|
||||||
|
export async function resetAppCache(){
|
||||||
|
|
||||||
|
if('serviceWorker' in navigator){
|
||||||
|
const registrations = await navigator.serviceWorker.getRegistrations()
|
||||||
|
await Promise.all(registrations.map(registration => registration.unregister()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if('caches' in window){
|
||||||
|
const keys = await caches.keys()
|
||||||
|
await Promise.all(keys.map(key => caches.delete(key)))
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
@@ -1,133 +1,41 @@
|
|||||||
import Vue from 'vue'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Router from 'vue-router'
|
|
||||||
|
|
||||||
//Breaking components into function sections allows webpack to load them dynamically
|
const HomePage = () => import('@/pages/HomePage.vue')
|
||||||
|
const LoginPage = () => import('@/pages/LoginPage.vue')
|
||||||
|
const HelpPage = () => import('@/pages/HelpPage.vue')
|
||||||
|
const TermsPage = () => import('@/pages/TermsPage.vue')
|
||||||
|
const SettingsPage = () => import('@/pages/SettingsPage.vue')
|
||||||
|
const SharePage = () => import('@/pages/SharePage.vue')
|
||||||
|
const NotesPage = () => import('@/pages/NotesPage.vue')
|
||||||
|
const QuickPage = () => import('@/pages/QuickPage.vue')
|
||||||
|
const AttachmentsPage = () => import('@/pages/AttachmentsPage.vue')
|
||||||
|
const OverviewPage = () => import('@/pages/OverviewPage.vue')
|
||||||
|
const BookmarkletPage = () => import('@/pages/BookmarkletPage.vue')
|
||||||
|
const NotFoundPage = () => import('@/pages/NotFoundPage.vue')
|
||||||
|
const MetrictrackingPage = () => import('@/pages/MetrictrackingPage.vue')
|
||||||
|
const ShareTargetPage = () => import('@/pages/ShareTargetPage.vue')
|
||||||
|
|
||||||
const HomePage = () => import(/* webpackChunkName: "HomePage" */ '@/pages/HomePage')
|
export default createRouter({
|
||||||
const LoginPage = () => import(/* webpackChunkName: "LoginPage" */ '@/pages/LoginPage')
|
history: createWebHistory(),
|
||||||
const HelpPage = () => import(/* webpackChunkName: "HelpPage" */ '@/pages/HelpPage')
|
|
||||||
const TermsPage = () => import(/* webpackChunkName: "TermsPage" */ '@/pages/TermsPage')
|
|
||||||
const SettingsPage = () => import(/* webpackChunkName: "SettingsPage" */ '@/pages/SettingsPage')
|
|
||||||
const SharePage = () => import(/* webpackChunkName: "SharePage" */ '@/pages/SharePage')
|
|
||||||
const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage')
|
|
||||||
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
|
|
||||||
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
|
|
||||||
const OverviewPage = () => import(/* webpackChunkName: "OverviewPage" */ '@/pages/OverviewPage')
|
|
||||||
const BookmarkletPage = () => import(/* webpackChunkName: "BookmarkletPage" */ '@/pages/BookmarkletPage')
|
|
||||||
const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
|
|
||||||
|
|
||||||
Vue.use(Router)
|
|
||||||
|
|
||||||
export default new Router({
|
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{ path: '/', name: 'HomePage', meta: { title: 'Home' }, component: HomePage },
|
||||||
path: '/',
|
{ path: '/login', name: 'LoginPage', meta: { title: 'Login' }, component: LoginPage },
|
||||||
name: 'HomePage',
|
{ path: '/notes', name: 'Note Page', meta: { title: 'Notes' }, component: NotesPage },
|
||||||
meta: {title:'Home'},
|
{ path: '/notes/open/:id', name: 'Open Note', meta: { title: 'Open Note' }, component: NotesPage },
|
||||||
component: HomePage
|
{ path: '/search/tags/:tag', name: 'Search Notes', meta: { title: 'Search Notes' }, component: NotesPage },
|
||||||
},
|
{ path: '/notes/open/:id/menu/:openMenu', name: 'Open Note Menu', meta: { title: 'Open Note Menu' }, component: NotesPage },
|
||||||
{
|
{ path: '/help', name: 'Help', meta: { title: 'Help' }, component: HelpPage },
|
||||||
path: '/login',
|
{ path: '/terms', name: 'Terms', meta: { title: 'Terms' }, component: TermsPage },
|
||||||
name: 'LoginPage',
|
{ path: '/bookmarklet', name: 'Bookmarklet', meta: { title: 'Bookmarklet' }, component: BookmarkletPage },
|
||||||
meta: {title:'Login'},
|
{ path: '/settings', name: 'Settings', meta: { title: 'Settings' }, component: SettingsPage },
|
||||||
component: LoginPage
|
{ path: '/public/note/:id/:token', name: 'Share', meta: { title: 'Shared' }, component: SharePage },
|
||||||
},
|
{ path: '/share-target', name: 'Share Target', meta: { title: 'Saving Share' }, component: ShareTargetPage },
|
||||||
{
|
{ path: '/quick', name: 'Quick', meta: { title: 'Scratch Pad' }, component: QuickPage },
|
||||||
path: '/notes',
|
{ path: '/attachments', name: 'Attachments', meta: { title: 'Attachments' }, component: AttachmentsPage },
|
||||||
name: 'Note Page', //don't change this
|
{ path: '/attachments/note/:id', name: 'Attachments for Note', meta: { title: 'Attachments for Note' }, component: AttachmentsPage },
|
||||||
meta: {title:'Notes'},
|
{ path: '/attachments/type/:type', name: 'Attachments by Type', meta: { title: 'Attachments by Type' }, component: AttachmentsPage },
|
||||||
component: NotesPage
|
{ path: '/overview', name: 'Overview of Notes', meta: { title: 'Overview of Notes' }, component: OverviewPage },
|
||||||
},
|
{ path: '/metrictrack', name: 'Metric Tracking', meta: { title: 'Metric Tracking' }, component: MetrictrackingPage },
|
||||||
{
|
{ path: '/:pathMatch(.*)*', name: 'Page Not Found', meta: { title: '404 Page Not Found' }, component: NotFoundPage },
|
||||||
path: '/notes/open/:id',
|
],
|
||||||
name: 'Open Note',
|
|
||||||
meta: {title: 'Open Note'},
|
|
||||||
component: NotesPage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/search/tags/:tag',
|
|
||||||
name: 'Search Notes',
|
|
||||||
meta: {title: 'Search Notes'},
|
|
||||||
component: NotesPage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/notes/open/:id/menu/:openMenu',
|
|
||||||
name: 'Open Note Menu',
|
|
||||||
meta: {title: 'Open Note Menu'},
|
|
||||||
component: NotesPage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/help',
|
|
||||||
name: 'Help',
|
|
||||||
meta: {title:'Help'},
|
|
||||||
component: HelpPage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/terms',
|
|
||||||
name: 'Terms',
|
|
||||||
meta: {title:'Terms'},
|
|
||||||
component: TermsPage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/bookmarklet',
|
|
||||||
name: 'Bookmarklet',
|
|
||||||
meta: {title:'Bookmarklet'},
|
|
||||||
component: BookmarkletPage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/settings',
|
|
||||||
name: 'Settings',
|
|
||||||
meta: {title:'Settings'},
|
|
||||||
component: SettingsPage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/public/note/:id/:token',
|
|
||||||
name: 'Share',
|
|
||||||
meta: {title:'Shared'},
|
|
||||||
component: SharePage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/quick',
|
|
||||||
name: 'Quick',
|
|
||||||
meta: {title:'Scratch Pad'},
|
|
||||||
component: QuickPage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/attachments',
|
|
||||||
name: 'Attachments',
|
|
||||||
meta: {title:'Attachments'},
|
|
||||||
component: AttachmentsPage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/attachments/note/:id',
|
|
||||||
name: 'Attachments for Note',
|
|
||||||
meta: {title:'Attachments for Note'},
|
|
||||||
component: AttachmentsPage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/attachments/type/:type',
|
|
||||||
name: 'Attachments by Type',
|
|
||||||
meta: {title:'Attachments by Type'},
|
|
||||||
component: AttachmentsPage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/overview',
|
|
||||||
name: 'Overview of Notes',
|
|
||||||
meta: {title:'Overview of Notes'},
|
|
||||||
component: OverviewPage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '*',
|
|
||||||
name: 'Page Not Found',
|
|
||||||
meta: {title:'404 Page Not Found'},
|
|
||||||
component: NotFoundPage
|
|
||||||
},
|
|
||||||
// Cycle Tracking
|
|
||||||
{
|
|
||||||
path: '/metrictrack',
|
|
||||||
name: 'Metric Tracking',
|
|
||||||
meta: {title:'Metric Tracking'},
|
|
||||||
component: () => import(/* webpackChunkName: "MetrictrackingPage" */ '@/pages/MetrictrackingPage')
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import Vuex from 'vuex'
|
|
||||||
|
|
||||||
Vue.use(Vuex)
|
|
||||||
|
|
||||||
export default new Vuex.Store({
|
|
||||||
state: {
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
},
|
|
||||||
modules: {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
import Vue from 'vue'
|
import { createStore } from 'vuex'
|
||||||
import Vuex from 'vuex'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
Vue.use(Vuex);
|
export default createStore({
|
||||||
|
|
||||||
export default new Vuex.Store({
|
|
||||||
state: {
|
state: {
|
||||||
username: null,
|
username: null,
|
||||||
nightMode: false,
|
nightMode: false,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { fileURLToPath, URL } from 'node:url'
|
|||||||
// needs the mkcert root CA installed on the device for the origin to be
|
// needs the mkcert root CA installed on the device for the origin to be
|
||||||
// service-worker eligible; clicking through a cert warning is not enough.
|
// service-worker eligible; clicking through a cert warning is not enough.
|
||||||
// DEV_HTTPS=1 pnpm dev
|
// DEV_HTTPS=1 pnpm dev
|
||||||
const certDir = fileURLToPath(new URL('../client/certs', import.meta.url))
|
const certDir = fileURLToPath(new URL('./certs', import.meta.url))
|
||||||
const keyPath = `${certDir}/192.168.1.164+4-key.pem`
|
const keyPath = `${certDir}/192.168.1.164+4-key.pem`
|
||||||
const certPath = `${certDir}/192.168.1.164+4.pem`
|
const certPath = `${certDir}/192.168.1.164+4.pem`
|
||||||
const useHttps =
|
const useHttps =
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
pwa: {
|
|
||||||
name: "SolidScribe",
|
|
||||||
iconPaths: {
|
|
||||||
favicon32: null,
|
|
||||||
favicon16: null,
|
|
||||||
appleTouchIcon: null,
|
|
||||||
maskIcon: null,
|
|
||||||
msTileImage: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// devServer: {
|
|
||||||
// disableHostCheck: true,
|
|
||||||
// proxy: "http://localhost:8081",
|
|
||||||
// public: "blarvin.local",
|
|
||||||
// https: {
|
|
||||||
// key: fs.readFileSync("./certs/192.168.1.164+4-key.pem"),
|
|
||||||
// cert: fs.readFileSync("./certs/192.168.1.164+4.pem"),
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#
|
||||||
|
# nginx for Solid Scribe on NixOS.
|
||||||
|
#
|
||||||
|
# Merge this into your configuration.nix and `nixos-rebuild switch`.
|
||||||
|
# Replace REPO_PATH with the absolute path of the repo clone on the server.
|
||||||
|
#
|
||||||
|
# Four things differ from the previous config, and the first two are required
|
||||||
|
# for the Vue 3 client to work at all:
|
||||||
|
#
|
||||||
|
# 1. SPA fallback. The old Vue 2 client used vue-router's default HASH mode
|
||||||
|
# (no `mode:` was set), so every request was just "/" and routing happened
|
||||||
|
# after the "#". That is why share links looked like /#/public/note/...
|
||||||
|
# The Vue 3 client uses createWebHistory, so /notes, /settings and
|
||||||
|
# /share-target are real HTTP requests. Without tryFiles they all 404.
|
||||||
|
#
|
||||||
|
# 2. root moves up to the vhost. It was inside locations."/", which left
|
||||||
|
# /assets/, /sw.js and /manifest.json unresolvable once they needed their
|
||||||
|
# own location blocks.
|
||||||
|
#
|
||||||
|
# 3. proxyWebsockets on /socket. recommendedProxySettings does NOT add the
|
||||||
|
# Upgrade/Connection headers, so Socket.IO has been silently falling back
|
||||||
|
# to long-polling -- that's the live note-sync path running degraded.
|
||||||
|
#
|
||||||
|
# 4. Cache headers. See the warning below.
|
||||||
|
#
|
||||||
|
# !! CLOUDFLARE !!
|
||||||
|
# The tunnel sits in front of this and Cloudflare caches .js by DEFAULT, which
|
||||||
|
# includes /sw.js. If the edge caches the service worker, the update check can
|
||||||
|
# never see a new one and the app is pinned to an old build with no remote way
|
||||||
|
# to recover. The nginx headers below are necessary but NOT sufficient --
|
||||||
|
# add a Cloudflare Cache Rule that BYPASSES cache for /sw.js (and /index.html)
|
||||||
|
# before cutting over.
|
||||||
|
#
|
||||||
|
# Also confirm which Host header cloudflared sends. This vhost is keyed on
|
||||||
|
# "192.168.1.7"; if the tunnel forwards the public hostname instead, nginx will
|
||||||
|
# not match this vhost and will fall through to the default server. Either add
|
||||||
|
# the public hostname to serverAliases or set the tunnel's httpHostHeader.
|
||||||
|
#
|
||||||
|
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
recommendedProxySettings = true;
|
||||||
|
recommendedGzipSettings = true;
|
||||||
|
|
||||||
|
virtualHosts."192.168.1.7" = {
|
||||||
|
# serverAliases = [ "notes.example.com" ]; # if the tunnel forwards the public host
|
||||||
|
|
||||||
|
root = "REPO_PATH/client/dist";
|
||||||
|
|
||||||
|
locations."/" = {
|
||||||
|
tryFiles = "$uri $uri/ /index.html";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Never let the HTTP layer cache the service worker or the app shell.
|
||||||
|
locations."= /sw.js".extraConfig = ''
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
expires -1;
|
||||||
|
'';
|
||||||
|
|
||||||
|
locations."= /index.html".extraConfig = ''
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
expires -1;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# The manifest changes rarely but should not be pinned for a year.
|
||||||
|
locations."= /manifest.json".extraConfig = ''
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
expires -1;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Vite content-hashes everything under /assets, so these are immutable.
|
||||||
|
locations."/assets/".extraConfig = ''
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
'';
|
||||||
|
|
||||||
|
locations."/api" = {
|
||||||
|
proxyPass = "http://localhost:3000";
|
||||||
|
};
|
||||||
|
|
||||||
|
locations."/socket" = {
|
||||||
|
proxyPass = "http://localhost:3001";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Note on file access: nginx runs as its own user, so REPO_PATH/client/dist must
|
||||||
|
# be readable by it -- every parent directory needs +x. If the repo lives under
|
||||||
|
# /home/mab this usually means loosening the home directory's permissions, or
|
||||||
|
# putting the clone somewhere like /srv instead.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Deploy Solid Scribe. Run this ON the server, from inside the repo.
|
||||||
|
#
|
||||||
|
# cd /path/to/SolidScribe && ./deploy.sh
|
||||||
|
#
|
||||||
|
# Replaces buildAndUpdateProd.sh, which built the old Vue 2 client and rsynced a
|
||||||
|
# tarball to /home/mab/pi -- a directory that no longer exists.
|
||||||
|
#
|
||||||
|
# Assumes:
|
||||||
|
# - this repo is a git clone with a tracking branch
|
||||||
|
# - node + pnpm are on PATH for the user running this (on NixOS that means
|
||||||
|
# environment.systemPackages or the user's profile -- NOT only inside a
|
||||||
|
# nix-shell, or this will fail at the install step)
|
||||||
|
# - pm2 is already managing NoteServer (pm2 resurrect on boot)
|
||||||
|
# - nginx serves client/dist with an SPA fallback to index.html
|
||||||
|
#
|
||||||
|
# nginx is NOT touched here. On NixOS it is declarative -- see configs/.
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$REPO"
|
||||||
|
|
||||||
|
step(){ printf '\n\033[32m::--:: %s\033[0m\n' "$1"; }
|
||||||
|
|
||||||
|
# --- sanity checks before changing anything ------------------------------
|
||||||
|
|
||||||
|
for tool in git pnpm pm2; do
|
||||||
|
command -v "$tool" >/dev/null || { echo "$tool is not on PATH" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
|
||||||
|
echo "Working tree has local modifications. Commit or stash them first:" >&2
|
||||||
|
git status --short >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- pull ----------------------------------------------------------------
|
||||||
|
|
||||||
|
step "Pulling $(git rev-parse --abbrev-ref HEAD)..."
|
||||||
|
# --ff-only: refuse to create a merge commit on the server. If this fails the
|
||||||
|
# server has diverged and wants a human.
|
||||||
|
git pull --ff-only
|
||||||
|
|
||||||
|
# --- dependencies --------------------------------------------------------
|
||||||
|
|
||||||
|
step "Installing server dependencies..."
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
step "Installing client dependencies..."
|
||||||
|
pnpm install --frozen-lockfile --dir client
|
||||||
|
|
||||||
|
# --- build ---------------------------------------------------------------
|
||||||
|
|
||||||
|
# dist is gitignored, so the server builds it. Built into a temp dir first so a
|
||||||
|
# failed build leaves the currently-served files untouched.
|
||||||
|
step "Building client..."
|
||||||
|
rm -rf client/dist.new
|
||||||
|
pnpm --dir client exec vite build --outDir dist.new --emptyOutDir
|
||||||
|
|
||||||
|
if [ ! -f client/dist.new/index.html ] || [ ! -f client/dist.new/sw.js ]; then
|
||||||
|
echo "Build output looks wrong (missing index.html or sw.js) -- keeping the previous build." >&2
|
||||||
|
rm -rf client/dist.new
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Swapping in the new build..."
|
||||||
|
rm -rf client/dist.old
|
||||||
|
[ -d client/dist ] && mv client/dist client/dist.old
|
||||||
|
mv client/dist.new client/dist
|
||||||
|
rm -rf client/dist.old
|
||||||
|
|
||||||
|
# --- restart -------------------------------------------------------------
|
||||||
|
|
||||||
|
step "Reloading the API server..."
|
||||||
|
# reload, not restart: graceful in cluster mode. --update-env so changes to
|
||||||
|
# ecosystem.config.js are picked up.
|
||||||
|
pm2 reload "$REPO/server/ecosystem.config.js" --env production --update-env
|
||||||
|
|
||||||
|
# Persist the process list so `pm2 resurrect` on boot gets the current state.
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
step "Done."
|
||||||
|
pm2 list
|
||||||
@@ -7,7 +7,10 @@ module.exports = {
|
|||||||
// args: 'one two',
|
// args: 'one two',
|
||||||
instances: 1,
|
instances: 1,
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
watch: true,
|
// Off by default so it is safe in production: with watch on, a `git pull`
|
||||||
|
// that touches server files restarts pm2 mid-pull, potentially booting a
|
||||||
|
// half-updated tree. startLocalDev.sh passes --watch for development.
|
||||||
|
watch: false,
|
||||||
ignore_watch : ["node_modules", "staticFiles"],
|
ignore_watch : ["node_modules", "staticFiles"],
|
||||||
max_memory_restart: '1G',
|
max_memory_restart: '1G',
|
||||||
env: {
|
env: {
|
||||||
@@ -18,14 +21,4 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
|
||||||
deploy : {
|
|
||||||
production : {
|
|
||||||
user : 'node',
|
|
||||||
host : '212.83.163.1',
|
|
||||||
ref : 'origin/master',
|
|
||||||
repo : '[email protected]:repo.git',
|
|
||||||
path : '/var/www/production',
|
|
||||||
'post-deploy' : 'npm install && pm2 reload ecosystem.config.js --env production'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# starts the local MySQL, and exports DB_* and JSON_KEY.
|
# starts the local MySQL, and exports DB_* and JSON_KEY.
|
||||||
#
|
#
|
||||||
# Unlike startDevServerAndClient.sh (remote dev box, /home/mab/ss, Vue 2
|
# Unlike startDevServerAndClient.sh (remote dev box, /home/mab/ss, Vue 2
|
||||||
# client) this is path-relative and points at v2client-vite-vue3.
|
# client) this is path-relative.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -28,14 +28,16 @@ cd "$REPO/server"
|
|||||||
# as $name, and pm2 uses $name to override the app name from ecosystem.config.js.
|
# as $name, and pm2 uses $name to override the app name from ecosystem.config.js.
|
||||||
pm2 delete NoteServer &>/dev/null || true
|
pm2 delete NoteServer &>/dev/null || true
|
||||||
pm2 flush &>/dev/null || true
|
pm2 flush &>/dev/null || true
|
||||||
env -u name pm2 start ecosystem.config.js
|
# --watch is a dev-only convenience; ecosystem.config.js leaves it off so a
|
||||||
|
# production `git pull` can't restart the server mid-update.
|
||||||
|
env -u name pm2 start ecosystem.config.js --watch
|
||||||
cd "$REPO"
|
cd "$REPO"
|
||||||
|
|
||||||
echo '::--:: Starting v2client dev server -> http://localhost:5173'
|
echo '::--:: Starting v2client dev server -> http://localhost:5173'
|
||||||
cd "$REPO/v2client-vite-vue3"
|
cd "$REPO/client"
|
||||||
[ -d node_modules ] || pnpm install
|
[ -d node_modules ] || pnpm install
|
||||||
screen -S "V2ClientScreen" -X quit &>/dev/null || true
|
screen -S "V2ClientScreen" -X quit &>/dev/null || true
|
||||||
screen -dmS "V2ClientScreen" bash -c "cd '$REPO/v2client-vite-vue3'; pnpm dev"
|
screen -dmS "V2ClientScreen" bash -c "cd '$REPO/client'; pnpm dev"
|
||||||
|
|
||||||
cat <<MSG
|
cat <<MSG
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Vue 3 + Vite
|
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
|
||||||
|
|
||||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "v2client-vite-vue3",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
|
||||||
"axios": "^1.1.3",
|
|
||||||
"chart.js": "^4.6.1",
|
|
||||||
"diff-match-patch": "^1.0.5",
|
|
||||||
"fomantic-ui-css": "^2.9.0",
|
|
||||||
"mitt": "^3.0.1",
|
|
||||||
"socket.io-client": "^2.5.0",
|
|
||||||
"tailwindcss": "^4.1.13",
|
|
||||||
"vue": "^3.5.18",
|
|
||||||
"vue-chartjs": "^5.3.4",
|
|
||||||
"vue-router": "^4.0.0",
|
|
||||||
"vuedraggable": "^4.1.0",
|
|
||||||
"vuex": "^4.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
|
||||||
"vite": "^7.1.2",
|
|
||||||
"vite-plugin-pwa": "^2.0.0",
|
|
||||||
"workbox-window": "^7.4.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode == 2) }">
|
|
||||||
|
|
||||||
<!-- <div class="ui container" v-if="showFakeSite">
|
|
||||||
<div class="ui basic very padded segment">
|
|
||||||
<div class="ui inverted red segment">
|
|
||||||
<h1>WARNING - False site detected</h1>
|
|
||||||
<h2>The Domain for this website is not correct.</h2>
|
|
||||||
<h2>Only trust <a class="ui button" href="https://www.solidscribe.com">https://www.solidscribe.com</a></h2>
|
|
||||||
<h2>Do not any enter any personal information into this website.</h2>
|
|
||||||
<h2>You will be redirected to the correct domain in {{redirectSeconds}}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="auth-block" v-if="requireAuth">
|
|
||||||
<div class="ui raised inverted segment">
|
|
||||||
<div class="ui centered header">
|
|
||||||
Authentication Required
|
|
||||||
</div>
|
|
||||||
<div class="ui small inverted centered header" v-if="$store.getters.getUsername">
|
|
||||||
<i class="green user outline icon"></i>
|
|
||||||
{{ $store.getters.getUsername }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui large form">
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui small inverted header">Password</div>
|
|
||||||
<div class="ui input">
|
|
||||||
<input type="password" v-model="password" placeholder="Password">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui small inverted header">One Time Password</div>
|
|
||||||
<div class="ui input">
|
|
||||||
<input type="password" v-model="otp" placeholder="One Time Password">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui fluid inverted black button">
|
|
||||||
<i class="unlock icon"></i>
|
|
||||||
Submit</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<global-site-menu v-if="!showFakeSite" />
|
|
||||||
|
|
||||||
<router-view v-if="!showFakeSite" />
|
|
||||||
|
|
||||||
<global-notification />
|
|
||||||
|
|
||||||
<update-prompt />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
import axios from 'axios'
|
|
||||||
import GlobalSiteMenu from '@/components/GlobalSiteMenu.vue'
|
|
||||||
import GlobalNotification from '@/components/GlobalNotificationComponent.vue'
|
|
||||||
import UpdatePrompt from '@/components/UpdatePrompt.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'App',
|
|
||||||
components: {
|
|
||||||
'global-site-menu': GlobalSiteMenu,
|
|
||||||
'global-notification': GlobalNotification,
|
|
||||||
'update-prompt': UpdatePrompt,
|
|
||||||
},
|
|
||||||
data: function(){
|
|
||||||
return {
|
|
||||||
showFakeSite:false, //Incorrect domain detection
|
|
||||||
redirectSeconds: 15,
|
|
||||||
fetchingInProgress: false, //Prevent start getting token while fetch is in progress
|
|
||||||
blockUntilNextRequest: false, //If token was just renewed, don't fetch more until next request
|
|
||||||
|
|
||||||
requireAuth: false,
|
|
||||||
password: '',
|
|
||||||
otp: '',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
//Axios response interceptor
|
|
||||||
// - Gets new session tokens from server and uses them in app
|
|
||||||
beforeCreate: function(){
|
|
||||||
|
|
||||||
//Before all requests going out
|
|
||||||
axios.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
|
|
||||||
//Enable token fetching after another request is made
|
|
||||||
if(this.blockUntilNextRequest){
|
|
||||||
this.fetchingInProgress = false
|
|
||||||
this.blockUntilNextRequest = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add a response interceptor, token can be renewed on every response
|
|
||||||
axios.interceptors.response.use(
|
|
||||||
(response) => {
|
|
||||||
|
|
||||||
if(typeof response.headers.remaininguses !== 'undefined'){
|
|
||||||
|
|
||||||
// console.log(response.headers.remaininguses)
|
|
||||||
//Look at remaining uses of token, if its less than five, request a new one
|
|
||||||
if(response.headers.remaininguses < 15 && !this.fetchingInProgress && !this.blockUntilNextRequest){
|
|
||||||
this.fetchingInProgress = true
|
|
||||||
const currentToken = localStorage.getItem('loginToken')
|
|
||||||
this.$io.emit('renew_session_token', currentToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
|
|
||||||
//Catch all authorization errors, log user out if we encounter one
|
|
||||||
if(error.response && error.response.status == 401){
|
|
||||||
|
|
||||||
this.$router.push('/')
|
|
||||||
this.$store.commit('destroyLoginToken')
|
|
||||||
this.$bus.$emit('notification', 'Error: You have been logged out.')
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
//Puts token into state on page load
|
|
||||||
let token = localStorage.getItem('loginToken')
|
|
||||||
let username = localStorage.getItem('username')
|
|
||||||
|
|
||||||
//
|
|
||||||
if(token && token.length > 0){
|
|
||||||
|
|
||||||
//setup username display
|
|
||||||
this.$store.commit('setUsername', username)
|
|
||||||
|
|
||||||
//Set session token on every request if set
|
|
||||||
axios.defaults.headers.common['authorizationtoken'] = token
|
|
||||||
|
|
||||||
//Setup websockets into vue instance
|
|
||||||
const socket = this.$io
|
|
||||||
socket.on('connect', () => {
|
|
||||||
|
|
||||||
//Put user into personal event room for live note updates, etc
|
|
||||||
this.$io.emit('user_connect', token)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//Detect if user is on a mobile browser and set a flag in store
|
|
||||||
this.$store.commit('detectIsUserOnMobile')
|
|
||||||
|
|
||||||
//Set Main theme color
|
|
||||||
const accentColor = localStorage.getItem('main-accent')
|
|
||||||
if(accentColor){
|
|
||||||
document.documentElement.style.setProperty('--main-accent', accentColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
//Set color theme based on local storage
|
|
||||||
const themeNumber = localStorage.getItem('nightMode')
|
|
||||||
if(themeNumber != null){
|
|
||||||
this.$store.commit('toggleNightMode', themeNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
mounted: function(){
|
|
||||||
|
|
||||||
// Vite does not shim node's `process` into the browser bundle the way
|
|
||||||
// webpack/vue-cli did; import.meta.env.DEV is the equivalent.
|
|
||||||
const isDev = import.meta.env.DEV
|
|
||||||
// if(window.location.hostname.toLowerCase().replace('www.','') != "solidscribe.com" && !isDev){
|
|
||||||
// this.showFakeSite = true
|
|
||||||
// setInterval(() => {
|
|
||||||
// this.redirectSeconds--
|
|
||||||
// if(this.redirectSeconds == 0){
|
|
||||||
// window.location.href = 'https://www.solidscribe.com'
|
|
||||||
// }
|
|
||||||
// }, 1000)
|
|
||||||
// }
|
|
||||||
|
|
||||||
//Update totals for entire app on event
|
|
||||||
this.$io.on('update_counts', () => {
|
|
||||||
console.log('Got event, update totals')
|
|
||||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$io.on('recievend_new_token', newToken => {
|
|
||||||
|
|
||||||
// console.log('Got a new token')
|
|
||||||
|
|
||||||
axios.defaults.headers.common['authorizationtoken'] = newToken
|
|
||||||
localStorage.setItem('loginToken', newToken)
|
|
||||||
|
|
||||||
//Disable getting new tokens until next request
|
|
||||||
this.blockUntilNextRequest = true
|
|
||||||
})
|
|
||||||
|
|
||||||
//Track users active sessions
|
|
||||||
this.$io.on('update_active_user_count', countData => {
|
|
||||||
this.$store.commit('setActiveSessions', countData)
|
|
||||||
})
|
|
||||||
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
loggedIn () {
|
|
||||||
//Map logged in from state
|
|
||||||
return this.$store.getters.getLoggedIn
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
loginGateway() {
|
|
||||||
if(!this.loggedIn){
|
|
||||||
console.log('This user is not logged in')
|
|
||||||
this.$router.push({'path':'/login'})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
},
|
|
||||||
logout() {
|
|
||||||
|
|
||||||
this.$router.push('/')
|
|
||||||
axios.post('/api/user/logout')
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$store.commit('destroyLoginToken')
|
|
||||||
this.$bus.$emit('notification', 'Logged Out')
|
|
||||||
}, 200)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import mitt from 'mitt'
|
|
||||||
|
|
||||||
const emitter = mitt()
|
|
||||||
|
|
||||||
// Preserves the Vue 2 $on/$emit/$off API used throughout the codebase
|
|
||||||
const EventBus = {
|
|
||||||
$on: (...args) => emitter.on(...args),
|
|
||||||
$off: (...args) => emitter.off(...args),
|
|
||||||
$emit: (...args) => emitter.emit(...args),
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EventBus
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
const helpers = {}
|
|
||||||
|
|
||||||
helpers.timeAgo = (time) => {
|
|
||||||
|
|
||||||
if(time == null){
|
|
||||||
time = Math.round(time/1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(time.toString().length >= 13){
|
|
||||||
time = Math.round(time/1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const time_formats = [
|
|
||||||
[ 60, 'seconds', 1 ],
|
|
||||||
[ 120, '1 minute ago', '1 minute from now' ],
|
|
||||||
[ 3600, 'minutes', 60 ],
|
|
||||||
[ 7200, '1 hour ago', '1 hour from now' ],
|
|
||||||
[ 86400, 'hours', 3600 ],
|
|
||||||
[ 172800, 'yesterday', 'tomorrow' ],
|
|
||||||
[ 604800, 'days', 86400 ],
|
|
||||||
[ 1209600, 'last week', 'next week' ],
|
|
||||||
[ 2419200, 'weeks', 604800 ],
|
|
||||||
[ 4838400, 'last month', 'next month' ],
|
|
||||||
[ 29030400, 'months', 2419200 ],
|
|
||||||
[ 58060800, 'last year', 'next year' ],
|
|
||||||
[ 2903040000, 'years', 29030400 ],
|
|
||||||
[ 5806080000, 'last century', 'next century' ],
|
|
||||||
[ 58060800000, 'centuries', 2903040000 ]
|
|
||||||
]
|
|
||||||
|
|
||||||
//How many seconds ago was input event time?
|
|
||||||
let seconds = Math.round((+new Date)/1000) - time
|
|
||||||
let token = 'ago'
|
|
||||||
let list_choice = 1
|
|
||||||
|
|
||||||
if (seconds == 0) {
|
|
||||||
return 'Just now'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seconds < 0) {
|
|
||||||
seconds = Math.abs(seconds)
|
|
||||||
token = 'from now'
|
|
||||||
list_choice = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let i = 0
|
|
||||||
let format = null
|
|
||||||
while (format = time_formats[i++]){
|
|
||||||
if (seconds < format[0]) {
|
|
||||||
if (typeof format[2] == 'string') {
|
|
||||||
return format[list_choice]
|
|
||||||
} else {
|
|
||||||
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return time
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugin = {
|
|
||||||
install(app) {
|
|
||||||
app.config.globalProperties.$helpers = helpers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default plugin
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
<style type="text/css" scoped>
|
|
||||||
|
|
||||||
.numtainer {
|
|
||||||
height: 1.1em;
|
|
||||||
font-size: 1em;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-high {
|
|
||||||
color: #4dc86a;
|
|
||||||
animation: startHigh 0.5s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-low {
|
|
||||||
color: #4dc86a;
|
|
||||||
animation: startLow 0.5s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes startLow {
|
|
||||||
0% {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
margin-top: -1.2em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes startHigh {
|
|
||||||
0% {
|
|
||||||
margin-top: -1.2em;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="numtainer">
|
|
||||||
|
|
||||||
<div v-if="animateUp">
|
|
||||||
<div class="start-high">{{ newNumber }}</div>
|
|
||||||
<div>{{ oldNumber }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="animateDown">
|
|
||||||
<div class="start-low">{{ oldNumber }}</div>
|
|
||||||
<div>{{ newNumber }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div v-if="totals">{{ totals[numberId] }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
import { mapGetters } from 'vuex'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'AnimatedCounterComponent',
|
|
||||||
props: [ 'numberId' ],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
oldNumber: 100,
|
|
||||||
newNumber: 99,
|
|
||||||
animateUp: false,
|
|
||||||
animateDown: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters(['totals'])
|
|
||||||
},
|
|
||||||
watch:{
|
|
||||||
totals(newVal, oldVal){
|
|
||||||
if(oldVal && newVal && newVal[this.numberId] != oldVal[this.numberId]){
|
|
||||||
|
|
||||||
// console.log('New number ', newVal[this.numberId])
|
|
||||||
|
|
||||||
this.oldNumber = oldVal[this.numberId]
|
|
||||||
this.newNumber = newVal[this.numberId]
|
|
||||||
|
|
||||||
if(this.oldNumber > this.newNumber){
|
|
||||||
this.animateDown = true
|
|
||||||
} else {
|
|
||||||
this.animateUp = true
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout( () => {
|
|
||||||
this.animateUp = false
|
|
||||||
this.animateDown = false
|
|
||||||
}, 550)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeMount(){
|
|
||||||
|
|
||||||
},
|
|
||||||
mounted(){
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onFileClick(file){
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
<style type="text/css" scoped>
|
|
||||||
|
|
||||||
.attachment-display-card {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
display: inline-block;
|
|
||||||
border: 1px solid;
|
|
||||||
border-color: var(--border_color);
|
|
||||||
background-color: var(--small_element_bg_color);
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 0 0 15px;
|
|
||||||
max-height: 10000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-image {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
max-height: 300px;
|
|
||||||
}
|
|
||||||
.image-placeholder {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
max-height: 75px;
|
|
||||||
}
|
|
||||||
.image-placeholder:after {
|
|
||||||
content: 'No Image';
|
|
||||||
display: block;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
background:
|
|
||||||
green;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.text {
|
|
||||||
width: 100%;
|
|
||||||
/*height: 100%;*/
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid;
|
|
||||||
border-bottom: 1px solid;
|
|
||||||
border-color: var(--border_color);
|
|
||||||
font-size: 1.2em;
|
|
||||||
line-height: 1.3em;
|
|
||||||
/*margin: 0 0 10px;*/
|
|
||||||
padding: 10px 0 10px;
|
|
||||||
|
|
||||||
color: var(--text_color);
|
|
||||||
overflow: hidden;
|
|
||||||
resize: none;
|
|
||||||
/*transition: height 0.4s ease; This breaks the resize */
|
|
||||||
}
|
|
||||||
.link {
|
|
||||||
font-size: 1.4em;
|
|
||||||
margin: 20px 0 20px;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow:hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
width: 100%;
|
|
||||||
line-height: 1.4em;
|
|
||||||
}
|
|
||||||
.flip-out {
|
|
||||||
animation: flip-out-hor-top 0.5s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.3s ease;
|
|
||||||
max-height: 0;
|
|
||||||
}
|
|
||||||
@keyframes flip-out-hor-top {
|
|
||||||
0% {
|
|
||||||
transform: rotateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotateX(70deg);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="attachment-display-card" :class="{ 'flip-out':!unfolded }" v-if="visible">
|
|
||||||
<div class="ui stackable grid">
|
|
||||||
|
|
||||||
<!-- image and text -->
|
|
||||||
<div class="six wide center aligned middle aligned column">
|
|
||||||
<a :href="linkUrl" target="_blank" >
|
|
||||||
<img v-if="item.file_location" class="attachment-image"
|
|
||||||
onerror="
|
|
||||||
this.onerror=null;
|
|
||||||
this.src='/api/static/assets/marketing/void.svg';
|
|
||||||
this.classList.add('image-placeholder');
|
|
||||||
this.insertAdjacentText('afterend', 'Image not found');
|
|
||||||
"
|
|
||||||
:src="`/api/static/thumb_${item.file_location}`">
|
|
||||||
<span v-else>
|
|
||||||
<img class="image-placeholder" loading="lazy" src="/api/static/assets/marketing/void.svg">
|
|
||||||
No Image
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ten wide column">
|
|
||||||
<textarea ref="edit" class="text" v-on:blur="saveIt()" v-on:keyup="checkKeyup"
|
|
||||||
v-model="text"
|
|
||||||
v-on:focus="showSave = true"
|
|
||||||
></textarea>
|
|
||||||
|
|
||||||
<div v-if="showSave" class="ui green button">Save</div>
|
|
||||||
|
|
||||||
<!-- link -->
|
|
||||||
<a class="link" :href="linkUrl" target="_blank">{{linkText}}</a>
|
|
||||||
|
|
||||||
<!-- Buttons -->
|
|
||||||
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openNote">
|
|
||||||
<i class="file outline icon"></i>
|
|
||||||
Open Note
|
|
||||||
</div>
|
|
||||||
<div v-if="!item.note_id" class="ui small compact basic disabled button">
|
|
||||||
<i class="angle double up icon"></i>
|
|
||||||
Pushed from Web
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openEditAttachments"
|
|
||||||
:class="{ 'disabled':this.searchParams.noteId }">
|
|
||||||
<i class="folder open outline icon"></i>
|
|
||||||
Note Files
|
|
||||||
</div>
|
|
||||||
<div class="ui small compact basic icon button" v-on:click="deleteAttachment">
|
|
||||||
<i v-if="!working" class="trash alternate outline icon"></i>
|
|
||||||
<i v-if="working" class="purple spinner loading icon icon"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
|
|
||||||
props: [ 'item', 'searchParams' ],
|
|
||||||
data: function(){
|
|
||||||
return {
|
|
||||||
text: '',
|
|
||||||
type: null,
|
|
||||||
|
|
||||||
linkText: 'Link',
|
|
||||||
linkUrl:null,
|
|
||||||
|
|
||||||
unfolded:true,
|
|
||||||
visible: true,
|
|
||||||
showSave: false,
|
|
||||||
|
|
||||||
working: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeCreate: function(){
|
|
||||||
},
|
|
||||||
mounted: function(){
|
|
||||||
this.text = this.item.text
|
|
||||||
this.type = this.item.attachment_type
|
|
||||||
|
|
||||||
|
|
||||||
//1 = URL, 2 = image, >= 3 files
|
|
||||||
if(this.type == 1 && this.item.url != null){
|
|
||||||
this.linkText = this.item.url
|
|
||||||
this.linkUrl = this.item.url
|
|
||||||
}
|
|
||||||
if(this.type == 2){
|
|
||||||
this.linkText = 'Download'
|
|
||||||
this.linkUrl = `/api/static/${this.item.file_location}`
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.checkKeyup()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updated: function(){
|
|
||||||
this.checkKeyup()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
checkKeyup(){
|
|
||||||
let elm = this.$refs.edit
|
|
||||||
if(elm){
|
|
||||||
elm.style.height = '0'
|
|
||||||
elm.style.height = elm.scrollHeight +'px'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openNote(){
|
|
||||||
const noteId = this.item.note_id
|
|
||||||
this.$router.push('/notes/open/'+noteId)
|
|
||||||
},
|
|
||||||
openEditAttachments(){
|
|
||||||
const noteId = this.item.note_id
|
|
||||||
this.$router.push('/attachments/note/'+noteId)
|
|
||||||
},
|
|
||||||
deleteAttachment(){
|
|
||||||
|
|
||||||
//No double clicks
|
|
||||||
if(this.working){ return }
|
|
||||||
|
|
||||||
this.working = true
|
|
||||||
axios.post('/api/attachment/delete', {'attachmentId':this.item.id})
|
|
||||||
.then( ({data}) => {
|
|
||||||
if(data){
|
|
||||||
this.unfolded = false
|
|
||||||
setTimeout( () => {
|
|
||||||
this.visible = false
|
|
||||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
|
||||||
}, 600)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to delete attachment') })
|
|
||||||
},
|
|
||||||
saveIt(){
|
|
||||||
|
|
||||||
this.showSave = false
|
|
||||||
|
|
||||||
//Don't save text if it didn'th change
|
|
||||||
if(this.item.text == this.text){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
'attachmentId': this.item.id,
|
|
||||||
'updatedText': this.text,
|
|
||||||
'noteId': this.item.note_id
|
|
||||||
}
|
|
||||||
|
|
||||||
//Save it, and don't think about it.
|
|
||||||
axios.post('/api/attachment/update', data)
|
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Save') })
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
<template>
|
|
||||||
|
|
||||||
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<div class="ui grid">
|
|
||||||
|
|
||||||
<div class="ui sixteen wide column">
|
|
||||||
<div class="ui dividing header">
|
|
||||||
Reset Background Color and Icon
|
|
||||||
</div>
|
|
||||||
<div class="ui labeled basic icon button" v-on:click="clearStyles">
|
|
||||||
<i class="refresh icon"></i>
|
|
||||||
Reset
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sixteen wide column rounded" :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
|
|
||||||
<div class="ui dividing header" :style="{ 'color':allStyles['noteText']}">
|
|
||||||
<i class="fill drip icon"></i>
|
|
||||||
Background Color
|
|
||||||
</div>
|
|
||||||
<div v-for="color in colors"
|
|
||||||
class="color-button"
|
|
||||||
:style="{ backgroundColor:color }"
|
|
||||||
v-on:click="chosenColor(color)"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sixteen wide column">
|
|
||||||
<div class="ui dividing header">
|
|
||||||
<span v-if="allStyles.noteIcon" >
|
|
||||||
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
|
||||||
</span>
|
|
||||||
Note Icon
|
|
||||||
</div>
|
|
||||||
<div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
|
|
||||||
<i :class="`large ${icon} icon`"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sixteen wide column">
|
|
||||||
<div class="ui dividing header">
|
|
||||||
<span v-if="allStyles.noteIcon" >
|
|
||||||
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
|
||||||
</span>
|
|
||||||
Icon Color
|
|
||||||
</div>
|
|
||||||
<div v-for="color in getReducedColors()"
|
|
||||||
class="color-button"
|
|
||||||
:style="{ backgroundColor:color }"
|
|
||||||
v-on:click="chooseIconColor(color)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ColorPicker',
|
|
||||||
props: [ 'location', 'styleObject' ],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
allStyles:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
|
|
||||||
blankStyle:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
|
|
||||||
colors: [null,
|
|
||||||
'rgb(67,67,67)','rgb(102,102,102)','rgb(153,153,153)','rgb(183,183,183)','rgb(204,204,204)','rgb(217,217,217)','rgb(239,239,239)','rgb(243,243,243)','rgb(255,255,255)','rgb(152,0,0)','rgb(255,0,0)','rgb(255,153,0)','rgb(255,255,0)','rgb(0,255,0)','rgb(0,255,255)','rgb(74,134,232)','rgb(0,0,255)','rgb(153,0,255)','rgb(255,0,255)','rgb(230,184,175)','rgb(244,204,204)','rgb(252,229,205)','rgb(255,242,204)','rgb(217,234,211)','rgb(208,224,227)','rgb(201,218,248)','rgb(207,226,243)','rgb(217,210,233)','rgb(234,209,220)','rgb(221,126,107)','rgb(234,153,153)','rgb(249,203,156)','rgb(255,229,153)','rgb(182,215,168)','rgb(162,196,201)','rgb(164,194,244)','rgb(159,197,232)','rgb(180,167,214)','rgb(213,166,189)','rgb(204,65,37)','rgb(224,102,102)','rgb(246,178,107)','rgb(255,217,102)','rgb(147,196,125)','rgb(118,165,175)','rgb(109,158,235)','rgb(111,168,220)','rgb(142,124,195)','rgb(194,123,160)','rgb(166,28,0)','rgb(204,0,0)','rgb(230,145,56)','rgb(241,194,50)','rgb(106,168,79)','rgb(69,129,142)','rgb(60,120,216)','rgb(61,133,198)','rgb(103,78,167)','rgb(166,77,121)','rgb(133,32,12)','rgb(153,0,0)','rgb(180,95,6)','rgb(191,144,0)','rgb(56,118,29)','rgb(19,79,92)','rgb(17,85,204)','rgb(11,83,148)','rgb(53,28,117)','rgb(116,27,71)','rgb(91,15,0)','rgb(102,0,0)','rgb(120,63,4)','rgb(127,96,0)','rgb(39,78,19)','rgb(12,52,61)','rgb(28,69,135)','rgb(7,55,99)','rgb(32,18,77)','rgb(76,17,48)'],
|
|
||||||
icons: ['cat','crow','dog','dove','dragon','feather','feather alternate','fish','frog','hippo','horse','horse head','kiwi bird','otter','paw','spider','video','headphones','motorcycle','truck','monster truck','campground','cloud sun','drumstick bite','football ball','fruit-apple','hiking','mountain','tractor','tree','wind','wine bottle','coffee','flask','glass cheers','glass martini','beer','toilet paper','gift','globe','hand holding heart','comment','graduation cap','hat cowboy','hat wizard','mitten','user tie','laptop code','microchip','shield alternate','mouse','plug','power off','satellite','hammer','wrench','bell','eye','marker','paperclip','atom','award','theater masks','music','grin alternate','grin tongue squint outline','laugh wink','fire','fire alternate','poop','sun','money bill alternate','piggy bank','heart outline','heartbeat','running','walking','bacon','bone','bread slice','candy cane','carrot','cheese','cloud meatball','cookie','egg','hamburger','hotdog','ice cream','lemon','lemon outline','pepper hot','pizza slice','seedling','stroopwafel','leaf','book dead','broom','cloud moon','ghost','mask','skull crossbones','certificate','check','check circle','joint','cannabis','bong','gem','futbol','brain','dna','hand spock','hand spock outline','meteor','moon','moon outline','robot','rocket','satellite dish','space shuttle','user astronaut','fingerprint','thumbs up','thumbs down']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch:{
|
|
||||||
styleObject: function(updatedStyles){
|
|
||||||
this.allStyles = updatedStyles
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted(){
|
|
||||||
this.allStyles = this.styleObject
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getReducedColors(){
|
|
||||||
|
|
||||||
let reduced = []
|
|
||||||
|
|
||||||
this.colors.forEach((color,i) => {
|
|
||||||
if(i < 20 || i > 69){
|
|
||||||
reduced.push(color)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return reduced
|
|
||||||
},
|
|
||||||
clearStyles(){
|
|
||||||
this.$emit('changeColor', this.blankStyle)
|
|
||||||
},
|
|
||||||
closeThisBitch(){
|
|
||||||
this.$emit('close')
|
|
||||||
},
|
|
||||||
chosenColor(inColor){
|
|
||||||
|
|
||||||
//Set not background to color that was chosen
|
|
||||||
this.allStyles.noteBackground = inColor
|
|
||||||
|
|
||||||
if(inColor == null){
|
|
||||||
this.$emit('changeColor', this.allStyles)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Automatically select note text color
|
|
||||||
|
|
||||||
//If you are using hex colors, use this
|
|
||||||
// Convert hex color to RGB - http://gist.github.com/983661
|
|
||||||
// let color = +("0x" + inColor.slice(1).replace(inColor.length < 5 && /./g, '$&$&'));
|
|
||||||
// let r = color >> 16;
|
|
||||||
// let g = color >> 8 & 255;
|
|
||||||
// let b = color & 255;
|
|
||||||
|
|
||||||
const set = inColor.match(/\d+/g)
|
|
||||||
const r = parseInt(set[0])
|
|
||||||
const g = parseInt(set[1])
|
|
||||||
const b = parseInt(set[2])
|
|
||||||
|
|
||||||
//Convert RGB to HSP
|
|
||||||
const hsp = Math.sqrt( 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) );
|
|
||||||
|
|
||||||
//If it has a BG color, default to sold black text
|
|
||||||
this.allStyles.noteText = '#000'
|
|
||||||
if(hsp < 127.5){
|
|
||||||
this.allStyles.noteText = '#FFF' //If color is dark, we need brighter text
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('changeColor', this.allStyles)
|
|
||||||
},
|
|
||||||
chosenIcon(inIcon){
|
|
||||||
|
|
||||||
this.allStyles.noteIcon = inIcon
|
|
||||||
|
|
||||||
this.$emit('changeColor', this.allStyles)
|
|
||||||
},
|
|
||||||
chooseIconColor(inColor){
|
|
||||||
|
|
||||||
this.allStyles.iconColor = inColor
|
|
||||||
|
|
||||||
this.$emit('changeColor', this.allStyles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style type="text/css" scoped>
|
|
||||||
.icon-button, .color-button {
|
|
||||||
height: 40px;
|
|
||||||
width: calc(15% - 1px);
|
|
||||||
display: inline-block;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.3em;
|
|
||||||
border: 1px solid grey;
|
|
||||||
text-align: center;
|
|
||||||
padding: 5px 0px 0 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0px 1px 3px 0px #3e3e3e;
|
|
||||||
margin: 2px 2px 0 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.color-button {
|
|
||||||
width: calc(10% - 4px);
|
|
||||||
}
|
|
||||||
.rounded {
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span v-on:mouseover="open = true" v-on:mouseleave="open = false" class="relative clickable filter-header" :class="{'filter-active':open}">
|
|
||||||
{{displayString()}} <i class="grey caret down icon"></i>
|
|
||||||
<span class="filter-menu" v-if="open">
|
|
||||||
<span v-for="(filter,label) in filterOptions" class="filter-option" v-on:click="updateFilter(filter, label)">{{label}}</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'FastFilters',
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
click: 0,
|
|
||||||
open: false,
|
|
||||||
filter: {},
|
|
||||||
orderString: 'Order by Last Edited',
|
|
||||||
filterOptions:{
|
|
||||||
'Order by Last Edited' :'lastEdited',
|
|
||||||
'Order by Last Opened' :'lastOpened',
|
|
||||||
'Order by Last Created' :'lastCreated',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeMount(){
|
|
||||||
// this.$bus.$on('reset_fast_filters', () => {
|
|
||||||
// this.orderString = 'Order by Last Edited'
|
|
||||||
// })
|
|
||||||
},
|
|
||||||
methods:{
|
|
||||||
displayString(){
|
|
||||||
return this.orderString.replace('Order by','').replace('Only Show','')
|
|
||||||
},
|
|
||||||
updateFilter(option, label){
|
|
||||||
|
|
||||||
this.orderString = label
|
|
||||||
this.open = false
|
|
||||||
|
|
||||||
let filter = {}
|
|
||||||
filter[option] = 1
|
|
||||||
|
|
||||||
// this.$bus.$emit('update_fast_filters', filter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style type="text/css" scoped>
|
|
||||||
|
|
||||||
.filter-header {
|
|
||||||
width: 200px;
|
|
||||||
padding: 0 0 0 10px;
|
|
||||||
border: 1px solid rgba(0,0,0,0);
|
|
||||||
border-bottom: none;
|
|
||||||
display: inline-block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-top-right-radius: 5px;
|
|
||||||
border-top-left-radius: 5px;
|
|
||||||
margin: 0 0 0 -11px;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.filter-menu {
|
|
||||||
|
|
||||||
color: var(--text_color);
|
|
||||||
background-color: var(--small_element_bg_color);
|
|
||||||
|
|
||||||
|
|
||||||
border: 1px solid;
|
|
||||||
border-top: none;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
width: 200px;
|
|
||||||
left: -1px;
|
|
||||||
z-index: 10;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-bottom-right-radius: 5px;
|
|
||||||
border-bottom-left-radius: 5px;
|
|
||||||
border-color: var(--border_color);
|
|
||||||
}
|
|
||||||
.filter-active {
|
|
||||||
border: 1px solid;
|
|
||||||
border-bottom: none;
|
|
||||||
border-color: var(--border_color);
|
|
||||||
}
|
|
||||||
.filter-option {
|
|
||||||
font-size: 17px;
|
|
||||||
width: 100%;
|
|
||||||
display: inline-block;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 0 5px 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||