Compare commits

..

88 Commits

Author SHA1 Message Date
Max
062996bf7c Updated dnydns script 2023-03-02 01:35:52 +00:00
Max
5d4376b4e7 Not really sure what is going on, have not done a commit in a while.
I assume this is all the metric tracking changes.
Looks like some script changes as well.
2023-02-12 18:41:55 +00:00
Max
51e35b0f11 Added timeout to fetch user totals which prevents
Duplicate calls which would be really annoying
2022-12-22 01:59:27 +00:00
Max
7f65587db6 Bugfix Day 1
* Fixed attachments being displayed that were on archived or deleted notes
* Added options to show attachments on archived or trashed notes
* Showing note files will show all attachments for note even if its archived or trashed with mixed file types
* Fixed text about "Flux" theme which was removed
* Fixed bug when opening metric tracking that would prevent default fields from being shown
2022-12-20 19:59:03 +00:00
Max
789a4e47d4 Added metric tracking and some other little fixes 2022-12-20 17:42:38 +00:00
Max
952a1dd1b1 Tweaking node versions and project settings
* Removed node sass lets hope it doesn't break anything
2022-10-23 19:37:05 +00:00
Max
e5c117bbdb Project restructuring, fixing minor bugs related to vue CLI upgrade
* Removed PWA kit from project, this removes a ton of dependencies
2022-10-23 19:14:31 +00:00
Max
178a7dfc2c Added cycle tracking beta to app 2022-10-21 19:34:13 +00:00
Max
f12be22765 Updated vue CLI to latest version
Added cycle tracking base
2022-10-13 19:28:35 +00:00
Max G
b7d22cb7fc Adding everything to get started on cycle tracking and maybe avid habit clone 2022-09-25 17:17:41 +00:00
Max G
df5e9f8c3b Added paste button and touched up some styles 2022-07-05 05:10:40 +00:00
Max G
7f5f4bea39 Updated marketing images to change with theme
Removed visible attribute that was left over from testing
Removed drag attribute on check boxes, needs better implimentation later. Drag prevented click events
2022-04-03 17:21:05 +00:00
Max G
c972430ef4 Added focus and interaction to refresh notes that have been changed while user was looking away 2022-02-25 04:26:12 +00:00
Max G
6d0187ee0a Lots of little ease of use tweaks 2022-02-25 02:33:49 +00:00
Max G
00500ecc33 Updated database script to make it more robust and not break the freaking database when you apply the prod DB to dev 2022-02-25 02:33:20 +00:00
Max G
848c86327a Tons of littele interface changes and cleanups
Massive update to image scraper with much better image getter
Lots of little ui updates for mobile
2022-01-27 04:48:19 +00:00
Max G
d4be0d6471 Bunch of changes and unfinished features. Just trying to keep everything up to date. This project is a mess. Don't worry. You are employed. 2021-12-18 22:18:22 +00:00
Max G
c99828dbad Checking in minor changes for server migration 2021-02-12 17:11:33 +00:00
Max G
217f052e63 * Removed unused get note diff function. It doesn't work because everything is encrypted now
* Added a script to sync down the prod database and files to dev
2020-10-10 21:27:52 +00:00
Max G
4e93bf23fb Added vue config 2020-10-05 06:46:13 +00:00
Max G
02899b3b75 Updating Everything to work correctly 2020-10-05 06:45:50 +00:00
Max G
bcc7d60fd3 * updated server packages 2020-10-04 18:59:30 +00:00
Max G
df4afeafc6 Updated Squire 2020-10-03 19:15:31 +00:00
Max G
1d891ea734 * Added an auth screen that isn't integrated at all or working
* Force logout of user with authorization error
* Wrong site blocker doesn't trigger on the solid scribe domain
* Added log out button to main side bar making it easier to find
* Improved icon set for notes
* Colored notes display better on mobile, fixed text color based on color brightness
* Moved terms of use link to the bottom of a few pages
* Updated feature sections on home page, make them clearer and easier to process
* Tweaked color themes
* Deleted links no longer show up in search
* Updated search to use multiple key words
* Updated tests to do a multi word search
* Tweaked a bunch of styles to look better on chrome and browsers
2020-08-03 02:40:27 +00:00
Max G
3447b2e0e6 * Added fake site warning
* Fixed a bunch of style bugs for chrome browsers
* Improved check box styles on desktop and mobile
* Touch up tool tip styles. Only dark now.
* Created a separate terms page
* Added 2FA auth token options to login
* Added tool tip displays to some buttons on editor
* Added pinned and archived options to overflow menu
* Changed shared note styles
* Disabled Scroll into view
* Made image display smaller when adding images to notes
* Added a last used color option
* Updated help page
* Fixed spelling error on terms page
* Added a big ass green label on the new note icon
* Scratch pad now opens a note, which is the scratch pad
* Added better 2fa guide
* Added change password option
* Added log out and log out all active sessions option
* Added strict rate limiting on login and register actions
* Added middleware to routes that force authentication to be accessed
* Fixed bug that was causing shared notes to appear empty
* Updated option now appears on shared notes after they are actually updated
2020-07-23 05:00:20 +00:00
Max G
e7d1cc7bc9 * Added theme colors to form fields
* Added some basic table styles for inserting some shitty tables
* Made popup notification styles look better and work better on mobile
* Quick note now opens a note and not some weird page
* Menu collapses when page is small, behaves like mobile menu
* Added terms and conditions to help and login forms
* Added password change functionality
* Better styles for shared page
* Added some tests for changing password
2020-07-14 05:31:02 +00:00
Max G
47fff0e1ee Added privacy policy
Updated marketing
Added some keyboard shortcuts
Added settings page
Added accent theming
Added beta 2FA
2020-07-07 04:04:55 +00:00
Max G
cca89a60d8 Update
* Added more version icons
* Added working sign to notes when archived or tagged
* Big sexy marketing update
* Clicking Tags now opens them in their tag category
2020-07-03 03:25:38 +00:00
Max G
a56ade5b08 Updated all global and local client packages
* Tweaked sessions to be a little more reliable on mobile
2020-06-21 02:07:36 +00:00
Max G
39f9a16fff * Updated server packages 2020-06-21 01:07:27 +00:00
Max G
6740200a33 Removed some transitions from tooltips 2020-06-21 01:01:12 +00:00
Max G
e4fae23623 * Added Much better session Management, key updating and deleting
* Force reload of JS if app numbers dont match
* Added cool tag display on side of note
* Cleaned up a bunch of code and tweaked little things to be better
2020-06-15 09:02:20 +00:00
Max G
56d4664d0d * Added new token system to add more security to logins
* Added simple tag editing from note page
2020-06-10 04:41:52 +00:00
Max G
d349fb8328 * 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!?
2020-06-07 20:57:35 +00:00
Max G
09cccf1983 * Small hack to fix images not closing window on mobile
* Made note active text modifier buttons better
* Fixed Colored notes being to big on mobile
2020-05-22 07:08:45 +00:00
Max G
97e7b011d9 * Fixed cursor clicking ToDo lists clicking to early
* Added login form to home page with focus on load
* Tags update after editing tags from title card
* Fixed uploading of images/files
* Fixed images not appearing when opening images tab
* Search hits all categories on search, like archived
* Got rid of brand icons to reduce size
* Got rid of DiffPatchMatch and Crypto from note input panel to reduce size
* Disabled animation on io events so they don't annoy the shit out of people on other computers
2020-05-20 07:57:15 +00:00
Max G
fc1f3f81fe Bugfix Batch
* Animations disabled on remote events, closing note still triggers animation for local user
* Created save icons to fix display on mobile
* Hidden URLs are hidden until note is deleted or URL is removed from note
* Tags search all categories, but probably not trash
* Back to all notes button clears search
* Deleted Notes are removed from search index
2020-05-19 03:38:43 +00:00
Max G
9c4fff7913 * Removed arrows from notification
* Added trash can function
* Tweaked status text to always be the same
* Removed some open second note code
* Edior always focuses on text now
* Added some extra loading note messages
* Notes are now removed from search index when deleted
* Lots more things happen and update in real time on multiple machines
* Shared notes can be reverted
* WAY more tests
* Note Categories are much more reliable
* Lots of code is much cleaner
2020-05-18 07:45:35 +00:00
Max G
b0eee636b5 * Made splash page dark and updated description
* Cleaned up unused things
* Updated squire which had a comment typo update...thats it
* Background color picker has matching colors and styles to text color picker
* Added new black theme
* Moved search to main page, show it on mobile and added options to push things to notes from search with experimental tag searching
* Added active note menu buttons based on cursor location in text
* Added more instant updating if app is open in two locations for the same user Scratch Pad and home page update with new notes and new text in real time
2020-05-15 23:12:09 +00:00
Max G
2861042485 * Delete Crunch Menu Component
* Disabled Quick Note
* Note crunches over when menu is open
* Added a cool loader
* Remomoved locked notes
* Added full note encryption
* Added encrypted search index
* Added encrypted shared notes
* Made search bar have a clear and search button
* Tags only loade when clicking on the tags menu
* Tweaked home page to be a little more sane
* built out some gigantic test cases
* simplified a lot of things to make entire app easier to maintain
2020-05-10 21:15:59 +00:00
Max G
1005913c0b Fully Encrypted notes Beta
* Encrypts all notes going to the database
* Creates encrypted snippets for loading note title cards
* Creates an encrypted search index when note is changed
* Migrates users to encrypted notes on login
* Creates new encrypted master keys for newly logged in users
2020-05-06 07:10:27 +00:00
Max G
c8033588dd Major Update: Changed Text Input View
* Created new toolbar that moves on mobile
2020-05-02 19:10:20 +00:00
Max G
bcb31e9af5 Tweaked shrinking buttons for better display on mobile 2020-04-16 01:41:47 +00:00
Max G
596e57eaf0 * Tags Dropbown Beta...kinda crappy 2020-04-15 21:54:36 +00:00
Max G
d91b0735fd * Little Bug Fixes All Around 2020-04-15 20:44:24 +00:00
Max G
71f909fb76 * Made dispay of last edit smaller on note title display card
* Made note menu buttons look better on mobile
* Moved around some note menu buttons
* Added a color picker with a rip off of google colors
* Added a remove formatting button
* Hide pin and archive icons, they appear green on hover, in the buttons
* Further simplified display card logic, now it just adds an end tag and returns the data
* Changed highlight text color to show colors (works on chrome...)
2020-04-15 06:28:58 +00:00
Max G
a44bca204c * Added error display to every axios server call
* Added better destroy of login token if invalid
* Block users from opening notes they don't own, note closes automatically
* Beefed up login and home page a little to make them more appealing
2020-04-14 05:09:19 +00:00
Max G
7c15427b3d * Added placeholder text to site when loading JS
* Added hidden text to site for scraping
* Login token will be destroyed if fetch site totals is called and the token is bad
* Moved passwords out of application and into a .env file that is loaded on startup
* Changed prod database password for primary user (which is dev)
* Set up .env for dev and prod
2020-04-13 07:44:57 +00:00
Max G
ed4a5e5291 * Added some better base information to site for scrapers
* Updated help text
* Refactored a lot of the scrape code into a SiteScrape helper
2020-04-13 06:17:37 +00:00
Max G
c11f1b1b6f Big Update:
* Menus open and close based on URL, allowing for back button on note menus to close

Minor Updates:
* Made night mode buttons green
* Widend the global menu
* Added a version display
* Made the create note button real big
* Made the creane note button more visible on mobile
* Hide the note button if there are no notes
* Changed quick menu item to "Quick Note"
* Added reload option if version is clicked
* Moved around menu buttons at the bottom of the note
* Moved tags back into the main footer on note
* Disabled hiding of toolbar on mobile when editor focused
* Updated locked note display on main title card
* Put last edit on note display
* Tweaked display styles to be more minimal, added fade-in on hover
* Added solid scribe to all title displays on the site
* Reactivated help page and put some good help on it...decent help
* Increased max upload size for files to 5MB
* Shortened text on title display cards to make them all the same size
2020-04-10 03:47:15 +00:00
Max G
0b5675e000 Added package lock 2020-03-30 05:32:46 +00:00
Max G
9309ea0821 * More aggressive dark theme styles, changing default icon colors and notification colors
* Better sortig of archived notes which clicking archived
* Scroll to closed note and show animation on save
* Better notification styles, more obvious
2020-03-30 05:31:09 +00:00
Max G
5975ab6d68 Update to latest version of squire. fixes #30
Already update later version of Fomantic. fixes #30
2020-03-29 23:08:22 +00:00
Max G
3d6e527e3a Small change to make user all note option menus fade in 2020-03-29 23:01:37 +00:00
Max G
88a0c7b26a Removed Semantic Added Fomantic 2020-03-26 05:05:31 +00:00
Max G
1b14a8fd31 Added rate limiting and server security
Ton of little visual style tweaks and little up improvements for mobile
2020-03-26 04:45:23 +00:00
Max G
4cc6014581 Remove style making double editing weird
fixes #29
2020-03-14 18:52:00 +00:00
Max G
196224d0b8 Bug fixes and encryption handling 2020-03-14 06:04:03 +00:00
Max G
795f1b7d76 Pressing enter in a note title moves cursor to end of note. 2020-03-14 02:09:43 +00:00
Max G
1600bd132c Minor bug fixes 2020-03-13 23:51:45 +00:00
Max G
2a379f8a4e Encrypted Notes Alpha!
fixes #28
2020-03-13 23:34:32 +00:00
Max G
3ed26bcc03 Updated to later version of vue cli to improve build process
* Updated some simples styles
* Added archive button to main notes
fixes #21
2020-03-11 03:47:07 +00:00
Max G
282cbfe7bc Added note tags to main note edit display 2020-03-09 03:11:05 +00:00
Max G
b50aecdfca Creating new tags doesn't throw an error
fixes #22
2020-03-04 05:20:14 +00:00
Max G
98f4695739 Tweaked display of note cards, again
* Added an option to pin notes, on the main screen
2020-03-02 05:33:49 +00:00
Max G
984ac6ccff Little green more note indicator.
fixes #23
2020-03-01 00:54:54 +00:00
Max G
f63c0c0d60 Testing new note display cards that use flexbox
Testing new simplified text processes, for smaller notes, it just sends all the text
2020-02-27 07:14:29 +00:00
Max G
a478cbe11c Minor Style Tweaks
* Changed the way the note menus display
* Added a blank note indicator text
* Added date created and date updated text to bottom of note

