Compare commits
93 Commits
d4be0d6471
...
dev
Author | SHA1 | Date | |
---|---|---|---|
|
276a72b4ce | ||
|
b5ef64485f | ||
|
c61f0c0198 | ||
|
d3acd62688 | ||
|
a1ca4c3d06 | ||
|
27699cd6fc | ||
|
39e153b8e1 | ||
|
48c1fa8e69 | ||
|
0202d1acda | ||
|
31473c02ea | ||
|
59f13484a7 | ||
|
0c107a60bd | ||
|
2b76f74dee | ||
|
1a6a7bdfd4 | ||
|
b51e5ac0d0 | ||
|
77cd95fdcb | ||
|
d94b8c90fc | ||
|
0c4f6e94c1 | ||
|
bc44b3db9a | ||
|
c1797474b8 | ||
|
148b822d49 | ||
|
b666bfc197 | ||
|
7ccf0417e0 | ||
|
c7e342be4d | ||
|
941d21d9cb | ||
|
e5adaefa0e | ||
|
cf426eba81 | ||
|
91e37d368d | ||
|
b0c487404c | ||
|
b34a62e114 | ||
|
a8a966866c | ||
|
06b8f0ad6a | ||
|
2ae84ab73e | ||
|
8b711ab508 | ||
|
0bbdda9a2a | ||
|
39b3eef64a | ||
|
071aaf22cd | ||
|
d2624628d8 | ||
|
6bb856689d | ||
|
8e5e06be9b | ||
|
06a140e0d4 | ||
|
543ecf0f2d | ||
|
5096e74a60 | ||
|
e87e8513bc | ||
|
67b218329b | ||
|
df073b0e4d | ||
|
a545ced98f | ||
|
29845e2294 | ||
|
e296775a31 | ||
|
3d2c9868fd | ||
|
9efe21476f | ||
|
2b8f70b5fa | ||
|
278b204b3b | ||
|
3535f0cb24 | ||
|
a3fa4b0f3c | ||
|
25b2bd237b | ||
|
aa4de83de6 | ||
|
35605e54d9 | ||
|
6dfe753d38 | ||
|
003153d994 | ||
|
ecbf6a9cde | ||
|
749d2cea94 | ||
|
342ac95282 | ||
|
ca0b649ff6 | ||
|
4533f6065d | ||
|
f481a97a8c | ||
|
f7fc937d26 | ||
|
4c17efceea | ||
|
d3c0d6e2b9 | ||
|
de3391eb94 | ||
|
2d0beec409 | ||
|
b838f9f571 | ||
|
72b7f8946a | ||
|
902e779a84 | ||
|
8f5d8049d0 | ||
|
ef0a6a44c9 | ||
|
fab0b3873f | ||
|
7fe702cb1b | ||
|
aded72928c | ||
|
e56042958b | ||
|
daa674c54f | ||
|
a51d81b013 | ||
|
771b739c37 | ||
|
c903bcbcd1 | ||
|
9b7fc679e8 | ||
|
623d094d7b | ||
|
ad91218359 | ||
|
97e7988ed1 | ||
|
86f7f61933 | ||
|
133a86e09e | ||
|
68effaa5c3 | ||
|
abb4e20ec3 | ||
|
6fe39406b7 |
6
.gitignore
vendored
@@ -7,3 +7,9 @@ pids
|
|||||||
*.seed
|
*.seed
|
||||||
*.pid.lock
|
*.pid.lock
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# exclude everything
|
||||||
|
staticFiles/*
|
||||||
|
|
||||||
|
# exception to the rule
|
||||||
|
!staticFiles/assets/
|
@@ -11,7 +11,9 @@ echo '-------'
|
|||||||
|
|
||||||
BACKUPDIR="/home/mab/databaseBackupSolidScribe"
|
BACKUPDIR="/home/mab/databaseBackupSolidScribe"
|
||||||
#DEVDBPASS="Crama!Lama*Jamma###88383!!!!!345345956245i"
|
#DEVDBPASS="Crama!Lama*Jamma###88383!!!!!345345956245i"
|
||||||
DEVDBPASS="RootPass1234!"
|
#DEVDBPASS="RootPass1234!"
|
||||||
|
DEVDBPASS="ReallySecureRootPass123!"
|
||||||
|
# LazaLinga&33Can't!Do!That34
|
||||||
|
|
||||||
cd $BACKUPDIR
|
cd $BACKUPDIR
|
||||||
|
|
||||||
@@ -28,8 +30,12 @@ gunzip -dkv $LASTZIPPEDFILE
|
|||||||
BACKUPFILE=$(ls -At *.sql | head -n1)
|
BACKUPFILE=$(ls -At *.sql | head -n1)
|
||||||
|
|
||||||
#Fix to replace incompatible DB type
|
#Fix to replace incompatible DB type
|
||||||
echo "Updating table name in $BACKUPFILE"
|
echo "Updating table name in -> $BACKUPFILE"
|
||||||
sed -i $BACKUPFILE -e 's/utf8mb4_0900_ai_ci/utf8mb4_unicode_ci/g'
|
#sed -i $BACKUPFILE -e 's/utf8mb4_0900_ai_ci/utf8mb4_unicode_ci/g'
|
||||||
|
|
||||||
|
#Fix encoding for dev DB and exclude system tables
|
||||||
|
sed -i 's/utf8mb4_0900_ai_ci/utf8mb4_general_ci/g' $BACKUPFILE
|
||||||
|
sed -r '/INSERT INTO `(sys|mysql)`/d' $BACKUPFILE > $BACKUPFILE
|
||||||
|
|
||||||
echo "Removing and syncing static files"
|
echo "Removing and syncing static files"
|
||||||
rm -r /home/mab/ss/staticFiles/*
|
rm -r /home/mab/ss/staticFiles/*
|
||||||
@@ -38,8 +44,20 @@ rsync -e 'ssh -p 13328' -hazC --update mab@solidscribe.com:/home/mab/pi/staticFi
|
|||||||
echo "Updating Database"
|
echo "Updating Database"
|
||||||
mysql -u root --password="$DEVDBPASS" < $BACKUPFILE
|
mysql -u root --password="$DEVDBPASS" < $BACKUPFILE
|
||||||
|
|
||||||
|
## Optimize Database Tables
|
||||||
|
# mysqlcheck --all-databases
|
||||||
|
mysqlcheck --all-databases -o -u root --password="$DEVDBPASS" --silent
|
||||||
|
# mysqlcheck --all-databases --auto-repair
|
||||||
|
# mysqlcheck --all-databases --analyze
|
||||||
|
|
||||||
|
# Fix an issues with DB after messing around with it
|
||||||
|
mysql_upgrade -u root --password="$DEVDBPASS"
|
||||||
|
|
||||||
|
#clean up extracted and modified SQL dumps
|
||||||
rm *.sql
|
rm *.sql
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
echo '-------'
|
echo '-------'
|
||||||
echo "Applied Prod database to Dev. LastFile: $BACKUPFILE"
|
echo "Applied Prod database to Dev. LastFile: $BACKUPFILE"
|
||||||
echo '-------'
|
echo '-------'
|
@@ -1,18 +1,24 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Take all variables in .env and turn them into local variables for this script
|
||||||
|
source ~/.env
|
||||||
|
|
||||||
BACKUPDIR="/home/mab/databaseBackupSolidScribe"
|
BACKUPDIR="/home/mab/databaseBackupSolidScribe"
|
||||||
|
|
||||||
mkdir -p $BACKUPDIR
|
mkdir -p $BACKUPDIR
|
||||||
cd $BACKUPDIR
|
cd $BACKUPDIR
|
||||||
|
|
||||||
NOW=$(date +"%Y-%m-%d_%H-%M")
|
NOW=$(date +"%Y-%m-%d_%H-%M")
|
||||||
ssh mab@solidscribe.com -p 13328 "mysqldump --all-databases --single-transaction --user root -pRootPass1234!" > "backup-$NOW.sql"
|
ssh mab@solidscribe.com -p 13328 "mysqldump --all-databases --single-transaction --user root -p$PROD_DB_PASS" > "backup-$NOW.sql"
|
||||||
gzip "backup-$NOW.sql"
|
gzip "backup-$NOW.sql"
|
||||||
|
|
||||||
# cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/backup-$NOW.sql"
|
# cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/backup-$NOW.sql"
|
||||||
|
|
||||||
echo "Database Backup Complete on $NOW"
|
echo "Database Backup Complete on $NOW"
|
||||||
|
|
||||||
|
# Delete all but last 8 files
|
||||||
|
ls -tp | grep -v '/$' | tail -n +9 | tr '\n' '\0' | xargs -0 rm --
|
||||||
|
|
||||||
##
|
##
|
||||||
# Restore DB
|
# Restore DB
|
||||||
##
|
##
|
||||||
|
3
client/.gitignore
vendored
@@ -6,6 +6,9 @@ node_modules
|
|||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
*.pem
|
||||||
|
*.crt
|
||||||
|
*.key
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
@@ -1 +1,19 @@
|
|||||||
# Solid Scribe
|
# client
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
|
19
client/jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "esnext",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lib": [
|
||||||
|
"esnext",
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"scripthost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
23433
client/package-lock.json
generated
@@ -7,21 +7,21 @@
|
|||||||
"build": "vue-cli-service build"
|
"build": "vue-cli-service build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.20.0",
|
"axios": "^1.1.3",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"es6-promise": "^4.2.8",
|
"es6-promise": "^4.2.8",
|
||||||
"fomantic-ui-css": "^2.8.7",
|
"fomantic-ui-css": "^2.9.0",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
|
"vue-chartjs": "^5.0.1",
|
||||||
"vue-router": "^3.2.0",
|
"vue-router": "^3.2.0",
|
||||||
|
"vuedraggable": "^2.24.3",
|
||||||
"vuex": "^3.4.0"
|
"vuex": "^3.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@vue/cli-plugin-babel": "^5.0.8",
|
||||||
"@vue/cli-plugin-router": "~4.5.0",
|
"@vue/cli-plugin-router": "^5.0.8",
|
||||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
"@vue/cli-plugin-vuex": "^5.0.8",
|
||||||
"@vue/cli-service": "~4.5.0",
|
"@vue/cli-service": "^5.0.8",
|
||||||
"node-sass": "^4.12.0",
|
|
||||||
"sass-loader": "^8.0.2",
|
|
||||||
"vue-template-compiler": "^2.6.11"
|
"vue-template-compiler": "^2.6.11"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
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 |
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 |
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 |
@@ -15,12 +15,17 @@
|
|||||||
<!-- <title><%= htmlWebpackPlugin.options.title %></title> -->
|
<!-- <title><%= htmlWebpackPlugin.options.title %></title> -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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">
|
<div id="app">
|
||||||
<!-- placeholder data for scrapers with no JS -->
|
<!-- placeholder data for scrapers with no JS -->
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-color: #212221;
|
background-color: #212221;
|
||||||
color: #aeaeae;
|
color: #aeaeae;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.centered {
|
.centered {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@@ -200,6 +200,11 @@ export default {
|
|||||||
this.blockUntilNextRequest = true
|
this.blockUntilNextRequest = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//Track users active sessions
|
||||||
|
this.$io.on('update_active_user_count', countData => {
|
||||||
|
this.$store.commit('setActiveSessions', countData)
|
||||||
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
loggedIn () {
|
loggedIn () {
|
||||||
|
@@ -53,7 +53,7 @@ helpers.timeAgo = (time) => {
|
|||||||
if (typeof format[2] == 'string') {
|
if (typeof format[2] == 'string') {
|
||||||
return format[list_choice]
|
return format[list_choice]
|
||||||
} else {
|
} else {
|
||||||
return Math.floor(seconds / format[2]) + ' ' + format[1]// + ' ' + token
|
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Roboto'), local('Roboto-Regular'), url(/api/static/assets/roboto-latin.woff2) format('woff2');
|
src: local('Roboto'), local('Roboto-Regular'), url(./roboto-latin.woff2) format('woff2');
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
/* latin */
|
/* latin */
|
||||||
@@ -11,10 +11,21 @@
|
|||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: local('Roboto Bold'), local('Roboto-Bold'), url(/api/static/assets/roboto-latin-bold.woff2) format('woff2');
|
src: local('Roboto Bold'), local('Roboto-Bold'), url(./roboto-latin-bold.woff2) format('woff2');
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
/* overflow-x: hidden;*/
|
||||||
|
min-width: 320px;
|
||||||
|
background: green;
|
||||||
|
font-family: 'Roboto', system-ui, -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4285em;
|
||||||
|
color: rgba(0, 0, 0, 0.87);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|
||||||
@@ -43,7 +54,7 @@ html {
|
|||||||
height:100%;
|
height:100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: none;
|
background: var(--body_bg_color);
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@@ -85,7 +96,7 @@ body {
|
|||||||
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
#app {
|
#app {
|
||||||
background: var(--body_bg_color);
|
/* background: var(--body_bg_color);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.segment {
|
.ui.segment {
|
||||||
@@ -136,6 +147,9 @@ body {
|
|||||||
.ui.dividing.header {
|
.ui.dividing.header {
|
||||||
border-bottom-color: var(--dark_border_color);
|
border-bottom-color: var(--dark_border_color);
|
||||||
}
|
}
|
||||||
|
.ui.dividing.header > .sub.header {
|
||||||
|
color: var(--dark_border_color);
|
||||||
|
}
|
||||||
.ui.icon.input > i.icon {
|
.ui.icon.input > i.icon {
|
||||||
color: var(--text_color);
|
color: var(--text_color);
|
||||||
}
|
}
|
||||||
@@ -164,12 +178,21 @@ div.ui.basic.green.label {
|
|||||||
border-color: var(--dark_border_color) !important;
|
border-color: var(--dark_border_color) !important;
|
||||||
}
|
}
|
||||||
/*Overwrites for modifiable theme color */
|
/*Overwrites for modifiable theme color */
|
||||||
i.green.icon.icon.icon.icon {
|
i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||||
color: var(--main-accent);
|
color: var(--main-accent);
|
||||||
}
|
}
|
||||||
.button {
|
.button {
|
||||||
box-shadow: 2px 2px 4px -2px rgba(40, 40, 40, 0.89) !important;
|
box-shadow: 2px 2px 4px -2px rgba(40, 40, 40, 0.89) !important;
|
||||||
|
transition: all 0.9s ease;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
.button:hover {
|
||||||
|
box-shadow: 3px 2px 3px -2px rgba(40, 40, 40, 0.95) !important;
|
||||||
|
}
|
||||||
|
.button:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
.ui.green.buttons, .ui.green.button, .ui.green.button:hover {
|
.ui.green.buttons, .ui.green.button, .ui.green.button:hover {
|
||||||
background-color: var(--main-accent);
|
background-color: var(--main-accent);
|
||||||
}
|
}
|
||||||
@@ -290,7 +313,7 @@ i.green.icon.icon.icon.icon {
|
|||||||
border: none;
|
border: none;
|
||||||
/*height: calc(100% - 69px);*/
|
/*height: calc(100% - 69px);*/
|
||||||
|
|
||||||
min-height: 500px;
|
min-height: 300px;
|
||||||
background-color: var(--small_element_bg_color);
|
background-color: var(--small_element_bg_color);
|
||||||
/*margin-bottom: 15px;*/
|
/*margin-bottom: 15px;*/
|
||||||
|
|
||||||
@@ -309,6 +332,9 @@ i.green.icon.icon.icon.icon {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
max-width: 1100px;
|
max-width: 1100px;
|
||||||
|
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
||||||
|
|
||||||
}
|
}
|
||||||
.squire-box::selection,
|
.squire-box::selection,
|
||||||
.squire-box::-moz-selection {
|
.squire-box::-moz-selection {
|
||||||
@@ -330,9 +356,14 @@ i.green.icon.icon.icon.icon {
|
|||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-card-text code,
|
||||||
|
.squire-box code,
|
||||||
.note-card-text pre,
|
.note-card-text pre,
|
||||||
.squire-box pre {
|
.squire-box pre {
|
||||||
/*word-wrap: break-word;*/
|
/*word-wrap: break-word;*/
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 2px solid var(--main-accent);
|
||||||
|
padding-left: 15px;
|
||||||
}
|
}
|
||||||
.note-card-text p,
|
.note-card-text p,
|
||||||
.squire-box p {
|
.squire-box p {
|
||||||
@@ -367,8 +398,37 @@ i.green.icon.icon.icon.icon {
|
|||||||
.squire-box ol,
|
.squire-box ol,
|
||||||
.note-card-text ul,
|
.note-card-text ul,
|
||||||
.squire-box ul {
|
.squire-box ul {
|
||||||
margin: 8px 0 0 0;
|
margin: 3px 0;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
/* Add border 1 indent level */
|
||||||
|
.note-card-text > ol > ol,
|
||||||
|
.squire-box > ol > ol,
|
||||||
|
.note-card-text > ul > ul,
|
||||||
|
.squire-box > ul > ul
|
||||||
|
{
|
||||||
|
border-left: 1px solid var(--border_color);
|
||||||
|
}
|
||||||
|
.note-card-text ol > ol,
|
||||||
|
.squire-box ol > ol,
|
||||||
|
.note-card-text ul > ul,
|
||||||
|
.squire-box ul > ul {
|
||||||
|
list-style-type: upper-alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ol {
|
||||||
|
counter-reset: item;
|
||||||
|
}
|
||||||
|
ol li {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
ol li:before {
|
||||||
|
content: counters(item, ".") ".";
|
||||||
|
counter-increment: item;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.note-card-text ul > li,
|
.note-card-text ul > li,
|
||||||
.squire-box ul > li {
|
.squire-box ul > li {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -495,10 +555,6 @@ i.green.icon.icon.icon.icon {
|
|||||||
/* adjust checkboxes for mobile. Make them a little bigger, easier to click */
|
/* adjust checkboxes for mobile. Make them a little bigger, easier to click */
|
||||||
@media only screen and (max-width: 740px) {
|
@media only screen and (max-width: 740px) {
|
||||||
|
|
||||||
.squire-box {
|
|
||||||
min-height: calc(100vh - 122px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui.button.shrinking {
|
.ui.button.shrinking {
|
||||||
font-size: 0.85714286rem;
|
font-size: 0.85714286rem;
|
||||||
margin: 0 3px;
|
margin: 0 3px;
|
||||||
@@ -536,6 +592,15 @@ i.green.icon.icon.icon.icon {
|
|||||||
color: var(--main-accent);
|
color: var(--main-accent);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Remove indent line on mobile */
|
||||||
|
.note-card-text > ol > ol,
|
||||||
|
.squire-box > ol > ol,
|
||||||
|
.note-card-text > ul > ul,
|
||||||
|
.squire-box > ul > ul
|
||||||
|
{
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -880,6 +945,14 @@ i.green.icon.icon.icon.icon {
|
|||||||
-webkit-transform-origin: left center;
|
-webkit-transform-origin: left center;
|
||||||
transform-origin: left center;
|
transform-origin: left center;
|
||||||
}
|
}
|
||||||
|
@media only screen and (max-width: 740px) {
|
||||||
|
/*hide tooltips on mobile*/
|
||||||
|
[data-tooltip]:hover:before,
|
||||||
|
[data-tooltip]:hover:after {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.glint:after {
|
.glint:after {
|
||||||
@@ -918,3 +991,13 @@ i.green.icon.icon.icon.icon {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shade {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: rgba(0,0,0,0.7);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
@@ -2117,7 +2117,7 @@ var cleanTree = function cleanTree ( node, config, preserveWS ) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data = data.replace( /^[ \t\r\n]+/g, sibling ? ' ' : '' );
|
data = data.replace( /^[ \r\n]+/g, sibling ? ' ' : '' );
|
||||||
}
|
}
|
||||||
if ( endsWithWS ) {
|
if ( endsWithWS ) {
|
||||||
walker.currentNode = child;
|
walker.currentNode = child;
|
||||||
@@ -2132,7 +2132,7 @@ var cleanTree = function cleanTree ( node, config, preserveWS ) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data = data.replace( /[ \t\r\n]+$/g, sibling ? ' ' : '' );
|
data = data.replace( /[ \r\n]+$/g, sibling ? ' ' : '' );
|
||||||
}
|
}
|
||||||
if ( data ) {
|
if ( data ) {
|
||||||
child.data = data;
|
child.data = data;
|
||||||
@@ -2693,7 +2693,8 @@ var sanitizeToDOMFragment = function ( html, isPaste, self ) {
|
|||||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||||
WHOLE_DOCUMENT: false,
|
WHOLE_DOCUMENT: false,
|
||||||
RETURN_DOM: true,
|
RETURN_DOM: true,
|
||||||
RETURN_DOM_FRAGMENT: true
|
RETURN_DOM_FRAGMENT: true,
|
||||||
|
FORCE_BODY: false
|
||||||
}) : null;
|
}) : null;
|
||||||
return frag ? doc.importNode( frag, true ) : doc.createDocumentFragment();
|
return frag ? doc.importNode( frag, true ) : doc.createDocumentFragment();
|
||||||
};
|
};
|
||||||
|
@@ -20,7 +20,7 @@
|
|||||||
.image-placeholder {
|
.image-placeholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 100px;
|
max-height: 75px;
|
||||||
}
|
}
|
||||||
.image-placeholder:after {
|
.image-placeholder:after {
|
||||||
content: 'No Image';
|
content: 'No Image';
|
||||||
@@ -89,7 +89,14 @@
|
|||||||
<!-- image and text -->
|
<!-- image and text -->
|
||||||
<div class="six wide center aligned middle aligned column">
|
<div class="six wide center aligned middle aligned column">
|
||||||
<a :href="linkUrl" target="_blank" >
|
<a :href="linkUrl" target="_blank" >
|
||||||
<img v-if="item.file_location" class="attachment-image" :src="`/api/static/thumb_${item.file_location}`">
|
<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>
|
<span v-else>
|
||||||
<img class="image-placeholder" loading="lazy" src="/api/static/assets/marketing/void.svg">
|
<img class="image-placeholder" loading="lazy" src="/api/static/assets/marketing/void.svg">
|
||||||
No Image
|
No Image
|
||||||
@@ -110,11 +117,16 @@
|
|||||||
<a class="link" :href="linkUrl" target="_blank">{{linkText}}</a>
|
<a class="link" :href="linkUrl" target="_blank">{{linkText}}</a>
|
||||||
|
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<div class="ui small compact basic button" v-on:click="openNote">
|
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openNote">
|
||||||
<i class="file outline icon"></i>
|
<i class="file outline icon"></i>
|
||||||
Open Note
|
Open Note
|
||||||
</div>
|
</div>
|
||||||
<div class="ui small compact basic button" v-on:click="openEditAttachments"
|
<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 }">
|
:class="{ 'disabled':this.searchParams.noteId }">
|
||||||
<i class="folder open outline icon"></i>
|
<i class="folder open outline icon"></i>
|
||||||
Note Files
|
Note Files
|
||||||
@@ -171,6 +183,9 @@
|
|||||||
this.checkKeyup()
|
this.checkKeyup()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
updated: function(){
|
||||||
|
this.checkKeyup()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
checkKeyup(){
|
checkKeyup(){
|
||||||
let elm = this.$refs.edit
|
let elm = this.$refs.edit
|
||||||
|
@@ -1,45 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
|
|
||||||
<div :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
|
<div>
|
||||||
<div class="ui basic segment">
|
|
||||||
<div class="ui grid">
|
<div class="ui grid">
|
||||||
|
|
||||||
<div class="ui sixteen wide center aligned column">
|
<div class="ui sixteen wide column">
|
||||||
<div class="ui fluid button" v-on:click="clearStyles">
|
<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>
|
<i class="refresh icon"></i>
|
||||||
Clear All Styles
|
Reset
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="sixteen wide column rounded" :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
|
||||||
<div class="sixteen wide column">
|
<div class="ui dividing header" :style="{ 'color':allStyles['noteText']}">
|
||||||
<br>
|
<i class="fill drip icon"></i>
|
||||||
<p>Note Color</p>
|
Background Color
|
||||||
|
</div>
|
||||||
<div v-for="color in colors"
|
<div v-for="color in colors"
|
||||||
class="color-button"
|
class="color-button"
|
||||||
:style="{ backgroundColor:color }"
|
:style="{ backgroundColor:color }"
|
||||||
v-on:click="chosenColor(color)"
|
v-on:click="chosenColor(color)"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="sixteen wide column">
|
<div class="sixteen wide column">
|
||||||
<p>Note Icon
|
<div class="ui dividing header">
|
||||||
<span v-if="allStyles.noteIcon" >
|
<span v-if="allStyles.noteIcon" >
|
||||||
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
Note Icon
|
||||||
<div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
|
|
||||||
<i :class="`large ${icon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
|
||||||
|
<i :class="`large ${icon} icon`"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="sixteen wide column">
|
<div class="sixteen wide column">
|
||||||
<p>Icon Color</p>
|
<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()"
|
<div v-for="color in getReducedColors()"
|
||||||
class="color-button"
|
class="color-button"
|
||||||
:style="{ backgroundColor:color }"
|
:style="{ backgroundColor:color }"
|
||||||
@@ -47,8 +53,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -147,20 +152,24 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style type="text/css" scoped>
|
<style type="text/css" scoped>
|
||||||
.icon-button {
|
.icon-button, .color-button {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: calc(10% - 7px);
|
width: calc(15% - 1px);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.3em;
|
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 {
|
.color-button {
|
||||||
display: inline-block;
|
width: calc(10% - 4px);
|
||||||
width: calc(10% - 7px);
|
}
|
||||||
height: 30px;
|
.rounded {
|
||||||
border-radius: 30px;
|
border-radius: 5px;
|
||||||
box-shadow: 0px 1px 3px 0px #3e3e3e;
|
|
||||||
margin: 7px 7px 0 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@@ -19,7 +19,7 @@
|
|||||||
padding: 1em 5px;
|
padding: 1em 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.popup-row > span {
|
.popup-row > p {
|
||||||
/*width: calc(100% - 50px);*/
|
/*width: calc(100% - 50px);*/
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -85,6 +85,18 @@
|
|||||||
animation: progressBar 3s linear;
|
animation: progressBar 3s linear;
|
||||||
animation-fill-mode: both;
|
animation-fill-mode: both;
|
||||||
}
|
}
|
||||||
|
.time-display {
|
||||||
|
display: inline-block;
|
||||||
|
width: calc(100% - 25px);
|
||||||
|
/*text-align: right;*/
|
||||||
|
color: white;
|
||||||
|
font-size: 0.7em;
|
||||||
|
margin: 0 0 0 25px;
|
||||||
|
}
|
||||||
|
.text-display {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes progressBar {
|
@keyframes progressBar {
|
||||||
0% { width: 0; }
|
0% { width: 0; }
|
||||||
@@ -101,7 +113,11 @@
|
|||||||
<div class="meter">
|
<div class="meter">
|
||||||
<span><span class="progress"></span></span>
|
<span><span class="progress"></span></span>
|
||||||
</div>
|
</div>
|
||||||
<span><i class="small info circle icon"></i>{{ item }}</span>
|
<p class="text-display">
|
||||||
|
<i class="small info circle icon"></i>
|
||||||
|
{{ item.text }}
|
||||||
|
<span class="time-display">{{ item.time }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -119,8 +135,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount(){
|
beforeMount(){
|
||||||
this.$bus.$on('notification', info => {
|
this.$bus.$on('notification', notificationText => {
|
||||||
this.displayNotification(info)
|
this.displayNotification(notificationText)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
mounted(){
|
mounted(){
|
||||||
@@ -131,8 +147,17 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
displayNotification(newNotification){
|
displayNotification(notificationText){
|
||||||
this.notifications.push(newNotification)
|
|
||||||
|
const date = new Date()
|
||||||
|
const time = date.toLocaleTimeString()
|
||||||
|
|
||||||
|
const notification = {
|
||||||
|
text: notificationText,
|
||||||
|
time: time
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifications.unshift(notification)
|
||||||
clearTimeout(this.totalTimeout)
|
clearTimeout(this.totalTimeout)
|
||||||
this.totalTimeout = setTimeout(() => {
|
this.totalTimeout = setTimeout(() => {
|
||||||
this.dismiss()
|
this.dismiss()
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.slotholder {
|
.slotholder {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 155px;
|
width: 180px;
|
||||||
display: block;
|
display: block;
|
||||||
float: left;
|
float: left;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.global-menu {
|
.global-menu {
|
||||||
width: 155px;
|
width: 180px;
|
||||||
/* background: #221f2b; */
|
/* background: #221f2b; */
|
||||||
background: #221f2b;
|
background: #221f2b;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -14,14 +15,14 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: block;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 111;
|
z-index: 900;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
.menu-logo-display {
|
.menu-logo-display {
|
||||||
width: 27px;
|
width: 27px;
|
||||||
margin: 5px 0 0 41px;
|
margin: 5px 0 0 55px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
@@ -54,9 +55,6 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-link-active i {
|
|
||||||
/*color: #16ab39;*/
|
|
||||||
}
|
|
||||||
.router-link-active {
|
.router-link-active {
|
||||||
background-color: #534c68;
|
background-color: #534c68;
|
||||||
}
|
}
|
||||||
@@ -68,7 +66,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(0,0,0,0.7);
|
background-color: rgba(0,0,0,0.7);
|
||||||
z-index: 100;
|
z-index: 899;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.top-menu-bar {
|
.top-menu-bar {
|
||||||
@@ -89,6 +87,7 @@
|
|||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.place-holder {
|
.place-holder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -109,6 +108,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: #8c80ae;
|
color: #8c80ae;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
background-color: var(--menu-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-button {
|
.mobile-button {
|
||||||
@@ -141,6 +141,17 @@
|
|||||||
.mobile-button.active {
|
.mobile-button.active {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
.single-line-text {
|
||||||
|
width: calc(100%);
|
||||||
|
/*margin: 5px 10px;*/
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.faded {
|
||||||
|
color: var(--dark_border_color);
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -170,7 +181,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- open straight to note -->
|
<!-- open straight to note -->
|
||||||
<router-link
|
<router-link
|
||||||
v-if="loggedIn && $store.getters.totals && $store.getters.totals['quickNote']"
|
v-if="loggedIn && $store.getters.totals && $store.getters.totals['quickNote']"
|
||||||
@@ -198,8 +208,8 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<!-- menu -->
|
<!-- menu -->
|
||||||
<div class="mobile-button">
|
<div class="mobile-button" v-on:click="collapseMenu">
|
||||||
<i class="green link bars icon" v-on:click="collapseMenu"></i>
|
<i class="green link bars icon" ></i>
|
||||||
Menu
|
Menu
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -220,12 +230,12 @@
|
|||||||
|
|
||||||
<div class="menu-section" v-if="loggedIn">
|
<div class="menu-section" v-if="loggedIn">
|
||||||
<div v-if="!disableNewNote" @click="createNote" class="menu-item menu-item menu-button">
|
<div v-if="!disableNewNote" @click="createNote" class="menu-item menu-item menu-button">
|
||||||
<div class="ui green button">
|
<div class="ui green fluid compact button">
|
||||||
<i class="plus icon"></i>New Note
|
<i class="plus icon"></i>New Note
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="disableNewNote" class="menu-item menu-item menu-button">
|
<div v-if="disableNewNote" class="menu-item menu-item menu-button">
|
||||||
<div class="ui basic button">
|
<div class="ui basic fluid compact button">
|
||||||
<i class="plus loading icon"></i>New Note
|
<i class="plus loading icon"></i>New Note
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,14 +248,19 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
<div>
|
<div>
|
||||||
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(3)" v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)">
|
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(3)" v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)">
|
||||||
<i class="grey mail outline icon"></i>Inbox
|
<i class="grey paper plane outline icon"></i>Shared
|
||||||
|
|
||||||
|
<counter v-if="$store.getters.totals && $store.getters.totals['sharedToNotes']" class="float-right" number-id="sharedToNotes" />
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
|
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
|
||||||
<i class="grey archive icon"></i>Archived
|
<i class="grey archive icon"></i>Archived
|
||||||
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
|
|
||||||
|
<counter v-if="$store.getters.totals && $store.getters.totals['archivedNotes']" class="float-right" number-id="archivedNotes" />
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['trashedNotes'] > 0">
|
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['trashedNotes'] > 0">
|
||||||
<i class="grey trash alternate outline icon"></i>Trashed
|
<i class="grey trash alternate outline icon"></i>Trashed
|
||||||
|
|
||||||
|
<counter v-if="$store.getters.totals && $store.getters.totals['trashedNotes']" class="float-right" number-id="trashedNotes" />
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="menu-item sub">Show Only <i class="caret down icon"></i></div> -->
|
<!-- <div class="menu-item sub">Show Only <i class="caret down icon"></i></div> -->
|
||||||
<!-- <div v-on:click="updateFastFilters(0)" class="menu-item menu-button sub"><i class="grey linkify icon"></i>Links</div> -->
|
<!-- <div v-on:click="updateFastFilters(0)" class="menu-item menu-button sub"><i class="grey linkify icon"></i>Links</div> -->
|
||||||
@@ -302,24 +317,49 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-section">
|
|
||||||
<router-link class="menu-item menu-button" exact-active-class="active" to="/help">
|
|
||||||
<i class="question circle outline icon"></i>Help
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="menu-section" v-if="loggedIn">
|
<div class="menu-section" v-if="loggedIn">
|
||||||
<router-link class="menu-item menu-button" exact-active-class="active" to="/settings">
|
<router-link class="menu-item menu-button" exact-active-class="active" to="/settings">
|
||||||
<i class="cog icon"></i>Settings
|
<i class="cog icon"></i>Settings
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-section" v-if="loggedIn">
|
||||||
|
<router-link class="menu-item menu-button" exact-active-class="active" to="/metrictrack">
|
||||||
|
<i class="calendar check outlin icon"></i>Metric Track
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-section">
|
||||||
|
<router-link class="menu-item menu-button" exact-active-class="active" to="/help">
|
||||||
|
<i class="question circle outline icon"></i>Help
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="menu-section" v-if="loggedIn">
|
<div class="menu-section" v-if="loggedIn">
|
||||||
<div class="menu-item menu-button" v-on:click="logout()">
|
<div class="menu-item menu-button" v-on:click="logout()">
|
||||||
<i class="log out icon"></i>Log Out
|
<i class="log out icon"></i>Log Out
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tags -->
|
||||||
|
<div class="menu-section" v-if="gotTags()">
|
||||||
|
<div class="menu-item">
|
||||||
|
<i class="green tags icon"></i>
|
||||||
|
Tags
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="gotTags()">
|
||||||
|
<div class="menu-section"
|
||||||
|
v-for="(data, tag) in $store.getters.totals['tags']">
|
||||||
|
<router-link class="menu-item menu-button" :to="`/search/tags/${tag}`">
|
||||||
|
<span class="single-line-text">
|
||||||
|
<!-- <i class="small grey tag icon"></i> -->
|
||||||
|
<span class="float-right">{{ data.uses }}</span>
|
||||||
|
<span class="faded"> #</span> {{ tag }}</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-on:click="reloadPage" class="version-display" v-if="version != 0" >
|
<div v-on:click="reloadPage" class="version-display" v-if="version != 0" >
|
||||||
<i :class="`${getVersionIcon()} icon`"></i> {{ version }}
|
<i :class="`${getVersionIcon()} icon`"></i> {{ version }}
|
||||||
</div>
|
</div>
|
||||||
@@ -379,6 +419,16 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
gotTags(){
|
||||||
|
|
||||||
|
if(this.loggedIn && this.$store.getters.totals && this.$store.getters.totals.tags
|
||||||
|
&& Object.keys(this.$store.getters.totals.tags).length
|
||||||
|
){
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
logout() {
|
logout() {
|
||||||
|
|
||||||
this.$router.push('/')
|
this.$router.push('/')
|
||||||
@@ -477,8 +527,11 @@
|
|||||||
location.reload(true)
|
location.reload(true)
|
||||||
},
|
},
|
||||||
getVersionIcon(){
|
getVersionIcon(){
|
||||||
|
if(!this.version){
|
||||||
|
return 'radiation alternate'
|
||||||
|
}
|
||||||
const icons = ['cat','crow','dog','dove','dragon','fish','frog','hippo','horse','kiwi bird','otter','spider', 'smile', 'robot', 'hat wizard', 'microchip', 'atom', 'grin tongue squint', 'radiation', 'ghost', 'dna', 'burn', 'brain', 'moon', 'torii gate']
|
const icons = ['cat','crow','dog','dove','dragon','fish','frog','hippo','horse','kiwi bird','otter','spider', 'smile', 'robot', 'hat wizard', 'microchip', 'atom', 'grin tongue squint', 'radiation', 'ghost', 'dna', 'burn', 'brain', 'moon', 'torii gate']
|
||||||
const index = ( parseInt(this.version.replace(/\./g,'')) % (icons.length))
|
const index = ( parseInt(String(this.version).replace(/\./g,'')) % (icons.length))
|
||||||
return icons[index]
|
return icons[index]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -39,9 +39,9 @@
|
|||||||
.loading-container {
|
.loading-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100px;
|
/*min-height: 100px;*/
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
padding: 40px;
|
/*padding: 40px;*/
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
background-color: var(--small_element_bg_color);
|
background-color: var(--small_element_bg_color);
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div v-on:keyup.enter="login()">
|
<div>
|
||||||
|
|
||||||
<!-- thicc form display -->
|
<!-- thicc form display -->
|
||||||
<div v-if="!thin" class="ui large form">
|
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui input">
|
<div class="ui input">
|
||||||
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
|
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
|
||||||
@@ -15,6 +15,11 @@
|
|||||||
<input v-model="password" type="password" name="password" placeholder="Password">
|
<input v-model="password" type="password" name="password" placeholder="Password">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui input">
|
||||||
|
<input v-model="password2" type="password" name="password2" placeholder="Re-type Password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field" v-if="require2FA">
|
<div class="field" v-if="require2FA">
|
||||||
<div class="ui input">
|
<div class="ui input">
|
||||||
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
|
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
|
||||||
@@ -24,18 +29,11 @@
|
|||||||
<div class="ui fluid buttons">
|
<div class="ui fluid buttons">
|
||||||
|
|
||||||
|
|
||||||
<div v-on:click="register()" class="ui button">
|
<div v-on:click="register" class="ui green button" :class="{ 'disabled':(username.length == 0 || password.length == 0)}">
|
||||||
<i class="plug icon"></i>
|
<i class="plug icon"></i>
|
||||||
Sign Up
|
Sign Up
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="or"></div>
|
|
||||||
|
|
||||||
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="login()" class="ui green button">
|
|
||||||
<i class="power icon"></i>
|
|
||||||
Login
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sixteen wide column">
|
<div class="sixteen wide column">
|
||||||
@@ -49,12 +47,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Thin form display -->
|
<!-- Thin form display -->
|
||||||
<div v-if="thin" class="ui small form">
|
<div v-if="thin" class="ui small form" v-on:keyup.enter="login">
|
||||||
|
|
||||||
<div v-if="!require2FA" class="field"><!-- hide this field if someone is logging in with 2FA -->
|
<div v-if="!require2FA" class="field"><!-- hide this field if someone is logging in with 2FA -->
|
||||||
<div class="ui grid">
|
<div class="ui grid">
|
||||||
<div class="ui sixteen wide center aligned column">
|
<div class="ui sixteen wide center aligned column">
|
||||||
<div v-on:click="register()" class="ui green button">
|
<div v-on:click="register" class="ui green button">
|
||||||
<i class="plug icon"></i>
|
<i class="plug icon"></i>
|
||||||
Sign Up Now!
|
Sign Up Now!
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +85,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div v-on:click="login()" class="ui fluid button">
|
<div v-on:click="login" class="ui fluid button">
|
||||||
<i class="power icon"></i>
|
<i class="power icon"></i>
|
||||||
Login
|
Login
|
||||||
</div>
|
</div>
|
||||||
@@ -128,6 +126,7 @@
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
password2: '',
|
||||||
authToken: '',
|
authToken: '',
|
||||||
require2FA: false,
|
require2FA: false,
|
||||||
}
|
}
|
||||||
@@ -160,13 +159,21 @@
|
|||||||
},
|
},
|
||||||
register(){
|
register(){
|
||||||
|
|
||||||
if( this.username.length == 0 || this.password.length == 0 ){
|
let error = false
|
||||||
|
|
||||||
if(this.$route.name == 'LoginPage'){
|
if( this.username.length == 0 || this.password.length == 0 || this.password2.length == 0 ){
|
||||||
this.$bus.$emit('notification', 'Both a Username and Password are Required')
|
|
||||||
return
|
this.$bus.$emit('notification', 'All fields are required.')
|
||||||
|
error = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if( this.password !== this.password2 ){
|
||||||
|
|
||||||
|
this.$bus.$emit('notification', 'Passwords must be identical.')
|
||||||
|
error = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if(error){
|
||||||
//Login section
|
//Login section
|
||||||
this.$router.push('/login')
|
this.$router.push('/login')
|
||||||
return
|
return
|
||||||
@@ -228,7 +235,6 @@
|
|||||||
<style type="text/css" scoped="true">
|
<style type="text/css" scoped="true">
|
||||||
.small-terms {
|
.small-terms {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: right;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
431
client/src/components/Metrictracking/MetricGraphsComponent.vue
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
<style type="text/css" scoped>
|
||||||
|
.an-graph {
|
||||||
|
background: #fefefe;
|
||||||
|
}
|
||||||
|
.inactive.segment {
|
||||||
|
|
||||||
|
}
|
||||||
|
.active.segment {
|
||||||
|
outline: 4px solid cyan;
|
||||||
|
outline-offset: -5px;
|
||||||
|
outline-style: dashed;
|
||||||
|
max-height: 2000px;
|
||||||
|
}
|
||||||
|
.not-padded {
|
||||||
|
margin-left: -5px;
|
||||||
|
margin-right: -5px;
|
||||||
|
margin-bottom: -10px;
|
||||||
|
padding-right: 5px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
.sticky-boy {
|
||||||
|
position: fixed;
|
||||||
|
top: -1px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 100;
|
||||||
|
width: 70%;
|
||||||
|
background: orange;
|
||||||
|
}
|
||||||
|
.animate-height {
|
||||||
|
transition: max-height 0.8s linear;
|
||||||
|
max-height: 450px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="ui very compact grid" :class="{'sticky-boy':editGraphs}">
|
||||||
|
<div class="sixteen wide column" v-if="!editGraphs">
|
||||||
|
<div class="ui basic padded segment">
|
||||||
|
<!-- Just a space to keep things clickable -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sixteen wide column">
|
||||||
|
<dix class="ui basic segment" v-if="!editGraphs">
|
||||||
|
<div class="ui button" v-on:click="toggleEditGraphs">
|
||||||
|
<i class="edit icon"></i>
|
||||||
|
<span>Add/Edit Graphs</span>
|
||||||
|
</div>
|
||||||
|
</dix>
|
||||||
|
|
||||||
|
|
||||||
|
<div v-if="editGraphs">
|
||||||
|
<div class="ui green button" v-on:click="addGraph()">
|
||||||
|
<i class="plus icon"></i>
|
||||||
|
New Graph
|
||||||
|
</div>
|
||||||
|
<div class="ui basic button" v-on:click="toggleEditGraphs">
|
||||||
|
<i class="check circle icon"></i>
|
||||||
|
Done Editing Graphs
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(graph, index) in graphs" :class="`ui not-padded ${editGraphs?'active ':'inactive '}segment animate-height`">
|
||||||
|
|
||||||
|
<!-- Edit options -->
|
||||||
|
<div class="ui small header" v-if="editGraphs">
|
||||||
|
<div class="ui grid">
|
||||||
|
<div class="eight wide column">
|
||||||
|
<b>Graph #{{ index+1 }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="eight wide right aligned column">
|
||||||
|
<span class="ui tiny compact inverted red button" v-on:click="removeGraph(index)">
|
||||||
|
Remove Graph
|
||||||
|
<i class="close icon"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="ui center aligned dividing header">
|
||||||
|
{{ getGraphTitle(graph) }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div v-if="graph?.type == PILL_CALENDAR">
|
||||||
|
<PillCalendarGraph
|
||||||
|
:graph="graph"
|
||||||
|
:tempChartDays="tempChartDays"
|
||||||
|
:userFields="userFields"
|
||||||
|
:cycleData="cycleData"
|
||||||
|
:edit-graphs="editGraphs"
|
||||||
|
:showZeroValues="graph?.options?.showZeroValues"
|
||||||
|
:showTextValues="graph?.options?.showTextValues"
|
||||||
|
:connectDays="graph?.options?.connectDays"
|
||||||
|
:hideValues="graph?.options?.hideValues"
|
||||||
|
:hideIcons="graph?.options?.hideIcons"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="editGraphs" class="ui segment">
|
||||||
|
<p>Calendar Graph Toggles</p>
|
||||||
|
<div v-on:click="toggelValue(index, 'hideIcons')"class="ui button">
|
||||||
|
<span v-if="graph?.options?.hideIcons">Show</span><span v-else>Hide</span> Icons
|
||||||
|
</div>
|
||||||
|
<div v-on:click="toggelValue(index, 'hideValues')"class="ui button">
|
||||||
|
<span v-if="graph?.options?.hideValues">Show</span><span v-else>Hide</span> Values
|
||||||
|
</div>
|
||||||
|
<div v-on:click="toggelValue(index, 'showZeroValues')"class="ui button">
|
||||||
|
<span v-if="!graph?.options?.showZeroValues">Show</span><span v-else>Hide</span> Lowest Value
|
||||||
|
</div>
|
||||||
|
<div v-on:click="toggelValue(index, 'showTextValues')"class="ui button">
|
||||||
|
<span v-if="!graph?.options?.showTextValues">Show</span><span v-else>Hide</span> Text Value
|
||||||
|
</div>
|
||||||
|
<div v-on:click="toggelValue(index, 'connectDays')"class="ui button">
|
||||||
|
<span v-if="!graph?.options?.connectDays">Connect</span><span v-else>Disconnect</span> Days
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="graph?.type == LAST_DONE">
|
||||||
|
Last done not implemented
|
||||||
|
</div>
|
||||||
|
<div v-if="!graph.fieldIds || graph.fieldIds && graph.fieldIds.length == 0">
|
||||||
|
<h5>Blank Graph</h5>
|
||||||
|
<span v-if="!editGraphs">Click "Edit Graphs" then,</span>
|
||||||
|
Select Graph type and Metrics to display
|
||||||
|
</div>
|
||||||
|
<div v-if="graph?.type == undefined && graph.fieldIds && graph.fieldIds.length > 0">
|
||||||
|
<div :id="`graphdiv${index}`" style="width: 100%; min-height: 320px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui segment" v-if="editGraphs">
|
||||||
|
|
||||||
|
<!-- change graph type -->
|
||||||
|
<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))}">
|
||||||
|
{{ graphType }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="fieldId in fields">
|
||||||
|
<span v-if="graph.fieldIds && graph.fieldIds.includes(fieldId)" v-on:click="toggleGraphField(fieldId, index)">
|
||||||
|
<i class="green check square icon"></i>
|
||||||
|
</span>
|
||||||
|
<span v-else v-on:click="toggleGraphField(fieldId, index)">
|
||||||
|
<i class="square outline icon"></i>
|
||||||
|
</span>
|
||||||
|
<i :class="`${$parent.getFieldColor(fieldId)} ${$parent.getFieldIcon(fieldId)} icon`"></i>
|
||||||
|
<b>{{ userFields[fieldId]?.label }}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="ui very compact grid" :class="{'sticky-boy':editGraphs}">
|
||||||
|
<div class="sixteen wide column" v-if="!editGraphs">
|
||||||
|
<div class="ui basic padded segment">
|
||||||
|
<!-- Just a space to keep things clickable -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sixteen wide column">
|
||||||
|
<dix class="ui basic segment" v-if="!editGraphs">
|
||||||
|
<div class="ui button" v-on:click="toggleEditGraphs">
|
||||||
|
<i class="edit icon"></i>
|
||||||
|
<span>Add/Edit Graphs</span>
|
||||||
|
</div>
|
||||||
|
</dix>
|
||||||
|
|
||||||
|
|
||||||
|
<div v-if="editGraphs">
|
||||||
|
<div class="ui green button" v-on:click="addGraph()">
|
||||||
|
<i class="plus icon"></i>
|
||||||
|
New Graph
|
||||||
|
</div>
|
||||||
|
<div class="ui basic button" v-on:click="toggleEditGraphs">
|
||||||
|
<i class="check circle icon"></i>
|
||||||
|
Done Editing Graphs
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Anchor for scrolling to the bottom of graphs -->
|
||||||
|
<div ref="anchor"></div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const PILL_CALENDAR = 'pillCalendar'
|
||||||
|
const LAST_DONE = 'lastDone'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MetricTrackingGraphs',
|
||||||
|
props: [
|
||||||
|
'tempChartDays', // Number of days to display
|
||||||
|
'fields', // field IDs for display/order
|
||||||
|
'userFields', // field values defined by user
|
||||||
|
'graphs', // Graph data defined by user
|
||||||
|
'cycleData', // ALL user data
|
||||||
|
'calendar', // Date data for currently open day
|
||||||
|
'editGraphs' // boolean for edit or not edit graphs
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
'PillCalendarGraph':require('@/components/Metrictracking/PillCalendarGraph.vue').default,
|
||||||
|
},
|
||||||
|
data: function(){
|
||||||
|
return {
|
||||||
|
graphTypesDef:{
|
||||||
|
// [LAST_DONE]: 'Last Done',
|
||||||
|
'undefined':'Line Graph (Default)',
|
||||||
|
[PILL_CALENDAR]:'Calendar Graph',
|
||||||
|
},
|
||||||
|
localGraphData:[],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeCreate() {
|
||||||
|
// Constants
|
||||||
|
this.PILL_CALENDAR = PILL_CALENDAR
|
||||||
|
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(){
|
||||||
|
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: {
|
||||||
|
saveGraphs(){
|
||||||
|
this.$emit('saveGraphs', this.localGraphData)
|
||||||
|
},
|
||||||
|
toggleEditGraphs(){
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// scroll last graph into view
|
||||||
|
this.$refs.anchor.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
inline: 'center'
|
||||||
|
})
|
||||||
|
}, 800)
|
||||||
|
|
||||||
|
this.$emit('toggleEditGraphs')
|
||||||
|
},
|
||||||
|
changeGraphType(index, newType){
|
||||||
|
console.log(index + ' change to ' + newType)
|
||||||
|
this.localGraphData[index]['type'] = newType
|
||||||
|
this.saveGraphs()
|
||||||
|
},
|
||||||
|
addGraph(){
|
||||||
|
this.localGraphData.push({})
|
||||||
|
this.saveGraphs()
|
||||||
|
},
|
||||||
|
removeGraph(index){
|
||||||
|
this.localGraphData.splice(index, 1)
|
||||||
|
this.saveGraphs()
|
||||||
|
},
|
||||||
|
toggelValue(graphIndex, optionName){
|
||||||
|
|
||||||
|
if(!this.localGraphData[graphIndex].options){
|
||||||
|
this.localGraphData[graphIndex].options = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.localGraphData[graphIndex].options[optionName]){
|
||||||
|
this.localGraphData[graphIndex].options[optionName] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
this.localGraphData[graphIndex].options[optionName] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(this.localGraphData[graphIndex].options[optionName])
|
||||||
|
|
||||||
|
this.saveGraphs()
|
||||||
|
|
||||||
|
},
|
||||||
|
toggleGraphField(fieldId, graphIndex){
|
||||||
|
|
||||||
|
if(!Array.isArray(this.localGraphData[graphIndex].fieldIds)){
|
||||||
|
this.localGraphData[graphIndex].fieldIds = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const inSetCheck = this.localGraphData[graphIndex]?.fieldIds.indexOf(fieldId)
|
||||||
|
|
||||||
|
if(inSetCheck == -1){
|
||||||
|
this.localGraphData[graphIndex]?.fieldIds.push(fieldId)
|
||||||
|
}
|
||||||
|
if(inSetCheck > -1){
|
||||||
|
this.localGraphData[graphIndex]?.fieldIds.splice(inSetCheck,1)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveGraphs()
|
||||||
|
|
||||||
|
},
|
||||||
|
getGraphTitle(graph){
|
||||||
|
|
||||||
|
const graphFields = graph?.fieldIds || []
|
||||||
|
let fieldTitles = []
|
||||||
|
graphFields.forEach(fieldId => {
|
||||||
|
fieldTitles.push(this.userFields[fieldId]?.label)
|
||||||
|
})
|
||||||
|
|
||||||
|
// console.log(fieldTitles)
|
||||||
|
const title = fieldTitles.join(', ')
|
||||||
|
|
||||||
|
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>
|
548
client/src/components/Metrictracking/PillCalendarGraph.vue
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
<style type="text/css" scoped>
|
||||||
|
div.calendar {
|
||||||
|
width: calc(100% - 4px);
|
||||||
|
min-height: 350px;
|
||||||
|
display: flex;
|
||||||
|
margin: 5px 8px 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.day {
|
||||||
|
flex: 0 0 calc(14.28% - 2px);
|
||||||
|
min-height: 50px;
|
||||||
|
border: 1px solid var(--border_color);
|
||||||
|
font-size: 1.2em;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
line-height: 1em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.today {
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.active-entry {
|
||||||
|
outline: #07f4f4;
|
||||||
|
outline-style: none;
|
||||||
|
outline-width: medium;
|
||||||
|
outline-style: none;
|
||||||
|
outline-offset: -1px;
|
||||||
|
outline-style: solid;
|
||||||
|
outline-width: 3px;
|
||||||
|
}
|
||||||
|
.day ~ .has-data {
|
||||||
|
|
||||||
|
}
|
||||||
|
.day ~ .no-data {
|
||||||
|
background: #c7c7c787;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.day > .number {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 5px;
|
||||||
|
z-index: 10;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
.day > .sex {
|
||||||
|
font-size: 0.7em;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: rgba(249, 0, 0, 0.15);
|
||||||
|
color: white;
|
||||||
|
padding: 0 0 0 4px;
|
||||||
|
z-index: 10;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
.day > .period {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1px;
|
||||||
|
left: 1px;
|
||||||
|
right: 1px;
|
||||||
|
height: 5px;
|
||||||
|
background: red;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.day > .mucus {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
min-height: 10px;
|
||||||
|
background: #abecff7d;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.day > .notes {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.pill {
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
min-height: 2px;
|
||||||
|
margin: 0 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
background: rgb(50 218 255 / 44%);
|
||||||
|
border-radius: 40px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1em;
|
||||||
|
position: relative;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.7em;
|
||||||
|
padding: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.pill.did-last {
|
||||||
|
margin-left: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
width: calc(100% - 5px);
|
||||||
|
}
|
||||||
|
.pill.did-next {
|
||||||
|
margin-right: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
width: calc(100% - 5px);
|
||||||
|
}
|
||||||
|
.pill.did-next.did-last {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
/* .last-high:after {
|
||||||
|
content: '';
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 15px solid transparent;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
border-left: 10px solid rgb(50 218 255 / 44%);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: -13px;
|
||||||
|
}
|
||||||
|
.next-high:before {
|
||||||
|
content: '';
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 15px solid transparent;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
border-right: 10px solid rgb(50 218 255 / 44%);
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: -13px;
|
||||||
|
}*/
|
||||||
|
.big-day {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 2px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.zero-day {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.icon-spacer {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: greenyellow;
|
||||||
|
width: 20px;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.past-entries {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
/* padding: 0 10px;*/
|
||||||
|
overflow-x: scroll;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
.past-entry {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: var(--dark_border_color);
|
||||||
|
color: var(--text_color);
|
||||||
|
flex-grow: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
margin: 5px 0 10px;
|
||||||
|
line-height: 2.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-list {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
background-color: green;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
overflow-x: scroll;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
.day-list-item {
|
||||||
|
flex-grow: 1;
|
||||||
|
border: 1px solid black;
|
||||||
|
width: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill.red { background-color: #db2828 }
|
||||||
|
.pill.orange { background-color: #f2711c }
|
||||||
|
.pill.yellow { background-color: #fbbd08 }
|
||||||
|
.pill.olive { background-color: #b5cc18 }
|
||||||
|
.pill.green { background-color: #21ba45 }
|
||||||
|
.pill.teal { background-color: #00b5ad }
|
||||||
|
.pill.blue { background-color: #2185d0 }
|
||||||
|
.pill.violet { background-color: #6435c9 }
|
||||||
|
.pill.purple { background-color: #a333c8 }
|
||||||
|
.pill.pink { background-color: #e03997 }
|
||||||
|
.pill.brown { background-color: #a5673f }
|
||||||
|
.pill.grey { background-color: #767676 }
|
||||||
|
.pill.black { background-color: #1b1c1d }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="calendar">
|
||||||
|
|
||||||
|
<div v-for="day in calendar.weekdays" class="day">
|
||||||
|
{{ day }}
|
||||||
|
</div>
|
||||||
|
<div v-for="day in calendar.days" class="day"
|
||||||
|
:class="{
|
||||||
|
'today':day == calendar.today,
|
||||||
|
'active-entry':calendar.dateCode == `${day}.${calendar.month}.${calendar.year}`,
|
||||||
|
'has-data':cycleData[`${day}.${calendar.month}.${calendar.year}`],
|
||||||
|
'no-data':showDayDataColor(day),
|
||||||
|
}">
|
||||||
|
<!-- v-on:click="openDayData(`${day}.${calendar.month}.${calendar.year}`)" -->
|
||||||
|
<span class="number">{{ day }}</span>
|
||||||
|
<!-- {{ `${day}.${calendar.month}.${calendar.year}` }} -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<span class="pill-container" v-for="(entry, dateCode) in getChartData" v-if="dateCode == `${day}.${calendar.month}.${calendar.year}`">
|
||||||
|
<span
|
||||||
|
v-for="(dayData, fieldId) in entry"
|
||||||
|
v-if="showZeroValuesCheck(dayData.value, fieldId)"
|
||||||
|
class="pill"
|
||||||
|
:class="[$parent.$parent.getFieldColor(fieldId), {
|
||||||
|
'did-next':dayData.didNext,
|
||||||
|
'did-last':dayData.didLast,
|
||||||
|
'last-high':dayData.lastHigh,
|
||||||
|
'next-high':dayData.nextHigh,
|
||||||
|
}]">
|
||||||
|
<!-- 'zero-day':dayData.value == lowestGraphValue, -->
|
||||||
|
<!-- <i v-if="dayData.value != 0" :class="`tiny ${$parent.$parent.getFieldColor(fieldId)} ${$parent.$parent.getFieldIcon(fieldId)} icon`"></i> -->
|
||||||
|
<!-- <span v-else class="icon-spacer"></span>
|
||||||
|
:style="{height:(Math.round(dayData.value*5)+'px')}"
|
||||||
|
|
||||||
|
-->
|
||||||
|
<span v-if="dayData.value > lowestGraphValue-1" class="big-day">
|
||||||
|
<i v-if="!hideIcons" :class="`tiny white ${$parent.$parent.getFieldIcon(fieldId)} icon`"></i>
|
||||||
|
<span v-if="!hideValues">
|
||||||
|
{{ getDayValue(fieldId, dayData.value) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<!-- <span v-for="fieldId in graph.fieldIds"></span> -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
// let chartData = {}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
'graph', // options associated with this graph
|
||||||
|
'userFields', // all field attributes
|
||||||
|
'tempChartDays', // number of days to display
|
||||||
|
'cycleData', // all users metric data
|
||||||
|
'editGraphs', // display additional edit options
|
||||||
|
// Graph options
|
||||||
|
'showZeroValues', // Hide graph data with value of zero
|
||||||
|
'showTextValues', // Show button text or button value
|
||||||
|
'connectDays', // Calculates next and previous day connections.
|
||||||
|
'hideValues', // Hide all values on the graph
|
||||||
|
'hideIcons', // option to hide icons
|
||||||
|
],
|
||||||
|
data: function(){
|
||||||
|
return {
|
||||||
|
openModel:true,
|
||||||
|
calendar: {
|
||||||
|
dateObject: null,
|
||||||
|
dateCode: null,
|
||||||
|
monthName: '',
|
||||||
|
dayName:'',
|
||||||
|
daysAgo:0,
|
||||||
|
month: '',
|
||||||
|
year: '',
|
||||||
|
days: [],
|
||||||
|
weekdays: ['S','M','T','W','T','F','S'],
|
||||||
|
today: 0,
|
||||||
|
},
|
||||||
|
chartDateCodes: [], // array of date codes in chart
|
||||||
|
listDateCodes: [],
|
||||||
|
dayList: true,
|
||||||
|
lowestGraphValue: 0,
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted(){
|
||||||
|
this.setupCalendar(new Date())
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getChartData(){
|
||||||
|
|
||||||
|
let chartData = {}
|
||||||
|
let chartValues = []
|
||||||
|
|
||||||
|
// iterate every day in month by day code
|
||||||
|
this.chartDateCodes.forEach((chartDayCode, codeIndex) => {
|
||||||
|
|
||||||
|
// lookup data for that day
|
||||||
|
const cycleDayData = this.cycleData[chartDayCode]
|
||||||
|
|
||||||
|
// if chart data is set for this day
|
||||||
|
if( cycleDayData && Object.keys(cycleDayData).length > 0){
|
||||||
|
chartData[chartDayCode] = {}
|
||||||
|
|
||||||
|
// go over each field to be displayed on graph
|
||||||
|
this.graph.fieldIds.forEach((graphFieldId) => {
|
||||||
|
|
||||||
|
if( cycleDayData[graphFieldId] == undefined ){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// track all chart values
|
||||||
|
chartValues.push(cycleDayData[graphFieldId])
|
||||||
|
|
||||||
|
chartData[chartDayCode][graphFieldId] = {
|
||||||
|
didLast: false,
|
||||||
|
lastHigh: false,
|
||||||
|
didNext: false,
|
||||||
|
nextHigh: false,
|
||||||
|
value: cycleDayData[graphFieldId]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.lowestGraphValue = Math.min(...chartValues)
|
||||||
|
|
||||||
|
// determine next and previous states for display
|
||||||
|
this.chartDateCodes.forEach((chartDayCode, codeIndex) => {
|
||||||
|
if(chartData[chartDayCode] && this.connectDays){
|
||||||
|
|
||||||
|
const previousDateCode = this.chartDateCodes[codeIndex-1]
|
||||||
|
const nextDateCode = this.chartDateCodes[codeIndex+1]
|
||||||
|
|
||||||
|
Object.keys(chartData[chartDayCode]).forEach((graphFieldId) => {
|
||||||
|
|
||||||
|
const currentValue = chartData[chartDayCode][graphFieldId].value
|
||||||
|
|
||||||
|
// check for previous entry
|
||||||
|
if( chartData[previousDateCode] && chartData[previousDateCode][graphFieldId] ){
|
||||||
|
|
||||||
|
chartData[chartDayCode][graphFieldId].didLast = true
|
||||||
|
|
||||||
|
// set low value flag
|
||||||
|
const lastHigh = chartData[previousDateCode][graphFieldId].value > 0
|
||||||
|
chartData[chartDayCode][graphFieldId].lastHigh = lastHigh && currentValue == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for next entry
|
||||||
|
if( chartData[nextDateCode] && chartData[nextDateCode][graphFieldId] ){
|
||||||
|
|
||||||
|
chartData[chartDayCode][graphFieldId].didNext = true
|
||||||
|
|
||||||
|
// set low value flag
|
||||||
|
const nextHigh = chartData[nextDateCode][graphFieldId].value > 0
|
||||||
|
chartData[chartDayCode][graphFieldId].nextHigh = nextHigh && currentValue == 0
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// console.log(chartData)
|
||||||
|
|
||||||
|
return chartData
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showZeroValuesCheck(dayValue, fieldId){
|
||||||
|
|
||||||
|
// if graph type is boolean or there are two options
|
||||||
|
let isBooleanField = this.userFields[fieldId].type == 'boolean'
|
||||||
|
if(this.userFields[fieldId].customOptions){
|
||||||
|
let options = this.userFields[fieldId].customOptions
|
||||||
|
|
||||||
|
isBooleanField = options.split(',').length == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isBooleanField && !this.showZeroValues){
|
||||||
|
|
||||||
|
const parsedValue = this.getDayValue(fieldId, dayValue)
|
||||||
|
if(parsedValue == 'Yes'){
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return this.showZeroValues || dayValue > this.lowestGraphValue
|
||||||
|
},
|
||||||
|
getDayValue(fieldId, value){
|
||||||
|
|
||||||
|
if( !this.showTextValues ){
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = 'error, Yes, No'
|
||||||
|
|
||||||
|
if(this.userFields[fieldId].customOptions){
|
||||||
|
options = this.userFields[fieldId].customOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const values = options.split(',')
|
||||||
|
const selection = String(values[value]).trim()
|
||||||
|
|
||||||
|
return selection
|
||||||
|
},
|
||||||
|
displayDayFromCode(dateCode){
|
||||||
|
|
||||||
|
const parts = dateCode.split('.')
|
||||||
|
return `${parts[0]}`
|
||||||
|
},
|
||||||
|
showDayDataColor(day){
|
||||||
|
// Determine if day has any data set
|
||||||
|
if(day == ''){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !(this.cycleData[`${day}.${this.calendar.month}.${this.calendar.year}`])
|
||||||
|
},
|
||||||
|
generateDateCode(date){
|
||||||
|
|
||||||
|
const dateSetup = [
|
||||||
|
date.getDate(), // 1-31 (Day)
|
||||||
|
date.getMonth()+1, // 0-11 (Month)
|
||||||
|
date.getFullYear(), // 1888-2022 (Year)
|
||||||
|
]
|
||||||
|
|
||||||
|
return dateSetup.join('.')
|
||||||
|
},
|
||||||
|
setupCalendar(date){
|
||||||
|
|
||||||
|
// visualize each day change
|
||||||
|
this.working = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.working = false
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
if(!date && this.dateObject){
|
||||||
|
date = this.dateObject
|
||||||
|
}
|
||||||
|
if(!date){
|
||||||
|
date = new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.calendar.dateObject = date
|
||||||
|
|
||||||
|
this.calendar.dateCode = this.generateDateCode(date)
|
||||||
|
|
||||||
|
// calculate days ago since current date
|
||||||
|
const now = new Date()
|
||||||
|
const diffSeconds = Math.floor((now - date) / 1000) // subtract unix timestamps, convert MS to S
|
||||||
|
const dayInterval = diffSeconds / 86400 // seconds in a day
|
||||||
|
this.calendar.daysAgo = Math.floor(dayInterval)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ------------
|
||||||
|
// setup calendar display
|
||||||
|
var y = date.getFullYear()
|
||||||
|
var m = date.getMonth()
|
||||||
|
|
||||||
|
var firstDay = new Date(y, m, 1);
|
||||||
|
var lastDay = new Date(y, m + 1, 0);
|
||||||
|
|
||||||
|
function getDaysInMonth(year, month) {
|
||||||
|
return new Date(year, month, 0).getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentYear = date.getFullYear();
|
||||||
|
const currentMonth = date.getMonth() + 1;
|
||||||
|
this.calendar.monthName = date.toLocaleString("en-US", { month: "long" });
|
||||||
|
this.calendar.dayName = date.toLocaleString("en-US", { weekday: "long" });
|
||||||
|
this.calendar.year = currentYear
|
||||||
|
const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth);
|
||||||
|
|
||||||
|
const monthStartDay = firstDay.getDay()
|
||||||
|
let days = Array(monthStartDay).fill(""); // Pad days to start on correct weekday
|
||||||
|
for (let i = 0; i < daysInCurrentMonth; i++) {
|
||||||
|
days.push(i+1)
|
||||||
|
}
|
||||||
|
this.calendar.days = days
|
||||||
|
|
||||||
|
// set today
|
||||||
|
this.calendar.today = date.getDate()
|
||||||
|
this.calendar.month = date.getMonth()+1
|
||||||
|
|
||||||
|
// setup date codes for key matching on calendar
|
||||||
|
this.calendar.days.forEach((day) => {
|
||||||
|
if( day !== "" ){
|
||||||
|
let dateDay = new Date(y, m, day);
|
||||||
|
let dayCode = this.generateDateCode(dateDay)
|
||||||
|
this.chartDateCodes.push(dayCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// generate past date codes for list
|
||||||
|
for (let i = 0; i < this.tempChartDays; i++) {
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const pastDate = now.setDate(now.getDate() - i)
|
||||||
|
const pastDateObj = new Date(pastDate)
|
||||||
|
const newCode = this.generateDateCode(pastDateObj)
|
||||||
|
this.listDateCodes.push(newCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// return codes.reverse()
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
October 2022
|
||||||
|
S M T W T F S
|
||||||
|
1 2 3 4 5 6
|
||||||
|
7 8 9
|
||||||
|
*/
|
||||||
|
|
||||||
|
// -------
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
164
client/src/components/ModalComponent.vue
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<style type="text/css" scoped>
|
||||||
|
.modal-content {
|
||||||
|
position: fixed;
|
||||||
|
top: 40%;
|
||||||
|
left: 50%;
|
||||||
|
/* bring your own prefixes */
|
||||||
|
transform: translate(-50%, -40%);
|
||||||
|
z-index: 300;
|
||||||
|
padding: 1em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 50%;
|
||||||
|
max-height: 100%;
|
||||||
|
/*overflow: hidden;*/
|
||||||
|
overflow-y: scroll;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.modal-content.fullscreen {
|
||||||
|
width: 96%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
.close-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
z-index: 320;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shrink button text for mobile */
|
||||||
|
@media only screen and (max-width: 740px) {
|
||||||
|
.modal-content {
|
||||||
|
width: 100%;
|
||||||
|
/* padding-bottom: 55px;*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content.right-side {
|
||||||
|
width: 60%;
|
||||||
|
max-height: none;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
left: auto;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
.close-container-right-side {
|
||||||
|
position: fixed;
|
||||||
|
top: 5px;
|
||||||
|
left: calc(60% + 2px);
|
||||||
|
z-index: 320;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shade {
|
||||||
|
position: fixed;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #0000007d;
|
||||||
|
z-index: 299;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-out-top {
|
||||||
|
animation: fade-out-top 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-out {
|
||||||
|
animation: fade-out 0.3s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out-top {
|
||||||
|
0% {
|
||||||
|
/*transform: translate(-50%, -50%);*/
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
/*transform: translate(-50%, -70%);*/
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
/*animation: fade-in 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;*/
|
||||||
|
}
|
||||||
|
@keyframes fade-in {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -70%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="openModel">
|
||||||
|
<div class="modal-content" :class="{ 'fade-out-top':(animateOut), 'fade-in':(!animateOut), 'fullscreen':(fullscreen)}">
|
||||||
|
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<!-- full screen close button -->
|
||||||
|
<div class="close-container" v-if="fullscreen && clickOutClose !== false">
|
||||||
|
<div class="ui green icon button" v-on:click="closeModel">
|
||||||
|
<i class="close icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shade" v-on:click="closeModel" v-on:mouseenter=" hoverOutClose?closeModel():null " :class="{ 'fade-out':(animateOut) }"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
'fullscreen', //Make the model really big
|
||||||
|
'clickOutClose', //Set to false to prevent closing of modal by clicking out
|
||||||
|
'hoverOutClose', //Close if cursor leaves modal
|
||||||
|
],
|
||||||
|
data: function(){
|
||||||
|
return {
|
||||||
|
openModel:true,
|
||||||
|
animateOut:false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeModel(){
|
||||||
|
|
||||||
|
//Don't allow closing by clicking out
|
||||||
|
if(this.clickOutClose === false){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Set stups to close model, animate out
|
||||||
|
this.animateOut = true
|
||||||
|
setTimeout( () => {
|
||||||
|
this.openModel = false
|
||||||
|
this.$emit('close')
|
||||||
|
|
||||||
|
//Once close event is sent, reset to default state
|
||||||
|
this.animateOut = false
|
||||||
|
this.openModel = true
|
||||||
|
|
||||||
|
}, 800)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@@ -1,12 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="note-title-display-card"
|
<div class="note-title-display-card"
|
||||||
:style="{'background-color':color, 'color':fontColor, 'border-color':color }"
|
:style="{'background-color':color, 'color':fontColor, 'border-color':color }"
|
||||||
:class="{'currently-open':(currentlyOpen || showWorking), 'bgboy':triggerClosedAnimation, 'title-view':titleView }"
|
:class="{
|
||||||
>
|
'currently-open':(currentlyOpen || showWorking),
|
||||||
|
'ring':triggerClosedAnimation,
|
||||||
|
'title-view':titleView
|
||||||
|
}">
|
||||||
|
|
||||||
|
|
||||||
<!-- Show title and snippet below it -->
|
<!-- Show title and snippet below it -->
|
||||||
<div class="overflow-hidden note-card-text" @click="cardClicked" v-if="!titleView">
|
<div class="overflow-hidden note-card-text" @click.stop="cardClicked" v-if="!titleView">
|
||||||
|
|
||||||
<span v-if="note.title == '' && note.subtext == ''">
|
<span v-if="note.title == '' && note.subtext == ''">
|
||||||
Empty Note
|
Empty Note
|
||||||
@@ -21,28 +24,10 @@
|
|||||||
class="big-text"><p>{{ note.title }}</p></span>
|
class="big-text"><p>{{ note.title }}</p></span>
|
||||||
|
|
||||||
<span class="tags" v-if="note.tags">
|
<span class="tags" v-if="note.tags">
|
||||||
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}</span>
|
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click.stop="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}</span>
|
||||||
<br>
|
<br>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Sub text display -->
|
|
||||||
<span v-if="note.subtext.length > 0"
|
|
||||||
class="small-text"
|
|
||||||
v-html="note.subtext"></span>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Not indexed warning -->
|
|
||||||
<!-- <span v-if="note.indexed != 1">
|
|
||||||
<span class="green label">Not Indexed</span>
|
|
||||||
</span> -->
|
|
||||||
|
|
||||||
|
|
||||||
<div class="ui fluid basic button" v-if="note.encrypted == 1">
|
|
||||||
<i class="green lock icon"></i>
|
|
||||||
Locked
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Shared Details -->
|
<!-- Shared Details -->
|
||||||
<span class="subtext" v-if="note.shared == 2">
|
<span class="subtext" v-if="note.shared == 2">
|
||||||
<i class="green paper plane outline icon"></i> Shared
|
<i class="green paper plane outline icon"></i> Shared
|
||||||
@@ -62,23 +47,85 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- Sub text display -->
|
||||||
|
<span v-if="note.subtext.length > 0"
|
||||||
|
class="small-text"
|
||||||
|
v-html="note.subtext"></span>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Not indexed warning -->
|
||||||
|
<!-- <span v-if="note.indexed != 1">
|
||||||
|
<span class="green label">Not Indexed</span>
|
||||||
|
</span> -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <div class="ui fluid basic button" v-if="note.encrypted == 1">
|
||||||
|
<i class="green lock icon"></i>
|
||||||
|
Locked
|
||||||
|
</div> -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="titleView" class="single-line-text" @click="cardClicked">
|
<!-- slim card view -->
|
||||||
<span class="title-line" v-if="note.title.length > 0">{{ note.title }}<br></span>
|
<div v-if="titleView" class="thin-container" @click="cardClicked">
|
||||||
<span class="sub-line" v-if="note.subtext.length > 0">{{ removeHtml(note.subtext) }}</span>
|
|
||||||
<span v-if="note.title.length == 0 && note.title.length == 0">Empty Note</span>
|
<!-- icon -->
|
||||||
|
<span v-if="noteIcon" class="thin-icon">
|
||||||
|
<i :class="`${noteIcon} icon`" :style="{ 'color':iconColor }"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- title -->
|
||||||
|
<span class="thin-title" v-if="note.title.length > 0">{{ note.title }}</span>
|
||||||
|
|
||||||
|
<!-- snippet -->
|
||||||
|
<span class="thick-sub" v-if="note.subtext.length > 0 && note.title.length == 0">
|
||||||
|
{{ removeHtml(note.subtext) }}
|
||||||
|
</span>
|
||||||
|
<span class="thin-sub" v-else-if="note.subtext.length > 0">
|
||||||
|
{{ removeHtml(note.subtext) }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="note.title.length == 0 && removeHtml(note.subtext).length == 0">
|
||||||
|
Empty Note
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- tags -->
|
||||||
|
<span v-if="note.tags" class="thin-tags" >
|
||||||
|
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- edited -->
|
||||||
|
<span class="thin-right">
|
||||||
|
{{$helpers.timeAgo( note.updated )}}
|
||||||
|
|
||||||
|
<i class="green link ellipsis vertical icon"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toolbar on the bottom -->
|
<!-- Toolbar on the bottom -->
|
||||||
<div class="tool-bar" @click.self="cardClicked" v-if="!titleView">
|
<div class="tool-bar" @click.self="cardClicked" v-if="!titleView">
|
||||||
<div class="icon-bar">
|
|
||||||
|
|
||||||
<span class="time-ago-display" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
|
<div v-if="getThumbs.length > 0">
|
||||||
|
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
|
||||||
|
<img v-for="thumb in getThumbs"
|
||||||
|
class="tiny-thumb"
|
||||||
|
:src="`/api/static/thumb_${thumb}`"
|
||||||
|
onerror="
|
||||||
|
this.onerror=null;
|
||||||
|
this.src='/api/static/assets/marketing/void.svg';
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="icon-bar" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
|
||||||
|
|
||||||
|
<span class="time-ago-display">
|
||||||
{{$helpers.timeAgo( note.updated )}}
|
{{$helpers.timeAgo( note.updated )}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="teeny-buttons" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
|
<span class="teeny-buttons">
|
||||||
|
|
||||||
<span v-if="!note.trashed">
|
<span v-if="!note.trashed">
|
||||||
|
|
||||||
@@ -115,19 +162,13 @@
|
|||||||
</i>
|
</i>
|
||||||
<delete-button class="teeny-button" :note-id="note.id" />
|
<delete-button class="teeny-button" :note-id="note.id" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="getThumbs.length > 0">
|
|
||||||
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
|
|
||||||
<img v-for="thumb in getThumbs" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- tag edit menu -->
|
||||||
<side-slide-menu v-if="showTagSlideMenu" v-on:close="toggleTags(false)" :full-shadow="true" :skip-history="true">
|
<side-slide-menu v-if="showTagSlideMenu" v-on:close="toggleTags(false)" :full-shadow="true" :skip-history="true">
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
<note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/>
|
<note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/>
|
||||||
@@ -186,10 +227,12 @@
|
|||||||
},
|
},
|
||||||
pinNote(){ //togglePinned() <- old name
|
pinNote(){ //togglePinned() <- old name
|
||||||
this.showWorking = true
|
this.showWorking = true
|
||||||
let postData = {'pinned': !this.note.pinned, 'noteId':this.note.id}
|
this.note.pinned = this.note.pinned == 1 ? 0:1
|
||||||
|
let postData = {'pinned': this.note.pinned, 'noteId':this.note.id}
|
||||||
axios.post('/api/note/setpinned', postData)
|
axios.post('/api/note/setpinned', postData)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.showWorking = false
|
this.showWorking = false
|
||||||
|
// this event is triggered by the server after note is saved
|
||||||
// this.$bus.$emit('update_single_note', this.note.id)
|
// this.$bus.$emit('update_single_note', this.note.id)
|
||||||
})
|
})
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Pin Note') })
|
.catch(error => { this.$bus.$emit('notification', 'Failed to Pin Note') })
|
||||||
@@ -205,11 +248,10 @@
|
|||||||
//Show message so no one worries where note went
|
//Show message so no one worries where note went
|
||||||
let message = 'Moved to Archive'
|
let message = 'Moved to Archive'
|
||||||
if(postData.archived != 1){
|
if(postData.archived != 1){
|
||||||
message = 'Moved to main list'
|
message = 'Moved out of Archive'
|
||||||
}
|
}
|
||||||
this.$bus.$emit('notification', message)
|
this.$bus.$emit('notification', message)
|
||||||
|
this.$bus.$emit('update_single_note', this.note.id)
|
||||||
// this.$bus.$emit('update_single_note', this.note.id)
|
|
||||||
})
|
})
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Archive Note') })
|
.catch(error => { this.$bus.$emit('notification', 'Failed to Archive Note') })
|
||||||
},
|
},
|
||||||
@@ -224,9 +266,10 @@
|
|||||||
//Show message so no one worries where note went
|
//Show message so no one worries where note went
|
||||||
let message = 'Moved to Trash'
|
let message = 'Moved to Trash'
|
||||||
if(postData.trashed == 0){
|
if(postData.trashed == 0){
|
||||||
message = 'Moved to main list'
|
message = 'Moved out of Trash'
|
||||||
}
|
}
|
||||||
this.$bus.$emit('notification', message)
|
this.$bus.$emit('notification', message)
|
||||||
|
this.$bus.$emit('update_single_note', this.note.id)
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Trash Note') })
|
.catch(error => { this.$bus.$emit('notification', 'Failed to Trash Note') })
|
||||||
@@ -242,23 +285,28 @@
|
|||||||
},
|
},
|
||||||
justClosed(){
|
justClosed(){
|
||||||
|
|
||||||
|
// Dont do anything when not is closed.
|
||||||
|
// Its already saved, this will make interface feel snappy
|
||||||
|
|
||||||
// Scroll note into view
|
// Scroll note into view
|
||||||
this.$el.scrollIntoView({
|
// this.$el.scrollIntoView({
|
||||||
behavior: 'smooth',
|
// behavior: 'smooth',
|
||||||
block: 'center',
|
// block: 'center',
|
||||||
inline: 'center'
|
// inline: 'center'
|
||||||
})
|
// })
|
||||||
|
|
||||||
//After scroll, trigger green outline animation
|
// this.$bus.$emit('notification','Note Saved')
|
||||||
setTimeout(() => {
|
|
||||||
|
|
||||||
this.triggerClosedAnimation = true
|
// //After scroll, trigger green outline animation
|
||||||
setTimeout(()=>{
|
// setTimeout(() => {
|
||||||
//After 3 seconds, hide it
|
|
||||||
this.triggerClosedAnimation = false
|
|
||||||
}, 3000)
|
|
||||||
|
|
||||||
}, 500)
|
// this.triggerClosedAnimation = true
|
||||||
|
// setTimeout(()=>{
|
||||||
|
// //After 3 seconds, hide it
|
||||||
|
// this.triggerClosedAnimation = false
|
||||||
|
// }, 1500)
|
||||||
|
|
||||||
|
// }, 500)
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -333,13 +381,11 @@
|
|||||||
|
|
||||||
.teeny-buttons {
|
.teeny-buttons {
|
||||||
float: right;
|
float: right;
|
||||||
width: 65%;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
.time-ago-display {
|
.time-ago-display {
|
||||||
width: 35%;
|
font-size: 11px;
|
||||||
float: left;
|
font-weight: bold;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
.tags {
|
.tags {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -364,9 +410,7 @@
|
|||||||
|
|
||||||
/*Strict font sizes for card display*/
|
/*Strict font sizes for card display*/
|
||||||
.small-text {
|
.small-text {
|
||||||
max-height: 267px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
|
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
|
||||||
@@ -414,10 +458,10 @@
|
|||||||
.note-title-display-card {
|
.note-title-display-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: var(--small_element_bg_color);
|
background-color: var(--small_element_bg_color);
|
||||||
|
|
||||||
/*The subtle shadow*/
|
/*The subtle shadow*/
|
||||||
/*box-shadow: 0px 1px 2px 1px rgba(210, 211, 211, 0.46);*/
|
|
||||||
box-shadow: 2px 2px 6px 0 rgba(0,0,0,.15);
|
box-shadow: 2px 2px 6px 0 rgba(0,0,0,.15);
|
||||||
transition: box-shadow ease 0.5s, transform linear 0.1s;
|
transition: box-shadow, border-color ease 0.5s, transform linear 0.5s;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
/*padding: 0.7em 1em;*/
|
/*padding: 0.7em 1em;*/
|
||||||
border-radius: .28571429rem;
|
border-radius: .28571429rem;
|
||||||
@@ -426,7 +470,7 @@
|
|||||||
/*width: calc(33.333% - 10px);*/
|
/*width: calc(33.333% - 10px);*/
|
||||||
width: calc(25% - 10px);
|
width: calc(25% - 10px);
|
||||||
/*min-width: 190px;*/
|
/*min-width: 190px;*/
|
||||||
min-height: 130px;
|
/*min-height: 130px;*/
|
||||||
/*transition: box-shadow 0.3s;*/
|
/*transition: box-shadow 0.3s;*/
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -435,32 +479,72 @@
|
|||||||
letter-spacing: 0.05rem;
|
letter-spacing: 0.05rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
|
min-height: 100px;
|
||||||
|
max-height: 450px;
|
||||||
}
|
}
|
||||||
.note-title-display-card:hover {
|
.note-title-display-card:hover {
|
||||||
/*box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8);*/
|
box-shadow: 0 8px 15px rgba(0,0,0,0.3);
|
||||||
/*transform: translateY(-2px);*/
|
border-color: var(--main-accent);
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
|
||||||
}
|
}
|
||||||
.note-title-display-card.title-view {
|
.note-title-display-card.title-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
|
/*overflow: hidden;*/
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
/*box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);*/
|
/*box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);*/
|
||||||
}
|
}
|
||||||
|
.title-view + .title-view {
|
||||||
|
border-top: 1px solid var(--border_color);
|
||||||
|
}
|
||||||
|
|
||||||
.single-line-text {
|
.thin-container.single-line-text {
|
||||||
width: calc(100% - 25px);
|
width: calc(100% - 25px);
|
||||||
margin: 5px 10px;
|
/*margin: 5px 10px;*/
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.title-line {
|
|
||||||
|
.thin-container .thin-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
padding: 0 20px 0 0;
|
}
|
||||||
|
.thin-container .thin-sub {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.thin-container .thick-sub {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.thin-container .thin-tags {
|
||||||
|
float: left;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
.thin-container .thin-right {
|
||||||
|
float: right;
|
||||||
|
color: var(--dark_border_color);
|
||||||
|
}
|
||||||
|
.thin-container .thin-icon {
|
||||||
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-bar {
|
.icon-bar {
|
||||||
@@ -468,6 +552,7 @@
|
|||||||
padding: 5px 10px 0;
|
padding: 5px 10px 0;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
background-color: rgba(200, 200, 200, 0.2);
|
||||||
}
|
}
|
||||||
.hover-hide {
|
.hover-hide {
|
||||||
opacity: 0.0;
|
opacity: 0.0;
|
||||||
@@ -647,4 +732,36 @@
|
|||||||
animation: bgin 4s cubic-bezier(0.19, 1, 0.22, 1) 1;
|
animation: bgin 4s cubic-bezier(0.19, 1, 0.22, 1) 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*switch between ring or BG boy to change save animation*/
|
||||||
|
|
||||||
|
.ring {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.ring::after {
|
||||||
|
content: '';
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 6px solid #00FFCB;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 800;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
animation: ring 1.5s 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ring {
|
||||||
|
0% {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 420px;
|
||||||
|
height: 420px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
116
client/src/components/PasteButton.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div class="button-fix">
|
||||||
|
<div class="ui right floated basic shrinking icon button" v-on:click="showPasteInputArea">
|
||||||
|
<i class="green paste icon"></i>
|
||||||
|
Paste
|
||||||
|
</div>
|
||||||
|
<div class="shade" v-if="showPasteArea" @click.prevent="close">
|
||||||
|
<div class="ui stackable grid full-height" @click.prevent="close">
|
||||||
|
<div class="four wide column"></div>
|
||||||
|
<div class="eight wide middle aligned center aligned column">
|
||||||
|
<div class="ui raised segment">
|
||||||
|
<div class="ui dividing header">
|
||||||
|
<i class="green paste icon"></i>
|
||||||
|
Paste & automatically Save
|
||||||
|
</div>
|
||||||
|
<div class="ui fluid action input">
|
||||||
|
<input
|
||||||
|
id="pastetextarea"
|
||||||
|
type="text"
|
||||||
|
ref="pastearea"
|
||||||
|
@paste.prevent="onPaste"
|
||||||
|
@keyup.enter.prevent="onEnter"
|
||||||
|
placeholder="Paste Here">
|
||||||
|
<button class="ui green labeled icon button" @click.prevent="onEnter">
|
||||||
|
<i class="save icon"></i>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="four wide column"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PasteButton',
|
||||||
|
props: {},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
showPasteArea: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close(){
|
||||||
|
this.showPasteArea = false
|
||||||
|
},
|
||||||
|
onEnter(e){
|
||||||
|
|
||||||
|
const text = this.$refs.pastearea.value
|
||||||
|
this.saveText(text)
|
||||||
|
|
||||||
|
},
|
||||||
|
onPaste(e){
|
||||||
|
|
||||||
|
// Get pasted data via clipboard API
|
||||||
|
const clipboardData = e.clipboardData || window.clipboardData
|
||||||
|
const pastedData = String(clipboardData.getData('Text')).trim()
|
||||||
|
|
||||||
|
this.saveText(pastedData)
|
||||||
|
|
||||||
|
},
|
||||||
|
saveText(text){
|
||||||
|
|
||||||
|
this.showPasteArea = false
|
||||||
|
if(!text){
|
||||||
|
this.$bus.$emit('notification', 'Nothing to save.')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.post('/api/quick-note/update', { 'pushText':text } )
|
||||||
|
.then( response => {
|
||||||
|
|
||||||
|
this.$bus.$emit('notification', 'Saved To Scratch Pad')
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$bus.$emit('notification', 'Failed to Save')
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
showPasteInputArea(){
|
||||||
|
|
||||||
|
// Show text area and focus its contents
|
||||||
|
this.showPasteArea = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const aux = document.getElementById('pastetextarea')
|
||||||
|
aux.focus();
|
||||||
|
})
|
||||||
|
|
||||||
|
// auto hide after 1 Minute
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showPasteArea = false
|
||||||
|
}, 60*1000)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="css">
|
||||||
|
.paste-text-container {
|
||||||
|
background-color: green;
|
||||||
|
position: absolute;
|
||||||
|
width: 50vw;
|
||||||
|
height: 80vh;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.full-height {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -35,7 +35,6 @@
|
|||||||
<i class="search icon"></i>
|
<i class="search icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="floating-button" v-if="searchTerm.length > 0">
|
<div class="floating-button" v-if="searchTerm.length > 0">
|
||||||
<i class="big link grey close icon" v-on:click="clear()"></i>
|
<i class="big link grey close icon" v-on:click="clear()"></i>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
<style type="text/css" scoped>
|
<style type="text/css" scoped>
|
||||||
.slide-container {
|
.slide-container {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 50%;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 1020;
|
z-index: 1020;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -98,11 +98,9 @@
|
|||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="slide-shadow" :class="{'full-shadow':fullShadow}" v-on:click="close"></div>
|
<!-- <div class="slide-shadow" :class="{'full-shadow':fullShadow}" v-on:click="close"></div> -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- </transition> -->
|
<!-- </transition> -->
|
||||||
|
44
client/src/components/SvgDisplayer.vue
Normal file
@@ -113,6 +113,7 @@
|
|||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
.button-fix {
|
.button-fix {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
float: left;
|
||||||
}
|
}
|
||||||
.hover-row:hover {
|
.hover-row:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@@ -2,22 +2,43 @@
|
|||||||
.colors {
|
.colors {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1023;
|
z-index: 1023;
|
||||||
top: 5px;
|
top: 35px;
|
||||||
/*height: 100px;*/
|
/*height: 100px;*/
|
||||||
width: 400px;
|
width: 400px;
|
||||||
left: 20%;
|
left: 20%;
|
||||||
}
|
}
|
||||||
.colors-container {
|
.colors-container {
|
||||||
max-width: 370px;
|
/*max-width: 360px;*/
|
||||||
|
display: flex;
|
||||||
|
/*flex-direction: column;*/
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
align-content: stretch;
|
||||||
|
|
||||||
|
height: 250px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.dot {
|
.dot {
|
||||||
display: inline-block;
|
/*display: inline-block;*/
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
box-shadow: 0px 1px 3px 0px #3e3e3e;
|
box-shadow: 0px 0px 0px 1px inset #3e3e3e;
|
||||||
margin: 7px 7px 0 0;
|
margin: 0 0 2px 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
flex-basis: 9%;
|
||||||
|
height: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.dot > i {
|
||||||
|
margin: 9px 0 0 0;
|
||||||
|
color: white;
|
||||||
|
text-shadow:
|
||||||
|
1px 1px 2px #3e3e3e,
|
||||||
|
1px -1px 2px #3e3e3e,
|
||||||
|
-1px 1px 2px #3e3e3e,
|
||||||
|
-1px -1px 2px #3e3e3e
|
||||||
|
;
|
||||||
}
|
}
|
||||||
.shade {
|
.shade {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -30,12 +51,16 @@
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
.big-shadow {
|
||||||
|
box-shadow: 0px 4px 5px 1px #a8a8a8;
|
||||||
|
}
|
||||||
@media only screen and (max-width: 740px) {
|
@media only screen and (max-width: 740px) {
|
||||||
.colors {
|
.colors {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 5px;
|
||||||
right: 0;
|
right: -5px;
|
||||||
top: 0;
|
top: 5px;
|
||||||
|
width: 95%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -43,13 +68,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="colors">
|
<div class="colors">
|
||||||
<div class="ui raised segment">
|
<div class="ui segment big-shadow">
|
||||||
|
<h3>Select Text Color</h3>
|
||||||
<div class="colors-container">
|
<div class="colors-container">
|
||||||
<span
|
<span
|
||||||
v-for="(color,index) in colors"
|
v-for="(color,index) in colors"
|
||||||
class="dot"
|
class="dot"
|
||||||
v-on:click="onColorClick(index)"
|
v-on:click="onColorClick(index)"
|
||||||
:style="`background-color: ${color};`">
|
:style="`background-color: ${color};`">
|
||||||
|
<i v-if="lastUsedColor == color" class="check icon"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,6 +92,7 @@
|
|||||||
components:{
|
components:{
|
||||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||||
},
|
},
|
||||||
|
props: [ 'lastUsedColor' ],
|
||||||
data: function(){
|
data: function(){
|
||||||
return {
|
return {
|
||||||
hover: false,
|
hover: false,
|
||||||
|
@@ -13,19 +13,19 @@ import router from './router'
|
|||||||
// import 'fomantic-ui-css/semantic.css';
|
// import 'fomantic-ui-css/semantic.css';
|
||||||
|
|
||||||
//Required site and reset CSS
|
//Required site and reset CSS
|
||||||
import 'fomantic-ui-css/components/reset.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' //modified to remove included LATO fonts
|
||||||
|
|
||||||
//Only include parts that are used
|
//Only include parts that are used
|
||||||
import 'fomantic-ui-css/components/button.css'
|
import 'fomantic-ui-css/components/button.min.css'
|
||||||
import 'fomantic-ui-css/components/container.css'
|
import 'fomantic-ui-css/components/container.min.css'
|
||||||
import 'fomantic-ui-css/components/form.css'
|
import 'fomantic-ui-css/components/form.min.css'
|
||||||
import 'fomantic-ui-css/components/grid.css'
|
import 'fomantic-ui-css/components/grid.min.css'
|
||||||
import 'fomantic-ui-css/components/header.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' //Modified to remove brand icons
|
||||||
import 'fomantic-ui-css/components/input.css'
|
import 'fomantic-ui-css/components/input.min.css'
|
||||||
import 'fomantic-ui-css/components/segment.css'
|
import 'fomantic-ui-css/components/segment.min.css'
|
||||||
import 'fomantic-ui-css/components/label.css'
|
import 'fomantic-ui-css/components/label.min.css'
|
||||||
|
|
||||||
|
|
||||||
//Overwrite and site styles and themes and good stuff
|
//Overwrite and site styles and themes and good stuff
|
||||||
|
@@ -9,6 +9,8 @@ const SquireButtonFunctions = {
|
|||||||
activeList: false,
|
activeList: false,
|
||||||
activeToDo: false,
|
activeToDo: false,
|
||||||
activeColor: null,
|
activeColor: null,
|
||||||
|
activeCode: false,
|
||||||
|
activeSubTitle: false,
|
||||||
//
|
//
|
||||||
lastUsedColor: null,
|
lastUsedColor: null,
|
||||||
}
|
}
|
||||||
@@ -28,6 +30,8 @@ const SquireButtonFunctions = {
|
|||||||
this.activeToDo = false
|
this.activeToDo = false
|
||||||
this.activeColor = null
|
this.activeColor = null
|
||||||
this.activeUnderline = false
|
this.activeUnderline = false
|
||||||
|
this.activeCode = false
|
||||||
|
this.activeSubTitle = false
|
||||||
|
|
||||||
if(e.path.indexOf('>U>') > -1 || e.path.search(/U$/) > -1){
|
if(e.path.indexOf('>U>') > -1 || e.path.search(/U$/) > -1){
|
||||||
this.activeUnderline = true
|
this.activeUnderline = true
|
||||||
@@ -38,15 +42,21 @@ const SquireButtonFunctions = {
|
|||||||
if(e.path.indexOf('>I') > -1){
|
if(e.path.indexOf('>I') > -1){
|
||||||
this.activeItalics = true
|
this.activeItalics = true
|
||||||
}
|
}
|
||||||
if(e.path.indexOf('fontSize') > -1){
|
if(e.path.indexOf('fontSize=1.4em') > -1){
|
||||||
this.activeTitle = true
|
this.activeTitle = true
|
||||||
}
|
}
|
||||||
|
if(e.path.indexOf('fontSize=0.9em') > -1){
|
||||||
|
this.activeSubTitle = true
|
||||||
|
}
|
||||||
if(e.path.indexOf('OL>LI') > -1){
|
if(e.path.indexOf('OL>LI') > -1){
|
||||||
this.activeList = true
|
this.activeList = true
|
||||||
}
|
}
|
||||||
if(e.path.indexOf('UL>LI') > -1){
|
if(e.path.indexOf('UL>LI') > -1){
|
||||||
this.activeToDo = true
|
this.activeToDo = true
|
||||||
}
|
}
|
||||||
|
if(e.path.indexOf('CODE') > -1){
|
||||||
|
this.activeCode= true
|
||||||
|
}
|
||||||
const colorIndex = e.path.indexOf('color=')
|
const colorIndex = e.path.indexOf('color=')
|
||||||
if(colorIndex > -1){
|
if(colorIndex > -1){
|
||||||
//Get all digigs after color index, then limit to 3
|
//Get all digigs after color index, then limit to 3
|
||||||
@@ -143,6 +153,12 @@ const SquireButtonFunctions = {
|
|||||||
this.editor.italic()
|
this.editor.italic()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
modifyCode(){
|
||||||
|
|
||||||
|
this.selectLineIfNoSelect()
|
||||||
|
|
||||||
|
this.editor.toggleCode()
|
||||||
|
},
|
||||||
undoCustom(){
|
undoCustom(){
|
||||||
//The same as pressing CTRL + Z
|
//The same as pressing CTRL + Z
|
||||||
// this.editor.focus()
|
// this.editor.focus()
|
||||||
@@ -156,15 +172,16 @@ const SquireButtonFunctions = {
|
|||||||
|
|
||||||
//Fetch the container
|
//Fetch the container
|
||||||
let container = document.getElementById('squire-id')
|
let container = document.getElementById('squire-id')
|
||||||
|
this.$router.go(-1)
|
||||||
|
|
||||||
|
setTimeout(()=>{
|
||||||
|
|
||||||
Array.from( container.getElementsByClassName('active') ).forEach(item => {
|
Array.from( container.getElementsByClassName('active') ).forEach(item => {
|
||||||
item.classList.remove('active');
|
item.classList.remove('active');
|
||||||
})
|
})
|
||||||
|
|
||||||
//Close menu if user is on mobile, then sort list
|
},600)
|
||||||
if(this.$store.getters.getIsUserOnMobile){
|
|
||||||
this.$router.go(-1)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
deleteCompletedListItems(){
|
deleteCompletedListItems(){
|
||||||
//
|
//
|
||||||
@@ -174,6 +191,11 @@ const SquireButtonFunctions = {
|
|||||||
//Fetch the container
|
//Fetch the container
|
||||||
let container = document.getElementById('squire-id')
|
let container = document.getElementById('squire-id')
|
||||||
|
|
||||||
|
//Close menu if user is on mobile, then sort list
|
||||||
|
this.$router.go(-1)
|
||||||
|
|
||||||
|
setTimeout(()=>{
|
||||||
|
|
||||||
//Go through each item, on first level, look for Unordered Lists
|
//Go through each item, on first level, look for Unordered Lists
|
||||||
container.childNodes.forEach( (node) => {
|
container.childNodes.forEach( (node) => {
|
||||||
if(node.nodeName == 'UL'){
|
if(node.nodeName == 'UL'){
|
||||||
@@ -217,10 +239,9 @@ const SquireButtonFunctions = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
//Close menu if user is on mobile, then sort list
|
}, 600)
|
||||||
if(this.$store.getters.getIsUserOnMobile){
|
|
||||||
this.$router.go(-1)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
sortList(){
|
sortList(){
|
||||||
//
|
//
|
||||||
@@ -230,6 +251,11 @@ const SquireButtonFunctions = {
|
|||||||
//Fetch the container
|
//Fetch the container
|
||||||
let container = document.getElementById('squire-id')
|
let container = document.getElementById('squire-id')
|
||||||
|
|
||||||
|
//Close menu if user is on mobile
|
||||||
|
this.$router.go(-1)
|
||||||
|
|
||||||
|
setTimeout(()=>{
|
||||||
|
|
||||||
//Go through each item, on first level, look for Unordered Lists
|
//Go through each item, on first level, look for Unordered Lists
|
||||||
container.childNodes.forEach( (node) => {
|
container.childNodes.forEach( (node) => {
|
||||||
if(node.nodeName == 'UL'){
|
if(node.nodeName == 'UL'){
|
||||||
@@ -281,10 +307,9 @@ const SquireButtonFunctions = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
//Close menu if user is on mobile
|
},600)
|
||||||
if(this.$store.getters.getIsUserOnMobile){
|
|
||||||
this.$router.go(-1)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
calculateMath(){
|
calculateMath(){
|
||||||
//
|
//
|
||||||
@@ -294,6 +319,9 @@ const SquireButtonFunctions = {
|
|||||||
//Fetch the container
|
//Fetch the container
|
||||||
let container = document.getElementById('squire-id')
|
let container = document.getElementById('squire-id')
|
||||||
|
|
||||||
|
//Close menu if user is on mobile, then sort list
|
||||||
|
this.$router.go(-1)
|
||||||
|
|
||||||
// simple function that trys to evaluate javascript
|
// simple function that trys to evaluate javascript
|
||||||
const shittyMath = (string) => {
|
const shittyMath = (string) => {
|
||||||
//Remove all chars but math chars
|
//Remove all chars but math chars
|
||||||
@@ -306,6 +334,8 @@ const SquireButtonFunctions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(()=>{
|
||||||
|
|
||||||
//Go through each item, on first level, look for Unordered Lists
|
//Go through each item, on first level, look for Unordered Lists
|
||||||
container.childNodes.forEach( (node) => {
|
container.childNodes.forEach( (node) => {
|
||||||
|
|
||||||
@@ -333,17 +363,28 @@ const SquireButtonFunctions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
},600)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Close menu if user is on mobile, then sort list
|
|
||||||
if(this.$store.getters.getIsUserOnMobile){
|
|
||||||
this.$router.go(-1)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setText(inText){
|
setText(inText){
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.editor.setHTML(inText)
|
this.editor.setHTML(inText)
|
||||||
// this.noteText = this.editor._getHTML()
|
// this.noteText = this.editor._getHTML()
|
||||||
// this.diffNoteText = this.editor._getHTML()
|
// this.diffNoteText = this.editor._getHTML()
|
||||||
|
|
||||||
|
//Make sure all list items have draggable property
|
||||||
|
let container = document.getElementById('squire-id')
|
||||||
|
let listItems = container.getElementsByTagName('li')
|
||||||
|
for(let itemIndex in listItems){
|
||||||
|
// console.log(listItems[itemIndex])
|
||||||
|
// listItems[itemIndex].setAttribute('draggable','true')
|
||||||
|
}
|
||||||
|
// console.log(listItems)
|
||||||
|
|
||||||
},
|
},
|
||||||
getText(){
|
getText(){
|
||||||
|
|
||||||
@@ -376,6 +417,26 @@ const SquireButtonFunctions = {
|
|||||||
|
|
||||||
this.$router.go(-1)
|
this.$router.go(-1)
|
||||||
},
|
},
|
||||||
|
indentText(){
|
||||||
|
|
||||||
|
// Lists use increase list level, increase quote breaks numbering
|
||||||
|
if(this.activeList || this.activeToDo){
|
||||||
|
|
||||||
|
this.editor.increaseListLevel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.editor.increaseQuoteLevel()
|
||||||
|
},
|
||||||
|
outdentText(){
|
||||||
|
|
||||||
|
// Lists use increase list level, increase quote breaks numbering
|
||||||
|
if(this.activeList || this.activeToDo){
|
||||||
|
|
||||||
|
this.editor.decreaseListLevel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.editor.decreaseQuoteLevel()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,6 +8,13 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
Files
|
Files
|
||||||
<div class="sub header">Uploaded Files and Websites from notes.</div>
|
<div class="sub header">Uploaded Files and Websites from notes.</div>
|
||||||
|
<div class="sub header">
|
||||||
|
<i class="green angle double up icon icon"></i>
|
||||||
|
<router-link
|
||||||
|
to="/bookmarklet">
|
||||||
|
Push any website to solid scribe
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -36,6 +43,32 @@
|
|||||||
Other Files
|
Other Files
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
v-if="$store.getters.totals && $store.getters.totals['archivedNotes']"
|
||||||
|
exact-active-class="green"
|
||||||
|
class="ui basic button shrinking"
|
||||||
|
to="/attachments/type/archived">
|
||||||
|
<i class="archive icon"></i>
|
||||||
|
Archived
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
v-if="$store.getters.totals && $store.getters.totals['trashedNotes']"
|
||||||
|
exact-active-class="green"
|
||||||
|
class="ui basic button shrinking"
|
||||||
|
to="/attachments/type/trashed">
|
||||||
|
<i class="trash icon"></i>
|
||||||
|
Trashed
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
v-if="$store.getters.totals && $store.getters.totals['sharedToNotes']"
|
||||||
|
exact-active-class="green"
|
||||||
|
class="ui basic button shrinking"
|
||||||
|
to="/attachments/type/shared">
|
||||||
|
<i class="send icon"></i>
|
||||||
|
Show Shared
|
||||||
|
</router-link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sixteen wide column" v-if="searchParams.noteId">
|
<div class="sixteen wide column" v-if="searchParams.noteId">
|
||||||
@@ -99,6 +132,11 @@
|
|||||||
//Load more attachments on scroll
|
//Load more attachments on scroll
|
||||||
window.addEventListener('scroll', this.onScroll)
|
window.addEventListener('scroll', this.onScroll)
|
||||||
|
|
||||||
|
this.$io.on('update_note_attachments', () => {
|
||||||
|
this.reset()
|
||||||
|
this.searchAttachments()
|
||||||
|
})
|
||||||
|
|
||||||
//Mount notes on load if note ID is set
|
//Mount notes on load if note ID is set
|
||||||
this.searchAttachments()
|
this.searchAttachments()
|
||||||
},
|
},
|
||||||
@@ -106,6 +144,8 @@
|
|||||||
|
|
||||||
//Remove scroll event on destroy
|
//Remove scroll event on destroy
|
||||||
window.removeEventListener('scroll', this.onScroll)
|
window.removeEventListener('scroll', this.onScroll)
|
||||||
|
|
||||||
|
this.$io.removeListener('update_note_attachments')
|
||||||
},
|
},
|
||||||
watch:{
|
watch:{
|
||||||
$route (to, from){
|
$route (to, from){
|
||||||
@@ -165,6 +205,12 @@
|
|||||||
this.searchParams.attachmentType = this.$route.params.type
|
this.searchParams.attachmentType = this.$route.params.type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// include files from shared notes or selected notes
|
||||||
|
this.searchParams.includeShared = false
|
||||||
|
if(this.$route.params.type == 'shared'){
|
||||||
|
this.searchParams.includeShared = true
|
||||||
|
}
|
||||||
|
|
||||||
//Set noteId in if in URL
|
//Set noteId in if in URL
|
||||||
if(this.$route.params.id){
|
if(this.$route.params.id){
|
||||||
this.searchParams.noteId = this.$route.params.id
|
this.searchParams.noteId = this.$route.params.id
|
||||||
|
66
client/src/pages/BookmarkletPage.vue
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-container squire-box">
|
||||||
|
|
||||||
|
<h2 class="ui header">
|
||||||
|
<i class="green angle double up icon icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
Push URL to Solid Scribe - Bookmarklet
|
||||||
|
<div class="sub header">Push any website to your file list.</div>
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>A bookmarklet is a small piece of code that can be run from a bookmark.</p>
|
||||||
|
<p>Use the bookmarklet below to push URLs of website to solid scribe for later</p>
|
||||||
|
<p>The bookmarklet works in a secure way and won't leak any data.</p>
|
||||||
|
<p>To install the bookmarklet, all you need to do is drag it to your bookmarks bar.</p>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Drag the link below to your bookmarks.
|
||||||
|
</h2>
|
||||||
|
<h3>
|
||||||
|
<a :href="`${(bookmarkletscript)}`" class="ui huge text">Push to SolidScribe</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
data: function(){
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
bookmarkletscript:'',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeCreate: function(){
|
||||||
|
// Perform Login check
|
||||||
|
this.$parent.loginGateway()
|
||||||
|
|
||||||
|
},
|
||||||
|
mounted: function(){
|
||||||
|
this.getBookmarklet()
|
||||||
|
},
|
||||||
|
beforeDestroy(){
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getBookmarklet(){
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
axios.post('/api/attachment/getbookmarklet')
|
||||||
|
.then( results => {
|
||||||
|
|
||||||
|
this.bookmarkletscript = results.data
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(error => { this.$bus.$emit('notification', 'Failed to get bookmarklet') })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@@ -12,7 +12,12 @@
|
|||||||
animation: fadeorama 16s ease infinite;
|
animation: fadeorama 16s ease infinite;
|
||||||
height: 350px;
|
height: 350px;
|
||||||
|
|
||||||
text-shadow: 1px 1px 2px black;
|
text-shadow:
|
||||||
|
1px 1px 1px rgba(69,69,69,0.1),
|
||||||
|
-1px -1px 1px rgba(69,69,69,0.1),
|
||||||
|
-1px 1px 1px rgba(69,69,69,0.1),
|
||||||
|
1px -1px 1px rgba(69,69,69,0.1)
|
||||||
|
;
|
||||||
}
|
}
|
||||||
.shine {
|
.shine {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -149,23 +154,23 @@
|
|||||||
|
|
||||||
<!-- All marketing images if you need to review -->
|
<!-- All marketing images if you need to review -->
|
||||||
<div v-if="false" class="sixteen wide column">
|
<div v-if="false" class="sixteen wide column">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/add.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/add.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/gardening.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/gardening.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/growth.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/growth.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/icecream.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/icecream.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/investing.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/investing.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/onboarding.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/onboarding.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/robot.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/robot.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/solution.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/solution.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/watching.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/watching.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/cloud.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/cloud.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/grandma.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/grandma.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/hamburger.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/hamburger.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/idea.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/idea.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/notebook.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/notebook.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/plan.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/plan.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/secure.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/secure.svg">
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/void.svg" alt="">
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/void.svg">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Go to notes button -->
|
<!-- Go to notes button -->
|
||||||
@@ -191,11 +196,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Overview -->
|
||||||
|
<div class="middle aligned centered row">
|
||||||
|
<div class="six wide column">
|
||||||
|
<h2 class="ui dividing header">Powerful text editing and privacy</h2>
|
||||||
|
<h3>Easily edit, share and organize thousands of notes.</h3>
|
||||||
|
<h3>Feel safe knowing no one can read your notes but you.</h3>
|
||||||
|
<!-- <h3>Tools to organize and collaborate on thousands of notes while maintaining security and respecting your privacy.</h3> -->
|
||||||
|
</div>
|
||||||
|
<div class="four wide column">
|
||||||
|
<svg-displayer file="idea" alt="Explosion of New Ideas" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- theme selector -->
|
<!-- theme selector -->
|
||||||
<div class="ui white row">
|
<div class="ui white row">
|
||||||
<div class="sixteen wide middle aligned column">
|
<div class="sixteen wide middle aligned column">
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<h2>
|
<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="$parent.loggedIn">Go to settings to change theme</h3>
|
||||||
@@ -211,19 +229,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Overview -->
|
|
||||||
<div class="middle aligned centered row">
|
|
||||||
<div class="six wide column">
|
|
||||||
<h2 class="ui dividing header">Powerful text editing and privacy</h2>
|
|
||||||
<h3>Easily edit, share and organize thousands of notes.</h3>
|
|
||||||
<h3>Feel safe knowing no one can read your notes but you.</h3>
|
|
||||||
<!-- <h3>Tools to organize and collaborate on thousands of notes while maintaining security and respecting your privacy.</h3> -->
|
|
||||||
</div>
|
|
||||||
<div class="four wide column">
|
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/idea.svg" alt="Explosion of New Ideas">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- features list -->
|
<!-- features list -->
|
||||||
<div class="top aligned centered row">
|
<div class="top aligned centered row">
|
||||||
|
|
||||||
@@ -355,7 +360,7 @@
|
|||||||
<i class="grey lock icon"></i>
|
<i class="grey lock icon"></i>
|
||||||
<i class="bottom left corner yellow key icon"></i>
|
<i class="bottom left corner yellow key icon"></i>
|
||||||
</i>
|
</i>
|
||||||
All Note Text is Encrypted
|
Secure Notes
|
||||||
<div class="sub header">All note text is encrypted. No one can read your notes. None of your data is shared.</div>
|
<div class="sub header">All note text is encrypted. No one can read your notes. None of your data is shared.</div>
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -365,7 +370,7 @@
|
|||||||
<i class="grey search icon"></i>
|
<i class="grey search icon"></i>
|
||||||
<i class="bottom left corner orange font icon"></i>
|
<i class="bottom left corner orange font icon"></i>
|
||||||
</i>
|
</i>
|
||||||
Note Search is Encrypted
|
Private Search
|
||||||
<div class="sub header">Search the contents of all your notes without compromising security.</div>
|
<div class="sub header">Search the contents of all your notes without compromising security.</div>
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -375,7 +380,7 @@
|
|||||||
<i class="grey share alternate icon"></i>
|
<i class="grey share alternate icon"></i>
|
||||||
<i class="bottom left corner share icon"></i>
|
<i class="bottom left corner share icon"></i>
|
||||||
</i>
|
</i>
|
||||||
Encrypted Note Sharing
|
Encrypted Sharing
|
||||||
<div class="sub header">Shared notes are still encrypted, only readable by you and the shared users.</div>
|
<div class="sub header">Shared notes are still encrypted, only readable by you and the shared users.</div>
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -392,7 +397,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="">
|
<svg-displayer file="onboarding" alt="Observe this chart" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -400,8 +405,7 @@
|
|||||||
|
|
||||||
<div class="middle aligned centered row">
|
<div class="middle aligned centered row">
|
||||||
<div class="four wide right aligned column">
|
<div class="four wide right aligned column">
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
|
<svg-displayer file="secure" alt="So dang secure" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<h2>Only you can read your notes. </h2>
|
<h2>Only you can read your notes. </h2>
|
||||||
@@ -415,13 +419,13 @@
|
|||||||
<h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3>
|
<h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="four wide right aligned column">
|
<div class="four wide right aligned column">
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos">
|
<svg-displayer file="cloud" alt="Girl falling into the spiral of digital chaos" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="middle aligned centered row">
|
<div class="middle aligned centered row">
|
||||||
<div class="four wide right aligned column">
|
<div class="four wide right aligned column">
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/robot.svg" alt="Shrunken man near giant tablet">
|
<svg-displayer file="robot" alt="Murder Robot in office environment" />
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<h2>Secure Data Sharing</h2>
|
<h2>Secure Data Sharing</h2>
|
||||||
@@ -440,7 +444,7 @@
|
|||||||
<a href="https://pi-hole.net/" target="_blank">Pi-hole</a> on the network.</h3>
|
<a href="https://pi-hole.net/" target="_blank">Pi-hole</a> on the network.</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="four wide column">
|
<div class="four wide column">
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/icecream.svg" alt="Emergence of a 4th dimensional being perceived as a large ice cream ">
|
<svg-displayer file="icecream" alt="Emergence of a 4th dimensional being perceived as a large ice cream" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -493,6 +497,8 @@
|
|||||||
<a target="_blank" href="https://www.maxg.cc">Solid Scribe was created by Max Gialanella</a>
|
<a target="_blank" href="https://www.maxg.cc">Solid Scribe was created by Max Gialanella</a>
|
||||||
</h3>
|
</h3>
|
||||||
<p><a target="_blank" href="https://www.maxg.cc">Check out my Resume</a></p>
|
<p><a target="_blank" href="https://www.maxg.cc">Check out my Resume</a></p>
|
||||||
|
<p>OR</p>
|
||||||
|
<p><a target="_blank" href="http://blog.maxg.cc">Check out my Programming Blog</a></p>
|
||||||
<p>
|
<p>
|
||||||
I was tired of all my data being owned by big companies, having it farmed out for marketing, and leaving the contents of my life exposed to corporations.
|
I was tired of all my data being owned by big companies, having it farmed out for marketing, and leaving the contents of my life exposed to corporations.
|
||||||
</p>
|
</p>
|
||||||
@@ -512,7 +518,7 @@
|
|||||||
<p>Awesomely Generic Marketing Images - <a target="_blank" href="https://undraw.co/">https://unDraw.co/</a></p>
|
<p>Awesomely Generic Marketing Images - <a target="_blank" href="https://undraw.co/">https://unDraw.co/</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="four wide column">
|
<div class="four wide column">
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/watching.svg" alt="Drinking the blood of the elderly">
|
<svg-displayer file="watching" alt="Drinking the blood of the elderly" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -531,6 +537,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
'login-form':require('@/components/LoginFormComponent.vue').default,
|
'login-form':require('@/components/LoginFormComponent.vue').default,
|
||||||
'logo':require('@/components/LogoComponent.vue').default,
|
'logo':require('@/components/LogoComponent.vue').default,
|
||||||
|
'svg-displayer':require('@/components/SvgDisplayer.vue').default,
|
||||||
},
|
},
|
||||||
data(){
|
data(){
|
||||||
return {
|
return {
|
||||||
@@ -543,7 +550,7 @@ export default {
|
|||||||
'#00b5ad', //Teal
|
'#00b5ad', //Teal
|
||||||
'#2185d0', //Blue
|
'#2185d0', //Blue
|
||||||
'#7128b9', //Violet
|
'#7128b9', //Violet
|
||||||
'#a333c8', // "Purple"
|
'#a333c8', //Purple
|
||||||
'#e03997', //Pink
|
'#e03997', //Pink
|
||||||
'#db2828', //Red
|
'#db2828', //Red
|
||||||
'#f2711c', //Orange
|
'#f2711c', //Orange
|
||||||
|
1690
client/src/pages/MetrictrackingPage.vue
Normal file
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container" v-on:scroll="onScroll">
|
<div class="page-container">
|
||||||
|
|
||||||
<div class="ui grid" ref="content">
|
<div class="ui grid" ref="content">
|
||||||
|
|
||||||
<div class="sixteen wide column">
|
<div class="sixteen wide column">
|
||||||
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
|
<!-- :class="{ 'sixteen wide column':showOneColumn 'sixteen wide column':!showOneColumn}" -->
|
||||||
|
|
||||||
<div class="ui stackable grid">
|
<div class="ui stackable grid">
|
||||||
|
|
||||||
@@ -12,6 +12,12 @@
|
|||||||
<search-input />
|
<search-input />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="sixteen wide column" v-if="$store.getters.totals && $store.getters.totals['showTrackMetricsButton']">
|
||||||
|
<router-link class="ui fluid green button" to="/metrictrack">
|
||||||
|
<i class="calendar check outlin icon"></i>Metric Track
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
|
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
|
||||||
|
|
||||||
<div class="ui basic button shrinking"
|
<div class="ui basic button shrinking"
|
||||||
@@ -23,14 +29,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tag-display
|
<tag-display
|
||||||
|
v-if="$store.getters.totals && Object.keys($store.getters.totals['tags'] || {}).length"
|
||||||
|
:user-tags="$store.getters.totals['tags']"
|
||||||
:active-tags="searchTags"
|
:active-tags="searchTags"
|
||||||
v-on:tagClick="tagId => toggleTagFilter(tagId)"
|
v-on:tagClick="tagId => toggleTagFilter(tagId)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="ui basic shrinking icon button" v-on:click="toggleTitleView()" v-if="$store.getters.totals && $store.getters.totals['totalNotes'] > 0">
|
<paste-button />
|
||||||
<i v-if="titleView" class="th icon"></i>
|
|
||||||
<i v-if="!titleView" class="bars icon"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -45,7 +50,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !loadingInProgress">
|
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !showLoading">
|
||||||
<h2 class="ui header">
|
<h2 class="ui header">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}"
|
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}"
|
||||||
@@ -57,11 +62,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="fastFilters['onlyArchived'] == 1" class="sixteen wide column">
|
<div v-if="fastFilters['onlyArchived'] == 1" class="sixteen wide column">
|
||||||
<h2>Archived Notes</h2>
|
<h2>
|
||||||
|
<i class="green archive icon"></i>
|
||||||
|
Archived Notes</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
|
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
|
||||||
<h2>Trash
|
<h2>
|
||||||
|
<i class="green trash alternate outline icon"></i>
|
||||||
|
Trashed Notes
|
||||||
<span>({{ $store.getters.totals['trashedNotes'] }})</span>
|
<span>({{ $store.getters.totals['trashedNotes'] }})</span>
|
||||||
<div class="ui right floated basic button" data-tooltip="This doesn't work yet">
|
<div class="ui right floated basic button" data-tooltip="This doesn't work yet">
|
||||||
<i class="poo storm icon"></i>
|
<i class="poo storm icon"></i>
|
||||||
@@ -71,7 +80,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sixteen wide column" v-if="fastFilters['onlyShowSharedNotes'] == 1">
|
<div class="sixteen wide column" v-if="fastFilters['onlyShowSharedNotes'] == 1">
|
||||||
<h2>Shared Notes</h2>
|
<h2><i class="green paper plane outline icon"></i>
|
||||||
|
Shared Notes</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sixteen wide column" v-if="tagSuggestions.length > 0">
|
<div class="sixteen wide column" v-if="tagSuggestions.length > 0">
|
||||||
@@ -82,6 +92,57 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Note title card display -->
|
||||||
|
<div class="sixteen wide column">
|
||||||
|
|
||||||
|
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0 && fastFilters['notesHome'] == 1">
|
||||||
|
No Notes Yet. <br>Thats ok.<br><br> <br>
|
||||||
|
<img loading="lazy" width="25%" src="/api/static/assets/marketing/hamburger.svg" alt="Create a new note"><br>
|
||||||
|
Create one when you feel ready.
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Go to one wide column, do not do this on mobile interface -->
|
||||||
|
<div :class="{'one-column':( showOneColumn), 'floating-list':( isFloatingList ), 'hidden-floating-list':(collapseFloatingList)}" v-on:scroll="onScroll">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="ui basic fitted right aligned segment" v-if="isFloatingList">
|
||||||
|
<div class="ui small basic green left floated button" v-on:click="closeAllNotes()" v-if="openNotes.length >= 1">
|
||||||
|
<i class="close icon"></i>
|
||||||
|
Close Notes
|
||||||
|
</div>
|
||||||
|
<div class="ui small green button" v-on:click="collapseFloatingList = true">
|
||||||
|
<i class="caret square left outline icon"></i>
|
||||||
|
Hide List
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- render each section based on notes in set -->
|
||||||
|
<div v-for="section,index in noteSections" 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>
|
||||||
|
|
||||||
|
<div class="note-card-display-area">
|
||||||
|
<note-title-display-card
|
||||||
|
v-on:tagClick="tagId => toggleTagFilter(tagId)"
|
||||||
|
v-for="note in section"
|
||||||
|
:ref="'note-'+note.id"
|
||||||
|
:onClick="openNote"
|
||||||
|
:data="note"
|
||||||
|
:title-view="titleView || isFloatingList"
|
||||||
|
:currently-open="openNotes.includes(note.id)"
|
||||||
|
:key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated + note.archived + note.pinned + note.trashed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="loading-section" v-if="showLoading">
|
||||||
|
<loading-icon message="Decrypting Notes" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- found attachments -->
|
<!-- found attachments -->
|
||||||
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
|
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
|
||||||
<h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
|
<h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
|
||||||
@@ -93,51 +154,24 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Note title card display -->
|
|
||||||
<div class="sixteen wide column">
|
|
||||||
|
|
||||||
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0 && fastFilters['notesHome'] == 1">
|
|
||||||
No Notes Yet. <br>Thats ok.<br><br> <br>
|
|
||||||
<img loading="lazy" width="25%" src="/api/static/assets/marketing/hamburger.svg" alt="Create a new note"><br>
|
|
||||||
Create one when you feel ready.
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- Go to one wide column, do not do this on mobile interface -->
|
|
||||||
<div :class="{'one-column':( showOneColumn() )}">
|
|
||||||
|
|
||||||
<!-- render each section based on notes in set -->
|
|
||||||
<div v-for="section,index in noteSections" 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>
|
|
||||||
|
|
||||||
<div class="note-card-display-area">
|
|
||||||
<note-title-display-card
|
|
||||||
v-on:tagClick="tagId => toggleTagFilter(tagId)"
|
|
||||||
v-for="note in section"
|
|
||||||
:ref="'note-'+note.id"
|
|
||||||
:onClick="openNote"
|
|
||||||
:data="note"
|
|
||||||
:title-view="titleView"
|
|
||||||
:currently-open="activeNoteId1 == note.id"
|
|
||||||
:key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="show-hidden-note-list-button"
|
||||||
<loading-icon v-if="loadingInProgress" message="Decrypting Notes" />
|
v-if="collapseFloatingList && openNotes.length > 0" v-on:click="collapseFloatingList = false">
|
||||||
|
<i class="caret square right outline icon"></i>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<!-- flexbox note container evenly spaces open notes -->
|
||||||
|
<div class="note-panel-container" :class="{ 'note-panel-fullwidth':collapseFloatingList}" v-if="openNotes.length">
|
||||||
|
|
||||||
<note-input-panel
|
<note-input-panel
|
||||||
v-if="activeNoteId1 != null"
|
v-for="noteId in openNotes"
|
||||||
:key="activeNoteId1"
|
v-if="noteId != null"
|
||||||
:noteid="activeNoteId1"
|
:key="noteId"
|
||||||
|
:noteid="noteId"
|
||||||
:url-data="$route.params"
|
:url-data="$route.params"
|
||||||
|
:open-notes="openNotes.length"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -147,7 +181,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SearchBar',
|
name: 'NotesPage',
|
||||||
components: {
|
components: {
|
||||||
|
|
||||||
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
|
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
|
||||||
@@ -158,6 +192,7 @@
|
|||||||
'attachment-display': require('@/components/AttachmentDisplayCard').default,
|
'attachment-display': require('@/components/AttachmentDisplayCard').default,
|
||||||
'tag-display':require('@/components/TagDisplayComponent.vue').default,
|
'tag-display':require('@/components/TagDisplayComponent.vue').default,
|
||||||
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
|
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
|
||||||
|
'paste-button':require('@/components/PasteButton.vue').default,
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -167,6 +202,8 @@
|
|||||||
searchResultsCount: 0,
|
searchResultsCount: 0,
|
||||||
searchTags: [],
|
searchTags: [],
|
||||||
notes: [],
|
notes: [],
|
||||||
|
openNotes: [],
|
||||||
|
collapseFloatingList: false,
|
||||||
highlights: [],
|
highlights: [],
|
||||||
searchDebounce: null,
|
searchDebounce: null,
|
||||||
fastFilters: {},
|
fastFilters: {},
|
||||||
@@ -174,10 +211,10 @@
|
|||||||
|
|
||||||
//Load up notes in batches
|
//Load up notes in batches
|
||||||
firstLoadBatchSize: 10, //First set of rapidly loaded notes
|
firstLoadBatchSize: 10, //First set of rapidly loaded notes
|
||||||
batchSize: 25, //Size of batch loaded when user scrolls through current batch
|
batchSize: 20, //Size of batch loaded when user scrolls through current batch
|
||||||
batchOffset: 0, //Tracks the current batch that has been loaded
|
batchOffset: 0, //Tracks the current batch that has been loaded
|
||||||
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
|
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
|
||||||
loadingInProgress: false,
|
showLoading: false,
|
||||||
scrollLoadEnabled: true,
|
scrollLoadEnabled: true,
|
||||||
|
|
||||||
//Clear button is not visible
|
//Clear button is not visible
|
||||||
@@ -223,37 +260,39 @@
|
|||||||
|
|
||||||
this.$parent.loginGateway()
|
this.$parent.loginGateway()
|
||||||
|
|
||||||
|
//If user is on title view,
|
||||||
|
this.titleView = this.$store.getters.getIsUserOnMobile
|
||||||
|
|
||||||
this.$io.on('new_note_created', noteId => {
|
this.$io.on('new_note_created', noteId => {
|
||||||
|
|
||||||
//Do not update note if its open
|
// Push new note to top of list and animate
|
||||||
if(this.activeNoteId1 != noteId){
|
this.updateSingleNote(noteId)
|
||||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
this.updateSingleNote(noteId, false)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$io.on('note_attribute_modified', noteId => {
|
this.$io.on('note_attribute_modified', noteId => {
|
||||||
|
|
||||||
|
const drawFocus = !this.openNotes.includes(parseInt(noteId))
|
||||||
|
this.updateSingleNote(noteId, drawFocus)
|
||||||
|
|
||||||
//Do not update note if its open
|
//Do not update note if its open
|
||||||
if(this.activeNoteId1 != noteId){
|
if(this.openNotes.includes(parseInt(noteId))){
|
||||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
this.updateSingleNote(noteId, false)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
//Update title cards when new note text is saved
|
//Update title cards when new note text is saved
|
||||||
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
|
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
|
||||||
|
|
||||||
//Do not update note if its open
|
const drawFocus = !this.openNotes.includes(parseInt(noteId))
|
||||||
if(this.activeNoteId1 != noteId){
|
this.updateSingleNote(noteId, drawFocus)
|
||||||
this.updateSingleNote(noteId, true)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$bus.$on('update_single_note', (noteId) => {
|
this.$bus.$on('update_single_note', (noteId) => {
|
||||||
//Do not update note if its open
|
|
||||||
if(this.activeNoteId1 != noteId){
|
const drawFocus = !this.openNotes.includes(parseInt(noteId))
|
||||||
this.updateSingleNote(noteId)
|
this.updateSingleNote(noteId, drawFocus)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
//Update totals for app
|
//Update totals for app
|
||||||
@@ -261,19 +300,7 @@
|
|||||||
|
|
||||||
//Close note event
|
//Close note event
|
||||||
this.$bus.$on('close_active_note', ({noteId, modified}) => {
|
this.$bus.$on('close_active_note', ({noteId, modified}) => {
|
||||||
|
this.closeNote(noteId, modified)
|
||||||
if(modified){
|
|
||||||
console.log('Just closed Note -> ' + noteId + ', modified -> ', modified)
|
|
||||||
}
|
|
||||||
|
|
||||||
//A note has been closed
|
|
||||||
if(this.$route.fullPath != '/notes'){
|
|
||||||
this.$router.push('/notes')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
|
||||||
//Focus and animate if modified
|
|
||||||
this.updateSingleNote(noteId, modified)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$bus.$on('note_deleted', (noteId) => {
|
this.$bus.$on('note_deleted', (noteId) => {
|
||||||
@@ -320,11 +347,13 @@
|
|||||||
|
|
||||||
//Reload page content - don't trigger if load is in progress
|
//Reload page content - don't trigger if load is in progress
|
||||||
this.$bus.$on('note_reload', () => {
|
this.$bus.$on('note_reload', () => {
|
||||||
if(!this.loadingInProgress){
|
if(!this.showLoading){
|
||||||
this.reset()
|
this.reset()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Window scroll needed when scrolling full page.
|
||||||
|
// second scroll event added on note-list for floating view scroll detection
|
||||||
window.addEventListener('scroll', this.onScroll)
|
window.addEventListener('scroll', this.onScroll)
|
||||||
|
|
||||||
//Close notes when back button is pressed
|
//Close notes when back button is pressed
|
||||||
@@ -351,9 +380,9 @@
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
||||||
//Open note on load if ID is set
|
//Open note on PAGE LOAD if ID is set
|
||||||
if(this.$route.params.id > 1){
|
if(this.$route.params.id > 1){
|
||||||
this.activeNoteId1 = this.$route.params.id
|
this.openNote(this.$route.params.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Loads initial batch and tags
|
//Loads initial batch and tags
|
||||||
@@ -362,37 +391,123 @@
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'$route.params.id': function(id){
|
'$route.params.id': function(id){
|
||||||
//Open note on ID, null id will close note
|
this.openNote(id)
|
||||||
this.activeNoteId1 = id
|
},
|
||||||
|
'$route' (to, from) {
|
||||||
|
|
||||||
|
|
||||||
|
// Reload the notes if returning to this page
|
||||||
|
if(to.fullPath == '/notes' && !from.fullPath.includes('/notes/open/')){
|
||||||
|
this.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all notes if returning to /notes page
|
||||||
|
if(to.fullPath == '/notes' && from.fullPath.includes('/notes/open/')){
|
||||||
|
this.closeAllNotes()
|
||||||
|
}
|
||||||
|
|
||||||
|
//Lookup tags set in URL
|
||||||
|
if(to.params.tag && this.$store.getters.totals && this.$store.getters.totals['tags'][to.params.tag]){
|
||||||
|
|
||||||
|
//Lookup tag in store by string
|
||||||
|
const tagObject = this.$store.getters.totals['tags'][to.params.tag]
|
||||||
|
|
||||||
|
//Pull key out of string and load tags for that key
|
||||||
|
this.toggleTagFilter(tagObject.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
computed: {
|
||||||
onScroll(e){
|
isFloatingList(){
|
||||||
console.log('Scroll')
|
|
||||||
},
|
//If note 1 or 2 is open, show floating column
|
||||||
toggleTitleView(){
|
return (this.openNotes.length > 0)
|
||||||
this.titleView = !this.titleView
|
|
||||||
},
|
},
|
||||||
showOneColumn(){
|
showOneColumn(){
|
||||||
|
|
||||||
return this.$store.getters.getIsUserOnMobile
|
return this.$store.getters.getIsUserOnMobile
|
||||||
|
|
||||||
//If note 1 or 2 is open, show one column. Or if the user is on mobile
|
}
|
||||||
return (this.activeNoteId1 != null || this.activeNoteId2 != null) &&
|
|
||||||
!this.$store.getters.getIsUserOnMobile
|
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
openNote(id, event = null){
|
openNote(id, event = null){
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
const intId = parseInt(id)
|
||||||
|
if(this.openNotes.includes(intId)){
|
||||||
|
|
||||||
|
console.log('Open already open note?')
|
||||||
|
|
||||||
|
// const openIndex = this.openNotes.indexOf(intId)
|
||||||
|
// if(openIndex != -1){
|
||||||
|
// console.log('Open note and remove it ', intId + ' on index ' + openIndex)
|
||||||
|
// this.openNotes.splice(openIndex, 1)
|
||||||
|
// }
|
||||||
|
// this.$bus.$emit('close_note_by_id', intId)
|
||||||
|
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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
//Open note if a link was not clicked
|
// Push note to stack if not open
|
||||||
|
if(Number.isInteger(intId) && !this.openNotes.includes(intId)){
|
||||||
|
this.openNotes.push(intId)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
this.$router.push('/notes/open/'+id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
|
closeNote(noteId, modified){
|
||||||
|
|
||||||
|
console.log('close note', this.$route.fullPath)
|
||||||
|
|
||||||
|
const openIndex = this.openNotes.indexOf(noteId)
|
||||||
|
if(openIndex != -1){
|
||||||
|
console.log('Removing note id ', noteId + ' on index ' + openIndex)
|
||||||
|
this.openNotes.splice(openIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// //A note has been closed
|
||||||
|
// if(this.$route.fullPath != '/notes'){
|
||||||
|
// this.$router.push('/notes')
|
||||||
|
// }
|
||||||
|
if(this.openNotes.length == 0 && this.$route.fullPath != '/notes'){
|
||||||
|
this.$router.push('/notes')
|
||||||
|
}
|
||||||
|
|
||||||
|
if(modified){
|
||||||
|
console.log('Just closed Note -> ' + noteId + ', modified -> ', modified)
|
||||||
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
|
//Focus and animate if modified
|
||||||
|
this.updateSingleNote(noteId, modified)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('closeNote(): Open notes length ', this.openNotes.length)
|
||||||
|
},
|
||||||
|
closeAllNotes(){
|
||||||
|
console.log('Close all notes ------------')
|
||||||
|
for (let i = this.openNotes.length - 1; i >= 0; i--) {
|
||||||
|
console.log('Close all notes -> ' + this.openNotes[i])
|
||||||
|
this.closeNote(this.openNotes[i])
|
||||||
|
}
|
||||||
|
console.log('----------------')
|
||||||
|
},
|
||||||
toggleTagFilter(tagId){
|
toggleTagFilter(tagId){
|
||||||
|
|
||||||
this.searchTags = [tagId]
|
this.searchTags = [tagId]
|
||||||
@@ -409,6 +524,10 @@
|
|||||||
},
|
},
|
||||||
onScroll(e){
|
onScroll(e){
|
||||||
|
|
||||||
|
if(!this.scrollLoadEnabled){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
clearTimeout(this.loadingBatchTimeout)
|
clearTimeout(this.loadingBatchTimeout)
|
||||||
this.loadingBatchTimeout = setTimeout(() => {
|
this.loadingBatchTimeout = setTimeout(() => {
|
||||||
|
|
||||||
@@ -418,12 +537,12 @@
|
|||||||
const height = document.getElementById('app').scrollHeight
|
const height = document.getElementById('app').scrollHeight
|
||||||
|
|
||||||
//Load if less than 500px from the bottom
|
//Load if less than 500px from the bottom
|
||||||
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled && !this.loadingInProgress){
|
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled){
|
||||||
|
|
||||||
this.search(false, this.batchSize, true)
|
this.search(true, this.batchSize, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 30)
|
}, 50)
|
||||||
|
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -444,21 +563,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.lastVisibilityState = document.visibilityState
|
this.lastVisibilityState = document.visibilityState
|
||||||
|
|
||||||
},
|
},
|
||||||
// @TODO Don't even trigger this if the note wasn't changed
|
// @TODO Don't even trigger this if the note wasn't changed
|
||||||
updateSingleNote(noteId, focuseAndAnimate = true){
|
updateSingleNote(noteId, focuseAndAnimate = true){
|
||||||
|
|
||||||
|
// console.log('updating single note', noteId)
|
||||||
|
|
||||||
noteId = parseInt(noteId)
|
noteId = parseInt(noteId)
|
||||||
|
|
||||||
//Find local note, if it exists; continue
|
//Find local note, if it exists; continue
|
||||||
let note = null
|
let note = null
|
||||||
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
|
if(this.$refs['note-'+noteId]?.[0]?.note){
|
||||||
note = this.$refs['note-'+noteId][0].note
|
note = this.$refs['note-'+noteId][0].note
|
||||||
//Show that note is working on updating
|
//Show that note is working on updating
|
||||||
this.$refs['note-'+noteId][0].showWorking = true
|
this.$refs['note-'+noteId][0].showWorking = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.rebuildNoteCategorise()
|
||||||
|
// return
|
||||||
|
|
||||||
//Lookup one note using passed in ID
|
//Lookup one note using passed in ID
|
||||||
const postData = {
|
const postData = {
|
||||||
@@ -480,6 +602,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if old note data and new note data exists
|
||||||
if(note && newNote){
|
if(note && newNote){
|
||||||
|
|
||||||
//go through each prop and update it with new values
|
//go through each prop and update it with new values
|
||||||
@@ -488,7 +611,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
//Push new note to front if its modified or we want it to
|
//Push new note to front if its modified or we want it to
|
||||||
if( focuseAndAnimate || note.updated != newNote.updated ){
|
if( note.updated != newNote.updated ){
|
||||||
|
|
||||||
// Find note, in section, move to front
|
// Find note, in section, move to front
|
||||||
Object.keys(this.noteSections).forEach( key => {
|
Object.keys(this.noteSections).forEach( key => {
|
||||||
@@ -502,6 +625,9 @@
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if( focuseAndAnimate ){
|
||||||
this.$nextTick( () => {
|
this.$nextTick( () => {
|
||||||
//Trigger close animation on note
|
//Trigger close animation on note
|
||||||
this.$refs['note-'+noteId][0].justClosed()
|
this.$refs['note-'+noteId][0].justClosed()
|
||||||
@@ -544,19 +670,14 @@
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
//Don't double load note batches
|
//Don't double load note batches
|
||||||
if(this.loadingInProgress){
|
if(this.showLoading){
|
||||||
console.log('Loading already in progress')
|
console.log('Loading already in progress')
|
||||||
return resolve(false)
|
return resolve(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Reset a lot of stuff if we are not merging batches
|
|
||||||
if(!mergeExisting){
|
if(!mergeExisting){
|
||||||
Object.keys(this.noteSections).forEach( key => {
|
this.batchOffset = 0 // Reset batch offset if we are not merging note batches or new set will be offset from current and overwrite current set with second batch
|
||||||
this.noteSections[key] = []
|
|
||||||
})
|
|
||||||
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
|
|
||||||
}
|
}
|
||||||
this.searchResultsCount = 0
|
|
||||||
|
|
||||||
//Remove all filter limits from previous queries
|
//Remove all filter limits from previous queries
|
||||||
delete this.fastFilters.limitSize
|
delete this.fastFilters.limitSize
|
||||||
@@ -584,25 +705,40 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Perform search - or die
|
//Perform search - or die
|
||||||
this.loadingInProgress = true
|
this.showLoading = showLoading
|
||||||
|
this.scrollLoadEnabled = false
|
||||||
axios.post('/api/note/search', postData)
|
axios.post('/api/note/search', postData)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
|
||||||
|
//Reset a lot of stuff if we are not merging batches
|
||||||
|
if(!mergeExisting){
|
||||||
|
Object.keys(this.noteSections).forEach( key => {
|
||||||
|
this.noteSections[key] = []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.searchResultsCount = 0
|
||||||
|
|
||||||
// console.timeEnd('Fetch TitleCard Batch '+notesInNextLoad)
|
// console.timeEnd('Fetch TitleCard Batch '+notesInNextLoad)
|
||||||
|
|
||||||
//Save the number of notes just loaded
|
//Save the number of notes just loaded
|
||||||
this.batchOffset += response.data.notes.length
|
this.batchOffset += response.data.notes.length
|
||||||
|
|
||||||
//Enable or disable scroll loading
|
//Enable scroll loading if endpoint retured notes
|
||||||
this.scrollLoadEnabled = response.data.notes.length > 0
|
this.scrollLoadEnabled = response.data.notes.length > 0
|
||||||
|
|
||||||
if(response.data.total > 0){
|
if(response.data.total > 0){
|
||||||
this.searchResultsCount = response.data.total
|
this.searchResultsCount = response.data.total
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadingInProgress = false
|
this.showLoading = false
|
||||||
this.generateNoteCategories(response.data.notes, mergeExisting)
|
this.generateNoteCategories(response.data.notes, mergeExisting)
|
||||||
|
|
||||||
|
//cache initial notes for faster reloads
|
||||||
|
if(!mergeExisting && this.showClear == false){
|
||||||
|
const cachedNotesJson = JSON.stringify(response.data.notes)
|
||||||
|
localStorage.setItem('snippetCache', cachedNotesJson)
|
||||||
|
}
|
||||||
|
|
||||||
return resolve(true)
|
return resolve(true)
|
||||||
})
|
})
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
|
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
|
||||||
@@ -717,7 +853,7 @@
|
|||||||
//clear out tags
|
//clear out tags
|
||||||
this.searchTags = []
|
this.searchTags = []
|
||||||
this.tagSuggestions = []
|
this.tagSuggestions = []
|
||||||
this.loadingInProgress = false
|
this.showLoading = false
|
||||||
this.searchTerm = ''
|
this.searchTerm = ''
|
||||||
this.$bus.$emit('reset_fast_filters') //Clear out search
|
this.$bus.$emit('reset_fast_filters') //Clear out search
|
||||||
|
|
||||||
@@ -734,15 +870,32 @@
|
|||||||
filter[options[index]] = 1
|
filter[options[index]] = 1
|
||||||
|
|
||||||
this.fastFilters = filter
|
this.fastFilters = filter
|
||||||
|
|
||||||
|
//If notes exist in cache, load them up
|
||||||
|
let showLoading = true
|
||||||
|
const cachedNotesJson = localStorage.getItem('snippetCache')
|
||||||
|
const cachedNotes = JSON.parse(cachedNotesJson)
|
||||||
|
if(cachedNotes && cachedNotes.length > 0 && !this.showClear){
|
||||||
|
|
||||||
|
//Load cache. do not merge existing
|
||||||
|
this.generateNoteCategories(cachedNotes, false)
|
||||||
|
showLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
//Fetch First batch of notes with new filter
|
//Fetch First batch of notes with new filter
|
||||||
this.search(true, this.firstLoadBatchSize, false)
|
this.search(showLoading, this.batchSize, false)
|
||||||
.then( r => this.search(false, this.batchSize, true))
|
// .then( r => this.search(false, this.batchSize, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style type="text/css" scoped>
|
<style type="text/css" scoped>
|
||||||
|
|
||||||
|
.text-fix {
|
||||||
|
padding: 8px 0 0 15px;
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--menu-accent);
|
||||||
|
}
|
||||||
.detail {
|
.detail {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
@@ -760,4 +913,150 @@
|
|||||||
.note-card-section + .note-card-section {
|
.note-card-section + .note-card-section {
|
||||||
padding: 15px 0 0;
|
padding: 15px 0 0;
|
||||||
}
|
}
|
||||||
|
.loading-section {
|
||||||
|
color: var(--main-accent);
|
||||||
|
box-shadow: 0 1px 3px 0 var(--main-accent);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--small_element_bg_color);
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
.floating-list {
|
||||||
|
z-index: 1000;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 25%;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--small_element_bg_color);
|
||||||
|
padding: 15px 5px 0px 10px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
background-color: var(--border_color);
|
||||||
|
}
|
||||||
|
.floating-list::-webkit-scrollbar {
|
||||||
|
display: none; /* Safari and Chrome */
|
||||||
|
}
|
||||||
|
.note-panel-container {
|
||||||
|
position: fixed;
|
||||||
|
width: 75%;
|
||||||
|
height: 100vh;
|
||||||
|
background: gray;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
align-content: stretch;
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.note-panel-fullwidth {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-panel-container > div {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.hidden-floating-list {
|
||||||
|
left: -1000px !important;
|
||||||
|
}
|
||||||
|
.show-hidden-note-list-button {
|
||||||
|
position: fixed;
|
||||||
|
top: 25px;
|
||||||
|
left: 0;
|
||||||
|
min-width: 45px;
|
||||||
|
background-color: var(--main-accent);
|
||||||
|
color: var(--text_color);
|
||||||
|
display: block;
|
||||||
|
z-index: 1100;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
padding: 8px 0px 8px 13px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width:320px) { /* smartphones, iPhone, portrait 480x320 phones */
|
||||||
|
.floating-list {
|
||||||
|
left: -1000px;
|
||||||
|
}
|
||||||
|
.note-panel-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width:481px) { /* portrait e-readers (Nook/Kindle), smaller tablets @ 600 or @ 640 wide. */
|
||||||
|
.floating-list {
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
.note-panel-container {
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width:641px) { /* portrait tablets, portrait iPad, landscape e-readers, landscape 800x480 or 854x480 phones */
|
||||||
|
|
||||||
|
}
|
||||||
|
@media (min-width:961px) { /* tablet, landscape iPad, lo-res laptops ands desktops */
|
||||||
|
|
||||||
|
}
|
||||||
|
@media (min-width:1025px) { /* big landscape tablets, laptops, and desktops */
|
||||||
|
|
||||||
|
}
|
||||||
|
@media (min-width:1281px) { /* hi-res laptops and desktops */
|
||||||
|
|
||||||
|
}
|
||||||
|
@media (min-width:2000px) { /* BIG hi-res laptops and desktops */
|
||||||
|
.floating-list {
|
||||||
|
left: 180px;
|
||||||
|
width: calc(30% - 180px);
|
||||||
|
}
|
||||||
|
.note-panel-container {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.master-note-edit {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--small_element_bg_color);
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.master-note-edit + .master-note-edit {
|
||||||
|
border-left: 2px solid var(--main-accent);
|
||||||
|
border-left: 5px solid var(--border_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
761
client/src/pages/OverviewPage.vue
Normal file
@@ -0,0 +1,761 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
|
||||||
|
<div class="ui grid" ref="content">
|
||||||
|
|
||||||
|
<div class="sixteen wide column">
|
||||||
|
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
|
||||||
|
|
||||||
|
<div class="ui stackable grid">
|
||||||
|
|
||||||
|
<div class="six wide column" v-if="$store.getters.totals && $store.getters.totals['totalNotes']">
|
||||||
|
<search-input />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
|
||||||
|
|
||||||
|
<div class="ui basic button shrinking"
|
||||||
|
v-on:click="updateFastFilters(3)"
|
||||||
|
v-if="$store.getters.totals && ($store.getters.totals['youGotMailCount'] > 0)"
|
||||||
|
style="position: relative;">
|
||||||
|
<i class="green mail icon"></i>Inbox
|
||||||
|
<span class="tiny circular floating ui green label">+{{ $store.getters.totals['youGotMailCount'] }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<tag-display
|
||||||
|
:active-tags="searchTags"
|
||||||
|
v-on:tagClick="tagId => toggleTagFilter(tagId)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="ui basic shrinking icon button" v-on:click="toggleTitleView()" v-if="$store.getters.totals && $store.getters.totals['totalNotes'] > 0">
|
||||||
|
<i v-if="titleView" class="th icon"></i>
|
||||||
|
<i v-if="!titleView" class="bars icon"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="eight wide column" v-if="showClear">
|
||||||
|
<!-- <fast-filters /> -->
|
||||||
|
<span class="ui fluid green button" @click="reset">
|
||||||
|
<i class="arrow circle left icon"></i>Show All Notes
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !loadingInProgress">
|
||||||
|
<h2 class="ui header">
|
||||||
|
<div class="content">
|
||||||
|
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}"
|
||||||
|
<div v-if="searchResultsCount == 0" class="sub header">
|
||||||
|
Search can only find key words. Try a single word search.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="fastFilters['onlyArchived'] == 1" class="sixteen wide column">
|
||||||
|
<h2>Archived Notes</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
|
||||||
|
<h2>Trash
|
||||||
|
<span>({{ $store.getters.totals['trashedNotes'] }})</span>
|
||||||
|
<div class="ui right floated basic button" data-tooltip="This doesn't work yet">
|
||||||
|
<i class="poo storm icon"></i>
|
||||||
|
Empty Trash
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sixteen wide column" v-if="fastFilters['onlyShowSharedNotes'] == 1">
|
||||||
|
<h2>Shared Notes</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sixteen wide column" v-if="tagSuggestions.length > 0">
|
||||||
|
<h5 class="ui tiny dividing header"><i class="green tags icon"></i> Tags ({{ tagSuggestions.length }})</h5>
|
||||||
|
<div class="ui clickable green label" v-for="tag in tagSuggestions" v-on:click="tagId => toggleTagFilter(tag.id)">
|
||||||
|
<i class="tag icon"></i>
|
||||||
|
{{ tag.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- found attachments -->
|
||||||
|
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
|
||||||
|
<h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
|
||||||
|
<attachment-display
|
||||||
|
v-for="item in foundAttachments"
|
||||||
|
:item="item"
|
||||||
|
:key="item.id"
|
||||||
|
:search-params="{}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Note title card display -->
|
||||||
|
<div class="sixteen wide column">
|
||||||
|
|
||||||
|
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0 && fastFilters['notesHome'] == 1">
|
||||||
|
No Notes Yet. <br>Thats ok.<br><br> <br>
|
||||||
|
<img loading="lazy" width="25%" src="/api/static/assets/marketing/hamburger.svg" alt="Create a new note"><br>
|
||||||
|
Create one when you feel ready.
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Go to one wide column, do not do this on mobile interface -->
|
||||||
|
<div :class="{'one-column':( showOneColumn() )}">
|
||||||
|
|
||||||
|
<!-- render each section based on notes in set -->
|
||||||
|
<div v-for="section,index in noteSections" 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>
|
||||||
|
|
||||||
|
<div class="note-card-display-area">
|
||||||
|
<note-title-display-card
|
||||||
|
v-on:tagClick="tagId => toggleTagFilter(tagId)"
|
||||||
|
v-for="note in section"
|
||||||
|
:ref="'note-'+note.id"
|
||||||
|
:onClick="openNote"
|
||||||
|
:data="note"
|
||||||
|
:title-view="titleView"
|
||||||
|
:currently-open="activeNoteId1 == note.id"
|
||||||
|
:key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<loading-icon v-if="loadingInProgress" message="Decrypting Notes" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<note-input-panel
|
||||||
|
v-if="activeNoteId1 != null"
|
||||||
|
:key="activeNoteId1"
|
||||||
|
:noteid="activeNoteId1"
|
||||||
|
:url-data="$route.params"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SearchBar',
|
||||||
|
components: {
|
||||||
|
|
||||||
|
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
|
||||||
|
|
||||||
|
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
|
||||||
|
// 'fast-filters': require('@/components/FastFilters.vue').default,
|
||||||
|
'search-input': require('@/components/SearchInput.vue').default,
|
||||||
|
'attachment-display': require('@/components/AttachmentDisplayCard').default,
|
||||||
|
'counter':require('@/components/AnimatedCounterComponent.vue').default,
|
||||||
|
'tag-display':require('@/components/TagDisplayComponent.vue').default,
|
||||||
|
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
initComponent: true,
|
||||||
|
tagSuggestions:[],
|
||||||
|
searchTerm: '',
|
||||||
|
searchResultsCount: 0,
|
||||||
|
searchTags: [],
|
||||||
|
notes: [],
|
||||||
|
highlights: [],
|
||||||
|
searchDebounce: null,
|
||||||
|
fastFilters: {},
|
||||||
|
titleView: false,
|
||||||
|
|
||||||
|
//Load up notes in batches
|
||||||
|
firstLoadBatchSize: 10, //First set of rapidly loaded notes
|
||||||
|
batchSize: 25, //Size of batch loaded when user scrolls through current batch
|
||||||
|
batchOffset: 0, //Tracks the current batch that has been loaded
|
||||||
|
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
|
||||||
|
loadingInProgress: false,
|
||||||
|
scrollLoadEnabled: true,
|
||||||
|
|
||||||
|
//Clear button is not visible
|
||||||
|
showClear: false,
|
||||||
|
initialPostData: null,
|
||||||
|
|
||||||
|
//Currently open notes in app
|
||||||
|
activeNoteId1: null,
|
||||||
|
activeNoteId2: null,
|
||||||
|
|
||||||
|
//Position determines how note is Positioned
|
||||||
|
activeNote1Position: 0,
|
||||||
|
activeNote2Position: 0,
|
||||||
|
|
||||||
|
lastVisibilityState: null,
|
||||||
|
|
||||||
|
foundAttachments: [],
|
||||||
|
|
||||||
|
sectionData: {
|
||||||
|
'pinned': ['thumbtack', 'Pinned'],
|
||||||
|
'archived': ['archive', 'Archived'],
|
||||||
|
'shared': ['envelope outline', 'Inbox'],
|
||||||
|
'sent': ['paper plane outline', 'Sent Notes'],
|
||||||
|
'notes': ['file','Notes'],
|
||||||
|
'highlights': ['paragraph', 'Found In Text'],
|
||||||
|
'trashed': ['poop', 'Trashed Notes'],
|
||||||
|
'tagged': ['tag', 'Tagged'],
|
||||||
|
},
|
||||||
|
noteSections: {
|
||||||
|
pinned: [],
|
||||||
|
archived: [],
|
||||||
|
shared:[],
|
||||||
|
sent:[],
|
||||||
|
notes: [],
|
||||||
|
highlights: [],
|
||||||
|
trashed: [],
|
||||||
|
tagged:[],
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeMount(){
|
||||||
|
|
||||||
|
this.$parent.loginGateway()
|
||||||
|
|
||||||
|
this.$io.on('new_note_created', noteId => {
|
||||||
|
|
||||||
|
//Do not update note if its open
|
||||||
|
if(this.activeNoteId1 != noteId){
|
||||||
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
|
this.updateSingleNote(noteId, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$io.on('note_attribute_modified', noteId => {
|
||||||
|
//Do not update note if its open
|
||||||
|
if(this.activeNoteId1 != noteId){
|
||||||
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
|
this.updateSingleNote(noteId, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//Update title cards when new note text is saved
|
||||||
|
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
|
||||||
|
|
||||||
|
//Do not update note if its open
|
||||||
|
if(this.activeNoteId1 != noteId){
|
||||||
|
this.updateSingleNote(noteId, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$bus.$on('update_single_note', (noteId) => {
|
||||||
|
//Do not update note if its open
|
||||||
|
if(this.activeNoteId1 != noteId){
|
||||||
|
this.updateSingleNote(noteId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//Update totals for app
|
||||||
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
|
|
||||||
|
//Close note event
|
||||||
|
this.$bus.$on('close_active_note', ({noteId, modified}) => {
|
||||||
|
|
||||||
|
if(modified){
|
||||||
|
console.log('Just closed Note -> ' + noteId + ', modified -> ', modified)
|
||||||
|
}
|
||||||
|
|
||||||
|
//A note has been closed
|
||||||
|
if(this.$route.fullPath != '/notes'){
|
||||||
|
this.$router.push('/notes')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
|
//Focus and animate if modified
|
||||||
|
this.updateSingleNote(noteId, modified)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$bus.$on('note_deleted', (noteId) => {
|
||||||
|
//Remove deleted note from set, its deleted
|
||||||
|
|
||||||
|
Object.keys(this.noteSections).forEach( key => {
|
||||||
|
this.noteSections[key].forEach( (note, index) => {
|
||||||
|
if(note.id == noteId){
|
||||||
|
this.noteSections[key].splice(index,1)
|
||||||
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$bus.$on('update_fast_filters', filterIndex => {
|
||||||
|
|
||||||
|
this.updateFastFilters(filterIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
//Event to update search from other areas
|
||||||
|
this.$bus.$on('update_search_term', sentInSearchTerm => {
|
||||||
|
this.searchTerm = sentInSearchTerm
|
||||||
|
this.search(true, this.batchSize)
|
||||||
|
.then( () => {
|
||||||
|
|
||||||
|
this.searchAttachments()
|
||||||
|
|
||||||
|
const postData = {
|
||||||
|
'tagText':this.searchTerm.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tagSuggestions = []
|
||||||
|
axios.post('/api/tag/suggest', postData)
|
||||||
|
.then( response => {
|
||||||
|
|
||||||
|
this.tagSuggestions = response.data
|
||||||
|
})
|
||||||
|
|
||||||
|
// return
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
//Reload page content - don't trigger if load is in progress
|
||||||
|
this.$bus.$on('note_reload', () => {
|
||||||
|
if(!this.loadingInProgress){
|
||||||
|
this.reset()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('scroll', this.onScroll)
|
||||||
|
|
||||||
|
//Close notes when back button is pressed
|
||||||
|
// window.addEventListener('hashchange', this.hashChangeAction)
|
||||||
|
|
||||||
|
//update note on visibility change
|
||||||
|
// document.addEventListener('visibilitychange', this.visibiltyChangeAction);
|
||||||
|
|
||||||
|
},
|
||||||
|
beforeDestroy(){
|
||||||
|
window.removeEventListener('scroll', this.onScroll)
|
||||||
|
// document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
|
||||||
|
|
||||||
|
this.$bus.$off('note_reload')
|
||||||
|
this.$bus.$off('close_active_note')
|
||||||
|
// this.$bus.$off('update_single_note')
|
||||||
|
this.$bus.$off('note_deleted')
|
||||||
|
this.$bus.$off('update_fast_filters')
|
||||||
|
this.$bus.$off('update_search_term')
|
||||||
|
|
||||||
|
//We want to remove event listeners, but something here is messing them up and preventing ALL event listeners from working
|
||||||
|
// this.$off() // Remove all event listeners
|
||||||
|
// this.$bus.$off()
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
//Open note on load if ID is set
|
||||||
|
if(this.$route.params.id > 1){
|
||||||
|
this.activeNoteId1 = this.$route.params.id
|
||||||
|
}
|
||||||
|
|
||||||
|
//Loads initial batch and tags
|
||||||
|
this.reset()
|
||||||
|
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$route.params.id': function(id){
|
||||||
|
//Open note on ID, null id will close note
|
||||||
|
this.activeNoteId1 = id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleTitleView(){
|
||||||
|
this.titleView = !this.titleView
|
||||||
|
},
|
||||||
|
showOneColumn(){
|
||||||
|
|
||||||
|
return this.$store.getters.getIsUserOnMobile
|
||||||
|
|
||||||
|
//If note 1 or 2 is open, show one column. Or if the user is on mobile
|
||||||
|
return (this.activeNoteId1 != null || this.activeNoteId2 != null) &&
|
||||||
|
!this.$store.getters.getIsUserOnMobile
|
||||||
|
},
|
||||||
|
openNote(id, event = null){
|
||||||
|
|
||||||
|
//Don't open note if a link is clicked in display card
|
||||||
|
if(event && event.target && event.target.nodeName){
|
||||||
|
const nodeClick = event.target.nodeName
|
||||||
|
if(nodeClick == 'A'){ return }
|
||||||
|
}
|
||||||
|
|
||||||
|
//Open note if a link was not clicked
|
||||||
|
this.$router.push('/notes/open/'+id)
|
||||||
|
return
|
||||||
|
},
|
||||||
|
toggleTagFilter(tagId){
|
||||||
|
|
||||||
|
this.searchTags = [tagId]
|
||||||
|
|
||||||
|
//Reset note set and load up notes and tags
|
||||||
|
if(this.searchTags.length > 0){
|
||||||
|
this.search(true, this.batchSize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//If no tags are selected, reset entire page
|
||||||
|
this.reset()
|
||||||
|
|
||||||
|
},
|
||||||
|
onScroll(e){
|
||||||
|
|
||||||
|
clearTimeout(this.loadingBatchTimeout)
|
||||||
|
this.loadingBatchTimeout = setTimeout(() => {
|
||||||
|
|
||||||
|
//Detect distance scrolled down the page
|
||||||
|
const scrolledDown = window.pageYOffset + window.innerHeight
|
||||||
|
//Get height of div to properly detect scroll distance down
|
||||||
|
const height = document.getElementById('app').scrollHeight
|
||||||
|
|
||||||
|
//Load if less than 500px from the bottom
|
||||||
|
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled && !this.loadingInProgress){
|
||||||
|
|
||||||
|
this.search(false, this.batchSize, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 30)
|
||||||
|
|
||||||
|
|
||||||
|
return
|
||||||
|
},
|
||||||
|
visibiltyChangeAction(event){
|
||||||
|
|
||||||
|
//Fuck this shit, just use web sockets
|
||||||
|
return
|
||||||
|
|
||||||
|
//@TODO - phase this out, update it via socket.io
|
||||||
|
//If user leaves page then returns to page, reload the first batch
|
||||||
|
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
|
||||||
|
//Load initial batch, then tags, then other batch
|
||||||
|
this.search(false, this.firstLoadBatchSize)
|
||||||
|
.then( () => {
|
||||||
|
// return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastVisibilityState = document.visibilityState
|
||||||
|
|
||||||
|
},
|
||||||
|
// @TODO Don't even trigger this if the note wasn't changed
|
||||||
|
updateSingleNote(noteId, focuseAndAnimate = true){
|
||||||
|
|
||||||
|
noteId = parseInt(noteId)
|
||||||
|
|
||||||
|
//Find local note, if it exists; continue
|
||||||
|
let note = null
|
||||||
|
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
|
||||||
|
note = this.$refs['note-'+noteId][0].note
|
||||||
|
//Show that note is working on updating
|
||||||
|
this.$refs['note-'+noteId][0].showWorking = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Lookup one note using passed in ID
|
||||||
|
const postData = {
|
||||||
|
searchQuery: this.searchTerm,
|
||||||
|
searchTags: this.searchTags,
|
||||||
|
fastFilters:{
|
||||||
|
noteIdSet:[noteId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Note data must be fetched, then sorted into existing note data
|
||||||
|
axios.post('/api/note/search', postData)
|
||||||
|
.then(results => {
|
||||||
|
|
||||||
|
//Pull note data out of note set
|
||||||
|
let newNote = results.data.notes[0]
|
||||||
|
|
||||||
|
if(newNote === undefined){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(note && newNote){
|
||||||
|
|
||||||
|
//go through each prop and update it with new values
|
||||||
|
Object.keys(newNote).forEach(prop => {
|
||||||
|
note[prop] = newNote[prop]
|
||||||
|
})
|
||||||
|
|
||||||
|
//Push new note to front if its modified or we want it to
|
||||||
|
if( focuseAndAnimate || note.updated != newNote.updated ){
|
||||||
|
|
||||||
|
// Find note, in section, move to front
|
||||||
|
Object.keys(this.noteSections).forEach( key => {
|
||||||
|
this.noteSections[key].forEach( (searchNote, index) => {
|
||||||
|
if(searchNote.id == noteId){
|
||||||
|
//Remove note from location and push to front
|
||||||
|
this.noteSections[key].splice(index, 1)
|
||||||
|
this.noteSections[key].unshift(note)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$nextTick( () => {
|
||||||
|
//Trigger close animation on note
|
||||||
|
this.$refs['note-'+noteId][0].justClosed()
|
||||||
|
this.$refs['note-'+noteId][0].showWorking = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//New notes don't exist in list, push them to the front
|
||||||
|
if(note == null){
|
||||||
|
this.noteSections.notes.unshift(newNote)
|
||||||
|
//Trigger close animation on note
|
||||||
|
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
|
||||||
|
this.$refs['note-'+noteId][0].justClosed()
|
||||||
|
this.$refs['note-'+noteId][0].showWorking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
|
||||||
|
this.$refs['note-'+noteId][0].showWorking = false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Trigger section rebuild
|
||||||
|
this.rebuildNoteCategorise()
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error)
|
||||||
|
this.$bus.$emit('notification', 'Failed to Update Note')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
searchAttachments(){
|
||||||
|
axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm})
|
||||||
|
.then(results => {
|
||||||
|
this.foundAttachments = results.data
|
||||||
|
})
|
||||||
|
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Attachments') })
|
||||||
|
},
|
||||||
|
search(showLoading = true, notesInNextLoad = 10, mergeExisting = false){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
//Don't double load note batches
|
||||||
|
if(this.loadingInProgress){
|
||||||
|
console.log('Loading already in progress')
|
||||||
|
return resolve(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Reset a lot of stuff if we are not merging batches
|
||||||
|
if(!mergeExisting){
|
||||||
|
Object.keys(this.noteSections).forEach( key => {
|
||||||
|
this.noteSections[key] = []
|
||||||
|
})
|
||||||
|
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
|
||||||
|
}
|
||||||
|
this.searchResultsCount = 0
|
||||||
|
|
||||||
|
//Remove all filter limits from previous queries
|
||||||
|
delete this.fastFilters.limitSize
|
||||||
|
delete this.fastFilters.limitOffset
|
||||||
|
|
||||||
|
let postData = {
|
||||||
|
searchQuery: this.searchTerm,
|
||||||
|
searchTags: this.searchTags,
|
||||||
|
fastFilters: this.fastFilters,
|
||||||
|
}
|
||||||
|
|
||||||
|
//Save initial post data on first load
|
||||||
|
if(this.initialPostData == null){
|
||||||
|
this.initialPostData = JSON.stringify(postData)
|
||||||
|
}
|
||||||
|
//If post data is not the same as initial, show clear button
|
||||||
|
if(JSON.stringify(postData) != this.initialPostData){
|
||||||
|
this.showClear = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if(notesInNextLoad && notesInNextLoad > 0){
|
||||||
|
//Create limit based off of the number of notes already loaded
|
||||||
|
postData.fastFilters.limitSize = notesInNextLoad
|
||||||
|
postData.fastFilters.limitOffset = this.batchOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
//Perform search - or die
|
||||||
|
this.loadingInProgress = true
|
||||||
|
axios.post('/api/note/search', postData)
|
||||||
|
.then(response => {
|
||||||
|
|
||||||
|
// console.timeEnd('Fetch TitleCard Batch '+notesInNextLoad)
|
||||||
|
|
||||||
|
//Save the number of notes just loaded
|
||||||
|
this.batchOffset += response.data.notes.length
|
||||||
|
|
||||||
|
//Enable or disable scroll loading
|
||||||
|
this.scrollLoadEnabled = response.data.notes.length > 0
|
||||||
|
|
||||||
|
if(response.data.total > 0){
|
||||||
|
this.searchResultsCount = response.data.total
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingInProgress = false
|
||||||
|
this.generateNoteCategories(response.data.notes, mergeExisting)
|
||||||
|
|
||||||
|
return resolve(true)
|
||||||
|
})
|
||||||
|
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
rebuildNoteCategorise(){
|
||||||
|
let currentNotes = []
|
||||||
|
Object.keys(this.noteSections).forEach( key => {
|
||||||
|
this.noteSections[key].forEach( note => {
|
||||||
|
currentNotes.push(note)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.generateNoteCategories(currentNotes, false)
|
||||||
|
},
|
||||||
|
generateNoteCategories(notes, mergeExisting){
|
||||||
|
// Place each note in a category based on certain attributes and fast filters
|
||||||
|
|
||||||
|
//Reset all sections if we are not merging existing
|
||||||
|
if(!mergeExisting){
|
||||||
|
Object.keys(this.noteSections).forEach( key => {
|
||||||
|
this.noteSections[key] = []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//Sort notes into defined sections
|
||||||
|
notes.forEach(note => {
|
||||||
|
|
||||||
|
if(this.searchTerm.length > 0){
|
||||||
|
if(note.pinned == 1){
|
||||||
|
this.noteSections.pinned.push(note)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Push to default note section
|
||||||
|
this.noteSections.notes.push(note)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Display all tags in tag section
|
||||||
|
if(this.searchTags.length >= 1){
|
||||||
|
this.noteSections.tagged.push(note)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Only show trashed notes when trashed
|
||||||
|
if(this.fastFilters.onlyShowTrashed == 1){
|
||||||
|
|
||||||
|
if(note.trashed == 1){
|
||||||
|
this.noteSections.trashed.push(note)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(note.trashed == 1){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Show archived notes
|
||||||
|
if(this.fastFilters.onlyArchived == 1){
|
||||||
|
|
||||||
|
if(note.pinned == 1 && note.archived == 1){
|
||||||
|
this.noteSections.pinned.push(note)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(note.archived == 1){
|
||||||
|
this.noteSections.archived.push(note)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(note.archived == 1){ return }
|
||||||
|
|
||||||
|
//Only show sent notes section if shared is selected
|
||||||
|
if(this.fastFilters.onlyShowSharedNotes == 1){
|
||||||
|
|
||||||
|
if(note.shared == 2){
|
||||||
|
this.noteSections.sent.push(note)
|
||||||
|
}
|
||||||
|
if(note.shareUsername != null){
|
||||||
|
this.noteSections.shared.push(note)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//Show shared notes on main list but not notes shared with you
|
||||||
|
if(note.shareUsername != null){ return }
|
||||||
|
|
||||||
|
// Pinned notes are always first, they can appear in the archive
|
||||||
|
if(note.pinned == 1){
|
||||||
|
this.noteSections.pinned.push(note)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Push to default note section
|
||||||
|
this.noteSections.notes.push(note)
|
||||||
|
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
reset(){
|
||||||
|
this.showClear = false
|
||||||
|
this.scrollLoadEnabled = true
|
||||||
|
this.searchTerm = ''
|
||||||
|
this.searchTags = []
|
||||||
|
this.tagSuggestions = []
|
||||||
|
this.fastFilters = {}
|
||||||
|
this.foundAttachments = [] //Remove all attachments
|
||||||
|
|
||||||
|
this.updateFastFilters(5) //This loads notes
|
||||||
|
|
||||||
|
},
|
||||||
|
updateFastFilters(index){
|
||||||
|
|
||||||
|
//clear out tags
|
||||||
|
this.searchTags = []
|
||||||
|
this.tagSuggestions = []
|
||||||
|
this.loadingInProgress = false
|
||||||
|
this.searchTerm = ''
|
||||||
|
this.$bus.$emit('reset_fast_filters') //Clear out search
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
'withLinks', // 'Only Show Notes with Links'
|
||||||
|
'withTags', // 'Only Show Notes with Tags'
|
||||||
|
'onlyArchived', //'Only Show Archived Notes'
|
||||||
|
'onlyShowSharedNotes', //Only show shared notes
|
||||||
|
'onlyShowTrashed',
|
||||||
|
'notesHome',
|
||||||
|
]
|
||||||
|
|
||||||
|
let filter = {}
|
||||||
|
filter[options[index]] = 1
|
||||||
|
|
||||||
|
this.fastFilters = filter
|
||||||
|
//Fetch First batch of notes with new filter
|
||||||
|
this.search(true, this.firstLoadBatchSize, false)
|
||||||
|
.then( r => this.search(false, this.batchSize, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style type="text/css" scoped>
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.note-card-display-area {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.display-area-title {
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.note-card-section {
|
||||||
|
/*padding-bottom: 15px;*/
|
||||||
|
}
|
||||||
|
.note-card-section + .note-card-section {
|
||||||
|
padding: 15px 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -13,6 +13,7 @@ const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/Note
|
|||||||
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
|
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
|
||||||
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
|
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
|
||||||
const OverviewPage = () => import(/* webpackChunkName: "OverviewPage" */ '@/pages/OverviewPage')
|
const OverviewPage = () => import(/* webpackChunkName: "OverviewPage" */ '@/pages/OverviewPage')
|
||||||
|
const BookmarkletPage = () => import(/* webpackChunkName: "BookmarkletPage" */ '@/pages/BookmarkletPage')
|
||||||
const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
|
const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
|
||||||
|
|
||||||
Vue.use(Router)
|
Vue.use(Router)
|
||||||
@@ -43,6 +44,12 @@ export default new Router({
|
|||||||
meta: {title: 'Open Note'},
|
meta: {title: 'Open Note'},
|
||||||
component: NotesPage,
|
component: NotesPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/search/tags/:tag',
|
||||||
|
name: 'Search Notes',
|
||||||
|
meta: {title: 'Search Notes'},
|
||||||
|
component: NotesPage,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/notes/open/:id/menu/:openMenu',
|
path: '/notes/open/:id/menu/:openMenu',
|
||||||
name: 'Open Note Menu',
|
name: 'Open Note Menu',
|
||||||
@@ -61,6 +68,12 @@ export default new Router({
|
|||||||
meta: {title:'Terms'},
|
meta: {title:'Terms'},
|
||||||
component: TermsPage
|
component: TermsPage
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/bookmarklet',
|
||||||
|
name: 'Bookmarklet',
|
||||||
|
meta: {title:'Bookmarklet'},
|
||||||
|
component: BookmarkletPage
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
@@ -109,5 +122,12 @@ export default new Router({
|
|||||||
meta: {title:'404 Page Not Found'},
|
meta: {title:'404 Page Not Found'},
|
||||||
component: NotFoundPage
|
component: NotFoundPage
|
||||||
},
|
},
|
||||||
|
// Cycle Tracking
|
||||||
|
{
|
||||||
|
path: '/metrictrack',
|
||||||
|
name: 'Metric Tracking',
|
||||||
|
meta: {title:'Metric Tracking'},
|
||||||
|
component: () => import(/* webpackChunkName: "MetrictrackingPage" */ '@/pages/MetrictrackingPage')
|
||||||
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -9,7 +9,9 @@ export default new Vuex.Store({
|
|||||||
username: null,
|
username: null,
|
||||||
nightMode: false,
|
nightMode: false,
|
||||||
isUserOnMobile: false,
|
isUserOnMobile: false,
|
||||||
userTotals: null,
|
fetchTotalsTimeout: null,
|
||||||
|
userTotals: null, // {} // setting this to object breaks reactivity
|
||||||
|
activeSessions: 0,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setUsername(state, username){
|
setUsername(state, username){
|
||||||
@@ -24,6 +26,7 @@ export default new Vuex.Store({
|
|||||||
localStorage.removeItem('loginToken')
|
localStorage.removeItem('loginToken')
|
||||||
localStorage.removeItem('username')
|
localStorage.removeItem('username')
|
||||||
localStorage.removeItem('currentVersion')
|
localStorage.removeItem('currentVersion')
|
||||||
|
localStorage.removeItem('snippetCache')
|
||||||
delete axios.defaults.headers.common['authorizationtoken']
|
delete axios.defaults.headers.common['authorizationtoken']
|
||||||
state.username = null
|
state.username = null
|
||||||
state.userTotals = null
|
state.userTotals = null
|
||||||
@@ -41,12 +44,12 @@ export default new Vuex.Store({
|
|||||||
'menu-text': '#5e6268',
|
'menu-text': '#5e6268',
|
||||||
},
|
},
|
||||||
'black':{
|
'black':{
|
||||||
'body_bg_color': 'linear-gradient(135deg, rgba(0,0,0,1) 0%, rgba(23,12,46,1) 100%)',
|
'body_bg_color': 'rgb(12 4 30)',
|
||||||
//'#0f0f0f',//'#000',
|
//'#0f0f0f',//'#000',
|
||||||
'small_element_bg_color': '#000',
|
'small_element_bg_color': '#000',
|
||||||
'text_color': '#FFF',
|
'text_color': '#FFF',
|
||||||
'dark_border_color': '#555',//'#ACACAC', //Lighter color to accent elemnts user can interact with
|
'dark_border_color': '#555',//'#ACACAC', //Lighter color to accent elemnts user can interact with
|
||||||
'border_color': '#555',
|
'border_color': '#505050',
|
||||||
'menu-accent': '#626262',
|
'menu-accent': '#626262',
|
||||||
'menu-text': '#d9d9d9',
|
'menu-text': '#d9d9d9',
|
||||||
},
|
},
|
||||||
@@ -98,8 +101,23 @@ export default new Vuex.Store({
|
|||||||
state.socket = socket
|
state.socket = socket
|
||||||
},
|
},
|
||||||
setUserTotals(state, totalsObject){
|
setUserTotals(state, totalsObject){
|
||||||
//Save all the totals for the user
|
|
||||||
state.userTotals = totalsObject
|
if(!state.userTotals){
|
||||||
|
state.userTotals = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// retain old values loaded on initial, extended options load
|
||||||
|
let oldMissingValues = {}
|
||||||
|
Object.keys(state.userTotals).forEach(key => {
|
||||||
|
if(!totalsObject[key] && totalsObject[key] !== 0){
|
||||||
|
oldMissingValues[key] = state.userTotals[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// combine old settings with updated settings
|
||||||
|
let oldAndNew = Object.assign(oldMissingValues, totalsObject)
|
||||||
|
|
||||||
|
state.userTotals = oldAndNew
|
||||||
|
|
||||||
//Set computer version from server
|
//Set computer version from server
|
||||||
const currentVersion = localStorage.getItem('currentVersion')
|
const currentVersion = localStorage.getItem('currentVersion')
|
||||||
@@ -119,6 +137,15 @@ export default new Vuex.Store({
|
|||||||
// Object.keys(totalsObject).forEach( key => {
|
// Object.keys(totalsObject).forEach( key => {
|
||||||
// console.log(key + ' -- ' + totalsObject[key])
|
// console.log(key + ' -- ' + totalsObject[key])
|
||||||
// })
|
// })
|
||||||
|
},
|
||||||
|
setActiveSessions(state, countData){
|
||||||
|
//Count of the number of active socket.io sessions for this user
|
||||||
|
state.activeSessions = countData
|
||||||
|
},
|
||||||
|
hideMetricTrackingReminder(state){
|
||||||
|
if(state.userTotals){
|
||||||
|
state.userTotals['showTrackMetricsButton'] = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@@ -144,10 +171,19 @@ export default new Vuex.Store({
|
|||||||
totals: state => {
|
totals: state => {
|
||||||
return state.userTotals
|
return state.userTotals
|
||||||
},
|
},
|
||||||
|
getActiveSessions: state => {
|
||||||
|
return state.activeSessions
|
||||||
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
fetchAndUpdateUserTotals ({ commit }) {
|
fetchAndUpdateUserTotals ({ commit, state }) {
|
||||||
axios.post('/api/user/totals')
|
clearTimeout(state.fetchTotalsTimeout)
|
||||||
|
state.fetchTotalsTimeout = setTimeout(() => {
|
||||||
|
// load extended options on initial load
|
||||||
|
let postData = {
|
||||||
|
extendedOptions: !state.userTotals
|
||||||
|
}
|
||||||
|
axios.post('/api/user/totals', postData)
|
||||||
.then( ({data}) => {
|
.then( ({data}) => {
|
||||||
commit('setUserTotals', data)
|
commit('setUserTotals', data)
|
||||||
})
|
})
|
||||||
@@ -157,6 +193,7 @@ export default new Vuex.Store({
|
|||||||
location.reload()
|
location.reload()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
@@ -1,18 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
pwa: {
|
|
||||||
name: 'SolidScribe',
|
|
||||||
iconPaths: {
|
|
||||||
favicon32: null,
|
|
||||||
favicon16: null,
|
|
||||||
appleTouchIcon: null,
|
|
||||||
maskIcon: null,
|
|
||||||
msTileImage: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
disableHostCheck: true,
|
|
||||||
proxy: 'http://localhost:8081',
|
|
||||||
public: 'marvin.local',
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
@@ -12,3 +12,4 @@ bundle.*
|
|||||||
client/dist*
|
client/dist*
|
||||||
server/public/*
|
server/public/*
|
||||||
client/dist*
|
client/dist*
|
||||||
|
*_scrape*
|
11
jest.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const path = '../../'
|
||||||
|
const prefix = '/$1'
|
||||||
|
module.exports = {
|
||||||
|
moduleNameMapper: {
|
||||||
|
"@root/(.*)": ".",
|
||||||
|
"@models/(.*)": path+"server/models"+prefix,
|
||||||
|
"@routes/(.*)": path+"server/routes"+prefix,
|
||||||
|
"@helpers/(.*)": path+"server/helpers"+prefix,
|
||||||
|
"@config/(.*)": path+"server/config"+prefix,
|
||||||
|
}
|
||||||
|
}
|
8812
package-lock.json
generated
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "personal-internet",
|
"name": "personal-internet",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Personal or Private net",
|
"description": "Encrypted note taking applications",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"author": "Max",
|
"author": "Max",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@@ -33,5 +33,8 @@
|
|||||||
"@routes": "server/routes",
|
"@routes": "server/routes",
|
||||||
"@helpers": "server/helpers",
|
"@helpers": "server/helpers",
|
||||||
"@config": "server/config"
|
"@config": "server/config"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^29.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
//Import mysql2 package
|
//Import mysql2 package
|
||||||
const mysql = require('mysql2');
|
const mysql = require('mysql2');
|
||||||
|
const os = require('os') //Used to get path of home directory
|
||||||
|
const result = require('dotenv').config({ path:(os.homedir()+'/.env') })
|
||||||
|
|
||||||
// Create the connection pool.
|
// Create the connection pool.
|
||||||
const pool = mysql.createPool({
|
const pool = mysql.createPool({
|
||||||
|
@@ -6,6 +6,7 @@ const speakeasy = require('speakeasy')
|
|||||||
let Auth = {}
|
let Auth = {}
|
||||||
|
|
||||||
const tokenSecretKey = process.env.JSON_KEY
|
const tokenSecretKey = process.env.JSON_KEY
|
||||||
|
const sessionTokenUses = 300 //Defines number of uses each session token has before being refreshed
|
||||||
|
|
||||||
//Creates session token
|
//Creates session token
|
||||||
Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => {
|
Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => {
|
||||||
@@ -26,7 +27,7 @@ Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) =>
|
|||||||
|
|
||||||
return db.promise().query(
|
return db.promise().query(
|
||||||
'INSERT INTO user_active_session (salt, encrypted_master_password, created, uses, user_hash, session_id) VALUES (?,?,?,?,?,?)',
|
'INSERT INTO user_active_session (salt, encrypted_master_password, created, uses, user_hash, session_id) VALUES (?,?,?,?,?,?)',
|
||||||
[salt, encryptedMasterPass, created, 40, userHash, sessionId])
|
[salt, encryptedMasterPass, created, sessionTokenUses, userHash, sessionId])
|
||||||
|
|
||||||
})
|
})
|
||||||
.then((r,f) => {
|
.then((r,f) => {
|
||||||
|
@@ -72,6 +72,8 @@ CryptoString.createSalt = () => {
|
|||||||
|
|
||||||
return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64')
|
return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creates a small random salt
|
||||||
CryptoString.createSmallSalt = () => {
|
CryptoString.createSmallSalt = () => {
|
||||||
|
|
||||||
return crypto.randomBytes(20).toString('base64')
|
return crypto.randomBytes(20).toString('base64')
|
||||||
|
@@ -6,7 +6,7 @@ let SiteScrape = module.exports = {}
|
|||||||
|
|
||||||
const removeWhitespace = /\s+/g
|
const removeWhitespace = /\s+/g
|
||||||
|
|
||||||
const commonWords = ['share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
|
const commonWords = ['just','start','what','these','how', 'was', 'being','can','way','share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
|
||||||
'give','use','find','tell','ask','work','seem','feel','try','leave','call','good','new','first','last','long','great','little','own','other','old',
|
'give','use','find','tell','ask','work','seem','feel','try','leave','call','good','new','first','last','long','great','little','own','other','old',
|
||||||
'right','big','high','different','small','large','next','early','young','important','few','public','bad','same','able','to','of','in','for','on',
|
'right','big','high','different','small','large','next','early','young','important','few','public','bad','same','able','to','of','in','for','on',
|
||||||
'with','at','by','from','up','about','into','over','after','the','and','a','that','I','it','not','he','as','you','this','but','his','they','her',
|
'with','at','by','from','up','about','into','over','after','the','and','a','that','I','it','not','he','as','you','this','but','his','they','her',
|
||||||
@@ -54,7 +54,7 @@ SiteScrape.getCleanUrls = (textBlock) => {
|
|||||||
SiteScrape.getHostName = (url) => {
|
SiteScrape.getHostName = (url) => {
|
||||||
|
|
||||||
var hostname = 'https://'+(new URL(url)).hostname;
|
var hostname = 'https://'+(new URL(url)).hostname;
|
||||||
console.log('hostname', hostname)
|
// console.log('hostname', hostname)
|
||||||
return hostname
|
return hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,36 +63,95 @@ SiteScrape.getDisplayImage = ($, url) => {
|
|||||||
|
|
||||||
const hostname = SiteScrape.getHostName(url)
|
const hostname = SiteScrape.getHostName(url)
|
||||||
|
|
||||||
let metaImg = $('meta[property="og:image"]')
|
let metaImg = $('[property="og:image"]')
|
||||||
let shortcutIcon = $('link[rel="shortcut icon"]')
|
let shortcutIcon = $('[rel="shortcut icon"]')
|
||||||
let favicon = $('link[rel="icon"]')
|
let favicon = $('[rel="icon"]')
|
||||||
let randomImg = $('img')
|
let randomImg = $('img')
|
||||||
|
|
||||||
console.log('----')
|
//Set of images we may want gathered from various places in source
|
||||||
|
let imagesWeWant = []
|
||||||
|
let thumbnail = ''
|
||||||
|
|
||||||
//Scrape metadata for page image
|
//Scrape metadata for page image
|
||||||
//Grab the first random image we find
|
if(randomImg && randomImg.length > 0){
|
||||||
if(randomImg && randomImg[0] && randomImg[0].attribs){
|
|
||||||
thumbnail = hostname + randomImg[0].attribs.src
|
let imgSrcs = []
|
||||||
console.log('random img '+thumbnail)
|
for (let i = 0; i < randomImg.length; i++) {
|
||||||
|
imgSrcs.push( randomImg[i].attribs.src )
|
||||||
}
|
}
|
||||||
//Grab the favicon of the site
|
|
||||||
|
const half = Math.ceil(imgSrcs.length / 2)
|
||||||
|
imagesWeWant = [...imgSrcs.slice(-half), ...imgSrcs.slice(0,half) ]
|
||||||
|
|
||||||
|
}
|
||||||
|
//Grab the shortcut icon
|
||||||
if(favicon && favicon[0] && favicon[0].attribs){
|
if(favicon && favicon[0] && favicon[0].attribs){
|
||||||
thumbnail = hostname + favicon[0].attribs.href
|
imagesWeWant.push(favicon[0].attribs.href)
|
||||||
console.log('favicon '+thumbnail)
|
|
||||||
}
|
}
|
||||||
//Grab the shortcut icon
|
//Grab the shortcut icon
|
||||||
if(shortcutIcon && shortcutIcon[0] && shortcutIcon[0].attribs){
|
if(shortcutIcon && shortcutIcon[0] && shortcutIcon[0].attribs){
|
||||||
thumbnail = hostname + shortcutIcon[0].attribs.href
|
imagesWeWant.push(shortcutIcon[0].attribs.href)
|
||||||
console.log('shortcut '+thumbnail)
|
|
||||||
}
|
}
|
||||||
//Grab the presentation image for the site
|
//Grab the presentation image for the site
|
||||||
if(metaImg && metaImg[0] && metaImg[0].attribs){
|
if(metaImg && metaImg[0] && metaImg[0].attribs){
|
||||||
thumbnail = metaImg[0].attribs.content
|
imagesWeWant.unshift(metaImg[0].attribs.content)
|
||||||
console.log('ogImg '+thumbnail)
|
}
|
||||||
|
|
||||||
|
// console.log(imagesWeWant)
|
||||||
|
|
||||||
|
//Remove everything that isn't an accepted file format
|
||||||
|
for (let i = imagesWeWant.length - 1; i >= 0; i--) {
|
||||||
|
|
||||||
|
let img = String(imagesWeWant[i])
|
||||||
|
|
||||||
|
if(
|
||||||
|
!img.includes('.jpg') &&
|
||||||
|
!img.includes('.jpeg') &&
|
||||||
|
!img.includes('.png') &&
|
||||||
|
!img.includes('.gif')
|
||||||
|
){
|
||||||
|
imagesWeWant.splice(i,1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Find if we have absolute thumbnails or not
|
||||||
|
let foundAbsolute = false
|
||||||
|
for (let i = imagesWeWant.length - 1; i >= 0; i--) {
|
||||||
|
|
||||||
|
let img = imagesWeWant[i]
|
||||||
|
|
||||||
|
//Add host name if its not included
|
||||||
|
if(String(img).includes('//') || String(img).includes('http')){
|
||||||
|
foundAbsolute = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Go through all found images. Grab the one closest to the top. Closer is better
|
||||||
|
for (let i = imagesWeWant.length - 1; i >= 0; i--) {
|
||||||
|
|
||||||
|
let img = imagesWeWant[i]
|
||||||
|
|
||||||
|
if(!String(img).includes('//') && foundAbsolute){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Only add host to images if no absolute images were found
|
||||||
|
if(!String(img).includes('//') ){
|
||||||
|
if(img.indexOf('/') != 0){
|
||||||
|
img = '/' + img
|
||||||
|
}
|
||||||
|
img = hostname + img
|
||||||
|
}
|
||||||
|
|
||||||
|
if(img.indexOf('//') == 0){
|
||||||
|
img = 'https:' + img //Scrape breaks without protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbnail = img
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('-----')
|
|
||||||
return thumbnail
|
return thumbnail
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,19 +162,28 @@ SiteScrape.getKeywords = ($) => {
|
|||||||
|
|
||||||
majorContent += $('[class*=content]').text()
|
majorContent += $('[class*=content]').text()
|
||||||
.replace(removeWhitespace, " ") //Remove all whitespace
|
.replace(removeWhitespace, " ") //Remove all whitespace
|
||||||
.replace(/\W\s/g, '') //Remove all non alphanumeric characters
|
// .replace(/\W\s/g, '') //Remove all non alphanumeric characters
|
||||||
.substring(0,3000) //Limit to 3000 characters
|
.substring(0,6000) //Limit to 6000 characters
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
.replace(/[^A-Za-z0-9- ]/g, '');
|
||||||
|
|
||||||
|
|
||||||
|
console.log(majorContent)
|
||||||
|
|
||||||
//Count frequency of each word in scraped text
|
//Count frequency of each word in scraped text
|
||||||
let frequency = {}
|
let frequency = {}
|
||||||
majorContent.split(' ').forEach(word => {
|
majorContent.split(' ').forEach(word => {
|
||||||
if(commonWords.includes(word)){
|
// Exclude short or common words
|
||||||
return //Exclude certain words
|
if(commonWords.includes(word) || word.length <= 2){
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if(!frequency[word]){
|
if(!frequency[word]){
|
||||||
frequency[word] = 0
|
frequency[word] = 0
|
||||||
}
|
}
|
||||||
|
// Skip some plurals
|
||||||
|
if(frequency[word+'s'] || frequency[word+'es']){
|
||||||
|
return
|
||||||
|
}
|
||||||
frequency[word]++
|
frequency[word]++
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -133,7 +201,7 @@ SiteScrape.getKeywords = ($) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let finalWords = []
|
let finalWords = []
|
||||||
for(let i=0; i<5; i++){
|
for(let i=0; i<6; i++){
|
||||||
if(sortable[i] && sortable[i][0]){
|
if(sortable[i] && sortable[i][0]){
|
||||||
finalWords.push(sortable[i][0])
|
finalWords.push(sortable[i][0])
|
||||||
}
|
}
|
||||||
|
115
server/index.js
@@ -1,7 +1,12 @@
|
|||||||
//Set up environmental variables, pulled from .env file used as process.env.DB_HOST
|
//Set up environmental variables, pulled from ~/.env file used as process.env.DB_HOST
|
||||||
const os = require('os') //Used to get path of home directory
|
const os = require('os') //Used to get path of home directory
|
||||||
const result = require('dotenv').config({ path:(os.homedir()+'/.env') })
|
const result = require('dotenv').config({ path:(os.homedir()+'/.env') })
|
||||||
|
|
||||||
|
const ports = {
|
||||||
|
express: 3000,
|
||||||
|
socketIo: 3001
|
||||||
|
}
|
||||||
|
|
||||||
//Allow user of @ in in require calls. Config in package.json
|
//Allow user of @ in in require calls. Config in package.json
|
||||||
require('module-alias/register')
|
require('module-alias/register')
|
||||||
|
|
||||||
@@ -15,7 +20,8 @@ const helmet = require('helmet')
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use( helmet() )
|
app.use( helmet() )
|
||||||
const port = 3000
|
// allow for the parsing of url encoded forms
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -51,12 +57,31 @@ io.on('connection', function(socket){
|
|||||||
Auth.decodeToken(token)
|
Auth.decodeToken(token)
|
||||||
.then(userData => {
|
.then(userData => {
|
||||||
socket.join(userData.userId)
|
socket.join(userData.userId)
|
||||||
|
|
||||||
|
//Track active logged in user accounts
|
||||||
|
const usersInRoom = io.sockets.adapter.rooms[userData.userId]
|
||||||
|
io.to(userData.userId).emit('update_active_user_count', usersInRoom.length)
|
||||||
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
//Don't add user to room if they are not logged in
|
//Don't add user to room if they are not logged in
|
||||||
// console.log(error)
|
// console.log(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
socket.on('get_active_user_count', token => {
|
||||||
|
Auth.decodeToken(token)
|
||||||
|
.then(userData => {
|
||||||
|
socket.join(userData.userId)
|
||||||
|
|
||||||
|
//Track active logged in user accounts
|
||||||
|
const usersInRoom = io.sockets.adapter.rooms[userData.userId]
|
||||||
|
io.to(userData.userId).emit('update_active_user_count', usersInRoom.length)
|
||||||
|
|
||||||
|
}).catch(error => {
|
||||||
|
// console.log(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
//Renew Session tokens when users request a new one
|
//Renew Session tokens when users request a new one
|
||||||
socket.on('renew_session_token', token => {
|
socket.on('renew_session_token', token => {
|
||||||
|
|
||||||
@@ -91,29 +116,12 @@ io.on('connection', function(socket){
|
|||||||
|
|
||||||
//Emit all sorted diffs to user
|
//Emit all sorted diffs to user
|
||||||
socket.emit('past_diffs', noteDiffs[rawTextId])
|
socket.emit('past_diffs', noteDiffs[rawTextId])
|
||||||
} else {
|
|
||||||
socket.emit('past_diffs', null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const usersInRoom = io.sockets.adapter.rooms[rawTextId]
|
const usersInRoom = io.sockets.adapter.rooms[rawTextId]
|
||||||
if(usersInRoom){
|
if(usersInRoom){
|
||||||
//Update users in room count
|
//Update users in room count
|
||||||
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
|
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
|
||||||
|
|
||||||
//Debugging text - prints out notes in limbo
|
|
||||||
let noteDiffKeys = Object.keys(noteDiffs)
|
|
||||||
let totalDiffs = 0
|
|
||||||
noteDiffKeys.forEach(diffSetKey => {
|
|
||||||
if(noteDiffs[diffSetKey]){
|
|
||||||
totalDiffs += noteDiffs[diffSetKey].length
|
|
||||||
}
|
|
||||||
})
|
|
||||||
//Debugging Text
|
|
||||||
if(noteDiffKeys.length > 0){
|
|
||||||
console.log('Total notes in limbo -> ', noteDiffKeys.length)
|
|
||||||
console.log('Total Diffs for all notes -> ', totalDiffs)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -139,31 +147,13 @@ io.on('connection', function(socket){
|
|||||||
|
|
||||||
noteDiffs[noteId].push(data)
|
noteDiffs[noteId].push(data)
|
||||||
|
|
||||||
//Remove duplicate diffs if they exist
|
// Go over each user in this note-room
|
||||||
for (var i = noteDiffs[noteId].length - 1; i >= 0; i--) {
|
|
||||||
|
|
||||||
let pastDiff = noteDiffs[noteId][i]
|
|
||||||
|
|
||||||
for (var j = noteDiffs[noteId].length - 1; j >= 0; j--) {
|
|
||||||
let currentDiff = noteDiffs[noteId][j]
|
|
||||||
|
|
||||||
if(i == j){
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if(currentDiff.diff == pastDiff.diff || currentDiff.time == pastDiff.time){
|
|
||||||
console.log('Removing Duplicate')
|
|
||||||
noteDiffs[noteId].splice(i,1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Each user joins a room when they open the app.
|
|
||||||
io.in(noteId).clients((error, clients) => {
|
io.in(noteId).clients((error, clients) => {
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
//Go through each client in note room and send them the diff
|
//Go through each client in note-room and send them the diff
|
||||||
clients.forEach(socketId => {
|
clients.forEach(socketId => {
|
||||||
|
// only send off diff if user
|
||||||
if(socketId != socket.id){
|
if(socketId != socket.id){
|
||||||
io.to(socketId).emit('incoming_diff', data)
|
io.to(socketId).emit('incoming_diff', data)
|
||||||
}
|
}
|
||||||
@@ -190,7 +180,6 @@ io.on('connection', function(socket){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo)
|
noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo)
|
||||||
|
|
||||||
if(noteDiffs[checkpoint.rawTextId].length == 0){
|
if(noteDiffs[checkpoint.rawTextId].length == 0){
|
||||||
@@ -205,14 +194,14 @@ io.on('connection', function(socket){
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('disconnect', function(){
|
socket.on('disconnect', function(socket){
|
||||||
// console.log('user disconnected');
|
// console.log('user disconnected');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
http.listen(3001, function(){
|
http.listen(ports.socketIo, function(){
|
||||||
console.log('socket.io liseting on port 3001');
|
console.log(`Socke.io: Listening on port ${ports.socketIo}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
//Enable json body parsing in requests. Allows me to post data in ajax calls
|
//Enable json body parsing in requests. Allows me to post data in ajax calls
|
||||||
@@ -253,22 +242,24 @@ app.use(function(req, res, next){
|
|||||||
|
|
||||||
|
|
||||||
// Test Area
|
// Test Area
|
||||||
const printResults = true
|
// const printResults = true
|
||||||
let UserTest = require('@models/User')
|
// let UserTest = require('@models/User')
|
||||||
let NoteTest = require('@models/Note')
|
// let NoteTest = require('@models/Note')
|
||||||
let AuthTest = require('@helpers/Auth')
|
// let AuthTest = require('@helpers/Auth')
|
||||||
|
// Auth.test()
|
||||||
Auth.test()
|
// UserTest.keyPairTest('genMan30', '1', printResults)
|
||||||
UserTest.keyPairTest('genMan30', '1', printResults)
|
// .then( ({testUserId, masterKey}) =>
|
||||||
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
|
// NoteTest.test(testUserId, masterKey, printResults))
|
||||||
.then( message => {
|
// .then( message => {
|
||||||
if(printResults) console.log(message)
|
// if(printResults) console.log(message)
|
||||||
Auth.testTwoFactor()
|
// Auth.testTwoFactor()
|
||||||
})
|
// })
|
||||||
|
// .catch((error) => {
|
||||||
|
// console.log(error)
|
||||||
|
// })
|
||||||
|
|
||||||
//Test
|
//Test
|
||||||
app.get('/api', (req, res) => res.send('Solidscribe API is up and running'))
|
app.get('/api', (req, res) => res.send('Solidscribe /API is up and running'))
|
||||||
|
|
||||||
//Serve up uploaded files
|
//Serve up uploaded files
|
||||||
app.use('/api/static', express.static( __dirname+'/../staticFiles' ))
|
app.use('/api/static', express.static( __dirname+'/../staticFiles' ))
|
||||||
@@ -297,9 +288,13 @@ app.use('/api/attachment', attachment)
|
|||||||
var quickNote = require('@routes/quicknoteController')
|
var quickNote = require('@routes/quicknoteController')
|
||||||
app.use('/api/quick-note', quickNote)
|
app.use('/api/quick-note', quickNote)
|
||||||
|
|
||||||
|
//cycle tracking endpoint
|
||||||
|
var metricTracking = require('@routes/metrictrackingController')
|
||||||
|
app.use('/api/metric-tracking', metricTracking)
|
||||||
|
|
||||||
//Output running status
|
//Output running status
|
||||||
app.listen(port, () => {
|
app.listen(ports.express, () => {
|
||||||
console.log(`Listening on port ${port}!`)
|
console.log(`Express: Listening on port ${ports.express}!`)
|
||||||
})
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
let db = require('@config/database')
|
let db = require('@config/database')
|
||||||
|
|
||||||
let SiteScrape = require('@helpers/SiteScrape')
|
let SiteScrape = require('@helpers/SiteScrape')
|
||||||
|
const cs = require('@helpers/CryptoString')
|
||||||
|
|
||||||
let Attachment = module.exports = {}
|
let Attachment = module.exports = {}
|
||||||
|
|
||||||
@@ -46,16 +47,28 @@ Attachment.textSearch = (userId, searchTerm) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Attachment.search = (userId, noteId, attachmentType, offset, setSize) => {
|
Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeShared) => {
|
||||||
|
console.log([userId, noteId, attachmentType, offset, setSize, includeShared])
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
let params = [userId]
|
let params = [userId]
|
||||||
let query = 'SELECT * FROM attachment WHERE user_id = ? AND visible = 1 '
|
let query = `
|
||||||
|
SELECT attachment.*, note.share_user_id FROM attachment
|
||||||
|
LEFT JOIN note ON (attachment.note_id = note.id)
|
||||||
|
WHERE attachment.user_id = ? AND visible = 1
|
||||||
|
`
|
||||||
|
|
||||||
if(noteId && noteId > 0){
|
if(noteId && noteId > 0){
|
||||||
query += 'AND note_id = ? '
|
//
|
||||||
|
// Show everything if note ID is present
|
||||||
|
//
|
||||||
|
query += 'AND attachment.note_id = ? '
|
||||||
params.push(noteId)
|
params.push(noteId)
|
||||||
}
|
|
||||||
|
} else {
|
||||||
|
//
|
||||||
|
// Other filters if NO note id
|
||||||
|
//
|
||||||
|
|
||||||
if(attachmentType == 'links'){
|
if(attachmentType == 'links'){
|
||||||
query += 'AND attachment_type = 1 '
|
query += 'AND attachment_type = 1 '
|
||||||
@@ -64,13 +77,30 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize) => {
|
|||||||
query += 'AND attachment_type > 1 '
|
query += 'AND attachment_type > 1 '
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query += `AND note.archived = ${ attachmentType == 'archived' ? '1':'0' } `
|
||||||
|
query += `AND note.trashed = ${ attachmentType == 'trashed' ? '1':'0' } `
|
||||||
|
|
||||||
|
if(!attachmentType){
|
||||||
|
// Null note ID means it was pushed by bookmarklet
|
||||||
|
query += 'OR attachment.note_id IS NULL '
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(!noteId){
|
||||||
|
const sharedOrNot = includeShared ? ' NOT ':' '
|
||||||
|
query += `AND note.share_user_id IS${sharedOrNot}NULL `
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
query += 'ORDER BY last_indexed DESC '
|
query += 'ORDER BY last_indexed DESC '
|
||||||
|
|
||||||
const limitOffset = parseInt(offset, 10) || 0 //Either parse int, or use zero
|
const limitOffset = parseInt(offset, 10) || 0 //Either parse int, or use zero
|
||||||
const parsedSetSize = parseInt(setSize, 10) || 20 //Either parse int, or use zero
|
const parsedSetSize = parseInt(setSize, 10) || 20
|
||||||
query += ` LIMIT ${limitOffset}, ${parsedSetSize}`
|
query += ` LIMIT ${limitOffset}, ${parsedSetSize}`
|
||||||
|
|
||||||
|
console.log(query)
|
||||||
|
|
||||||
db.promise()
|
db.promise()
|
||||||
.query(query, params)
|
.query(query, params)
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
@@ -80,18 +110,6 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//Returns all attachments
|
|
||||||
Attachment.forNote = (userId, noteId) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.promise()
|
|
||||||
.query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND visible = 1 ORDER BY last_indexed DESC;`, [userId, noteId])
|
|
||||||
.then((rows, fields) => {
|
|
||||||
resolve(rows[0]) //Return all attachments found by query
|
|
||||||
})
|
|
||||||
.catch(console.log)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Attachment.urlForNote = (userId, noteId) => {
|
Attachment.urlForNote = (userId, noteId) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.promise()
|
db.promise()
|
||||||
@@ -167,6 +185,7 @@ Attachment.delete = (userId, attachmentId, urlDelete = false) => {
|
|||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.catch(console.log)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,9 +302,13 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
|
|||||||
//Once everything is done being scraped, emit new attachment events
|
//Once everything is done being scraped, emit new attachment events
|
||||||
SocketIo.to(userId).emit('update_counts')
|
SocketIo.to(userId).emit('update_counts')
|
||||||
|
|
||||||
|
// Tell user to update attachments with scraped text
|
||||||
|
SocketIo.to(userId).emit('update_note_attachments')
|
||||||
|
|
||||||
solrAttachmentText += freshlyScrapedText
|
solrAttachmentText += freshlyScrapedText
|
||||||
resolve(solrAttachmentText)
|
resolve(solrAttachmentText)
|
||||||
})
|
})
|
||||||
|
.catch(console.log)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -313,9 +336,13 @@ Attachment.scrapeUrlsCreateAttachments = (userId, noteId, foundUrls) => {
|
|||||||
|
|
||||||
//All URLs have been scraped, return data
|
//All URLs have been scraped, return data
|
||||||
if(processedCount == foundUrls.length){
|
if(processedCount == foundUrls.length){
|
||||||
resolve(scrapedText)
|
console.log('All urls scraped')
|
||||||
|
return resolve(scrapedText)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log('Site Scrape error', error)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -325,17 +352,16 @@ Attachment.downloadFileFromUrl = (url) => {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
if(url == null){
|
if(!url){
|
||||||
resolve(null)
|
return resolve(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const random = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
const random = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
||||||
const extension = '.'+url.split('.').pop() //This is throwing an error
|
let extension = ''
|
||||||
let fileName = random+'_scrape'+extension
|
let fileName = random+'_scrape'
|
||||||
const thumbPath = 'thumb_'+fileName
|
let thumbPath = 'thumb_'+fileName
|
||||||
|
|
||||||
console.log('Scraping image url')
|
console.log('Scraping image url', url)
|
||||||
console.log(url)
|
|
||||||
|
|
||||||
console.log('Getting ready to scrape ', url)
|
console.log('Getting ready to scrape ', url)
|
||||||
|
|
||||||
@@ -347,6 +373,8 @@ Attachment.downloadFileFromUrl = (url) => {
|
|||||||
.on('response', res => {
|
.on('response', res => {
|
||||||
console.log(res.statusCode)
|
console.log(res.statusCode)
|
||||||
console.log(res.headers['content-type'])
|
console.log(res.headers['content-type'])
|
||||||
|
//Get mime type from header content type
|
||||||
|
// extension = '.'+String(res.headers['content-type']).split('/').pop()
|
||||||
})
|
})
|
||||||
.pipe(fs.createWriteStream(filePath+thumbPath))
|
.pipe(fs.createWriteStream(filePath+thumbPath))
|
||||||
.on('close', () => {
|
.on('close', () => {
|
||||||
@@ -354,21 +382,24 @@ Attachment.downloadFileFromUrl = (url) => {
|
|||||||
//resize image if its real big
|
//resize image if its real big
|
||||||
gm(filePath+thumbPath)
|
gm(filePath+thumbPath)
|
||||||
.resize(550) //Resize to width of 550 px
|
.resize(550) //Resize to width of 550 px
|
||||||
.quality(75) //compression level 0 - 100 (best)
|
.quality(85) //compression level 0 - 100 (best)
|
||||||
.write(filePath+thumbPath, function (err) {
|
.write(filePath+thumbPath, function (err) {
|
||||||
if(err){ console.log(err) }
|
if(err){
|
||||||
})
|
console.log(err)
|
||||||
|
return resolve(null)
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Saved Image')
|
console.log('Saved Image')
|
||||||
resolve(fileName)
|
return resolve(fileName)
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Attachment.processUrl = (userId, noteId, url) => {
|
Attachment.processUrl = (userId, noteId, url) => {
|
||||||
|
|
||||||
const scrapeTime = 20*1000;
|
const scrapeTime = 5*1000;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
@@ -396,7 +427,7 @@ Attachment.processUrl = (userId, noteId, url) => {
|
|||||||
.query(`INSERT INTO attachment
|
.query(`INSERT INTO attachment
|
||||||
(note_id, user_id, attachment_type, text, url, last_indexed, file_location)
|
(note_id, user_id, attachment_type, text, url, last_indexed, file_location)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[noteId, userId, 1, 'Processing...', url, created, null])
|
[noteId, userId, 1, url, url, created, null])
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
//Set two bigger variables then return request for processing
|
//Set two bigger variables then return request for processing
|
||||||
request = rp(options)
|
request = rp(options)
|
||||||
@@ -421,9 +452,12 @@ Attachment.processUrl = (userId, noteId, url) => {
|
|||||||
const keywords = SiteScrape.getKeywords($)
|
const keywords = SiteScrape.getKeywords($)
|
||||||
|
|
||||||
var desiredSearchText = ''
|
var desiredSearchText = ''
|
||||||
desiredSearchText += pageTitle + "\n"
|
desiredSearchText += pageTitle
|
||||||
desiredSearchText += keywords
|
if(keywords){
|
||||||
|
desiredSearchText += "\n " + keywords
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Results from site scrape-------------')
|
||||||
console.log({
|
console.log({
|
||||||
pageTitle,
|
pageTitle,
|
||||||
hostname,
|
hostname,
|
||||||
@@ -473,40 +507,142 @@ Attachment.processUrl = (userId, noteId, url) => {
|
|||||||
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
// console.log('Scrape pooped out')
|
console.log('Scrape pooped out')
|
||||||
// console.log('Issue with scrape')
|
console.log('Issue with scrape', error.statusCode)
|
||||||
console.log(error)
|
clearTimeout(requestTimeout)
|
||||||
// resolve('')
|
return resolve('No site text')
|
||||||
})
|
})
|
||||||
|
|
||||||
requestTimeout = setTimeout( () => {
|
requestTimeout = setTimeout( () => {
|
||||||
console.log('Cancel the request, its taking to long.')
|
console.log('Cancel the request, its taking to long.')
|
||||||
request.cancel()
|
request.cancel()
|
||||||
|
return resolve('Request Timeout')
|
||||||
desiredSearchText = 'No Description for -> '+url
|
|
||||||
|
|
||||||
created = Math.round((+new Date)/1000)
|
|
||||||
db.promise()
|
|
||||||
.query(`UPDATE attachment SET
|
|
||||||
text = ?,
|
|
||||||
last_indexed = ?,
|
|
||||||
WHERE id = ?
|
|
||||||
`, [desiredSearchText, created, insertedId])
|
|
||||||
.then((rows, fields) => {
|
|
||||||
resolve(desiredSearchText) //Return found text
|
|
||||||
})
|
|
||||||
.catch(console.log)
|
|
||||||
|
|
||||||
//Create attachment in DB with scrape text and provided data
|
|
||||||
// db.promise()
|
|
||||||
// .query(`INSERT INTO attachment
|
|
||||||
// (note_id, user_id, attachment_type, text, url, last_indexed)
|
|
||||||
// VALUES (?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created])
|
|
||||||
// .then((rows, fields) => {
|
|
||||||
// resolve(desiredSearchText) //Return found text
|
|
||||||
// })
|
|
||||||
// .catch(console.log)
|
|
||||||
|
|
||||||
}, scrapeTime )
|
}, scrapeTime )
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Attachment.generatePushKey = (userId) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
db.promise()
|
||||||
|
.query("SELECT pushkey FROM user WHERE id = ? LIMIT 1", [userId])
|
||||||
|
.then((rows, fields) => {
|
||||||
|
|
||||||
|
const pushKey = rows[0][0].pushkey
|
||||||
|
|
||||||
|
// push key exists
|
||||||
|
if(pushKey && pushKey.length > 0){
|
||||||
|
|
||||||
|
return resolve(pushKey)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// generate and save a new key
|
||||||
|
const newPushKey = cs.createSmallSalt()
|
||||||
|
db.promise()
|
||||||
|
.query('UPDATE user SET pushkey = ? WHERE id = ? LIMIT 1', [newPushKey,userId])
|
||||||
|
.then((rows, fields) => {
|
||||||
|
|
||||||
|
return resolve(newPushKey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Attachment.deletePushKey = (userId) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
db.promise()
|
||||||
|
.query('UPDATE user SET pushkey = null WHERE id = ? LIMIT 1', [userId])
|
||||||
|
.then((rows, fields) => {
|
||||||
|
|
||||||
|
return resolve(rows[0].affectedRows == 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Attachment.getPushkeyBookmarklet = (userId) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
Attachment.generatePushKey(userId)
|
||||||
|
.then( pushKey => {
|
||||||
|
|
||||||
|
let bookmarklet = Attachment.generateBookmarkletText(pushKey)
|
||||||
|
return resolve(bookmarklet)
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Attachment.pushUrl = (pushkey,url) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
let userId = null
|
||||||
|
pushkey = pushkey.replace(/ /g, '+')
|
||||||
|
|
||||||
|
db.promise()
|
||||||
|
.query("SELECT id FROM user WHERE pushkey = ? LIMIT 1", [pushkey])
|
||||||
|
.then((rows, fields) => {
|
||||||
|
|
||||||
|
if(rows[0].length == 0){
|
||||||
|
return resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
userId = rows[0][0].id
|
||||||
|
return Attachment.scrapeUrlsCreateAttachments(userId, null, [url])
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
|
||||||
|
if(typeof SocketIo != 'undefined'){
|
||||||
|
//Once everything is done being scraped, emit new attachment events
|
||||||
|
SocketIo.to(userId).emit('update_counts')
|
||||||
|
|
||||||
|
// Tell user to update attachments with scraped text
|
||||||
|
SocketIo.to(userId).emit('update_note_attachments')
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(true)
|
||||||
|
})
|
||||||
|
.catch(console.log)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Attachment.generateBookmarkletText = (pushKey) => {
|
||||||
|
|
||||||
|
const endpoint = '/api/public/pushmebaby'
|
||||||
|
let url = 'https://www.solidscribe.com' + endpoint
|
||||||
|
if(process.env.NODE_ENV === 'development'){
|
||||||
|
// url = 'https://192.168.1.164' + endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminate each line with a semi-colon, super important, since spaces are removed.
|
||||||
|
|
||||||
|
// document.getElementById(id).remove();
|
||||||
|
url += '?pushkey='+encodeURIComponent(pushKey)
|
||||||
|
const bookmarkletV3 = `
|
||||||
|
javascript: (() => {
|
||||||
|
var p = encodeURIComponent(window.location.href);
|
||||||
|
var n = "`+url+`&url="+p;
|
||||||
|
window.open(n, '_blank', 'noopener=noopener');
|
||||||
|
window.focus();
|
||||||
|
|
||||||
|
var k = document.createElement("div");
|
||||||
|
k.setAttribute("style", "position:fixed;right:10px;top:10px;z-index:222222;border-radius:4px;font-size:1.3em;padding:20px 15px;background: #8f51be;color:white;");
|
||||||
|
k.innerHTML = "Posted URL to your Solid Scribe account";
|
||||||
|
|
||||||
|
document.body.appendChild(k);
|
||||||
|
|
||||||
|
setTimeout(()=>{
|
||||||
|
k.remove();
|
||||||
|
},5000);
|
||||||
|
|
||||||
|
})();
|
||||||
|
`
|
||||||
|
|
||||||
|
return bookmarkletV3
|
||||||
|
.replace(/\t|\r|\n/gm, "") // Remove tabs, new lines, returns
|
||||||
|
.replace(/\s+/g, ' ') // remove double spaces
|
||||||
|
.trim()
|
||||||
|
}
|
71
server/models/MetricTracking.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
let db = require('@config/database')
|
||||||
|
|
||||||
|
let Note = require('@models/Note')
|
||||||
|
|
||||||
|
let MetricTracking = module.exports = {};
|
||||||
|
|
||||||
|
|
||||||
|
MetricTracking.get = (userId, masterKey) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
db.promise()
|
||||||
|
.query(`
|
||||||
|
SELECT note.id FROM note WHERE quick_note = 2 AND user_id = ? LIMIT 1`, [userId])
|
||||||
|
.then((rows, fields) => {
|
||||||
|
|
||||||
|
//Quick Note is set, return note object
|
||||||
|
if(rows[0][0] != undefined){
|
||||||
|
|
||||||
|
let noteId = rows[0][0].id
|
||||||
|
const note = Note.get(userId, noteId, masterKey)
|
||||||
|
.then(noteData => {
|
||||||
|
return resolve(noteData)
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return resolve('no data')
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(console.log)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
MetricTracking.create = (userId, masterKey) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let finalId = null
|
||||||
|
return Note.create(userId, 'Metric Tracking', '', masterKey)
|
||||||
|
.then(insertedId => {
|
||||||
|
finalId = insertedId
|
||||||
|
db.promise().query('UPDATE note SET quick_note = 2 WHERE id = ? AND user_id = ?',[insertedId, userId])
|
||||||
|
.then((rows, fields) => {
|
||||||
|
|
||||||
|
const note = Note.get(userId, finalId, masterKey)
|
||||||
|
.then(noteData => {
|
||||||
|
return resolve(noteData)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(console.log)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MetricTracking.save = (userId, metricData, masterKey) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
let finalId = null
|
||||||
|
|
||||||
|
MetricTracking.get(userId, masterKey)
|
||||||
|
.then(noteObject => {
|
||||||
|
|
||||||
|
return Note.update(userId, noteObject.id, metricData, noteObject.title, noteObject.color, noteObject.pinned, noteObject.archived, null, masterKey)
|
||||||
|
|
||||||
|
})
|
||||||
|
.then( saveResults => {
|
||||||
|
return resolve(saveResults)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
@@ -17,6 +17,7 @@ const fs = require('fs')
|
|||||||
const gm = require('gm')
|
const gm = require('gm')
|
||||||
|
|
||||||
Note.test = (userId, masterKey, printResults) => {
|
Note.test = (userId, masterKey, printResults) => {
|
||||||
|
return false;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
|
||||||
@@ -162,6 +163,10 @@ Note.test = (userId, masterKey, printResults) => {
|
|||||||
return resolve('Test: Complete ---')
|
return resolve('Test: Complete ---')
|
||||||
|
|
||||||
})
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error)
|
||||||
|
return reject(error)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +198,7 @@ Note.create = (userId, noteTitle = '', noteText = '', masterKey) => {
|
|||||||
})
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
if(SocketIo){
|
if(typeof SocketIo != 'undefined'){
|
||||||
SocketIo.to(userId).emit('new_note_created', rows[0].insertId)
|
SocketIo.to(userId).emit('new_note_created', rows[0].insertId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +346,7 @@ Note.reindex = (userId, masterKey, removeId = null) => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
||||||
if(masterKey == null || note.salt == null){
|
if(masterKey == null || note.salt == null){
|
||||||
console.log('Error indexing note', note.id)
|
console.log('Error indexing note - master key or salt missing', note.id)
|
||||||
return resolve(true)
|
return resolve(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,13 +395,13 @@ Note.reindex = (userId, masterKey, removeId = null) => {
|
|||||||
|
|
||||||
return Promise.all(reindexQueue)
|
return Promise.all(reindexQueue)
|
||||||
})
|
})
|
||||||
.then(rawSearchIndex => {
|
.then(updatePromiseResults => {
|
||||||
|
|
||||||
const created = Math.round((+new Date)/1000)
|
const created = Math.round((+new Date)/1000)
|
||||||
const jsonSearchIndex = JSON.stringify(searchIndex)
|
const jsonSearchIndex = JSON.stringify(searchIndex)
|
||||||
const encryptedJsonIndex = cs.encrypt(masterKey, searchIndexSalt, jsonSearchIndex)
|
const encryptedJsonIndex = cs.encrypt(masterKey, searchIndexSalt, jsonSearchIndex)
|
||||||
|
|
||||||
return db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
|
db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
|
||||||
[encryptedJsonIndex, created, userId])
|
[encryptedJsonIndex, created, userId])
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
@@ -406,6 +411,7 @@ Note.reindex = (userId, masterKey, removeId = null) => {
|
|||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
// console.log('Indexd Note Count: ' + rows[0]['affectedRows'])
|
// console.log('Indexd Note Count: ' + rows[0]['affectedRows'])
|
||||||
|
// @TODO - Return number of reindexed notes
|
||||||
resolve(true)
|
resolve(true)
|
||||||
|
|
||||||
})
|
})
|
||||||
@@ -442,6 +448,10 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
|
|||||||
})
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
|
if(!rows[0] || !rows[0][0] || !rows[0][0]['note_raw_text_id']){
|
||||||
|
return reject(false)
|
||||||
|
}
|
||||||
|
|
||||||
const textId = rows[0][0]['note_raw_text_id']
|
const textId = rows[0][0]['note_raw_text_id']
|
||||||
let salt = rows[0][0]['salt']
|
let salt = rows[0][0]['salt']
|
||||||
let snippetSalt = rows[0][0]['snippet_salt']
|
let snippetSalt = rows[0][0]['snippet_salt']
|
||||||
@@ -503,12 +513,12 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
|
|||||||
})
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
if(SocketIo){
|
if(typeof SocketIo != 'undefined'){
|
||||||
SocketIo.to(userId).emit('new_note_text_saved', {noteId, hash})
|
SocketIo.to(userId).emit('new_note_text_saved', {noteId, hash})
|
||||||
}
|
|
||||||
|
|
||||||
//Async attachment reindex
|
//Async attachment reindex
|
||||||
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
|
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
|
||||||
|
}
|
||||||
|
|
||||||
//Send back updated response
|
//Send back updated response
|
||||||
resolve(rows[0])
|
resolve(rows[0])
|
||||||
@@ -658,6 +668,9 @@ Note.delete = (userId, noteId, masterKey = null) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Returns noteData
|
||||||
|
//
|
||||||
Note.get = (userId, noteId, masterKey) => {
|
Note.get = (userId, noteId, masterKey) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
@@ -681,6 +694,7 @@ Note.get = (userId, noteId, masterKey) => {
|
|||||||
note_raw_text.text,
|
note_raw_text.text,
|
||||||
note_raw_text.salt,
|
note_raw_text.salt,
|
||||||
note_raw_text.updated as updated,
|
note_raw_text.updated as updated,
|
||||||
|
GROUP_CONCAT(DISTINCT(tag.text) ORDER BY tag.text DESC) AS tags,
|
||||||
note.id,
|
note.id,
|
||||||
note.user_id,
|
note.user_id,
|
||||||
note.created,
|
note.created,
|
||||||
@@ -697,7 +711,9 @@ Note.get = (userId, noteId, masterKey) => {
|
|||||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||||
LEFT JOIN attachment ON (note.id = attachment.note_id)
|
LEFT JOIN attachment ON (note.id = attachment.note_id)
|
||||||
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
|
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
|
||||||
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, noteId])
|
LEFT JOIN note_tag ON (note.id = note_tag.note_id AND note_tag.user_id = ?)
|
||||||
|
LEFT JOIN tag ON (note_tag.tag_id = tag.id)
|
||||||
|
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, userId, noteId])
|
||||||
|
|
||||||
})
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
@@ -729,12 +745,13 @@ Note.get = (userId, noteId, masterKey) => {
|
|||||||
|
|
||||||
const nowTime = Math.round((+new Date)/1000)
|
const nowTime = Math.round((+new Date)/1000)
|
||||||
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
|
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
|
||||||
|
.then(results => {
|
||||||
//Return note data
|
//Return note data
|
||||||
// delete noteData.salt //remove salt from return data
|
// delete noteData.salt //remove salt from return data
|
||||||
// delete noteData.encrypted_share_password_key
|
// delete noteData.encrypted_share_password_key
|
||||||
noteData.lockedOut = noteLockedOut
|
noteData.lockedOut = noteLockedOut
|
||||||
resolve(noteData)
|
resolve(noteData)
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -985,6 +1002,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.visible = 1)
|
LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.visible = 1)
|
||||||
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
|
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
|
||||||
WHERE note.user_id = ?
|
WHERE note.user_id = ?
|
||||||
|
AND note.quick_note <= 1
|
||||||
`
|
`
|
||||||
|
|
||||||
//If text search returned results, limit search to those ids
|
//If text search returned results, limit search to those ids
|
||||||
|
@@ -13,11 +13,14 @@ QuickNote.get = (userId, masterKey) => {
|
|||||||
SELECT note.id FROM note WHERE quick_note = 1 AND user_id = ? LIMIT 1`, [userId])
|
SELECT note.id FROM note WHERE quick_note = 1 AND user_id = ? LIMIT 1`, [userId])
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
//Quick Note is set, return note text
|
//Quick Note is set, return note object
|
||||||
if(rows[0][0] != undefined){
|
if(rows[0][0] != undefined){
|
||||||
|
|
||||||
let noteId = rows[0][0].id
|
let noteId = rows[0][0].id
|
||||||
return resolve({'noteId':noteId})
|
const note = Note.get(userId, noteId, masterKey)
|
||||||
|
.then(noteData => {
|
||||||
|
return resolve(noteData)
|
||||||
|
})
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
//Or create a new note and get the id
|
//Or create a new note and get the id
|
||||||
@@ -81,7 +84,7 @@ QuickNote.update = (userId, pushText, masterKey) => {
|
|||||||
.replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities
|
.replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities
|
||||||
.replace(/<[^>]+>/g, '') //Rip out all HTML tags
|
.replace(/<[^>]+>/g, '') //Rip out all HTML tags
|
||||||
|
|
||||||
//Turn links into actual linx
|
//Turn links into actual link
|
||||||
clean = QuickNote.makeUrlLink(clean)
|
clean = QuickNote.makeUrlLink(clean)
|
||||||
|
|
||||||
if(clean == ''){ clean = ' ' }
|
if(clean == ''){ clean = ' ' }
|
||||||
@@ -114,7 +117,7 @@ QuickNote.update = (userId, pushText, masterKey) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then( saveResults => {
|
.then( saveResults => {
|
||||||
return resolve(true)
|
return resolve(saveResults)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -138,6 +138,33 @@ Tag.get = (userId, noteId) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Get just tag string for note
|
||||||
|
//
|
||||||
|
Tag.fornote = (userId, noteId) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
|
||||||
|
db.promise()
|
||||||
|
.query(`SELECT GROUP_CONCAT(DISTINCT(tag.text) ORDER BY tag.text DESC) AS tags
|
||||||
|
FROM note_tag
|
||||||
|
LEFT JOIN tag ON (note_tag.tag_id = tag.id)
|
||||||
|
WHERE note_tag.note_id = ?
|
||||||
|
AND user_id = ?;
|
||||||
|
`, [noteId,userId])
|
||||||
|
.then((rows, fields) => {
|
||||||
|
|
||||||
|
//pull IDs out of returned results
|
||||||
|
// let ids = rows[0].map( item => {})
|
||||||
|
|
||||||
|
resolve( rows[0][0] ) //Return all tags found by query
|
||||||
|
})
|
||||||
|
.catch(console.log)
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Get all tags for a note and concatinate into a string 'all, tags, like, this'
|
// Get all tags for a note and concatinate into a string 'all, tags, like, this'
|
||||||
//
|
//
|
||||||
|
@@ -9,7 +9,8 @@ const speakeasy = require('speakeasy')
|
|||||||
|
|
||||||
let User = module.exports = {}
|
let User = module.exports = {}
|
||||||
|
|
||||||
const version = '3.3.1'
|
const version = '3.8.0'
|
||||||
|
// 3.7.3 - diff/patch update
|
||||||
|
|
||||||
//Login a user, if that user does not exist create them
|
//Login a user, if that user does not exist create them
|
||||||
//Issues login token
|
//Issues login token
|
||||||
@@ -193,17 +194,19 @@ User.register = (username, password) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Counts notes, pinned notes, archived notes, shared notes, unread notes, total files and types
|
//Counts notes, pinned notes, archived notes, shared notes, unread notes, total files and types
|
||||||
User.getCounts = (userId) => {
|
User.getCounts = (userId, extendedOptions) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
let countTotals = {}
|
let countTotals = {
|
||||||
const userHash = cs.hash(String(userId)).toString('base64')
|
tags: {}
|
||||||
|
}
|
||||||
|
// const userHash = cs.hash(String(userId)).toString('base64')
|
||||||
|
|
||||||
db.promise().query(
|
db.promise().query(
|
||||||
`SELECT
|
`SELECT
|
||||||
SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes,
|
SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes,
|
||||||
SUM(trashed = 1) AS trashedNotes,
|
SUM(trashed = 1) AS trashedNotes,
|
||||||
SUM(share_user_id IS NULL && trashed = 0) AS totalNotes,
|
SUM(share_user_id IS NULL && trashed = 0 AND quick_note < 2) AS totalNotes,
|
||||||
SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount,
|
SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount,
|
||||||
SUM(share_user_id != ? && trashed = 0) AS sharedToNotes
|
SUM(share_user_id != ? && trashed = 0) AS sharedToNotes
|
||||||
FROM note
|
FROM note
|
||||||
@@ -244,17 +247,73 @@ User.getCounts = (userId) => {
|
|||||||
|
|
||||||
Object.assign(countTotals, rows[0][0]) //combine results
|
Object.assign(countTotals, rows[0][0]) //combine results
|
||||||
|
|
||||||
|
//Count usages of user tags, sort by most popular
|
||||||
|
return db.promise().query(`
|
||||||
|
SELECT
|
||||||
|
tag.text, COUNT(tag_id) AS uses, tag.id
|
||||||
|
FROM note_tag
|
||||||
|
JOIN tag ON (tag.id = note_tag.tag_id)
|
||||||
|
WHERE user_id = ?
|
||||||
|
GROUP BY tag_id
|
||||||
|
ORDER BY uses DESC
|
||||||
|
LIMIT 16
|
||||||
|
`, [userId])
|
||||||
|
|
||||||
|
}).then( (rows, fields) => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Convert everything to an int or 0
|
//Convert everything to an int or 0
|
||||||
Object.keys(countTotals).forEach( key => {
|
Object.keys(countTotals).forEach( key => {
|
||||||
const count = parseInt(countTotals[key])
|
const count = parseInt(countTotals[key])
|
||||||
countTotals[key] = count ? count : 0
|
countTotals[key] = count ? count : 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//Build out tags object
|
||||||
|
let tagsObject = {}
|
||||||
|
rows[0].forEach(tagRow => {
|
||||||
|
tagsObject[tagRow['text']] = {'id':tagRow.id, 'uses':tagRow.uses}
|
||||||
|
})
|
||||||
|
|
||||||
|
//Assign after counts are updated
|
||||||
|
countTotals['tags'] = tagsObject
|
||||||
|
|
||||||
countTotals['currentVersion'] = version
|
countTotals['currentVersion'] = version
|
||||||
|
|
||||||
|
// Allow for extended options set on page load
|
||||||
|
if(extendedOptions){
|
||||||
|
|
||||||
|
db.promise().query(
|
||||||
|
`SELECT updated FROM note
|
||||||
|
JOIN note_raw_text ON note_raw_text.id = note.note_raw_text_id
|
||||||
|
WHERE note.quick_note = 2
|
||||||
|
AND user_id = ?`, [userId])
|
||||||
|
.then( (rows, fields) => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if(rows[0][0] && rows[0][0].updated){
|
||||||
|
const lastOpened = rows[0][0].updated
|
||||||
|
const timeDiff = Math.round(((+new Date) - (lastOpened))/1000)
|
||||||
|
const hoursInSeconds = (12 * 60 * 60) //12 hours
|
||||||
|
|
||||||
|
// Show metric tracking button if its been 12 hours since last entry
|
||||||
|
if(lastOpened && timeDiff > hoursInSeconds){
|
||||||
|
countTotals['showTrackMetricsButton'] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
resolve(countTotals)
|
resolve(countTotals)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
resolve(countTotals)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,6 +553,12 @@ User.revokeActiveSessions = (userId, sessionId) => {
|
|||||||
|
|
||||||
User.deleteUser = (userId, password) => {
|
User.deleteUser = (userId, password) => {
|
||||||
|
|
||||||
|
if(!userId || !password){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
return resolve('Missing User ID or Password. No Action Taken.')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
//Verify user is correct by decryptig master key with password
|
//Verify user is correct by decryptig master key with password
|
||||||
|
|
||||||
let deletePromises = []
|
let deletePromises = []
|
||||||
@@ -526,77 +591,3 @@ User.deleteUser = (userId, password) => {
|
|||||||
|
|
||||||
return Promise.all(deletePromises)
|
return Promise.all(deletePromises)
|
||||||
}
|
}
|
||||||
|
|
||||||
User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
let masterKey = null
|
|
||||||
let testUserId = null
|
|
||||||
|
|
||||||
|
|
||||||
const randomUsername = Math.random().toString(36).substring(2, 15);
|
|
||||||
const randomPassword = '1'
|
|
||||||
const secondPassword = '2'
|
|
||||||
|
|
||||||
User.register(testUserName, password)
|
|
||||||
.then( ({ token, userId }) => {
|
|
||||||
testUserId = userId
|
|
||||||
|
|
||||||
if(printResults) console.log('Test: Register User '+testUserName+' - Pass')
|
|
||||||
|
|
||||||
return User.getMasterKey(testUserId, password)
|
|
||||||
})
|
|
||||||
.then(newMasterKey => {
|
|
||||||
masterKey = newMasterKey
|
|
||||||
|
|
||||||
if(printResults) console.log('Test: Generate/Decrypt Master Key - Pass')
|
|
||||||
|
|
||||||
return User.generateKeypair(testUserId, masterKey)
|
|
||||||
})
|
|
||||||
.then(({publicKey, privateKey}) => {
|
|
||||||
|
|
||||||
const publicKeyMessage = 'Test: Public key decrypt - Pass'
|
|
||||||
const privateKeyMessage = 'Test: Private key decrypt - Pass'
|
|
||||||
|
|
||||||
//Encrypt Message with private Key
|
|
||||||
const privateKeyEncrypted = crypto.privateEncrypt(privateKey, Buffer.from(privateKeyMessage, 'utf8')).toString('base64')
|
|
||||||
const decryptedPrivate = crypto.publicDecrypt(publicKey, Buffer.from(privateKeyEncrypted, 'base64'))
|
|
||||||
//Conver back to a string
|
|
||||||
if(printResults) console.log(decryptedPrivate.toString('utf8'))
|
|
||||||
|
|
||||||
//Encrypt with public key
|
|
||||||
const pubEncrMsc = crypto.publicEncrypt(publicKey, Buffer.from(publicKeyMessage, 'utf8')).toString('base64')
|
|
||||||
const publicDeccryptMessage = crypto.privateDecrypt(privateKey, Buffer.from(pubEncrMsc, 'base64') )
|
|
||||||
//Convert it back to string
|
|
||||||
if(printResults) console.log(publicDeccryptMessage.toString('utf8'))
|
|
||||||
|
|
||||||
return User.login(testUserName, password)
|
|
||||||
})
|
|
||||||
.then( ({token, userId}) => {
|
|
||||||
|
|
||||||
if(printResults) console.log('Test: Login New User - Pass')
|
|
||||||
|
|
||||||
return User.changePassword(testUserId, randomPassword, secondPassword)
|
|
||||||
|
|
||||||
})
|
|
||||||
.then(passwordChangeResults => {
|
|
||||||
|
|
||||||
if(printResults) console.log('Test: Password Change - ', passwordChangeResults?'Pass':'Fail')
|
|
||||||
|
|
||||||
return User.login(testUserName, secondPassword)
|
|
||||||
|
|
||||||
})
|
|
||||||
.then(reLogin => {
|
|
||||||
|
|
||||||
if(printResults) console.log('Test: Login With new Password - Pass')
|
|
||||||
|
|
||||||
return User.getMasterKey(testUserId, secondPassword)
|
|
||||||
})
|
|
||||||
.then(newMasterKey => {
|
|
||||||
|
|
||||||
masterKey = newMasterKey
|
|
||||||
|
|
||||||
resolve({testUserId, masterKey})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@@ -26,7 +26,7 @@ router.use(function setUserId (req, res, next) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.post('/search', function (req, res) {
|
router.post('/search', function (req, res) {
|
||||||
Attachment.search(userId, req.body.noteId, req.body.attachmentType, req.body.offset, req.body.setSize)
|
Attachment.search(userId, req.body.noteId, req.body.attachmentType, req.body.offset, req.body.setSize, req.body.includeShared)
|
||||||
.then( data => res.send(data) )
|
.then( data => res.send(data) )
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -35,11 +35,6 @@ router.post('/textsearch', function (req, res) {
|
|||||||
.then( data => res.send(data) )
|
.then( data => res.send(data) )
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/get', function (req, res) {
|
|
||||||
Attachment.forNote(userId, req.body.noteId)
|
|
||||||
.then( data => res.send(data) )
|
|
||||||
})
|
|
||||||
|
|
||||||
router.post('/update', function (req, res) {
|
router.post('/update', function (req, res) {
|
||||||
Attachment.update(userId, req.body.attachmentId, req.body.updatedText, req.body.noteId)
|
Attachment.update(userId, req.body.attachmentId, req.body.updatedText, req.body.noteId)
|
||||||
.then( result => {
|
.then( result => {
|
||||||
@@ -65,5 +60,26 @@ router.post('/upload', upload.single('file'), function (req, res, next) {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Push URL to attachments
|
||||||
|
// push action on - public controller
|
||||||
|
//
|
||||||
|
|
||||||
|
// get push key
|
||||||
|
router.post('/getbookmarklet', function (req, res) {
|
||||||
|
|
||||||
|
Attachment.getPushkeyBookmarklet(userId)
|
||||||
|
.then( data => res.send(data) )
|
||||||
|
})
|
||||||
|
|
||||||
|
// generate new push key
|
||||||
|
router.post('/generatepushkey', function (req, res) {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// delete push key
|
||||||
|
router.post('/deletepushkey', function (req, res) {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
45
server/routes/metrictrackingController.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// /api/metric-tracking
|
||||||
|
//
|
||||||
|
|
||||||
|
var express = require('express')
|
||||||
|
var router = express.Router()
|
||||||
|
|
||||||
|
let MetricTracking = require('@models/MetricTracking');
|
||||||
|
|
||||||
|
let userId = null
|
||||||
|
let masterKey = null
|
||||||
|
|
||||||
|
// middleware that is specific to this router
|
||||||
|
router.use(function setUserId (req, res, next) {
|
||||||
|
|
||||||
|
//Session key is required to continue
|
||||||
|
if(!req.headers.sessionId){
|
||||||
|
next('Unauthorized')
|
||||||
|
}
|
||||||
|
|
||||||
|
if(req.headers.userId){
|
||||||
|
userId = req.headers.userId
|
||||||
|
masterKey = req.headers.masterKey
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post('/get', function (req, res) {
|
||||||
|
MetricTracking.get(userId, masterKey)
|
||||||
|
.then( data => res.send(data) )
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post('/create', function (req, res) {
|
||||||
|
MetricTracking.create(userId, masterKey)
|
||||||
|
.then( data => res.send(data) )
|
||||||
|
})
|
||||||
|
|
||||||
|
//Push text to quick note
|
||||||
|
router.post('/save', function (req, res) {
|
||||||
|
MetricTracking.save(userId, req.body.cycleData, masterKey)
|
||||||
|
.then( data => res.send(data) )
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router
|
@@ -4,6 +4,7 @@ const rateLimit = require('express-rate-limit')
|
|||||||
|
|
||||||
const Note = require('@models/Note')
|
const Note = require('@models/Note')
|
||||||
const User = require('@models/User')
|
const User = require('@models/User')
|
||||||
|
const Attachment = require('@models/Attachment')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -56,6 +57,29 @@ router.post('/register', registerLimiter, function (req, res) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Public Pushme Action
|
||||||
|
//
|
||||||
|
const pushMeLimiter = rateLimit({
|
||||||
|
windowMs: 30 * 60 * 1000, //30 min window
|
||||||
|
max: 50, // start blocking after x requests
|
||||||
|
message:'Error'
|
||||||
|
})
|
||||||
|
router.get('/pushmebaby', pushMeLimiter, function (req, res) {
|
||||||
|
|
||||||
|
|
||||||
|
Attachment.pushUrl(req.query.pushkey, req.query.url)
|
||||||
|
.then((() => {
|
||||||
|
const jsCode = `
|
||||||
|
<script>
|
||||||
|
window.close();
|
||||||
|
</script>
|
||||||
|
<h1>Posting URL</h1>
|
||||||
|
`;
|
||||||
|
res.header('Content-Security-Policy', "script-src 'unsafe-inline'");
|
||||||
|
res.set('Content-Type', 'text/html');
|
||||||
|
res.send(Buffer.from(jsCode));
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
@@ -50,6 +50,12 @@ router.post('/get', function (req, res) {
|
|||||||
.then( data => res.send(data) )
|
.then( data => res.send(data) )
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//Get the latest notes the user has created
|
||||||
|
router.post('/fornote', function (req, res) {
|
||||||
|
Tags.fornote(userId, req.body.noteId)
|
||||||
|
.then( data => res.send(data) )
|
||||||
|
})
|
||||||
|
|
||||||
//Get all the tags for this user in order of usage
|
//Get all the tags for this user in order of usage
|
||||||
router.post('/usertags', function (req, res) {
|
router.post('/usertags', function (req, res) {
|
||||||
Tags.userTags(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters)
|
Tags.userTags(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters)
|
||||||
|
@@ -53,7 +53,7 @@ router.post('/revokesessions', function(req, res) {
|
|||||||
|
|
||||||
// fetch counts of users notes
|
// fetch counts of users notes
|
||||||
router.post('/totals', function (req, res) {
|
router.post('/totals', function (req, res) {
|
||||||
User.getCounts(req.headers.userId)
|
User.getCounts(req.headers.userId, req.body.extendedOptions)
|
||||||
.then( countsObject => res.send( countsObject ))
|
.then( countsObject => res.send( countsObject ))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
100
server/tests/models/Attachment.test.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
const Attachment = require('../../models/Attachment')
|
||||||
|
const User = require('../../models/User')
|
||||||
|
|
||||||
|
const testUserName = 'jestTestUserAttachment'
|
||||||
|
const password = 'Beans19934!!!'
|
||||||
|
|
||||||
|
let newUserId = null
|
||||||
|
let masterKey = null
|
||||||
|
let newPushKey = null
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
|
||||||
|
// Find and Delete Previous Test user, log in, get key
|
||||||
|
return User.getByUserName(testUserName)
|
||||||
|
.then((user) => {
|
||||||
|
return User.deleteUser(user?.id, password)
|
||||||
|
})
|
||||||
|
.then((results) => {
|
||||||
|
|
||||||
|
return User.register(testUserName, password)
|
||||||
|
})
|
||||||
|
.then(({ token, userId }) => {
|
||||||
|
newUserId = userId
|
||||||
|
|
||||||
|
return User.getMasterKey(userId, password)
|
||||||
|
})
|
||||||
|
.then((newMasterKey) => {
|
||||||
|
masterKey = newMasterKey
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.catch(((error) => {
|
||||||
|
console.log(error)
|
||||||
|
}))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('Test Generate Push Key', () => {
|
||||||
|
|
||||||
|
return Attachment.generatePushKey(newUserId)
|
||||||
|
.then( (pushKey) => {
|
||||||
|
newPushKey = pushKey
|
||||||
|
return Attachment.generatePushKey(newUserId)
|
||||||
|
})
|
||||||
|
.then( (pushKey) => {
|
||||||
|
// expect a long, defined pushkey
|
||||||
|
expect(pushKey).toBeDefined()
|
||||||
|
expect(pushKey?.length).toBeGreaterThan(20)
|
||||||
|
expect(pushKey).toMatch(newPushKey)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test('Test get Push Key Bookmarklet', () => {
|
||||||
|
|
||||||
|
return Attachment.getPushkeyBookmarklet(newUserId)
|
||||||
|
.then(( bookmarklet => {
|
||||||
|
// Expect a bookmarklet containting URL encoded pushkey from above
|
||||||
|
const keyCheck = bookmarklet.includes(encodeURIComponent(newPushKey))
|
||||||
|
|
||||||
|
expect(bookmarklet).toBeDefined()
|
||||||
|
expect(keyCheck).toBe(true)
|
||||||
|
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('Test Push URL', () => {
|
||||||
|
|
||||||
|
let url = 'https://www.solidscribe.com'
|
||||||
|
|
||||||
|
return Attachment.pushUrl(newPushKey, url)
|
||||||
|
.then(( results => {
|
||||||
|
|
||||||
|
return Attachment.textSearch(newUserId, 'scribe')
|
||||||
|
|
||||||
|
}))
|
||||||
|
.then((results) => {
|
||||||
|
|
||||||
|
expect(results.length == 1).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test Delete Push Key', () => {
|
||||||
|
|
||||||
|
return Attachment.deletePushKey(newUserId)
|
||||||
|
.then(( results => {
|
||||||
|
// Expect a true bool
|
||||||
|
expect(results).toBe(true)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(done => {
|
||||||
|
// Close Database
|
||||||
|
const db = require('../../config/database')
|
||||||
|
db.end()
|
||||||
|
done()
|
||||||
|
})
|
117
server/tests/models/Note.test.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
const Note = require('../../models/Note')
|
||||||
|
const User = require('../../models/User')
|
||||||
|
|
||||||
|
const testUserName = 'jestTestUserNote'
|
||||||
|
const password = 'Beans1234!!!'
|
||||||
|
const secondPassword = 'Rice1234!!!'
|
||||||
|
|
||||||
|
let newUserId = null
|
||||||
|
let masterKey = null
|
||||||
|
|
||||||
|
let testNoteId = 0
|
||||||
|
let testNoteId2 = 0
|
||||||
|
|
||||||
|
|
||||||
|
const searchWord1 = 'beans'
|
||||||
|
const searchWord2 = 'RICE'
|
||||||
|
const updatedNoteText = 'Some Note Text for Testing more '+searchWord2+' is nice'
|
||||||
|
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
|
||||||
|
// Find and Delete Previous Test user, log in, get key
|
||||||
|
return User.getByUserName(testUserName)
|
||||||
|
.then((user) => {
|
||||||
|
return User.deleteUser(user?.id, password)
|
||||||
|
})
|
||||||
|
.then((results) => {
|
||||||
|
|
||||||
|
return User.register(testUserName, password)
|
||||||
|
})
|
||||||
|
.then(({ token, userId }) => {
|
||||||
|
newUserId = userId
|
||||||
|
|
||||||
|
return User.getMasterKey(userId, password)
|
||||||
|
})
|
||||||
|
.then((newMasterKey) => {
|
||||||
|
masterKey = newMasterKey
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.catch(((error) => {
|
||||||
|
console.log(error)
|
||||||
|
}))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Create Note', () => {
|
||||||
|
const noteTitle = 'Test Note'
|
||||||
|
const noteText = 'Some Note Text for Testing'
|
||||||
|
|
||||||
|
return Note.create(newUserId, noteTitle, noteText, masterKey)
|
||||||
|
.then((noteId) => {
|
||||||
|
testNoteId = noteId
|
||||||
|
expect(noteId).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Create Another Note', () => {
|
||||||
|
const noteTitle = 'Test Note2'
|
||||||
|
const noteText = 'Some Note Text for Testing more '+searchWord1
|
||||||
|
|
||||||
|
return Note.create(newUserId, noteTitle, noteText, masterKey)
|
||||||
|
.then((noteId) => {
|
||||||
|
testNoteId2 = noteId
|
||||||
|
expect(noteId).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Update a note', () => {
|
||||||
|
|
||||||
|
return Note.update(newUserId, testNoteId, updatedNoteText, 'title', 0, 0, 0, 'hash', masterKey)
|
||||||
|
.then((results) => {
|
||||||
|
expect(results.changedRows).toEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Decrypt a note', () => {
|
||||||
|
|
||||||
|
return Note.get(newUserId, testNoteId, masterKey)
|
||||||
|
.then((noteData) => {
|
||||||
|
expect(noteData.text).toMatch(updatedNoteText)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Update note search index', () => {
|
||||||
|
return Note.reindex(newUserId, masterKey)
|
||||||
|
.then((results) => {
|
||||||
|
expect(results).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Search Encrypted Index', () => {
|
||||||
|
const searchString = `${searchWord1} ${searchWord2}`
|
||||||
|
|
||||||
|
return Note.encryptedIndexSearch(newUserId, searchString, null, masterKey)
|
||||||
|
.then(({ids}) => {
|
||||||
|
// Make sure beans is in one note and rice is in updated text
|
||||||
|
expect(ids.length).toEqual(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Search Encrypted Index no results', () => {
|
||||||
|
|
||||||
|
return Note.encryptedIndexSearch(newUserId, 'zzz', null, masterKey)
|
||||||
|
.then(({ids}) => {
|
||||||
|
// Make sure beans is in one note and rice is in updated text
|
||||||
|
expect(ids.length).toEqual(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
afterAll(done => {
|
||||||
|
// Close Database
|
||||||
|
const db = require('../../config/database')
|
||||||
|
db.end()
|
||||||
|
done()
|
||||||
|
})
|
67
server/tests/models/ShareNote.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
const Note = require('../../models/Note')
|
||||||
|
const User = require('../../models/User')
|
||||||
|
const ShareNote = require('../../models/ShareNote')
|
||||||
|
|
||||||
|
const testUserName = 'jestTestUserNote'
|
||||||
|
const password = 'Beans1234!!!'
|
||||||
|
let newUserId = null
|
||||||
|
let masterKey = null
|
||||||
|
|
||||||
|
const testUserName2 = 'jestTestUserDude'
|
||||||
|
const password2 = 'Rice1234!!!'
|
||||||
|
let newUserId2 = null
|
||||||
|
let masterKey2 = null
|
||||||
|
|
||||||
|
|
||||||
|
let testNoteId = 0
|
||||||
|
let testNoteId2 = 0
|
||||||
|
// let sharedNoteId = 0 //ID of note shared with user
|
||||||
|
const shareUserId = 61
|
||||||
|
const searchWord1 = 'beans'
|
||||||
|
const searchWord2 = 'RICE'
|
||||||
|
const updatedNoteText = 'Some Note Text for Testing more '+searchWord2+' is nice'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
|
||||||
|
// Find and Delete Previous Test user, log in, get key
|
||||||
|
return
|
||||||
|
User.getByUserName(testUserName)
|
||||||
|
.then(user => {
|
||||||
|
User.deleteUser(user?.id, password)
|
||||||
|
})
|
||||||
|
.then(user => {
|
||||||
|
User.getByUserName(testUserName2)
|
||||||
|
})
|
||||||
|
.then(user => {
|
||||||
|
User.deleteUser(user?.id, password)
|
||||||
|
})
|
||||||
|
.then((results) => {
|
||||||
|
|
||||||
|
return User.register(testUserName, password)
|
||||||
|
})
|
||||||
|
.then(({ token, userId }) => {
|
||||||
|
newUserId = userId
|
||||||
|
|
||||||
|
return User.getMasterKey(userId, password)
|
||||||
|
})
|
||||||
|
.then((newMasterKey) => {
|
||||||
|
masterKey = newMasterKey
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.catch(((error) => {
|
||||||
|
console.log(error)
|
||||||
|
}))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
afterAll(done => {
|
||||||
|
// Close Database
|
||||||
|
const db = require('../../config/database')
|
||||||
|
db.end()
|
||||||
|
done()
|
||||||
|
})
|
112
server/tests/models/User.test.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
const User = require('../../models/User')
|
||||||
|
const crypto = require('crypto')
|
||||||
|
|
||||||
|
const testUserName = 'jestTestUser'
|
||||||
|
const password = 'Beans1234!!!'
|
||||||
|
const secondPassword = 'Rice1234!!!'
|
||||||
|
|
||||||
|
let testUserId = null
|
||||||
|
let masterKey = null
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
|
||||||
|
// Find and Delete Previous Test user
|
||||||
|
return User.getByUserName(testUserName)
|
||||||
|
.then((user) => {
|
||||||
|
return User.deleteUser(user?.id, password)
|
||||||
|
})
|
||||||
|
.then((results) => {
|
||||||
|
|
||||||
|
return results
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test User Registration', () => {
|
||||||
|
|
||||||
|
return User.register(testUserName, password)
|
||||||
|
.then((({ token, userId }) => {
|
||||||
|
|
||||||
|
testUserId = userId
|
||||||
|
|
||||||
|
expect(token).toBeDefined()
|
||||||
|
expect(userId).toBeGreaterThan(0)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test decrypting user masterKey', () => {
|
||||||
|
|
||||||
|
return User.getMasterKey(testUserId, password)
|
||||||
|
.then((newMasterKey) => {
|
||||||
|
masterKey = newMasterKey
|
||||||
|
|
||||||
|
expect(masterKey).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test generating public and private key pair', () => {
|
||||||
|
|
||||||
|
return User.generateKeypair(testUserId, masterKey)
|
||||||
|
.then(({publicKey, privateKey}) => {
|
||||||
|
|
||||||
|
const publicKeyMessage = 'Test: Public key decrypt - Pass'
|
||||||
|
const privateKeyMessage = 'Test: Private key decrypt - Pass'
|
||||||
|
|
||||||
|
//Encrypt Message with private Key
|
||||||
|
const privateKeyEncrypted = crypto.privateEncrypt(privateKey, Buffer.from(privateKeyMessage, 'utf8')).toString('base64')
|
||||||
|
const decryptedPrivate = crypto.publicDecrypt(publicKey, Buffer.from(privateKeyEncrypted, 'base64'))
|
||||||
|
//Conver back to a string
|
||||||
|
expect(decryptedPrivate.toString('utf8')).toMatch(privateKeyMessage)
|
||||||
|
|
||||||
|
//Encrypt with public key
|
||||||
|
const pubEncrMsc = crypto.publicEncrypt(publicKey, Buffer.from(publicKeyMessage, 'utf8')).toString('base64')
|
||||||
|
const publicDeccryptMessage = crypto.privateDecrypt(privateKey, Buffer.from(pubEncrMsc, 'base64') )
|
||||||
|
//Convert it back to string
|
||||||
|
expect(publicDeccryptMessage.toString('utf8')).toMatch(publicKeyMessage)
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test Logging in User', () => {
|
||||||
|
|
||||||
|
return User.login(testUserName, password)
|
||||||
|
.then(({token, userId}) => {
|
||||||
|
expect(token).toBeDefined()
|
||||||
|
expect(userId).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test Changing Password', () => {
|
||||||
|
return User.changePassword(testUserId, password, secondPassword)
|
||||||
|
.then((passwordChangeResults) => {
|
||||||
|
|
||||||
|
expect(passwordChangeResults).toBe(true)
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test Login with wrong password', () => {
|
||||||
|
|
||||||
|
return User.login(testUserName, password)
|
||||||
|
.then(({token, userId}) => {
|
||||||
|
|
||||||
|
expect(token).toBeNull()
|
||||||
|
expect(userId).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test decrypting masterKey with new Password', () => {
|
||||||
|
return User.getMasterKey(testUserId, secondPassword)
|
||||||
|
.then((newMasterKey) => {
|
||||||
|
|
||||||
|
expect(newMasterKey).toBeDefined()
|
||||||
|
expect(newMasterKey.length).toBe(28)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(done => {
|
||||||
|
// Close Database
|
||||||
|
const db = require('../../config/database')
|
||||||
|
db.end()
|
||||||
|
done()
|
||||||
|
})
|
@@ -3,8 +3,9 @@
|
|||||||
cd /home/mab/ss
|
cd /home/mab/ss
|
||||||
|
|
||||||
echo '::--:: Starting dev server. cd client; npm run serve -> 192.168.1.164:8081'
|
echo '::--:: Starting dev server. cd client; npm run serve -> 192.168.1.164:8081'
|
||||||
screen -dmS "NoteClientScreen" bash -c "cd /home/mab/ss/client; npm run serve"
|
screen -dmS "NoteClientScreen" bash -c "cd /home/mab/ss/client; npm run serve -- --port 8081 --https true"
|
||||||
|
|
||||||
echo '::--:: Starting API server (/api), watching for file changes...'
|
echo '::--:: Starting API server (/api), watching for file changes...'
|
||||||
cd /home/mab/ss/server
|
cd /home/mab/ss/server
|
||||||
|
pm2 flush
|
||||||
pm2 start ecosystem.config.js
|
pm2 start ecosystem.config.js
|
4
staticFiles/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
*
|
|
||||||
*/
|
|
||||||
!.gitignore
|
|
||||||
!assets
|
|
BIN
staticFiles/assets/logo.png
Executable file
After Width: | Height: | Size: 12 KiB |
19
staticFiles/assets/logo.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" id="svg8" version="1.1" viewBox="0 0 132.29166 132.29167" height="500" width="500">
|
||||||
|
<defs 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/>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g style="display:inline" transform="translate(0,-164.70832)" id="layer1">
|
||||||
|
<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;shape-rendering:crispedges"/>
|
||||||
|
<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;shape-rendering:crispedges"/>
|
||||||
|
<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;shape-rendering:crispedges"/>
|
||||||
|
<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;shape-rendering:crispedges"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
24
staticFiles/assets/manifest.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"theme_color":"#000",
|
||||||
|
"background_color": "#000",
|
||||||
|
"description": "Take Notes",
|
||||||
|
"display": "standalone",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/api/static/assets/logo.png",
|
||||||
|
"sizes": "496x496",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/api/static/assets/maskable_icon.png",
|
||||||
|
"sizes": "826x826",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Solid Scribe",
|
||||||
|
"short_name": "Solid Scribe",
|
||||||
|
"start_url": "/#/notes",
|
||||||
|
"author":"Max"
|
||||||
|
}
|
15
staticFiles/assets/manifest.webmanifest
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"background_color": "purple",
|
||||||
|
"description": "Take Notes",
|
||||||
|
"display": "fullscreen",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/api/static/assets/favicon.ico",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Notes",
|
||||||
|
"short_name": "Notes",
|
||||||
|
"start_url": "/#/notes"
|
||||||
|
}
|
BIN
staticFiles/assets/marketing/favicon.ico
Normal file
After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
19
staticFiles/assets/marketing/logo.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" id="svg8" version="1.1" viewBox="0 0 132.29166 132.29167" height="500" width="500">
|
||||||
|
<defs 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/>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g style="display:inline" transform="translate(0,-164.70832)" id="layer1">
|
||||||
|
<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: 2.3 KiB |
24
staticFiles/assets/marketing/manifest.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"theme_color":"#000",
|
||||||
|
"background_color": "#000",
|
||||||
|
"description": "Take Notes",
|
||||||
|
"display": "standalone",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/api/static/assets/logo.png",
|
||||||
|
"sizes": "496x496",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/api/static/assets/maskable_icon.png",
|
||||||
|
"sizes": "826x826",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Solid Scribe",
|
||||||
|
"short_name": "Solid Scribe",
|
||||||
|
"start_url": "/#/notes",
|
||||||
|
"author":"Max"
|
||||||
|
}
|
BIN
staticFiles/assets/marketing/maskable_icon.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
staticFiles/assets/marketing/wallet.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
staticFiles/assets/maskable_icon.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
staticFiles/assets/roboto-latin-bold.woff2
Normal file
BIN
staticFiles/assets/roboto-latin.woff2
Normal file
@@ -12,4 +12,4 @@
|
|||||||
# z - Compress for speed
|
# z - Compress for speed
|
||||||
# h - Human Readable file sizes
|
# h - Human Readable file sizes
|
||||||
|
|
||||||
rsync -e 'ssh' --exclude-from=dontSync.txt -havzC --update mab@marvin.local:/home/mab/pi/ .
|
rsync -e 'ssh' --exclude-from=dontSync.txt -havzC --update mab@marvin.local:/home/mab/ss/ .
|
||||||
|