* 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:
Max G
2020-06-07 20:57:35 +00:00
parent 8e5e06be9b
commit 6bb856689d
31 changed files with 1605 additions and 1095 deletions

View File

@@ -3,6 +3,11 @@ import Vue from 'vue'
const helpers = {}
helpers.timeAgo = (time) => {
if(time.toString().length >= 13){
time = Math.round(time/1000)
}
const time_formats = [
[ 60, 'seconds', 1 ],
[ 120, '1 minute ago', '1 minute from now' ],

View File

@@ -17,10 +17,11 @@
:root {
--background_color: #fff;
--body_bg_color: #f5f6f7;
--small_element_bg_color: #fff;
--text_color: #3d3d3d;
--outline_color: rgba(34,36,38,.15);
--border_color: rgba(34,36,38,.20);
--dark_border_color: #DFE1E6;
--border_color: #DFE1E6;
/* Global purple menu styles */
--menu-border: #534c68;
@@ -34,7 +35,9 @@
html {
/*scrollbar-width: none;*/
}
a:hover {
text-decoration: underline;
}
div.ui.basic.segment.no-fluf-segment {
margin-top: 0px;
}
@@ -59,21 +62,21 @@ div.ui.basic.segment.no-fluf-segment {
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
body {
color: var(--text_color);
background-color: var(--background_color);
background-color: var(--body_bg_color);
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
.ui.segment {
color: var(--text_color);
background-color: var(--background_color);
border-color: var(--border_color);
background-color: var(--small_element_bg_color);
border-color: var(--dark_border_color);
}
.button-sub {
display: inline-block;
width: 100%;
font-size: 0.9em;
color: white;
opacity: 0.8;
color: grey;
opacity: 0.9;
padding: 4px 0 0 0;
text-align: center;
}
@@ -83,71 +86,43 @@ body {
.ui.form textarea:not([type]),
.ui.form textarea:not([type]):focus {
color: var(--text_color);
background-color: var(--background_color);
border-color: var(--border_color);
background-color: var(--small_element_bg_color);
border-color: var(--dark_border_color);
}
.ui.basic.label, .ui.header, .ui.header div.sub.header {
color: var(--text_color);
background-color: var(--background_color);
border-color: var(--border_color);
background-color: transparent;
border-color: var(--dark_border_color);
}
.ui.icon.input > i.icon {
color: var(--text_color);
}
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 {
background-color: var(--background_color) !important;
background-color: var(--small_element_bg_color) !important;
color: var(--text_color) !important;
border: 1px solid;
border-color: var(--border_color) !important;
border-color: var(--dark_border_color) !important;
box-shadow: none;
}
.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;
box-shadow: none;
}
.ui.tabular.menu .item {
background-color: var(--background_color) !important;
background-color: var(--small_element_bg_color) !important;
color: var(--text_color) !important;
}
.ui.tabular.menu .item.active {
background-color: var(--background_color) !important;
background-color: var(--small_element_bg_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*/
/* 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
//*/
@@ -255,6 +230,8 @@ a:hover {
word-wrap: break-word;
/*border-bottom: 1px solid #ccc;*/
scrollbar-width: none;
scrollbar-color: transparent transparent;
caret-color: #21BA45;
}
.squire-box::selection,
.squire-box::-moz-selection {
@@ -271,19 +248,15 @@ a:hover {
.squire-box a {
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 .squire-box i {
background-color: rgba(255, 255, 255, 0.2);
}
.note-card-text pre,
.squire-box pre {
word-wrap: break-word;
}
.note-card-text p,
.squire-box p {
margin-bottom: 0;

View File

@@ -6,6 +6,7 @@
display: inline-block;
border: 1px solid;
border-color: var(--border_color);
background-color: var(--small_element_bg_color);
border-radius: 4px;
margin: 0 0 15px;
max-height: 10000px;

View File

@@ -40,7 +40,7 @@
let filter = {}
filter[option] = 1
this.$bus.$emit('update_fast_filters', filter)
// this.$bus.$emit('update_fast_filters', filter)
}
}
}
@@ -62,7 +62,7 @@
.filter-menu {
color: var(--text_color);
background-color: var(--background_color);
background-color: var(--small_element_bg_color);
border: 1px solid;

View File

@@ -20,16 +20,16 @@
}
.menu-logo-display {
width: 25px;
margin: 5px 0 0 34px;
margin: 5px 0 0 42px;
display: inline-block;
}
.menu-item {
color: #fff;
padding: 0.8em 10px 0.8em 10px;
padding: 9px 10px;
display: inline-block;
width: 100%;
font-size: 1.15em;
font-size: 1.1em;
box-sizing: border-box;
}
.menu-item i.icon {
@@ -76,7 +76,7 @@
left: 0;
right: 0;
z-index: 999;
background-color: var(--background_color);
background-color: var(--small_element_bg_color);
border-bottom: 1px solid;
border-color: var(--border_color);
padding: 5px 1rem 5px;
@@ -117,9 +117,9 @@
<i class="green bars icon"></i>
</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>
</router-link>
</router-link> -->
<router-link v-if="loggedIn" class="ui basic icon button" exact-active-class="active" to="/attachments">
<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" />
</router-link>
<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 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> -->
@@ -218,7 +228,7 @@
<span v-if="$store.getters.getIsNightMode == 0">
<i class="moon outline icon"></i>Black Theme</span>
<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">
<i class="moon outline icon"></i>Light Theme</span>
</div>
@@ -257,7 +267,7 @@
},
data: function(){
return {
version: '2.2.3',
version: '2.3.4',
username: '',
collapsed: false,
mobile: false,
@@ -354,27 +364,17 @@
//Reloads note page to initial state
this.$bus.$emit('note_reload')
},
updateFastFilters(index){
updateFastFilters(filterIndex){
//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')
setTimeout( () => {
this.updateFastFilters(index)
this.$bus.$emit('update_fast_filters', filterIndex)
}, 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(){
location.reload(true)

View File

@@ -33,29 +33,17 @@
export default {
name: 'LoadingIcon',
props:[ 'message' ],
data () {
return {
items: []
}
},
beforeMount(){
},
mounted(){
},
methods: {
onClickTag(index){
console.log('yup')
},
}
}
</script>
<style type="text/css" scoped>
.loading-container {
text-align: center;
width: 100%;
height: 100px;
min-height: 100px;
margin: 20px 0;
padding: 40px;
border-radius: 7px;
background-color: var(--small_element_bg_color);
}
.loading-container svg {
width: 60px;

View File

@@ -1,11 +1,12 @@
<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 class="field">
<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 class="field">
@@ -13,27 +14,48 @@
<input v-model="password" type="password" name="password" placeholder="Password">
</div>
</div>
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="submit" class="ui massive compact fluid green submit button">Sign Up / Login</div>
</div>
<div v-if="thin" class="ui small form">
<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 class="sixteen wide field">
<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>
Login
</div>
<div class="six wide field">
<div class="ui input">
<input v-model="password" type="password" name="password" placeholder="Password">
</div>
</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 class="or"></div>
<div v-on:click="register()" class="ui button">
<i class="plug icon"></i>
Sign Up
</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: {
submit(){
finalizeLogin(data){
//Both fields are required
if(this.username <= 0){
return false
}
if(this.password <= 0){
return false
//Destroy local data if there is an error
if(data == false){
this.$store.commit('destroyLoginToken')
return
}
let vm = this
//Login user if we have a valid token
if(data && data.token && data.token.length > 0){
const token = data.token
const username = this.username
let data = {
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)
.then(response => {
if(response.data.success){
const token = response.data.token
const username = response.data.username
const masterKey = response.data.masterKey
axios.post('/api/user/register', {'username': this.username, 'password': this.password})
.then(({data}) => {
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')
if(data == false){
this.$bus.$emit('notification', 'Username already in use')
}
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 => {
this.$bus.$emit('notification', 'Incorrect Username or Password')

File diff suppressed because it is too large Load Diff

View File

@@ -323,7 +323,7 @@
height: 40px;
padding: 10px 15px;
cursor: pointer;
background-color: var(--background_color);
background-color: var(--small_element_bg_color);
color: var(--text_color);
}
.suggestion-item.active {

View File

@@ -242,11 +242,11 @@
justClosed(){
//Scroll note into view
this.$el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
})
// this.$el.scrollIntoView({
// behavior: 'smooth',
// block: 'center',
// inline: 'center'
// })
//After scroll, trigger green outline animation
setTimeout(() => {
@@ -353,7 +353,7 @@
display: inline-block;
min-width: 30px;
color: var(--text_color);
background-color: var(--background_color);
background-color: var(--small_element_bg_color);
}
.subtext {
display: inline-block;
@@ -368,7 +368,7 @@
}
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
/*font-size: 1.0em !important;*/
font-size: 15px !important;
font-size: 16px !important;
}
.small-text > p, , .small-text > h1, .small-text > h2 {
margin-bottom: 0.5em;
@@ -410,19 +410,17 @@
.note-title-display-card {
position: relative;
/*box-shadow: 0 1px 3px 0 rgba(34,36,38,.15);*/
/*box-shadow: 0 0px 5px 1px rgba(34,36,38,0);*/
/*box-shadow: 0 1px 3px 0 rgba(34,36,38,.15);*/
box-shadow: 0px 1px 2px 1px rgba(210, 211, 211, 0.46);
transition: box-shadow ease 0.3s;
background-color: var(--small_element_bg_color);
/*The subtle shadow*/
/*box-shadow: 0px 1px 2px 1px rgba(210, 211, 211, 0.46);*/
transition: box-shadow ease 0.5s, transform linear 0.1s;
margin: 5px;
/*padding: 0.7em 1em;*/
border-radius: .28571429rem;
border: 1px solid transparent;
/*border-color: var(--border_color);*/
border-color: var(--border_color);
/*width: calc(33.333% - 10px);*/
width: calc(25% - 10px);
max-width: 300px;
min-width: 190px;
min-height: 130px;
/*transition: box-shadow 0.3s;*/
@@ -436,13 +434,15 @@
text-align: left;
}
.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 {
width: 100%;
min-height: 10px;
min-height: 20px;
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 {
@@ -527,6 +527,7 @@
.one-column .note-title-display-card {
width: 100%;
max-width: none;
/*margin: 0px -5px 10px -5px;*/
}
.overflow-hidden {
overflow: hidden;
@@ -561,11 +562,15 @@
}
/* 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) {
.note-title-display-card {
width: calc(100% + 10px);
margin: 0px -5px 10px -5px;
max-width: none;
}
}

View File

@@ -71,14 +71,6 @@
</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>
@@ -141,11 +133,7 @@
axios.post('/api/quick-note/update', { 'pushText':text.trim() } )
.then( response => {
//Open Quick Note
if(response.data && response.data.id){
this.$router.push('/notes/open/'+response.data.id)
this.$bus.$emit('open_note', response.data.id)
}
this.$bus.$emit('notification', 'Saved To Scratch Pad')
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Update The Scratch Pad') })
},
@@ -173,20 +161,20 @@
clearTimeout(this.tagSearchDebounce)
if(this.searchTerm.length == 0){
this.tagSuggestions = []
return
}
// if(this.searchTerm.length == 0){
// this.tagSuggestions = []
// return
// }
this.tagSearchDebounce = setTimeout(() => {
this.tagSuggestions = []
axios.post('/api/tag/suggest', postData)
.then( response => {
// this.tagSearchDebounce = setTimeout(() => {
// this.tagSuggestions = []
// axios.post('/api/tag/suggest', postData)
// .then( response => {
this.tagSuggestions = response.data
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
}, 800)
// this.tagSuggestions = response.data
// })
// .catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
// }, 800)
},
onKeyDown(event){

View File

@@ -5,7 +5,30 @@
<template>
<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="eight wide column">
@@ -38,7 +61,7 @@
</div>
<div class="ui grid" v-if="this.shareUsername != null">
<div class="ui grid" v-if="shareUsername != null">
<div class="sixteen wide column">
Shared with you by <h3><i class="green user circle icon"></i>{{shareUsername}}</h3>
</div>
@@ -56,10 +79,12 @@
props: [ 'noteId', 'rawTextId', 'shareUsername' ],
data () {
return {
isNoteShared: false,
sharedWithUsers: [],
shareUserInput: '',
debounce: null,
enableSubmitShare: false,
sharedUrl: '',
}
},
beforeMount(){
@@ -67,6 +92,8 @@
},
mounted(){
// this.isNoteShared = this.noteShared
if(this.shareUsername == null){
this.loadShareList()
}
@@ -74,12 +101,39 @@
},
methods: {
loadShareList(){
axios.post('/api/note/getshareusers', {'rawTextId':this.rawTextId })
axios.post('/api/note/getshareinfo', {'noteId':this.noteId, 'rawTextId':this.rawTextId })
.then( ({data}) => {
this.sharedWithUsers = data
this.isNoteShared = (data.shareStatus == 2)
this.sharedWithUsers = data.shareUsers
})
.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){
const postData = {

View File

@@ -10,14 +10,14 @@
height: 100%;
color: var(--text_color);
background-color: var(--background_color);
background-color: var(--small_element_bg_color);
}
.slide-content {
box-sizing: border-box;
/*padding: 1em 1.5em;*/
height: calc(100% - 43px);
border-right: 1px solid var(--menu-border);
/*background-color: var(--background_color);*/
/*background-color: var(--small_element_bg_color);*/
overflow-x: scroll;
}
.slide-shadow {

View 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

View File

@@ -11,7 +11,7 @@
<!-- Content copied from note -->
<!-- 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 -->
</div>

View File

@@ -78,6 +78,10 @@
.home-main img {
max-height: 400px !important;
}
.white-link {
text-decoration: underline;
color: white;
}
</style>
@@ -120,7 +124,7 @@
</h2>
<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>
</div>
@@ -146,7 +150,7 @@
<div class="ui text container">
<h2>
<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" />
</div>
</div>
@@ -155,7 +159,7 @@
<!-- set -->
<div class="middle aligned centered row">
<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>
</div>
<div class="six wide column">
@@ -163,24 +167,25 @@
</div>
</div>
<div class="middle aligned centered row">
<div class="middle aligned centered green row">
<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 class="six wide column">
<h2>Tools to organize thousands of notes</h2>
<h3>Tag, Pin, Color, Archive, Attach Images and Search notes or links in notes</h3>
<h2>All Note text is encrypted</h2>
<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>
<!-- set -->
<div class="middle aligned centered green row">
<div class="middle aligned centered row">
<div class="six wide column">
<h2>Privacy through Encryption</h2>
<h3>All notes are encrypted. No one can read your notes, even if they steal the data from the database.</h3>
<h2>Organize your notes</h2>
<h3>Tag, Pin, Color, Archive, Attach Images, Share Encrypted Notes and Search</h3>
</div>
<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>
@@ -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">
</div>
<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>
</div>
</div>
@@ -198,7 +203,7 @@
<div class="middle aligned centered row">
<div class="six wide right aligned column">
<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 class="six wide column">
<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">
</div>
<div class="six wide column">
<h2>Embrace the Void</h2>
<h3>Remove unnecessary clutter for your brain and save it to the cloud, allowing you to easily embrace the gaping abyss</h3>
<h2>Create Lists with Check Boxes</h2>
<h3>Todo lists are supported. With options to removed checked items, sort by completed and un-check all.</h3>
</div>
</div>
<!-- set -->
<div class="middle aligned centered row">
<div class="six wide right aligned column">
<h2>Space for Growth</h2>
<h3>Groom a clear path for new expressions and innovations. Elevate your being and lower your cholesterol</h3>
<h2>Powerful Text Editing</h2>
<h3>A plethora of editing tools are provided for coloring, underlining, bolding, attaching images and more.</h3>
</div>
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/growth.svg" alt="Endless progress at the cost of sanity and health">
@@ -231,13 +236,13 @@
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="Shrunken man near giant tablet">
</div>
<div class="six wide column">
<h2>Become your Data</h2>
<h3>We exist as electrical impulses, no different from data on a computer</h3>
<h2>Secure Data Sharing</h2>
<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>
<!-- set -->
<div class="middle aligned centered row">
<!-- <div class="middle aligned centered row">
<div class="six wide right aligned column">
<h2>Ice Cream</h2>
<h3>Get excited without all the screaming</h3>
@@ -265,7 +270,7 @@
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/grandma.svg" alt="Drinking the blood of the elderly">
</div>
</div>
</div> -->
<!-- final slide -->
<div class="middle aligned centered green row">
@@ -282,20 +287,18 @@
<br>
<br>
<br>
OR
<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 v-if="realInformation" class="middle aligned centered row" ref="real">
<div class="six wide column">
<h2 class="ui center aligned">
What is this really?
Why Does this App exist?
</h2>
<h3>Its just a little web app for taking notes. This page is mocking the "over the top" marketing sites use to sell their products.</h3>
<p>
This App exists because I was tired of all my data being owned by big companies, having it farmed out for marketing, and leaving the contents of my life exposed to corporations.
</p>

View File

@@ -10,7 +10,7 @@
<div class="ui text container">
<div class="ui segment" v-on:keyup.enter="submit">
<div class="ui segment">
<h4 class="ui header">
<i class="plug icon"></i>
@@ -44,53 +44,10 @@
data () {
return {
enabled: false,
username: '',
password: ''
}
},
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>

View 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>

View File

@@ -7,27 +7,19 @@
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
<div class="ui stackable grid">
<div class="six wide column" v-if="$store.getters.totals && $store.getters.totals['totalNotes']">
<search-input />
</div>
<div class="ten wide column"
:class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
<div class="ui basic button shrinking"
v-on:click="updateFastFilters(3)"
v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)"
v-if="$store.getters.totals && ($store.getters.totals['youGotMailCount'] > 0)"
style="position: relative;">
<i class="green mail icon"></i>Shared Notes
<span class="floating ui green label" v-if="$store.getters.totals['unreadNotes'] > 0">
{{ $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>
<i class="green mail icon"></i>Inbox
+{{ $store.getters.totals['youGotMailCount'] }}
</div>
<tag-display
@@ -42,12 +34,6 @@
</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">
<!-- <fast-filters /> -->
<span class="ui fluid green button" @click="reset">
@@ -75,7 +61,7 @@
</div>
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
<h2 >Trash
<h2>Trash
<span>({{ $store.getters.totals['trashedNotes'] }})</span>
<div class="ui right floated basic button" data-tooltip="This doesn't work yet">
<i class="poo storm icon"></i>
@@ -88,6 +74,25 @@
<h2>Shared Notes</h2>
</div>
<div class="sixteen wide column" v-if="tagSuggestions.length > 0">
<h5 class="ui tiny dividing header"><i class="green tags icon"></i> Tags ({{ tagSuggestions.length }})</h5>
<div class="ui clickable green label" v-for="tag in tagSuggestions" v-on:click="tagId => toggleTagFilter(tag.id)">
<i class="tag icon"></i>
{{ tag.text }}
</div>
</div>
<!-- found attachments -->
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
<h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
<attachment-display
v-for="item in foundAttachments"
:item="item"
:key="item.id"
:search-params="{}"
/>
</div>
<!-- Note title card display -->
<div class="sixteen wide column">
@@ -123,18 +128,6 @@
</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>
@@ -170,7 +163,7 @@
data () {
return {
initComponent: true,
commonTags: [],
tagSuggestions:[],
searchTerm: '',
searchResultsCount: 0,
searchTags: [],
@@ -191,13 +184,6 @@
//Clear button is not visible
showClear: false,
initialPostData: null,
currentPostData: null,
containsNormalNotes: 0,
containsPinnednotes: 0,
containsTextResults: 0,
// containsTagResults: 0,
// containsAttachmentResults: 0,
//Currently open notes in app
activeNoteId1: null,
@@ -214,8 +200,8 @@
sectionData: {
'pinned': ['thumbtack', 'Pinned'],
'archived': ['archive', 'Archived'],
'shared': ['envelope outline', 'Received Notes'],
'sent': ['paper plane outline', 'Shared Notes'],
'shared': ['envelope outline', 'Inbox'],
'sent': ['paper plane outline', 'Sent Notes'],
'notes': ['file','Notes'],
'highlights': ['paragraph', 'Found In Text'],
'trashed': ['poop', 'Trashed Notes'],
@@ -275,13 +261,12 @@
this.$store.dispatch('fetchAndUpdateUserTotals')
//Close note event
this.$bus.$on('close_active_note', ({position, noteId, modified}) => {
this.$bus.$on('close_active_note', ({noteId, modified}) => {
this.closeNote()
if(modified){
this.$store.dispatch('fetchAndUpdateUserTotals')
this.updateSingleNote(parseInt(noteId))
}
this.$store.dispatch('fetchAndUpdateUserTotals')
//Focus and animate if modified
this.updateSingleNote(parseInt(noteId), modified)
})
this.$bus.$on('note_deleted', (noteId) => {
@@ -295,15 +280,12 @@
return
}
})
})
})
})
this.$bus.$on('update_fast_filters', newFilter => {
this.fastFilters = newFilter
//Fast filters always return all the results and tags
this.search(true, this.batchSize, false).then( () => {
// return
})
this.$bus.$on('update_fast_filters', filterIndex => {
this.updateFastFilters(filterIndex)
})
//Event to update search from other areas
@@ -312,8 +294,18 @@
this.search(true, this.batchSize)
.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
})
@@ -365,6 +357,9 @@
//Loads initial batch and tags
this.reset()
// this.search(true, this.firstLoadBatchSize, false)
// .then( r => this.search(false, this.batchSize, true))
},
methods: {
toggleTitleView(){
@@ -390,7 +385,7 @@
if(this.activeNoteId1 == null){
this.activeNoteId1 = id
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
}
},
@@ -417,24 +412,18 @@
clearTimeout(this.loadingBatchTimeout)
this.loadingBatchTimeout = setTimeout(() => {
//Distance to bottom of page
const bottomOfWindow =
Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop)
+ window.innerHeight
//Detect distance scrolled down the page
const scrolledDown = window.pageYOffset + window.innerHeight
//Get height of div to properly detect scroll distance down
const height = document.getElementById('app').scrollHeight
//height of page
const offsetHeight = this.$refs.content.clientHeight
//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){
//Load if less than 500px from the bottom
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled && !this.loadingInProgress){
this.search(false, this.batchSize, true)
}
}, 50)
}, 30)
return
@@ -491,7 +480,10 @@
noteId = parseInt(noteId)
//Find local note, if it exists; continue
let note = null
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
note = this.$refs['note-'+noteId][0].note
}
//Lookup one note using passed in ID
const postData = {
@@ -508,60 +500,62 @@
//Pull note data out of note set
let newNote = results.data.notes[0]
let foundNote = false
if(newNote === undefined){
return
}
//Find Just updated note and modify all its attributes
Object.keys(this.noteSections).forEach(key => {
if(note && newNote){
this.noteSections[key].forEach( (note,index) => {
//Don't move notes that were not changed
if(note.updated == newNote.updated){
return
}
if(note.id == noteId){
foundNote = true
//go through each prop and update it with new values
Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop]
})
//Don't move notes that were not changed
if(note.updated == newNote.updated){
// return
}
//Push new note to front if its modified
if(focuseAndAnimate){
//Compare note tags, if they changed, reload tags
if(newNote.tag_count != note.tag_count){
}
//go through each prop and update it with new values
Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop]
})
//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()
// Find note, in section, move to front
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key].forEach( (searchNote, index) => {
if(searchNote.id == noteId){
//Remove note from location and push to front
this.noteSections[key].splice(index, 1)
this.noteSections[key].unshift(note)
return
}
})
})
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
if(!foundNote){
if(note == null){
this.noteSections.notes.unshift(newNote)
//Trigger close animation on note
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].justClosed()
}
}
//Trigger section rebuild
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(){
axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm})
@@ -570,13 +564,13 @@
})
.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) => {
//Don't double load note batches
if(this.loadingInProgress){
console.log('Loading in progress, cancel operation')
return resolve()
console.log('Loading already in progress')
return resolve(false)
}
//Reset a lot of stuff if we are not merging batches
@@ -585,7 +579,6 @@
this.noteSections[key] = []
})
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
@@ -628,11 +621,6 @@
//Enable or disable scroll loading
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){
this.searchResultsCount = response.data.total
}
@@ -742,33 +730,22 @@
this.scrollLoadEnabled = true
this.searchTerm = ''
this.searchTags = []
this.tagSuggestions = []
this.fastFilters = {}
this.updateFastFilters(5)
this.foundAttachments = [] //Remove all attachments
this.$bus.$emit('reset_fast_filters')
//Load initial batch, then tags, then other batch
this.search(true, this.firstLoadBatchSize)
.then( () => {
//Load a larger batch once first batch has loaded
return this.search(false, this.batchSize, true)
})
this.$bus.$emit('reset_fast_filters')
this.updateFastFilters(5) //This loads notes
},
updateFastFilters(index){
//clear out tags
this.searchTags = []
this.tagSuggestions = []
this.loadingInProgress = false
this.searchTerm = ''
this.$bus.$emit('reset_fast_filters')
//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 )
}
this.$bus.$emit('reset_fast_filters') //Clear out search
const options = [
'withLinks', // 'Only Show Notes with Links'
@@ -782,7 +759,10 @@
let filter = {}
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))
}
}
}

View File

@@ -12,11 +12,11 @@
</h2>
</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">
<i class="sync alternate reload icon"></i>
New Quick Note
New Scratch Pad
</div>
<div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="showNewNoteConfirm = false">
<i class="close icon"></i>

View File

@@ -1,9 +1,49 @@
<template>
<div class="ui basic segment">
<div class="ui container">
<div class="fun" :style="{'color':color}" v-if="noteText" v-html="noteText"></div>
<div class="ui grid">
<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 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>
</template>
@@ -15,36 +55,51 @@
name: 'SharePage',
data(){
return {
noteText: null,
color: '#000'
title: '',
text: '',
failText: '',
styleObject:{},
}
},
beforeMount(){
//Mount notes on load if note ID is set
if(this.$route.params && this.$route.params.id){
const id = this.$route.params.id
this.openNote(id)
}
//You can put something here for live updates
// this.$io.on
this.openNote()
},
methods:{
openNote(noteId){
axios.post('/api/public/note', {'noteId': noteId})
.then( response => {
fail(){
this.failText = 'Failed to open Shared Note'
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){
document.body.style.background = colors.noteBackground
axios.post('/api/public/opensharednote', {noteId, sharedKey})
.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>

View File

@@ -10,6 +10,7 @@ const SharePage = () => import(/* webpackChunkName: "SharePage" */ '@/pages/Shar
const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage')
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
Vue.use(Router)
@@ -52,7 +53,7 @@ export default new Router({
component: HelpPage
},
{
path: '/share/:id',
path: '/public/note/:id/:token',
name: 'Share',
meta: {title:'Shared'},
component: SharePage
@@ -81,5 +82,11 @@ export default new Router({
meta: {title:'Attachments by Type'},
component: AttachmentsPage
},
{
path: '*',
name: 'Page Not Found',
meta: {title:'404 Page Not Found'},
component: NotFoundPage
},
]
})

View File

@@ -45,34 +45,36 @@ export default new Vuex.Store({
const themes = {
'white':{
'background_color': '#fff',
'body_bg_color': '#f5f6f7',
'small_element_bg_color': '#fff',
'text_color': '#3d3d3d',
'outline_color': 'rgba(34,36,38,0.15)',
'border_color': 'rgba(34,36,38,0.20)',
'dark_border_color': '#DFE1E6',
'border_color': '#DFE1E6',
'menu-accent': '#cecece',
'menu-text': '#5e6268',
},
'black':{
'background_color': '#000',
'body_bg_color': '#000',
'small_element_bg_color': '#000',
'text_color': '#FFF',
'outline_color': '#FFF',
'border_color': 'rgba(255, 255, 255, 0.70)',
'dark_border_color': '#ACACAC', //Lighter color to accent elemnts user can interact with
'border_color': '#555',
'menu-accent': '#626262',
'menu-text': '#d9d9d9',
},
'night':{
'background_color': '#000',
'body_bg_color': '#000',
'small_element_bg_color': '#000',
'text_color': '#a98457',
'outline_color': '#a98457',
'border_color': 'rgba(255, 255, 255, 0.31)',
'dark_border_color': '#a98457',
'border_color': '#555',
'menu-accent': '#626262',
'menu-text': '#d9d9d9',
'menu-text': '#a69682',
},
}
//Catch values not in set
const totalThemes = Object.keys(themes).length
state.nightMode++
if(state.nightMode > totalThemes-1){