fixes #18
fixes #5
2020-02-27 04:50:08 +00:00
Max G
99b69c234f Added new status to shared notes
* Shared notes can be new or updated
* New - Note has never been opened
* Updated - Shared note was read but modified by other user
2020-02-24 06:09:28 +00:00
Max G
f0b6d7b85e Shared notes now share updated times
* Updating a shared note, updates the information for other shared users
* Unread shared notes now have a badge
* Updated shared notes now have a badge
* Shared notes can not be reshared, sharer username appears in interface to stop sharing
fixes #15
2020-02-24 06:01:14 +00:00
Max G
596703a963 * Fixed title display issue on note 2020-02-23 06:46:23 +00:00
Max G
21f606b480 * Fixed a bunch of little bugs
* Added more options to attachment page and filters
* Much better rendering and updating on attachment page
* Math bug is fixed with better string parsing fixes #14
* Icons are limited to 4 per note
* If an image is visible on note preview it will not appear in images preview
* Touched up text algorithm to better display note titles
2020-02-23 06:27:49 +00:00
Max G
b961a69a91 Added a function to calculate math on notes 2020-02-19 00:31:18 +00:00
Max G
8d3762e106 Better sorting of note categories
fixes #1
2020-02-18 22:52:12 +00:00
Max G
b2f241dbba Added paste event to quick notes allowing for quick note to submit the second data is pasted in
Added option on quick note to submit with enter or CTRL + ENTER
Removed a console log statement.
2020-02-14 05:31:38 +00:00
Max G
8833a213a7 Added some realtime events to the app
* When a user gets a new shared message, it will popup instantly
* When a new website is scraped, it will update in real time
* Various other little bug fixes and improvements
* Sharing displays correct notes and handles shared notes correctly
* Tags were not displaying on notes, they do now. They better.
2020-02-14 01:08:46 +00:00
Max G
f833845452 * Search bar only appears in header menu on mobile
* Added tooltip to logout button
* Tags follow archived, inbox, main note fast filters
2020-02-12 05:29:56 +00:00
Max G
05152cd5a4 Added counts to each category
Counts update on certain events and show or hide various elements
Fixed various little ui element issues

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

View File

@@ -16,9 +16,6 @@ gzip "backup-$NOW.sql"
echo "Database Backup Complete on $NOW"
# Delete all but last 8 files
ls -tp | grep -v '/$' | tail -n +9 | tr '\n' '\0' | xargs -0 rm --
##
# Restore DB
##

View File

@@ -17,14 +17,13 @@
body {
margin: 0;
padding: 0;
/* overflow-x: hidden;*/
overflow-x: hidden;
min-width: 320px;
background: green;
background: #FFFFFF;
font-family: 'Roboto', system-ui, -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 14px;
line-height: 1.4285em;
color: rgba(0, 0, 0, 0.87);
position: relative;
}
:root {
@@ -96,7 +95,7 @@ body {
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
#app {
/* background: var(--body_bg_color);*/
background: var(--body_bg_color);
}
.ui.segment {
@@ -592,15 +591,6 @@ padding-right: 10px;
color: var(--main-accent);
opacity: 1;
}
/* Remove indent line on mobile */
.note-card-text > ol > ol,
.squire-box > ol > ol,
.note-card-text > ul > ul,
.squire-box > ul > ul
{
border-left: none;
}
}

View File

@@ -117,16 +117,11 @@
<a class="link" :href="linkUrl" target="_blank">{{linkText}}</a>
<!-- Buttons -->
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openNote">
<div class="ui small compact basic button" v-on:click="openNote">
<i class="file outline icon"></i>
Open Note
</div>
<div v-if="!item.note_id" class="ui small compact basic disabled button">
<i class="angle double up icon"></i>
Pushed from Web
</div>
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openEditAttachments"
<div class="ui small compact basic button" v-on:click="openEditAttachments"
:class="{ 'disabled':this.searchParams.noteId }">
<i class="folder open outline icon"></i>
Note Files

View File

