Compare commits

...

11 Commits

Author SHA1 Message Date
Max G
05152cd5a4 Added counts to each category
Counts update on certain events and show or hide various elements
Fixed various little ui element issues

fixes #6
2020-02-11 21:11:14 +00:00
Max G
cf3289aac6 Fixing quick notes
Updating all the icons
making search bar thinner
2020-02-11 06:05:28 +00:00
Max G
acf72ca67e * Tags can now be toggled by clicking
* Side slide component now respects note colors
2020-02-10 22:21:06 +00:00
Max G
7f93925f74 fixes #4 for real
* Deleting a link deletes the thumbnails
* Joining thumbnails now ignores not visible
2020-02-10 21:09:09 +00:00
Max G
d2c1dedffb fixes #4 Hidden attachment images don't appear on note title display card
* Fixed typo on home page
2020-02-10 20:43:34 +00:00
Max G
003c7e32b1 re #2 Force HTTPS on production when loading home page
* Updated text on home page
2020-02-10 20:03:14 +00:00
Max G
de646cf1de Created a uniform menu for notes that works on mobile
Added list sorting
Added shared notes
Fixed some little bugs here and there
2020-02-10 17:44:43 +00:00
Max G
2828cc9462 Remove TinyMce Added Squire 2020-02-01 22:21:22 +00:00
Max G
f99d6ed430 Some minor bug fixing 2020-01-03 01:54:11 +00:00
Max G
4216c1825e I swear, I'm going to start doing regular commits
+ Added a ton of shit
+ About to add socket.io oh god.
2020-01-03 01:26:55 +00:00
Max G
8d07a8e11a I need to get back into using git. The hell is wrong with me!? 2019-12-20 05:50:50 +00:00
80 changed files with 53385 additions and 925 deletions

18
backupDatabase.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
BACKUPDIR="/home/mab/databaseBackupPi"
mkdir -p $BACKUPDIR
cd $BACKUPDIR
NOW=$(date +"%Y-%m-%d_%H-%M")
ssh mab@avidhabit.com -p 13328 "mysqldump --all-databases --user root -pRootPass1234!" > "backup-$NOW.sql"
cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/backup-$NOW.sql"
echo "Database Backup Complete on $NOW"
#Restore DB
# copy file over, run restore
# scp -P 13328 backup-2019-12-04_03-00.sql mab@avidhabit.com:/home/mab
# mysql -u root -p < backup-2019-12-04_03-00.sql

View File

@@ -5,19 +5,16 @@
# Push built release files to production server # Push built release files to production server
# #
echo -e "\e[32m\nStarting Build, hold onto your parts... \n\e[0m" echo -e "\e[32m\nStarting Build. \n\e[0m"
# Build out new release # Build out new release
cd client cd client
npm run build npm run build
cd .. cd ..
# Remove old releases
rm release.tar.gz
# only compress client/dist and server with node_modules # only compress client/dist and server with node_modules
echo -e "\e[32m\nCompressing client and server code... \n\e[0m" echo -e "\e[32m\nCompressing client and server code... \n\e[0m"
tar -czf release.tar.gz server node_modules client/dist package.json tar -czf release.tar.gz server node_modules client/dist staticFiles/assets
#send compressed release to remote machine #send compressed release to remote machine
echo -e "\e[32m\nMoving compressed release to production... \n\e[0m" echo -e "\e[32m\nMoving compressed release to production... \n\e[0m"
@@ -28,7 +25,7 @@ rm release.tar.gz
#uncompress release on server #uncompress release on server
echo -e "\e[32m\nExtracting release on production... \n\e[0m" echo -e "\e[32m\nExtracting release on production... \n\e[0m"
ssh mab@avidhabit.com -p 13328 "cd /home/mab/pi/; rm -r server node_modules client; tar -xzf *.tar.gz; rm *.tar.gz; pm2 reload all" ssh mab@avidhabit.com -p 13328 "cd /home/mab/pi/; rm -r server node_modules client; tar -xzf *.tar.gz --overwrite; rm *.tar.gz; pm2 reload all"
#Congratulate how awesome you are #Congratulate how awesome you are
echo -e "\e[32m\nRelease Complete! Nice Work! \n\e[0m" echo -e "\e[32m\nRelease Complete! Nice Work! \n\e[0m"

View File

@@ -58,11 +58,11 @@ module.exports = {
} }
}, },
{ {
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, test: /\.(eot|ttf|otf|woff|woff2)(\?.*)?$/,
loader: 'url-loader', loader: 'url-loader',
options: { options: {
limit: 10000, limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]') name: utils.assetsPath('fonts/[name].[ext]')
} }
} }
] ]

View File

