* Adjusted theme colors to add more contrast on white theme while making black more OLED friendly
* Links now get an underline on hover * Cleaned up CSS variable names, added another theme color for more control * Cleaned up unused CSS, removed scrollbars popping up, tons of other little UI tweaks * Renamed shared notes to inbox * Tweaked form display, seperated login and create accouts * Put login/sign up form on home page * Created more legitimate marketing for home page * Tons up updates to note page and note input panel * Better support for two users editing a note * MUCH better diff handling, web sockets restore notes with unsaved diffs * Moved all squire text modifier functions into a mixin class * It now says saving when closing a note * Lots of cleanup and better handiling of events on mount and destroy * Scroll behavior modified to load notes when closer to bottom of page * Pretty decent shared notes and sharable link support * Updated help text * Search now includes tag suggestions and attachment suggestions * Cleaned up scratch pad a ton, allow for users to create new scratch pads * Created a 404 Page and a Shared note page * So many other small improvements. Oh my god, what is wrong with me, not doing commits!?
This commit is contained in:
parent
09cccf1983
commit
d349fb8328
@ -3,6 +3,11 @@ import Vue from 'vue'
|
|||||||
const helpers = {}
|
const helpers = {}
|
||||||
|
|
||||||
helpers.timeAgo = (time) => {
|
helpers.timeAgo = (time) => {
|
||||||
|
|
||||||
|
if(time.toString().length >= 13){
|
||||||
|
time = Math.round(time/1000)
|
||||||
|
}
|
||||||
|
|
||||||
const time_formats = [
|
const time_formats = [
|
||||||
[ 60, 'seconds', 1 ],
|
[ 60, 'seconds', 1 ],
|
||||||
[ 120, '1 minute ago', '1 minute from now' ],
|
[ 120, '1 minute ago', '1 minute from now' ],
|
||||||
|
@ -17,10 +17,11 @@
|
|||||||
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background_color: #fff;
|
--body_bg_color: #f5f6f7;
|
||||||
|
--small_element_bg_color: #fff;
|
||||||
--text_color: #3d3d3d;
|
--text_color: #3d3d3d;
|
||||||
--outline_color: rgba(34,36,38,.15);
|
--dark_border_color: #DFE1E6;
|
||||||
--border_color: rgba(34,36,38,.20);
|
--border_color: #DFE1E6;
|
||||||
|
|
||||||
/* Global purple menu styles */
|
/* Global purple menu styles */
|
||||||
--menu-border: #534c68;
|
--menu-border: #534c68;
|
||||||
@ -34,7 +35,9 @@
|
|||||||
html {
|
html {
|
||||||
/*scrollbar-width: none;*/
|
/*scrollbar-width: none;*/
|
||||||
}
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
div.ui.basic.segment.no-fluf-segment {
|
div.ui.basic.segment.no-fluf-segment {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
@ -59,21 +62,21 @@ div.ui.basic.segment.no-fluf-segment {
|
|||||||
/* 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(--body_bg_color);
|
||||||
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.segment {
|
.ui.segment {
|
||||||
color: var(--text_color);
|
color: var(--text_color);
|
||||||
background-color: var(--background_color);
|
background-color: var(--small_element_bg_color);
|
||||||
border-color: var(--border_color);
|
border-color: var(--dark_border_color);
|
||||||
}
|
}
|
||||||
.button-sub {
|
.button-sub {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: white;
|
color: grey;
|
||||||
opacity: 0.8;
|
opacity: 0.9;
|
||||||
padding: 4px 0 0 0;
|
padding: 4px 0 0 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@ -83,71 +86,43 @@ body {
|
|||||||
.ui.form textarea:not([type]),
|
.ui.form textarea:not([type]),
|
||||||
.ui.form textarea:not([type]):focus {
|
.ui.form textarea:not([type]):focus {
|
||||||
color: var(--text_color);
|
color: var(--text_color);
|
||||||
background-color: var(--background_color);
|
background-color: var(--small_element_bg_color);
|
||||||
border-color: var(--border_color);
|
border-color: var(--dark_border_color);
|
||||||
}
|
}
|
||||||
.ui.basic.label, .ui.header, .ui.header div.sub.header {
|
.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: transparent;
|
||||||
border-color: var(--border_color);
|
border-color: var(--dark_border_color);
|
||||||
}
|
}
|
||||||
.ui.icon.input > i.icon {
|
.ui.icon.input > i.icon {
|
||||||
color: var(--text_color);
|
color: var(--text_color);
|
||||||
}
|
}
|
||||||
div.ui.basic.green.label {
|
div.ui.basic.green.label {
|
||||||
background-color: var(--background_color) !important;
|
background-color: var(--small_element_bg_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(--small_element_bg_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(--dark_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(--small_element_bg_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(--small_element_bg_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(--small_element_bg_color) !important;
|
||||||
color: var(--text_color) !important;
|
color: var(--text_color) !important;
|
||||||
border-color: var(--border_color) !important;
|
border-color: var(--dark_border_color) !important;
|
||||||
}
|
}
|
||||||
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
|
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
|
||||||
|
|
||||||
/* Styles for public display pages */
|
|
||||||
.fun {
|
|
||||||
color: rgba(0, 0, 0, 0.87);
|
|
||||||
color: var(--text_color);
|
|
||||||
}
|
|
||||||
.fun h1 {
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
.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
|
// Purple Global Menu
|
||||||
//*/
|
//*/
|
||||||
@ -255,6 +230,8 @@ a:hover {
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
/*border-bottom: 1px solid #ccc;*/
|
/*border-bottom: 1px solid #ccc;*/
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
scrollbar-color: transparent transparent;
|
||||||
|
caret-color: #21BA45;
|
||||||
}
|
}
|
||||||
.squire-box::selection,
|
.squire-box::selection,
|
||||||
.squire-box::-moz-selection {
|
.squire-box::-moz-selection {
|
||||||
@ -271,19 +248,15 @@ a:hover {
|
|||||||
.squire-box a {
|
.squire-box a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.note-card-text i:not(.icon),
|
|
||||||
.squire-box i {
|
|
||||||
padding: 0.5em 0.99em;
|
|
||||||
border-radius: 1px;
|
|
||||||
display: inline-block;
|
|
||||||
font-style: normal;
|
|
||||||
background-color: rgba(113, 113, 113, 0.1);
|
|
||||||
}
|
|
||||||
.night-mode .note-card-text i:not(.icon),
|
.night-mode .note-card-text i:not(.icon),
|
||||||
.night-mode .squire-box i {
|
.night-mode .squire-box i {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-card-text pre,
|
||||||
|
.squire-box pre {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
.note-card-text p,
|
.note-card-text p,
|
||||||
.squire-box p {
|
.squire-box p {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-color: var(--border_color);
|
border-color: var(--border_color);
|
||||||
|
background-color: var(--small_element_bg_color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin: 0 0 15px;
|
margin: 0 0 15px;
|
||||||
max-height: 10000px;
|
max-height: 10000px;
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
let filter = {}
|
let filter = {}
|
||||||
filter[option] = 1
|
filter[option] = 1
|
||||||
|
|
||||||
this.$bus.$emit('update_fast_filters', filter)
|
// this.$bus.$emit('update_fast_filters', filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,7 +62,7 @@
|
|||||||
.filter-menu {
|
.filter-menu {
|
||||||
|
|
||||||
color: var(--text_color);
|
color: var(--text_color);
|
||||||
background-color: var(--background_color);
|
background-color: var(--small_element_bg_color);
|
||||||
|
|
||||||
|
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
|
@ -20,16 +20,16 @@
|
|||||||
}
|
}
|
||||||
.menu-logo-display {
|
.menu-logo-display {
|
||||||
width: 25px;
|
width: 25px;
|
||||||
margin: 5px 0 0 34px;
|
margin: 5px 0 0 42px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 0.8em 10px 0.8em 10px;
|
padding: 9px 10px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 1.15em;
|
font-size: 1.1em;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.menu-item i.icon {
|
.menu-item i.icon {
|
||||||
@ -76,7 +76,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
background-color: var(--background_color);
|
background-color: var(--small_element_bg_color);
|
||||||
border-bottom: 1px solid;
|
border-bottom: 1px solid;
|
||||||
border-color: var(--border_color);
|
border-color: var(--border_color);
|
||||||
padding: 5px 1rem 5px;
|
padding: 5px 1rem 5px;
|
||||||
@ -117,9 +117,9 @@
|
|||||||
<i class="green bars icon"></i>
|
<i class="green bars icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link v-if="loggedIn" class="ui large basic compact icon button" to="/notes" v-on:click.native="emitReloadEvent()">
|
<!-- <router-link v-if="loggedIn" class="ui large basic compact icon button" to="/notes" v-on:click.native="emitReloadEvent()">
|
||||||
<i class="green home icon"></i>
|
<i class="green home icon"></i>
|
||||||
</router-link>
|
</router-link> -->
|
||||||
|
|
||||||
<router-link v-if="loggedIn" class="ui basic icon button" exact-active-class="active" to="/attachments">
|
<router-link v-if="loggedIn" class="ui basic icon button" exact-active-class="active" to="/attachments">
|
||||||
<i class="open folder outline icon"></i>
|
<i class="open folder outline icon"></i>
|
||||||
@ -184,6 +184,16 @@
|
|||||||
<counter v-if="$store.getters.totals && $store.getters.totals['totalNotes']" class="float-right" number-id="totalNotes" />
|
<counter v-if="$store.getters.totals && $store.getters.totals['totalNotes']" class="float-right" number-id="totalNotes" />
|
||||||
</router-link>
|
</router-link>
|
||||||
<div>
|
<div>
|
||||||
|
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(3)" v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)">
|
||||||
|
<i class="grey mail outline icon"></i>Inbox
|
||||||
|
</div>
|
||||||
|
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
|
||||||
|
<i class="grey archive icon"></i>Archived
|
||||||
|
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
|
||||||
|
</div>
|
||||||
|
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['trashedNotes'] > 0">
|
||||||
|
<i class="grey trash alternate outline icon"></i>Trashed
|
||||||
|
</div>
|
||||||
<!-- <div class="menu-item sub">Show Only <i class="caret down icon"></i></div> -->
|
<!-- <div class="menu-item sub">Show Only <i class="caret down icon"></i></div> -->
|
||||||
<!-- <div v-on:click="updateFastFilters(0)" class="menu-item menu-button sub"><i class="grey linkify icon"></i>Links</div> -->
|
<!-- <div v-on:click="updateFastFilters(0)" class="menu-item menu-button sub"><i class="grey linkify icon"></i>Links</div> -->
|
||||||
<!-- <div v-on:click="updateFastFilters(1)" class="menu-item menu-button sub"><i class="grey tags icon"></i>Tags</div> -->
|
<!-- <div v-on:click="updateFastFilters(1)" class="menu-item menu-button sub"><i class="grey tags icon"></i>Tags</div> -->
|
||||||
@ -218,7 +228,7 @@
|
|||||||
<span v-if="$store.getters.getIsNightMode == 0">
|
<span v-if="$store.getters.getIsNightMode == 0">
|
||||||
<i class="moon outline icon"></i>Black Theme</span>
|
<i class="moon outline icon"></i>Black Theme</span>
|
||||||
<span v-if="$store.getters.getIsNightMode == 1">
|
<span v-if="$store.getters.getIsNightMode == 1">
|
||||||
<i class="moon outline icon"></i>Night Theme</span>
|
<i class="moon outline icon"></i>Flux Theme</span>
|
||||||
<span v-if="$store.getters.getIsNightMode == 2">
|
<span v-if="$store.getters.getIsNightMode == 2">
|
||||||
<i class="moon outline icon"></i>Light Theme</span>
|
<i class="moon outline icon"></i>Light Theme</span>
|
||||||
</div>
|
</div>
|
||||||
@ -257,7 +267,7 @@
|
|||||||
},
|
},
|
||||||
data: function(){
|
data: function(){
|
||||||
return {
|
return {
|
||||||
version: '2.2.3',
|
version: '2.3.4',
|
||||||
username: '',
|
username: '',
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
mobile: false,
|
mobile: false,
|
||||||
@ -354,27 +364,17 @@
|
|||||||
//Reloads note page to initial state
|
//Reloads note page to initial state
|
||||||
this.$bus.$emit('note_reload')
|
this.$bus.$emit('note_reload')
|
||||||
},
|
},
|
||||||
updateFastFilters(index){
|
updateFastFilters(filterIndex){
|
||||||
|
|
||||||
//A little hacky, brings user to notes page then filters on click
|
//A little hacky, brings user to notes page then filters on click
|
||||||
if(this.$route.name != 'NotesPage'){
|
if(this.$route.name != 'Note Page'){
|
||||||
this.$router.push('/notes')
|
this.$router.push('/notes')
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
this.updateFastFilters(index)
|
this.$bus.$emit('update_fast_filters', filterIndex)
|
||||||
}, 500 )
|
}, 500 )
|
||||||
|
} else {
|
||||||
|
this.$bus.$emit('update_fast_filters', filterIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
},
|
},
|
||||||
reloadPage(){
|
reloadPage(){
|
||||||
location.reload(true)
|
location.reload(true)
|
||||||
|
@ -33,29 +33,17 @@
|
|||||||
export default {
|
export default {
|
||||||
name: 'LoadingIcon',
|
name: 'LoadingIcon',
|
||||||
props:[ 'message' ],
|
props:[ 'message' ],
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
items: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeMount(){
|
|
||||||
|
|
||||||
},
|
|
||||||
mounted(){
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onClickTag(index){
|
|
||||||
console.log('yup')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style type="text/css" scoped>
|
<style type="text/css" scoped>
|
||||||
.loading-container {
|
.loading-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100px;
|
min-height: 100px;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 7px;
|
||||||
|
background-color: var(--small_element_bg_color);
|
||||||
}
|
}
|
||||||
.loading-container svg {
|
.loading-container svg {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div v-on:keyup.enter="submit()">
|
<div v-on:keyup.enter="login()">
|
||||||
|
|
||||||
|
<!-- thicc form display -->
|
||||||
<div v-if="!thin" class="ui large form">
|
<div v-if="!thin" class="ui large form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui input">
|
<div class="ui input">
|
||||||
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail address" autofocus>
|
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@ -13,27 +14,48 @@
|
|||||||
<input v-model="password" type="password" name="password" placeholder="Password">
|
<input v-model="password" type="password" name="password" placeholder="Password">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="submit" class="ui massive compact fluid green submit button">Sign Up / Login</div>
|
<div class="sixteen wide field">
|
||||||
</div>
|
<div class="ui fluid buttons">
|
||||||
|
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="login()" class="ui green button">
|
||||||
|
<i class="power icon"></i>
|
||||||
<div v-if="thin" class="ui small form">
|
Login
|
||||||
<div class="fields">
|
|
||||||
<div class="six wide field">
|
|
||||||
<div class="ui input">
|
|
||||||
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail address" autofocus>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide field">
|
<div class="or"></div>
|
||||||
<div class="ui input">
|
<div v-on:click="register()" class="ui button">
|
||||||
<input v-model="password" type="password" name="password" placeholder="Password">
|
<i class="plug icon"></i>
|
||||||
</div>
|
Sign Up
|
||||||
</div>
|
|
||||||
<div class="four wide field">
|
|
||||||
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="submit" class="ui fluid green submit button">Sign Up / Login</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Thin form display -->
|
||||||
|
<div v-if="thin" class="ui small form">
|
||||||
|
<div class="fields">
|
||||||
|
<div class="four wide field">
|
||||||
|
<div class="ui input">
|
||||||
|
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="four wide field">
|
||||||
|
<div class="ui input">
|
||||||
|
<input v-model="password" type="password" name="password" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="four wide field">
|
||||||
|
<div v-on:click="register()" class="ui fluid green button">
|
||||||
|
<i class="plug icon"></i>
|
||||||
|
Sign Up
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="four wide field">
|
||||||
|
<div v-on:click="login()" class="ui fluid button">
|
||||||
|
<i class="power icon"></i>
|
||||||
|
Login
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -65,43 +87,64 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit(){
|
finalizeLogin(data){
|
||||||
|
|
||||||
//Both fields are required
|
//Destroy local data if there is an error
|
||||||
if(this.username <= 0){
|
if(data == false){
|
||||||
return false
|
this.$store.commit('destroyLoginToken')
|
||||||
}
|
return
|
||||||
if(this.password <= 0){
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let vm = this
|
//Login user if we have a valid token
|
||||||
|
if(data && data.token && data.token.length > 0){
|
||||||
|
|
||||||
let data = {
|
const token = data.token
|
||||||
username: this.username,
|
const username = this.username
|
||||||
password: this.password
|
|
||||||
|
this.$store.commit('setLoginToken', {token, username})
|
||||||
|
|
||||||
|
//Setup socket io after user logs in
|
||||||
|
this.$io.emit('user_connect', token)
|
||||||
|
|
||||||
|
//Redirect user to notes section after login
|
||||||
|
this.$router.push('/notes')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
register(){
|
||||||
|
|
||||||
|
if( this.username.length == 0 || this.password.length == 0 ){
|
||||||
|
this.$bus.$emit('notification', 'Username and Password Required')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.post('/api/user/login', data)
|
axios.post('/api/user/register', {'username': this.username, 'password': this.password})
|
||||||
.then(response => {
|
.then(({data}) => {
|
||||||
if(response.data.success){
|
|
||||||
|
|
||||||
const token = response.data.token
|
if(data == false){
|
||||||
const username = response.data.username
|
this.$bus.$emit('notification', 'Username already in use')
|
||||||
const masterKey = response.data.masterKey
|
|
||||||
|
|
||||||
this.$store.commit('setLoginToken', {token, username, masterKey})
|
|
||||||
|
|
||||||
//Setup socket io after user logs in
|
|
||||||
this.$io.emit('user_connect', token)
|
|
||||||
|
|
||||||
//Redirect user to notes section after login
|
|
||||||
this.$router.push('/notes')
|
|
||||||
} else {
|
|
||||||
// this.password = ''
|
|
||||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
|
||||||
vm.$store.commit('destroyLoginToken')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.finalizeLogin(data)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$bus.$emit('notification', 'Username already in use')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
login(){
|
||||||
|
|
||||||
|
if( this.username.length == 0 || this.password.length == 0 ){
|
||||||
|
this.$bus.$emit('notification', 'Username and Password Required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.post('/api/user/login', {'username': this.username, 'password': this.password})
|
||||||
|
.then(({data}) => {
|
||||||
|
|
||||||
|
if(data == false){
|
||||||
|
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.finalizeLogin(data)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -323,7 +323,7 @@
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--background_color);
|
background-color: var(--small_element_bg_color);
|
||||||
color: var(--text_color);
|
color: var(--text_color);
|
||||||
}
|
}
|
||||||
.suggestion-item.active {
|
.suggestion-item.active {
|
||||||
|
@ -242,11 +242,11 @@
|
|||||||
justClosed(){
|
justClosed(){
|
||||||
|
|
||||||
//Scroll note into view
|
//Scroll note into view
|
||||||
this.$el.scrollIntoView({
|
// this.$el.scrollIntoView({
|
||||||
behavior: 'smooth',
|
// behavior: 'smooth',
|
||||||
block: 'center',
|
// block: 'center',
|
||||||
inline: 'center'
|
// inline: 'center'
|
||||||
})
|
// })
|
||||||
|
|
||||||
//After scroll, trigger green outline animation
|
//After scroll, trigger green outline animation
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -353,7 +353,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
color: var(--text_color);
|
color: var(--text_color);
|
||||||
background-color: var(--background_color);
|
background-color: var(--small_element_bg_color);
|
||||||
}
|
}
|
||||||
.subtext {
|
.subtext {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -368,7 +368,7 @@
|
|||||||
}
|
}
|
||||||
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
|
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
|
||||||
/*font-size: 1.0em !important;*/
|
/*font-size: 1.0em !important;*/
|
||||||
font-size: 15px !important;
|
font-size: 16px !important;
|
||||||
}
|
}
|
||||||
.small-text > p, , .small-text > h1, .small-text > h2 {
|
.small-text > p, , .small-text > h1, .small-text > h2 {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
@ -410,19 +410,17 @@
|
|||||||
|
|
||||||
.note-title-display-card {
|
.note-title-display-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
/*box-shadow: 0 1px 3px 0 rgba(34,36,38,.15);*/
|
background-color: var(--small_element_bg_color);
|
||||||
/*box-shadow: 0 0px 5px 1px rgba(34,36,38,0);*/
|
/*The subtle shadow*/
|
||||||
/*box-shadow: 0 1px 3px 0 rgba(34,36,38,.15);*/
|
/*box-shadow: 0px 1px 2px 1px rgba(210, 211, 211, 0.46);*/
|
||||||
box-shadow: 0px 1px 2px 1px rgba(210, 211, 211, 0.46);
|
transition: box-shadow ease 0.5s, transform linear 0.1s;
|
||||||
transition: box-shadow ease 0.3s;
|
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
/*padding: 0.7em 1em;*/
|
/*padding: 0.7em 1em;*/
|
||||||
border-radius: .28571429rem;
|
border-radius: .28571429rem;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
/*border-color: var(--border_color);*/
|
border-color: var(--border_color);
|
||||||
/*width: calc(33.333% - 10px);*/
|
/*width: calc(33.333% - 10px);*/
|
||||||
width: calc(25% - 10px);
|
width: calc(25% - 10px);
|
||||||
max-width: 300px;
|
|
||||||
min-width: 190px;
|
min-width: 190px;
|
||||||
min-height: 130px;
|
min-height: 130px;
|
||||||
/*transition: box-shadow 0.3s;*/
|
/*transition: box-shadow 0.3s;*/
|
||||||
@ -436,13 +434,15 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.note-title-display-card:hover {
|
.note-title-display-card:hover {
|
||||||
box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8);
|
/*box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8);*/
|
||||||
|
/*transform: translateY(-2px);*/
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
.note-title-display-card.title-view {
|
.note-title-display-card.title-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 10px;
|
min-height: 20px;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);
|
/*box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.single-line-text {
|
.single-line-text {
|
||||||
@ -527,6 +527,7 @@
|
|||||||
.one-column .note-title-display-card {
|
.one-column .note-title-display-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
/*margin: 0px -5px 10px -5px;*/
|
||||||
}
|
}
|
||||||
.overflow-hidden {
|
.overflow-hidden {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -561,11 +562,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Tweak mobile display to show only one column */
|
/* Tweak mobile display to show only one column */
|
||||||
|
@media only screen and (min-width: 1500px) {
|
||||||
|
.note-title-display-card {
|
||||||
|
width: calc(20% - 10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
@media only screen and (max-width: 740px) {
|
@media only screen and (max-width: 740px) {
|
||||||
.note-title-display-card {
|
.note-title-display-card {
|
||||||
width: calc(100% + 10px);
|
width: calc(100% + 10px);
|
||||||
margin: 0px -5px 10px -5px;
|
margin: 0px -5px 10px -5px;
|
||||||
max-width: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,14 +71,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui very compact grid" v-if="tagSuggestions.length > 0">
|
|
||||||
<div class="sixteen wide column">
|
|
||||||
<div class="ui clickable green label" v-for="tag in tagSuggestions" v-on:click="tagClick(tag.id)">
|
|
||||||
<i class="tag icon"></i>
|
|
||||||
{{ tag.text }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -141,11 +133,7 @@
|
|||||||
axios.post('/api/quick-note/update', { 'pushText':text.trim() } )
|
axios.post('/api/quick-note/update', { 'pushText':text.trim() } )
|
||||||
.then( response => {
|
.then( response => {
|
||||||
|
|
||||||
//Open Quick Note
|
this.$bus.$emit('notification', 'Saved To Scratch Pad')
|
||||||
if(response.data && response.data.id){
|
|
||||||
this.$router.push('/notes/open/'+response.data.id)
|
|
||||||
this.$bus.$emit('open_note', response.data.id)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Update The Scratch Pad') })
|
.catch(error => { this.$bus.$emit('notification', 'Failed to Update The Scratch Pad') })
|
||||||
},
|
},
|
||||||
@ -173,20 +161,20 @@
|
|||||||
|
|
||||||
clearTimeout(this.tagSearchDebounce)
|
clearTimeout(this.tagSearchDebounce)
|
||||||
|
|
||||||
if(this.searchTerm.length == 0){
|
// if(this.searchTerm.length == 0){
|
||||||
this.tagSuggestions = []
|
// this.tagSuggestions = []
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.tagSearchDebounce = setTimeout(() => {
|
// this.tagSearchDebounce = setTimeout(() => {
|
||||||
this.tagSuggestions = []
|
// this.tagSuggestions = []
|
||||||
axios.post('/api/tag/suggest', postData)
|
// axios.post('/api/tag/suggest', postData)
|
||||||
.then( response => {
|
// .then( response => {
|
||||||
|
|
||||||
this.tagSuggestions = response.data
|
// this.tagSuggestions = response.data
|
||||||
})
|
// })
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
|
// .catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
|
||||||
}, 800)
|
// }, 800)
|
||||||
},
|
},
|
||||||
onKeyDown(event){
|
onKeyDown(event){
|
||||||
|
|
||||||
|
@ -5,7 +5,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div class="ui grid" v-if="this.shareUsername == null">
|
<div class="ui grid" v-if="shareUsername == null">
|
||||||
|
|
||||||
|
<div v-if="!isNoteShared" class="sixteen wide column">
|
||||||
|
<div class="ui button" v-on:click="makeShared()">Enable Shared</div>
|
||||||
|
<p>Shared notes are different and junk.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isNoteShared" class="sixteen wide column">
|
||||||
|
<p>Generating a shared URL will expose the password of this note.</p>
|
||||||
|
<div class="ui button" v-on:click="removeShared()">Remove Shared</div>
|
||||||
|
|
||||||
|
<div class="ui button" v-on:click="getSharedUrl()">Get Shareable URL</div>
|
||||||
|
|
||||||
|
<div v-if="sharedUrl.length > 0">
|
||||||
|
<a target="_blank" :href="sharedUrl">{{ sharedUrl }}</a>
|
||||||
|
<div class="ui input">
|
||||||
|
<input type="text" v-model="sharedUrl">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui grid" v-if="shareUsername == null">
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
@ -38,7 +61,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui grid" v-if="this.shareUsername != null">
|
<div class="ui grid" v-if="shareUsername != null">
|
||||||
<div class="sixteen wide column">
|
<div class="sixteen wide column">
|
||||||
Shared with you by <h3><i class="green user circle icon"></i>{{shareUsername}}</h3>
|
Shared with you by <h3><i class="green user circle icon"></i>{{shareUsername}}</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -56,10 +79,12 @@
|
|||||||
props: [ 'noteId', 'rawTextId', 'shareUsername' ],
|
props: [ 'noteId', 'rawTextId', 'shareUsername' ],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
isNoteShared: false,
|
||||||
sharedWithUsers: [],
|
sharedWithUsers: [],
|
||||||
shareUserInput: '',
|
shareUserInput: '',
|
||||||
debounce: null,
|
debounce: null,
|
||||||
enableSubmitShare: false,
|
enableSubmitShare: false,
|
||||||
|
sharedUrl: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount(){
|
beforeMount(){
|
||||||
@ -67,6 +92,8 @@
|
|||||||
},
|
},
|
||||||
mounted(){
|
mounted(){
|
||||||
|
|
||||||
|
// this.isNoteShared = this.noteShared
|
||||||
|
|
||||||
if(this.shareUsername == null){
|
if(this.shareUsername == null){
|
||||||
this.loadShareList()
|
this.loadShareList()
|
||||||
}
|
}
|
||||||
@ -74,12 +101,39 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadShareList(){
|
loadShareList(){
|
||||||
axios.post('/api/note/getshareusers', {'rawTextId':this.rawTextId })
|
axios.post('/api/note/getshareinfo', {'noteId':this.noteId, 'rawTextId':this.rawTextId })
|
||||||
.then( ({data}) => {
|
.then( ({data}) => {
|
||||||
this.sharedWithUsers = data
|
|
||||||
|
this.isNoteShared = (data.shareStatus == 2)
|
||||||
|
this.sharedWithUsers = data.shareUsers
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Load Shared') })
|
.catch(error => { this.$bus.$emit('notification', 'Failed to Load Shared') })
|
||||||
},
|
},
|
||||||
|
makeShared(){
|
||||||
|
axios.post('/api/note/enableshare', {'noteId':this.noteId })
|
||||||
|
.then( ({data}) => {
|
||||||
|
this.isNoteShared = true
|
||||||
|
})
|
||||||
|
.catch(error => { this.$bus.$emit('notification', 'Failed to fetch Shared URL') })
|
||||||
|
},
|
||||||
|
removeShared(){
|
||||||
|
axios.post('/api/note/disableshare', {'noteId':this.noteId })
|
||||||
|
.then( ({data}) => {
|
||||||
|
this.isNoteShared = false
|
||||||
|
})
|
||||||
|
.catch(error => { this.$bus.$emit('notification', 'Failed to remove share status') })
|
||||||
|
},
|
||||||
|
getSharedUrl(){
|
||||||
|
axios.post('/api/note/getsharekey', {'noteId':this.noteId })
|
||||||
|
.then( ({data}) => {
|
||||||
|
|
||||||
|
const encodedKey = encodeURIComponent(data)
|
||||||
|
|
||||||
|
this.sharedUrl = `${window.location.protocol}//${window.location.hostname}/#/public/note/${this.noteId}/${encodedKey}`
|
||||||
|
})
|
||||||
|
.catch(error => { this.$bus.$emit('notification', 'Failed to fetch Shared URL') })
|
||||||
|
},
|
||||||
onRevokeAccess(sharedNoteId){
|
onRevokeAccess(sharedNoteId){
|
||||||
|
|
||||||
const postData = {
|
const postData = {
|
||||||
|
@ -10,14 +10,14 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
color: var(--text_color);
|
color: var(--text_color);
|
||||||
background-color: var(--background_color);
|
background-color: var(--small_element_bg_color);
|
||||||
}
|
}
|
||||||
.slide-content {
|
.slide-content {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/*padding: 1em 1.5em;*/
|
/*padding: 1em 1.5em;*/
|
||||||
height: calc(100% - 43px);
|
height: calc(100% - 43px);
|
||||||
border-right: 1px solid var(--menu-border);
|
border-right: 1px solid var(--menu-border);
|
||||||
/*background-color: var(--background_color);*/
|
/*background-color: var(--small_element_bg_color);*/
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
.slide-shadow {
|
.slide-shadow {
|
||||||
|
344
client/src/mixins/SquireButtonFunctions.js
Normal file
344
client/src/mixins/SquireButtonFunctions.js
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
const SquireButtonFunctions = {
|
||||||
|
data(){
|
||||||
|
return {
|
||||||
|
//active button states
|
||||||
|
activeBold: false,
|
||||||
|
activeItalics: false,
|
||||||
|
activeUnderline: false,
|
||||||
|
activeTitle: false,
|
||||||
|
activeList: false,
|
||||||
|
activeToDo: false,
|
||||||
|
activeColor: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
//
|
||||||
|
// Inside squire init function
|
||||||
|
//
|
||||||
|
|
||||||
|
pathChangeEvent(e){
|
||||||
|
//Reset all button states
|
||||||
|
this.activeBold = false
|
||||||
|
this.activeTitle = false
|
||||||
|
this.activeItalics = false
|
||||||
|
this.activeList = false
|
||||||
|
this.activeToDo = false
|
||||||
|
this.activeColor = null
|
||||||
|
this.activeUnderline = false
|
||||||
|
|
||||||
|
if(e.path.indexOf('>U>') > -1 || e.path.search(/U$/) > -1){
|
||||||
|
this.activeUnderline = true
|
||||||
|
}
|
||||||
|
if(e.path.indexOf('>B>') > -1 || e.path.search(/B$/) > -1){
|
||||||
|
this.activeBold = true
|
||||||
|
}
|
||||||
|
if(e.path.indexOf('>I') > -1){
|
||||||
|
this.activeItalics = true
|
||||||
|
}
|
||||||
|
if(e.path.indexOf('fontSize') > -1){
|
||||||
|
this.activeTitle = true
|
||||||
|
}
|
||||||
|
if(e.path.indexOf('OL>LI') > -1){
|
||||||
|
this.activeList = true
|
||||||
|
}
|
||||||
|
if(e.path.indexOf('UL>LI') > -1){
|
||||||
|
this.activeToDo = true
|
||||||
|
}
|
||||||
|
const colorIndex = e.path.indexOf('color=')
|
||||||
|
if(colorIndex > -1){
|
||||||
|
//Get all digigs after color index, then limit to 3
|
||||||
|
let colors = e.path.substring(colorIndex).match(/\d+/g).slice(0,3)
|
||||||
|
this.activeColor=`rgb(${colors.join(',')})`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
//Inside Squire Init
|
||||||
|
//
|
||||||
|
|
||||||
|
removeFormatting(){
|
||||||
|
this.selectLineIfNoSelect()
|
||||||
|
this.editor.removeAllFormatting()
|
||||||
|
},
|
||||||
|
//If nothing is selected, select the entire line
|
||||||
|
selectLineIfNoSelect(){
|
||||||
|
|
||||||
|
//Select entire line if range is not set
|
||||||
|
let selection = this.editor.getSelection()
|
||||||
|
|
||||||
|
if(selection.startOffset == selection.endOffset && selection.startContainer == selection.endContainer){
|
||||||
|
|
||||||
|
let squireRange = this.editor.createRange(
|
||||||
|
selection.startContainer, 0,
|
||||||
|
selection.endContainer, selection.commonAncestorContainer.textContent.length)
|
||||||
|
|
||||||
|
this.editor.setSelection(squireRange)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifyFont(inSize){
|
||||||
|
|
||||||
|
this.selectLineIfNoSelect()
|
||||||
|
|
||||||
|
let fontInfo = this.editor.getFontInfo()
|
||||||
|
//Toggle font size between large and normal
|
||||||
|
if(fontInfo.size){
|
||||||
|
this.editor.setFontSize(null)
|
||||||
|
} else {
|
||||||
|
this.editor.setFontSize(inSize)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifyColor(color){
|
||||||
|
|
||||||
|
this.selectLineIfNoSelect()
|
||||||
|
//Set color of font
|
||||||
|
this.editor.setTextColour(color)
|
||||||
|
},
|
||||||
|
toggleList(type){
|
||||||
|
|
||||||
|
//Undo list if its already a lits
|
||||||
|
if(this.editor.hasFormat(type)){
|
||||||
|
this.editor.removeList()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type == 'ol'){
|
||||||
|
this.editor.makeOrderedList()
|
||||||
|
}
|
||||||
|
if(type == 'ul'){
|
||||||
|
this.editor.makeUnorderedList()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleUnderline(){
|
||||||
|
this.selectLineIfNoSelect()
|
||||||
|
if( this.editor.hasFormat('u') ){
|
||||||
|
this.editor.removeUnderline()
|
||||||
|
} else {
|
||||||
|
this.editor.underline()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleBold(){
|
||||||
|
this.selectLineIfNoSelect()
|
||||||
|
if( this.editor.hasFormat('b') ){
|
||||||
|
this.editor.removeBold()
|
||||||
|
} else {
|
||||||
|
this.editor.bold()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleItalic(){
|
||||||
|
this.selectLineIfNoSelect()
|
||||||
|
if( this.editor.hasFormat('i') ){
|
||||||
|
this.editor.removeItalic()
|
||||||
|
} else {
|
||||||
|
this.editor.italic()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undoCustom(){
|
||||||
|
//The same as pressing CTRL + Z
|
||||||
|
// this.editor.focus()
|
||||||
|
// document.execCommand("undo", false, null)
|
||||||
|
this.editor.undo()
|
||||||
|
},
|
||||||
|
uncheckAllListItems(){
|
||||||
|
//
|
||||||
|
// Uncheck All List Items
|
||||||
|
//
|
||||||
|
|
||||||
|
//Close menu if user is on mobile, then sort list
|
||||||
|
if(this.$store.getters.getIsUserOnMobile){
|
||||||
|
this.options = false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Fetch the container
|
||||||
|
let container = document.getElementById('squire-id')
|
||||||
|
|
||||||
|
Array.from( container.getElementsByClassName('active') ).forEach(item => {
|
||||||
|
item.classList.remove('active');
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteCompletedListItems(){
|
||||||
|
//
|
||||||
|
// Delete Completed List Items
|
||||||
|
//
|
||||||
|
|
||||||
|
//Close menu if user is on mobile, then sort list
|
||||||
|
if(this.$store.getters.getIsUserOnMobile){
|
||||||
|
this.options = false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Fetch the container
|
||||||
|
let container = document.getElementById('squire-id')
|
||||||
|
|
||||||
|
//Go through each item, on first level, look for Unordered Lists
|
||||||
|
container.childNodes.forEach( (node) => {
|
||||||
|
if(node.nodeName == 'UL'){
|
||||||
|
|
||||||
|
//Create two categories, done and not done list items
|
||||||
|
let undoneElements = document.createDocumentFragment()
|
||||||
|
|
||||||
|
//Go through each item in each list we found
|
||||||
|
node.childNodes.forEach( (checkListItem, index) => {
|
||||||
|
|
||||||
|
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
|
||||||
|
if(checkListItem.nodeName == 'UL'){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if list item has active class
|
||||||
|
const checkedItem = checkListItem.classList.contains('active')
|
||||||
|
|
||||||
|
//Check if the next item is a list, Keep lists with intented items together
|
||||||
|
let sublist = null
|
||||||
|
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
|
||||||
|
sublist = node.childNodes[index+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
//Push checked items and their sub lists to the done set
|
||||||
|
if(!checkedItem){
|
||||||
|
|
||||||
|
undoneElements.appendChild( checkListItem.cloneNode(true) )
|
||||||
|
if(sublist){
|
||||||
|
undoneElements.appendChild( sublist.cloneNode(true) )
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
//Remove all HTML from node, push unfinished items, then finished below them
|
||||||
|
node.innerHTML = null
|
||||||
|
node.appendChild(undoneElements)
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sortList(){
|
||||||
|
//
|
||||||
|
// Sort list, checked at the bottom, unchecked at the top
|
||||||
|
//
|
||||||
|
|
||||||
|
//Close menu if user is on mobile, then sort list
|
||||||
|
if(this.$store.getters.getIsUserOnMobile){
|
||||||
|
this.options = false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Fetch the container
|
||||||
|
let container = document.getElementById('squire-id')
|
||||||
|
|
||||||
|
//Go through each item, on first level, look for Unordered Lists
|
||||||
|
container.childNodes.forEach( (node) => {
|
||||||
|
if(node.nodeName == 'UL'){
|
||||||
|
|
||||||
|
//Create two categories, done and not done list items
|
||||||
|
let doneElements = document.createDocumentFragment()
|
||||||
|
let undoneElements = document.createDocumentFragment()
|
||||||
|
|
||||||
|
//Go through each item in each list we found
|
||||||
|
node.childNodes.forEach( (checkListItem, index) => {
|
||||||
|
|
||||||
|
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
|
||||||
|
if(checkListItem.nodeName == 'UL'){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if list item has active class
|
||||||
|
const checkedItem = checkListItem.classList.contains('active')
|
||||||
|
|
||||||
|
//Check if the next item is a list, Keep lists with intented items together
|
||||||
|
let sublist = null
|
||||||
|
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
|
||||||
|
sublist = node.childNodes[index+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
//Push checked items and their sub lists to the done set
|
||||||
|
if(checkedItem){
|
||||||
|
|
||||||
|
doneElements.appendChild( checkListItem.cloneNode(true) )
|
||||||
|
if(sublist){
|
||||||
|
doneElements.appendChild( sublist.cloneNode(true) )
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
undoneElements.appendChild( checkListItem.cloneNode(true) )
|
||||||
|
if(sublist){
|
||||||
|
undoneElements.appendChild( sublist.cloneNode(true) )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
//Remove all HTML from node, push unfinished items, then finished below them
|
||||||
|
node.innerHTML = null
|
||||||
|
node.appendChild(undoneElements)
|
||||||
|
node.appendChild(doneElements)
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
calculateMath(){
|
||||||
|
//
|
||||||
|
// Find math in note and calculate the outcome
|
||||||
|
//
|
||||||
|
|
||||||
|
//Close menu if user is on mobile, then sort list
|
||||||
|
if(this.$store.getters.getIsUserOnMobile){
|
||||||
|
this.options = false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Fetch the container
|
||||||
|
let container = document.getElementById('squire-id')
|
||||||
|
|
||||||
|
// simple function that trys to evaluate javascript
|
||||||
|
const shittyMath = (string) => {
|
||||||
|
//Remove all chars but math chars
|
||||||
|
const cleanString = String(string).replace(/[a-zA-Z\s]*/g,'')
|
||||||
|
try {
|
||||||
|
return Function('"use strict"; return (' + cleanString + ')')();
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Math Error: ', string)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Go through each item, on first level, look for Unordered Lists
|
||||||
|
container.childNodes.forEach( (node) => {
|
||||||
|
|
||||||
|
const line = node.innerText.trim()
|
||||||
|
|
||||||
|
// = sign exists and its the last character in the string
|
||||||
|
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
|
||||||
|
|
||||||
|
//Pull out everything before the formula and try to evaluate it
|
||||||
|
const formula = line.split('=').shift()
|
||||||
|
const output = shittyMath(formula)
|
||||||
|
|
||||||
|
//If its a number and didn't throw an error, update the line
|
||||||
|
if(!isNaN(output) && output != null){
|
||||||
|
|
||||||
|
//Since there is HTML in the line, splice in the number after the = sign
|
||||||
|
let equalLocation = node.innerHTML.indexOf('=')
|
||||||
|
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
|
||||||
|
newLine += ` ${output}`
|
||||||
|
newLine += node.innerHTML.slice(equalLocation+1).trim()
|
||||||
|
|
||||||
|
//Slam in that new HTML with the output
|
||||||
|
node.innerHTML = newLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setText(inText){
|
||||||
|
|
||||||
|
this.editor.setHTML(inText)
|
||||||
|
// this.noteText = this.editor._getHTML()
|
||||||
|
// this.diffNoteText = this.editor._getHTML()
|
||||||
|
},
|
||||||
|
getText(){
|
||||||
|
|
||||||
|
return this.editor.getHTML()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SquireButtonFunctions
|
@ -11,7 +11,7 @@
|
|||||||
<!-- Content copied from note -->
|
<!-- Content copied from note -->
|
||||||
<!-- https://www.solidscribe.com/#/notes/open/552 -->
|
<!-- https://www.solidscribe.com/#/notes/open/552 -->
|
||||||
|
|
||||||
<p><b>Every Note is Encrypted</b><br></p><p>Only you can read your notes. Even if every note in the database was leaked, nothing would be readable. If the government asked for your notes, it would all be gibberish. <br></p><p><br></p><p><b>Some Data is not encrypted</b><br></p><p>Everything isn't encrypted, to keep up ease of use. Files, Tags and Attachments are not encrypted.<br></p><p><br></p><p><b>Searching is somewhat limited</b><br></p><p>Since every note is encrypted, searching is limited. To maintain security, only single words can be searched. Your search index is private and Encrypted.<br></p><p><br></p><p><b>Quick Note</b><br></p><p>The Quick note feature was designed to allow rapid input to a single note. Rather than junking up all your notes with random links, numbers or haikus, you can put them all in one place. <br></p><p>All data pushed to the quick note can still be edited like a normal note.<br></p><p><br></p><p><b>Dark Theme</b><br></p><p>Dark theme was designed to minimize the amount of blue. Less blue entering your eyes is supposed to help you fall asleep.<br></p><p>Most things turn sepia and a filter is applied to images to make them more sepia.<br></p><p>Here is some good research on the topic: <a href="https://justgetflux.com/research.html">https://justgetflux.com/research.html</a><br></p><p><br></p><p><b>Password Protected Notes</b><br></p><p>Note protected with a password are encrypted. This means the data is scrambled and unreadable unless the correct password is used to decrypt them.<br></p><p>If a password is forgotten, it can never be recovered. Passwords are not saved for encrypted notes. If you lose the password to a protected note, that note text is lost. <br></p><p>Only the text of the note is protected. Tags, Files attached to the note, and the title of the note are still visible without a password. You can not search text in a password protected note. But you can search by the title.<br></p><p><br></p><p><b>Links in notes</b><br></p><p>Links put into notes are automatically scraped. This means the data from the link will be scanned to get an image and some text from the website to help make that link more accessible in the future. <br></p><p><br></p><p><b>Files in notes</b><br></p><p>Files can be uploaded to notes. If its an image, the picture will be put into the note.<br></p><p>Images added to notes will have the text pulled out so it can be searched (This isn't super accurate so don't rely to heavily on it.) The text can be updated at any time.<br></p><p><br></p><p><b>Deleting notes</b><br></p><p>When<b> </b>notes are deleted, none of the files related to the note are deleted. <br></p><p><br></p><p><b>Daily Backups</b><br></p><p>All notes are backed up, every night, at midnight. If there is data loss, it can be restored from a backup. If you experience some sort of cataclysmic data loss please contact the system administrator for a copy of your data or a restoration procedure. <br></p>
|
<p><b>Only Note Text is Encrypted</b><br></p><p>Only you can read your notes. Encryption is the transformation of data into a form unreadable by anyone without the password. Its purpose is to ensure privacy by keeping the information hidden from anyone for whom it is not intended, even those who can see the encrypted data. Even if every note in the database was leaked, nothing would be readable. If the government asked for your notes, it would all be gibberish. <br></p><p><br></p><p><b>Some Data is not encrypted</b><br></p><p>Everything isn't encrypted, to keep up ease of use. Files, Tags and Attachments are not encrypted.<br></p><p><br></p><p><b>Searching is somewhat limited</b><br></p><p>Since every note is encrypted, searching is limited. To maintain security, only single words can be searched. Your search index is private and Encrypted.<br></p><p><br></p><p><b>The Scrat</b><b></b><b>ch Pad</b><b></b><br></p><p>The Scratch Pad was designed to allow rapid input to a single note. Rather than junking up all your notes with random links, numbers or haikus, you can put them all in one place. <br></p><ul><li>All data pushed to the quick note can still be edited like a normal note.<br></li><li>Creating a new quick note will not erase your old <br></li></ul><p><br></p><p><b>Flux Theme</b><br></p><p>Flux theme limits the amount of blue emitted by your screen. Most things turn sepia and a filter is applied to images to make them more sepia. Less blue light at night is supposed to be helpful for falling asleep.<br></p><p>Here is some good research on the topic: <a href="https://justgetflux.com/research.html">https://justgetflux.com/research.html</a><br></p><p><br></p><p><b>Keyboard Shor</b><b></b><b>tcuts</b><br></p><p>Number List - CTRL + SHIFT + 9<br></p><p>Todo List - CTRL + SHIFT + 8<br></p><p>Underline CTRL + u<br></p><p>Bold - CTRL + b<br></p><p>Quote - CRTL + i<br></p><p>Indent - CTL + ]<br></p><p>Outdent - CRTL + [<br></p><p>Undo - CTRL + z<br></p><p>Redo - CTRL + y<br></p><p><br></p><p><b>Links in notes</b><br></p><p>Links put into notes are automatically scraped. This means the data from the link will be scanned to get an image and some text from the website to help make that link more accessible in the future. You can edit the text of scarped links and any time and search for it later. <br></p><p><br></p><p><b>Files in notes</b><br></p><p>Files can be uploaded to notes. If its an image, the picture will be put into the note.<br></p><p>Images added to notes will have text scanned, so it can be searched (This isn't super accurate so don't rely to heavily on it.) The text can be updated at any time.<br></p><p><br></p><p><b>Deleting notes</b><br></p><p>When<b> </b>notes are deleted, none of the files related to the note are deleted. <br></p><p><br></p><p><b>Daily Backups</b><br></p><p>All notes are backed up, every night, at midnight. If there is data loss, it can be restored from a backup. If you experience some sort of cataclysmic data loss please contact the system administrator for a copy of your data or a restoration procedure. <br></p>
|
||||||
|
|
||||||
<!-- content copied from note -->
|
<!-- content copied from note -->
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,6 +78,10 @@
|
|||||||
.home-main img {
|
.home-main img {
|
||||||
max-height: 400px !important;
|
max-height: 400px !important;
|
||||||
}
|
}
|
||||||
|
.white-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@ -120,7 +124,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<h3 class="subtext">
|
<h3 class="subtext">
|
||||||
An easy, free, secure Note App<i class="i cursor icon blinking"></i>
|
A free, secure Note App<i class="i cursor icon blinking"></i>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -146,7 +150,7 @@
|
|||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
<h2>
|
<h2>
|
||||||
<i class="plug icon"></i>
|
<i class="plug icon"></i>
|
||||||
Sign Up Now - Only a Username and Password are required.</h2>
|
Sign Up Now - Only a Username and Password required</h2>
|
||||||
<login-form :thin="true" />
|
<login-form :thin="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -155,7 +159,7 @@
|
|||||||
<!-- set -->
|
<!-- set -->
|
||||||
<div class="middle aligned centered row">
|
<div class="middle aligned centered row">
|
||||||
<div class="six wide right aligned column">
|
<div class="six wide right aligned column">
|
||||||
<h2>Solid Scribe is an online note application that focuses on ease of use and security</h2>
|
<h2>Solid Scribe is a browser based note application that focuses on ease of use while keeping your data private</h2>
|
||||||
<h3>Tools to organize and collaborate on notes while maintaining security and respecting your privacy.</h3>
|
<h3>Tools to organize and collaborate on notes while maintaining security and respecting your privacy.</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
@ -163,24 +167,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="middle aligned centered row">
|
<div class="middle aligned centered green row">
|
||||||
<div class="six wide right aligned column">
|
<div class="six wide right aligned column">
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/gardening.svg" alt="Pruning the mind garden">
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<h2>Tools to organize thousands of notes</h2>
|
<h2>All Note text is encrypted</h2>
|
||||||
<h3>Tag, Pin, Color, Archive, Attach Images and Search notes or links in notes</h3>
|
<h3>Only you can read your notes. <a class="white-link" target="_blank" href="https://www.forbes.com/sites/zakdoffman/2019/01/30/facebook-has-just-been-caught-spying-on-users-private-messages-and-data-again/#1e27e00a31ce"> Employees can not snoop your account</a>. <a class="white-link" target="_blank" href="https://mashable.com/article/google-reading-your-emails-response/">No one can read your data for advertising</a>. Note text is completely unreadable without your password.</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- set -->
|
<!-- set -->
|
||||||
<div class="middle aligned centered green row">
|
<div class="middle aligned centered row">
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<h2>Privacy through Encryption</h2>
|
<h2>Organize your notes</h2>
|
||||||
<h3>All notes are encrypted. No one can read your notes, even if they steal the data from the database.</h3>
|
<h3>Tag, Pin, Color, Archive, Attach Images, Share Encrypted Notes and Search</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/gardening.svg" alt="Pruning the mind garden">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -189,7 +194,7 @@
|
|||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos">
|
<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="six wide column">
|
<div class="six wide column">
|
||||||
<h2>Extremely accessible</h2>
|
<h2>Extremely accessible - Nothing to install</h2>
|
||||||
<h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3>
|
<h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -198,7 +203,7 @@
|
|||||||
<div class="middle aligned centered row">
|
<div class="middle aligned centered row">
|
||||||
<div class="six wide right aligned column">
|
<div class="six wide right aligned column">
|
||||||
<h2>Secure Search</h2>
|
<h2>Secure Search</h2>
|
||||||
<h3>Keyword search using an encrypted search index helps you find what you need without compromising security</h3>
|
<h3>Keyword search using an encrypted search index helps you find what you need without compromising security.</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/solution.svg" alt="Hypercube of Solutions">
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/solution.svg" alt="Hypercube of Solutions">
|
||||||
@ -210,16 +215,16 @@
|
|||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/plan.svg" alt="Scheme for planetary destruction">
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/plan.svg" alt="Scheme for planetary destruction">
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<h2>Embrace the Void</h2>
|
<h2>Create Lists with Check Boxes</h2>
|
||||||
<h3>Remove unnecessary clutter for your brain and save it to the cloud, allowing you to easily embrace the gaping abyss</h3>
|
<h3>Todo lists are supported. With options to removed checked items, sort by completed and un-check all.</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- set -->
|
<!-- set -->
|
||||||
<div class="middle aligned centered row">
|
<div class="middle aligned centered row">
|
||||||
<div class="six wide right aligned column">
|
<div class="six wide right aligned column">
|
||||||
<h2>Space for Growth</h2>
|
<h2>Powerful Text Editing</h2>
|
||||||
<h3>Groom a clear path for new expressions and innovations. Elevate your being and lower your cholesterol</h3>
|
<h3>A plethora of editing tools are provided for coloring, underlining, bolding, attaching images and more.</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column">
|
<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">
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/growth.svg" alt="Endless progress at the cost of sanity and health">
|
||||||
@ -231,13 +236,13 @@
|
|||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="Shrunken man near giant tablet">
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="Shrunken man near giant tablet">
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<h2>Become your Data</h2>
|
<h2>Secure Data Sharing</h2>
|
||||||
<h3>We exist as electrical impulses, no different from data on a computer</h3>
|
<h3>Share notes with friends without compromising privacy. The data remains encrypted with a shared password for you and people you invite to view it.</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- set -->
|
<!-- set -->
|
||||||
<div class="middle aligned centered row">
|
<!-- <div class="middle aligned centered row">
|
||||||
<div class="six wide right aligned column">
|
<div class="six wide right aligned column">
|
||||||
<h2>Ice Cream</h2>
|
<h2>Ice Cream</h2>
|
||||||
<h3>Get excited without all the screaming</h3>
|
<h3>Get excited without all the screaming</h3>
|
||||||
@ -265,7 +270,7 @@
|
|||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/grandma.svg" alt="Drinking the blood of the elderly">
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/grandma.svg" alt="Drinking the blood of the elderly">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- final slide -->
|
<!-- final slide -->
|
||||||
<div class="middle aligned centered green row">
|
<div class="middle aligned centered green row">
|
||||||
@ -282,20 +287,18 @@
|
|||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
OR
|
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<span class="ui button" v-on:click="showRealInformation">View real information about this site</span>
|
<span class="ui button" v-on:click="showRealInformation">About</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="realInformation" class="middle aligned centered row" ref="real">
|
<div v-if="realInformation" class="middle aligned centered row" ref="real">
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<h2 class="ui center aligned">
|
<h2 class="ui center aligned">
|
||||||
What is this really?
|
Why Does this App exist?
|
||||||
</h2>
|
</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>
|
<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.
|
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>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
|
|
||||||
|
|
||||||
<div class="ui segment" v-on:keyup.enter="submit">
|
<div class="ui segment">
|
||||||
|
|
||||||
<h4 class="ui header">
|
<h4 class="ui header">
|
||||||
<i class="plug icon"></i>
|
<i class="plug icon"></i>
|
||||||
@ -44,53 +44,10 @@
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
username: '',
|
|
||||||
password: ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit(){
|
|
||||||
|
|
||||||
//Both fields are required
|
|
||||||
if(this.username <= 0){
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if(this.password <= 0){
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let vm = this
|
|
||||||
|
|
||||||
let data = {
|
|
||||||
username: this.username,
|
|
||||||
password: this.password
|
|
||||||
}
|
|
||||||
|
|
||||||
axios.post('/api/user/login', data)
|
|
||||||
.then(response => {
|
|
||||||
if(response.data.success){
|
|
||||||
|
|
||||||
const token = response.data.token
|
|
||||||
const username = response.data.username
|
|
||||||
const masterKey = response.data.masterKey
|
|
||||||
|
|
||||||
this.$store.commit('setLoginToken', {token, username, masterKey})
|
|
||||||
|
|
||||||
//Setup socket io after user logs in
|
|
||||||
this.$io.emit('user_connect', token)
|
|
||||||
|
|
||||||
//Redirect user to notes section after login
|
|
||||||
this.$router.push('/notes')
|
|
||||||
} else {
|
|
||||||
// this.password = ''
|
|
||||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
|
||||||
vm.$store.commit('destroyLoginToken')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
30
client/src/pages/NotFoundPage.vue
Normal file
30
client/src/pages/NotFoundPage.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ui basic segment">
|
||||||
|
<div class="ui grid">
|
||||||
|
<div class="sixteen wide column">
|
||||||
|
<div class="ui text container">
|
||||||
|
|
||||||
|
<h2 class="ui dividing header">
|
||||||
|
Page Not Found
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'NotFoundPage',
|
||||||
|
props:[ 'message' ],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
items: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -8,26 +8,18 @@
|
|||||||
|
|
||||||
<div class="ui stackable grid">
|
<div class="ui stackable grid">
|
||||||
|
|
||||||
<div class="ten wide column"
|
<div class="six wide column" v-if="$store.getters.totals && $store.getters.totals['totalNotes']">
|
||||||
:class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
|
<search-input />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
|
||||||
|
|
||||||
<div class="ui basic button shrinking"
|
<div class="ui basic button shrinking"
|
||||||
v-on:click="updateFastFilters(3)"
|
v-on:click="updateFastFilters(3)"
|
||||||
v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)"
|
v-if="$store.getters.totals && ($store.getters.totals['youGotMailCount'] > 0)"
|
||||||
style="position: relative;">
|
style="position: relative;">
|
||||||
<i class="green mail icon"></i>Shared Notes
|
<i class="green mail icon"></i>Inbox
|
||||||
<span class="floating ui green label" v-if="$store.getters.totals['unreadNotes'] > 0">
|
+{{ $store.getters.totals['youGotMailCount'] }}
|
||||||
{{ $store.getters.totals['unreadNotes'] }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui basic button shrinking" 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 class="ui basic icon button shrinking" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['trashedNotes'] > 0">
|
|
||||||
<i class="trash alternate outline icon"></i>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tag-display
|
<tag-display
|
||||||
@ -42,12 +34,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="six wide column">
|
|
||||||
<search-input
|
|
||||||
v-on:tagClick="tagId => toggleTagFilter(tagId)"
|
|
||||||
v-if="$store.getters.totals && $store.getters.totals['totalNotes']" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="eight wide column" v-if="showClear">
|
<div class="eight wide column" v-if="showClear">
|
||||||
<!-- <fast-filters /> -->
|
<!-- <fast-filters /> -->
|
||||||
<span class="ui fluid green button" @click="reset">
|
<span class="ui fluid green button" @click="reset">
|
||||||
@ -75,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
|
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
|
||||||
<h2 >Trash
|
<h2>Trash
|
||||||
<span>({{ $store.getters.totals['trashedNotes'] }})</span>
|
<span>({{ $store.getters.totals['trashedNotes'] }})</span>
|
||||||
<div class="ui right floated basic button" data-tooltip="This doesn't work yet">
|
<div class="ui right floated basic button" data-tooltip="This doesn't work yet">
|
||||||
<i class="poo storm icon"></i>
|
<i class="poo storm icon"></i>
|
||||||
@ -88,6 +74,25 @@
|
|||||||
<h2>Shared Notes</h2>
|
<h2>Shared Notes</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="sixteen wide column" v-if="tagSuggestions.length > 0">
|
||||||
|
<h5 class="ui tiny dividing header"><i class="green tags icon"></i> Tags ({{ tagSuggestions.length }})</h5>
|
||||||
|
<div class="ui clickable green label" v-for="tag in tagSuggestions" v-on:click="tagId => toggleTagFilter(tag.id)">
|
||||||
|
<i class="tag icon"></i>
|
||||||
|
{{ tag.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- found attachments -->
|
||||||
|
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
|
||||||
|
<h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
|
||||||
|
<attachment-display
|
||||||
|
v-for="item in foundAttachments"
|
||||||
|
:item="item"
|
||||||
|
:key="item.id"
|
||||||
|
:search-params="{}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Note title card display -->
|
<!-- Note title card display -->
|
||||||
<div class="sixteen wide column">
|
<div class="sixteen wide column">
|
||||||
|
|
||||||
@ -123,18 +128,6 @@
|
|||||||
</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>
|
||||||
|
|
||||||
|
|
||||||
@ -170,7 +163,7 @@
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
initComponent: true,
|
initComponent: true,
|
||||||
commonTags: [],
|
tagSuggestions:[],
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
searchResultsCount: 0,
|
searchResultsCount: 0,
|
||||||
searchTags: [],
|
searchTags: [],
|
||||||
@ -191,13 +184,6 @@
|
|||||||
//Clear button is not visible
|
//Clear button is not visible
|
||||||
showClear: false,
|
showClear: false,
|
||||||
initialPostData: null,
|
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,
|
||||||
@ -214,8 +200,8 @@
|
|||||||
sectionData: {
|
sectionData: {
|
||||||
'pinned': ['thumbtack', 'Pinned'],
|
'pinned': ['thumbtack', 'Pinned'],
|
||||||
'archived': ['archive', 'Archived'],
|
'archived': ['archive', 'Archived'],
|
||||||
'shared': ['envelope outline', 'Received Notes'],
|
'shared': ['envelope outline', 'Inbox'],
|
||||||
'sent': ['paper plane outline', 'Shared Notes'],
|
'sent': ['paper plane outline', 'Sent Notes'],
|
||||||
'notes': ['file','Notes'],
|
'notes': ['file','Notes'],
|
||||||
'highlights': ['paragraph', 'Found In Text'],
|
'highlights': ['paragraph', 'Found In Text'],
|
||||||
'trashed': ['poop', 'Trashed Notes'],
|
'trashed': ['poop', 'Trashed Notes'],
|
||||||
@ -275,13 +261,12 @@
|
|||||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
|
|
||||||
//Close note event
|
//Close note event
|
||||||
this.$bus.$on('close_active_note', ({position, noteId, modified}) => {
|
this.$bus.$on('close_active_note', ({noteId, modified}) => {
|
||||||
|
|
||||||
this.closeNote()
|
this.closeNote()
|
||||||
if(modified){
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
//Focus and animate if modified
|
||||||
this.updateSingleNote(parseInt(noteId))
|
this.updateSingleNote(parseInt(noteId), modified)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$bus.$on('note_deleted', (noteId) => {
|
this.$bus.$on('note_deleted', (noteId) => {
|
||||||
@ -298,12 +283,9 @@
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$bus.$on('update_fast_filters', newFilter => {
|
this.$bus.$on('update_fast_filters', filterIndex => {
|
||||||
this.fastFilters = newFilter
|
|
||||||
//Fast filters always return all the results and tags
|
this.updateFastFilters(filterIndex)
|
||||||
this.search(true, this.batchSize, false).then( () => {
|
|
||||||
// return
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
//Event to update search from other areas
|
//Event to update search from other areas
|
||||||
@ -312,8 +294,18 @@
|
|||||||
this.search(true, this.batchSize)
|
this.search(true, this.batchSize)
|
||||||
.then( () => {
|
.then( () => {
|
||||||
|
|
||||||
console.log('Search attachments disabled for now')
|
this.searchAttachments()
|
||||||
// this.searchAttachments()
|
|
||||||
|
const postData = {
|
||||||
|
'tagText':this.searchTerm.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tagSuggestions = []
|
||||||
|
axios.post('/api/tag/suggest', postData)
|
||||||
|
.then( response => {
|
||||||
|
|
||||||
|
this.tagSuggestions = response.data
|
||||||
|
})
|
||||||
|
|
||||||
// return
|
// return
|
||||||
})
|
})
|
||||||
@ -365,6 +357,9 @@
|
|||||||
|
|
||||||
//Loads initial batch and tags
|
//Loads initial batch and tags
|
||||||
this.reset()
|
this.reset()
|
||||||
|
|
||||||
|
// this.search(true, this.firstLoadBatchSize, false)
|
||||||
|
// .then( r => this.search(false, this.batchSize, true))
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleTitleView(){
|
toggleTitleView(){
|
||||||
@ -390,7 +385,7 @@
|
|||||||
if(this.activeNoteId1 == null){
|
if(this.activeNoteId1 == null){
|
||||||
this.activeNoteId1 = id
|
this.activeNoteId1 = id
|
||||||
this.activeNote1Position = 0 //Middel of page
|
this.activeNote1Position = 0 //Middel of page
|
||||||
this.$router.push('/notes/open/'+this.activeNoteId1)
|
this.$router.push('/notes/open/'+this.activeNoteId1).catch(e => { console.log(e) })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -417,24 +412,18 @@
|
|||||||
clearTimeout(this.loadingBatchTimeout)
|
clearTimeout(this.loadingBatchTimeout)
|
||||||
this.loadingBatchTimeout = setTimeout(() => {
|
this.loadingBatchTimeout = setTimeout(() => {
|
||||||
|
|
||||||
//Distance to bottom of page
|
//Detect distance scrolled down the page
|
||||||
const bottomOfWindow =
|
const scrolledDown = window.pageYOffset + window.innerHeight
|
||||||
Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop)
|
//Get height of div to properly detect scroll distance down
|
||||||
+ window.innerHeight
|
const height = document.getElementById('app').scrollHeight
|
||||||
|
|
||||||
//height of page
|
//Load if less than 500px from the bottom
|
||||||
const offsetHeight = this.$refs.content.clientHeight
|
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled && !this.loadingInProgress){
|
||||||
|
|
||||||
//Determine percentage down the page
|
|
||||||
const percentageDown = Math.round( (bottomOfWindow/offsetHeight)*100 )
|
|
||||||
|
|
||||||
//If greater than 80 of the way down the page, load the next batch
|
|
||||||
if(percentageDown >= 65 && this.scrollLoadEnabled){
|
|
||||||
|
|
||||||
this.search(false, this.batchSize, true)
|
this.search(false, this.batchSize, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 50)
|
}, 30)
|
||||||
|
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -491,7 +480,10 @@
|
|||||||
noteId = parseInt(noteId)
|
noteId = parseInt(noteId)
|
||||||
|
|
||||||
//Find local note, if it exists; continue
|
//Find local note, if it exists; continue
|
||||||
|
let note = null
|
||||||
|
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
|
||||||
|
note = this.$refs['note-'+noteId][0].note
|
||||||
|
}
|
||||||
|
|
||||||
//Lookup one note using passed in ID
|
//Lookup one note using passed in ID
|
||||||
const postData = {
|
const postData = {
|
||||||
@ -508,60 +500,62 @@
|
|||||||
|
|
||||||
//Pull note data out of note set
|
//Pull note data out of note set
|
||||||
let newNote = results.data.notes[0]
|
let newNote = results.data.notes[0]
|
||||||
let foundNote = false
|
|
||||||
|
|
||||||
if(newNote === undefined){
|
if(newNote === undefined){
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//Find Just updated note and modify all its attributes
|
if(note && newNote){
|
||||||
Object.keys(this.noteSections).forEach(key => {
|
|
||||||
|
|
||||||
this.noteSections[key].forEach( (note,index) => {
|
//Don't move notes that were not changed
|
||||||
|
if(note.updated == newNote.updated){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if(note.id == noteId){
|
//go through each prop and update it with new values
|
||||||
foundNote = true
|
Object.keys(newNote).forEach(prop => {
|
||||||
|
note[prop] = newNote[prop]
|
||||||
|
})
|
||||||
|
|
||||||
//Don't move notes that were not changed
|
//Push new note to front if its modified
|
||||||
if(note.updated == newNote.updated){
|
if(focuseAndAnimate){
|
||||||
// return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Compare note tags, if they changed, reload tags
|
// Find note, in section, move to front
|
||||||
if(newNote.tag_count != note.tag_count){
|
Object.keys(this.noteSections).forEach( key => {
|
||||||
|
this.noteSections[key].forEach( (searchNote, index) => {
|
||||||
}
|
if(searchNote.id == noteId){
|
||||||
|
//Remove note from location and push to front
|
||||||
//go through each prop and update it with new values
|
this.noteSections[key].splice(index, 1)
|
||||||
Object.keys(newNote).forEach(prop => {
|
this.noteSections[key].unshift(note)
|
||||||
note[prop] = newNote[prop]
|
return
|
||||||
})
|
|
||||||
|
|
||||||
//Remove note from location and push to front
|
|
||||||
this.noteSections[key].splice(index, 1)
|
|
||||||
this.noteSections[key].unshift(note)
|
|
||||||
|
|
||||||
this.$nextTick( () => {
|
|
||||||
|
|
||||||
//Trigger close animation on note
|
|
||||||
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && focuseAndAnimate){
|
|
||||||
this.$refs['note-'+noteId][0].justClosed()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
return
|
this.$nextTick( () => {
|
||||||
}
|
//Trigger close animation on note
|
||||||
})
|
this.$refs['note-'+noteId][0].justClosed()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
//New notes don't exist in list, push them to the front
|
//New notes don't exist in list, push them to the front
|
||||||
if(!foundNote){
|
if(note == null){
|
||||||
this.noteSections.notes.unshift(newNote)
|
this.noteSections.notes.unshift(newNote)
|
||||||
|
//Trigger close animation on note
|
||||||
|
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
|
||||||
|
this.$refs['note-'+noteId][0].justClosed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Trigger section rebuild
|
//Trigger section rebuild
|
||||||
this.rebuildNoteCategorise()
|
this.rebuildNoteCategorise()
|
||||||
})
|
})
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
|
.catch(error => {
|
||||||
|
console.log(error)
|
||||||
|
this.$bus.$emit('notification', 'Failed to Update Note')
|
||||||
|
})
|
||||||
},
|
},
|
||||||
searchAttachments(){
|
searchAttachments(){
|
||||||
axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm})
|
axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm})
|
||||||
@ -570,13 +564,13 @@
|
|||||||
})
|
})
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Attachments') })
|
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Attachments') })
|
||||||
},
|
},
|
||||||
search(showLoading = true, notesInNextLoad = null, mergeExisting = false){
|
search(showLoading = true, notesInNextLoad = 10, mergeExisting = false){
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
//Don't double load note batches
|
//Don't double load note batches
|
||||||
if(this.loadingInProgress){
|
if(this.loadingInProgress){
|
||||||
console.log('Loading in progress, cancel operation')
|
console.log('Loading already in progress')
|
||||||
return resolve()
|
return resolve(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Reset a lot of stuff if we are not merging batches
|
//Reset a lot of stuff if we are not merging batches
|
||||||
@ -585,7 +579,6 @@
|
|||||||
this.noteSections[key] = []
|
this.noteSections[key] = []
|
||||||
})
|
})
|
||||||
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
|
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
|
|
||||||
}
|
}
|
||||||
this.searchResultsCount = 0
|
this.searchResultsCount = 0
|
||||||
|
|
||||||
@ -628,11 +621,6 @@
|
|||||||
//Enable or disable scroll loading
|
//Enable or disable scroll loading
|
||||||
this.scrollLoadEnabled = response.data.notes.length > 0
|
this.scrollLoadEnabled = response.data.notes.length > 0
|
||||||
|
|
||||||
//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
|
|
||||||
// }
|
|
||||||
|
|
||||||
if(response.data.total > 0){
|
if(response.data.total > 0){
|
||||||
this.searchResultsCount = response.data.total
|
this.searchResultsCount = response.data.total
|
||||||
}
|
}
|
||||||
@ -742,33 +730,22 @@
|
|||||||
this.scrollLoadEnabled = true
|
this.scrollLoadEnabled = true
|
||||||
this.searchTerm = ''
|
this.searchTerm = ''
|
||||||
this.searchTags = []
|
this.searchTags = []
|
||||||
|
this.tagSuggestions = []
|
||||||
this.fastFilters = {}
|
this.fastFilters = {}
|
||||||
this.updateFastFilters(5)
|
|
||||||
this.foundAttachments = [] //Remove all attachments
|
this.foundAttachments = [] //Remove all attachments
|
||||||
this.$bus.$emit('reset_fast_filters')
|
|
||||||
|
|
||||||
//Load initial batch, then tags, then other batch
|
this.$bus.$emit('reset_fast_filters')
|
||||||
this.search(true, this.firstLoadBatchSize)
|
this.updateFastFilters(5) //This loads notes
|
||||||
.then( () => {
|
|
||||||
//Load a larger batch once first batch has loaded
|
|
||||||
return this.search(false, this.batchSize, true)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
updateFastFilters(index){
|
updateFastFilters(index){
|
||||||
|
|
||||||
//clear out tags
|
//clear out tags
|
||||||
this.searchTags = []
|
this.searchTags = []
|
||||||
|
this.tagSuggestions = []
|
||||||
this.loadingInProgress = false
|
this.loadingInProgress = false
|
||||||
this.searchTerm = ''
|
this.searchTerm = ''
|
||||||
this.$bus.$emit('reset_fast_filters')
|
this.$bus.$emit('reset_fast_filters') //Clear out search
|
||||||
|
|
||||||
//A little hacky, brings user to notes page then filters on click
|
|
||||||
if(this.$route.name != 'Note Page'){
|
|
||||||
this.$router.push('/notes')
|
|
||||||
setTimeout( () => {
|
|
||||||
this.updateFastFilters(index)
|
|
||||||
}, 500 )
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
'withLinks', // 'Only Show Notes with Links'
|
'withLinks', // 'Only Show Notes with Links'
|
||||||
@ -782,7 +759,10 @@
|
|||||||
let filter = {}
|
let filter = {}
|
||||||
filter[options[index]] = 1
|
filter[options[index]] = 1
|
||||||
|
|
||||||
this.$bus.$emit('update_fast_filters', filter)
|
this.fastFilters = filter
|
||||||
|
//Fetch First batch of notes with new filter
|
||||||
|
this.search(true, this.firstLoadBatchSize, false)
|
||||||
|
.then( r => this.search(false, this.batchSize, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,11 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sixteen wide middle aligned column">
|
<div class="sixteen wide middle aligned column" v-if="quickNoteId > 0">
|
||||||
|
|
||||||
<div class="ui compact basic right floated button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
|
<div class="ui compact basic right floated button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
|
||||||
<i class="sync alternate reload icon"></i>
|
<i class="sync alternate reload icon"></i>
|
||||||
New Quick Note
|
New Scratch Pad
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="showNewNoteConfirm = false">
|
<div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="showNewNoteConfirm = false">
|
||||||
<i class="close icon"></i>
|
<i class="close icon"></i>
|
||||||
|
@ -1,9 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ui basic segment">
|
<div class="ui grid">
|
||||||
<div class="ui container">
|
|
||||||
<div class="fun" :style="{'color':color}" v-if="noteText" v-html="noteText"></div>
|
<div class="sixteen wide column"></div>
|
||||||
|
|
||||||
|
<div class="sixteen wide column" v-if="text.length > 0 || title.length > 0">
|
||||||
|
<div class="ui text container squire-box" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
|
||||||
|
|
||||||
|
<h1 v-if="title">{{title}}</h1>
|
||||||
|
|
||||||
|
<div v-if="text" v-html="text"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui basic segment"></div>
|
|
||||||
|
<div class="sixteen wide column" v-if="!$store.getters.getLoggedIn">
|
||||||
|
<div class="ui text container">
|
||||||
|
<h2 class="ui header">
|
||||||
|
<img class="small-logo" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
|
||||||
|
<div class="content">
|
||||||
|
Solid Scribe is an easy, free, secure Note App
|
||||||
|
<div class="sub header">
|
||||||
|
Encrypted notes, only readable by you. Unless you share them.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
<div class="ui grid">
|
||||||
|
<div class="eight wide center aligned column">
|
||||||
|
<router-link class="ui compact green button" to="/login">
|
||||||
|
<i class="plug icon"></i>Sign Up
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="eight wide center aligned column">
|
||||||
|
<router-link class="ui compact green button" to="/">
|
||||||
|
<i class="comment outline icon"></i>
|
||||||
|
Learn More
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui sixteen wide center aligned column">
|
||||||
|
<h4>{{ failText }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -15,36 +55,51 @@
|
|||||||
name: 'SharePage',
|
name: 'SharePage',
|
||||||
data(){
|
data(){
|
||||||
return {
|
return {
|
||||||
noteText: null,
|
title: '',
|
||||||
color: '#000'
|
text: '',
|
||||||
|
failText: '',
|
||||||
|
styleObject:{},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount(){
|
beforeMount(){
|
||||||
//Mount notes on load if note ID is set
|
|
||||||
if(this.$route.params && this.$route.params.id){
|
//You can put something here for live updates
|
||||||
const id = this.$route.params.id
|
// this.$io.on
|
||||||
this.openNote(id)
|
|
||||||
}
|
this.openNote()
|
||||||
|
|
||||||
},
|
},
|
||||||
methods:{
|
methods:{
|
||||||
openNote(noteId){
|
fail(){
|
||||||
axios.post('/api/public/note', {'noteId': noteId})
|
this.failText = 'Failed to open Shared Note'
|
||||||
.then( response => {
|
this.$bus.$emit('notification', 'Failed to Open Shared Note')
|
||||||
|
},
|
||||||
|
openNote(){
|
||||||
|
|
||||||
let colors = JSON.parse(response.data.color)
|
const noteId = this.$route.params.id
|
||||||
|
const sharedKey = this.$route.params.token
|
||||||
|
|
||||||
if(colors && colors.noteBackground){
|
axios.post('/api/public/opensharednote', {noteId, sharedKey})
|
||||||
document.body.style.background = colors.noteBackground
|
.then( ({data}) => {
|
||||||
|
|
||||||
|
if(data.success){
|
||||||
|
this.title = data.title
|
||||||
|
this.text = data.text
|
||||||
|
this.styleObject = data.styleObject
|
||||||
|
} else {
|
||||||
|
this.fail()
|
||||||
}
|
}
|
||||||
if(colors && colors.noteText){
|
|
||||||
this.color = colors.noteText
|
|
||||||
}
|
|
||||||
|
|
||||||
this.noteText = response.data.text
|
|
||||||
})
|
})
|
||||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Open Public Note') })
|
.catch(error => { this.fail() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style type="text/css" scoped>
|
||||||
|
.small-logo {
|
||||||
|
width: 30px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
@ -10,6 +10,7 @@ const SharePage = () => import(/* webpackChunkName: "SharePage" */ '@/pages/Shar
|
|||||||
const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage')
|
const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage')
|
||||||
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
|
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
|
||||||
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
|
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
|
||||||
|
const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
|
||||||
|
|
||||||
Vue.use(Router)
|
Vue.use(Router)
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ export default new Router({
|
|||||||
component: HelpPage
|
component: HelpPage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/share/:id',
|
path: '/public/note/:id/:token',
|
||||||
name: 'Share',
|
name: 'Share',
|
||||||
meta: {title:'Shared'},
|
meta: {title:'Shared'},
|
||||||
component: SharePage
|
component: SharePage
|
||||||
@ -81,5 +82,11 @@ export default new Router({
|
|||||||
meta: {title:'Attachments by Type'},
|
meta: {title:'Attachments by Type'},
|
||||||
component: AttachmentsPage
|
component: AttachmentsPage
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
name: 'Page Not Found',
|
||||||
|
meta: {title:'404 Page Not Found'},
|
||||||
|
component: NotFoundPage
|
||||||
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -45,34 +45,36 @@ export default new Vuex.Store({
|
|||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
'white':{
|
'white':{
|
||||||
'background_color': '#fff',
|
'body_bg_color': '#f5f6f7',
|
||||||
|
'small_element_bg_color': '#fff',
|
||||||
'text_color': '#3d3d3d',
|
'text_color': '#3d3d3d',
|
||||||
'outline_color': 'rgba(34,36,38,0.15)',
|
'dark_border_color': '#DFE1E6',
|
||||||
'border_color': 'rgba(34,36,38,0.20)',
|
'border_color': '#DFE1E6',
|
||||||
'menu-accent': '#cecece',
|
'menu-accent': '#cecece',
|
||||||
'menu-text': '#5e6268',
|
'menu-text': '#5e6268',
|
||||||
},
|
},
|
||||||
'black':{
|
'black':{
|
||||||
'background_color': '#000',
|
'body_bg_color': '#000',
|
||||||
|
'small_element_bg_color': '#000',
|
||||||
'text_color': '#FFF',
|
'text_color': '#FFF',
|
||||||
'outline_color': '#FFF',
|
'dark_border_color': '#ACACAC', //Lighter color to accent elemnts user can interact with
|
||||||
'border_color': 'rgba(255, 255, 255, 0.70)',
|
'border_color': '#555',
|
||||||
'menu-accent': '#626262',
|
'menu-accent': '#626262',
|
||||||
'menu-text': '#d9d9d9',
|
'menu-text': '#d9d9d9',
|
||||||
},
|
},
|
||||||
'night':{
|
'night':{
|
||||||
'background_color': '#000',
|
'body_bg_color': '#000',
|
||||||
|
'small_element_bg_color': '#000',
|
||||||
'text_color': '#a98457',
|
'text_color': '#a98457',
|
||||||
'outline_color': '#a98457',
|
'dark_border_color': '#a98457',
|
||||||
'border_color': 'rgba(255, 255, 255, 0.31)',
|
'border_color': '#555',
|
||||||
'menu-accent': '#626262',
|
'menu-accent': '#626262',
|
||||||
'menu-text': '#d9d9d9',
|
'menu-text': '#a69682',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
//Catch values not in set
|
//Catch values not in set
|
||||||
|
|
||||||
|
|
||||||
const totalThemes = Object.keys(themes).length
|
const totalThemes = Object.keys(themes).length
|
||||||
state.nightMode++
|
state.nightMode++
|
||||||
if(state.nightMode > totalThemes-1){
|
if(state.nightMode > totalThemes-1){
|
||||||
|
110
server/index.js
110
server/index.js
@ -40,11 +40,10 @@ var io = require('socket.io')(http, {
|
|||||||
//Set socket IO as a global in the app
|
//Set socket IO as a global in the app
|
||||||
global.SocketIo = io
|
global.SocketIo = io
|
||||||
|
|
||||||
|
let noteDiffs = {}
|
||||||
|
|
||||||
io.on('connection', function(socket){
|
io.on('connection', function(socket){
|
||||||
|
|
||||||
// console.log('New user ', socket.id)
|
|
||||||
|
|
||||||
//When a user connects, add them to their own room
|
//When a user connects, add them to their own room
|
||||||
// This allows the server to emit events to that specific user
|
// This allows the server to emit events to that specific user
|
||||||
// access socket.io in the controller with SocketIo global
|
// access socket.io in the controller with SocketIo global
|
||||||
@ -58,14 +57,43 @@ io.on('connection', function(socket){
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('join_room', roomId => {
|
socket.on('join_room', rawTextId => {
|
||||||
// console.log('Join room ', roomId)
|
// Join user to rawtextid room when they enter
|
||||||
socket.join(roomId)
|
socket.join(rawTextId)
|
||||||
|
|
||||||
const usersInRoom = io.sockets.adapter.rooms[roomId]
|
//If there are past diffs for this note, send them to the user
|
||||||
|
if(noteDiffs[rawTextId] != undefined){
|
||||||
|
|
||||||
|
//Sort all note diffs by when they were created.
|
||||||
|
noteDiffs[rawTextId].sort((a,b) => { return a.time - b.time })
|
||||||
|
|
||||||
|
//Emit all sorted diffs to user
|
||||||
|
socket.emit('past_diffs', noteDiffs[rawTextId])
|
||||||
|
} else {
|
||||||
|
socket.emit('past_diffs', null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersInRoom = io.sockets.adapter.rooms[rawTextId]
|
||||||
if(usersInRoom){
|
if(usersInRoom){
|
||||||
// console.log('Users in room', usersInRoom.length)
|
//Update users in room count
|
||||||
io.to(roomId).emit('update_user_count', usersInRoom.length)
|
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
|
||||||
|
|
||||||
|
//Debugging text
|
||||||
|
console.log('Note diff object')
|
||||||
|
console.log(noteDiffs)
|
||||||
|
|
||||||
|
|
||||||
|
let noteDiffKeys = Object.keys(noteDiffs)
|
||||||
|
let totalDiffs = 0
|
||||||
|
noteDiffKeys.forEach(diffSetKey => {
|
||||||
|
if(noteDiffs[diffSetKey]){
|
||||||
|
totalDiffs += noteDiffs[diffSetKey].length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Total notes in limbo -> ', noteDiffKeys.length)
|
||||||
|
console.log('Total Diffs for all notes -> ', totalDiffs)
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -83,13 +111,41 @@ io.on('connection', function(socket){
|
|||||||
|
|
||||||
socket.on('note_diff', data => {
|
socket.on('note_diff', data => {
|
||||||
|
|
||||||
|
//Log each diff for note
|
||||||
|
const noteId = data.id
|
||||||
|
delete data.id
|
||||||
|
if(noteDiffs[noteId] == undefined){ noteDiffs[noteId] = [] }
|
||||||
|
data.time = +new Date
|
||||||
|
|
||||||
|
noteDiffs[noteId].push(data)
|
||||||
|
|
||||||
|
//Remove duplicate diffs if they exist
|
||||||
|
for (var i = noteDiffs[noteId].length - 1; i >= 0; i--) {
|
||||||
|
|
||||||
|
let pastDiff = noteDiffs[noteId][i]
|
||||||
|
|
||||||
|
for (var j = noteDiffs[noteId].length - 1; j >= 0; j--) {
|
||||||
|
let currentDiff = noteDiffs[noteId][j]
|
||||||
|
|
||||||
|
if(i == j){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if(currentDiff.diff == pastDiff.diff || currentDiff.time == pastDiff.time){
|
||||||
|
console.log('Removing Duplicate')
|
||||||
|
noteDiffs[noteId].splice(i,1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Each user joins a room when they open the app.
|
//Each user joins a room when they open the app.
|
||||||
io.in(data.id).clients((error, clients) => {
|
io.in(noteId).clients((error, clients) => {
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
//Go through each client in note room and send them the diff
|
//Go through each client in note room and send them the diff
|
||||||
clients.forEach(socketId => {
|
clients.forEach(socketId => {
|
||||||
if(socketId != socket.id){
|
if(socketId != socket.id){
|
||||||
io.to(socketId).emit('incoming_diff', data.diff)
|
io.to(socketId).emit('incoming_diff', data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -97,6 +153,38 @@ io.on('connection', function(socket){
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
socket.on('truncate_diffs_at_save', checkpoint => {
|
||||||
|
|
||||||
|
let diffSet = noteDiffs[checkpoint.rawTextId]
|
||||||
|
if(diffSet && diffSet.length > 0){
|
||||||
|
|
||||||
|
//Make sure all diffs are sorted before cleaning
|
||||||
|
noteDiffs[checkpoint.rawTextId].sort((a,b) => { return a.time - b.time })
|
||||||
|
|
||||||
|
// Remove all diffs until it reaches the current hash
|
||||||
|
let sliceTo = 0
|
||||||
|
for (var i = 0; i < diffSet.length; i++) {
|
||||||
|
if(diffSet[i].hash == checkpoint){
|
||||||
|
sliceTo = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo)
|
||||||
|
|
||||||
|
if(noteDiffs[checkpoint.rawTextId].length == 0){
|
||||||
|
delete noteDiffs[checkpoint.rawTextId]
|
||||||
|
}
|
||||||
|
//Debugging
|
||||||
|
else {
|
||||||
|
console.log('Diffset after save')
|
||||||
|
console.log(noteDiffs[checkpoint.rawTextId])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
socket.on('disconnect', function(){
|
socket.on('disconnect', function(){
|
||||||
// console.log('user disconnected');
|
// console.log('user disconnected');
|
||||||
});
|
});
|
||||||
@ -139,7 +227,7 @@ app.use(function(req, res, next){
|
|||||||
const printResults = false
|
const printResults = false
|
||||||
let UserTest = require('@models/User')
|
let UserTest = require('@models/User')
|
||||||
let NoteTest = require('@models/Note')
|
let NoteTest = require('@models/Note')
|
||||||
UserTest.keyPairTest('genMan2', '1', printResults)
|
UserTest.keyPairTest('genMan12', '1', printResults)
|
||||||
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
|
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
|
||||||
.then( message => {
|
.then( message => {
|
||||||
if(printResults) console.log(message)
|
if(printResults) console.log(message)
|
||||||
|
@ -280,9 +280,7 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
|
|||||||
Attachment.scrapeUrlsCreateAttachments(userId, noteId, foundUrls).then( freshlyScrapedText => {
|
Attachment.scrapeUrlsCreateAttachments(userId, noteId, foundUrls).then( freshlyScrapedText => {
|
||||||
|
|
||||||
//Once everything is done being scraped, emit new attachment events
|
//Once everything is done being scraped, emit new attachment events
|
||||||
if(io){
|
SocketIo.to(userId).emit('update_counts')
|
||||||
io.to(userId).emit('update_counts')
|
|
||||||
}
|
|
||||||
|
|
||||||
solrAttachmentText += freshlyScrapedText
|
solrAttachmentText += freshlyScrapedText
|
||||||
resolve(solrAttachmentText)
|
resolve(solrAttachmentText)
|
||||||
|
@ -72,7 +72,7 @@ Note.test = (userId, masterKey, printResults) => {
|
|||||||
if(printResults) console.log('Test: Normal Note Search Index - Pass')
|
if(printResults) console.log('Test: Normal Note Search Index - Pass')
|
||||||
} else { console.log('Test: Search Index - Fail') }
|
} else { console.log('Test: Search Index - Fail') }
|
||||||
|
|
||||||
return ShareNote.migrateNoteToShared(userId, testNoteId, shareUserId, masterKey)
|
return ShareNote.addUserToSharedNote(userId, testNoteId, shareUserId, masterKey)
|
||||||
})
|
})
|
||||||
.then(({success, shareUserId, sharedUserNoteId}) => {
|
.then(({success, shareUserId, sharedUserNoteId}) => {
|
||||||
|
|
||||||
@ -134,13 +134,13 @@ Note.test = (userId, masterKey, printResults) => {
|
|||||||
|
|
||||||
if(printResults) console.log('Test: Delete Shared Note - Pass')
|
if(printResults) console.log('Test: Delete Shared Note - Pass')
|
||||||
|
|
||||||
return ShareNote.migrateNoteToShared(userId, testNoteId2, shareUserId, masterKey)
|
return ShareNote.addUserToSharedNote(userId, testNoteId2, shareUserId, masterKey)
|
||||||
})
|
})
|
||||||
.then(({success, shareUserId, sharedUserNoteId}) => {
|
.then(({success, shareUserId, sharedUserNoteId}) => {
|
||||||
|
|
||||||
if(printResults) console.log('Test: Created Another New Shared Note - pass')
|
if(printResults) console.log('Test: Created Another New Shared Note - pass')
|
||||||
|
|
||||||
return ShareNote.removeUserFromShared(userId, testNoteId2, sharedUserNoteId, masterKey)
|
return ShareNote.removeUserFromSharedNote(userId, testNoteId2, sharedUserNoteId, masterKey)
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
||||||
@ -162,75 +162,6 @@ Note.test = (userId, masterKey, printResults) => {
|
|||||||
return resolve('Test: Complete ---')
|
return resolve('Test: Complete ---')
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//User doesn't have an encrypted note set. Encrypt all notes
|
|
||||||
Note.encryptEveryNote = (userId, masterKey) => {
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
//Select all the user notes
|
|
||||||
db.promise().query(`
|
|
||||||
SELECT * FROM note
|
|
||||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
|
||||||
WHERE salt IS NULL AND user_id = ? AND shared = 0`, [userId])
|
|
||||||
.then((rows, fields) => {
|
|
||||||
|
|
||||||
let foundNotes = rows[0]
|
|
||||||
console.log('Encrypting user notes ',rows[0].length)
|
|
||||||
|
|
||||||
// return resolve(true)
|
|
||||||
|
|
||||||
let allTheUpdates = []
|
|
||||||
let timeoutAdder = 0
|
|
||||||
foundNotes.forEach(note => {
|
|
||||||
timeoutAdder += 100
|
|
||||||
const newUpdate = new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('Encrypting Note ', note.id)
|
|
||||||
|
|
||||||
const created = Math.round((+new Date)/1000)
|
|
||||||
const salt = cs.createSmallSalt()
|
|
||||||
|
|
||||||
const noteText = note.text
|
|
||||||
const noteTitle = note.title
|
|
||||||
|
|
||||||
const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
|
|
||||||
const noteSnippet = cs.encrypt(masterKey, salt, snippet)
|
|
||||||
|
|
||||||
const textObject = JSON.stringify([noteTitle, noteText])
|
|
||||||
const encryptedText = cs.encrypt(masterKey, salt, textObject)
|
|
||||||
|
|
||||||
db.promise()
|
|
||||||
.query('UPDATE note_raw_text SET title = ?, text = ?, snippet = ?, salt = ? WHERE id = ?',
|
|
||||||
[null, encryptedText, noteSnippet, salt, note.note_raw_text_id])
|
|
||||||
.then(() => {
|
|
||||||
resolve(true)
|
|
||||||
})
|
|
||||||
}, timeoutAdder)
|
|
||||||
})
|
|
||||||
allTheUpdates.push(newUpdate)
|
|
||||||
})
|
|
||||||
|
|
||||||
Promise.all(allTheUpdates).then(done => {
|
|
||||||
|
|
||||||
console.log('Indexing first 100')
|
|
||||||
return Note.reindex(userId, masterKey)
|
|
||||||
|
|
||||||
}).then(results => {
|
|
||||||
|
|
||||||
console.log('Done')
|
|
||||||
resolve(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,7 +421,8 @@ Note.reindex = (userId, masterKey, removeId = null) => {
|
|||||||
Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, hash, masterKey) => {
|
Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, hash, masterKey) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
const now = Math.round((+new Date)/1000)
|
// const now = Math.round((+new Date)/1000)
|
||||||
|
const now = +new Date
|
||||||
|
|
||||||
let noteSnippet = ''
|
let noteSnippet = ''
|
||||||
|
|
||||||
@ -691,10 +623,6 @@ Note.delete = (userId, noteId, masterKey = null) => {
|
|||||||
// delete tags
|
// delete tags
|
||||||
return db.promise()
|
return db.promise()
|
||||||
.query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
|
.query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
|
||||||
})
|
|
||||||
.then((rows, fields) => {
|
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
@ -706,6 +634,8 @@ Note.delete = (userId, noteId, masterKey = null) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
SocketIo.to(userId).emit('update_counts')
|
||||||
|
|
||||||
if(masterKey){
|
if(masterKey){
|
||||||
//Remove note ID from index
|
//Remove note ID from index
|
||||||
Note.reindex(userId, masterKey, [noteId])
|
Note.reindex(userId, masterKey, [noteId])
|
||||||
@ -803,6 +733,7 @@ Note.get = (userId, noteId, masterKey) => {
|
|||||||
note.archived,
|
note.archived,
|
||||||
note.trashed,
|
note.trashed,
|
||||||
note.color,
|
note.color,
|
||||||
|
note.shared,
|
||||||
note.encrypted_share_password_key,
|
note.encrypted_share_password_key,
|
||||||
count(distinct attachment.id) as attachment_count,
|
count(distinct attachment.id) as attachment_count,
|
||||||
note.note_raw_text_id as rawTextId,
|
note.note_raw_text_id as rawTextId,
|
||||||
@ -845,8 +776,8 @@ Note.get = (userId, noteId, masterKey) => {
|
|||||||
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
|
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
|
||||||
|
|
||||||
//Return note data
|
//Return note data
|
||||||
delete noteData.salt //remove salt from return data
|
// delete noteData.salt //remove salt from return data
|
||||||
delete noteData.encrypted_share_password_key
|
// delete noteData.encrypted_share_password_key
|
||||||
noteData.lockedOut = noteLockedOut
|
noteData.lockedOut = noteLockedOut
|
||||||
resolve(noteData)
|
resolve(noteData)
|
||||||
|
|
||||||
@ -859,17 +790,48 @@ Note.get = (userId, noteId, masterKey) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Public note share action -> may not be used
|
//Public note share action -> may not be used
|
||||||
Note.getShared = (noteId) => {
|
Note.getShared = (noteId, sharedKey) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.promise()
|
db.promise()
|
||||||
.query('SELECT text, color FROM note WHERE id = ? AND shared = 1 LIMIT 1', [noteId])
|
.query(`
|
||||||
|
SELECT
|
||||||
|
note_raw_text.text,
|
||||||
|
note_raw_text.salt,
|
||||||
|
note_raw_text.updated as updated,
|
||||||
|
note.id,
|
||||||
|
note.user_id,
|
||||||
|
note.created,
|
||||||
|
note.pinned,
|
||||||
|
note.archived,
|
||||||
|
note.trashed,
|
||||||
|
note.color,
|
||||||
|
note.shared,
|
||||||
|
note.encrypted_share_password_key,
|
||||||
|
note.note_raw_text_id as rawTextId
|
||||||
|
FROM note
|
||||||
|
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||||
|
WHERE note.id = ? LIMIT 1`, [noteId])
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
//Return note data
|
let noteData = rows[0][0]
|
||||||
resolve(rows[0][0])
|
|
||||||
|
const decipheredText = cs.decrypt(sharedKey, noteData.salt, noteData.text)
|
||||||
|
if(decipheredText == null){
|
||||||
|
throw new Error('Unable to decropt note text')
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = true
|
||||||
|
const noteObject = JSON.parse(decipheredText)
|
||||||
|
const title = noteObject[0]
|
||||||
|
const text = noteObject[1]
|
||||||
|
const styleObject = JSON.parse(noteData.color)
|
||||||
|
|
||||||
|
return resolve({success, title, text, styleObject})
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(console.log)
|
.catch(error => {
|
||||||
|
resolve({'success':false})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,19 +12,19 @@ let ShareNote = module.exports = {}
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const cs = require('@helpers/CryptoString')
|
const cs = require('@helpers/CryptoString')
|
||||||
|
|
||||||
ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
|
ShareNote.addUserToSharedNote = (userId, noteId, shareUserId, masterKey) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
const Note = require('@models/Note')
|
const Note = require('@models/Note')
|
||||||
const User = require('@models/User')
|
const User = require('@models/User')
|
||||||
|
|
||||||
//generate new random salts and password
|
//generate new random salts and password
|
||||||
const sharedNoteMasterKey = cs.createSmallSalt()
|
let sharedNoteMasterKey = null
|
||||||
|
|
||||||
let encryptedSharedKey = null //new key for note encrypted with shared users pubic key
|
let encryptedSharedKey = null //new key for note encrypted with shared users pubic key
|
||||||
|
|
||||||
//Current note object
|
//Current note object
|
||||||
let note = null
|
let noteObject = null
|
||||||
let publicKey = null
|
let publicKey = null
|
||||||
|
|
||||||
db.promise().query('SELECT id FROM user WHERE id = ?', [shareUserId])
|
db.promise().query('SELECT id FROM user WHERE id = ?', [shareUserId])
|
||||||
@ -34,20 +34,16 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
|
|||||||
throw new Error('User Does Not Exist')
|
throw new Error('User Does Not Exist')
|
||||||
}
|
}
|
||||||
|
|
||||||
return Note.get(userId, noteId, masterKey)
|
return ShareNote.migrateToShared(userId, noteId, masterKey)
|
||||||
|
|
||||||
})
|
})
|
||||||
.then( noteObject => {
|
.then(({note, sharedNoteKey})=> {
|
||||||
|
|
||||||
if(!noteObject){
|
sharedNoteMasterKey = sharedNoteKey
|
||||||
throw new Error('Note Not Found')
|
noteObject = note
|
||||||
}
|
|
||||||
|
|
||||||
note = noteObject
|
|
||||||
|
|
||||||
return db.promise()
|
return db.promise()
|
||||||
.query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, note.rawTextId])
|
.query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, note.rawTextId])
|
||||||
|
|
||||||
})
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
@ -55,46 +51,6 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
|
|||||||
throw new Error('User Already has this note shared with them')
|
throw new Error('User Already has this note shared with them')
|
||||||
}
|
}
|
||||||
|
|
||||||
//All check pass, proceed with sharing note
|
|
||||||
return User.getPublicKey(userId)
|
|
||||||
})
|
|
||||||
.then( userPublicKey => {
|
|
||||||
|
|
||||||
//Get users public key
|
|
||||||
publicKey = userPublicKey
|
|
||||||
|
|
||||||
//
|
|
||||||
// Modify note to have a shared password, encrypt text with this password
|
|
||||||
//
|
|
||||||
const sharedNoteSalt = cs.createSmallSalt()
|
|
||||||
|
|
||||||
//Encrypt note text with new password
|
|
||||||
const textObject = JSON.stringify([note.title, note.text])
|
|
||||||
const encryptedText = cs.encrypt(sharedNoteMasterKey, sharedNoteSalt, textObject)
|
|
||||||
|
|
||||||
//Update note raw text with new data
|
|
||||||
return db.promise()
|
|
||||||
.query("UPDATE `application`.`note_raw_text` SET `text` = ?, `salt` = ? WHERE (`id` = ?)",
|
|
||||||
[encryptedText, sharedNoteSalt, note.rawTextId])
|
|
||||||
|
|
||||||
})
|
|
||||||
.then((rows, fields) => {
|
|
||||||
|
|
||||||
//New Encrypted snippet, using new shared password
|
|
||||||
const sharedNoteSnippetSalt = cs.createSmallSalt()
|
|
||||||
const snippet = JSON.stringify([note.title, note.text.substring(0, 500)])
|
|
||||||
const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, sharedNoteSnippetSalt, snippet)
|
|
||||||
|
|
||||||
//Encrypt shared password for this user
|
|
||||||
const encryptedSharedKey = crypto.publicEncrypt(publicKey, Buffer.from(sharedNoteMasterKey, 'utf8')).toString('base64')
|
|
||||||
|
|
||||||
//Update note snippet for current user with public key encoded snippet
|
|
||||||
return db.promise().query('UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 2 WHERE id = ? AND user_id = ?',
|
|
||||||
[encryptedSnippet, sharedNoteSnippetSalt, encryptedSharedKey, noteId, userId])
|
|
||||||
|
|
||||||
})
|
|
||||||
.then((rows, fields) => {
|
|
||||||
|
|
||||||
return User.getPublicKey(shareUserId)
|
return User.getPublicKey(shareUserId)
|
||||||
|
|
||||||
})
|
})
|
||||||
@ -102,7 +58,7 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
|
|||||||
|
|
||||||
//New Encrypted snippet, using new shared password
|
//New Encrypted snippet, using new shared password
|
||||||
const newSnippetSalt = cs.createSmallSalt()
|
const newSnippetSalt = cs.createSmallSalt()
|
||||||
const snippet = JSON.stringify([note.title, note.text.substring(0, 500)])
|
const snippet = JSON.stringify([noteObject.title, noteObject.text.substring(0, 500)])
|
||||||
const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, newSnippetSalt, snippet)
|
const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, newSnippetSalt, snippet)
|
||||||
|
|
||||||
//Encrypt shared password for this user
|
//Encrypt shared password for this user
|
||||||
@ -111,7 +67,7 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
|
|||||||
//Insert new note for shared user
|
//Insert new note for shared user
|
||||||
return db.promise().query(`
|
return db.promise().query(`
|
||||||
INSERT INTO note (user_id, note_raw_text_id, created, color, share_user_id, snippet, snippet_salt, encrypted_share_password_key) VALUES (?,?,?,?,?,?,?,?);
|
INSERT INTO note (user_id, note_raw_text_id, created, color, share_user_id, snippet, snippet_salt, encrypted_share_password_key) VALUES (?,?,?,?,?,?,?,?);
|
||||||
`, [shareUserId, note.rawTextId, note.created, note.color, userId, encryptedSnippet, newSnippetSalt, encryptedSharedKey])
|
`, [shareUserId, noteObject.rawTextId, noteObject.created, noteObject.color, userId, encryptedSnippet, newSnippetSalt, encryptedSharedKey])
|
||||||
|
|
||||||
})
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
@ -119,7 +75,7 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
|
|||||||
const sharedUserNoteId = rows[0]['insertId']
|
const sharedUserNoteId = rows[0]['insertId']
|
||||||
|
|
||||||
//Emit update count event to user shared with - so they see the note in real time
|
//Emit update count event to user shared with - so they see the note in real time
|
||||||
SocketIo.to(sharedUserNoteId).emit('update_counts')
|
SocketIo.to(shareUserId).emit('update_counts')
|
||||||
|
|
||||||
let success = true
|
let success = true
|
||||||
return resolve({success, shareUserId, sharedUserNoteId})
|
return resolve({success, shareUserId, sharedUserNoteId})
|
||||||
@ -133,7 +89,7 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ShareNote.removeUserFromShared = (userId, noteId, shareNoteUserId, masterKey) => {
|
ShareNote.removeUserFromSharedNote = (userId, noteId, shareNoteUserId, masterKey) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
let rawTextId = null
|
let rawTextId = null
|
||||||
@ -160,32 +116,9 @@ ShareNote.removeUserFromShared = (userId, noteId, shareNoteUserId, masterKey) =>
|
|||||||
//Convert back to normal note if there is only one person with this note
|
//Convert back to normal note if there is only one person with this note
|
||||||
if(rows[0][0]['count'] == 1){
|
if(rows[0][0]['count'] == 1){
|
||||||
|
|
||||||
Note.get(userId, noteId, masterKey)
|
return ShareNote.migrateToNormal(userId, noteId, masterKey)
|
||||||
.then(noteObject => {
|
.then(results => {
|
||||||
|
resolve(true)
|
||||||
const salt = cs.createSmallSalt()
|
|
||||||
const snippetSalt = cs.createSmallSalt()
|
|
||||||
|
|
||||||
const snippetObj = JSON.stringify([noteObject.title, noteObject.text.substring(0, 500)])
|
|
||||||
const snippet = cs.encrypt(masterKey, snippetSalt, snippetObj)
|
|
||||||
|
|
||||||
const textObject = JSON.stringify([noteObject.title, noteObject.text])
|
|
||||||
const encryptedText = cs.encrypt(masterKey, salt, textObject)
|
|
||||||
|
|
||||||
db.promise()
|
|
||||||
.query(`UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 0 WHERE id = ? AND user_id = ?`,
|
|
||||||
[snippet, snippetSalt, null, noteId, userId])
|
|
||||||
.then((r,f) => {
|
|
||||||
|
|
||||||
db.promise()
|
|
||||||
.query('UPDATE note_raw_text SET text = ?, salt = ? WHERE id = ?',
|
|
||||||
[encryptedText, salt, noteObject.rawTextId])
|
|
||||||
.then(() => {
|
|
||||||
return resolve(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@ -196,9 +129,163 @@ ShareNote.removeUserFromShared = (userId, noteId, shareNoteUserId, masterKey) =>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get users who see a shared note
|
//Encrypt note with private shared key
|
||||||
ShareNote.getUsers = (userId, rawTextId) => {
|
ShareNote.migrateToShared = (userId, noteId, masterKey) => {
|
||||||
|
|
||||||
|
const User = require('@models/User')
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
//generate new random password
|
||||||
|
const sharedNoteMasterKey = cs.createSmallSalt()
|
||||||
|
let userPublicKey = null
|
||||||
|
let userPrivateKey = null
|
||||||
|
let note = null
|
||||||
|
|
||||||
|
User.generateKeypair(userId, masterKey)
|
||||||
|
.then( ({publicKey, privateKey}) => {
|
||||||
|
|
||||||
|
//Get users public key
|
||||||
|
userPublicKey = publicKey
|
||||||
|
userPrivateKey = privateKey
|
||||||
|
|
||||||
|
return Note.get(userId, noteId, masterKey)
|
||||||
|
})
|
||||||
|
.then(noteObject => {
|
||||||
|
|
||||||
|
note = noteObject
|
||||||
|
|
||||||
|
if(note.shared == 2){
|
||||||
|
|
||||||
|
//Note is already shared, decrypt sharedKey
|
||||||
|
let sharedNoteKey = null
|
||||||
|
const encryptedShareKey = note.encrypted_share_password_key
|
||||||
|
if(encryptedShareKey != null){
|
||||||
|
sharedNoteKey = crypto.privateDecrypt(userPrivateKey,
|
||||||
|
Buffer.from(encryptedShareKey, 'base64') )
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve({note, sharedNoteKey})
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Update raw_text to have a shared password, encrypt text with this password
|
||||||
|
//
|
||||||
|
const sharedNoteSalt = cs.createSmallSalt()
|
||||||
|
|
||||||
|
//Encrypt note text with new password
|
||||||
|
const textObject = JSON.stringify([note.title, note.text])
|
||||||
|
const encryptedText = cs.encrypt(sharedNoteMasterKey, sharedNoteSalt, textObject)
|
||||||
|
|
||||||
|
//Update note raw text with new data
|
||||||
|
db.promise()
|
||||||
|
.query("UPDATE `application`.`note_raw_text` SET `text` = ?, `salt` = ? WHERE (`id` = ?)",
|
||||||
|
[encryptedText, sharedNoteSalt, note.rawTextId])
|
||||||
|
.then((rows, fields) => {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Update snippet using new shared password
|
||||||
|
// + Save shared password (encrypted with public key)
|
||||||
|
//
|
||||||
|
const sharedNoteSnippetSalt = cs.createSmallSalt()
|
||||||
|
const snippet = JSON.stringify([note.title, note.text.substring(0, 500)])
|
||||||
|
const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, sharedNoteSnippetSalt, snippet)
|
||||||
|
|
||||||
|
//Encrypt shared password for this user
|
||||||
|
const encryptedSharedKey = crypto.publicEncrypt(userPublicKey, Buffer.from(sharedNoteMasterKey, 'utf8')).toString('base64')
|
||||||
|
|
||||||
|
//Update note snippet for current user with public key encoded snippet
|
||||||
|
return db.promise().query('UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 2 WHERE id = ? AND user_id = ?',
|
||||||
|
[encryptedSnippet, sharedNoteSnippetSalt, encryptedSharedKey, noteId, userId])
|
||||||
|
|
||||||
|
})
|
||||||
|
.then((rows, fields) => {
|
||||||
|
return resolve({note, 'sharedNoteKey':sharedNoteMasterKey})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//Remove private shared key, encrypt note with users master password
|
||||||
|
ShareNote.migrateToNormal = (userId, noteId, masterKey) => {
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
Note.get(userId, noteId, masterKey)
|
||||||
|
.then(noteObject => {
|
||||||
|
|
||||||
|
const salt = cs.createSmallSalt()
|
||||||
|
const snippetSalt = cs.createSmallSalt()
|
||||||
|
|
||||||
|
const snippetObj = JSON.stringify([noteObject.title, noteObject.text.substring(0, 500)])
|
||||||
|
const snippet = cs.encrypt(masterKey, snippetSalt, snippetObj)
|
||||||
|
|
||||||
|
const textObject = JSON.stringify([noteObject.title, noteObject.text])
|
||||||
|
const encryptedText = cs.encrypt(masterKey, salt, textObject)
|
||||||
|
|
||||||
|
db.promise()
|
||||||
|
.query(`UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 0 WHERE id = ? AND user_id = ?`,
|
||||||
|
[snippet, snippetSalt, null, noteId, userId])
|
||||||
|
.then((r,f) => {
|
||||||
|
|
||||||
|
db.promise()
|
||||||
|
.query('UPDATE note_raw_text SET text = ?, salt = ? WHERE id = ?',
|
||||||
|
[encryptedText, salt, noteObject.rawTextId])
|
||||||
|
.then(() => {
|
||||||
|
return resolve(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ShareNote.decryptSharedKey = (userId, noteId, masterKey) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
let userPrivateKey = null
|
||||||
|
|
||||||
|
const User = require('@models/User')
|
||||||
|
User.generateKeypair(userId, masterKey)
|
||||||
|
.then( ({publicKey, privateKey}) => {
|
||||||
|
|
||||||
|
userPrivateKey = privateKey
|
||||||
|
|
||||||
|
return Note.get(userId, noteId, masterKey)
|
||||||
|
})
|
||||||
|
.then(noteObject => {
|
||||||
|
|
||||||
|
//Shared notes use encrypted key - decrypt key then return it.
|
||||||
|
const encryptedShareKey = noteObject.encrypted_share_password_key
|
||||||
|
|
||||||
|
if(encryptedShareKey != null && userPrivateKey != null){
|
||||||
|
const currentNoteKey = crypto.privateDecrypt(userPrivateKey,
|
||||||
|
Buffer.from(encryptedShareKey, 'base64') )
|
||||||
|
|
||||||
|
return resolve(currentNoteKey)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return resolve(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users who see a shared note
|
||||||
|
ShareNote.getShareInfo = (userId, noteId, rawTextId) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
let shareUsers = []
|
||||||
|
let shareStatus = 0
|
||||||
|
|
||||||
db.promise()
|
db.promise()
|
||||||
.query(`
|
.query(`
|
||||||
SELECT username, note.id as noteId
|
SELECT username, note.id as noteId
|
||||||
@ -211,47 +298,14 @@ ShareNote.getUsers = (userId, rawTextId) => {
|
|||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
//Return a list of user names
|
//Return a list of user names
|
||||||
return resolve (rows[0])
|
shareUsers = rows[0]
|
||||||
|
return db.promise().query('SELECT shared FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
|
||||||
|
})
|
||||||
|
.then((rows, fields) => {
|
||||||
|
|
||||||
|
shareStatus = rows[0][0]['shared']
|
||||||
|
|
||||||
|
return resolve({ shareStatus, shareUsers })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a user from a shared note
|
|
||||||
ShareNote.removeUser = (userId, noteId) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
const Note = require('@models/Note')
|
|
||||||
|
|
||||||
let rawTextId = null
|
|
||||||
let removeUserId = null
|
|
||||||
|
|
||||||
//note.id = noteId, share_user_id = userId
|
|
||||||
db.promise()
|
|
||||||
.query('SELECT note_raw_text_id, user_id FROM note WHERE id = ? AND share_user_id = ?', [noteId, userId])
|
|
||||||
.then( (rows, fields) => {
|
|
||||||
|
|
||||||
rawTextId = rows[0][0]['note_raw_text_id']
|
|
||||||
removeUserId = rows[0][0]['user_id']
|
|
||||||
|
|
||||||
//Delete note entry for other user - remove users access
|
|
||||||
if(removeUserId && Number.isInteger(removeUserId)){
|
|
||||||
//Delete this users access to the note
|
|
||||||
return Note.delete(removeUserId, noteId, masterKey)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => { resolve(true) })
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
.then(stuff => {
|
|
||||||
resolve(true)
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.log(error)
|
|
||||||
resolve(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
@ -19,15 +19,6 @@ User.login = (username, password) => {
|
|||||||
.query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName])
|
.query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName])
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
// Create New Account
|
|
||||||
//
|
|
||||||
if(rows[0].length == 0){
|
|
||||||
User.create(lowerName, password)
|
|
||||||
.then( ({token, userId}) => {
|
|
||||||
return resolve({ token, userId })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login User
|
// Login User
|
||||||
//
|
//
|
||||||
if(rows[0].length == 1){
|
if(rows[0].length == 1){
|
||||||
@ -60,6 +51,8 @@ User.login = (username, password) => {
|
|||||||
reject('Password does not match database')
|
reject('Password does not match database')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
return reject('Incorrect Username or Password')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
@ -69,7 +62,7 @@ User.login = (username, password) => {
|
|||||||
|
|
||||||
//Create user account
|
//Create user account
|
||||||
//Issues login token
|
//Issues login token
|
||||||
User.create = (username, password) => {
|
User.register = (username, password) => {
|
||||||
|
|
||||||
//For some reason, username won't get into the promise. But password will @TODO figure this out
|
//For some reason, username won't get into the promise. But password will @TODO figure this out
|
||||||
const lowerName = username.toLowerCase().trim()
|
const lowerName = username.toLowerCase().trim()
|
||||||
@ -152,8 +145,8 @@ User.getCounts = (userId) => {
|
|||||||
SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes,
|
SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes,
|
||||||
SUM(trashed = 1) AS trashedNotes,
|
SUM(trashed = 1) AS trashedNotes,
|
||||||
SUM(share_user_id IS NULL && trashed = 0) AS totalNotes,
|
SUM(share_user_id IS NULL && trashed = 0) AS totalNotes,
|
||||||
SUM(share_user_id != ? && trashed = 0) AS sharedToNotes,
|
SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount,
|
||||||
SUM( (share_user_id != ? && opened IS null && trashed = 0) || (share_user_id != ? && note_raw_text.updated > opened && trashed = 0) ) AS unreadNotes
|
SUM(share_user_id != ? && trashed = 0) AS sharedToNotes
|
||||||
FROM note
|
FROM note
|
||||||
LEFT JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
LEFT JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||||
WHERE user_id = ?`, [userId, userId, userId, userId])
|
WHERE user_id = ?`, [userId, userId, userId, userId])
|
||||||
@ -407,11 +400,11 @@ User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
|
|||||||
const randomUsername = Math.random().toString(36).substring(2, 15);
|
const randomUsername = Math.random().toString(36).substring(2, 15);
|
||||||
const randomPassword = '1'
|
const randomPassword = '1'
|
||||||
|
|
||||||
User.login(testUserName, password)
|
User.register(testUserName, password)
|
||||||
.then( ({ token, userId }) => {
|
.then( ({ token, userId }) => {
|
||||||
testUserId = userId
|
testUserId = userId
|
||||||
|
|
||||||
if(printResults) console.log('Test: Create/Login User '+testUserName+' - Pass')
|
if(printResults) console.log('Test: Register User '+testUserName+' - Pass')
|
||||||
|
|
||||||
return User.getMasterKey(testUserId, password)
|
return User.getMasterKey(testUserId, password)
|
||||||
})
|
})
|
||||||
@ -439,6 +432,12 @@ User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
|
|||||||
//Convert it back to string
|
//Convert it back to string
|
||||||
if(printResults) console.log(publicDeccryptMessage.toString('utf8'))
|
if(printResults) console.log(publicDeccryptMessage.toString('utf8'))
|
||||||
|
|
||||||
|
return User.login(testUserName, password)
|
||||||
|
})
|
||||||
|
.then( ({token, userId}) => {
|
||||||
|
|
||||||
|
if(printResults) console.log('Test: Login New User - Pass')
|
||||||
|
|
||||||
resolve({testUserId, masterKey})
|
resolve({testUserId, masterKey})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -23,8 +23,13 @@ router.use(function setUserId (req, res, next) {
|
|||||||
//
|
//
|
||||||
router.post('/get', function (req, res) {
|
router.post('/get', function (req, res) {
|
||||||
Note.get(userId, req.body.noteId, masterKey)
|
Note.get(userId, req.body.noteId, masterKey)
|
||||||
.then( data => {
|
.then( noteObject => {
|
||||||
res.send(data)
|
|
||||||
|
delete noteObject.snippet_salt
|
||||||
|
delete noteObject.salt
|
||||||
|
delete noteObject.encrypted_share_password_key
|
||||||
|
|
||||||
|
res.send(noteObject)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -91,8 +96,8 @@ router.post('/settrashed', function (req, res) {
|
|||||||
//
|
//
|
||||||
// Share Note Actions
|
// Share Note Actions
|
||||||
//
|
//
|
||||||
router.post('/getshareusers', function (req, res) {
|
router.post('/getshareinfo', function (req, res) {
|
||||||
ShareNote.getUsers(userId, req.body.rawTextId)
|
ShareNote.getShareInfo(userId, req.body.noteId, req.body.rawTextId)
|
||||||
.then(results => res.send(results))
|
.then(results => res.send(results))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -100,7 +105,7 @@ router.post('/shareadduser', function (req, res) {
|
|||||||
// ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username, masterKey)
|
// ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username, masterKey)
|
||||||
User.getByUserName(req.body.username)
|
User.getByUserName(req.body.username)
|
||||||
.then( user => {
|
.then( user => {
|
||||||
return ShareNote.migrateNoteToShared(userId, req.body.noteId, user.id, masterKey)
|
return ShareNote.addUserToSharedNote(userId, req.body.noteId, user.id, masterKey)
|
||||||
})
|
})
|
||||||
.then( ({success, shareUserId}) => {
|
.then( ({success, shareUserId}) => {
|
||||||
|
|
||||||
@ -110,10 +115,28 @@ router.post('/shareadduser', function (req, res) {
|
|||||||
|
|
||||||
router.post('/shareremoveuser', function (req, res) {
|
router.post('/shareremoveuser', function (req, res) {
|
||||||
// (userId, noteId, shareNoteUserId, shareUserId, masterKey)
|
// (userId, noteId, shareNoteUserId, shareUserId, masterKey)
|
||||||
ShareNote.removeUserFromShared(userId, req.body.noteId, req.body.shareUserNoteId, masterKey)
|
ShareNote.removeUserFromSharedNote(userId, req.body.noteId, req.body.shareUserNoteId, masterKey)
|
||||||
.then(results => res.send(results))
|
.then(results => res.send(results))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.post('/enableshare', function (req, res) {
|
||||||
|
//Create Shared Encryption Key for Note
|
||||||
|
ShareNote.migrateToShared(userId, req.body.noteId, masterKey)
|
||||||
|
.then(results => res.send(true))
|
||||||
|
})
|
||||||
|
router.post('/getsharekey', function (req, res) {
|
||||||
|
//Get Shared Key for a note
|
||||||
|
ShareNote.decryptSharedKey(userId, req.body.noteId, masterKey)
|
||||||
|
.then(results => res.send(results))
|
||||||
|
})
|
||||||
|
router.post('/disableshare', function (req, res) {
|
||||||
|
//Removed shared encryption key from note
|
||||||
|
ShareNote.migrateToNormal(userId, req.body.noteId, masterKey)
|
||||||
|
.then(results => res.send(true))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Testing Action
|
// Testing Action
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
var express = require('express')
|
var express = require('express')
|
||||||
var router = express.Router()
|
var router = express.Router()
|
||||||
|
|
||||||
let Notes = require('@models/Note')
|
let Note = require('@models/Note')
|
||||||
|
|
||||||
router.post('/note', function (req, res) {
|
//
|
||||||
|
// Public Note action
|
||||||
|
//
|
||||||
|
router.post('/opensharednote', function (req, res) {
|
||||||
|
|
||||||
Notes.getShared(req.body.noteId)
|
Note.getShared(req.body.noteId, req.body.sharedKey)
|
||||||
.then( data => res.send(data) )
|
.then(results => res.send(results))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,34 +18,31 @@ router.get('/about', function (req, res) {
|
|||||||
User.getUsername(req.headers.userId)
|
User.getUsername(req.headers.userId)
|
||||||
.then( data => res.send(data) )
|
.then( data => res.send(data) )
|
||||||
})
|
})
|
||||||
// define the login route
|
// Login User
|
||||||
router.post('/login', function (req, res) {
|
router.post('/login', function (req, res) {
|
||||||
|
|
||||||
//Pull out variables we want
|
User.login(req.body.username, req.body.password)
|
||||||
const username = req.body.username
|
.then( returnData => {
|
||||||
const password = req.body.password
|
|
||||||
|
|
||||||
let returnData = {
|
|
||||||
success: false,
|
|
||||||
token: '',
|
|
||||||
username: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
User.login(username, password)
|
|
||||||
.then( ({token, userId}) => {
|
|
||||||
|
|
||||||
returnData['username'] = username
|
|
||||||
returnData['token'] = token
|
|
||||||
returnData['success'] = true
|
|
||||||
|
|
||||||
res.send(returnData)
|
res.send(returnData)
|
||||||
return
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
console.log(e)
|
res.send(false)
|
||||||
res.send(returnData)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
// Login User
|
||||||
|
router.post('/register', function (req, res) {
|
||||||
|
|
||||||
|
User.register(req.body.username, req.body.password)
|
||||||
|
.then( returnData => {
|
||||||
|
|
||||||
|
res.send(returnData)
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
res.send(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
// fetch counts of users notes
|
// fetch counts of users notes
|
||||||
router.post('/totals', function (req, res) {
|
router.post('/totals', function (req, res) {
|
||||||
|
Loading…
Reference in New Issue
Block a user