@@ -87,7 +87,6 @@
margin: 0;
padding: 0;
overflow: hidden;
}
.place-holder {
width: 100%;

View File

@@ -4,7 +4,7 @@
<div>
<!-- thicc form display -->
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register">
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register()">
<div class="field">
<div class="ui input">
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
@@ -15,11 +15,6 @@
<input v-model="password" type="password" name="password" placeholder="Password">
</div>
</div>
<div class="field">
<div class="ui input">
<input v-model="password2" type="password" name="password2" placeholder="Re-type Password">
</div>
</div>
<div class="field" v-if="require2FA">
<div class="ui input">
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
@@ -29,10 +24,17 @@
<div class="ui fluid buttons">
<div v-on:click="register" class="ui green button" :class="{ 'disabled':(username.length == 0 || password.length == 0)}">
<div v-on:click="register()" class="ui green button" :class="{ 'disabled':(username.length == 0 || password.length == 0)}">
<i class="plug icon"></i>
Sign Up
</div>
<div class="or"></div>
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="login()" class="ui button">
<i class="power icon"></i>
Login
</div>
</div>
</div>
@@ -47,12 +49,12 @@
</div>
<!-- Thin form display -->
<div v-if="thin" class="ui small form" v-on:keyup.enter="login">
<div v-if="thin" class="ui small form" v-on:keyup.enter="login()">
<div v-if="!require2FA" class="field"><!-- hide this field if someone is logging in with 2FA -->
<div class="ui grid">
<div class="ui sixteen wide center aligned column">
<div v-on:click="register" class="ui green button">
<div v-on:click="register()" class="ui green button">
<i class="plug icon"></i>
Sign Up Now!
</div>
@@ -85,7 +87,7 @@
</div>
</div>
<div class="field">
<div v-on:click="login" class="ui fluid button">
<div v-on:click="login()" class="ui fluid button">
<i class="power icon"></i>
Login
</div>
@@ -126,7 +128,6 @@
enabled: false,
username: '',
password: '',
password2: '',
authToken: '',
require2FA: false,
}
@@ -159,21 +160,13 @@
},
register(){
let error = false
if( this.username.length == 0 || this.password.length == 0 ){
if( this.username.length == 0 || this.password.length == 0 || this.password2.length == 0 ){
if(this.$route.name == 'LoginPage'){
this.$bus.$emit('notification', 'Both a Username and Password are Required')
return
}
this.$bus.$emit('notification', 'All fields are required.')
error = true
}
if( this.password !== this.password2 ){
this.$bus.$emit('notification', 'Passwords must be identical.')
error = true
}
if(error){
//Login section
this.$router.push('/login')
return

View File

@@ -1,431 +0,0 @@
<style type="text/css" scoped>
.an-graph {
background: #fefefe;
}
.inactive.segment {
}
.active.segment {
outline: 4px solid cyan;
outline-offset: -5px;
outline-style: dashed;
max-height: 2000px;
}
.not-padded {
margin-left: -5px;
margin-right: -5px;
margin-bottom: -10px;
padding-right: 5px;
padding-left: 5px;
}
.sticky-boy {
position: fixed;
top: -1px;
right: 10px;
z-index: 100;
width: 70%;
background: orange;
}
.animate-height {
transition: max-height 0.8s linear;
max-height: 450px;
overflow: hidden;
}
</style>
<template>
<div>
<div class="ui very compact grid" :class="{'sticky-boy':editGraphs}">
<div class="sixteen wide column" v-if="!editGraphs">
<div class="ui basic padded segment">
<!-- Just a space to keep things clickable -->
</div>
</div>
<div class="sixteen wide column">
<dix class="ui basic segment" v-if="!editGraphs">
<div class="ui button" v-on:click="toggleEditGraphs">
<i class="edit icon"></i>
<span>Add/Edit Graphs</span>
</div>
</dix>
<div v-if="editGraphs">
<div class="ui green button" v-on:click="addGraph()">
<i class="plus icon"></i>
New Graph
</div>
<div class="ui basic button" v-on:click="toggleEditGraphs">
<i class="check circle icon"></i>
Done Editing Graphs
</div>
</div>
</div>
</div>
<div v-for="(graph, index) in graphs" :class="`ui not-padded ${editGraphs?'active ':'inactive '}segment animate-height`">
<!-- Edit options -->
<div class="ui small header" v-if="editGraphs">
<div class="ui grid">
<div class="eight wide column">
<b>Graph #{{ index+1 }}</b>
</div>
<div class="eight wide right aligned column">
<span class="ui tiny compact inverted red button" v-on:click="removeGraph(index)">
Remove Graph
<i class="close icon"></i>
</span>
</div>
</div>
</div>
<h3 class="ui center aligned dividing header">
{{ getGraphTitle(graph) }}
</h3>
<div v-if="graph?.type == PILL_CALENDAR">
<PillCalendarGraph
:graph="graph"
:tempChartDays="tempChartDays"
:userFields="userFields"
:cycleData="cycleData"
:edit-graphs="editGraphs"
:showZeroValues="graph?.options?.showZeroValues"
:showTextValues="graph?.options?.showTextValues"
:connectDays="graph?.options?.connectDays"
:hideValues="graph?.options?.hideValues"
:hideIcons="graph?.options?.hideIcons"
/>
<div v-if="editGraphs" class="ui segment">
<p>Calendar Graph Toggles</p>
<div v-on:click="toggelValue(index, 'hideIcons')"class="ui button">
<span v-if="graph?.options?.hideIcons">Show</span><span v-else>Hide</span> Icons
</div>
<div v-on:click="toggelValue(index, 'hideValues')"class="ui button">
<span v-if="graph?.options?.hideValues">Show</span><span v-else>Hide</span> Values
</div>
<div v-on:click="toggelValue(index, 'showZeroValues')"class="ui button">
<span v-if="!graph?.options?.showZeroValues">Show</span><span v-else>Hide</span> Lowest Value
</div>
<div v-on:click="toggelValue(index, 'showTextValues')"class="ui button">
<span v-if="!graph?.options?.showTextValues">Show</span><span v-else>Hide</span> Text Value
</div>
<div v-on:click="toggelValue(index, 'connectDays')"class="ui button">
<span v-if="!graph?.options?.connectDays">Connect</span><span v-else>Disconnect</span> Days
</div>
</div>
</div>
<div v-if="graph?.type == LAST_DONE">
Last done not implemented
</div>
<div v-if="!graph.fieldIds || graph.fieldIds && graph.fieldIds.length == 0">
<h5>Blank Graph</h5>
<span v-if="!editGraphs">Click "Edit Graphs" then,</span>
Select Graph type and Metrics to display
</div>
<div v-if="graph?.type == undefined && graph.fieldIds && graph.fieldIds.length > 0">
<div :id="`graphdiv${index}`" style="width: 100%; min-height: 320px;"></div>
</div>
<div class="ui segment" v-if="editGraphs">
<!-- change graph type -->
<div v-for="(graphType, graphId) in graphTypesDef" class="ui buttons">
<div class="ui tiny button" v-on:click="changeGraphType(index, graphId)" :class="{'green':(String(graphId) == String(graph?.type))}">
{{ graphType }}
</div>
</div>
<div v-for="fieldId in fields">
<span v-if="graph.fieldIds && graph.fieldIds.includes(fieldId)" v-on:click="toggleGraphField(fieldId, index)">
<i class="green check square icon"></i>
</span>
<span v-else v-on:click="toggleGraphField(fieldId, index)">
<i class="square outline icon"></i>
</span>
<i :class="`${$parent.getFieldColor(fieldId)} ${$parent.getFieldIcon(fieldId)} icon`"></i>
<b>{{ userFields[fieldId]?.label }}</b>
</div>
</div>
</div>
<div class="ui very compact grid" :class="{'sticky-boy':editGraphs}">
<div class="sixteen wide column" v-if="!editGraphs">
<div class="ui basic padded segment">
<!-- Just a space to keep things clickable -->
</div>
</div>
<div class="sixteen wide column">
<dix class="ui basic segment" v-if="!editGraphs">
<div class="ui button" v-on:click="toggleEditGraphs">
<i class="edit icon"></i>
<span>Add/Edit Graphs</span>
</div>
</dix>
<div v-if="editGraphs">
<div class="ui green button" v-on:click="addGraph()">
<i class="plus icon"></i>
New Graph
</div>
<div class="ui basic button" v-on:click="toggleEditGraphs">
<i class="check circle icon"></i>
Done Editing Graphs
</div>
</div>
</div>
</div>
<!-- Anchor for scrolling to the bottom of graphs -->
<div ref="anchor"></div>
</div>
</template>
<script>
const PILL_CALENDAR = 'pillCalendar'
const LAST_DONE = 'lastDone'
export default {
name: 'MetricTrackingGraphs',
props: [
'tempChartDays', // Number of days to display
'fields', // field IDs for display/order
'userFields', // field values defined by user
'graphs', // Graph data defined by user
'cycleData', // ALL user data
'calendar', // Date data for currently open day
'editGraphs' // boolean for edit or not edit graphs
],
components: {
'PillCalendarGraph':require('@/components/Metrictracking/PillCalendarGraph.vue').default,
},
data: function(){
return {
graphTypesDef:{
// [LAST_DONE]: 'Last Done',
'undefined':'Line Graph (Default)',
[PILL_CALENDAR]:'Calendar Graph',
},
localGraphData:[],
}
},
beforeCreate() {
// Constants
this.PILL_CALENDAR = PILL_CALENDAR
this.LAST_DONE = LAST_DONE
// Include JS libraries
let graphsScript = document.createElement('script')
graphsScript.setAttribute('src', '//cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js')
document.head.appendChild(graphsScript)
},
mounted(){
this.localGraphData = this.graphs
this.graphCurrentData()
},
updated(){
// update graphs here? Or watch graphs prop
},
watch: {
// whenever question changes, this function will run
userFields(newFields, oldFields) {
// console.log([newFields, oldFields])
if( JSON.stringify(oldFields) == "{}" ){
this.graphCurrentData()
}
},
tempChartDays(newDays, oldDays){
if( newDays != oldDays ){
this.graphCurrentData()
}
},
},
methods: {
saveGraphs(){
this.$emit('saveGraphs', this.localGraphData)
},
toggleEditGraphs(){
setTimeout(() => {
// scroll last graph into view
this.$refs.anchor.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
})
}, 800)
this.$emit('toggleEditGraphs')
},
changeGraphType(index, newType){
console.log(index + ' change to ' + newType)
this.localGraphData[index]['type'] = newType
this.saveGraphs()
},
addGraph(){
this.localGraphData.push({})
this.saveGraphs()
},
removeGraph(index){
this.localGraphData.splice(index, 1)
this.saveGraphs()
},
toggelValue(graphIndex, optionName){
if(!this.localGraphData[graphIndex].options){
this.localGraphData[graphIndex].options = {}
}
if(this.localGraphData[graphIndex].options[optionName]){
this.localGraphData[graphIndex].options[optionName] = false
}
else {
this.localGraphData[graphIndex].options[optionName] = true
}
console.log(this.localGraphData[graphIndex].options[optionName])
this.saveGraphs()
},
toggleGraphField(fieldId, graphIndex){
if(!Array.isArray(this.localGraphData[graphIndex].fieldIds)){
this.localGraphData[graphIndex].fieldIds = []
}
const inSetCheck = this.localGraphData[graphIndex]?.fieldIds.indexOf(fieldId)
if(inSetCheck == -1){
this.localGraphData[graphIndex]?.fieldIds.push(fieldId)
}
if(inSetCheck > -1){
this.localGraphData[graphIndex]?.fieldIds.splice(inSetCheck,1)
}
this.saveGraphs()
},
getGraphTitle(graph){
const graphFields = graph?.fieldIds || []
let fieldTitles = []
graphFields.forEach(fieldId => {
fieldTitles.push(this.userFields[fieldId]?.label)
})
// console.log(fieldTitles)
const title = fieldTitles.join(', ')
return title
},
graphCurrentData(){
// try again if dygraphs isn't loaded
if( typeof(window.Dygraph) != 'function' ){
setTimeout(() => {
this.graphCurrentData()
}, 100)
return
}
const graphOptions = {
interactionModel: {},
// pointClickCallback: function(e, pt){
// console.log(e)
// console.log(pt)
// console.log(this.getValue(pt.idx, 0))
// }
}
// Excel date format YYYYMMDD
const convertToExcelDate = (dateCode) => {
return dateCode
.split('.')
.reverse()
.map(item => String(item).padStart(2,0))
.join('')
}
// Generate set of keys for graph length
let dataKeys = Object.keys(this.cycleData)
dataKeys = dataKeys.splice(0, this.tempChartDays)
console.log(dataKeys)
// build CSV data for each graph
this.graphs.forEach((graph,index) => {
// only chart line graphs with dygraphs
if( graph.type != undefined ){
return
}
if( !graph.fieldIds ){
return
}
// CSV or path to a CSV file.
let dataString = ""
// Lookup graph field titles
let graphLabels = ['Date']
graph.fieldIds.forEach(fieldId => {
const graphLabel = this.userFields[fieldId]?.label
const escapedLabel = graphLabel.replaceAll(',','')
graphLabels.push(escapedLabel)
})
dataString += graphLabels.join(',') + '\n'
// build each row, for each day
for (var i = 0; i < dataKeys.length; i++) {
let nextFragment = []
// push date code to first column
nextFragment.push(convertToExcelDate(dataKeys[i]))
graph.fieldIds.forEach(fieldId => {
const currentEntry = this.cycleData[dataKeys[i]]
let currentValue = currentEntry[fieldId]
// setup correct float graphing
if(fieldId == 'BT'){
// parse temp to fixed length float 00.00
currentValue = parseFloat(currentValue).toFixed(2)
}
if( currentValue == undefined ){
currentValue = -1
}
nextFragment.push(currentValue)
})
dataString += nextFragment.join(',') + "\n"
}
let graphDiv = document.getElementById("graphdiv"+index)
const g = new Dygraph(graphDiv, dataString ,graphOptions)
})
return
},
}
}
</script>

View File

@@ -1,548 +0,0 @@
<style type="text/css" scoped>
div.calendar {
width: calc(100% - 4px);
min-height: 350px;
display: flex;
margin: 5px 8px 15px;
flex-wrap: wrap;
flex-direction: row;
justify-content: flex-start;
}
.day {
flex: 0 0 calc(14.28% - 2px);
min-height: 50px;
border: 1px solid var(--border_color);
font-size: 1.2em;
overflow: hidden;
box-sizing: border-box;
position: relative;
line-height: 1em;
display: flex;
align-items: flex-end;
}
.today {
font-weight: bold;
text-decoration: underline;
}
.active-entry {
outline: #07f4f4;
outline-style: none;
outline-width: medium;
outline-style: none;
outline-offset: -1px;
outline-style: solid;
outline-width: 3px;
}
.day ~ .has-data {
}
.day ~ .no-data {
background: #c7c7c787;
opacity: 0.6;
}
.day > .number {
position: absolute;
top: 0;
right: 5px;
z-index: 10;
opacity: 0.4;
}
.day > .sex {
font-size: 0.7em;
border-radius: 5px;
background: rgba(249, 0, 0, 0.15);
color: white;
padding: 0 0 0 4px;
z-index: 10;
position: absolute;
left: 0;
height: 26px;
}
.day > .period {
position: absolute;
bottom: 1px;
left: 1px;
right: 1px;
height: 5px;
background: red;
z-index: 10;
}
.day > .mucus {
position: absolute;
bottom: 0;
left: 0;
right: 0;
min-height: 10px;
background: #abecff7d;
z-index: 2;
}
.day > .notes {
}
.pill-container {
width: 100%;
}
.pill {
width: calc(100% - 8px);
min-height: 2px;
margin: 0 4px;
box-sizing: border-box;
display: inline-block;
background: rgb(50 218 255 / 44%);
border-radius: 40px;
text-align: center;
line-height: 1em;
position: relative;
color: white;
font-size: 0.7em;
padding: 2px;
overflow: hidden;
white-space: nowrap;
}
.pill.did-last {
margin-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
width: calc(100% - 5px);
}
.pill.did-next {
margin-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
width: calc(100% - 5px);
}
.pill.did-next.did-last {
width: 100%;
}
/* .last-high:after {
content: '';
width: 0;
height: 0;
border-top: 15px solid transparent;
border-bottom: 3px solid transparent;
border-left: 10px solid rgb(50 218 255 / 44%);
position: absolute;
left: 0;
top: -13px;
}
.next-high:before {
content: '';
width: 0;
height: 0;
border-top: 15px solid transparent;
border-bottom: 3px solid transparent;
border-right: 10px solid rgb(50 218 255 / 44%);
position: absolute;
right: 0;
top: -13px;
}*/
.big-day {
display: inline-block;
width: 100%;
min-height: 2px;
margin: 0 auto;
text-align: center;
}
.zero-day {
opacity: 0.5;
}
.icon-spacer {
display: inline-block;
background-color: greenyellow;
width: 20px;
height: 2px;
}
.past-entries {
width: 100%;
display: flex;
justify-content: space-around;
/* padding: 0 10px;*/
overflow-x: scroll;
overflow-y: hidden;
}
.past-entry {
position: relative;
text-align: center;
border: 1px solid;
border-color: var(--dark_border_color);
color: var(--text_color);
flex-grow: 1;
cursor: pointer;
font-weight: bold;
min-width: 40px;
min-height: 40px;
margin: 5px 0 10px;
line-height: 2.3em;
}
.day-list {
width: 100%;
height: 80px;
background-color: green;
display: flex;
justify-content: space-around;
overflow-x: scroll;
overflow-y: hidden;
}
.day-list-item {
flex-grow: 1;
border: 1px solid black;
width: 25px;
}
.pill.red { background-color: #db2828 }
.pill.orange { background-color: #f2711c }
.pill.yellow { background-color: #fbbd08 }
.pill.olive { background-color: #b5cc18 }
.pill.green { background-color: #21ba45 }
.pill.teal { background-color: #00b5ad }
.pill.blue { background-color: #2185d0 }
.pill.violet { background-color: #6435c9 }
.pill.purple { background-color: #a333c8 }
.pill.pink { background-color: #e03997 }
.pill.brown { background-color: #a5673f }
.pill.grey { background-color: #767676 }
.pill.black { background-color: #1b1c1d }
</style>
<template>
<div>
<div class="calendar">
<div v-for="day in calendar.weekdays" class="day">
{{ day }}
</div>
<div v-for="day in calendar.days" class="day"
:class="{
'today':day == calendar.today,
'active-entry':calendar.dateCode == `${day}.${calendar.month}.${calendar.year}`,
'has-data':cycleData[`${day}.${calendar.month}.${calendar.year}`],
'no-data':showDayDataColor(day),
}">
<!-- v-on:click="openDayData(`${day}.${calendar.month}.${calendar.year}`)" -->
<span class="number">{{ day }}</span>
<!-- {{ `${day}.${calendar.month}.${calendar.year}` }} -->
<span class="pill-container" v-for="(entry, dateCode) in getChartData" v-if="dateCode == `${day}.${calendar.month}.${calendar.year}`">
<span
v-for="(dayData, fieldId) in entry"
v-if="showZeroValuesCheck(dayData.value, fieldId)"
class="pill"
:class="[$parent.$parent.getFieldColor(fieldId), {
'did-next':dayData.didNext,
'did-last':dayData.didLast,
'last-high':dayData.lastHigh,
'next-high':dayData.nextHigh,
}]">
<!-- 'zero-day':dayData.value == lowestGraphValue, -->
<!-- <i v-if="dayData.value != 0" :class="`tiny ${$parent.$parent.getFieldColor(fieldId)} ${$parent.$parent.getFieldIcon(fieldId)} icon`"></i> -->
<!-- <span v-else class="icon-spacer"></span>
:style="{height:(Math.round(dayData.value*5)+'px')}"
-->
<span v-if="dayData.value > lowestGraphValue-1" class="big-day">
<i v-if="!hideIcons" :class="`tiny white ${$parent.$parent.getFieldIcon(fieldId)} icon`"></i>
<span v-if="!hideValues">
{{ getDayValue(fieldId, dayData.value) }}
</span>
</span>
</span>
</span>
<!-- <span v-for="fieldId in graph.fieldIds"></span> -->
</div>
</div>
</div>
</template>
<script>
// let chartData = {}
export default {
props: [
'graph', // options associated with this graph
'userFields', // all field attributes
'tempChartDays', // number of days to display
'cycleData', // all users metric data
'editGraphs', // display additional edit options
// Graph options
'showZeroValues', // Hide graph data with value of zero
'showTextValues', // Show button text or button value
'connectDays', // Calculates next and previous day connections.
'hideValues', // Hide all values on the graph
'hideIcons', // option to hide icons
],
data: function(){
return {
openModel:true,
calendar: {
dateObject: null,
dateCode: null,
monthName: '',
dayName:'',
daysAgo:0,
month: '',
year: '',
days: [],
weekdays: ['S','M','T','W','T','F','S'],
today: 0,
},
chartDateCodes: [], // array of date codes in chart
listDateCodes: [],
dayList: true,
lowestGraphValue: 0,
}
},
mounted(){
this.setupCalendar(new Date())
},
computed: {
getChartData(){
let chartData = {}
let chartValues = []
// iterate every day in month by day code
this.chartDateCodes.forEach((chartDayCode, codeIndex) => {
// lookup data for that day
const cycleDayData = this.cycleData[chartDayCode]
// if chart data is set for this day
if( cycleDayData && Object.keys(cycleDayData).length > 0){
chartData[chartDayCode] = {}
// go over each field to be displayed on graph
this.graph.fieldIds.forEach((graphFieldId) => {
if( cycleDayData[graphFieldId] == undefined ){
return
}
// track all chart values
chartValues.push(cycleDayData[graphFieldId])
chartData[chartDayCode][graphFieldId] = {
didLast: false,
lastHigh: false,
didNext: false,
nextHigh: false,
value: cycleDayData[graphFieldId]
}
})
}
})
this.lowestGraphValue = Math.min(...chartValues)
// determine next and previous states for display
this.chartDateCodes.forEach((chartDayCode, codeIndex) => {
if(chartData[chartDayCode] && this.connectDays){
const previousDateCode = this.chartDateCodes[codeIndex-1]
const nextDateCode = this.chartDateCodes[codeIndex+1]
Object.keys(chartData[chartDayCode]).forEach((graphFieldId) => {
const currentValue = chartData[chartDayCode][graphFieldId].value
// check for previous entry
if( chartData[previousDateCode] && chartData[previousDateCode][graphFieldId] ){
chartData[chartDayCode][graphFieldId].didLast = true
// set low value flag
const lastHigh = chartData[previousDateCode][graphFieldId].value > 0
chartData[chartDayCode][graphFieldId].lastHigh = lastHigh && currentValue == 0
}
// check for next entry
if( chartData[nextDateCode] && chartData[nextDateCode][graphFieldId] ){
chartData[chartDayCode][graphFieldId].didNext = true
// set low value flag
const nextHigh = chartData[nextDateCode][graphFieldId].value > 0
chartData[chartDayCode][graphFieldId].nextHigh = nextHigh && currentValue == 0
}
})
}
})
// console.log(chartData)
return chartData
},
},
methods: {
showZeroValuesCheck(dayValue, fieldId){
// if graph type is boolean or there are two options
let isBooleanField = this.userFields[fieldId].type == 'boolean'
if(this.userFields[fieldId].customOptions){
let options = this.userFields[fieldId].customOptions
isBooleanField = options.split(',').length == 2
}
if(isBooleanField && !this.showZeroValues){
const parsedValue = this.getDayValue(fieldId, dayValue)
if(parsedValue == 'Yes'){
return true
} else {
return false
}
}
return this.showZeroValues || dayValue > this.lowestGraphValue
},
getDayValue(fieldId, value){
if( !this.showTextValues ){
return value
}
let options = 'error, Yes, No'
if(this.userFields[fieldId].customOptions){
options = this.userFields[fieldId].customOptions
}
const values = options.split(',')
const selection = String(values[value]).trim()
return selection
},
displayDayFromCode(dateCode){
const parts = dateCode.split('.')
return `${parts[0]}`
},
showDayDataColor(day){
// Determine if day has any data set
if(day == ''){
return false
}
return !(this.cycleData[`${day}.${this.calendar.month}.${this.calendar.year}`])
},
generateDateCode(date){
const dateSetup = [
date.getDate(), // 1-31 (Day)
date.getMonth()+1, // 0-11 (Month)
date.getFullYear(), // 1888-2022 (Year)
]
return dateSetup.join('.')
},
setupCalendar(date){
// visualize each day change
this.working = true
setTimeout(() => {
this.working = false
}, 500)
if(!date && this.dateObject){
date = this.dateObject
}
if(!date){
date = new Date()
}
this.calendar.dateObject = date
this.calendar.dateCode = this.generateDateCode(date)
// calculate days ago since current date
const now = new Date()
const diffSeconds = Math.floor((now - date) / 1000) // subtract unix timestamps, convert MS to S
const dayInterval = diffSeconds / 86400 // seconds in a day
this.calendar.daysAgo = Math.floor(dayInterval)
// ------------
// setup calendar display
var y = date.getFullYear()
var m = date.getMonth()
var firstDay = new Date(y, m, 1);
var lastDay = new Date(y, m + 1, 0);
function getDaysInMonth(year, month) {
return new Date(year, month, 0).getDate();
}
const currentYear = date.getFullYear();
const currentMonth = date.getMonth() + 1;
this.calendar.monthName = date.toLocaleString("en-US", { month: "long" });
this.calendar.dayName = date.toLocaleString("en-US", { weekday: "long" });
this.calendar.year = currentYear
const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth);
const monthStartDay = firstDay.getDay()
let days = Array(monthStartDay).fill(""); // Pad days to start on correct weekday
for (let i = 0; i < daysInCurrentMonth; i++) {
days.push(i+1)
}
this.calendar.days = days
// set today
this.calendar.today = date.getDate()
this.calendar.month = date.getMonth()+1
// setup date codes for key matching on calendar
this.calendar.days.forEach((day) => {
if( day !== "" ){
let dateDay = new Date(y, m, day);
let dayCode = this.generateDateCode(dateDay)
this.chartDateCodes.push(dayCode)
}
})
// generate past date codes for list
for (let i = 0; i < this.tempChartDays; i++) {
const now = new Date()
const pastDate = now.setDate(now.getDate() - i)
const pastDateObj = new Date(pastDate)
const newCode = this.generateDateCode(pastDateObj)
this.listDateCodes.push(newCode)
}
// return codes.reverse()
/*
October 2022
S M T W T F S
1 2 3 4 5 6
7 8 9
*/
// -------
},
}
}
</script>

View File

@@ -30,7 +30,7 @@
@media only screen and (max-width: 740px) {
.modal-content {
width: 100%;
/* padding-bottom: 55px;*/
padding-bottom: 55px;
}
}

View File

@@ -172,13 +172,8 @@
<span class="status-menu" v-on:click=" hash=0; save()">
<span v-if="idleNote" data-position="left center" data-tooltip="Idle: Awaiting Changes">
<i class="vertically flipped grey wifi icon"></i>
</span>
<span v-if="diffsApplied > 0">
<i class="blue wave square icon"></i>
+{{ diffsApplied }}
+{{ diffsApplied }} Unsaved Changes
</span>
<span v-if="usersOnNote > 1" :data-tooltip="`Viewers`" data-position="left center">
@@ -351,10 +346,6 @@
:class="{ 'fade-me-out':sizeDown }"
v-on:click="closeButtonAction()"></div> -->
<div>
</div>
</div>
</template>
@@ -368,8 +359,6 @@
const dmp = new DiffMatchPatch.diff_match_patch()
import SquireButtonFunctions from '@/mixins/SquireButtonFunctions.js'
let rawNoteText = '' // Used for comparing and generating diffs
export default {
name: 'NoteInputPanel',
@@ -401,6 +390,7 @@
created: '',
updated: '',
shareUsername: null,
// diffNoteText: '',
statusText: 'saved',
lastNoteHash: null,
saveDebounce: null, //Prevent save from being called numerous times quickly
@@ -412,11 +402,13 @@
pinned: 0,
archived: 0,
attachmentCount: 0,
attachments: [],
styleObject: { 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, //Style object. Determines colors and badges
sizeDown: false, //Used to animate close state
//Settings vars
lastVisibilityState: null,
//All the squire settings
editor: null,
usersOnNote: 0,
@@ -432,14 +424,10 @@
//Diff text/sync text variables
diffTextTimeout: null,
diffsApplied: null,
idleNote: true, // If note is idle, get updates from server
idleNoteTimeout: null,
reloadNoteDebounce: null,
//Used to restore caret position
lastRange: null,
startOffset: 0,
childIndex: null,
//Tag Display
allTags: [],
@@ -495,12 +483,9 @@
this.$bus.$off('new_file_upload')
this.destroyAttachmentStyles()
this.destroyWebSockets()
window.removeEventListener('blur', this.windowBlurEvent)
window.removeEventListener('focus', this.windowFocusEvent)
document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
//Obliterate squire instance
this.editor.destroy()
@@ -516,9 +501,7 @@
this.forceShowLoading = true
}, 500)
window.addEventListener('blur', this.windowBlurEvent)
window.addEventListener('focus', this.windowFocusEvent)
// this.logNoteInteraction()
document.addEventListener('visibilitychange', this.checkForUpdatedNote)
//Init squire as early as possible
if(this.editor && this.editor.destroy){
@@ -587,31 +570,33 @@
})
},
initSquireEvents(){
initSquire(){
//Set up squire and load note text
this.setText(this.noteText)
// Use squire box HTML for diff/patch changes
rawNoteText = document.getElementById('squire-id').innerHTML
//focus on open, not on mobile, it causes the keyboard to pop up, thats annoying
if(!this.$store.getters.getIsUserOnMobile){
this.editor.focus()
this.editor.moveCursorToEnd()
}
//Set up websockets after squire is set up
setTimeout(() => {
this.setupWebSockets()
}, 500)
this.editor.addEventListener('cursor', e => {
this.saveCaretPosition(e)
//Save range to replace cursor if someone else makes an update
this.lastRange = e.range
this.startOffset = parseInt(e.range.startOffset)
return
})
//Change button states on editor when element is active
//eg; Bold button turns green when on bold text
this.editor.addEventListener('pathChange', e => {
this.pathChangeEvent(e)
this.diffText(e)
})
this.editor.addEventListener('pathChange', e => this.pathChangeEvent(e))
//Click Event - Open links when clicked in editor or toggle checks
this.editor.addEventListener('click', e => {
@@ -696,20 +681,10 @@
})
})
//Bind event handlers
this.editor.addEventListener('keyup', event => {
this.onKeyup(event)
this.diffText(event)
this.logNoteInteraction()
})
this.editor.addEventListener('focus', e => {
this.logNoteInteraction()
// this.diffText(e)
})
this.editor.addEventListener('blur', e => {
this.idleNote = true
this.diffText(e)
})
// this.editor.addEventListener("dragstart", e => {
@@ -717,6 +692,12 @@
// console.log(e)
// if(){}
// });
//Show and hide additional toolbars
// this.editor.addEventListener('focus', e => {
// })
// this.editor.addEventListener('blur', e => {
// })
},
openEditAttachment(){
@@ -779,18 +760,13 @@
//Setup all responsive vue data
this.setupLoadedNoteData(response)
this.loading = false
this.$nextTick(() => {
//Adjust note title size after load
this.titleResize()
this.initSquireEvents()
//Set up websockets after squire is set up
setTimeout(() => {
this.initWebsocketEvents()
this.loading = false
}, 500)
this.initSquire()
})
})
@@ -810,11 +786,14 @@
this.created = response.data.created
this.updated = response.data.updated
this.lastInteractionTimestamp = +new Date
this.noteTitle = response.data.title || ''
this.noteTitle = ''
if(response.data.title){
this.noteTitle = response.data.title
}
this.noteText = response.data.text
this.lastNoteHash = this.hashString( response.data.text )
// this.diffNoteText = response.data.text
//Setup note tags
this.allTags = response.data.tags ? response.data.tags.split(','):[]
@@ -828,186 +807,86 @@
this.pinned = response.data.pinned
}
this.archived = response.data.archived
// Fetch attachmets if the count changed
if(response.data.attachment_count > 0){
this.getAttachments()
}
this.attachmentCount = response.data.attachment_count
return true
},
generateSelector(el){
if (!(el instanceof Element))
return;
var path = [];
while (el.nodeType === Node.ELEMENT_NODE) {
var selector = el.nodeName.toLowerCase();
if (el.id) {
selector += '#' + el.id;
path.unshift(selector);
break;
} else {
var sib = el, nth = 1;
while (sib = sib.previousElementSibling) {
if (sib.nodeName.toLowerCase() == selector)
nth++;
}
if (nth != 1)
selector += ":nth-of-type("+nth+")";
}
path.unshift(selector);
el = el.parentNode;
}
return path.join(" > ");
},
//Called on squire event for keyup
diffText(event){
// console.log(event.type)
const diffEvents = ['keyup','pathChange', 'click']
//Diff the changed lines only
// only process changes on certain events
if( !diffEvents.includes(event?.type) ){
return
let oldText = this.noteText
// let newText = this.getText()
let newText = document.getElementById('squire-id').innerHTML
const diff = dmp.diff_main(oldText, newText)
// dmp.diff_cleanupSemantic(diff)
const patch_list = dmp.patch_make(oldText, newText, diff);
const patch_text = dmp.patch_toText(patch_list);
if(patch_text == ''){ return }
//Save computed diff text
this.noteText = newText
let newPatch = {
id: this.rawTextId,
diff: patch_text,
}
clearTimeout(this.diffTextTimeout)
this.diffTextTimeout = setTimeout(() => {
// Current Editor Text
const liveEditorElm = document.getElementById('squire-id')
// virtual element for selecting div
let virtualEditorElm = document.createElement('div')
virtualEditorElm.innerHTML = rawNoteText
// element at cursor
const elmAtCaret = window.getSelection().getRangeAt(0).startContainer.parentNode
// Remove beginngin selector from path, make it more generic
const path = this.generateSelector(elmAtCaret).replace('div#squire-id > ','')
let workingPath = ''
// default to entire note text, select down if path
let selectedDivText = virtualEditorElm
let newSelectedDivText = liveEditorElm
if( path != ''){
const pathParts = path.split(' > ')
let testedPathParts = []
let workingPathParts = []
for (var i = 0; i < pathParts.length; i++) {
testedPathParts.push(pathParts[i])
let currentTestPath = testedPathParts.join(' > ')
// console.log('elm test ',i,currentTestPath)
let elmTest = virtualEditorElm.querySelector(currentTestPath)
if(!elmTest){
break
}
workingPathParts.push(pathParts[i])
}
workingPath = workingPathParts.join(' > ')
if(workingPath){
// Select text from virtual editor text
selectedDivText = selectedDivText.querySelector(workingPath)
// select text from current editor text
newSelectedDivText = newSelectedDivText.querySelector(workingPath)
}
}
const oldDivText = selectedDivText.innerHTML
const newDivText = newSelectedDivText.innerHTML
if(oldDivText == newDivText){ return }
const diff = dmp.diff_main(oldDivText, newDivText)
const patch_list = dmp.patch_make(oldDivText, newDivText, diff)
const patch_text = dmp.patch_toText(patch_list)
// save raw text for future diffs
rawNoteText = liveEditorElm.innerHTML
let newPatch = {
id: this.rawTextId,
diff: patch_text,
path: path,
// testing metrics
'old text':oldDivText,
'new text':newDivText,
'starting path':path,
'working path':workingPath,
}
// console.log('Sending out patch', newPatch)
this.$io.emit('note_diff', newPatch)
}, 100)
this.$io.emit('note_diff', newPatch)
},
patchText(incomingPatchs){
// console.log('incoming patches ', incomingPatchs)
return new Promise((resolve, reject) => {
const editorElement = document.getElementById('squire-id')
if(incomingPatchs == null){ return resolve(true) }
if(incomingPatchs.length == 0){ return resolve(true) }
// iterate over incoming patches because they apply to specific divs
// let currentText = this.getText()
let currentText = document.getElementById('squire-id').innerHTML
//Convert text of all new patches into patches array
let patches = []
incomingPatchs.forEach(patch => {
// default to parent element, change to child if set
let editedElement = editorElement
if(patch.path){
editedElement = editorElement.querySelector(patch.path)
if(patch.time <= this.updated){
return
}
if( !editedElement ){
editedElement = editorElement
}
// convert patch from text and then apply to selected element
const patches = dmp.patch_fromText(patch.diff)
const patchResults = dmp.patch_apply(patches, editedElement.innerHTML)
// console.log('Patch results')
// console.log([patch.path, editedElement.innerHTML, patchResults[0]])
// patch changed directly into editor
editedElement.innerHTML = patchResults[0]
patches.push(...dmp.patch_fromText(patch.diff))
})
// save editor HTML after change for future comparisons
rawNoteText = editorElement.innerHTML
// update hash on patch
this.lastNoteHash = this.hashString( rawNoteText )
this.$nextTick(() => {
if(patches.length == 0){
return resolve(true)
})
}
var results = dmp.patch_apply(patches, currentText);
let newText = results[0]
this.noteText = newText
// this.editor.setHTML(newText)
document.getElementById('squire-id').innerHTML = newText
return resolve(true)
})
},
onKeyup(event){
this.statusText = 'modified'
this.idleNote = false
// Small debounce on diff generation
clearTimeout(this.diffTextTimeout)
this.diffTextTimeout = setTimeout(() => {
this.diffText()
}, 25)
//Save after x seconds
clearTimeout(this.editDebounce)
this.editDebounce = setTimeout(() => {
this.save()
}, 4 * 1000)
}, 5 * 1000)
//Save after x keystrokes
this.keyPressesCounter = (this.keyPressesCounter + 1)
@@ -1040,7 +919,6 @@
}
//tell websockets to truncate history at this save
this.lastNoteHash = currentHash //Update last saved note hash
this.$io.emit('truncate_diffs_at_save', {'rawTextId':this.rawTextId, 'hash':currentHash })
const postData = {
@@ -1061,54 +939,44 @@
this.modified = true
this.diffsApplied = 0
//Update last saved note hash
this.lastNoteHash = currentHash
return resolve(true)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Save Note') })
})
},
loadNoteNextFromServer(){
checkForUpdatedNote(){
clearTimeout(this.reloadNoteDebounce)
this.reloadNoteDebounce = setTimeout(() => {
// flash note text to show the update
// this.setText('')
const now = +new Date
//Only check every 3 seconds
const checkForUpdateTimeout = now - this.lastInteractionTimestamp > (2 * 1000)
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible' && checkForUpdateTimeout){
//Focus Regained on Note, check for update
axios.post('/api/note/get', { 'noteId': this.noteid })
.then(response => {
this.setupLoadedNoteData(response)
const serverTextHash = this.hashString( response.data.text )
//Manually set squire text to show
this.setText(this.noteText)
if(this.lastNoteHash != serverTextHash){
// console.log('note was changed UPDATE THAT BITCH!!!!')
this.setupLoadedNoteData(response)
//Manually set squire text to show
this.setText(this.noteText)
}
})
}, 200)
},
windowFocusEvent(){
//Only check if its been greater than a few seconds
const now = +new Date
const checkForUpdateTimeout = now - this.lastInteractionTimestamp > (3 * 1000)
//If user leaves page then returns to page, reload the first batch
if(checkForUpdateTimeout){
this.loadNoteNextFromServer()
this.lastInteractionTimestamp = now
}
},
windowBlurEvent(){
this.idleNote = true
//Keep track of visibility change and last interaction time
this.lastVisibilityState = document.visibilityState
this.lastInteractionTimestamp = +new Date
},
hashString(inText){
@@ -1162,14 +1030,11 @@
})
},
destroyWebSockets(){
this.$io.removeListener('past_diffs')
this.$io.removeListener('update_user_count')
this.$io.removeListener('incoming_diff')
this.$io.removeListener('update_note_attachments')
clearTimeout(this.idleNoteTimeout)
// this.$io.removeListener('past_diffs')
// this.$io.removeListener('update_user_count')
// this.$io.removeListener('incoming_diff')
},
initWebsocketEvents(){
setupWebSockets(){
//Tell server to push this note into a room
this.$io.emit('join_room', this.rawTextId )
@@ -1185,79 +1050,36 @@
this.diffsApplied = diffSinceLastUpdate.length
// console.log('Got Diffs Total -> ', diffSinceLastUpdate)
}
// console.log(diffSinceLastUpdate)
this.patchText(diffSinceLastUpdate)
.then(() => {
this.restoreCaretPosition()
})
})
this.$io.on('incoming_diff', incomingDiff => {
//Save current caret position
//Find index of child element based on past range
const element = window.getSelection().getRangeAt(0).startContainer.parentNode
const textLines = document.getElementById('squire-id').children
const childIndex = [...textLines].indexOf(element)
this.patchText([incomingDiff])
.then(() => {
this.restoreCaretPosition()
if(childIndex == -1){
console.log('Cursor position lost. Div being updated was lost.')
return
}
//Reset caret position
//Find child index of old range and create a new one
let allChildren = document.getElementById('squire-id').children
const newLine = allChildren[childIndex].firstChild
let range = document.createRange()
range.setStart(newLine, this.startOffset)
range.setEnd(newLine, this.startOffset)
this.editor.setSelection(range)
})
})
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
const sameIdCheck = (this.idleNote && this.noteid == noteId)
const differentHashCheck = (hash != this.lastNoteHash)
// if hashes do not match, reload text from server
if(sameIdCheck && differentHashCheck){
this.loadNoteNextFromServer()
}
})
this.$io.on('update_note_attachments', () => {
this.getAttachments()
})
},
logNoteInteraction(){
this.idleNote = false
clearTimeout(this.idleNoteTimeout)
this.idleNoteTimeout = setTimeout(() => {
this.idleNote = true
}, 5000)
},
saveCaretPosition(event){
//Find index of child element based on past range
const element = window.getSelection().getRangeAt(0).startContainer.parentNode
//Save range to replace cursor if someone else makes an update
this.lastRange = this.generateSelector(element)
this.startOffset = parseInt(event.range.startOffset) || 0
return
},
restoreCaretPosition(){
return new Promise((resolve, reject) => {
// This code is intended to restore caret position to previous location
// when a third party updates the note.
if(!this.lastRange){ return resolve(true) }
const editorElement = document.getElementById('squire-id')
const lastElement = editorElement.querySelector(this.lastRange)
if( !lastElement ){ return resolve(true) }
let range = document.createRange()
range.setStart(lastElement.firstChild, this.startOffset)
range.setEnd(lastElement.firstChild, this.startOffset)
// Set range in editor element
this.editor.setSelection(range)
return resolve(true)
})
},
titleResize(){
//Resize the title field
@@ -1268,88 +1090,6 @@
element.style.height = (element.scrollHeight) +'px'
}
},
destroyAttachmentStyles(){
// Remove attachment preview styles
var head = document.head
var styleElement = document.getElementById('attachmentGeneratedStyles'+this.noteid)
if(styleElement){
head.removeChild(styleElement)
}
},
getAttachments(){
axios.post('/api/attachment/search', {'noteId':this.noteid})
.then( results => {
// generate new style group
var style = document.createElement('style')
style.id = 'attachmentGeneratedStyles'+this.noteid
style.type = 'text/css'
// iterate attachments and build unique style for each
let attachmentStyles = []
results.data.forEach(attachment => {
// thumbnail location
const bgurl = `/api/static/thumb_${attachment.file_location}`
let padding = '2px 0 0'
// increase padding if there is a valid file
if(attachment.file_location){
padding = '13px 0 13px 155px'
}
// unescaped characters will break content attribute
const strippedText = attachment.text
.replace(/'/g, "\\'") //Escape ' s
.replace(/\n/g, '\\A') //Escape new lines
// strip down URL, *= matches anywhere is string
const substringsToRemove = ['https://','http://','www.']
var pattern = new RegExp(substringsToRemove.join('|'), 'g');
const strippedurl = attachment.url
.replace(pattern, '') // remove url protocol
.replace(/&.*/, '') // remove anything after &
const cleanStyle = `
.squire-box a[href*="${strippedurl}" i]::before {
content: '${strippedText}';
display: inline-block;
padding: ${padding};
pointer-events: none;
font-size: 1.3em !important;
width: 100%;
background-position: left center;
background-repeat: no-repeat;
background-size: 140px auto;
background-image: url(${bgurl});
border-bottom: solid 1px var(--main-accent);
margin-bottom: -10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.squire-box a[href*="${strippedurl}" i] {
font-size: 0.8em;
}
`
.replace(/\t|\r|\n/gm, "") // Remove tabs, new lines, returns
.replace(/\s+/g, ' ') // remove double spaces
attachmentStyles.push(cleanStyle)
})
// Destroy just before creating new to prevent page jumping
this.destroyAttachmentStyles()
// push new styles into <head>
style.innerHTML = attachmentStyles.join(' ')
document.head.appendChild(style)
})
.catch(error => { console.log(error);this.$bus.$emit('notification', 'Failed to Search Attachments') })
},
}
}
</script>
@@ -1362,11 +1102,6 @@
z-index: 1019;
text-align: right;
}
.status-menu span + span {
border-left: 1px solid #ccc;
margin-left: 4px;
padding-left: 4px;
}
.font-color-bar {
/*width: calc(100% - 8px);*/
@@ -1634,7 +1369,6 @@
}
.edit-button {
padding: 6px 0px 0;
flex-grow: 1;
}
.edit-button > span:not(.ui) {
display: none;

View File

@@ -285,28 +285,23 @@
},
justClosed(){
// Dont do anything when not is closed.
// Its already saved, this will make interface feel snappy
// Scroll note into view
// this.$el.scrollIntoView({
// behavior: 'smooth',
// block: 'center',
// inline: 'center'
// })
this.$el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
})
// this.$bus.$emit('notification','Note Saved')
//After scroll, trigger green outline animation
setTimeout(() => {
// //After scroll, trigger green outline animation
// setTimeout(() => {
this.triggerClosedAnimation = true
setTimeout(()=>{
//After 3 seconds, hide it
this.triggerClosedAnimation = false
}, 1500)
// this.triggerClosedAnimation = true
// setTimeout(()=>{
// //After 3 seconds, hide it
// this.triggerClosedAnimation = false
// }, 1500)
// }, 500)
}, 500)
},
},