@@ -17,6 +17,9 @@ const devWebpackConfig = merge(baseWebpackConfig, {
module: { module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
}, },
watchOptions: {
ignored: ['uploads', 'node_modules']
},
// cheap-module-eval-source-map is faster for development // cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool, devtool: config.dev.devtool,

View File

@@ -13,7 +13,7 @@ module.exports = {
proxyTable: {}, proxyTable: {},
// Various Dev Server settings // Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST host: '0.0.0.0',//'localhost', // can be overwritten by process.env.HOST
port: 8444, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined port: 8444, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false, autoOpenBrowser: false,
errorOverlay: true, errorOverlay: true,

View File

@@ -3,10 +3,17 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>client</title>
<link rel="icon" href="/api/static/assets/favicon.ico" type="image/ico"/>
<link rel="shortcut icon" href="/api/static/assets/favicon.ico" type="image/x-icon"/>
<meta name="theme-color" content="#000" />
<link rel="manifest" href="/api/static/assets/manifest.webmanifest">
<title>Notes</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected, somewhere around here -->
</body> </body>
</html> </html>

View File

@@ -11,20 +11,12 @@
"build": "node build/build.js" "build": "node build/build.js"
}, },
"dependencies": { "dependencies": {
"@ckeditor/ckeditor5-build-classic": "^12.3.1",
"@ckeditor/ckeditor5-build-decoupled-document": "^12.3.1",
"@ckeditor/ckeditor5-dev-utils": "^12.0.2",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^8.0.2",
"@ckeditor/ckeditor5-indent": "^10.0.1",
"@ckeditor/ckeditor5-paragraph": "^11.0.4",
"@ckeditor/ckeditor5-theme-lark": "^14.1.1",
"@ckeditor/ckeditor5-vue": "^1.0.0-beta.2",
"axios": "^0.18.0", "axios": "^0.18.0",
"ckeditor5-indent-text": "^1.0.8",
"es6-promise": "^4.2.6", "es6-promise": "^4.2.6",
"pell": "^1.0.6",
"postcss-loader": "^2.1.6", "postcss-loader": "^2.1.6",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"semantic-ui": "^2.4.2", "socket.io-client": "^2.3.0",
"vue": "^2.5.2", "vue": "^2.5.2",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vuex": "^3.1.0" "vuex": "^3.1.0"
@@ -46,6 +38,7 @@
"file-loader": "^1.1.4", "file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1", "friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1", "html-webpack-plugin": "^2.30.1",
"ip": "^1.1.5",
"node-notifier": "^5.1.2", "node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0", "optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0", "ora": "^1.2.0",

View File

@@ -1,37 +1,25 @@
<template> <template>
<div id="app"> <div id="app">
<link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet"> <global-site-menu />
<!-- Hide this menu on the notes page --> <router-view />
<div class="ui basic segment" v-if="
this.$router.currentRoute.name != 'NotesPage'
">
<div class="ui container">
<div class="ui tabular menu">
<router-link class="item" exact-active-class="active" to="/">Home</router-link> <global-notification />
<router-link v-if="loggedIn" exact-active-class="active" class="item" to="/notes">Notes</router-link>
<router-link class="item" exact-active-class="active" to="/help">Help</router-link>
<router-link v-if="!loggedIn" exact-active-class="active" class="item" to="/login">Login</router-link>
<div v-if="loggedIn" v-on:click="destroyLoginToken" class="item">Logout</div>
</div>
</div>
</div>
<router-view/>
</div> </div>
</template> </template>
<script> <script>
// import io from 'socket.io-client'
import { mapGetters } from 'vuex' import axios from 'axios'
export default { export default {
components: {
'global-site-menu': require('@/components/GlobalSiteMenu.vue').default,
'global-notification':require('@/components/GlobalNotificationComponent.vue').default
},
data: function(){ data: function(){
return { return {
// loggedIn: // loggedIn:
@@ -39,6 +27,15 @@ export default {
}, },
beforeCreate: function(){ beforeCreate: function(){
// const socket = io({ path:'/socket' });
const socket = this.$io
socket.on('connect', () => {
this.$store.commit('setSocketIoSocket', socket.id)
})
//Detect if user is on a mobile browser and set a flag in store
this.$store.commit('detectIsUserOnMobile')
//Set color theme based on local storage //Set color theme based on local storage
if(localStorage.getItem('nightMode') == 'true'){ if(localStorage.getItem('nightMode') == 'true'){
this.$store.commit('toggleNightMode') this.$store.commit('toggleNightMode')
@@ -50,13 +47,8 @@ export default {
if(token){ if(token){
this.$store.commit('setLoginToken', {token, username}) this.$store.commit('setLoginToken', {token, username})
} else {
this.$store.commit('destroyLoginToken')
this.$router.push({'path':'/'})
} }
//Detect if user is on a mobile browser and set a flag in store
this.$store.commit('detectIsUserOnMobile')
}, },
mounted: function(){ mounted: function(){
}, },
@@ -69,6 +61,13 @@ export default {
methods: { methods: {
destroyLoginToken() { destroyLoginToken() {
this.$store.commit('destroyLoginToken') this.$store.commit('destroyLoginToken')
},
loginGateway() {
if(!this.loggedIn){
console.log('This user is not logged in')
this.$router.push({'path':'/login'})
return
}
} }
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,110 +1,357 @@
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(/static/fonts/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;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(/static/fonts/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;
}
:root { :root {
--background_color: #fff; --background_color: #fff;
--text_color: #3d3d3d; --text_color: #3d3d3d;
--outline_color: rgba(34,36,38,.15); --outline_color: rgba(34,36,38,.15);
--border_color: rgba(34,36,38,.20);
/*Global purple menu styles */
--menu-border: #534c68;
--menu-background: #221f2b;
} }
html {
/*scrollbar-width: none;*/
}
div.ui.basic.segment.no-fluf-segment {
margin-top: 0px;
}
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/ /* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
body{ body {
color: var(--text_color); color: var(--text_color);
background-color: var(--background_color); background-color: var(--background_color);
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
} }
.ui.form input:not([type]), .ui.form input:not([type]),
.ui.form input:not([type]):focus { .ui.form input:not([type]):focus,
color: var(--text_color); .ui.form textarea:not([type]),
background-color: var(--background_color); .ui.form textarea:not([type]):focus {
border-color: var(--border_color); color: var(--text_color);
background-color: var(--background_color);
border-color: var(--border_color);
} }
.ui.basic.label { .ui.basic.label, .ui.header, .ui.header div.sub.header {
color: var(--text_color); color: var(--text_color);
background-color: var(--background_color); background-color: var(--background_color);
border-color: var(--border_color); border-color: var(--border_color);
} }
div.ui.basic.green.label { div.ui.basic.green.label {
background-color: var(--background_color) !important; background-color: var(--background_color) !important;
} }
.ui.basic.button, .ui.basic.buttons .button { .ui.basic.button, .ui.basic.buttons .button {
background-color: var(--background_color) !important; background-color: var(--background_color) !important;
color: var(--text_color) !important; color: var(--text_color) !important;
border: 1px solid; border: 1px solid;
border-color: var(--border_color) !important; border-color: var(--border_color) !important;
box-shadow: none; box-shadow: none;
} }
.ui.basic.button:focus, .ui.basic.button:hover { .ui.basic.button:focus, .ui.basic.button:hover {
background-color: var(--background_color) !important; background-color: var(--background_color) !important;
color: var(--text_color) !important; color: var(--text_color) !important;
box-shadow: none; box-shadow: none;
} }
.ui.tabular.menu .item { .ui.tabular.menu .item {
background-color: var(--background_color) !important; background-color: var(--background_color) !important;
color: var(--text_color) !important; color: var(--text_color) !important;
} }
.ui.tabular.menu .item.active { .ui.tabular.menu .item.active {
background-color: var(--background_color) !important; background-color: var(--background_color) !important;
color: var(--text_color) !important; color: var(--text_color) !important;
border-color: var(--border_color) !important; border-color: var(--border_color) !important;
} }
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/ /* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
/* Styles for public display pages */
.color-picker { .fun {
color: var(--text_color); color: rgba(0, 0, 0, 0.87);
background-color: var(--background_color); color: var(--text_color);
position: absolute;
width: 175px;
height: 100px;
top: 26px;
padding: 10px;
border-radius: 5px;
left: -63px;
z-index: 100;
border: 1px solid;
border-color: var(--border_color) !important;
} }
.color-picker .button{ .fun h1 {
border: 1px solid !important; font-size: 2em;
border-color: var(--border_color) !important;
color: var(--border_color) !important;
} }
.fun h2 {
font-size: 1.9em;
}
.fun h3 {
font-size: 1.7em;
}
.fun p {
/*font-size: 1.5em;*/
}
.fun blockquote {
border-left: 5px solid cornflowerblue;
padding-left: 25px;
margin-left: 5px;
}
/* Styles for public display pages */
a:hover {
text-decoration: underline;
}
/*//
// Purple Global Menu
//*/
.note-menu {
width: 100%;
/*display: block;*/
display: inline-table;
background: var(--menu-background);
color: white;
/*overflow: hidden;*/
border: 1px solid var(--menu-border);
/*height: 50px;*/
}
.note-menu > .nm-button {
padding: 10px 15px;
cursor: pointer;
text-align: center;
box-sizing: border-box;
font-size: 1.2em;
vertical-align: middle;
/*height: 40px;*/
display: table-cell;
position: relative;
}
.nm-button i.icon {
margin: 0;
}
.nm-button span {
font-size: 0.9em;
}
.nm-button.right {
float: right;
border-left: 1px solid var(--menu-border);
}
.nm-button:hover {
background-color: #534c68;
color: white;
}
.nm-button + .nm-button {
border-left: 1px solid #534c68;
}
/*.shrink-icons-on-mobile.note-menu span {
display: none;
}*/
/* Shrink button text for mobile */
@media only screen and (max-width: 740px) {
.note-menu .nm-button span {
font-size: 0.7em;
line-height: 0.4em;
margin-left: 0;
}
.nm-button i.icon {
width: 100%;
}
/*prevents buttons from being jammed into corners of round phones*/
.shrink-icons-on-mobile.note-menu {
padding: 0 20px;
}
.shrink-icons-on-mobile .nm-button {
padding: 2px 3px;
}
.shrink-icons-on-mobile .nm-button i.icon {
font-size: 0.7em;
}
}
/*//
// Purple Global Menu
//*/
.note-status-indicator { .note-status-indicator {
float: right; position: absolute;
width: 100px; width: 100px;
padding: 9px 0; padding: 16px 0;
box-sizing: border-box; box-sizing: border-box;
text-align: center; text-align: center;
color: var(--text_color); color: var(--text_color);
bottom: -13px;
right: -7px;
z-index: 100;
cursor: pointer;
} }
/* squire text styles */
.squire-box {
border: none;
height: calc(100% - 69px);
box-sizing: border-box;
padding: 10px 15px 10px;
background: transparent;
overflow-x: scroll;
/*color: var(--text_color);*/
font-size: 1.2em;
line-height: 1.5em;
word-wrap: break-word;
/*border-bottom: 1px solid #ccc;*/
scrollbar-width: none;
}
/*Makes the first line real big */
.squire-box p:first-child {
font-size: 1.4em;
line-height: 1.7em;
}
.squire-box:focus {
outline: none;
}
.squire-box span.size {
line-height: 1.3em;
}
.squire-box a {
cursor: pointer;
}
.note-card-text i,
.squire-box i {
padding: 0.5em 0.99em;
border: 1px solid #CCC;
margin: 1px;
border-radius: 9px;
display: inline-block;
}
.squire-box p {
margin-bottom: 0;
}
.squire-box blockquote {
margin: 0;
padding: 0.8em;
border-left: 2px solid blue;
}
.note-card-text img {
max-width:100%;
height: auto;
max-height: 200px;
}
.squire-box img {
max-width:100%;
height: auto;
}
.note-card-text li > p,
.squire-box p,
.squire-box li > p {
margin-bottom: 0;
}
.note-card-text ul > li,
.squire-box ul > li {
position: relative;
list-style-type: none;
}
.note-card-text ul > li:before,
.squire-box ul > li:before {
content: "\f111";
font-family: 'Icons';
backface-visibility: hidden;
font-style: normal;
font-weight: normal;
text-decoration: inherit;
text-align: center;
line-height: 1.4em;
font-size: 0.75em;
height: 17px;
width: 17px;
display: inline-block;
position: absolute;
left: -30px;
/*border: 2px solid #444;*/
/*border-radius: 4px;*/
bottom: 0;
top: 4px;
cursor: pointer;
opacity: 0.7;
}
ul > li.active:before {
font-family: 'Icons';
content: "\f058";
color: #21BA45;
opacity: 1;
}
/* adjust checkboxes for mobile. Make them a little bigger, easier to click */
@media only screen and (max-width: 740px) {
.note-card-text ul > li,
.squire-box ul > li {
min-height: 30px;
}
.note-card-text ul > li:before,
.squire-box ul > li:before {
content: "\f111";
font-family: outline-icons;
height: 24px;
width: 24px;
left: -40px;
bottom: 0;
top: 0px;
cursor: pointer;
line-height: 0.9em;
font-size: 1.4em;
}
ul > li.active:before {
font-family: 'Icons';
content: "\f058";
color: #21BA45;
opacity: 1;
}
}
.clickable { .clickable {
cursor: pointer; cursor: pointer;
} }
.relative { .relative {
position: relative; position: relative;
} }
.float-right { .float-right {
float: right; float: right;
} }
.textarea-height { .textarea-height {
height: calc(100% - 90px); height: calc(100% - 90px);
} }
.ck-content { .mobile-textarea-height {
font-family: 'Open Sans' !important; height: 100%;
font-size: 1.3rem !important;
background-color: rgba(255, 255, 255, 0);
height: calc(100% - 40px);
overflow: hidden;
}
.ck .ck-editor__nested-editable:focus {
background-color: var(--background_color) !important;
} }
.ui.white.button { .ui.white.button {
background: #FFF; background: #FFF;
} }
.input-floating-button {
position: absolute;
top: 19px;
transform: translateY(-50%);
right: 1px;
}
.fade-in-fwd { .fade-in-fwd {
animation: fade-in-fwd 0.8s both; animation: fade-in-fwd 0.8s both;
@@ -116,12 +363,12 @@ div.ui.basic.green.label {
* ---------------------------------------- * ----------------------------------------
*/ */
@keyframes fade-in-fwd { @keyframes fade-in-fwd {
0% { 0% {
transform: translateZ(-80px); transform: translateZ(-80px);
opacity: 0; opacity: 0;
} }
100% { 100% {
transform: translateZ(0); transform: translateZ(0);
opacity: 1; opacity: 1;
} }
} }

File diff suppressed because one or more lines are too long

4995
client/src/assets/squire.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,219 @@
<style type="text/css" scoped>
.attachment-display-card {
width: 100%;
padding: 10px;
display: inline-block;
border: 1px solid;
border-color: var(--border_color);
border-radius: 4px;
margin: 0 0 15px;
max-height: 10000px;
}
.attachment-image {
max-width: 100%;
height: auto;
max-height: 300px;
}
.image-placeholder {
width: 100%;
height: 100%;
max-height: 100px;
}
.image-placeholder:after {
content: 'No Image';
display: block;
width: 20px;
height: 20px;
background:
green;
position: absolute;
top: 0;
left: 0;
}
.text {
width: 100%;
/*height: 100%;*/
background: transparent;
border: none;
border-top: 1px solid;
border-bottom: 1px solid;
border-color: var(--border_color);
font-size: 1.2em;
line-height: 1.3em;
/*margin: 0 0 10px;*/
padding: 10px 0 10px;
color: var(--text_color);
overflow: hidden;
resize: none;
/*transition: height 0.4s ease; This breaks the resize */
}
.link {
font-size: 1.4em;
margin: 20px 0 20px;
display: inline-block;
white-space: nowrap;
overflow:hidden;
text-overflow: ellipsis;
width: 100%;
line-height: 1.4em;
}
.flip-out {
animation: flip-out-hor-top 0.5s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
overflow: hidden;
transition: max-height 0.3s ease;
max-height: 0;
}
@keyframes flip-out-hor-top {
0% {
transform: rotateX(0);
opacity: 1;
}
100% {
transform: rotateX(70deg);
opacity: 0;
}
}
</style>
<template>
<div class="attachment-display-card" :class="{ 'flip-out':!unfolded }" v-if="visible">
<div class="ui stackable grid">
<!-- image and text -->
<div class="six wide center aligned middle aligned column">
<a :href="linkUrl" target="_blank" >
<img v-if="item.file_location" class="attachment-image" :src="`/api/static/thumb_${item.file_location}`">
<span v-else>
<img class="image-placeholder" loading="lazy" src="/api/static/assets/marketing/void.svg">
No Image
</span>
</a>
</div>
<div class="ten wide column">
<textarea ref="edit" class="text" v-on:blur="saveIt()" v-on:keyup="checkKeyup" v-model="text"></textarea>
<!-- link -->
<a class="link" :href="linkUrl" target="_blank">{{linkText}}</a>
<!-- Buttons -->
<div class="ui small compact basic button" v-on:click="openNote">
<i class="file outline icon"></i>
Open Note
</div>
<div class="ui small compact basic button" v-on:click="openEditAttachments"
:class="{ 'disabled':this.searchParams.noteId }">
<i class="folder open outline icon"></i>
Note Files
</div>
<div class="ui small compact basic icon button" v-on:click="deleteAttachment">
<i v-if="!working" class="trash alternate outline icon"></i>
<i v-if="working" class="purple spinner loading icon icon"></i>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
props: [ 'item', 'searchParams' ],
data: function(){
return {
text: '',
type: null,
linkText: 'Link',
linkUrl:null,
unfolded:true,
visible: true,
working: false,
}
},
beforeCreate: function(){
},
mounted: function(){
this.text = this.item.text
this.type = this.item.attachment_type
//1 = URL, 2 = image, >= 3 files
if(this.type == 1 && this.item.url != null){
this.linkText = this.item.url
this.linkUrl = this.item.url
}
if(this.type == 2){
this.linkText = 'Download'
this.linkUrl = `/api/static/${this.item.file_location}`
}
this.$nextTick(() => {
this.checkKeyup()
})
},
methods: {
checkKeyup(){
let elm = this.$refs.edit
if(elm){
elm.style.height = '0'
elm.style.height = elm.scrollHeight +'px'
}
},
openNote(){
const noteId = this.item.note_id
this.$router.push('/notes/open/'+noteId)
},
openEditAttachments(){
const noteId = this.item.note_id
this.$router.push('/attachments/note/'+noteId)
},
deleteAttachment(){
//No double clicks
if(this.working){ return }
this.working = true
axios.post('/api/attachment/delete', {'attachmentId':this.item.id})
.then( ({data}) => {
if(data){
this.unfolded = false
setTimeout( () => {
this.visible = false
this.$store.dispatch('fetchAndUpdateUserTotals')
}, 600)
}
})
},
saveIt(){
//Don't save text if it didn'th change
if(this.item.text == this.text){
return
}
const data = {
'attachmentId': this.item.id,
'updatedText': this.text,
'noteId': this.item.note_id
}
//Save it, and don't think about it.
axios.post('/api/attachment/update', data)
},
}
}
</script>

View File

@@ -0,0 +1,163 @@
<template>
<div :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
<div class="ui basic segment">
<div class="ui grid">
<div class="ui sixteen wide center aligned column">
<div class="ui fluid button" v-on:click="clearStyles">
<i class="refresh icon"></i>
Clear All Styles
</div>
</div>
<div class="row">
<div class="sixteen wide column">
<br>
<p>Note Color</p>
<div v-for="color in getReducedColors()"
class="color-button"
:style="{ backgroundColor:color }"
v-on:click="chosenColor(color)"
></div>
</div>
</div>
<div class="row">
<div class="sixteen wide column">
<p>Note Icon
<span v-if="allStyles.noteIcon" >
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
</span>
</p>
<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>
<div class="row">
<div class="sixteen wide column">
<p>Icon Color</p>
<div v-for="color in getReducedColors()"
class="color-button"
:style="{ backgroundColor:color }"
v-on:click="chooseIconColor(color)"
>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ColorPicker',
props: [ 'location', 'styleObject' ],
data () {
return {
allStyles:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
blankStyle:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
colors: [
"#ffebee","#ffcdd2","#ef9a9a","#e57373","#ef5350","#f44336","#e53935","#d32f2f","#c62828","#b71c1c","#fce4ec","#f8bbd0","#f48fb1","#f06292","#ec407a","#e91e63","#d81b60","#c2185b","#ad1457","#880e4f","#f3e5f5","#e1bee7","#ce93d8","#ba68c8","#ab47bc","#9c27b0","#8e24aa","#7b1fa2","#6a1b9a","#4a148c","#ede7f6","#d1c4e9","#b39ddb","#9575cd","#7e57c2","#673ab7","#5e35b1","#512da8","#4527a0","#311b92","#e8eaf6","#c5cae9","#9fa8da","#7986cb","#5c6bc0","#3f51b5","#3949ab","#303f9f","#283593","#1a237e","#e3f2fd","#bbdefb","#90caf9","#64b5f6","#42a5f5","#2196f3","#1e88e5","#1976d2","#1565c0","#0d47a1","#e1f5fe","#b3e5fc","#81d4fa","#4fc3f7","#29b6f6","#03a9f4","#039be5","#0288d1","#0277bd","#01579b","#e0f7fa","#b2ebf2","#80deea","#4dd0e1","#26c6da","#00bcd4","#00acc1","#0097a7","#00838f","#006064","#e0f2f1","#b2dfdb","#80cbc4","#4db6ac","#26a69a","#009688","#00897b","#00796b","#00695c","#004d40","#e8f5e9","#c8e6c9","#a5d6a7","#81c784","#66bb6a","#4caf50","#43a047","#388e3c","#2e7d32","#1b5e20","#f1f8e9","#dcedc8","#c5e1a5","#aed581","#9ccc65","#8bc34a","#7cb342","#689f38","#558b2f","#33691e","#f9fbe7","#f0f4c3","#e6ee9c","#dce775","#d4e157","#cddc39","#c0ca33","#afb42b","#9e9d24","#827717","#fffde7","#fff9c4","#fff59d","#fff176","#ffee58","#ffeb3b","#fdd835","#fbc02d","#f9a825","#f57f17","#fff8e1","#ffecb3","#ffe082","#ffd54f","#ffca28","#ffc107","#ffb300","#ffa000","#ff8f00","#ff6f00","#fff3e0","#ffe0b2","#ffcc80","#ffb74d","#ffa726","#ff9800","#fb8c00","#f57c00","#ef6c00","#e65100","#fbe9e7","#ffccbc","#ffab91","#ff8a65","#ff7043","#ff5722","#f4511e","#e64a19","#d84315","#bf360c","#efebe9","#d7ccc8","#bcaaa4","#a1887f","#8d6e63","#795548","#6d4c41","#5d4037","#4e342e","#3e2723","#fafafa","#f5f5f5","#eeeeee","#e0e0e0","#bdbdbd","#9e9e9e","#757575","#616161","#424242","#212121","#eceff1","#cfd8dc","#b0bec5","#90a4ae","#78909c","#607d8b","#546e7a","#455a64","#37474f","#263238","#ffffff","#000000"],
icons: ['ambulance','anchor','balance scale','bath','bed','beer','bell','bell slash','bell slash outline','bicycle','binoculars','birthday cake','blind','bomb','book','bookmark','briefcase','building','car','coffee','crosshairs','dollar sign','eye','eye slash','fighter jet','fire','fire extinguisher','flag','flag checkered','flask','gamepad','gavel','gift','glass martini','globe','graduation cap','h square','heart','heart outline','heartbeat','home','hospital','hospital outline','image','image outline','images','images outline','industry','info','info circle','key','leaf','lemon','lemon outline','life ring','life ring outline','lightbulb','lightbulb outline','location arrow','low vision','magnet','male','map','map outline','map marker','map marker alternate','map pin','map signs','medkit','money bill alternate','money bill alternate outline','motorcycle','music','newspaper','newspaper outline','paw','phone','phone square','phone volume','plane','plug','plus','plus square','plus square outline','print','recycle','road','rocket','search','search minus','search plus','ship','shopping bag','shopping basket','shopping cart','shower','street view','subway','suitcase','tag','tags','taxi','thumbtack','ticket alternate','tint','train','tree','trophy','truck','tty','umbrella','university','utensil spoon','utensils','wheelchair','wifi','wrench']
}
},
watch:{
styleObject: function(updatedStyles){
this.allStyles = updatedStyles
}
},
mounted(){
this.allStyles = this.styleObject
},
methods: {
getReducedColors(){
let reduced = []
this.colors.forEach((color,i) => {
if(i%20 <= 10){
return
}
let mod = (i % 10)+1 //1 - 10
let lines = [3, 5, 8, 9, 10]
if(lines.includes(mod)){
reduced.push(color)
}
})
reduced.push("#000")
return reduced
},
clearStyles(){
this.$emit('changeColor', this.blankStyle)
},
closeThisBitch(){
this.$emit('close')
},
chosenColor(inColor){
//Set not background to color that was chosen
this.allStyles.noteBackground = inColor
//Automatically select note text color
// Convert hex color to RGB - http://gist.github.com/983661
let color = +("0x" + inColor.slice(1).replace(inColor.length < 5 && /./g, '$&$&'));
let r = color >> 16;
let g = color >> 8 & 255;
let b = color & 255;
//Convert RGB to HSP
const hsp = Math.sqrt( 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) );
//If it has a BG color, default to sold black text
this.allStyles.noteText = '#000'
if(hsp < 127.5){
this.allStyles.noteText = '#FFF' //If color is dark, we need brighter text
}
this.$emit('changeColor', this.allStyles)
},
chosenIcon(inIcon){
this.allStyles.noteIcon = inIcon
this.$emit('changeColor', this.allStyles)
},
chooseIconColor(inColor){
this.allStyles.iconColor = inColor
this.$emit('changeColor', this.allStyles)
}
}
}
</script>
<style type="text/css" scoped>
.icon-button {
height: 40px;
width: 14.2%;
display: inline-block;
cursor: pointer;
font-size: 1.3em;
}
.color-button {
height: 50px;
width: 20%;
display: block;
cursor: pointer;
float: left;
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<div>
<p>Crunch Menu</p>
<div v-for="(item, index) in items">
<slot :name="index"></slot>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'CrunchMenu',
data () {
return {
items: []
}
},
beforeMount(){
},
mounted(){
console.log(this)
// console.log(this.$slots.default)
this.$slots.default.forEach( vnode => {
if(vnode.tag && vnode.tag.length > 0){
this.items.push(vnode)
}
})
console.log(this.items)
},
methods: {
onClickTag(index){
console.log('yup')
},
}
}
</script>
<style type="text/css" scoped>
</style>

View File

@@ -20,9 +20,6 @@
'Order by Last Edited' :'lastEdited', 'Order by Last Edited' :'lastEdited',
'Order by Last Opened' :'lastOpened', 'Order by Last Opened' :'lastOpened',
'Order by Last Created' :'lastCreated', 'Order by Last Created' :'lastCreated',
'Only Show Notes with Links' :'withLinks',
'Only Show Notes with Tags' :'withTags',
'Only Show Archived Notes' :'onlyArchived',
} }
} }
}, },
@@ -32,9 +29,6 @@
}) })
}, },
methods:{ methods:{
confirmDelete(){
this.click++
},
displayString(){ displayString(){
return this.orderString.replace('Order by','').replace('Only Show','') return this.orderString.replace('Order by','').replace('Only Show','')
}, },
@@ -54,7 +48,7 @@
<style type="text/css" scoped> <style type="text/css" scoped>
.filter-header { .filter-header {
width: 270px; width: 200px;
padding: 0 0 0 10px; padding: 0 0 0 10px;
border: 1px solid rgba(0,0,0,0); border: 1px solid rgba(0,0,0,0);
border-bottom: none; border-bottom: none;
@@ -62,24 +56,27 @@
box-sizing: border-box; box-sizing: border-box;
border-top-right-radius: 5px; border-top-right-radius: 5px;
border-top-left-radius: 5px; border-top-left-radius: 5px;
margin: 0 0 0 -11px;
float: right;
} }
.filter-menu { .filter-menu {
color: var(--text_color); color: var(--text_color);
background-color: var(--background_color); background-color: var(--background_color);
border-color: var(--border_color);
border: 1px solid; border: 1px solid;
border-top: none; border-top: none;
position: absolute; position: absolute;
top: 100%; top: 100%;
width: 270px; width: 200px;
left: -1px; left: -1px;
z-index: 10; z-index: 10;
padding-top: 10px; padding-top: 10px;
border-bottom-right-radius: 5px; border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px; border-bottom-left-radius: 5px;
border-color: var(--border_color);
} }
.filter-active { .filter-active {
border: 1px solid; border: 1px solid;

View File

@@ -0,0 +1,91 @@
<style type="text/css" scoped>
.hidden-up {
opacity: 0;
position: absolute;
top: -50000px;
}
</style>
<template>
<form data-tooltip="Upload File" data-inverted>
<label :for="`upfile-${noteId}`" class="clickable">
<nm-button icon="upload" :text="uploadStatusText"/>
</label>
<input class="hidden-up" type="file" :id="`upfile-${noteId}`" ref="file" v-on:change="handleFileUpload()" />
<!-- <button v-if="file" v-on:click="uploadFileToServer()">Submit</button> -->
</form>
</template>
<script>
import axios from 'axios'
export default {
name: 'FileUploadButton',
props: [ 'noteId' ],
components: {
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
},
data () {
return {
file: null,
uploadStatusText: 'Upload',
}
},
mounted(){
// console.log(this.noteId)
},
methods: {
uploadFileToServer() {
let formData = new FormData();
formData.append('file', this.file);
formData.append('noteId', this.noteId)
console.log('>> formData >> ', formData);
// You should have a server side REST API
axios.post('/api/attachment/upload',
formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: ( progressEvent ) => {
this.uploadStatusText = parseInt(
Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) )
}
}
).then(results => {
this.uploadStatusText = 'Working'
this.file = null
// console.log('File upload results')
// console.log(results.data)
const name = results.data.fileName
const location = results.data.goodFileName
this.$bus.$emit('notification', 'Processing Upload')
if(name && location){
this.uploadStatusText = 'Upload File'
const imageCode = `<img alt="image" src="/api/static/thumb_${location}">`
this.$bus.$emit('new_file_upload', {noteId: this.noteId, imageCode})
this.$store.dispatch('fetchAndUpdateUserTotals')
}
})
.catch(results => {
this.uploadStatusText = 0
})
},
handleFileUpload() {
//Grab file and push note id to into data
this.file = this.$refs.file.files[0]
console.log('>>>> 1st element in files array >>>> ')
console.log(this.file)
if(this.file){
this.uploadFileToServer()
}
}
}
}
</script>

View File

@@ -0,0 +1,82 @@
<style type="text/css" scoped>
.popup-body {
position: fixed;
bottom: 15px;
right: 15px;
min-height: 50px;
min-width: 200px;
max-width: calc(100% - 20px);
z-index: 1002;
border-top: 2px solid #21ba45;
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
border-top-right-radius: 4px;
border-top-left-radius: 4px;
color: var(--text_color);
background-color: var(--background_color);
}
.popup-row {
padding: 1em 5px;
cursor: pointer;
white-space: nowrap;
}
.popup-row > span {
width: calc(100% - 50px);
display: inline-block;
text-align: center;
box-sizing: border-box;
padding: 0 10px 0;
font-size: 1.25em;
}
.popup-row + .popup-row {
border-top: 1px solid #000;
}
</style>
<template>
<div class="popup-body" v-on:click="dismiss" v-if="notifications.length > 0">
<div class="popup-row" v-for="item in notifications">
<i class="disabled angle left icon"></i>
<span>{{ item }}</span>
<i class="disabled angle right icon"></i>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'GlobalNotificationComponent',
data () {
return {
notifications: [],
totalTimeout: null,
}
},
beforeMount(){
this.$bus.$on('notification', info => {
this.displayNotification(info)
})
},
mounted(){
},
methods: {
displayNotification(newNotification){
this.notifications.push(newNotification)
clearTimeout(this.totalTimeout)
this.totalTimeout = setTimeout(() => {
this.dismiss()
}, 4000)
},
dismiss(){
this.notifications = []
}
}
}
</script>

View File

@@ -0,0 +1,322 @@
<style scoped>
.slotholder {
height: 100vh;
width: 140px;
display: block;
float: left;
}
.global-menu {
width: 140px;
background: #221f2b;
margin: 0;
padding: 0;
box-sizing: border-box;
display: block;
position: fixed;
z-index: 111;
top: 0;
left: 0;
bottom: 0;
}
.menu-item {
color: #fff;
padding: 0.8em 0px 0.8em 10px;
display: inline-block;
width: 100%;
font-size: 1.15em;
box-sizing: border-box;
}
.menu-item i.icon {
margin-right: 10px;
}
.sub {
padding-left: 20px;
}
.menu-section {}
.menu-section + .menu-section {
border-top: 1px solid #534c68;
}
.menu-button {
cursor: pointer;
}
.menu-button:hover {
background-color: #534c68;
text-decoration: none;
}
.router-link-active i {
/*color: #16ab39;*/
}
.router-link-active {
background-color: #534c68;
}
.shade {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.7);
z-index: 100;
cursor: pointer;
}
.top-menu-bar {
/*color: var(--text_color);*/
/*width: 100%;*/
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background-color: var(--background_color);
border-bottom: 1px solid;
border-color: var(--border_color);
padding: 5px 1rem 5px;
}
.place-holder {
width: 100%;
height: 50px;
}
.top-menu-bar img {
width: 30px;
height: 30px;
}
</style>
<template>
<div>
<div class="place-holder" v-if="collapsed && !menuOpen"></div>
<!-- collapsed menu, appears as a bar -->
<div class="top-menu-bar" v-if="(collapsed || mobile) && !menuOpen">
<div class="ui grid">
<div class="seven wide column">
<div class="ui large basic compact icon button" v-on:click="collapseMenu">
<i class="green bars icon"></i>
</div>
<router-link class="ui large basic compact icon button" to="/notes" v-on:click.native="emitReloadEvent()">
<i class="green home icon"></i>
</router-link>
<div v-on:click="toggleNightMode" class="ui large basic compact icon button">
<i class="green moon outline icon"></i>
</div>
</div>
<div class="six wide center aligned column">
<img v-if="!loggedIn" src="/api/static/assets/favicon.ico" alt="logo" />
<search-input v-if="loggedIn"></search-input>
</div>
<div class="three wide right aligned column">
<!-- mobile create note button -->
<div v-if="loggedIn">
<div v-if="!disableNewNote" @click="createNote" class="ui large basic compact icon button">
<i class="green plus icon"></i>
</div>
<div v-if="disableNewNote" class="ui large basic compact icon button">
<i class="grey plus icon"></i>
</div>
</div>
</div>
</div>
</div>
<div class="shade" v-if="mobile && !collapsed" v-on:click="collapseMenu"></div>
<div class="slotholder" v-if="!collapsed && !mobile">
</div>
<div class="global-menu" v-if="!collapsed" v-on:click="menuClicked">
<div class="menu-section">
<div class="menu-item menu-button" v-on:click="collapseMenu">
<i class="angle left icon"></i>
</div>
</div>
<div class="menu-section" v-if="loggedIn">
<div v-if="!disableNewNote" @click="createNote" class="menu-item menu-item menu-button">
<i class="green plus icon"></i>New Note
</div>
<div v-if="disableNewNote" class="menu-item menu-item menu-button">
<i class="purple plus icon"></i>Creating
</div>
</div>
<div class="menu-section" v-if="loggedIn">
<router-link exact-active-class="active" class="menu-item menu-button" to="/notes" v-on:click.native="emitReloadEvent()">
<i class="file outline icon"></i>Notes
<!-- <span v-if="$store.getters.totals">{{ $store.getters.totals['totalNotes'] }}</span> -->
</router-link>
<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(1)" class="menu-item menu-button sub"><i class="grey tags icon"></i>Tags</div> -->
</div>
</div>
<div class="menu-section" v-if="loggedIn && $store.getters.totals && $store.getters.totals['totalFiles']">
<router-link class="menu-item menu-button" exact-active-class="active" to="/attachments">
<i class="folder open outline icon"></i>Files
<!-- <span>{{ $store.getters.totals['totalFiles'] }}</span> -->
</router-link>
</div>
<div class="menu-section" v-if="loggedIn">
<router-link v-if="loggedIn" exact-active-class="active" class="menu-item menu-button" to="/quick">
<i class="paper plane outline icon"></i>Quick
</router-link>
</div>
<div class="menu-section" v-if="!loggedIn">
<router-link v-if="!loggedIn" class="menu-item menu-button" exact-active-class="active" to="/">
<i class="home icon"></i>Welcome
</router-link>
<router-link exact-active-class="active" class="menu-item menu-button" to="/login">
<i class="plug icon"></i>Login
</router-link>
</div>
<div class="menu-section">
<div v-on:click="toggleNightMode" class="menu-item menu-button">
<span v-if="$store.getters.getIsNightMode">
<i class="moon outline icon"></i>Light Theme</span>
<span v-else>
<i class="moon outline icon"></i>Dark Theme</span>
</div>
</div>
<div class="menu-section" v-if="loggedIn">
<div v-if="loggedIn" v-on:click="destroyLoginToken" class="menu-item menu-button">
<i class="user outline icon"></i>{{ucWords($store.getters.getUsername)}}
</div>
</div>
<!-- <router-link class="ui basic compact button" exact-active-class="active" to="/help">
<i class="question mark icon"></i>Help
</router-link> -->
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
components: {
'search-input': require('@/components/SearchInput.vue').default,
},
data: function(){
return {
username: '',
collapsed: false,
mobile: false,
disableNewNote: false,
menuOpen: true,
}
},
beforeCreate: function(){
},
mounted: function(){
this.mobile = this.$store.getters.getIsUserOnMobile
this.collapsed = this.$store.getters.getIsUserOnMobile
// {{ totals['totalNotes'] }}
if(this.mobile){
this.menuOpen = false
}
},
computed: {
loggedIn () {
//Map logged in from state
return this.$store.getters.getLoggedIn
}
},
methods: {
menuClicked(){
//Collapse menu when item is clicked in mobile
if(this.mobile && !this.collapsed){
this.collapsed = true
this.menuOpen = false
}
},
collapseMenu(){
this.collapsed = !this.collapsed
if(!this.collapsed){
this.menuOpen = true
} else {
this.menuOpen = false
}
},
createNote(event){
const title = ''
this.disableNewNote = true
axios.post('/api/note/create', {title})
.then(response => {
if(response.data && response.data.id){
this.$router.push('/notes/open/'+response.data.id)
this.$bus.$emit('open_note', response.data.id)
this.disableNewNote = false
}
})
},
destroyLoginToken() {
this.$bus.$emit('notification', 'Logged Out')
this.$store.commit('destroyLoginToken')
this.$router.push('/')
},
toggleNightMode(){
this.$store.commit('toggleNightMode')
},
ucWords(str){
return (str + '')
.replace(/^(.)|\s+(.)/g, function ($1) {
return $1.toUpperCase()
})
},
emitReloadEvent(){
//Reloads note page to initial state
this.$bus.$emit('note_reload')
},
updateFastFilters(index){
//A little hacky, brings user to notes page then filters on click
if(this.$route.name != 'NotesPage'){
this.$router.push('/notes')
setTimeout( () => {
this.updateFastFilters(index)
}, 500 )
}
const options = [
'withLinks', // 'Only Show Notes with Links'
'withTags', // 'Only Show Notes with Tags'
'onlyArchived', //'Only Show Archived Notes'
'onlyShowSharedNotes', //Only show shared notes
]
let filter = {}
filter[options[index]] = 1
this.$bus.$emit('update_fast_filters', filter)
}
}
}
</script>

View File

@@ -1,9 +1,9 @@
<template> <template>
<span> <span>
<span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete"> <span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete" data-inverted="" data-position="top right">
<i class="grey trash alternate icon"></i> <i class="grey trash alternate icon"></i>
</span> </span>
<span class="clickable" @click="actuallyDelete()" @mouseleave="reset" v-if="click == 1" data-tooltip="Click again to delete." data-position="left center"> <span class="clickable" @click="actuallyDelete()" @mouseleave="reset" v-if="click == 1" data-tooltip="Click again to delete." data-position="top right" data-inverted="">
<i class="red trash alternate icon"></i> <i class="red trash alternate icon"></i>
</span> </span>
</span> </span>
@@ -26,9 +26,11 @@
this.click++ this.click++
}, },
actuallyDelete(){ actuallyDelete(){
axios.post('/api/note/delete', {'noteId':this.noteId}).then(response => { axios.post('/api/note/delete', {'noteId':this.noteId}).then(response => {
if(response.data == true){ if(response.data == true){
this.$bus.$emit('note_deleted') this.$bus.$emit('note_deleted', this.noteId)
this.$store.dispatch('fetchAndUpdateUserTotals')
} }
}) })
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
<template>
<div class="nm-button" :class="moreClass" :data-tooltip="tip" data-inverted>
<!-- Display Icon and text -->
<i v-if="icon" :class="`${icon} icon`"></i>
<span v-if="(text && mobile) || (text && showText)">{{text}}</span>
<slot></slot>
</div>
</template>
<script>
/*
Menu button
Single Icon View
Single Icon With small text on mobile
Tooltips on desktop
Tooltip above or below
*/
export default {
name: 'NoteMenuButtonComponent',
props: [ 'icon', 'text', 'tooltip', 'moreClass', 'showText', 'tip'],
data () {
return {
files: [],
mobile: false,
showTooltip: false,
}
},
beforeMount(){
this.mobile = this.$store.getters.getIsUserOnMobile
},
mounted(){
// console.log('Im a button')
},
methods: {
onFileClick(file){
},
}
}
</script>

View File

@@ -1,32 +1,36 @@
<template> <template>
<div v-on:mouseover="fullTagEdit = true"> <div>
<!-- simple string view -->
<div v-if="!fullTagEdit" class="ui basic segment">
<div class="simple-tag-display">
<!-- Show Loading -->
<span v-if="!loaded">Loading Tags...</span>
<!-- Default count -->
<span v-if="loaded">
<i class="tags icon"></i> <b>{{tags.length}} Tags</b>
</span>
<!-- No tags default text -->
<span v-if="tags.length == 0 && loaded" class="ui small compact green button">
<i class="plus icon"></i> Add a tag
</span>
<!-- display tags in comma delimited list -->
<span v-if="tags.length > 0">
<span v-for="(tag, i) in tags"><span v-if="i > 0">, </span>{{ucWords(tag.text)}}</span>
</span>
</div>
</div>
<!-- hover over view --> <!-- hover over view -->
<div v-if="fullTagEdit" class="full-tag-area fade-in-fwd" v-on:mouseleave="fullTagEdit = false; clearSuggestions()"> <div v-if="fullTagEdit" class="full-tag-area fade-in-fwd">
<div class="ui grid">
<div class="sixteen wide column">
<h2><i class="green tags icon"></i>Edit Tags</h2>
</div>
<div class="sixteen wide column">
<h3>All Tags</h3>
<h4 v-if="allTags.length == 0">No tags yet, add a tag.</h4>
<div v-if="allTags.length > 0">
<div class="ui icon large label clickable" v-for="tag in allTags" :class="{ 'green':isTagOnNote(tag.id) }" v-on:click="toggleTag(tag.text, tag.id, tag.entryId)">
{{ ucWords(tag.text) }}
</div>
</div>
</div>
<div class="sixteen wide column">
<h3>Tags on Note</h3>
<div v-if="allTags.length > 0 && noteTagIds.length > 0">
<div class="ui icon large label" v-for="tag in noteTagIds">
{{ getTagTextById(tag['tagId']) }} <i class="delete icon" v-on:click="removeTag(tag['entryId'])"></i>
</div>
</div>
</div>
</div>
<!-- tag input and suggestion popup --> <!-- tag input and suggestion popup -->
<div class="ui form"> <div class="ui form">
@@ -39,19 +43,12 @@
v-on:focus="onFocus" v-on:focus="onFocus"
/> />
<div class="suggestion-box" v-if="suggestions.length > 0"> <div class="suggestion-box" v-if="suggestions.length > 0">
<div class="suggestion-item" v-for="(item, index) in suggestions" :class="{ 'active':(index == selection) }" v-on:click="onClickTag(index)"> <div class="suggestion-item" v-for="(item, index) in suggestions" :class="{ 'active':(index == selection) }" v-on:click="onSuggestionClick(index)">
{{ucWords(item.text)}} <span class="suggestion-tip" v-if="index == selection">Press Enter to add</span> {{ucWords(item.text)}} <span class="suggestion-tip" v-if="index == selection">Press Enter to add</span>
</div> </div>
</div> </div>
</div> </div>
<!-- existing tags -->
<div class="delete-tag-display" v-if="tags.length > 0">
<div class="ui icon large label" v-for="tag in tags" :class="{ 'green':(newTagInput == tag.text) }">
{{ucWords(tag.text)}} <i class="delete icon" v-on:click="removeTag(tag.id)"></i>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -69,9 +66,11 @@
newTagInput: '', newTagInput: '',
typeDebounce: null, typeDebounce: null,
allTags: [],
noteTagIds: [],
suggestions: [], suggestions: [],
selection: 0, selection: 0,
fullTagEdit: false, fullTagEdit: true,
loaded: false, loaded: false,
} }
}, },
@@ -84,14 +83,38 @@
methods: { methods: {
getTags(){ getTags(){
//Get Note Tags -> /api/tags/get //Get Note Tags -> /api/tags/get
let vm = this
axios.post('/api/tag/get', {'noteId': this.noteId}) axios.post('/api/tag/get', {'noteId': this.noteId})
.then(response => { .then( ({data}) => {
vm.loaded = true
//Set up local data this.loaded = true
vm.tags = response.data
this.allTags = data.allTags
this.noteTagIds = data.noteTagIds
}) })
}, },
isTagOnNote(id){
for (let i = 0; i < this.noteTagIds.length; i++) {
const current = this.noteTagIds[i]
if(current && current['tagId'] == id){
return true
}
}
return false
},
getTagTextById(id){
let tag = this.getTagById(id)
if(tag && tag.text){
return this.ucWords( tag.text )
}
},
getTagById(id){
for (let i = 0; i < this.allTags.length; i++) {
const current = this.allTags[i]
if(current && current['id'] == id){
return current
}
}
},
tagInput(event){ tagInput(event){
let vm = this let vm = this
@@ -150,9 +173,38 @@
} }
}, 300) }, 300)
}, },
onClickTag(index){ onSuggestionClick(index){
this.newTagInput = this.suggestions[index].text this.newTagInput = this.suggestions[index].text
this.addTag() this.addTag()
},
toggleTag(tagText, id){
//remove tag
if(this.isTagOnNote(id)){
//Find database ID for tag
let entryId = null
this.noteTagIds.forEach(tag => {
if(tag.tagId == id){
entryId = tag.entryId
return
}
})
//Submit database entry to be removed
if(entryId){
this.removeTag(entryId)
}
return
}
//Add Tag
this.newTagInput = tagText
this.addTag()
return
}, },
addTag(){ addTag(){
@@ -162,7 +214,7 @@
} }
let postData = { let postData = {
'tagText':this.newTagInput, 'tagText':this.newTagInput.trim(),
'noteId':this.noteId 'noteId':this.noteId
} }
let vm = this let vm = this
@@ -176,6 +228,7 @@
}) })
}, },
onFocus(){ onFocus(){
return
//Show suggested tags //Show suggested tags
let vm = this let vm = this
let postData = { let postData = {
@@ -206,14 +259,16 @@
}, },
removeTag(tagId){ removeTag(tagId){
console.log(tagId)
let postData = { let postData = {
'tagId':tagId, 'tagId':tagId,
'noteId':this.noteId 'noteId':this.noteId
} }
let vm = this
axios.post('/api/tag/removefromnote', postData) axios.post('/api/tag/removefromnote', postData)
.then(response => { .then(response => {
vm.getTags() this.getTags()
}) })
}, },
clearSuggestions(){ clearSuggestions(){
@@ -233,28 +288,24 @@
/* note tag edit area */ /* note tag edit area */
.full-tag-area { .full-tag-area {
position: absolute; /*padding: 15px;*/
bottom: 0; /*border: 1px solid;*/
left: 0;
right: 0;
color: var(--text_color);
background-color: var(--background_color);
padding: 15px;
border: 1px solid;
border-color: var(--border_color); border-color: var(--border_color);
} }
.full-tag-area .delete-tag-display { .full-tag-area .delete-tag-display {
margin-top: 15px; /*margin-top: 15px;*/
} }
.full-tag-area .ui.label { .full-tag-area .ui.label {
margin-bottom: 5px; margin-bottom: 5px;
} }
.simple-tag-display { .simple-tag-display {
width: 100%; width: calc(100% - 0px);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-height: 35px; max-height: 35px;
color: var(--text_color);
cursor: pointer;
} }
/* tag suggestion box styles */ /* tag suggestion box styles */
@@ -269,8 +320,8 @@
height: 40px; height: 40px;
padding: 10px 15px; padding: 10px 15px;
cursor: pointer; cursor: pointer;
background: white; background-color: var(--background_color);
color: black; color: var(--text_color);
} }
.suggestion-item.active { .suggestion-item.active {
background: green; background: green;

View File

@@ -1,71 +1,78 @@
<template> <template>
<div class="note-title-display-card fade-in-fwd" <div class="note-title-display-card"
:style="{'background-color':color, 'color':fontColor}" :style="{'background-color':color, 'color':fontColor, 'border-color':color }"
:class="{'currently-open':currentlyOpen}" :class="{'currently-open':currentlyOpen}"
> >
<!-- fade-in-fwd -->
<div v-if="noteIcon" class="badge">
<i :class="`large ${noteIcon} icon`" :style="{ 'color':iconColor }"></i>
</div>
<div class="ui grid max-height"> <div class="ui grid max-height">
<!-- Show title and snippet below it --> <!-- Show title and snippet below it -->
<div class="top aligned row" @click.stop="onClick(note.id)"> <div class="top aligned row" @click.self="onClick(note.id)">
<div class="sixteen wide column overflow-hidden note-card-text" @click="e => onClick(note.id, e)">
<!-- Title display -->
<div v-if="note.title.length > 0"
data-test-id="title"
:class="{ 'big-text':(note.titleLength <= 100), 'small-text-title':(note.titleLength >= 100) }"
v-html="note.title"></div>
<!-- Sub text display -->
<div v-if="note.subtext.length > 0 && !isShowingSearchResults()"
data-test-id="subtext"
:class="{ 'big-text':(note.subtextLength <= 100 && note.titleLength <= 100), 'small-text':(note.subtextLength >= 100) }"
v-html="note.subtext"></div>
<div class="sixteen wide column overflow-hidden" v-if="isShowingSearchResults()">
<!-- Display highlights from solr results --> <!-- Display highlights from solr results -->
<div v-if="note.note_highlights.length > 0" class="term-usage"> <div v-if="note.note_highlights.length > 0 && textResults" class="term-usage">
<h4><i class="paragraph icon"></i> Found in Text</h4> <div
<div class="usage-row" v-for="highlight in note.note_highlights" v-html="cleanHighlight(highlight)"></div> class="usage-row"
v-for="highlight in note.note_highlights"
:class="{ 'big-text':(highlight <= 100), 'small-text-title':(highlight >= 100) }"
v-html="cleanHighlight(highlight)"></div>
</div> </div>
<div v-if="note.attachment_highlights.length > 0" class="term-usage">
<h4><i class="linkify icon"></i> Found in URL</h4> </div>
<div class="usage-row" v-for="highlight in note.attachment_highlights" v-html="cleanHighlight(highlight)"></div>
</div> </div>
<div v-if="note.tag_highlights.length > 0" class="term-usage">
<h4><i class="tags icon"></i> Found in Tags</h4>
<div class="usage-row" v-for="highlight in note.tag_highlights"> <!-- Toolbar on the bottom -->
<span <div class="bottom aligned row" @click.self="onClick(note.id)">
v-for="tag in splitTags(highlight)" <div class="sixteen wide column">
class="ui label" <div class="ui grid reduced-padding">
>
<span v-html="tag"></span> <div class="thirteen wide column clickable icon-bar" @click="onClick(note.id)">
<!-- {{$helpers.timeAgo(note.updated)}} -->
<span v-if="note.tags">
<span v-for="tag in (note.tags.split(','))" class="little-tag">{{ tag }}</span>
</span>
<span v-if="note.pinned == 1" data-position="top right" data-tooltip="Pinned" data-inverted="">
<i class="green pin icon"></i>
</span>
<span v-if="note.archived == 1" data-position="top right" data-tooltip="Archived" data-inverted="">
<i class="green archive icon"></i>
</span> </span>
</div> </div>
<div class="three wide right aligned column">
<delete-button :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }" :note-id="note.id" />
</div>
<div class="row" v-if="note.thumbs">
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
<img v-for="thumb in note.thumbs.split(',').reverse()" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`">
</div>
</div>
</div> </div>
</div> </div>
<div class="sixteen wide column overflow-hidden">
<h3 class="clickable">{{note.title}}</h3>
</div>
<div class="sixteen wide column overflow-hidden">
<p class="clickable">{{note.subtext}}</p>
</div>
</div> </div>
<div class="bottom aligned row" @click.self.stop="onClick(note.id)">
<div class="six wide column clickable" @click.stop="onClick(note.id)">
{{$helpers.timeAgo(note.updated)}}
</div>
<div class="ten wide right aligned column split-spans">
<span v-if="note.pinned == 1" data-tooltip="Pinned">
<i class="green pin icon"></i>
</span>
<span v-if="note.archived == 1" data-tooltip="Archived">
<i class="green archive icon"></i>
</span>
<span v-if="note.attachment_count > 0">
<i class="linkify icon"></i> {{note.attachment_count}}
</span>
<span v-if="note.tag_count == 1" data-tooltip="Note has 1 tag">
<i class="tags icon"></i> {{note.tag_count}}
</span>
<span v-if="note.tag_count > 1" :data-tooltip="`Note has ${note.tag_count} tags`">
<i class="tags icon"></i> {{note.tag_count}}
</span>
<delete-button :note-id="note.id" />
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -73,7 +80,7 @@
export default { export default {
name: 'NoteTitleDisplayCard', name: 'NoteTitleDisplayCard',
props: [ 'onClick', 'data', 'currentlyOpen' ], props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults' ],
components: { components: {
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default, 'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
}, },
@@ -93,37 +100,86 @@
}, },
splitTags(text){ splitTags(text){
return text.split(',') return text.split(',')
} },
openEditAttachment(){
this.$router.push('/attachments/note/'+this.note.id)
},
}, },
data () { data () {
return { return {
note: null, note: null,
color: null, //'#FFF', color: null,
fontColor: null, //'#000' fontColor: null,
noteIcon: null,
iconColor: null,
} }
}, },
beforeMount(){ beforeMount(){
this.note = this.data this.note = this.data
if(this.note.color != null && this.note.color != '#FFF'){ if(this.note.color != null){
this.color = this.note.color
this.fontColor = '#FFF' const styles = JSON.parse(this.note.color)
//Set background color
if(styles.noteBackground){
this.color = styles.noteBackground
}
//set text color
if(styles.noteText){
this.fontColor = styles.noteText
}
if(styles.noteIcon){
this.noteIcon = styles.noteIcon
}
if(styles.iconColor){
this.iconColor = styles.iconColor
}
} }
} }
} }
</script> </script>
<style type="text/css"> <style type="text/css">
/*Strict font sizes for card display*/
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
/*font-size: 1.0em !important;*/
font-size: 15px !important;
}
.small-text > p, , .small-text > h1, .small-text > h2 {
margin-bottom: 0.5em;
}
.big-text, .big-text > p, .big-text > h1, .big-text > h2 {
/*font-size: 1.3em !important;*/
font-size: 16px !important;
font-weight: bold;
}
.big-text > p, .big-text > h1, .big-text > h2 {
margin-bottom: 0.3em;
}
.note-title-display-card h3 {
font-size: 1rem;
font-weight: bold;
line-height: 1.5rem;
}
.term-usage { .term-usage {
border-bottom: 1px solid #DDD; /*border-bottom: 1px solid #DDD;*/
padding-bottom: 10px; /*padding-bottom: 10px;*/
margin-bottom: 10px; margin-top: 15px;
width: 100%; width: 100%;
} }
.term-usage em { .term-usage em {
color: green; color: green;
font-weight: bold; font-weight: bold;
font-size: 1.1rem;
font-style: normal;
} }
.usage-row + .usage-row { .usage-row + .usage-row {
padding: 8px 0 0; padding: 8px 0 0;
@@ -133,22 +189,77 @@
.note-title-display-card { .note-title-display-card {
position: relative; position: relative;
box-shadow: 0 1px 2px 0 rgba(34,36,38,.15); /*box-shadow: 0 1px 2px 0 rgba(34,36,38,.15);*/
margin: 0 15px 15px 0; box-shadow: 0 0px 5px 1px rgba(34,36,38,0);
padding: 1em; margin: 5px;
padding: 0.7em 1em;
border-radius: .28571429rem; border-radius: .28571429rem;
border: 1px solid; border: 1px solid;
border-color: var(--border_color); border-color: var(--border_color);
width: calc(33.333% - 15px); /*width: calc(33.333% - 10px);*/
transition: box-shadow 0.3s; width: calc(25% - 10px);
/*transition: box-shadow 0.3s;*/
box-sizing: border-box; box-sizing: border-box;
cursor: pointer;
line-height: 1.8rem;
letter-spacing: 0.02rem;
} }
.note-title-display-card:hover { .note-title-display-card:hover {
box-shadow: 0 1px 2px -0 rgba(34,36,38,.50); /*box-shadow: 0 3px 6px -0 rgba(34,36,38,.50);*/
box-shadow: 0 0px 5px 1px rgba(34,36,38,0.3);
} }
.icon-bar {
opacity: 0.8;
/*margin-top: -2.2rem;*/
}
.hover-hide {
opacity: 0.0;
}
.little-tag {
font-size: 0.7em;
padding: 5px 5px;
border: 1px solid var(--border_color);
margin: 5px 3px 0 0;
border-radius: 3px;
white-space: nowrap;
max-width: 100px;
overflow: hidden;
display: inline-block;
line-height: 0.8em;
text-overflow: ellipsis;
}
.tiny-thumb-box {
max-height: 70px;
overflow: hidden;
width: 100%;
display: inline-block;
background-color: rgba(200, 200, 200, 0.2);
white-space: nowrap;
overflow-x: scroll;
border: 1px solid var(--border_color);
border-left: none;
border-right: none;
text-align: center;
scrollbar-width: none;
}
.tiny-thumb {
max-height: 70px;
display: inline-block;
}
.note-title-display-card:hover .icon-bar {
opacity: 1;
}
.note-title-display-card:hover .hover-hide {
opacity: 1;
}
.one-column .note-title-display-card { .one-column .note-title-display-card {
margin-right: 65%; /*margin-right: 65%;*/
width: 33%; /*width: 33%;*/
width: 100%;
} }
.overflow-hidden { .overflow-hidden {
overflow: hidden; overflow: hidden;
@@ -162,6 +273,7 @@
.currently-open:after { .currently-open:after {
content: 'Open'; content: 'Open';
position: absolute; position: absolute;
cursor: default;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
@@ -176,10 +288,17 @@
font-size: 3rem; font-size: 3rem;
} }
.badge {
position: absolute;
top: 7px;
right: 6px;
}
/* Tweak mobile display to show only one column */
@media only screen and (max-width: 740px) { @media only screen and (max-width: 740px) {
.note-title-display-card { .note-title-display-card {
width: 100%; width: calc(100% + 10px);
margin: 15px 0 0 0; margin: 0px -5px 10px -5px;
} }
} }
</style> </style>

View File

@@ -0,0 +1,43 @@
<template>
<div class="ui form">
<div class="ui left icon fluid input">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes and Files" />
<i class="search icon"></i>
</div>
</div>
</template>
<script>
export default {
data: function(){
return {
searchTerm: '',
searchTimeout: null,
searchDebounceDuration: 300,
}
},
beforeCreate: function(){
},
mounted: function(){
//search clear
this.$bus.$on('reset_fast_filters', () => {
this.searchTerm = ''
})
},
methods: {
searchKeyUp(){
clearTimeout(this.searchTimeout)
this.searchTimeout = setTimeout(() => {
this.search()
}, this.searchDebounceDuration)
},
search(){
this.$bus.$emit('update_search_term', this.searchTerm)
},
}
}
</script>

View File

@@ -0,0 +1,112 @@
<style type="text/css" scoped>
</style>
<template>
<div>
<div class="ui grid" v-if="this.shareUsername == null">
<div class="row">
<div class="eight wide column">
<div class="ui form">
<div class="field">
<input type="text" placeholder="Share with someone" v-model="shareUserInput" v-on:keyup="onKeyup">
</div>
</div>
</div>
<div class="eight wide column">
<div class="ui disabled button" v-if="shareUserInput.length == 0">
Share
</div>
<div class="ui green button" v-if="shareUserInput.length > 0" v-on:click="onSubmitClick">
Share
</div>
</div>
</div>
<div class="sixteen wide column" v-if="sharedWithUsers.length > 0">
<h3>Users who can edit this note</h3>
</div>
<div class="row" v-for="item in sharedWithUsers">
<div class="eight wide middle aligned column">
<h3><i class="green user circle icon"></i>{{item.username}}</h3>
</div>
<div class="eight wide column">
<div class="ui basic compact button" v-on:click="onRevokeAccess(item.noteId)">Remove Access</div>
</div>
</div>
</div>
<div class="ui grid" v-if="this.shareUsername != null">
<div class="sixteen wide column">
Shared with you by <h3><i class="green user circle icon"></i>{{shareUsername}}</h3>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'ShareNoteComponent',
props: [ 'noteId', 'rawTextId', 'shareUsername' ],
data () {
return {
sharedWithUsers: [],
shareUserInput: '',
debounce: null,
enableSubmitShare: false,
}
},
beforeMount(){
},
mounted(){
if(this.shareUsername == null){
this.loadShareList()
}
},
methods: {
loadShareList(){
axios.post('/api/note/getshareusers', {'rawTextId':this.rawTextId })
.then( ({data}) => {
this.sharedWithUsers = data
})
},
onRevokeAccess(noteId){
axios.post('/api/note/shareremoveuser', {'noteId':noteId})
.then( ({data}) => {
console.log(data)
if(data == true){
this.loadShareList()
}
})
},
onKeyup(event){
if(event.keyCode == 13){
this.onSubmitClick()
return
}
},
onSubmitClick(){
axios.post('/api/note/shareadduser', {'noteId':this.noteId, 'rawTextId':this.rawTextId, 'username':this.shareUserInput })
.then( ({data}) => {
if(data == true){
this.shareUserInput = ''
this.loadShareList()
} else {
this.$bus.$emit('notification', 'User not found')
}
})
},
}
}
</script>

View File

@@ -0,0 +1,148 @@
<style type="text/css" scoped>
.slide-container {
position: fixed;
top: 0;
left: 0;
right: 55%;
bottom: 0;
z-index: 400;
overflow: hidden;
height: 100%;
color: var(--text_color);
background-color: var(--background_color);
}
.slide-content {
box-sizing: border-box;
/*padding: 1em 1.5em;*/
height: calc(100% - 43px);
border-right: 1px solid var(--menu-border);
/*background-color: var(--background_color);*/
overflow-x: scroll;
}
.slide-shadow {
position: fixed;
top: 0;
left: 0;
right: 50%;
bottom: 0;
color: red;
background-color: rgba(0,0,0,0.5);
/*background: linear-gradient(90deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 55%);*/
z-index: 399;
overflow: hidden;
cursor: pointer;
}
.note-menu {
height: 43px;
}
@media only screen and (max-width: 740px) {
.slide-shadow {
background-color: rgba(0,0,0,0.5);
}
.slide-content {
height: calc(100% - 55px);
}
.slide-container {
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.note-menu {
height: 55px;
padding: 0 30px;
}
}
.modal-fade-enter,
.modal-fade-leave-active {
opacity: 0;
}
.modal-fade-enter-active,
.modal-fade-leave-active {
transition: opacity .5s ease;
}
</style>
<template>
<transition name="modal-fade">
<div>
<div class="slide-container" :style="{ 'background-color':bgColor, 'color':textColor}">
<!-- content of the editor -->
<div class="slide-content">
<slot></slot>
</div>
<!-- close menu on bottom -->
<div class="note-menu">
<nm-button more-class="right" icon="close" text="close" :show-text="true" v-on:click.native="close" />
</div>
</div>
<div class="slide-shadow" v-on:click="close"></div>
</div>
</transition>
</template>
<script>
export default {
name: 'SideSlideMenu',
props: [ 'name', 'styleObject' ],
components: {
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
},
data () {
return {
items: [],
bgColor: null,
textColor: null,
}
},
beforeMount(){
//Other panels will tell this one to close
this.$bus.$on('destroy_all_other_side_panels', (name) => {
if(this.name != name){
this.close()
}
})
},
beforeDestroy(){
},
mounted(){
//If note style object is set, use that on the slide menu
if(this.styleObject && this.styleObject.noteText){
this.textColor = this.styleObject.noteText
}
if(this.styleObject && this.styleObject.noteBackground){
this.bgColor = this.styleObject.noteBackground
}
//Close all other panels that are not this one
this.$nextTick( () => {
this.$bus.$emit('destroy_all_other_side_panels', this.name)
})
},
methods: {
onClickTag(index){
console.log('yup')
},
close() {
this.$emit('close');
},
}
}
</script>

View File

@@ -0,0 +1,119 @@
<style type="text/css" scoped>
.img-container {
display: flex;
flex-wrap: wrap;
}
.img-row {
height: 30vh;
flex-grow: 1;
}
.img-row:last-child {
/* There's no science in using "10" here. In all my testing, this delivered the best results. */
flex-grow: 10;
}
.img-row > img {
max-height: calc(100% - 10px);
min-width: calc(100% - 10px);
max-width: calc(100% - 10px);
object-fit: cover;
vertical-align: bottom;
/*padding: 5px;*/
box-shadow: 0px 2px 2px 1px rgba(34,36,38,0.3);
cursor: pointer;
}
</style>
<template>
<div>
<div v-if="uploadedToNote.length > 0">
<h2>Images Uploaded to Note</h2>
<div class="ui fluid green button" v-on:click="$router.push('/attachments/note/'+noteId)">
Manage Files on this Note
<i class="chevron circle right icon"></i>
</div>
<p></p>
<div class="img-container">
<div v-for="file in uploadedToNote" class="img-row" v-on:click="onFileClick(file)">
<img :src="`/api/static/thumb_${file.file_location}`">
</div>
<!-- extra row helps it display properly -->
<div class="img-row"></div>
</div>
</div>
<!-- Add images to note -->
<div v-if="files.length > 0">
<h2>All other Images</h2>
<div class="ui fluid green button" v-on:click="$router.push('/attachments')">
Manage All Files
<i class="chevron circle right icon"></i>
</div>
<p></p>
<div class="img-container">
<div v-for="file in files" class="img-row" v-on:click="onFileClick(file)">
<img :src="`/api/static/thumb_${file.file_location}`">
</div>
<!-- extra row helps it display properly -->
<div class="img-row"></div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'SimpleAttachmentNoteComponent',
props: [ 'noteId', 'squireEditor' ],
data () {
return {
files: [],
uploadedToNote: [],
}
},
beforeMount(){
},
mounted(){
axios.post('/api/attachment/search', {'attachmentType':2})
.then( ({data}) => {
//Sort files into two categories
data.forEach(file => {
if(file['note_id'] == this.noteId){
this.uploadedToNote.push(file)
} else {
this.files.push(file)
}
})
})
},
methods: {
onFileClick(file){
const imageCode = `<img alt="image" src="/api/static/thumb_${file.file_location}">`
this.$bus.$emit('new_file_upload', {noteId: this.noteId, imageCode})
if(this.$store.getters.getIsUserOnMobile){
this.close()
}
},
close() {
this.$emit('close');
},
}
}
</script>

View File

@@ -1,5 +1,6 @@
// The Vue build version to load with the `import` command // The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias. // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
@@ -9,10 +10,43 @@ import store from './stores/mainStore';
import App from './App' import App from './App'
import router from './router' import router from './router'
require('./assets/semantic-min.css')
require('./assets/semantic-helper.css')
// Fonts
require('./assets/roboto-latin.woff2')
require('./assets/roboto-latin-bold.woff2')
require('./assets/themes/default/assets/fonts/icons.eot')
require('./assets/themes/default/assets/fonts/icons.otf')
require('./assets/themes/default/assets/fonts/icons.svg')
require('./assets/themes/default/assets/fonts/icons.ttf')
require('./assets/themes/default/assets/fonts/icons.woff')
require('./assets/themes/default/assets/fonts/icons.woff2')
require('./assets/themes/default/assets/fonts/outline-icons.eot')
require('./assets/themes/default/assets/fonts/outline-icons.svg')
require('./assets/themes/default/assets/fonts/outline-icons.ttf')
require('./assets/themes/default/assets/fonts/outline-icons.woff')
require('./assets/themes/default/assets/fonts/outline-icons.woff2')
require('./assets/squire.js')
//Import socket io, init using nginx configured socket path
import io from 'socket.io-client';
const socket = io({ path:'/socket' });
//integrate connected socket into vue instance
Object.defineProperties(Vue.prototype, {
$io: {
get: () => socket
}
})
// This callback runs before every route change, including on page load. // This callback runs before every route change, including on page load.
// Sets the title of the page using vue router // Sets the title of the page using vue router
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
document.title = to.meta.title; document.title = to.meta.title;
next(); next();
}); });
@@ -21,16 +55,10 @@ router.beforeEach((to, from, next) => {
import EventBus from './EventBus' import EventBus from './EventBus'
import Helpers from './Helpers' import Helpers from './Helpers'
import CKEditor from '@ckeditor/ckeditor5-vue';
Vue.use( CKEditor )
require('./assets/semantic-min.css')
require('./assets/semantic-helper.css')
Vue.use(Vuex) Vue.use(Vuex)
Vue.config.productionTip = false Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({ new Vue({
el: '#app', el: '#app',
router, router,

View File

@@ -0,0 +1,108 @@
<template>
<div class="ui basic segment no-fluf-segment">
<div class="ui grid">
<div class="ui sixteen wide column">
<h2 class="ui header">
<i class="folder open outline icon"></i>
<div class="content">
Files
<div class="sub header">Uploaded Files and Websites from notes.</div>
</div>
</h2>
</div>
<div class="sixteen wide column" v-if="searchParams.noteId">
<div class="ui green button" v-on:click="clearNote">
<i class="chevron circle left icon"></i>
Show All Attachments
</div>
<div class="ui green button" v-on:click="openNote">
<i class="file outline icon"></i>
Open Note
</div>
</div>
<div class="sixteen wide column" v-if="searchParams['noteId'] && attachments.length == 0">
<h3>There are no attachments for this note.</h3>
<h3>Attachments are links or files added to the note.</h3>
</div>
<div class="sixteen wide column">
<attachment-display
v-for="item in attachments"
:item="item"
:key="item.id"
:search-params="searchParams"
/>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
components: {
'attachment-display': require('@/components/AttachmentDisplayCard').default,
},
data: function(){
return {
attachments: [],
searchParams: {}
}
},
beforeCreate: function(){
//
// Perform Login check
//
this.$parent.loginGateway()
},
mounted: function(){
//Mount notes on load if note ID is set
this.openNoteAttachments()
this.searchAttachments()
},
watch:{
$route (to, from){
//Open or close notes on route change
this.openNoteAttachments()
this.searchAttachments()
}
},
methods: {
openNoteAttachments(){
if(this.$route.params && this.$route.params.id){
const inputNoteId = this.$route.params.id
this.searchParams['noteId'] = inputNoteId
}
},
openNote(){
const noteId = this.searchParams['noteId']
this.$router.push('/notes/open/'+noteId)
},
clearNote(){
this.$router.push('/attachments/')
delete this.searchParams.noteId
},
searchAttachments (){
axios.post('/api/attachment/search', this.searchParams)
.then( results => {
this.attachments = results.data
})
},
}
}
</script>
<style type="text/css" scoped>
.attachment-display-area {
width: 100%;
margin-top: 15px;
box-sizing: border-box;
padding: 0 5%;
}
</style>

View File

@@ -1,13 +1,325 @@
<style type="text/css" scoped>
.hero {
background-size: 50%;
background-color: #0a2f13;
background: linear-gradient(270deg, #21ba45, #3710a4);
background-size: 400% 400%;
overflow: hidden;
-webkit-animation: fadeorama 16s ease infinite;
-moz-animation: fadeorama 16s ease infinite;
animation: fadeorama 16s ease infinite;
}
.lightly-padded {
margin-top: 10px;
}
.massive-text {
color: white;
font-size: 4rem;
}
.blinking {
animation:blinkingText 1.5s linear infinite;
}
@keyframes blinkingText{
0%{ opacity: 0.9; }
50%{ opacity: 0; }
100%{ opacity: 0.9; }
}
.subtext {
border-bottom: 1px solid white;
border-right: 1px solid white;
color: white;
font-size: 1.5rem;
padding: 0 0 0 10px;
}
.stand-out {
color: white;
text-shadow:
2px 2px 1px black,
-2px -2px 1px black,
-2px 2px 1px black,
2px -2px 1px black;
}
h2, h3 {
font-weight: normal;
}
@-webkit-keyframes fadeorama {
0%{background-position:0% 50%}
50%{background-position:100% 50%}
100%{background-position:0% 50%}
}
@-moz-keyframes fadeorama {
0%{background-position:0% 50%}
50%{background-position:100% 50%}
100%{background-position:0% 50%}
}
@keyframes fadeorama {
0%{background-position:0% 50%}
50%{background-position:100% 50%}
100%{background-position:0% 50%}
}
/*safari fix - prevents page from being below the menu */
.dont-pad-me {
margin-right: 0 !important;
margin-left: 0 !important;
}
</style>
<template> <template>
<div class="ui basic segment"> <div class="lightly-padded">
<div class="ui container"> <div class="ui centered vertically divided stackable grid">
<h1>Welcome</h1>
<div class="row hero fadeBg" :style="{ 'height':(height+'px') }">
<!-- All marketing images if you need to review -->
<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/gardening.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/growth.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/icecream.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/investing.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/onboarding.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/robot.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/solution.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/watching.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/cloud.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/grandma.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/hamburger.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/idea.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/notebook.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/plan.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/secure.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/void.svg" alt="">
</div>
<div class="one wide large screen only column"></div>
<!-- desktop column - large screen only -->
<div class="seven wide middle aligned left aligned column">
<h2 class="massive-text">Take Notes, <br>Like Never Before</h2>
<h3 class="subtext">
Using an online note application <i class="i cursor icon blinking"></i>
</h3>
<p>Assuming you have never used a note application previously in your life.</p>
<br>
<i class="huge inverted chevron circle down icon"></i>
</div>
<div class="eight wide middle aligned left aligned column">
<img loading="lazy" width="90%" src="/api/static/assets/marketing/notebook.svg" alt="The Venus fly laptop about to capture another victim">
</div>
</div>
<!-- set -->
<div class="middle aligned centered row">
<div class="six wide column">
<h2>Everyone has knowledge that need to be expressed</h2>
<h3>Utilize action potential to create notes by encoding raw brainwaves converted to written language</h3>
</div>
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/idea.svg" alt="Explosion of New Ideas">
</div>
</div>
<div class="middle aligned centered row">
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/gardening.svg" alt="Pruning the mind garden">
</div>
<div class="six wide column">
<h2>Dream it, then do it</h2>
<h3>Easily record your unlimited imagination. Ideas, stories, notes, plays, poems anything, that can reasonably be put into text</h3>
</div>
</div>
<!-- set -->
<div class="middle aligned centered green row">
<div class="six wide column">
<h2>Unbridled Input</h2>
<h3>Revolutionary technology allows the use of any keyboard with up to 395 keys</h3>
</div>
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/add.svg" alt="A shpere of newness">
</div>
</div>
<div class="middle aligned centered row">
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/solution.svg" alt="Hypercube of Solutions">
</div>
<div class="six wide column">
<h2>Solutions with the Internet</h2>
<h3>With the power to save any combination of letters, you can easily inscribe thoughts</h3>
</div>
</div>
<!-- set -->
<div class="middle aligned centered row">
<div class="six wide column">
<h2>Search your data</h2>
<h3>Type in a word and find that same word but somewhere else</h3>
</div>
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos">
</div>
</div>
<div class="middle aligned centered row">
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/plan.svg" alt="Scheme for planetary destruction">
</div>
<div class="six wide column">
<h2>Embrace the Void</h2>
<h3>Remove unnecessary clutter for your brain and save it to the cloud, allowing you to easily embrace the gaping abyss</h3>
</div>
</div>
<!-- set -->
<div class="middle aligned centered row">
<div class="six wide column">
<h2>Space for Growth</h2>
<h3>Groom a clear path for new expressions and innovations. Elevate your being and lower your cholesterol</h3>
</div>
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/growth.svg" alt="Endless progress at the cost of sanity and health">
</div>
</div>
<div class="middle aligned centered row">
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="Shrunken man near giant tablet">
</div>
<div class="six wide column">
<h2>Become your Data</h2>
<h3>We exist as electrical impulses, no different from data on a computer</h3>
</div>
</div>
<!-- set -->
<div class="middle aligned centered row">
<div class="six wide column">
<h2>Ice Cream</h2>
<h3>Get excited without all the screaming</h3>
</div>
<div class="six 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 ">
</div>
</div>
<div class="middle aligned centered row">
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
</div>
<div class="six wide column">
<h2>Data Backups</h2>
<h3>Nothing you do will be forgotten.<br>You can never take back what you have done</h3>
</div>
</div>
<div class="middle aligned centered row">
<div class="six wide column">
<h2>Freedom to unleash yourself</h2>
<h3>Imagine an awakening of what could be</h3>
</div>
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/grandma.svg" alt="Drinking the blood of the elderly">
</div>
</div>
<!-- final slide -->
<div class="middle aligned centered green row">
<div class="twelve wide center aligned column">
<br>
<br>
<br>
<br>
<h2>What are you waiting for?<br>Sign up now.</h2>
<br>
<router-link class="ui huge white labeled icon button" to="/login">
<i class="plug icon"></i>Sign Me Up!
</router-link>
<br>
<br>
<br>
OR
<br>
<br>
<br>
<span class="ui button" v-on:click="showRealInformation">View real information about this site</span>
</div>
</div>
<div v-if="realInformation" class="middle aligned centered row" ref="real">
<div class="six wide column">
<h2 class="ui center aligned">
What is this really?
</h2>
<h3>Its just a little web app for taking notes. This page is mocking the "over the top" marketing sites use to sell their products.</h3>
<p>
This App exists because 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>
If you want to give it a shot, feel free to make an account. There are no ads. None of this data is shared or public. I don't make any money.
</p>
<p>
If you see anything broken or want to see a feature implemented, I'm open to suggestions. <i class="thumbs up icon"></i>
</p>
<p>Hero Slide Photo Credit - <a target="_blank" href="https://unsplash.com/@tkaslik14">https://unsplash.com/@tkaslik14</a></p>
<p>Generic Marketing Images - <a target="_blank" href="https://undraw.co/">https://unDraw.co/</a></p>
</div>
<div class="four wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/watching.svg" alt="Drinking the blood of the elderly">
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'WelcomePage' name: 'WelcomePage',
data(){
return {
height: null,
realInformation: false,
}
},
beforeMount(){
//Force HTTPS on prod, always. Dev doesn't have certs
const isDev = process.env['NODE_ENV'] == 'development'
if (!isDev && location.protocol != 'https:'){
window.location.replace('https://www.avidhabit.com')
}
//Don't change hero banner on mobile
if(!this.$store.getters.getIsUserOnMobile){
let windowHeight = window.innerHeight
this.height = windowHeight - (windowHeight * 0.10)
}
},
methods: {
showRealInformation(){
this.realInformation = !this.realInformation
if(this.realInformation){
this.$nextTick(() => {
this.$refs.real.scrollIntoView({'behavior':'smooth'})
})
}
}
}
} }
</script> </script>

View File

@@ -1,41 +1,41 @@
<template> <template>
<div class="ui container"> <div class="ui basic segment no-fluf-segment">
<h3>Login</h3> <div class="ui grid">
<p>Begin the login process by typing your username or email.</p> <div class="ui sixteen wide column">
<p>To create an account, type in the username you want to use followed by the password.</p>
<p>You will remain logged in on this browser, until you log out.</p>
<div class="ui segment" v-on:keyup.enter="submit"> <p><b>Create an account:</b> type in the username you want to use followed by the password.</p>
<div class="ui large form">
<div class="field"> <div class="ui segment" v-on:keyup.enter="submit">
<div class="ui input"> <div class="ui large form">
<input v-model="username" type="text" name="email" placeholder="Username or E-mail address" autofocus> <div class="field">
<div class="ui input">
<input v-model="username" type="text" name="email" placeholder="Username or E-mail address" autofocus>
</div>
</div>
<div class="field">
<div class="ui input">
<input v-model="password" type="password" name="password" placeholder="Password">
</div>
</div>
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="submit" class="ui massive compact fluid green submit button">Login</div>
</div> </div>
</div> </div>
<div class="field" v-if="username.length > 0">
<div class="ui input"> <p>You will remain logged in on this browser, until you log out.</p>
<input v-model="password" type="password" name="password" placeholder="Password">
</div> </div>
</div>
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="submit" class="ui massive compact fluid green submit button">Login</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
//ajax calls
import axios from 'axios'; import axios from 'axios';
import { mapGetters } from 'vuex'
export default { export default {
name: 'Login', name: 'Login',
data () { data () {
return { return {
message: 'Login stuff',
enabled: false, enabled: false,
username: '', username: '',
password: '' password: ''
@@ -61,26 +61,24 @@
axios.post('/api/user/login', data) axios.post('/api/user/login', data)
.then(response => { .then(response => {
console.log(response)
if(response.data.success){ if(response.data.success){
const token = response.data.token const token = response.data.token
const username = response.data.username const username = response.data.username
vm.$store.commit('setLoginToken', {token, username}) vm.$store.commit('setLoginToken', {token, username})
//Redirect user to notes section after login //Redirect user to notes section after login
this.$router.push('/notes') vm.$router.push('/notes')
} else {
this.$bus.$emit('notification', 'Incorrect Username or Password')
vm.$store.commit('destroyLoginToken')
} }
}) })
.catch(error => { .catch(error => {
console.log('There was an error with log in request') this.$bus.$emit('notification', 'Incorrect Username or Password')
}) })
} }
},
computed: {
...mapGetters([
'getRudeMessage'
])
} }
} }
</script> </script>

View File

@@ -1,120 +1,159 @@
<template> <template>
<div class="ui basic segment"> <div class="ui basic segment no-fluf-segment">
<div class="ui equal width grid"> <div class="ui grid" :class="{ 'mush-it-up':showOneColumn() }" ref="content">
<!-- <div class="ui row">{{ $store.getters.getIsUserOnMobile ? 'Mobile Device':'Normal Browser' }}</div> --> <div v-if="!$store.getters.getIsUserOnMobile" class="sixteen wide column">
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
<!-- mobile search menu --> <div class="ui grid">
<div class="ui mobile only row">
<!-- Small screen new note button --> <div class="eight wide column">
<div class="ui four wide column"> <search-input></search-input>
<div @click="createNote" class="ui fluid green icon button">
<i class="plus icon"></i>
</div> </div>
<div class="eight wide column">
<div class="ui basic button" v-on:click="updateFastFilters(3)" v-if="$store.getters.totals && $store.getters.totals['sharedToNotes'] > 0" style="position: relative;">
<i class="green mail icon"></i>Inbox
<span class="floating ui green label" v-if="$store.getters.totals['unreadNotes'] > 0">
{{ $store.getters.totals['unreadNotes'] }}
</span>
</div>
<div class="ui basic button" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
<i class="green archive icon"></i>Archived
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
</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>Back to All Notes
</span>
</div>
</div> </div>
<div class="ui twelve wide column"> </div>
<div class="ui form">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" />
</div> <div v-if="$store.getters.getIsUserOnMobile && showClear" class="row">
<div class="sixteen wide column">
<span class="ui fluid green button" @click="reset">
<i class="arrow circle left icon"></i>Back to All Notes
</span>
</div> </div>
</div> </div>
<!-- search menu --> <div v-if="commonTags.length > 0" class="sixteen wide column">
<div class="ui large screen only row"> <h4><i class="green tags icon"></i>Tags</h4>
<span v-for="tag in commonTags" @click="toggleTagFilter(tag.id)">
<div class="ui two wide column"> <span class="ui clickable basic label" :class="{ 'green':(searchTags.includes(tag.id)) }">
<div @click="createNote" class="ui fluid green button"> {{ucWords(tag.text)}} <span class="detail">{{tag.usages}}</span>
<i class="plus icon"></i> </span>
New Note </span>
</div>
</div>
<div class="ui five wide column">
<div class="ui form">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" />
</div>
</div>
<div class="ui nine wide column">
<router-link class="ui basic button" to="/help">Help</router-link>
<div v-on:click="toggleNightMode" class="ui basic icon button">
<i class="eye icon"></i>&nbsp;Dark Theme:
<span v-if="$store.getters.getIsNightMode">On</span>
<span v-else>Off</span>
</div>
<div v-on:click="toggleArchivedVisible" class="ui basic icon button">
<i class="archive icon"></i>&nbsp;Archived:
<span v-if="showArchived == 1">Visible</span>
<span v-else>Hidden</span>
</div>
<div class="ui right floated basic button"
data-tooltip="Log Out" data-position="left center"
v-on:click="destroyLoginToken"><i class="user icon"></i> {{username}}</div>
</div>
</div> </div>
<div class="ui row"> <h2 v-if="fastFilters['withLinks'] == 1">Only showing notes containing Links</h2>
<h2 v-if="fastFilters['withTags'] == 1">Only showing notse with Tags</h2>
<h2 v-if="fastFilters['onlyArchived'] == 1">Only showing Archived notes.</h2>
<!-- tags display --> <!-- Note title card display -->
<div class="ui two wide large screen only column" v-if="activeNoteId1 == null && activeNoteId2 == null"> <div class="sixteen wide column">
<div class="ui small basic fluid button" @click="reset"> <h3 v-if="searchTerm.length > 0 && notes.length == 0">No notes found. Check your spelling, try completing the word or using a different phrase.</h3>
<i class="undo icon"></i>Reset Filters
<h3 v-if="searchTerm.length == 0 && notes.length == 0">Create your first note. Click the "New Note" button.</h3>
<div v-if="working">
<div class="ui active inline loader"></div> Working...
</div>
<!-- Go to one wide column, do not do this on mobile interface -->
<div v-if="notes !== null && notes.length > 0"
:class="{'one-column':(
(activeNoteId1 != null || activeNoteId2 != null) &&
!$store.getters.getIsUserOnMobile
)}">
<!-- pinned notes -->
<div v-if="containsPinnednotes > 0" class="note-card-section">
<!-- ({{containsPinnednotes}}) -->
<h4><i class="green pin icon"></i>Pinned</h4>
<div class="note-card-display-area">
<note-title-display-card
v-for="note in notes"
v-if="note.pinned"
:onClick="openNote"
:data="note"
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length"
/>
</div>
</div> </div>
<div class="ui divider"></div>
<div class="ui section list"> <!-- normal notes -->
<div class="item" v-for="tag in commonTags" @click="toggleTagFilter(tag.id)"> <div v-if="containsNormalNotes > 0" class="note-card-section">
<div class="ui clickable basic fluid large label" :class="{ 'green':(searchTags.includes(tag.id)) }"> <!-- ({{containsNormalNotes}}) -->
{{ucWords(tag.text)}} <div class="detail">{{tag.usages}}</div> <h4><i class="green file icon"></i>Notes</h4>
</div> <div class="note-card-display-area">
<note-title-display-card
v-for="note in notes"
v-if="note.note_highlights && !note.pinned"
:onClick="openNote"
:data="note"
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length"
/>
</div>
</div>
<!-- found in text -->
<div v-if="containsTextResults" class="note-card-section">
<h4><i class="green paragraph icon"></i> Found in Text ({{containsTextResults}})</h4>
<div class="note-card-display-area">
<note-title-display-card
v-for="note in notes"
v-if="note.note_highlights && note.note_highlights.length"
:textResults="true"
:onClick="openNote"
:data="note"
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length"
/>
</div> </div>
</div> </div>
</div> </div>
<!-- Note title cards -->
<div class="ui fourteen wide computer sixteen wide mobile column">
<h2>
({{notes.length}}) <fast-filters />
</h2>
<h3 v-if="searchTerm.length > 0 && notes.length == 0">No notes found. Check your spelling, try completing the word or using a different phrase.</h3>
<h3 v-if="searchTerm.length == 0 && notes.length == 0">Create your first note. Click the "New Note" button.</h3>
<div v-if="working"><div class="ui active inline loader"></div> Working...</div>
<div v-if="notes !== null && !working"
class="note-card-display-area"
:class="{'one-column':(activeNoteId1 != null || activeNoteId2 != null )}
">
<note-title-display-card
v-for="note in notes"
:onClick="openNote"
:data="note"
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length"
/>
</div>
</div>
</div> </div>
<!-- found attachments -->
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
<h4><i class="folder open outline icon"></i> Found in Files ({{ foundAttachments.length }})</h4>
<attachment-display
v-for="item in foundAttachments"
:item="item"
:key="item.id"
:search-params="{}"
/>
</div>
</div> </div>
<input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" /> <input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" ref="note1" />
<input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" /> <input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" ref="note2" />
</div> </div>
</template> </template>
<script> <script>
import axios from 'axios'; import axios from 'axios'
export default { export default {
name: 'SearchBar', name: 'SearchBar',
@@ -122,60 +161,152 @@
'input-notes': require('@/components/NoteInputPanel.vue').default, 'input-notes': require('@/components/NoteInputPanel.vue').default,
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default, 'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
'fast-filters': require('@/components/FastFilters.vue').default, 'fast-filters': require('@/components/FastFilters.vue').default,
'search-input': require('@/components/SearchInput.vue').default,
'attachment-display': require('@/components/AttachmentDisplayCard').default,
}, },
data () { data () {
return { return {
username:'',
initComponent: true, initComponent: true,
commonTags: [], commonTags: [],
searchTerm: '', searchTerm: '',
searchTags: [], searchTags: [],
notes: [], notes: [],
highlights: [],
searchDebounce: null, searchDebounce: null,
fastFilters: {}, fastFilters: {},
showArchived: 0,
working: false, working: false,
//Load up notes in batches
firstLoadBatchSize: 30, //First set of rapidly loaded notes
batchSize: 100, //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,
fetchTags: false,
//Clear button is not visible
showClear: false,
initialPostData: null,
currentPostData: null,
containsNormalNotes: 0,
containsPinnednotes: 0,
containsTextResults: 0,
// containsTagResults: 0,
// containsAttachmentResults: 0,
//Currently open notes in app //Currently open notes in app
activeNoteId1: null, activeNoteId1: null,
activeNoteId2: null, activeNoteId2: null,
//Position determines how note is Positioned //Position determines how note is Positioned
activeNote1Position: 0, activeNote1Position: 0,
activeNote2Position: 0, activeNote2Position: 0,
lastVisibilityState: null,
foundAttachments: []
} }
}, },
beforeMount(){ beforeMount(){
let username = this.$store.getters.getUsername this.$parent.loginGateway()
this.username = this.ucWords(username)
this.$bus.$on('close_active_note', position => { //Update totals for app
this.$store.dispatch('fetchAndUpdateUserTotals')
this.$bus.$on('close_active_note', ({position, noteId, modified}) => {
this.closeNote(position) this.closeNote(position)
this.$store.dispatch('fetchAndUpdateUserTotals')
if(modified){
this.updateSingleNote(noteId)
}
}) })
this.$bus.$on('note_deleted', () => { this.$bus.$on('note_deleted', (noteId) => {
this.search() //Remove deleted note from set, its deleted
this.notes.forEach( (note, index) => {
if(note.id == noteId){
if(note.pinned == 1){
this.containsPinnednotes--
} else {
this.containsNormalNotes--
}
this.notes.splice(index, 1)
}
})
}) })
this.$bus.$on('update_fast_filters', newFilter => { this.$bus.$on('update_fast_filters', newFilter => {
this.fastFilters = newFilter this.fastFilters = newFilter
this.search() //Fast filters always return all the results and tags
this.search(true, this.batchSize, false).then( () => {
return this.fetchUserTags()
})
})
//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()
return this.fetchUserTags()
})
})
//New note button pushes open note event
this.$bus.$on('open_note', noteId => {
this.openNote(noteId)
})
//Reload page content
this.$bus.$on('note_reload', () => {
this.reset()
}) })
//Mount notes on load if note ID is set //Mount notes on load if note ID is set
if(this.$route.params && this.$route.params.id){ if(this.$route.params && this.$route.params.id){
const id = this.$route.params.id const id = this.$route.params.id
console.log('About to load note ', id)
this.openNote(id) this.openNote(id)
} }
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)
window.removeEventListener('hashchange', this.hashChangeAction)
document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
//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() { mounted() {
this.search() //Loads initial batch and tags
this.reset()
}, },
methods: { methods: {
openNote(id){ showOneColumn(){
//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 }
}
//Do not open same note twice //Do not open same note twice
if(this.activeNoteId1 == id || this.activeNoteId2 == id){ if(this.activeNoteId1 == id || this.activeNoteId2 == id){
@@ -196,7 +327,6 @@
this.activeNote2Position = 2 //Left side of page this.activeNote2Position = 2 //Left side of page
return return
} }
//2 notes open //2 notes open
if(this.activeNoteId2 != null && this.activeNoteId1 == null){ if(this.activeNoteId2 != null && this.activeNoteId1 == null){
this.activeNoteId1 = id this.activeNoteId1 = id
@@ -219,12 +349,17 @@
this.activeNoteId2 = null this.activeNoteId2 = null
} }
this.$router.push('/notes') //IF two notes get opened, update ID of open note
if(this.activeNoteId1 || this.activeNoteId2){
this.$router.push('/notes/open/'+Math.max(this.activeNoteId1,this.activeNoteId2))
} else {
//No notes are open, just show notes page
this.$router.push('/notes')
}
this.activeNote1Position = 0 this.activeNote1Position = 0
this.activeNote2Position = 0 this.activeNote2Position = 0
this.search(false)
}, },
toggleTagFilter(tagId){ toggleTagFilter(tagId){
@@ -234,55 +369,261 @@
this.searchTags.push(tagId) this.searchTags.push(tagId)
} }
this.search() //Reset note set and load up notes and tags
}, if(this.searchTags.length > 0){
search(showLoading = true){ this.search(true, this.batchSize)
return
//Add archived to fast filters
this.fastFilters['archived'] = 0
if(this.showArchived == 1){
this.fastFilters['archived'] = 1
} }
let postData = { //If no tags are selected, reset entire page
this.reset()
},
onScroll(e){
clearTimeout(this.loadingBatchTimeout)
this.loadingBatchTimeout = setTimeout(() => {
//Distance to bottom of page
const bottomOfWindow =
Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop)
+ window.innerHeight
//height of page
const offsetHeight = this.$refs.content.clientHeight
//Determine percentage down the page
const percentageDown = Math.round( (bottomOfWindow/offsetHeight)*100 )
// console.log( percentageDown + '%' )
//If greater than 80 of the way down the page, load the next batch
if(percentageDown >= 80){
this.search(false, this.batchSize, true)
}
}, 50)
return
},
//Try to close notes on URL hash change /notes/open/123 to /notes - parse 123, close note id 123
hashChangeAction(event){
//Clean up path of hash change
let path = window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.hash
let newPath = event.newURL.replace(path,'')
let oldPath = event.oldURL.replace(path,'')
//If we go from open note ID to no note ID, close the note
if(newPath == '' && oldPath.indexOf('/open/') != -1){
//Pull note ID out of URL
const noteIdToClose = oldPath.split('/').pop()
if(this.$refs.note1 && this.$refs.note1.currentNoteId == noteIdToClose){
this.$refs.note1.close()
}
if(this.$refs.note2 && this.$refs.note2.currentNoteId == noteIdToClose){
this.$refs.note2.close()
}
}
},
visibiltyChangeAction(event){
//@TODO - set a timeout on this like 2 minutes or just dont do shit and update it via socket.io
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
// console.log('Welcome back. Reloading a batch')
//Load initial batch, then tags, then other batch
this.search(false, this.firstLoadBatchSize)
.then( () => {
return this.fetchUserTags()
})
}
this.lastVisibilityState = document.visibilityState
},
// @TODO Don't even trigger this if the note wasn't changed
updateSingleNote(noteId){
//Lookup one note using passed in ID
const postData = {
searchQuery: this.searchTerm, searchQuery: this.searchTerm,
searchTags: this.searchTags, searchTags: this.searchTags,
fastFilters: this.fastFilters, fastFilters:{
noteIdSet:[noteId]
}
} }
if(showLoading){ axios.post('/api/note/search', postData)
this.working = true .then(results => {
}
//Pull note data out of note set
let newNote = results.data.notes[0]
let foundNote = false
//Perform search if(newNote === undefined){
let vm = this console.log('Note not visible on this page')
axios.post('/api/note/search', postData). return
then(response => { }
vm.commonTags = response.data.tags
vm.notes = response.data.notes //Go through each note and find the one just updated
vm.highlights = response.data.highlights this.notes.forEach( (note,index) => {
this.working = false
if(note.id == noteId){
foundNote = true
//Don't move notes that were not changed
if(note.updated == newNote.updated){
return
}
//Compare note tags, if they changed, reload tags
if(newNote.tag_count != note.tag_count){
console.log('Tags changed, update those bitches')
this.fetchUserTags()
}
//go through each prop and update it with new values
Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop]
})
this.notes.splice(index, 1)
this.notes.unshift(note)
}
})
//This note was not found, update it in list
if(foundNote == false){
if(newNote.pinned == 1){
this.containsPinnednotes++
} else {
this.containsNormalNotes++
}
this.notes.unshift(newNote)
}
})
},
searchAttachments(){
axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm})
.then(results => {
console.log('Attachment Results')
console.log(results.data)
this.foundAttachments = results.data
})
},
search(showLoading = true, notesInNextLoad = null, mergeExisting = false){
return new Promise((resolve, reject) => {
//Don't double load note batches
if(this.loadingInProgress){
return resolve()
}
//Reset a lot of stuff if we are not merging batches
if(!mergeExisting){
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
// this.commonTags = [] //Don't reset tags, if search returns tags, they will be set
}
//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,
}
if(showLoading){
this.working = true
}
//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 => {
//Save the number of notes just loaded
this.batchOffset += response.data.notes.length
//Mush the two new sets of data together (set will be empty is reset is on)
if(response.data.tags.length > 0){
this.commonTags = response.data.tags
}
//Either reload all notes with return data or merge return data
if(!mergeExisting){
this.notes = response.data.notes
} else {
this.notes = this.notes.concat(response.data.notes)
}
//Go through each note and see which section to display
let textResultsCount = 0
let pinnedResultsCount = 0
let normalNotesCount = 0
response.data.notes.forEach(note => {
if(note.note_highlights.length > 0){
textResultsCount++
return
}
if(note.pinned == 1){
pinnedResultsCount++
return
}
normalNotesCount++
})
if(!mergeExisting){
this.containsNormalNotes = normalNotesCount
this.containsPinnednotes = pinnedResultsCount
this.containsTextResults = textResultsCount
} else {
this.containsNormalNotes += normalNotesCount
this.containsPinnednotes += pinnedResultsCount
this.containsTextResults += textResultsCount
}
this.working = false
this.loadingInProgress = false
return resolve(true)
})
}) })
}, },
searchKeyUp(){ searchKeyUp(){
let vm = this let vm = this
clearTimeout(vm.searchDebounce) clearTimeout(vm.searchDebounce)
vm.searchDebounce = setTimeout(() => { vm.searchDebounce = setTimeout(() => {
vm.search() this.search(true, this.batchSize)
}, 300) .then( () => {
}, return this.fetchUserTags()
createNote(event){ })
const title = '' }, 500)
let vm = this
axios.post('/api/note/create', {title})
.then(response => {
if(response.data && response.data.id){
vm.openNote(response.data.id)
}
})
}, },
ucWords(str){ ucWords(str){
return (str + '') return (str + '')
@@ -291,31 +632,67 @@
}) })
}, },
reset(){ reset(){
this.showClear = false
this.searchTerm = '' this.searchTerm = ''
this.searchTags = [] this.searchTags = []
this.fastFilters = {} this.fastFilters = {}
this.foundAttachments = [] //Remove all attachments
this.$bus.$emit('reset_fast_filters') this.$bus.$emit('reset_fast_filters')
this.search()
//Load initial batch, then tags, then other batch
this.search(true, this.firstLoadBatchSize)
.then( () => {
return this.fetchUserTags()
})
.then( () => {
//Load a larger batch once first batch has loaded
return this.search(false, this.batchSize, true)
})
.then( i => {
//Thats how you promise chain
})
}, },
destroyLoginToken() { fetchUserTags(){
this.$store.commit('destroyLoginToken') return new Promise((resolve, reject) => {
this.$router.push('/') axios.post('/api/tag/usertags')
.then( ({data}) => {
this.commonTags = data
resolve(data)
})
})
}, },
toggleNightMode(){ updateFastFilters(index){
this.$store.commit('toggleNightMode')
}, //clear out tags
toggleArchivedVisible(){ this.searchTags = []
if(this.showArchived == 0){
this.showArchived = 1 //A little hacky, brings user to notes page then filters on click
} else { if(this.$route.name != 'NotesPage'){
this.showArchived = 0 this.$router.push('/notes')
setTimeout( () => {
this.updateFastFilters(index)
}, 500 )
} }
this.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
]
let filter = {}
filter[options[index]] = 1
this.$bus.$emit('update_fast_filters', filter)
} }
} }
} }
</script> </script>
<style type="text/css" scoped> <style type="text/css" scoped>
.mush-it-up {
width: calc(50% - 130px);
}
.detail { .detail {
float: right; float: right;
} }
@@ -323,4 +700,14 @@
display: flex; display: flex;
flex-wrap: wrap; 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> </style>

View File

@@ -0,0 +1,127 @@
<template>
<div class="ui basic segment no-fluf-segment">
<div class="ui grid">
<div class="ui sixteen wide column">
<h2 class="ui header">
<i class="paper plane outline icon"></i>
<div class="content">
Quick
<div class="sub header">Add new information with great speed</div>
</div>
</h2>
</div>
<div class="ui sixteen wide column">
<div class="ui form">
<div class="field">
<textarea
class="quick-note-input"
rows="1"
ref="fastInput"
v-model="newText"
v-on:keydown="checkKeyup"
placeholder="Push to the top of the quick note."
></textarea>
</div>
<div class="field">
<div v-on:click="appendQuickNote" class="ui green button">Save (CRTL + Enter)</div>
<div v-if="quickNoteId" class="ui right floated basic button" v-on:click="$router.push('/attachments/note/'+quickNoteId)">
<i class="folder open outline icon"></i>
Quick Files + Links
</div>
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button">
<i class="file outline icon"></i>
Edit
</div>
</div>
</div>
</div>
<div class="fun" v-html="savedQuickNoteText"></div>
</div>
</div>
</template>
<style type="text/css" scoped>
.quick-note-input {
box-sizing: border-box !important;
resize: none !important;
}
</style>
<script>
import axios from 'axios'
export default {
data: function(){
return {
newText: '',
savedQuickNoteText: '',
quickNoteId: null
}
},
beforeCreate: function(){
//
// Perform Login check
//
this.$parent.loginGateway()
},
mounted: function(){
if(this.$refs.fastInput){
//Load up note text
this.getQuickNote()
//Set focus to input pane
this.$nextTick(() => {
this.$refs.fastInput.focus()
})
}
},
computed: {
loggedIn () {
return this.$store.getters.getLoggedIn
}
},
methods: {
openNoteEdit(){
this.$router.push({'path':'/notes/open/'+this.quickNoteId})
},
checkKeyup(event){
let element = event.target
let padding = 0
element.style.height = 'auto';
element.style.height = (element.scrollHeight + padding) +'px';
//If command+enter or control+enter is pressed, submit
if((event.metaKey || event.ctrlKey) && [13].includes(event.keyCode)){
this.appendQuickNote()
}
},
appendQuickNote(){
//Don't submit empty note
if(this.newText.trim() == ''){ return }
axios.post('/api/quick-note/update', { 'pushText':this.newText.trim() } )
.then( results => {
this.newText = '' //Clear text area
this.$refs.fastInput.style.height = 'auto' //Back to normal size
this.savedQuickNoteText = results.data.text
this.quickNoteId = results.data.id
})
},
getQuickNote (){
axios.post('/api/quick-note/get')
.then( results => {
this.savedQuickNoteText = results.data.text
this.quickNoteId = results.data.id
})
},
}
}
</script>

View File

@@ -0,0 +1,49 @@
<template>
<div class="ui basic segment">
<div class="ui container">
<div class="fun" :style="{'color':color}" v-if="noteText" v-html="noteText"></div>
</div>
<div class="ui basic segment"></div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'SharePage',
data(){
return {
noteText: null,
color: '#000'
}
},
beforeMount(){
//Mount notes on load if note ID is set
if(this.$route.params && this.$route.params.id){
const id = this.$route.params.id
this.openNote(id)
}
},
methods:{
openNote(noteId){
axios.post('/api/public/note', {'noteId': noteId})
.then( response => {
let colors = JSON.parse(response.data.color)
if(colors && colors.noteBackground){
document.body.style.background = colors.noteBackground
}
if(colors && colors.noteText){
this.color = colors.noteText
}
this.noteText = response.data.text
})
}
}
}
</script>

View File

@@ -1,10 +1,23 @@
import Vue from 'vue' import Vue from 'vue'
import Router from 'vue-router' import Router from 'vue-router'
import HomePage from '@/pages/HomePage' //Breaking components into function sections allows webpack to load them dynamically
import LoginPage from '@/pages/LoginPage' //import HomePage from '@/pages/HomePage'
const HomePage = () => import('@/pages/HomePage')
// import LoginPage from '@/pages/LoginPage'
const LoginPage = () => import('@/pages/LoginPage')
// import HelpPage from '@/pages/HelpPage'
const HelpPage = () => import('@/pages/HelpPage')
// import SharePage from '@/pages/SharePage'
const SharePage = () => import('@/pages/SharePage')
//These guys can all be loaded as a chunk
import NotesPage from '@/pages/NotesPage' import NotesPage from '@/pages/NotesPage'
import HelpPage from '@/pages/HelpPage' import QuickPage from '@/pages/QuickPage'
import AttachmentsPage from '@/pages/AttachmentsPage'
Vue.use(Router) Vue.use(Router)
@@ -40,5 +53,29 @@ export default new Router({
meta: {title:'Help'}, meta: {title:'Help'},
component: HelpPage component: HelpPage
}, },
{
path: '/share/:id',
name: 'Share',
meta: {title:'Shared'},
component: SharePage
},
{
path: '/quick',
name: 'Quick',
meta: {title:'Quick'},
component: QuickPage
},
{
path: '/attachments',
name: 'Attachments',
meta: {title:'Attachments'},
component: AttachmentsPage
},
{
path: '/attachments/note/:id',
name: 'Attachments',
meta: {title:'Attachments'},
component: AttachmentsPage
},
] ]
}) })

View File

@@ -6,17 +6,15 @@ Vue.use(Vuex);
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
count: 0,
message: 'Get out me yard ya wankers',
token: null, token: null,
username: null, username: null,
nightMode: false, nightMode: false,
isUserOnMobile: false, isUserOnMobile: false,
isNoteSettingsOpen: false, //Little note settings pane
socket: null,
userTotals: null,
}, },
mutations: { mutations: {
increment (state) {
state.count++
},
setLoginToken(state, userData){ setLoginToken(state, userData){
const username = userData.username const username = userData.username
@@ -29,7 +27,7 @@ export default new Vuex.Store({
localStorage.setItem('username', username) localStorage.setItem('username', username)
//Set default token to axios, every request will have header //Set default token to axios, every request will have header
axios.defaults.headers.common['Authorization'] = token axios.defaults.headers.common['authorizationtoken'] = token
state.token = token state.token = token
state.username = username state.username = username
@@ -39,7 +37,7 @@ export default new Vuex.Store({
//Remove login token from local storage and from headers //Remove login token from local storage and from headers
localStorage.removeItem('loginToken') localStorage.removeItem('loginToken')
localStorage.removeItem('username') localStorage.removeItem('username')
delete axios.defaults.headers.common['Authorization'] delete axios.defaults.headers.common['authorizationtoken']
state.token = null state.token = null
state.username = null state.username = null
}, },
@@ -54,6 +52,7 @@ export default new Vuex.Store({
'background_color': '#fff', 'background_color': '#fff',
'text_color': '#3d3d3d', 'text_color': '#3d3d3d',
'outline_color': 'rgba(34,36,38,.15)', 'outline_color': 'rgba(34,36,38,.15)',
'border_color': 'rgba(34,36,38,.20)',
} }
//Night mode colors //Night mode colors
if(state.nightMode){ if(state.nightMode){
@@ -61,6 +60,7 @@ export default new Vuex.Store({
'background_color': '#000', 'background_color': '#000',
'text_color': '#a98457', 'text_color': '#a98457',
'outline_color': '#a98457', 'outline_color': '#a98457',
'border_color': '#a98457',
} }
} }
@@ -80,13 +80,27 @@ export default new Vuex.Store({
state.isUserOnMobile = true state.isUserOnMobile = true
} }
})(navigator.userAgent||navigator.vendor||window.opera, state); })(navigator.userAgent||navigator.vendor||window.opera, state);
},
toggleNoteSettingsPane(state){
state.isNoteSettingsOpen = !state.isNoteSettingsOpen
},
setSocketIoSocket(state, socket){
//Put socket id in axios headers
axios.defaults.headers.common['socketId'] = socket
state.socket = socket
},
setUserTotals(state, totalsObject){
//Save all the totals for the user
state.userTotals = totalsObject
// Object.keys(totalsObject).forEach( key => {
// console.log(key + ' -- ' + totalsObject[key])
// })
} }
}, },
getters: { getters: {
getRudeMessage: state => {
return state.message
},
getUsername: state => { getUsername: state => {
return state.username return state.username
}, },
@@ -102,6 +116,23 @@ export default new Vuex.Store({
}, },
getIsUserOnMobile: state => { getIsUserOnMobile: state => {
return state.isUserOnMobile return state.isUserOnMobile
},
getIsNoteSettingsOpen: state => {
return state.isNoteSettingsOpen
},
getSocket: state => {
return state.socket
},
totals: state => {
return state.userTotals
},
},
actions: {
fetchAndUpdateUserTotals ({ commit }) {
axios.post('/api/user/totals')
.then( ({data}) => {
commit('setUserTotals', data)
})
} }
} }
}) })

View File

@@ -1,32 +1,31 @@
##
#
# This is just a mock config file, describing what is needed to run the app
# The app currently only needs two paths / and /api
#
##
#
# This is needed to define any ports the app may use from node
#
upstream expressapp { upstream expressapp {
server 127.0.0.1:3000; server 127.0.0.1:3000;
keepalive 8; keepalive 8;
} }
server { server {
listen 80;
server_name logiclabs.icu;
root /home/mab/pi/client/dist;
access_log /var/log/nginx/httpslocalhost.access.log;
error_log /var/log/nginx/httpslocalhost.error.log;
#
# Needed to server up static, compiled JS files and index.html
#
location / { location / {
autoindex on; autoindex on;
#try_files $uri $uri/ /index.html;
} }
location /app { #
proxy_pass http://127.0.0.1:8444; # define the api route to connect to the backend and serve up static files
proxy_http_version 1.1; #
proxy_set_header Upgrade $http_upgrade; location /api {
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /api {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
@@ -34,15 +33,6 @@ server {
proxy_pass http://expressapp; proxy_pass http://expressapp;
proxy_redirect off; proxy_redirect off;
} }
location /solr {
proxy_pass http://127.0.0.1:8983;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
} }

View File

@@ -10,3 +10,5 @@ common.js
*/unminified/bundle.* */unminified/bundle.*
bundle.* bundle.*
client/dist* client/dist*
server/public/*
client/dist*

View File

@@ -12,10 +12,14 @@
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"cheerio": "^1.0.0-rc.3", "cheerio": "^1.0.0-rc.3",
"express": "^4.16.4", "express": "^4.16.4",
"gm": "^1.23.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"multer": "^1.4.2",
"mysql2": "^1.6.5", "mysql2": "^1.6.5",
"node-tesseract-ocr": "^1.0.0",
"request": "^2.88.0", "request": "^2.88.0",
"request-promise": "^4.2.4", "request-promise": "^4.2.4",
"socket.io": "^2.3.0",
"solr-node": "^1.2.1" "solr-node": "^1.2.1"
}, },
"_moduleAliases": { "_moduleAliases": {

View File

@@ -0,0 +1,31 @@
module.exports = {
apps : [{
name: 'NoteServer',
script: 'index.js',
// Options reference: https://pm2.io/doc/en/runtime/reference/ecosystem-file/
// args: 'one two',
instances: 1,
autorestart: true,
watch: true,
ignore_watch : ["node_modules", "staticFiles"],
max_memory_restart: '1G',
env: {
NODE_ENV: 'development'
},
env_production: {
NODE_ENV: 'production'
}
}],
deploy : {
production : {
user : 'node',
host : '212.83.163.1',
ref : 'origin/master',
repo : 'git@github.com:repo.git',
path : '/var/www/production',
'post-deploy' : 'npm install && pm2 reload ecosystem.config.js --env production'
}
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,161 @@
let ProcessText = module.exports = {}
ProcessText.removeHtml = (string) => {
if(string == undefined || string == null || string.length == 0){
return ''
}
return string
.replace(/&[[#A-Za-z0-9]+A-Za-z0-9]+;/g,' ') //Rip out all HTML entities
.replace(/<[^>]+>/g, ' ') //Rip out all HTML tags
.replace(/\s+/g, ' ') //Remove all whitespace
.trim()
}
ProcessText.getUrlsFromString = (string) => {
const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm
return string.match(urlPattern)
}
/*
Pulls out title and subtext of note
+ Title is always first line
+ Empty lines are skipped
+ URLs are turned into links
+ All URLs are givent the target="_blank" property
+ Lists are given extra display characters
+ If note starts as a list, skip the title
*/
ProcessText.deduceNoteTitle = (inString) => {
let title = '' //Title of note
let sub = '' //sub text below note
if(!inString || inString == null || inString.length == 0){
return {title, sub}
}
//Remove inline styles that may be added by editor
inString = inString.replace(/style=".*?"/g,'')
//Match full line and closing tag or just closing tag
let lines = inString.match(/[<[a-zA-Z0-9]+>(.*?)<\/[a-zA-Z0-9]+>|<\/[a-zA-Z0-9>]+?>/g)
if(lines == null){ lines = [inString] }
//.match(/[^\r\n]+/g) //Match return or newline
// console.log(lines)
let finalLines = []
const startTags = ['<ol','<li','<ul']
const endTags = ['</o','</l','</u']
let totalLines = Math.min(lines.length, 6)
let charLimit = 250
let listStart = false
let noTitleJustList = false
for(let i=0; i < totalLines; i++){
//Just in case 'i' gets bigger than array
if(lines[i] === undefined){
continue
}
//Various empty chars are possible
const cleanLine = ProcessText.removeHtml(lines[i])
.replace('<br>','')
.trim()
const lineStart = lines[i].trim().substring(0, 3)
charLimit -= cleanLine.length
//Close out list if char limit is hit
if(charLimit <= 0 && listStart){
finalLines.push(lines[i])
break
}
//Images appear as empty, push em!
if(cleanLine.length == 0 && lines[i].indexOf('<img') != -1){
finalLines.push(lines[i])
continue
}
//Check if note starts with a list, don't include title, just show list
if(finalLines.length == 0 && startTags.includes(lineStart)){
noTitleJustList = true
}
//Empty line, may be a list open or close
if(cleanLine.length == 0 && (startTags.includes(lineStart) || endTags.includes(lineStart) )){
if(listStart == false){
charLimit = 400 //Double size for list notes
}
finalLines.push(lines[i])
totalLines++
listStart = true
continue
}
//If line is part of a list, up counter, we want the whole list
if(startTags.includes(lineStart)){
totalLines++
}
//Skip empty lines
if(!cleanLine || cleanLine.length == 0){
totalLines++
continue
}
//turn urls into links, don't process if its already an <a href=
const containsUrls = ProcessText.getUrlsFromString(cleanLine)
if(containsUrls && containsUrls.length == 1 && lines[i].indexOf('</a>') == -1){
const url = containsUrls[0]
lines[i] = lines[i].replace(url, `<a href="${url}">${url}</a>`)
}
//Insert target=_blank into links if set, do it for every link in line
if(lines[i].indexOf('</a>') > 0){
lines[i] = lines[i].replace(/<a /g, '<a target="_blank" ')
}
//Limit output characters
//Check character limit
if(charLimit <= 0 && listStart == false){
//Cut the string down to character limit
const cutString = lines[i].substring(0, lines[i].length+charLimit)
//Find last space and cut off everything after it
let cleanCutString = cutString.substring(0, cutString.lastIndexOf(' '))
//Some strings may not contain a space resulting in no string
if(cleanCutString.length == 0){
cleanCutString = cutString
}
finalLines.push(cleanCutString + '...')
break;
}
finalLines.push(lines[i])
}
//Pull out title if its not an empty string
if(ProcessText.removeHtml(finalLines[0]).trim().replace('&nbsp','').length > 0 && !noTitleJustList){
title = finalLines.shift()
}
sub = finalLines.join('')
//Return final display lengths
let titleLength = ProcessText.removeHtml(title).trim().replace('&nbsp','').length
let subtextLength = ProcessText.removeHtml(sub).trim().replace('&nbsp','').length
return { title, sub, titleLength, subtextLength }
}

View File

@@ -7,20 +7,84 @@ const express = require('express')
const app = express() const app = express()
const port = 3000 const port = 3000
var http = require('http').createServer(app);
var io = require('socket.io')(http, {
path:'/socket'
});
// Make io accessible to our router
app.use(function(req,res,next){
req.io = io;
next();
});
io.on('connection', function(socket){
// console.log('New user ', socket.id)
socket.on('join_room', roomId => {
// console.log('Join room ', roomId)
socket.join(roomId)
const usersInRoom = io.sockets.adapter.rooms[roomId]
if(usersInRoom){
// console.log('Users in room', usersInRoom.length)
io.to(roomId).emit('update_user_count', usersInRoom.length)
}
})
socket.on('leave_room', roomId => {
socket.leave(roomId)
// console.log('User Left room')
const usersInRoom = io.sockets.adapter.rooms[roomId]
if(usersInRoom){
// console.log('Users in room', usersInRoom.length)
io.to(roomId).emit('update_user_count', usersInRoom.length)
}
})
socket.on('note_diff', data => {
//Each user joins a room when they open the app.
io.in(data.id).clients((error, clients) => {
if (error) throw error;
//Go through each client in note room and send them the diff
clients.forEach(socketId => {
if(socketId != socket.id){
io.to(socketId).emit('incoming_diff', data.diff)
}
})
});
})
socket.on('disconnect', function(){
// console.log('user disconnected');
});
});
http.listen(3001, function(){
console.log('socket.io liseting on port 3001');
});
//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
app.use(express.json()) app.use(express.json({limit: '2mb'}))
//Prefix defied by route in nginx config //Prefix defied by route in nginx config
const prefix = '/api' const prefix = '/api'
//App Auth, all requests will come in with a token, decode the token and set global var //App Auth, all requests will come in with a token, decode the token and set global var
app.use(function(req, res, next){ app.use(function(req, res, next){
//auth token set by axios in headers
let token = req.headers.authorization let token = req.headers.authorizationtoken
if(token && token != null && typeof token === 'string'){ if(token && token != null && typeof token === 'string'){
Auth.decodeToken(token) Auth.decodeToken(token)
.then(userData => { .then(userData => {
req.headers.userId = userData.id //Update headers for the rest of the application req.headers.userId = userData.id //Update headers for the rest of the application
next() next()
}).catch(error => { }).catch(error => {
@@ -36,17 +100,32 @@ app.use(function(req, res, next){
//Test //Test
app.get(prefix, (req, res) => res.send('The api is running')) app.get(prefix, (req, res) => res.send('The api is running'))
//Init user endpoint //Serve up uploaded files
app.use(prefix+'/static', express.static( __dirname+'/../staticFiles' ))
//Public routes
var public = require('@routes/publicController')
app.use(prefix+'/public', public)
//user endpoint
var user = require('@routes/userController') var user = require('@routes/userController')
app.use(prefix+'/user', user) app.use(prefix+'/user', user)
//Init notes endpoint //notes endpoint
var notes = require('@routes/noteController') var notes = require('@routes/noteController')
app.use(prefix+'/note', notes) app.use(prefix+'/note', notes)
//Init tags endpoint //tags endpoint
var tags = require('@routes/tagController') var tags = require('@routes/tagController')
app.use(prefix+'/tag', tags) app.use(prefix+'/tag', tags)
//notes endpoint
var attachment = require('@routes/attachmentController')
app.use(prefix+'/attachment', attachment)
//quick notes endpoint
var quickNote = require('@routes/quicknoteController')
app.use(prefix+'/quick-note', quickNote)
//Output running status //Output running status
app.listen(port, () => console.log(`Listening on port ${port}!`)) app.listen(port, () => console.log(`Listening on port ${port}!`))

View File

@@ -2,31 +2,230 @@ let db = require('@config/database')
let Attachment = module.exports = {} let Attachment = module.exports = {}
const cheerio = require('cheerio'); const cheerio = require('cheerio')
const rp = require('request-promise'); const rp = require('request-promise')
const request = require('request')
const fs = require('fs')
const gm = require('gm')
const tesseract = require("node-tesseract-ocr")
const filePath = '../staticFiles/'
// Attachment.migrateOld
Attachment.textSearch = (userId, searchTerm) => {
return new Promise((resolve, reject) => {
const front = 5
const tail = 150
const query = `
SELECT
*,
substring(
text,
IF(LOCATE(?, text) > ${tail}, LOCATE(?, text) - ${front}, 1),
${tail} + LENGTH(?) + ${front}
) as snippet
FROM attachment
WHERE user_id = ?
AND MATCH(text)
AGAINST(? IN NATURAL LANGUAGE MODE)
LIMIT 1000`
db.promise()
.query(query, [searchTerm, searchTerm, searchTerm, userId, searchTerm])
.then((rows, fields) => {
resolve(rows[0]) //Return all attachments found by query
})
.catch(console.log)
})
}
Attachment.search = (userId, noteId, attachmentType) => {
return new Promise((resolve, reject) => {
let params = [userId]
let query = 'SELECT * FROM attachment WHERE user_id = ? AND visible = 1 '
if(noteId && noteId > 0){
query += 'AND note_id = ? '
params.push(noteId)
}
if(Number.isInteger(attachmentType)){
query += 'AND attachment_type = ? '
params.push(attachmentType)
}
query += 'ORDER BY last_indexed DESC '
db.promise()
.query(query, params)
.then((rows, fields) => {
resolve(rows[0]) //Return all attachments found by query
})
.catch(console.log)
})
}
//Returns all attachments
Attachment.forNote = (userId, noteId) => { Attachment.forNote = (userId, noteId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise() db.promise()
.query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND attachment_type = 1;`, [userId, noteId]) .query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND visible = 1 ORDER BY last_indexed DESC;`, [userId, noteId])
.then((rows, fields) => { .then((rows, fields) => {
resolve(rows[0]) //Return all tags found by query resolve(rows[0]) //Return all attachments found by query
}) })
.catch(console.log) .catch(console.log)
}) })
} }
Attachment.delete = (attachmentId) => { Attachment.urlForNote = (userId, noteId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise() db.promise()
.query(`DELETE FROM attachment WHERE id = ?`, [attachmentId]) .query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND attachment_type = 1 ORDER BY last_indexed DESC;`, [userId, noteId])
.then((rows, fields) => { .then((rows, fields) => {
resolve(rows[0]) //Return all tags found by query resolve(rows[0]) //Return all attachments found by query
}) })
.catch(console.log) .catch(console.log)
}) })
} }
//Update attachment in database
Attachment.update = (userId, attachmentId, updatedText, noteId) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`UPDATE attachment SET text = ? WHERE id = ? AND user_id = ?`,
[updatedText, attachmentId, userId])
.then((rows, fields) => {
resolve(true)
})
.catch(console.log)
})
}
Attachment.delete = (userId, attachmentId, urlDelete = false) => {
return new Promise((resolve, reject) => {
db.promise()
.query('SELECT * FROM attachment WHERE id = ? AND user_id = ? LIMIT 1', [attachmentId, userId])
.then((rows, fields) => {
//Attachment doesn't exist, return done
if(rows[0].length == 0){
return resolve(true)
}
//Pull data we want out of
let row = rows[0][0]
let url = row.url
const noteId = row.note_id
//Try to delete file and thumbnail
try {
fs.unlinkSync(filePath+row.file_location)
} catch(err) { console.error('File Does not exist') }
try {
fs.unlinkSync(filePath+'thumb_'+row.file_location)
} catch(err) { console.error('Thumbnail Does not exist') }
//Do not delete link attachments, just hide them. They will be deleted if removed from note
if(row.attachment_type == 1 && !urlDelete){
db.promise()
.query(`UPDATE attachment SET visible = 0 WHERE id = ?`, [attachmentId])
.then((rows, fields) => { })
.catch(console.log)
return resolve(true)
}
db.promise()
.query(`DELETE FROM attachment WHERE id = ?`, [attachmentId])
.then((rows, fields) => { })
.catch(console.log)
return resolve(true)
})
})
}
Attachment.processUploadedFile = (userId, noteId, fileObject) => {
return new Promise((resolve, reject) => {
const rawFilename = fileObject.filename
const extension = '.'+fileObject.originalname.split('.').pop()
const goodFileName = rawFilename+extension
const fileName = fileObject.originalname //Actual name of the file, dog.jpg
//Rename random file name to one with an extension
fs.rename(filePath+rawFilename, filePath+goodFileName, (err) => {
const created = Math.round((+new Date)/1000)
db.promise()
.query(`
INSERT INTO attachment
(note_id, user_id, attachment_type, \`text\`, last_indexed, file_location)
VALUES
(?, ?, ?, ?, ?, ?)
`, [noteId, userId, 2, 'Add a description to -> '+fileName, created, goodFileName])
.then((rows, fields) => {
Attachment.generateThumbnail(goodFileName)
//If its an image, scrape text
if(true){
// https://github.com/tesseract-ocr/tesseract/wiki/ImproveQuality
//psm 3 - default, 11 - as much text as possible
const config = { lang: "eng", oem: 1, psm: 3 }
tesseract.recognize(filePath+goodFileName, config)
.then(text => {
text = text.slice(0, -1).trim()
if(text.length > 5){
console.log('Inserting text')
db.promise().query(
`UPDATE attachment SET text = ? WHERE id = ? AND user_id = ? LIMIT 1`,
[text, rows[0].insertId, userId]
).then(results => {
resolve({ fileName, goodFileName })
})
} else {
return resolve({ fileName, goodFileName })
}
})
.catch(error => {
console.log(error.message)
})
} else {
resolve({ fileName, goodFileName })
}
})
.catch(console.log)
})
})
}
Attachment.generateThumbnail = (fileName) => {
return new Promise((resolve, reject) => {
gm(filePath+fileName)
.resize(550) //Resize to width of 550 px
.quality(75) //compression level 0 - 100 (best)
.write(filePath + 'thumb_'+fileName, function (err) {
resolve(fileName)
})
})
}
//Scans text for websites, returns all attachments
Attachment.scanTextForWebsites = (userId, noteId, noteText) => { Attachment.scanTextForWebsites = (userId, noteId, noteText) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -34,16 +233,37 @@ Attachment.scanTextForWebsites = (userId, noteId, noteText) => {
if(noteText.length == 0){ resolve(solrAttachmentText) } if(noteText.length == 0){ resolve(solrAttachmentText) }
Attachment.forNote(userId, noteId).then(attachments => { Attachment.urlForNote(userId, noteId).then(attachments => {
//Find all URLs in text //Find all URLs in text
//@TODO - Use the process text library for this function
const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm
let allUrls = noteText.match(urlPattern) let allUrls = noteText.match(urlPattern)
//Remove all duplicates if(allUrls == null){
let foundUrls = [...new Set(allUrls)] allUrls = []
}
//Go through each attachment, check for existing URLs //Every URL needs HTTPS
let foundUrls = []
allUrls.forEach( (item, index) => {
//Every URL should have HTTPS
if(item.indexOf('https://') == -1 && item.indexOf('http://') == -1){
allUrls[index] = 'https://'+item
}
//URLs should all have HTTPS!!!
if(item.indexOf('http://') >= 0){
allUrls[index] = item.replace('http://','https://')
}
})
//Remove all duplicates
foundUrls = [...new Set(allUrls)]
//Go through each saved URL, remove new URLs from saved URLs
//If a URL is not found, delete it
attachments.forEach(attachment => { attachments.forEach(attachment => {
//URL already scraped, push text and continue //URL already scraped, push text and continue
let urlIndex = foundUrls.indexOf( attachment.url ) let urlIndex = foundUrls.indexOf( attachment.url )
@@ -52,7 +272,8 @@ Attachment.scanTextForWebsites = (userId, noteId, noteText) => {
solrAttachmentText += attachment.text solrAttachmentText += attachment.text
foundUrls.splice(urlIndex, 1) //Remove existing from set of found foundUrls.splice(urlIndex, 1) //Remove existing from set of found
} else { } else {
Attachment.delete(attachment.id) //If existing attachment is not found in note, remove it
Attachment.delete(userId, attachment.id, true)
} }
}) })
@@ -75,11 +296,13 @@ Attachment.scanTextForWebsites = (userId, noteId, noteText) => {
Attachment.scrapeUrlsCreateAttachments = (userId, noteId, foundUrls) => { Attachment.scrapeUrlsCreateAttachments = (userId, noteId, foundUrls) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if(foundUrls == null || foundUrls.length == 0){
return resolve('')
}
console.log('About to scrape') console.log('About to scrape')
console.log(foundUrls) console.log(foundUrls)
if(foundUrls == null || foundUrls.length == 0){resolve('')}
let processedCount = 0 let processedCount = 0
let scrapedText = '' let scrapedText = ''
@@ -99,8 +322,56 @@ Attachment.scrapeUrlsCreateAttachments = (userId, noteId, foundUrls) => {
}) })
} }
Attachment.downloadFileFromUrl = (url) => {
return new Promise((resolve, reject) => {
if(url == null){
resolve(null)
}
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 fileName = random+'_scrape'+extension
const thumbPath = 'thumb_'+fileName
console.log('Scraping image url')
console.log(url)
console.log('Getting ready to scrape ', url)
request(url)
.on('error', error => {
console.log(error)
resolve(null)
})
.on('response', res => {
console.log(res.statusCode)
console.log(res.headers['content-type'])
})
.pipe(fs.createWriteStream(filePath+thumbPath))
.on('close', () => {
//resize image if its real big
gm(filePath+thumbPath)
.resize(550) //Resize to width of 550 px
.quality(75) //compression level 0 - 100 (best)
.write(filePath+thumbPath, function (err) {
if(err){ console.log(err) }
})
console.log('Saved Image')
resolve(fileName)
})
})
}
Attachment.processUrl = (userId, noteId, url) => { Attachment.processUrl = (userId, noteId, url) => {
const scrapeTime = 20*1000;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const excludeWords = ['share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want', const excludeWords = ['share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
@@ -112,12 +383,10 @@ Attachment.processUrl = (userId, noteId, url) => {
var removeWhitespace = /\s+/g var removeWhitespace = /\s+/g
// console.log('Scraping ', website)
const options = { const options = {
uri: url, uri: url,
simple: true, simple: true,
timeout: 1000 * 10, // 10 seconds timeout: scrapeTime,
headers: { headers: {
'User-Agent':'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' //Simulate google headers 'User-Agent':'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' //Simulate google headers
}, },
@@ -127,8 +396,24 @@ Attachment.processUrl = (userId, noteId, url) => {
} }
let requestTimeout = null let requestTimeout = null
let thumbnail = null
let request = null
let created = Math.round((+new Date)/1000)
let insertedId = null
let request = rp(options) //Create a shell attachment for each URL, put in processing state
db.promise()
.query(`INSERT INTO attachment
(note_id, user_id, attachment_type, text, url, last_indexed, file_location)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[noteId, userId, 1, 'Processing...', url, created, null])
.then((rows, fields) => {
//Set two bigger variables then return request for processing
request = rp(options)
insertedId = rows[0].insertId
return request
})
.then($ => { .then($ => {
clearTimeout(requestTimeout) clearTimeout(requestTimeout)
@@ -138,8 +423,15 @@ Attachment.processUrl = (userId, noteId, url) => {
let pageTitle = $('title').text().replace(removeWhitespace, " ") let pageTitle = $('title').text().replace(removeWhitespace, " ")
desiredSearchText += pageTitle + "\n" desiredSearchText += pageTitle + "\n"
let header = $('h1').text().replace(removeWhitespace, " ") // let header = $('h1').text().replace(removeWhitespace, " ")
desiredSearchText += header + "\n" // desiredSearchText += header + "\n"
//Scrape metadata for page image
let metadata = $('meta[property="og:image"]')
if(metadata && metadata[0] && metadata[0].attribs){
thumbnail = metadata[0].attribs.content
}
let majorContent = '' let majorContent = ''
majorContent += $('[class*=content]').text() majorContent += $('[class*=content]').text()
@@ -179,27 +471,52 @@ Attachment.processUrl = (userId, noteId, url) => {
}); });
let finalWords = [] let finalWords = []
for(let i=0; i<15; i++){ for(let i=0; i<5; i++){
if(sortable[i] && sortable[i][0]){ if(sortable[i] && sortable[i][0]){
finalWords.push(sortable[i][0]) finalWords.push(sortable[i][0])
} }
} }
desiredSearchText += finalWords.join(', ') if(finalWords.length > 0){
console.log('TexT Scraped') desiredSearchText += 'Keywords: ' + finalWords.join(', ')
console.log(desiredSearchText) }
const created = Math.round((+new Date)/1000)
//Create attachment in DB with scrape text and provided data // console.log('TexT Scraped')
db.promise() // console.log(desiredSearchText)
.query(`INSERT INTO attachment
(note_id, user_id, attachment_type, text, url, last_indexed) created = Math.round((+new Date)/1000)
VALUES (?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created])
.then((rows, fields) => { //Scrape URL for thumbnail - take filename and save in attachment
resolve(desiredSearchText) //Return found text Attachment.downloadFileFromUrl(thumbnail)
.then(thumbnailFilename => {
//Update text and thumbnail filename
created = Math.round((+new Date)/1000)
db.promise()
.query(`UPDATE attachment SET
text = ?,
last_indexed = ?,
file_location = ?
WHERE id = ?
`, [desiredSearchText, created, thumbnailFilename, 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, file_location)
// VALUES (?, ?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created, thumbnailFilename])
// .then((rows, fields) => {
// resolve(desiredSearchText) //Return found text
// })
// .catch(console.log)
}) })
.catch(console.log)
}) })
.catch(error => { .catch(error => {
@@ -212,19 +529,30 @@ Attachment.processUrl = (userId, noteId, url) => {
console.log('Cancel the request, its taking to long.') console.log('Cancel the request, its taking to long.')
request.cancel() request.cancel()
desiredSearchText = 'Unable to Scrape URL at this time' desiredSearchText = 'No Description for -> '+url
const created = Math.round((+new Date)/1000)
//Create attachment in DB with scrape text and provided data created = Math.round((+new Date)/1000)
db.promise() db.promise()
.query(`INSERT INTO attachment .query(`UPDATE attachment SET
(note_id, user_id, attachment_type, text, url, last_indexed) text = ?,
VALUES (?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created]) last_indexed = ?,
WHERE id = ?
`, [desiredSearchText, created, insertedId])
.then((rows, fields) => { .then((rows, fields) => {
resolve(desiredSearchText) //Return found text resolve(desiredSearchText) //Return found text
}) })
.catch(console.log) .catch(console.log)
}, (5000)) //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 )
}) })
} }

View File

@@ -3,20 +3,101 @@ let db = require('@config/database')
let Tags = require('@models/Tag') let Tags = require('@models/Tag')
let Attachment = require('@models/Attachment') let Attachment = require('@models/Attachment')
let ProcessText = require('@helpers/ProcessText')
const DiffMatchPatch = require('@helpers/DiffMatchPatch')
var rp = require('request-promise'); var rp = require('request-promise');
var SolrNode = require('solr-node'); const fs = require('fs')
let Note = module.exports = {} let Note = module.exports = {}
// Create client const gm = require('gm')
var client = new SolrNode({
host: '127.0.0.1',
port: '8983',
core: 'note',
protocol: 'http'
});
Note.create = (userId, noteText) => { // --------------
Note.migrateNoteTextToNewTable = () => {
return new Promise((resolve, reject) => {
db.promise()
.query('SELECT id, text FROM note WHERE note_raw_text_id IS NULL')
.then((rows, fields) => {
rows[0].forEach( ({id, text}) => {
db.promise()
.query('INSERT INTO note_raw_text (text) VALUES (?)', [text])
.then((rows, fields) => {
db.promise()
.query(`UPDATE note SET note_raw_text_id = ? WHERE (id = ?)`, [rows[0].insertId, id])
.then((rows, fields) => {
return 'Nice'
})
})
})
resolve('Its probably running... :-D')
})
})
}
Note.fixAttachmentThumbnails = () => {
const filePath = '../staticFiles/'
db.promise()
.query(`SELECT * FROM attachment WHERE file_location NOT LIKE "%.%"`)
.then( (rows, fields) => {
rows[0].forEach(line => {
const rawFilename = line['file_location']
const goodFileName = rawFilename+'.jpg'
//Rename file to have jpg extension, create thumbnail, update database
fs.rename(filePath+rawFilename, filePath+goodFileName, (err) => {
db.promise()
.query(`UPDATE attachment SET file_location = ? WHERE id = ?`,[goodFileName, line['id'] ])
.then( (rows, fields) => {
gm(filePath+goodFileName)
.resize(550) //Resize to width of 550 px
.quality(75) //compression level 0 - 100 (best)
.write(filePath + 'thumb_'+goodFileName, function (err) {
console.log('Done for -> ', goodFileName)
})
})
})
})
})
}
Note.stressTest = () => {
return new Promise((resolve, reject) => {
db.promise()
.query(`
SELECT text FROM note;
`)
.then((rows, fields) => {
console.log()
rows[0].forEach(item => {
Note.create(68, item['text'])
})
resolve(true)
})
.catch(console.log)
})
}
// --------------
Note.create = (userId, noteText, quickNote = 0) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if(userId == null || userId < 10){ reject('User Id required to create note') } if(userId == null || userId < 10){ reject('User Id required to create note') }
@@ -24,48 +105,94 @@ Note.create = (userId, noteText) => {
const created = Math.round((+new Date)/1000) const created = Math.round((+new Date)/1000)
db.promise() db.promise()
.query('INSERT INTO note (user_id, text, created) VALUES (?,?,?)', [userId, noteText, created]) .query(`INSERT INTO note_raw_text (text) VALUE (?)`, [noteText])
.then( (rows, fields) => {
const rawTextId = rows[0].insertId
return db.promise()
.query('INSERT INTO note (user_id, note_raw_text_id, updated, created, quick_note) VALUES (?,?,?,?,?)',
[userId, rawTextId, created, created, quickNote])
})
.then((rows, fields) => { .then((rows, fields) => {
// Indexing is done on save
resolve(rows[0].insertId) //Only return the new note ID when creating a new note resolve(rows[0].insertId) //Only return the new note ID when creating a new note
}) })
.catch(console.log) .catch(console.log)
}) })
} }
Note.update = (userId, noteId, noteText, fancyInput, color, pinned, archived) => { Note.reindex = (userId, noteId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Note.get(userId, noteId)
.then(note => {
const noteText = note.text
//
// Update Solr index
//
Tags.string(userId, noteId)
.then(tagString => {
const fullText = ProcessText.removeHtml(noteText) +' '+ tagString
db.promise()
.query(`
INSERT INTO note_text_index (note_id, user_id, text)
VALUES (?,?,?)
ON DUPLICATE KEY UPDATE text = ?
`, [noteId, userId, fullText, fullText])
.then((rows, fields) => {
resolve(true)
})
.catch(console.log)
})
})
})
}
Note.update = (userId, noteId, noteText, color, pinned, archived) => {
return new Promise((resolve, reject) => {
//Prevent note loss if it saves with empty text
if(ProcessText.removeHtml(noteText) == ''){
console.log('Not saving empty note')
resolve(false)
}
const now = Math.round((+new Date)/1000) const now = Math.round((+new Date)/1000)
db.promise() db.promise()
.query('UPDATE note SET text = ?, raw_input = ?, pinned = ?, archived = ?, updated = ?, color = ? WHERE id = ? AND user_id = ? LIMIT 1', .query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
[noteText, fancyInput, pinned, archived, now, color, noteId, userId])
.then((rows, fields) => { .then((rows, fields) => {
//Process note text and attachment data const textId = rows[0][0]['note_raw_text_id']
Attachment.scanTextForWebsites(userId, noteId, noteText).then( attachmentText => {
//
// Update Solr index
//
Tags.string(userId, noteId).then(tagString => {
// JSON Data
var data = {
'id': noteId,//string - ID of note
'user_id': userId,//int
'note_text': noteText,
'note_tag': tagString,
'attachment_text': attachmentText,
};
// Update document to Solr server
client.update(data, function(err, result) {
if (err) { console.log(err); return; }
console.log('Note Solr Update, node/solrid ('+noteId+'):');
console.log(result.responseHeader)
});
}) //Update Note text
}) return db.promise()
.query('UPDATE note_raw_text SET text = ? WHERE id = ?', [noteText, textId])
})
.then( (rows, fields) => {
//Update other note attributes
return db.promise()
.query('UPDATE note SET pinned = ?, archived = ?, updated = ?, color = ? WHERE id = ? AND user_id = ? LIMIT 1',
[pinned, archived, now, color, noteId, userId])
})
.then((rows, fields) => {
//Async solr note reindex
Note.reindex(userId, noteId)
//Async attachment reindex
Attachment.scanTextForWebsites(userId, noteId, noteText)
//Send back updated response //Send back updated response
resolve(rows[0]) resolve(rows[0])
@@ -74,25 +201,156 @@ Note.update = (userId, noteId, noteText, fancyInput, color, pinned, archived) =>
}) })
} }
//
// Delete a note and all its remaining parts
//
Note.delete = (userId, noteId) => { Note.delete = (userId, noteId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise().query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId,userId])
.then((rows, fields) => { //
db.promise().query('DELETE FROM attachment WHERE attachment.note_id = ? AND attachment.user_id = ?', [noteId,userId]) // Delete, note, text, search index and associated tags
.then((rows, fields)=> { // Leave the attachments, they can be deleted on their own
db.promise().query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId]) // Leave Tags, their text is shared
.then((rows, fields)=> {
let rawTextId = null
let noteTextCount = 0
// Lookup the note text ID, we need this to count usages
db.promise()
.query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
.then((rows, fields) => {
//Save the raw text ID
rawTextId = rows[0][0]['note_raw_text_id']
return db.promise()
.query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId])
})
.then((rows, fields) => {
//Save the number of times the note is used
noteTextCount = rows[0][0]['count']
//Don't delete text if its shared
if(noteTextCount == 1){
//If text is only used on one note, delete it (its not shared)
return db.promise()
.query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId])
} else {
return new Promise((resolve, reject) => {
resolve(true) resolve(true)
}) })
}) }
})
.then( results => {
// Delete Note entry for this user.
return db.promise()
.query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId, userId])
})
.then((rows, fields) => {
// Delete search index
return db.promise()
.query('DELETE FROM note_text_index WHERE note_text_index.note_id = ? AND note_text_index.user_id = ?', [noteId,userId])
})
.then((rows, fields) => {
// delete tags
return db.promise()
.query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
})
.then((rows, fields) => {
resolve(true)
}) })
}) })
} }
//text is the current text for the note that will be compared to the text in the database
Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
return new Promise((resolve, reject) => {
Note.get(userId, noteId)
.then(noteObject => {
let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
let newText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
if(noteObject.updated == lastUpdated){
// console.log('No note diff')
resolve(null)
}
if(noteObject.updated > lastUpdated){
newText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
oldText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
}
const dmp = new DiffMatchPatch.diff_match_patch()
const diff = dmp.diff_main(oldText, newText)
dmp.diff_cleanupSemantic(diff)
const patch_list = dmp.patch_make(oldText, newText, diff);
const patch_text = dmp.patch_toText(patch_list);
//Patch text - shows a list of changes
var patches = dmp.patch_fromText(patch_text);
// console.log(patch_text)
//results[1] - contains diagnostic data for patch apply, its possible it can fail
var results = dmp.patch_apply(patches, oldText);
//Compile return data for front end
const returnData = {
updatedText: results[0],
diffs: results[1].length, //Only use length for now
updated: Math.max(noteObject.updated,lastUpdated) //Return most recently updated date
}
//Final change in notes
// console.log(returnData)
resolve(returnData)
})
})
}
Note.get = (userId, noteId) => { Note.get = (userId, noteId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise() db.promise()
.query('SELECT text, updated, raw_input, pinned, archived, color FROM note WHERE user_id = ? AND id = ? LIMIT 1', [userId,noteId]) .query(`
SELECT
note_raw_text.text,
note.updated,
note.pinned,
note.archived,
note.color,
count(distinct attachment.id) as attachment_count,
note.note_raw_text_id as rawTextId,
user.username as shareUsername
FROM note
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 user ON (note.share_user_id = user.id)
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId,noteId])
.then((rows, fields) => {
const created = Math.round((+new Date)/1000)
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [created, noteId])
//Return note data
resolve(rows[0][0])
})
.catch(console.log)
})
}
//Public note share action -> may not be used
Note.getShared = (noteId) => {
return new Promise((resolve, reject) => {
db.promise()
.query('SELECT text, updated, color FROM note WHERE id = ? AND shared = 1 LIMIT 1', [noteId])
.then((rows, fields) => { .then((rows, fields) => {
//Return note data //Return note data
@@ -103,94 +361,132 @@ Note.get = (userId, noteId) => {
}) })
} }
// Searches text index, returns nothing if there is no search query
Note.solrQuery = (userId, searchQuery, searchTags) => { Note.solrQuery = (userId, searchQuery, searchTags) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if(searchQuery != '' && searchQuery != null){ if(searchQuery.length == 0){
let urlQuery = `/solr/note/select?hl.fl=note_text&hl=on&q=user_id:${userId} AND note_text:${searchQuery}&wt=json` resolve(null)
urlQuery = `/solr/note/select?
hl.fl=note_text,attachment_text,note_tag&
hl=on&
q=user_id:${userId} AND (note_text:${searchQuery} OR attachment_text:${searchQuery} OR note_tag:${searchQuery})&
wt=json&
fl=id&
hl.fl=note_text,attachment_text,note_tag&
hl.snippets=20&
hl.maxAnalyzedChars=100000`
rp('http://127.0.0.1:8983'+urlQuery)
.then(function (htmlString) {
let solrResult = JSON.parse(htmlString)
resolve(solrResult)
})
} else { } else {
resolve([])
//Number of characters before and after search word
const front = 5
const tail = 150
db.promise()
.query(`
SELECT
note_id,
substring(
text,
IF(LOCATE(?, text) > ${tail}, LOCATE(?, text) - ${front}, 1),
${tail} + LENGTH(?) + ${front}
) as snippet
FROM note_text_index
WHERE user_id = ?
AND MATCH(text)
AGAINST(? IN NATURAL LANGUAGE MODE)
LIMIT 1000
;
`, [searchQuery, searchQuery, searchQuery, userId, searchQuery])
.then((rows, fields) => {
let results = []
let snippets = {}
rows[0].forEach(item => {
let noteId = parseInt(item['note_id'])
//Setup array of ids to use for query
results.push( noteId )
//Get text snippet and highlight the key word
snippets[noteId] = item['snippet'].replace(new RegExp(searchQuery,"ig"), '<em>'+searchQuery+'</em>');
//.replace(searchQuery,'<em>'+searchQuery+'</em>')
})
resolve({ 'ids':results, 'snippets':snippets })
})
} }
}) })
} }
Note.search = (userId, searchQuery, searchTags, fastFilters) => { Note.search = (userId, searchQuery, searchTags, fastFilters) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
//Define return data objects //Define return data objects
let returnData = { let returnData = {
'notes':[], 'notes':[],
'tags':[] 'tags':[]
} }
Note.solrQuery(userId, searchQuery, searchTags).then( solrResult => { Note.solrQuery(userId, searchQuery, searchTags).then( (textSearchResults) => {
let highlights = solrResult.highlighting //Pull out search results from previous query
let textSearchIds = []
let highlights = {}
let returnTagResults = false
//Parse Note ID's from solr search if(textSearchResults != null){
let solrNoteIds = [] textSearchIds = textSearchResults['ids']
if(solrResult.response){ highlights = textSearchResults['snippets']
solrResult.response.docs.forEach(item => {
solrNoteIds.push(parseInt(item.id))
})
}
//No results, return empty data
if(solrNoteIds.length == 0 && searchQuery.length > 0){
resolve(returnData)
} }
//Default note lookup gets all notes //No results, return empty data
if(textSearchIds.length == 0 && searchQuery.length > 0){
return resolve(returnData)
}
// Base of the query, modified with fastFilters
// Add to query for character counts -> CHAR_LENGTH(note.text) as chars
let searchParams = [userId]
let noteSearchQuery = ` let noteSearchQuery = `
SELECT note.id, SELECT note.id,
SUBSTRING(note.text, 1, 400) as text, SUBSTRING(note_raw_text.text, 1, 1500) as text,
updated, color, updated,
color,
count(distinct note_tag.id) as tag_count, count(distinct note_tag.id) as tag_count,
count(distinct attachment.id) as attachment_count, count(distinct attachment.id) as attachment_count,
note.pinned, note.pinned,
note.archived note.archived,
GROUP_CONCAT(DISTINCT tag.text) as tags,
GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs,
shareUser.username as username
FROM note FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
LEFT JOIN note_tag ON (note.id = note_tag.note_id) LEFT JOIN note_tag ON (note.id = note_tag.note_id)
LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.attachment_type = 1) LEFT JOIN tag ON (tag.id = note_tag.tag_id)
WHERE note.user_id = ?` LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.visible = 1)
let searchParams = [userId] LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
WHERE note.user_id = ?
`
if(solrNoteIds.length > 0){ //Show shared notes
searchParams.push(solrNoteIds) if(fastFilters.onlyShowSharedNotes == 1){
noteSearchQuery += ' AND note.share_user_id IS NOT NULL' //Show Archived
} else {
noteSearchQuery += ' AND note.share_user_id IS NULL'
}
//If text search returned results, limit search to those ids
if(textSearchIds.length > 0){
searchParams.push(textSearchIds)
noteSearchQuery += ' AND note.id IN (?)' noteSearchQuery += ' AND note.id IN (?)'
} }
// if(searchQuery != ''){ if(fastFilters.noteIdSet && fastFilters.noteIdSet.length > 0){
// //If a search query is defined, search notes for that word searchParams.push(fastFilters.noteIdSet)
// searchParams.push('%'+searchQuery+'%') noteSearchQuery += ' AND note.id IN (?)'
// noteSearchQuery += ' AND text LIKE ?' }
// }
//If tags are passed, use those tags in search
if(searchTags.length > 0){ if(searchTags.length > 0){
//If tags are passed, use those tags in search
searchParams.push(searchTags) searchParams.push(searchTags)
noteSearchQuery += ' AND note_tag.tag_id IN (?)' noteSearchQuery += ' AND note_tag.tag_id IN (?)'
} }
//Toggle archived, show archived if tags are searched //Show archived notes, only if fast filter is set, default to not archived
// - archived will show archived in search results if(fastFilters.onlyArchived == 1){
// - onlyArchive will exclude notes that are not archived noteSearchQuery += ' AND note.archived = 1' //Show Archived
if(fastFilters.archived == 1 || searchTags.length > 0 || fastFilters.onlyArchived == 1){
//Do nothing
} else { } else {
noteSearchQuery += ' AND note.archived = 0' //Exclude archived noteSearchQuery += ' AND note.archived = 0' //Exclude archived
} }
@@ -200,32 +496,54 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
//Only show notes with Tags //Only show notes with Tags
if(fastFilters.withTags == 1){ if(fastFilters.withTags == 1){
returnTagResults = true
noteSearchQuery += ' HAVING tag_count > 0' noteSearchQuery += ' HAVING tag_count > 0'
} }
//Only show notes with links //Only show notes with links
if(fastFilters.withLinks == 1){ if(fastFilters.withLinks == 1){
returnTagResults = true
noteSearchQuery += ' HAVING attachment_count > 0' noteSearchQuery += ' HAVING attachment_count > 0'
} }
//Only show archived notes //Only show archived notes
if(fastFilters.onlyArchived == 1){ if(fastFilters.onlyArchived == 1){
returnTagResults = true
noteSearchQuery += ' HAVING note.archived = 1' noteSearchQuery += ' HAVING note.archived = 1'
} }
//
// Always prioritize pinned notes in searches.
//Default Sort, order by last updated //Default Sort, order by last updated
let defaultOrderBy = ' ORDER BY note.pinned DESC, updated DESC, created DESC, opened DESC, id DESC' let defaultOrderBy = ' ORDER BY note.pinned DESC, note.updated DESC, note.created DESC, note.opened DESC, id DESC'
//Order by Last Created Date //Order by Last Created Date
if(fastFilters.lastCreated == 1){ if(fastFilters.lastCreated == 1){
defaultOrderBy = ' ORDER BY note.pinned DESC, created DESC, updated DESC, opened DESC, id DESC' defaultOrderBy = ' ORDER BY note.pinned DESC, note.created DESC, note.updated DESC, note.opened DESC, id DESC'
} }
//Order by last Opened Date //Order by last Opened Date
if(fastFilters.lastOpened == 1){ if(fastFilters.lastOpened == 1){
defaultOrderBy = ' ORDER BY note.pinned DESC, opened DESC, updated DESC, created DESC, id DESC' defaultOrderBy = ' ORDER BY note.pinned DESC, opened DESC, note.updated DESC, note.created DESC, id DESC'
} }
//Append Order by to query //Append Order by to query
noteSearchQuery += defaultOrderBy noteSearchQuery += defaultOrderBy
//Manage limit params if set
if(fastFilters.limitSize > 0 || fastFilters.limitOffset > 0){
const limitSize = parseInt(fastFilters.limitSize, 10) || 10 //Use int or default to 10
const limitOffset = parseInt(fastFilters.limitOffset, 10) || 0 //Either parse int, or use zero
// console.log(` LIMIT ${limitOffset}, ${limitSize}`)
noteSearchQuery += ` LIMIT ${limitOffset}, ${limitSize}`
}
// console.log('------------- Final Query --------------')
// console.log(noteSearchQuery)
// console.log('------------- ----------- --------------')
db.promise() db.promise()
.query(noteSearchQuery, searchParams) .query(noteSearchQuery, searchParams)
.then((noteRows, noteFields) => { .then((noteRows, noteFields) => {
@@ -240,54 +558,42 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
//Grab note ID for finding tags //Grab note ID for finding tags
noteIds.push(note.id) noteIds.push(note.id)
//Attempt to pull string out of first tag in note if(note.text == null){ note.text = '' }
let reg = note.text.match(/<([\w]+)[^>]*>(.*?)<\/\1>/g)
//Pull out first html tag contents, that is the title //Deduce note title
if(reg != null && reg[0]){ const textData = ProcessText.deduceNoteTitle(note.text)
note.title = reg[0] //First line from HTML // console.log(textData)
} else {
note.title = note.text //Entire note
}
//Clean up html title // console.log(textData)
note.title = note.title
.replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities
.replace(/<[^>]+>/g, '') //Rip out all HTML tags
//Generate Subtext
note.subtext = ''
if(note.text != '' && note.title != ''){
note.subtext = note.text
.replace(/&[#A-Za-z0-9]+;/g,' ') //Rip out all HTML entities
.replace(/<[^>]+>/g, ' ') //Rip out all HTML tags
.replace(/\s+/g, ' ') //Remove all whitespace
.substring(note.title.length + 2)
}
note.title = textData.title
note.subtext = textData.sub
note.titleLength = textData.titleLength
note.subtextLength = textData.subtextLength
note.note_highlights = [] note.note_highlights = []
note.attachment_highlights = [] note.attachment_highlights = []
note.tag_highlights = [] note.tag_highlights = []
//Push in solr highlights //Push in search highlights
if(highlights && highlights[note.id] && highlights[note.id].note_text){ if(highlights && highlights[note.id]){
note['note_highlights'] = highlights[note.id].note_text note['note_highlights'] = [highlights[note.id]]
}
if(highlights && highlights[note.id] && highlights[note.id].attachment_text){
note['attachment_highlights'] = highlights[note.id].attachment_text
}
if(highlights && highlights[note.id] && highlights[note.id].note_tag){
note['tag_highlights'] = highlights[note.id].note_tag
} }
//Clear out note.text before sending it to front end //Clear out note.text before sending it to front end, its being used in title and subtext
delete note.text delete note.text
}) })
//If no notes are returned, there are no tags, return empty //If no notes are returned, there are no tags, return empty
if(noteIds.length == 0){ if(noteIds.length == 0){
resolve(returnData) return resolve(returnData)
}
//Return all notes, tags are not being searched
// if tags are being searched, continue
// if notes are being filtered, return tags
if(searchTags.length == 0 && returnTagResults == false){
return resolve(returnData)
} }
//Only show tags of selected notes //Only show tags of selected notes

101
server/models/QuickNote.js Normal file
View File

@@ -0,0 +1,101 @@
let db = require('@config/database')
let Note = require('@models/Note')
let QuickNote = module.exports = {}
QuickNote.get = (userId) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`
SELECT note.id, text FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE quick_note = 1 AND user_id = ? LIMIT 1
`, [userId])
.then((rows, fields) => {
//Quick Note is set, return note text
if(rows[0].length == 1){
resolve({
id: rows[0][0].id,
text: rows[0][0].text
})
}
resolve({
id: null,
text: 'Enter something to create a quick note.'
})
})
.catch(console.log)
})
}
QuickNote.update = (userId, pushText) => {
return new Promise((resolve, reject) => {
//Process pushText, split at \n (new lines), put <p> tags around each new line
let broken = '<blockquote>' +
pushText.split(/\r?\n/).map( (line, index) => {
let clean = line
.replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities
.replace(/<[^>]+>/g, '') //Rip out all HTML tags
if(clean == ''){ clean = '&nbsp;' }
let newLine = ''
if(index > 0){ newLine = '<br>' }
//Return line wrapped in p tags
return `${newLine}<span>${clean}</span>`
}).join('') + '</blockquote>'
db.promise()
.query(`
SELECT note.id, text, color, pinned, archived
FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE quick_note = 1 AND user_id = ? LIMIT 1
`, [userId])
.then((rows, fields) => {
//Quick Note is set, push it the front of existing note
if(rows[0].length == 1){
let d = rows[0][0] //Get row data
//Push old text behind fresh new text
let newText = broken +''+ d.text
//Save that, then return the new text
Note.update(userId, d.id, newText, d.color, d.pinned, d.archived)
.then( saveResults => {
resolve({
id:d.id,
text:newText
})
})
} else {
//Create a new note with the quick text submitted.
Note.create(userId, broken, 1)
.then( insertId => {
resolve({
id:insertId,
text:broken
})
})
}
})
.catch(console.log)
})
//Lookup quick note,
//Note.create(userId, 'Quick Note', 1)
}

127
server/models/ShareNote.js Normal file
View File

@@ -0,0 +1,127 @@
//
// All actions in noteController.js
//
const db = require('@config/database')
const Note = require('@models/Note')
let ShareNote = module.exports = {}
// Share a note with a user, given the correct username
ShareNote.addUser = (userId, noteId, rawTextId, username) => {
return new Promise((resolve, reject) => {
let shareUserId = null
let newNoteShare = null
//Check that user actually exists
db.promise().query(`SELECT id FROM user WHERE username = ?`, [username])
.then((rows, fields) => {
if(rows[0].length == 0){
throw new Error('User Does Not Exist')
}
shareUserId = rows[0][0]['id']
//Check if note has already been added for user
return db.promise()
.query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, rawTextId])
})
.then((rows, fields) => {
if(rows[0].length != 0){
throw new Error('User Already Has Note')
}
//Lookup note to share with user, clone this data to create users new note
return db.promise()
.query(`SELECT * FROM note WHERE id = ? LIMIT 1`, [noteId])
})
.then((rows, fields) => {
newNoteShare = rows[0][0]
//Modify note with the share attributes we want
delete newNoteShare['id']
delete newNoteShare['opened']
newNoteShare['share_user_id'] = userId //User who shared the note
newNoteShare['user_id'] = shareUserId //User who gets note
//Setup db colums, db values and number of '?' to put into prepared statement
let dbColumns = []
let dbValues = []
let escapeChars = []
//Pull out all the data we need from object to create prepared statemnt
Object.keys(newNoteShare).forEach( key => {
escapeChars.push('?')
dbColumns.push(key)
dbValues.push(newNoteShare[key])
})
//Stick all the note value back into query, insert updated note
return db.promise()
.query(`INSERT INTO note (${dbColumns.join()}) VALUES (${escapeChars.join()})`, dbValues)
})
.then((rows, fields) => {
//Success!
return resolve(true)
})
.catch(error => {
// console.log(error)
resolve(false)
})
})
}
// Get users who see a shared note
ShareNote.getUsers = (userId, rawTextId) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`
SELECT username, note.id as noteId
FROM note
JOIN user ON (user.id = note.user_id)
WHERE note_raw_text_id = ?
AND share_user_id = ?
AND user_id != ?
`, [rawTextId, userId, userId])
.then((rows, fields) => {
//Return a list of user names
return resolve (rows[0])
})
})
}
// Remove a user from a shared note
ShareNote.removeUser = (userId, noteId) => {
return new Promise((resolve, reject) => {
//note.id = noteId, share_user_id = userId
db.promise()
.query('SELECT user_id FROM note WHERE id = ? AND share_user_id = ?', [noteId, userId])
.then( (rows, fields) => {
//User has shared this note, with this user
if(rows[0].length == 1 && Number.isInteger(rows[0][0]['user_id'])){
Note.delete(rows[0][0]['user_id'], noteId)
.then( result => {
resolve(result)
})
} else {
return resolve(false)
}
})
})
}