View File

@@ -1,7 +1,7 @@
<template>
<div class="button-fix">
<div>
<div class="ui right floated basic shrinking icon button" v-on:click="showPasteInputArea">
<i class="green paste icon"></i>
<i class="paste icon"></i>
Paste
</div>
<div class="shade" v-if="showPasteArea" @click.prevent="close">

View File

@@ -172,16 +172,15 @@ const SquireButtonFunctions = {
//Fetch the container
let container = document.getElementById('squire-id')
this.$router.go(-1)
setTimeout(()=>{
Array.from( container.getElementsByClassName('active') ).forEach(item => {
item.classList.remove('active');
})
Array.from( container.getElementsByClassName('active') ).forEach(item => {
item.classList.remove('active');
})
},600)
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},
deleteCompletedListItems(){
//
@@ -191,57 +190,53 @@ const SquireButtonFunctions = {
//Fetch the container
let container = document.getElementById('squire-id')
//Close menu if user is on mobile, then sort list
this.$router.go(-1)
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
setTimeout(()=>{
//Create two categories, done and not done list items
let undoneElements = document.createDocumentFragment()
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Create two categories, done and not done list items
let undoneElements = document.createDocumentFragment()
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
//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 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) )
}
//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){
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
}
})
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)
}
})
}, 600)
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},
sortList(){
//
@@ -251,65 +246,61 @@ const SquireButtonFunctions = {
//Fetch the container
let container = document.getElementById('squire-id')
//Close menu if user is on mobile
this.$router.go(-1)
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
setTimeout(()=>{
//Create two categories, done and not done list items
let doneElements = document.createDocumentFragment()
let undoneElements = document.createDocumentFragment()
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Create two categories, done and not done list items
let doneElements = document.createDocumentFragment()
let undoneElements = document.createDocumentFragment()
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
//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 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) )
}
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
} else {
//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]
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
//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) )
}
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
node.appendChild(doneElements)
}
})
} 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)
}
})
},600)
//Close menu if user is on mobile
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},
calculateMath(){
//
@@ -319,9 +310,6 @@ const SquireButtonFunctions = {
//Fetch the container
let container = document.getElementById('squire-id')
//Close menu if user is on mobile, then sort list
this.$router.go(-1)
// simple function that trys to evaluate javascript
const shittyMath = (string) => {
//Remove all chars but math chars
@@ -334,39 +322,38 @@ const SquireButtonFunctions = {
}
}
setTimeout(()=>{
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
const line = node.innerText.trim()
const line = node.innerText.trim()
// = sign exists and its the last character in the string
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
// = 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)
//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){
//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()
//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
}
//Slam in that new HTML with the output
node.innerHTML = newLine
}
})
},600)
}
})
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},
setText(inText){

View File

@@ -8,13 +8,6 @@
<div class="content">
Files
<div class="sub header">Uploaded Files and Websites from notes.</div>
<div class="sub header">
<i class="green angle double up icon icon"></i>
<router-link
to="/bookmarklet">
Push any website to solid scribe
</router-link>
</div>
</div>
</h2>
@@ -132,11 +125,6 @@
//Load more attachments on scroll
window.addEventListener('scroll', this.onScroll)
this.$io.on('update_note_attachments', () => {
this.reset()
this.searchAttachments()
})
//Mount notes on load if note ID is set
this.searchAttachments()
},
@@ -144,8 +132,6 @@
//Remove scroll event on destroy
window.removeEventListener('scroll', this.onScroll)
this.$io.removeListener('update_note_attachments')
},
watch:{
$route (to, from){

View File

@@ -1,66 +0,0 @@
<template>
<div class="text-container squire-box">
<h2 class="ui header">
<i class="green angle double up icon icon"></i>
<div class="content">
Push URL to Solid Scribe - Bookmarklet
<div class="sub header">Push any website to your file list.</div>
</div>
</h2>
<p>A bookmarklet is a small piece of code that can be run from a bookmark.</p>
<p>Use the bookmarklet below to push URLs of website to solid scribe for later</p>
<p>The bookmarklet works in a secure way and won't leak any data.</p>
<p>To install the bookmarklet, all you need to do is drag it to your bookmarks bar.</p>
<h2>
Drag the link below to your bookmarks.
</h2>
<h3>
<a :href="`${(bookmarkletscript)}`" class="ui huge text">Push to SolidScribe</a>
</h3>
</div>
</template>
<script>
import axios from 'axios'
export default {
components: {
},
data: function(){
return {
loading: true,
bookmarkletscript:'',
}
},
beforeCreate: function(){
// Perform Login check
this.$parent.loginGateway()
},
mounted: function(){
this.getBookmarklet()
},
beforeDestroy(){
},
methods: {
getBookmarklet(){
this.loading = true
axios.post('/api/attachment/getbookmarklet')
.then( results => {
this.bookmarkletscript = results.data
})
.catch(error => { this.$bus.$emit('notification', 'Failed to get bookmarklet') })
},
}
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,10 @@
/>
<paste-button />
<span class="ui grey text text-fix">
Active Sessions {{ $store.getters.getActiveSessions }}
</span>
</div>
@@ -567,7 +571,7 @@
// @TODO Don't even trigger this if the note wasn't changed
updateSingleNote(noteId, focuseAndAnimate = true){
// console.log('updating single note', noteId)
console.log('updating single note', noteId)
noteId = parseInt(noteId)

View File

@@ -13,7 +13,6 @@ const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/Note
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
const OverviewPage = () => import(/* webpackChunkName: "OverviewPage" */ '@/pages/OverviewPage')
const BookmarkletPage = () => import(/* webpackChunkName: "BookmarkletPage" */ '@/pages/BookmarkletPage')
const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
Vue.use(Router)
@@ -68,12 +67,6 @@ export default new Router({
meta: {title:'Terms'},
component: TermsPage
},
{
path: '/bookmarklet',
name: 'Bookmarklet',
meta: {title:'Bookmarklet'},
component: BookmarkletPage
},
{
path: '/settings',
name: 'Settings',

View File

@@ -44,7 +44,7 @@ export default new Vuex.Store({
'menu-text': '#5e6268',
},
'black':{
'body_bg_color': 'rgb(12 4 30)',
'body_bg_color': 'linear-gradient(135deg, rgba(0,0,0,1) 0%, rgba(23,12,46,1) 100%)',
//'#0f0f0f',//'#000',
'small_element_bg_color': '#000',
'text_color': '#FFF',

View File

@@ -0,0 +1,97 @@
#
# Working dev server config
#
server {
listen 80;
listen [::]:80;
server_name 192.168.1.164;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
ssl_certificate /home/mab/ss/client/certs/nginx-selfsigned.crt;
ssl_certificate_key /home/mab/ss/client/certs/nginx-selfsigned.key;
ssl_dhparam /home/mab/ss/client/certs/dhparam.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_protocols TLSV1.1 TLSV1.2 TLSV1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/httpslocalhost.access.log;
error_log /var/log/nginx/httpslocalhost.error.log;
client_max_body_size 20M;
location / {
proxy_pass https://127.0.0.1:8081;
proxy_set_header Host localhost;
proxy_set_header X-Forwarded-Host localhost;
proxy_set_header X-Forwarded-Server localhost;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect off;
proxy_connect_timeout 90s;
proxy_read_timeout 90s;
proxy_send_timeout 90s;
proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
}
location /sockjs-node {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass https://127.0.0.1:8081;
proxy_redirect off;
proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
}
location /api {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:3000;
proxy_redirect off;
proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
}
location /socket {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
}
# Prod settings to serve static index
# location / {
# autoindex on;
# #try_files $uri $uri/ /index.html;
# }
# location / {
# #autoindex on
#
# proxy_pass http://127.0.0.1:8444;
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection 'upgrade';
# proxy_set_header Host $host;
# proxy_cache_bypass $http_upgrade;
# }

View File

@@ -1,11 +0,0 @@
const path = '../../'
const prefix = '/$1'
module.exports = {
moduleNameMapper: {
"@root/(.*)": ".",
"@models/(.*)": path+"server/models"+prefix,
"@routes/(.*)": path+"server/routes"+prefix,
"@helpers/(.*)": path+"server/helpers"+prefix,
"@config/(.*)": path+"server/config"+prefix,
}
}

6486
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{
"name": "personal-internet",
"version": "1.0.0",
"description": "Encrypted note taking applications",
"description": "Personal or Private net",
"main": "index.js",
"scripts": {
"test": "jest"
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Max",
"license": "ISC",
@@ -33,8 +33,5 @@
"@routes": "server/routes",
"@helpers": "server/helpers",
"@config": "server/config"
},
"devDependencies": {
"jest": "^29.7.0"
}
}

View File

@@ -1,7 +1,5 @@
//Import mysql2 package
const mysql = require('mysql2');
const os = require('os') //Used to get path of home directory
const result = require('dotenv').config({ path:(os.homedir()+'/.env') })
// Create the connection pool.
const pool = mysql.createPool({

View File

@@ -72,8 +72,6 @@ CryptoString.createSalt = () => {
return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64')
}
// Creates a small random salt
CryptoString.createSmallSalt = () => {
return crypto.randomBytes(20).toString('base64')

View File

@@ -6,7 +6,7 @@ let SiteScrape = module.exports = {}
const removeWhitespace = /\s+/g
const commonWords = ['just','start','what','these','how', 'was', 'being','can','way','share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
const commonWords = ['share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
'give','use','find','tell','ask','work','seem','feel','try','leave','call','good','new','first','last','long','great','little','own','other','old',
'right','big','high','different','small','large','next','early','young','important','few','public','bad','same','able','to','of','in','for','on',
'with','at','by','from','up','about','into','over','after','the','and','a','that','I','it','not','he','as','you','this','but','his','they','her',
@@ -162,28 +162,19 @@ SiteScrape.getKeywords = ($) => {
majorContent += $('[class*=content]').text()
.replace(removeWhitespace, " ") //Remove all whitespace
// .replace(/\W\s/g, '') //Remove all non alphanumeric characters
.substring(0,6000) //Limit to 6000 characters
.replace(/\W\s/g, '') //Remove all non alphanumeric characters
.substring(0,3000) //Limit to 3000 characters
.toLowerCase()
.replace(/[^A-Za-z0-9- ]/g, '');
console.log(majorContent)
//Count frequency of each word in scraped text
let frequency = {}
majorContent.split(' ').forEach(word => {
// Exclude short or common words
if(commonWords.includes(word) || word.length <= 2){
return
if(commonWords.includes(word)){
return //Exclude certain words
}
if(!frequency[word]){
frequency[word] = 0
}
// Skip some plurals
if(frequency[word+'s'] || frequency[word+'es']){
return
}
frequency[word]++
})
@@ -201,7 +192,7 @@ SiteScrape.getKeywords = ($) => {
});
let finalWords = []
for(let i=0; i<6; i++){
for(let i=0; i<5; i++){
if(sortable[i] && sortable[i][0]){
finalWords.push(sortable[i][0])
}

View File

@@ -20,8 +20,6 @@ const helmet = require('helmet')
const express = require('express')
const app = express()
app.use( helmet() )
// allow for the parsing of url encoded forms
app.use(express.urlencoded({ extended: true }));
//
@@ -116,12 +114,29 @@ io.on('connection', function(socket){
//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){
//Update users in room count
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
//Debugging text - prints out notes in limbo
let noteDiffKeys = Object.keys(noteDiffs)
let totalDiffs = 0
noteDiffKeys.forEach(diffSetKey => {
if(noteDiffs[diffSetKey]){
totalDiffs += noteDiffs[diffSetKey].length
}
})
//Debugging Text
if(noteDiffKeys.length > 0){
console.log('Total notes in limbo -> ', noteDiffKeys.length)
console.log('Total Diffs for all notes -> ', totalDiffs)
}
}
})
@@ -147,13 +162,31 @@ io.on('connection', function(socket){
noteDiffs[noteId].push(data)
// Go over each user in this note-room
//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.
io.in(noteId).clients((error, clients) => {
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 => {
// only send off diff if user
if(socketId != socket.id){
io.to(socketId).emit('incoming_diff', data)
}
@@ -180,6 +213,7 @@ io.on('connection', function(socket){
}
}
noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo)
if(noteDiffs[checkpoint.rawTextId].length == 0){
@@ -201,7 +235,7 @@ io.on('connection', function(socket){
http.listen(ports.socketIo, function(){
console.log(`Socke.io: Listening on port ${ports.socketIo}`)
console.log(`Socke.io: Listening on port ${ports.socketIo}!`)
});
//Enable json body parsing in requests. Allows me to post data in ajax calls
@@ -242,21 +276,17 @@ app.use(function(req, res, next){
// Test Area
// const printResults = true
// let UserTest = require('@models/User')
// let NoteTest = require('@models/Note')
// let AuthTest = require('@helpers/Auth')
// Auth.test()
// UserTest.keyPairTest('genMan30', '1', printResults)
// .then( ({testUserId, masterKey}) =>
// NoteTest.test(testUserId, masterKey, printResults))
// .then( message => {
// if(printResults) console.log(message)
// Auth.testTwoFactor()
// })
// .catch((error) => {
// console.log(error)
// })
const printResults = true
let UserTest = require('@models/User')
let NoteTest = require('@models/Note')
let AuthTest = require('@helpers/Auth')
Auth.test()
UserTest.keyPairTest('genMan30', '1', printResults)
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
.then( message => {
if(printResults) console.log(message)
Auth.testTwoFactor()
})
//Test
app.get('/api', (req, res) => res.send('Solidscribe /API is up and running'))

View File

@@ -1,7 +1,6 @@
let db = require('@config/database')
let SiteScrape = require('@helpers/SiteScrape')
const cs = require('@helpers/CryptoString')
let Attachment = module.exports = {}
@@ -48,15 +47,13 @@ Attachment.textSearch = (userId, searchTerm) => {
}
Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeShared) => {
console.log([userId, noteId, attachmentType, offset, setSize, includeShared])
return new Promise((resolve, reject) => {
let params = [userId]
let query = `
SELECT attachment.*, note.share_user_id FROM attachment
LEFT JOIN note ON (attachment.note_id = note.id)
WHERE attachment.user_id = ? AND visible = 1
`
JOIN note ON (attachment.note_id = note.id)
WHERE attachment.user_id = ? AND visible = 1 `
if(noteId && noteId > 0){
//
@@ -79,11 +76,6 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeSha
query += `AND note.archived = ${ attachmentType == 'archived' ? '1':'0' } `
query += `AND note.trashed = ${ attachmentType == 'trashed' ? '1':'0' } `
if(!attachmentType){
// Null note ID means it was pushed by bookmarklet
query += 'OR attachment.note_id IS NULL '
}
}
@@ -110,6 +102,18 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeSha
})
}
//Returns all attachments
Attachment.forNote = (userId, noteId) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND visible = 1 ORDER BY last_indexed DESC;`, [userId, noteId])
.then((rows, fields) => {
resolve(rows[0]) //Return all attachments found by query
})
.catch(console.log)
})
}
Attachment.urlForNote = (userId, noteId) => {
return new Promise((resolve, reject) => {
db.promise()
@@ -185,7 +189,6 @@ Attachment.delete = (userId, attachmentId, urlDelete = false) => {
.catch(console.log)
}
})
.catch(console.log)
})
}
@@ -302,13 +305,9 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
//Once everything is done being scraped, emit new attachment events
SocketIo.to(userId).emit('update_counts')
// Tell user to update attachments with scraped text
SocketIo.to(userId).emit('update_note_attachments')
solrAttachmentText += freshlyScrapedText
resolve(solrAttachmentText)
})
.catch(console.log)
})
})
}
@@ -336,13 +335,9 @@ Attachment.scrapeUrlsCreateAttachments = (userId, noteId, foundUrls) => {
//All URLs have been scraped, return data
if(processedCount == foundUrls.length){
console.log('All urls scraped')
return resolve(scrapedText)
resolve(scrapedText)
}
})
.catch(error => {
console.log('Site Scrape error', error)
})
})
})
}
@@ -352,8 +347,8 @@ Attachment.downloadFileFromUrl = (url) => {
return new Promise((resolve, reject) => {
if(!url){
return resolve(null)
if(url == null || url == undefined || url == ''){
resolve(null)
}
const random = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
@@ -361,7 +356,8 @@ Attachment.downloadFileFromUrl = (url) => {
let fileName = random+'_scrape'
let thumbPath = 'thumb_'+fileName
console.log('Scraping image url', url)
console.log('Scraping image url')
console.log(url)
console.log('Getting ready to scrape ', url)
@@ -399,7 +395,7 @@ Attachment.downloadFileFromUrl = (url) => {
Attachment.processUrl = (userId, noteId, url) => {
const scrapeTime = 5*1000;
const scrapeTime = 20*1000;
return new Promise((resolve, reject) => {
@@ -454,10 +450,9 @@ Attachment.processUrl = (userId, noteId, url) => {
var desiredSearchText = ''
desiredSearchText += pageTitle
if(keywords){
desiredSearchText += "\n " + keywords
desiredSearchText += "\n" + keywords
}
console.log('Results from site scrape-------------')
console.log({
pageTitle,
hostname,
@@ -507,142 +502,40 @@ Attachment.processUrl = (userId, noteId, url) => {
})
.catch(error => {
console.log('Scrape pooped out')
console.log('Issue with scrape', error.statusCode)
clearTimeout(requestTimeout)
return resolve('No site text')
// console.log('Scrape pooped out')
// console.log('Issue with scrape')
console.log(error)
// resolve('')
})
requestTimeout = setTimeout( () => {
console.log('Cancel the request, its taking to long.')
request.cancel()
return resolve('Request Timeout')
desiredSearchText = 'No Description for -> '+url
created = Math.round((+new Date)/1000)
db.promise()
.query(`UPDATE attachment SET
text = ?,
last_indexed = ?,
WHERE id = ?
`, [desiredSearchText, created, insertedId])
.then((rows, fields) => {
resolve(desiredSearchText) //Return found text
})
.catch(console.log)
//Create attachment in DB with scrape text and provided data
// db.promise()
// .query(`INSERT INTO attachment
// (note_id, user_id, attachment_type, text, url, last_indexed)
// VALUES (?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created])
// .then((rows, fields) => {
// resolve(desiredSearchText) //Return found text
// })
// .catch(console.log)
}, scrapeTime )
})
}
Attachment.generatePushKey = (userId) => {
return new Promise((resolve, reject) => {
db.promise()
.query("SELECT pushkey FROM user WHERE id = ? LIMIT 1", [userId])
.then((rows, fields) => {
const pushKey = rows[0][0].pushkey
// push key exists
if(pushKey && pushKey.length > 0){
return resolve(pushKey)
} else {
// generate and save a new key
const newPushKey = cs.createSmallSalt()
db.promise()
.query('UPDATE user SET pushkey = ? WHERE id = ? LIMIT 1', [newPushKey,userId])
.then((rows, fields) => {
return resolve(newPushKey)
})
}
})
})
}
Attachment.deletePushKey = (userId) => {
return new Promise((resolve, reject) => {
db.promise()
.query('UPDATE user SET pushkey = null WHERE id = ? LIMIT 1', [userId])
.then((rows, fields) => {
return resolve(rows[0].affectedRows == 1)
})
})
}
Attachment.getPushkeyBookmarklet = (userId) => {
return new Promise((resolve, reject) => {
Attachment.generatePushKey(userId)
.then( pushKey => {
let bookmarklet = Attachment.generateBookmarkletText(pushKey)
return resolve(bookmarklet)
})
})
}
Attachment.pushUrl = (pushkey,url) => {
return new Promise((resolve, reject) => {
let userId = null
pushkey = pushkey.replace(/ /g, '+')
db.promise()
.query("SELECT id FROM user WHERE pushkey = ? LIMIT 1", [pushkey])
.then((rows, fields) => {
if(rows[0].length == 0){
return resolve(true)
}
userId = rows[0][0].id
return Attachment.scrapeUrlsCreateAttachments(userId, null, [url])
})
.then(() => {
if(typeof SocketIo != 'undefined'){
//Once everything is done being scraped, emit new attachment events
SocketIo.to(userId).emit('update_counts')
// Tell user to update attachments with scraped text
SocketIo.to(userId).emit('update_note_attachments')
}
return resolve(true)
})
.catch(console.log)
})
}
Attachment.generateBookmarkletText = (pushKey) => {
const endpoint = '/api/public/pushmebaby'
let url = 'https://www.solidscribe.com' + endpoint
if(process.env.NODE_ENV === 'development'){
// url = 'https://192.168.1.164' + endpoint
}
// Terminate each line with a semi-colon, super important, since spaces are removed.
// document.getElementById(id).remove();
url += '?pushkey='+encodeURIComponent(pushKey)
const bookmarkletV3 = `
javascript: (() => {
var p = encodeURIComponent(window.location.href);
var n = "`+url+`&url="+p;
window.open(n, '_blank', 'noopener=noopener');
window.focus();
var k = document.createElement("div");
k.setAttribute("style", "position:fixed;right:10px;top:10px;z-index:222222;border-radius:4px;font-size:1.3em;padding:20px 15px;background: #8f51be;color:white;");
k.innerHTML = "Posted URL to your Solid Scribe account";
document.body.appendChild(k);
setTimeout(()=>{
k.remove();
},5000);
})();
`
return bookmarkletV3
.replace(/\t|\r|\n/gm, "") // Remove tabs, new lines, returns
.replace(/\s+/g, ' ') // remove double spaces
.trim()
}

View File

@@ -17,7 +17,6 @@ const fs = require('fs')
const gm = require('gm')
Note.test = (userId, masterKey, printResults) => {
return false;
return new Promise((resolve, reject) => {
@@ -163,10 +162,6 @@ Note.test = (userId, masterKey, printResults) => {
return resolve('Test: Complete ---')
})
.catch(error => {
console.log(error)
return reject(error)
})
})
}
@@ -198,7 +193,7 @@ Note.create = (userId, noteTitle = '', noteText = '', masterKey) => {
})
.then((rows, fields) => {
if(typeof SocketIo != 'undefined'){
if(SocketIo){
SocketIo.to(userId).emit('new_note_created', rows[0].insertId)
}
@@ -346,7 +341,7 @@ Note.reindex = (userId, masterKey, removeId = null) => {
setTimeout(() => {
if(masterKey == null || note.salt == null){
console.log('Error indexing note - master key or salt missing', note.id)
console.log('Error indexing note', note.id)
return resolve(true)
}
@@ -395,13 +390,13 @@ Note.reindex = (userId, masterKey, removeId = null) => {
return Promise.all(reindexQueue)
})
.then(updatePromiseResults => {
.then(rawSearchIndex => {
const created = Math.round((+new Date)/1000)
const jsonSearchIndex = JSON.stringify(searchIndex)
const encryptedJsonIndex = cs.encrypt(masterKey, searchIndexSalt, jsonSearchIndex)
db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
return db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
[encryptedJsonIndex, created, userId])
.then((rows, fields) => {
@@ -411,7 +406,6 @@ Note.reindex = (userId, masterKey, removeId = null) => {
.then((rows, fields) => {
// console.log('Indexd Note Count: ' + rows[0]['affectedRows'])
// @TODO - Return number of reindexed notes
resolve(true)
})
@@ -513,13 +507,13 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
})
.then((rows, fields) => {
if(typeof SocketIo != 'undefined'){
if(SocketIo){
SocketIo.to(userId).emit('new_note_text_saved', {noteId, hash})
//Async attachment reindex
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
}
//Async attachment reindex
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
//Send back updated response
resolve(rows[0])
})
@@ -745,13 +739,12 @@ Note.get = (userId, noteId, masterKey) => {
const nowTime = Math.round((+new Date)/1000)
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
.then(results => {
//Return note data
// delete noteData.salt //remove salt from return data
// delete noteData.encrypted_share_password_key
noteData.lockedOut = noteLockedOut
resolve(noteData)
})
//Return note data
// delete noteData.salt //remove salt from return data
// delete noteData.encrypted_share_password_key
noteData.lockedOut = noteLockedOut
resolve(noteData)
})
.catch(error => {

View File

@@ -9,8 +9,7 @@ const speakeasy = require('speakeasy')
let User = module.exports = {}
const version = '3.8.0'
// 3.7.3 - diff/patch update
const version = '3.6.3'
//Login a user, if that user does not exist create them
//Issues login token
@@ -553,12 +552,6 @@ User.revokeActiveSessions = (userId, sessionId) => {
User.deleteUser = (userId, password) => {
if(!userId || !password){
return new Promise((resolve, reject) => {
return resolve('Missing User ID or Password. No Action Taken.')
})
}
//Verify user is correct by decryptig master key with password
let deletePromises = []
@@ -590,4 +583,78 @@ User.deleteUser = (userId, password) => {
//Remove all note attachments and files
return Promise.all(deletePromises)
}
User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
return new Promise((resolve, reject) => {
let masterKey = null
let testUserId = null
const randomUsername = Math.random().toString(36).substring(2, 15);
const randomPassword = '1'
const secondPassword = '2'
User.register(testUserName, password)
.then( ({ token, userId }) => {
testUserId = userId
if(printResults) console.log('Test: Register User '+testUserName+' - Pass')
return User.getMasterKey(testUserId, password)
})
.then(newMasterKey => {
masterKey = newMasterKey
if(printResults) console.log('Test: Generate/Decrypt Master Key - Pass')
return User.generateKeypair(testUserId, masterKey)
})
.then(({publicKey, privateKey}) => {
const publicKeyMessage = 'Test: Public key decrypt - Pass'
const privateKeyMessage = 'Test: Private key decrypt - Pass'
//Encrypt Message with private Key
const privateKeyEncrypted = crypto.privateEncrypt(privateKey, Buffer.from(privateKeyMessage, 'utf8')).toString('base64')
const decryptedPrivate = crypto.publicDecrypt(publicKey, Buffer.from(privateKeyEncrypted, 'base64'))
//Conver back to a string
if(printResults) console.log(decryptedPrivate.toString('utf8'))
//Encrypt with public key
const pubEncrMsc = crypto.publicEncrypt(publicKey, Buffer.from(publicKeyMessage, 'utf8')).toString('base64')
const publicDeccryptMessage = crypto.privateDecrypt(privateKey, Buffer.from(pubEncrMsc, 'base64') )
//Convert it back to string
if(printResults) console.log(publicDeccryptMessage.toString('utf8'))
return User.login(testUserName, password)
})
.then( ({token, userId}) => {
if(printResults) console.log('Test: Login New User - Pass')
return User.changePassword(testUserId, randomPassword, secondPassword)
})
.then(passwordChangeResults => {
if(printResults) console.log('Test: Password Change - ', passwordChangeResults?'Pass':'Fail')
return User.login(testUserName, secondPassword)
})
.then(reLogin => {
if(printResults) console.log('Test: Login With new Password - Pass')
return User.getMasterKey(testUserId, secondPassword)
})
.then(newMasterKey => {
masterKey = newMasterKey
resolve({testUserId, masterKey})
})
})
}

View File

@@ -35,6 +35,11 @@ router.post('/textsearch', function (req, res) {
.then( data => res.send(data) )
})
router.post('/get', function (req, res) {
Attachment.forNote(userId, req.body.noteId)
.then( data => res.send(data) )
})
router.post('/update', function (req, res) {
Attachment.update(userId, req.body.attachmentId, req.body.updatedText, req.body.noteId)
.then( result => {
@@ -60,26 +65,5 @@ router.post('/upload', upload.single('file'), function (req, res, next) {
})
//
// Push URL to attachments
// push action on - public controller
//
// get push key
router.post('/getbookmarklet', function (req, res) {
Attachment.getPushkeyBookmarklet(userId)
.then( data => res.send(data) )
})
// generate new push key
router.post('/generatepushkey', function (req, res) {
})
// delete push key
router.post('/deletepushkey', function (req, res) {
})
module.exports = router

View File

@@ -4,7 +4,6 @@ const rateLimit = require('express-rate-limit')
const Note = require('@models/Note')
const User = require('@models/User')
const Attachment = require('@models/Attachment')
@@ -57,29 +56,6 @@ router.post('/register', registerLimiter, function (req, res) {
})
})
//
// Public Pushme Action
//
const pushMeLimiter = rateLimit({
windowMs: 30 * 60 * 1000, //30 min window
max: 50, // start blocking after x requests
message:'Error'
})
router.get('/pushmebaby', pushMeLimiter, function (req, res) {
Attachment.pushUrl(req.query.pushkey, req.query.url)
.then((() => {
const jsCode = `
<script>
window.close();
</script>
<h1>Posting URL</h1>
`;
res.header('Content-Security-Policy', "script-src 'unsafe-inline'");
res.set('Content-Type', 'text/html');
res.send(Buffer.from(jsCode));
}))
})
module.exports = router

View File

@@ -1,100 +0,0 @@
const Attachment = require('../../models/Attachment')
const User = require('../../models/User')
const testUserName = 'jestTestUserAttachment'
const password = 'Beans19934!!!'
let newUserId = null
let masterKey = null
let newPushKey = null
beforeAll(() => {
// Find and Delete Previous Test user, log in, get key
return User.getByUserName(testUserName)
.then((user) => {
return User.deleteUser(user?.id, password)
})
.then((results) => {
return User.register(testUserName, password)
})
.then(({ token, userId }) => {
newUserId = userId
return User.getMasterKey(userId, password)
})
.then((newMasterKey) => {
masterKey = newMasterKey
return true
})
.catch(((error) => {
console.log(error)
}))
})
test('Test Generate Push Key', () => {
return Attachment.generatePushKey(newUserId)
.then( (pushKey) => {
newPushKey = pushKey
return Attachment.generatePushKey(newUserId)
})
.then( (pushKey) => {
// expect a long, defined pushkey
expect(pushKey).toBeDefined()
expect(pushKey?.length).toBeGreaterThan(20)
expect(pushKey).toMatch(newPushKey)
})
})
test('Test get Push Key Bookmarklet', () => {
return Attachment.getPushkeyBookmarklet(newUserId)
.then(( bookmarklet => {
// Expect a bookmarklet containting URL encoded pushkey from above
const keyCheck = bookmarklet.includes(encodeURIComponent(newPushKey))
expect(bookmarklet).toBeDefined()
expect(keyCheck).toBe(true)
}))
})
test('Test Push URL', () => {
let url = 'https://www.solidscribe.com'
return Attachment.pushUrl(newPushKey, url)
.then(( results => {
return Attachment.textSearch(newUserId, 'scribe')
}))
.then((results) => {
expect(results.length == 1).toBe(true)
})
})
test('Test Delete Push Key', () => {
return Attachment.deletePushKey(newUserId)
.then(( results => {
// Expect a true bool
expect(results).toBe(true)
}))
})
afterAll(done => {
// Close Database
const db = require('../../config/database')
db.end()
done()
})

View File

@@ -1,117 +0,0 @@
const Note = require('../../models/Note')
const User = require('../../models/User')
const testUserName = 'jestTestUserNote'
const password = 'Beans1234!!!'
const secondPassword = 'Rice1234!!!'
let newUserId = null
let masterKey = null
let testNoteId = 0
let testNoteId2 = 0
const searchWord1 = 'beans'
const searchWord2 = 'RICE'
const updatedNoteText = 'Some Note Text for Testing more '+searchWord2+' is nice'
beforeAll(() => {
// Find and Delete Previous Test user, log in, get key
return User.getByUserName(testUserName)
.then((user) => {
return User.deleteUser(user?.id, password)
})
.then((results) => {
return User.register(testUserName, password)
})
.then(({ token, userId }) => {
newUserId = userId
return User.getMasterKey(userId, password)
})
.then((newMasterKey) => {
masterKey = newMasterKey
return true
})
.catch(((error) => {
console.log(error)
}))
})
test('Create Note', () => {
const noteTitle = 'Test Note'
const noteText = 'Some Note Text for Testing'
return Note.create(newUserId, noteTitle, noteText, masterKey)
.then((noteId) => {
testNoteId = noteId
expect(noteId).toBeGreaterThan(0)
})
})
test('Create Another Note', () => {
const noteTitle = 'Test Note2'
const noteText = 'Some Note Text for Testing more '+searchWord1
return Note.create(newUserId, noteTitle, noteText, masterKey)
.then((noteId) => {
testNoteId2 = noteId
expect(noteId).toBeGreaterThan(0)
})
})
test('Update a note', () => {
return Note.update(newUserId, testNoteId, updatedNoteText, 'title', 0, 0, 0, 'hash', masterKey)
.then((results) => {
expect(results.changedRows).toEqual(1)
})
})
test('Decrypt a note', () => {
return Note.get(newUserId, testNoteId, masterKey)
.then((noteData) => {
expect(noteData.text).toMatch(updatedNoteText)
})
})
test('Update note search index', () => {
return Note.reindex(newUserId, masterKey)
.then((results) => {
expect(results).toBe(true)
})
})
test('Search Encrypted Index', () => {
const searchString = `${searchWord1} ${searchWord2}`
return Note.encryptedIndexSearch(newUserId, searchString, null, masterKey)
.then(({ids}) => {
// Make sure beans is in one note and rice is in updated text
expect(ids.length).toEqual(2)
})
})
test('Search Encrypted Index no results', () => {
return Note.encryptedIndexSearch(newUserId, 'zzz', null, masterKey)
.then(({ids}) => {
// Make sure beans is in one note and rice is in updated text
expect(ids.length).toEqual(0)
})
})
afterAll(done => {
// Close Database
const db = require('../../config/database')
db.end()
done()
})

View File

@@ -1,67 +0,0 @@
const Note = require('../../models/Note')
const User = require('../../models/User')
const ShareNote = require('../../models/ShareNote')
const testUserName = 'jestTestUserNote'
const password = 'Beans1234!!!'
let newUserId = null
let masterKey = null
const testUserName2 = 'jestTestUserDude'
const password2 = 'Rice1234!!!'
let newUserId2 = null
let masterKey2 = null
let testNoteId = 0
let testNoteId2 = 0
// let sharedNoteId = 0 //ID of note shared with user
const shareUserId = 61
const searchWord1 = 'beans'
const searchWord2 = 'RICE'
const updatedNoteText = 'Some Note Text for Testing more '+searchWord2+' is nice'
beforeAll(() => {
// Find and Delete Previous Test user, log in, get key
return
User.getByUserName(testUserName)
.then(user => {
User.deleteUser(user?.id, password)
})
.then(user => {
User.getByUserName(testUserName2)
})
.then(user => {
User.deleteUser(user?.id, password)
})
.then((results) => {
return User.register(testUserName, password)
})
.then(({ token, userId }) => {
newUserId = userId
return User.getMasterKey(userId, password)
})
.then((newMasterKey) => {
masterKey = newMasterKey
return true
})
.catch(((error) => {
console.log(error)
}))
})
afterAll(done => {
// Close Database
const db = require('../../config/database')
db.end()
done()
})

View File

@@ -1,112 +0,0 @@
const User = require('../../models/User')
const crypto = require('crypto')
const testUserName = 'jestTestUser'
const password = 'Beans1234!!!'
const secondPassword = 'Rice1234!!!'
let testUserId = null
let masterKey = null
beforeAll(() => {
// Find and Delete Previous Test user
return User.getByUserName(testUserName)
.then((user) => {
return User.deleteUser(user?.id, password)
})
.then((results) => {
return results
})
})
test('Test User Registration', () => {
return User.register(testUserName, password)
.then((({ token, userId }) => {
testUserId = userId
expect(token).toBeDefined()
expect(userId).toBeGreaterThan(0)
}))
})
test('Test decrypting user masterKey', () => {
return User.getMasterKey(testUserId, password)
.then((newMasterKey) => {
masterKey = newMasterKey
expect(masterKey).toBeDefined()
})
})
test('Test generating public and private key pair', () => {
return User.generateKeypair(testUserId, masterKey)
.then(({publicKey, privateKey}) => {
const publicKeyMessage = 'Test: Public key decrypt - Pass'
const privateKeyMessage = 'Test: Private key decrypt - Pass'
//Encrypt Message with private Key
const privateKeyEncrypted = crypto.privateEncrypt(privateKey, Buffer.from(privateKeyMessage, 'utf8')).toString('base64')
const decryptedPrivate = crypto.publicDecrypt(publicKey, Buffer.from(privateKeyEncrypted, 'base64'))
//Conver back to a string
expect(decryptedPrivate.toString('utf8')).toMatch(privateKeyMessage)
//Encrypt with public key
const pubEncrMsc = crypto.publicEncrypt(publicKey, Buffer.from(publicKeyMessage, 'utf8')).toString('base64')
const publicDeccryptMessage = crypto.privateDecrypt(privateKey, Buffer.from(pubEncrMsc, 'base64') )
//Convert it back to string
expect(publicDeccryptMessage.toString('utf8')).toMatch(publicKeyMessage)
})
})
test('Test Logging in User', () => {
return User.login(testUserName, password)
.then(({token, userId}) => {
expect(token).toBeDefined()
expect(userId).toBeGreaterThan(0)
})
})
test('Test Changing Password', () => {
return User.changePassword(testUserId, password, secondPassword)
.then((passwordChangeResults) => {
expect(passwordChangeResults).toBe(true)
})
})
test('Test Login with wrong password', () => {
return User.login(testUserName, password)
.then(({token, userId}) => {
expect(token).toBeNull()
expect(userId).toBeNull()
})
})
test('Test decrypting masterKey with new Password', () => {
return User.getMasterKey(testUserId, secondPassword)
.then((newMasterKey) => {
expect(newMasterKey).toBeDefined()
expect(newMasterKey.length).toBe(28)
})
})
afterAll(done => {
// Close Database
const db = require('../../config/database')
db.end()
done()
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,15 +0,0 @@
{
"background_color": "purple",
"description": "Take Notes",
"display": "fullscreen",
"icons": [
{
"src": "/api/static/assets/favicon.ico",
"sizes": "192x192",
"type": "image/png"
}
],
"name": "Notes",
"short_name": "Notes",
"start_url": "/#/notes"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

22
updatedomain.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
# Setup env variables
source ~/.env
# Send updated dynamic IP address to Namecheap, in order to update subdomains.
# This uses curl (separate pkg) to send the change; Namecheap automatically detects source IP if the ip field (like domain, password) ..
# is not specified.
# info helper
info() { printf "\n%s %s\n\n" "$( date )" "$*" >&2; }
info "Starting IP update for subdomains"
echo "https://dynamicdns.park-your-domain.com/update?host=$DYDNS_HOST&domain=$DYDNS_DOMAIN&password=$DYDNS_PASS"
# first subdomain
curl "https://dynamicdns.park-your-domain.com/update?host=$DYDNS_HOST&domain=$DYDNS_DOMAIN&password=$DYDNS_PASS"
# second subdomain
curl "https://dynamicdns.park-your-domain.com/update?host=$DYDNS_HOST2&domain=$DYDNS_DOMAIN&password=$DYDNS_PASS"
info "IP update done"