View File

@@ -2,6 +2,21 @@ let db = require('@config/database')
let Tag = module.exports = {} let Tag = module.exports = {}
Tag.userTags = (userId) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`
SELECT tag.id, text, COUNT(note_tag.note_id) as usages FROM tag
JOIN note_tag ON tag.id = note_tag.tag_id
WHERE note_tag.user_id = ?
GROUP BY tag.id
ORDER BY id DESC
`, [userId])
.then( (rows, fields) => {
resolve(rows[0])
})
})
}
Tag.removeTagFromNote = (userId, tagId) => { Tag.removeTagFromNote = (userId, tagId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -83,39 +98,50 @@ Tag.add = (tagText) => {
}) })
} }
//
// Get all tags AND tags associated to note
//
Tag.get = (userId, noteId) => { Tag.get = (userId, noteId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
//Update last opened date of note Tag.userTags(userId).then(userTags => {
const now = Math.round((+new Date)/1000) db.promise()
db.promise() .query(`SELECT tag_id as tagId, id as entryId
.query('UPDATE note SET opened = ? WHERE id = ? AND user_id = ? LIMIT 1', [now, noteId, userId]) FROM note_tag
.then((rows, fields) => {}) WHERE user_id = ? AND note_id = ?;`, [userId, noteId])
.then((rows, fields) => {
db.promise() //pull IDs out of returned results
.query(`SELECT note_tag.id, tag.text FROM note_tag // let ids = rows[0].map( item => {})
JOIN tag ON (tag.id = note_tag.tag_id)
WHERE user_id = ? AND note_id = ?;`, [userId, noteId]) resolve({'noteTagIds':rows[0], 'allTags':userTags }) //Return all tags found by query
.then((rows, fields) => { })
resolve(rows[0]) //Return all tags found by query .catch(console.log)
}) })
.catch(console.log)
}) })
} }
//
// Get all tags for a note and concatinate into a string 'all, tags, like, this'
//
Tag.string = (userId, noteId) => { Tag.string = (userId, noteId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Tag.get(userId, noteId).then(tagArray => { db.promise()
.query(`SELECT GROUP_CONCAT(DISTINCT tag.text SEPARATOR ', ') as text
FROM note_tag
JOIN tag ON note_tag.tag_id = tag.id
WHERE user_id = ? AND note_id = ?;`, [userId, noteId])
.then((rows, fields) => {
let tagString = '' let finalText = rows[0][0]['text']
tagArray.forEach( (tag, i) => { if(finalText == null){
if(i > 0){ tagString += ',' } finalText = ''
tagString += tag.text }
})
//Output comma delimited list of tag strings
resolve(tagString)
return resolve(finalText) //Return all tags found by query
}) })
.catch(console.log)
}) })
} }

View File

@@ -113,3 +113,56 @@ User.create = (username, password) => {
}) })
} }
//Counts notes, pinned notes, archived notes, shared notes, unread notes, total files and types
User.getCounts = (userId) => {
return new Promise((resolve, reject) => {
let countTotals = {}
db.promise().query(
`SELECT
SUM(pinned = 1 && archived = 0 && share_user_id IS NULL) AS pinnedNotes,
SUM(pinned = 0 && archived = 1 && share_user_id IS NULL) AS archivedNotes,
SUM(share_user_id IS NULL) AS totalNotes,
SUM(share_user_id != ?) AS sharedToNotes,
SUM( (share_user_id != ? && opened IS null) || (share_user_id != ? && updated > opened) ) AS unreadNotes
FROM note
WHERE user_id = ?`, [userId, userId, userId, userId])
.then( (rows, fields) => {
Object.assign(countTotals, rows[0][0]) //combine results
return db.promise().query(
`SELECT count(id) AS sharedFromNotes
FROM note WHERE share_user_id = ?`, [userId]
)
})
.then( (rows, fields) => {
Object.assign(countTotals, rows[0][0]) //combine results
return db.promise().query(
`SELECT
SUM(attachment_type = 1) as linkFiles,
SUM(attachment_type != 1) as otherFiles,
COUNT(id) as totalFiles
FROM attachment WHERE visible = 1
AND user_id = ?
`, [userId]
)
}).then( (rows, fields) => {
Object.assign(countTotals, rows[0][0]) //combine results
//Convert everything to an int or 0
Object.keys(countTotals).forEach( key => {
const count = parseInt(countTotals[key])
countTotals[key] = count ? count : 0
})
resolve(countTotals)
})
})
}

View File

@@ -0,0 +1,63 @@
let express = require('express')
var multer = require('multer')
var upload = multer({ dest: '../staticFiles/' }) //@TODO make this a global value
let router = express.Router()
let Attachment = require('@models/Attachment');
let Note = require('@models/Note')
let userId = null
// middleware that is specific to this router
router.use(function setUserId (req, res, next) {
if(userId = req.headers.userId){
userId = req.headers.userId
}
next()
})
router.post('/search', function (req, res) {
Attachment.search(userId, req.body.noteId, req.body.attachmentType)
.then( data => res.send(data) )
})
router.post('/textsearch', function (req, res) {
Attachment.textSearch(userId, req.body.searchTerm)
.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) {
Attachment.update(userId, req.body.attachmentId, req.body.updatedText, req.body.noteId)
.then( result => {
Note.reindex(userId, req.body.noteId)
.then( data => res.send(data) )
})
})
router.post('/delete', function (req, res) {
Attachment.delete(userId, req.body.attachmentId)
.then( data => res.send(data) )
})
router.post('/upload', upload.single('file'), function (req, res, next) {
//Create attachment with file information and node id
const noteId = parseInt(req.body.noteId)
Attachment.processUploadedFile(userId, noteId, req.file)
.then( uploadResults => {
//Reindex note, attachment may have had text
Note.reindex(userId, noteId)
.then( data => res.send(uploadResults) )
})
})
module.exports = router

View File

@@ -2,20 +2,34 @@ var express = require('express')
var router = express.Router() var router = express.Router()
let Notes = require('@models/Note'); let Notes = require('@models/Note');
let ShareNote = require('@models/ShareNote');
let userId = null let userId = null
let socket = null
// middleware that is specific to this router // middleware that is specific to this router
router.use(function setUserId (req, res, next) { router.use(function setUserId (req, res, next) {
if(userId = req.headers.userId){ if(req.headers.userId){
userId = req.headers.userId userId = req.headers.userId
} }
if(req.headers.socket){
// socket = req.
}
next() next()
}) })
//
// Note actions
//
router.post('/get', function (req, res) { router.post('/get', function (req, res) {
req.io.emit('welcome_homie', 'Welcome, dont poop from excitement')
Notes.get(userId, req.body.noteId) Notes.get(userId, req.body.noteId)
.then( data => res.send(data) ) .then( data => {
//Join room when user opens note
// req.io.join('note_room')
res.send(data)
})
}) })
router.post('/delete', function (req, res) { router.post('/delete', function (req, res) {
@@ -29,16 +43,56 @@ router.post('/create', function (req, res) {
}) })
router.post('/update', function (req, res) { router.post('/update', function (req, res) {
Notes.update(userId, req.body.noteId, req.body.text, req.body.fancyInput, req.body.color, req.body.pinned, req.body.archived) Notes.update(userId, req.body.noteId, req.body.text, req.body.color, req.body.pinned, req.body.archived)
.then( id => res.send({id}) ) .then( id => res.send({id}) )
}) })
router.post('/search', function (req, res) { router.post('/search', function (req, res) {
Notes.search(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters) Notes.search(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters)
.then( notesAndTags => res.send(notesAndTags)) .then( notesAndTags => {
res.send(notesAndTags)
})
})
router.post('/difftext', function (req, res) {
Notes.getDiffText(userId, req.body.noteId, req.body.text, req.body.updated)
.then( fullDiffText => {
//Response should be full diff text
res.send(fullDiffText)
})
})
//
// Share Note Actions
//
router.post('/getshareusers', function (req, res) {
ShareNote.getUsers(userId, req.body.rawTextId)
.then(results => res.send(results))
})
router.post('/shareadduser', function (req, res) {
ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username)
.then(results => res.send(results))
})
router.post('/shareremoveuser', function (req, res) {
ShareNote.removeUser(userId, req.body.noteId)
.then(results => res.send(results))
})
//
// Testing Action
//
//Reindex all notes. Not a very good function, not public
router.get('/reindex5yu43prchuj903mrc', function (req, res) {
Notes.migrateNoteTextToNewTable().then(status => {
return res.send(status)
})
}) })
module.exports = router module.exports = router

View File

@@ -0,0 +1,15 @@
var express = require('express')
var router = express.Router()
let Notes = require('@models/Note')
router.post('/note', function (req, res) {
Notes.getShared(req.body.noteId)
.then( data => res.send(data) )
})
module.exports = router

View File

@@ -0,0 +1,35 @@
var express = require('express')
var router = express.Router()
let QuickNote = require('@models/QuickNote');
let userId = null
// middleware that is specific to this router
router.use(function setUserId (req, res, next) {
if(userId = req.headers.userId){
userId = req.headers.userId
}
next()
})
//Get quick note text
router.post('/get', function (req, res) {
QuickNote.get(userId)
.then( data => res.send(data) )
})
//Push text to quick note
router.post('/update', function (req, res) {
QuickNote.update(userId, req.body.pushText)
.then( data => res.send(data) )
})
//Change quick note to a new note
router.post('/change', function (req, res) {
QuickNote.change(userId, req.body.noteId)
.then( data => res.send(data) )
})
module.exports = router

View File

@@ -42,4 +42,10 @@ router.post('/get', function (req, res) {
.then( data => res.send(data) ) .then( data => res.send(data) )
}) })
//Get all the tags for this user in order of usage
router.post('/usertags', function (req, res) {
Tags.userTags(userId)
.then( data => res.send(data) )
})
module.exports = router module.exports = router

View File

@@ -46,4 +46,11 @@ router.post('/login', function (req, res) {
}) })
}) })
// fetch counts of users notes
router.post('/totals', function (req, res) {
User.getCounts(req.headers.userId)
.then( countsObject => res.send( countsObject ))
})
module.exports = router module.exports = router

View File

@@ -2,9 +2,9 @@
echo 'Make sure this is being run from root folder of project' echo 'Make sure this is being run from root folder of project'
echo 'Starting API server (/api), watching for file changes...'
pm2 start server/index.js --watch
echo 'Starting Client webpack dev server (/app), in a screen, watching for file changes...' echo 'Starting Client webpack dev server (/app), in a screen, watching for file changes...'
screen -dm bash -c "cd client/; npm run watch" screen -dm bash -c "cd client/; npm run watch"
echo 'Starting API server (/api), watching for file changes...'
cd server
pm2 start ecosystem.config.js

4
staticFiles/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*
*/
!.gitignore
!assets

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1 @@
<svg id="bac3cfc7-b61b-48ce-8441-8100e40ddaa6" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="797.5" height="834.5" viewBox="0 0 797.5 834.5"><title>void</title><ellipse cx="308.5" cy="780" rx="308.5" ry="54.5" fill="#3f3d56"/><circle cx="496" cy="301.5" r="301.5" fill="#3f3d56"/><circle cx="496" cy="301.5" r="248.89787" opacity="0.05"/><circle cx="496" cy="301.5" r="203.99362" opacity="0.05"/><circle cx="496" cy="301.5" r="146.25957" opacity="0.05"/><path d="M398.42029,361.23224s-23.70394,66.72221-13.16886,90.42615,27.21564,46.52995,27.21564,46.52995S406.3216,365.62186,398.42029,361.23224Z" transform="translate(-201.25 -32.75)" fill="#d0cde1"/><path d="M398.42029,361.23224s-23.70394,66.72221-13.16886,90.42615,27.21564,46.52995,27.21564,46.52995S406.3216,365.62186,398.42029,361.23224Z" transform="translate(-201.25 -32.75)" opacity="0.1"/><path d="M415.10084,515.74682s-1.75585,16.68055-2.63377,17.55847.87792,2.63377,0,5.26754-1.75585,6.14547,0,7.02339-9.65716,78.13521-9.65716,78.13521-28.09356,36.8728-16.68055,94.81576l3.51169,58.82089s27.21564,1.75585,27.21564-7.90132c0,0-1.75585-11.413-1.75585-16.68055s4.38962-5.26754,1.75585-7.90131-2.63377-4.38962-2.63377-4.38962,4.38961-3.51169,3.51169-4.38962,7.90131-63.2105,7.90131-63.2105,9.65716-9.65716,9.65716-14.92471v-5.26754s4.38962-11.413,4.38962-12.29093,23.70394-54.43127,23.70394-54.43127l9.65716,38.62864,10.53509,55.3092s5.26754,50.04165,15.80262,69.356c0,0,18.4364,63.21051,18.4364,61.45466s30.72733-6.14547,29.84941-14.04678-18.4364-118.5197-18.4364-118.5197L533.62054,513.991Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><path d="M391.3969,772.97846s-23.70394,46.53-7.90131,48.2858,21.94809,1.75585,28.97148-5.26754c3.83968-3.83968,11.61528-8.99134,17.87566-12.87285a23.117,23.117,0,0,0,10.96893-21.98175c-.463-4.29531-2.06792-7.83444-6.01858-8.16366-10.53508-.87792-22.826-10.53508-22.826-10.53508Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><path d="M522.20753,807.21748s-23.70394,46.53-7.90131,48.28581,21.94809,1.75584,28.97148-5.26754c3.83968-3.83969,11.61528-8.99134,17.87566-12.87285a23.117,23.117,0,0,0,10.96893-21.98175c-.463-4.29531-2.06792-7.83444-6.01857-8.16367-10.53509-.87792-22.826-10.53508-22.826-10.53508Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><circle cx="295.90488" cy="215.43252" r="36.90462" fill="#ffb8b8"/><path d="M473.43048,260.30832S447.07,308.81154,444.9612,308.81154,492.41,324.62781,492.41,324.62781s13.70743-46.39439,15.81626-50.61206Z" transform="translate(-201.25 -32.75)" fill="#ffb8b8"/><path d="M513.86726,313.3854s-52.67543-28.97148-57.943-28.09356-61.45466,50.04166-60.57673,70.2339,7.90131,53.55335,7.90131,53.55335,2.63377,93.05991,7.90131,93.93783-.87792,16.68055.87793,16.68055,122.90931,0,123.78724-2.63377S513.86726,313.3854,513.86726,313.3854Z" transform="translate(-201.25 -32.75)" fill="#d0cde1"/><path d="M543.2777,521.89228s16.68055,50.91958,2.63377,49.16373-20.19224-43.89619-20.19224-43.89619Z" transform="translate(-201.25 -32.75)" fill="#ffb8b8"/><path d="M498.50359,310.31267s-32.48318,7.02339-27.21563,50.91957,14.9247,87.79237,14.9247,87.79237l32.48318,71.11182,3.51169,13.16886,23.70394-6.14547L528.353,425.32067s-6.14547-108.86253-14.04678-112.37423A33.99966,33.99966,0,0,0,498.50359,310.31267Z" transform="translate(-201.25 -32.75)" fill="#d0cde1"/><polygon points="277.5 414.958 317.885 486.947 283.86 411.09 277.5 414.958" opacity="0.1"/><path d="M533.896,237.31585l.122-2.82012,5.6101,1.39632a6.26971,6.26971,0,0,0-2.5138-4.61513l5.97581-.33413a64.47667,64.47667,0,0,0-43.1245-26.65136c-12.92583-1.87346-27.31837.83756-36.182,10.43045-4.29926,4.653-7.00067,10.57018-8.92232,16.60685-3.53926,11.11821-4.26038,24.3719,3.11964,33.40938,7.5006,9.18513,20.602,10.98439,32.40592,12.12114,4.15328.4,8.50581.77216,12.35457-.83928a29.721,29.721,0,0,0-1.6539-13.03688,8.68665,8.68665,0,0,1-.87879-4.15246c.5247-3.51164,5.20884-4.39635,8.72762-3.9219s7.74984,1.20031,10.062-1.49432c1.59261-1.85609,1.49867-4.559,1.70967-6.99575C521.28248,239.785,533.83587,238.70653,533.896,237.31585Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><circle cx="559" cy="744.5" r="43" fill="#21ba45"/><circle cx="54" cy="729.5" r="43" fill="#21ba45"/><circle cx="54" cy="672.5" r="31" fill="#21ba45"/><circle cx="54" cy="624.5" r="22" fill="#21ba45"/></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB