Compare commits

..

93 Commits

Author SHA1 Message Date
Max
276a72b4ce Gigantic Update
* Migrated manual tests to jest and started working on better coverage
* Added a bookmarklet and push key generation tool allowing URL pushing from bookmarklets
* Updated web scraping with tons of bug fixes
* Updated attachments page to handle new push links
* Aggressive note change checking, if patches get out of sync, server overwrites bad updates.
2023-10-17 19:46:14 +00:00
Max
b5ef64485f + Giant update to multiple users editing notes.
- Edits are done per DOM element, making diffs smaller and faster
- Multiple users can now edit the same note witheout it turning into a gaint mess
- Caret position is saved better and wont jump around as much

+ Removed Active sessions text
2023-07-30 04:18:17 +00:00
Max
c61f0c0198 Graph update and little noe ui tweaks 2023-07-23 23:13:28 +00:00
Max
d3acd62688 Deleting unused files 2023-03-02 19:53:32 +00:00
Max
a1ca4c3d06 Commiting all changes for repo cleaning 2023-03-02 19:46:51 +00:00
Max
27699cd6fc Updated dnydns script 2023-03-02 01:35:52 +00:00
Max
39e153b8e1 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
48c1fa8e69 Added timeout to fetch user totals which prevents
Duplicate calls which would be really annoying
2022-12-22 01:59:27 +00:00
Max
0202d1acda 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
31473c02ea Added metric tracking and some other little fixes 2022-12-20 17:42:38 +00:00
Max
59f13484a7 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
0c107a60bd 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
2b76f74dee Added cycle tracking beta to app 2022-10-21 19:34:13 +00:00
Max
1a6a7bdfd4 Updated vue CLI to latest version
Added cycle tracking base
2022-10-13 19:28:35 +00:00
Max G
b51e5ac0d0 Adding everything to get started on cycle tracking and maybe avid habit clone 2022-09-25 17:17:41 +00:00
Max G
77cd95fdcb Added paste button and touched up some styles 2022-07-05 05:10:40 +00:00
Max G
d94b8c90fc 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
0c4f6e94c1 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
bc44b3db9a Lots of little ease of use tweaks 2022-02-25 02:33:49 +00:00
Max G
c1797474b8 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
148b822d49 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
b666bfc197 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
7ccf0417e0 Checking in minor changes for server migration 2021-02-12 17:11:33 +00:00
Max G
c7e342be4d * 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
941d21d9cb Added vue config 2020-10-05 06:46:13 +00:00
Max G
e5adaefa0e Updating Everything to work correctly 2020-10-05 06:45:50 +00:00
Max G
cf426eba81 * updated server packages 2020-10-04 18:59:30 +00:00
Max G
91e37d368d Updated Squire 2020-10-03 19:15:31 +00:00
Max G
b0c487404c * 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
b34a62e114 * 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
a8a966866c * 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
06b8f0ad6a 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
2ae84ab73e 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
8b711ab508 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
0bbdda9a2a * Updated server packages 2020-06-21 01:07:27 +00:00
Max G
39b3eef64a Removed some transitions from tooltips 2020-06-21 01:01:12 +00:00
Max G
071aaf22cd * 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
d2624628d8 * 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
6bb856689d * 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
8e5e06be9b * 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
06a140e0d4 * 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
543ecf0f2d 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
5096e74a60 * 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
e87e8513bc * 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
67b218329b * 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
df073b0e4d 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
a545ced98f Major Update: Changed Text Input View
* Created new toolbar that moves on mobile
2020-05-02 19:10:20 +00:00
Max G
29845e2294 Tweaked shrinking buttons for better display on mobile 2020-04-16 01:41:47 +00:00
Max G
e296775a31 * Tags Dropbown Beta...kinda crappy 2020-04-15 21:54:36 +00:00
Max G
3d2c9868fd * Little Bug Fixes All Around 2020-04-15 20:44:24 +00:00
Max G
9efe21476f * 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
2b8f70b5fa * 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
278b204b3b * 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
3535f0cb24 * 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
a3fa4b0f3c 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
25b2bd237b Added package lock 2020-03-30 05:32:46 +00:00
Max G
aa4de83de6 * 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
35605e54d9 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
6dfe753d38 Small change to make user all note option menus fade in 2020-03-29 23:01:37 +00:00
Max G
003153d994 Removed Semantic Added Fomantic 2020-03-26 05:05:31 +00:00
Max G
ecbf6a9cde 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
749d2cea94 Remove style making double editing weird
fixes #29
2020-03-14 18:52:00 +00:00
Max G
342ac95282 Bug fixes and encryption handling 2020-03-14 06:04:03 +00:00
Max G
ca0b649ff6 Pressing enter in a note title moves cursor to end of note. 2020-03-14 02:09:43 +00:00
Max G
4533f6065d Minor bug fixes 2020-03-13 23:51:45 +00:00
Max G
f481a97a8c Encrypted Notes Alpha!
fixes #28
2020-03-13 23:34:32 +00:00
Max G
f7fc937d26 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
4c17efceea Added note tags to main note edit display 2020-03-09 03:11:05 +00:00
Max G
d3c0d6e2b9 Creating new tags doesn't throw an error
fixes #22
2020-03-04 05:20:14 +00:00
Max G
de3391eb94 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
2d0beec409 Little green more note indicator.
fixes #23
2020-03-01 00:54:54 +00:00
Max G
b838f9f571 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
72b7f8946a 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
902e779a84 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
8f5d8049d0 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
ef0a6a44c9 * Fixed title display issue on note 2020-02-23 06:46:23 +00:00
Max G
fab0b3873f * 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
7fe702cb1b Added a function to calculate math on notes 2020-02-19 00:31:18 +00:00
Max G
aded72928c Better sorting of note categories
fixes #1
2020-02-18 22:52:12 +00:00
Max G
e56042958b 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
daa674c54f 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
a51d81b013 * 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
771b739c37 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
c903bcbcd1 Fixing quick notes
Updating all the icons
making search bar thinner
2020-02-11 06:05:28 +00:00
Max G
9b7fc679e8 * Tags can now be toggled by clicking
* Side slide component now respects note colors
2020-02-10 22:21:06 +00:00
Max G
623d094d7b 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
ad91218359 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
97e7988ed1 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
86f7f61933 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
133a86e09e Remove TinyMce Added Squire 2020-02-01 22:21:22 +00:00
Max G
68effaa5c3 Some minor bug fixing 2020-01-03 01:54:11 +00:00
Max G
abb4e20ec3 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
6fe39406b7 I need to get back into using git. The hell is wrong with me!? 2019-12-20 05:50:50 +00:00
97 changed files with 30619 additions and 10158 deletions

6
.gitignore vendored
View File

@@ -7,3 +7,9 @@ pids
*.seed
*.pid.lock
.env
# exclude everything
staticFiles/*
# exception to the rule
!staticFiles/assets/

View File

@@ -11,7 +11,9 @@ echo '-------'
BACKUPDIR="/home/mab/databaseBackupSolidScribe"
#DEVDBPASS="Crama!Lama*Jamma###88383!!!!!345345956245i"
DEVDBPASS="RootPass1234!"
#DEVDBPASS="RootPass1234!"
DEVDBPASS="ReallySecureRootPass123!"
# LazaLinga&33Can't!Do!That34
cd $BACKUPDIR
@@ -28,8 +30,12 @@ gunzip -dkv $LASTZIPPEDFILE
BACKUPFILE=$(ls -At *.sql | head -n1)
#Fix to replace incompatible DB type
echo "Updating table name in $BACKUPFILE"
sed -i $BACKUPFILE -e 's/utf8mb4_0900_ai_ci/utf8mb4_unicode_ci/g'
echo "Updating table name in -> $BACKUPFILE"
#sed -i $BACKUPFILE -e 's/utf8mb4_0900_ai_ci/utf8mb4_unicode_ci/g'
#Fix encoding for dev DB and exclude system tables
sed -i 's/utf8mb4_0900_ai_ci/utf8mb4_general_ci/g' $BACKUPFILE
sed -r '/INSERT INTO `(sys|mysql)`/d' $BACKUPFILE > $BACKUPFILE
echo "Removing and syncing static files"
rm -r /home/mab/ss/staticFiles/*
@@ -38,8 +44,20 @@ rsync -e 'ssh -p 13328' -hazC --update mab@solidscribe.com:/home/mab/pi/staticFi
echo "Updating Database"
mysql -u root --password="$DEVDBPASS" < $BACKUPFILE
## Optimize Database Tables
# mysqlcheck --all-databases
mysqlcheck --all-databases -o -u root --password="$DEVDBPASS" --silent
# mysqlcheck --all-databases --auto-repair
# mysqlcheck --all-databases --analyze
# Fix an issues with DB after messing around with it
mysql_upgrade -u root --password="$DEVDBPASS"
#clean up extracted and modified SQL dumps
rm *.sql
echo '-------'
echo "Applied Prod database to Dev. LastFile: $BACKUPFILE"
echo '-------'

View File

@@ -1,18 +1,24 @@
#!/bin/bash
# Take all variables in .env and turn them into local variables for this script
source ~/.env
BACKUPDIR="/home/mab/databaseBackupSolidScribe"
mkdir -p $BACKUPDIR
cd $BACKUPDIR
NOW=$(date +"%Y-%m-%d_%H-%M")
ssh mab@solidscribe.com -p 13328 "mysqldump --all-databases --single-transaction --user root -pRootPass1234!" > "backup-$NOW.sql"
ssh mab@solidscribe.com -p 13328 "mysqldump --all-databases --single-transaction --user root -p$PROD_DB_PASS" > "backup-$NOW.sql"
gzip "backup-$NOW.sql"
# cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/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
##

3
client/.gitignore vendored
View File

@@ -6,6 +6,9 @@ node_modules
# local env files
.env.local
.env.*.local
*.pem
*.crt
*.key
# Log files
npm-debug.log*

View File

@@ -1 +1,19 @@
# Solid Scribe
# client
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

19
client/jsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

23433
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,21 +7,21 @@
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^0.20.0",
"axios": "^1.1.3",
"core-js": "^3.6.5",
"es6-promise": "^4.2.8",
"fomantic-ui-css": "^2.8.7",
"fomantic-ui-css": "^2.9.0",
"vue": "^2.6.11",
"vue-chartjs": "^5.0.1",
"vue-router": "^3.2.0",
"vuedraggable": "^2.24.3",
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"node-sass": "^4.12.0",
"sass-loader": "^8.0.2",
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-router": "^5.0.8",
"@vue/cli-plugin-vuex": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"vue-template-compiler": "^2.6.11"
},
"browserslist": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 215 B

View File

@@ -15,12 +15,17 @@
<!-- <title><%= htmlWebpackPlugin.options.title %></title> -->
</head>
<body>
<noscript>
<strong>We're sorry but Solid Scribe doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<!-- placeholder data for scrapers with no JS -->
<style>
body {
background-color: #212221;
color: #aeaeae;
height: 100vh;
width: 100%;
}
.centered {
position: fixed;

View File

@@ -200,6 +200,11 @@ export default {
this.blockUntilNextRequest = true
})
//Track users active sessions
this.$io.on('update_active_user_count', countData => {
this.$store.commit('setActiveSessions', countData)
})
},
computed: {
loggedIn () {

View File

@@ -53,7 +53,7 @@ helpers.timeAgo = (time) => {
if (typeof format[2] == 'string') {
return format[list_choice]
} else {
return Math.floor(seconds / format[2]) + ' ' + format[1]// + ' ' + token
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token
}
}
}

View File

@@ -3,7 +3,7 @@
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(/api/static/assets/roboto-latin.woff2) format('woff2');
src: local('Roboto'), local('Roboto-Regular'), url(./roboto-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin */
@@ -11,10 +11,21 @@
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(/api/static/assets/roboto-latin-bold.woff2) format('woff2');
src: local('Roboto Bold'), local('Roboto-Bold'), url(./roboto-latin-bold.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
body {
margin: 0;
padding: 0;
/* overflow-x: hidden;*/
min-width: 320px;
background: green;
font-family: 'Roboto', system-ui, -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 14px;
line-height: 1.4285em;
color: rgba(0, 0, 0, 0.87);
position: relative;
}
:root {
@@ -43,7 +54,7 @@ html {
height:100%;
padding: 0;
margin: 0;
background: none;
background: var(--body_bg_color);
}
a:hover {
text-decoration: underline;
@@ -85,7 +96,7 @@ body {
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
#app {
background: var(--body_bg_color);
/* background: var(--body_bg_color);*/
}
.ui.segment {
@@ -136,6 +147,9 @@ body {
.ui.dividing.header {
border-bottom-color: var(--dark_border_color);
}
.ui.dividing.header > .sub.header {
color: var(--dark_border_color);
}
.ui.icon.input > i.icon {
color: var(--text_color);
}
@@ -164,12 +178,21 @@ div.ui.basic.green.label {
border-color: var(--dark_border_color) !important;
}
/*Overwrites for modifiable theme color */
i.green.icon.icon.icon.icon {
i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
color: var(--main-accent);
}
.button {
box-shadow: 2px 2px 4px -2px rgba(40, 40, 40, 0.89) !important;
transition: all 0.9s ease;
position: relative;
}
.button:hover {
box-shadow: 3px 2px 3px -2px rgba(40, 40, 40, 0.95) !important;
}
.button:active {
transform: translateY(1px);
}
.ui.green.buttons, .ui.green.button, .ui.green.button:hover {
background-color: var(--main-accent);
}
@@ -290,7 +313,7 @@ i.green.icon.icon.icon.icon {
border: none;
/*height: calc(100% - 69px);*/
min-height: 500px;
min-height: 300px;
background-color: var(--small_element_bg_color);
/*margin-bottom: 15px;*/
@@ -309,6 +332,9 @@ i.green.icon.icon.icon.icon {
margin-left: auto;
margin-right: auto;
max-width: 1100px;
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
}
.squire-box::selection,
.squire-box::-moz-selection {
@@ -330,9 +356,14 @@ i.green.icon.icon.icon.icon {
background-color: rgba(255, 255, 255, 0.2);
}
.note-card-text code,
.squire-box code,
.note-card-text pre,
.squire-box pre {
/*word-wrap: break-word;*/
display: inline-block;
border-left: 2px solid var(--main-accent);
padding-left: 15px;
}
.note-card-text p,
.squire-box p {
@@ -367,8 +398,37 @@ i.green.icon.icon.icon.icon {
.squire-box ol,
.note-card-text ul,
.squire-box ul {
margin: 8px 0 0 0;
margin: 3px 0;
display: block;
}
/* Add border 1 indent level */
.note-card-text > ol > ol,
.squire-box > ol > ol,
.note-card-text > ul > ul,
.squire-box > ul > ul
{
border-left: 1px solid var(--border_color);
}
.note-card-text ol > ol,
.squire-box ol > ol,
.note-card-text ul > ul,
.squire-box ul > ul {
list-style-type: upper-alpha;
}
ol {
counter-reset: item;
}
ol li {
display: block;
}
ol li:before {
content: counters(item, ".") ".";
counter-increment: item;
padding-right: 10px;
}
.note-card-text ul > li,
.squire-box ul > li {
position: relative;
@@ -495,10 +555,6 @@ i.green.icon.icon.icon.icon {
/* adjust checkboxes for mobile. Make them a little bigger, easier to click */
@media only screen and (max-width: 740px) {
.squire-box {
min-height: calc(100vh - 122px);
}
.ui.button.shrinking {
font-size: 0.85714286rem;
margin: 0 3px;
@@ -536,6 +592,15 @@ i.green.icon.icon.icon.icon {
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;
}
}
@@ -880,6 +945,14 @@ i.green.icon.icon.icon.icon {
-webkit-transform-origin: left center;
transform-origin: left center;
}
@media only screen and (max-width: 740px) {
/*hide tooltips on mobile*/
[data-tooltip]:hover:before,
[data-tooltip]:hover:after {
visibility: visible;
opacity: 0;
}
}
.glint:after {
@@ -918,3 +991,13 @@ i.green.icon.icon.icon.icon {
opacity: 0;
}
}
.shade {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0,0,0,0.7);
z-index: 1000;
}

View File

@@ -2117,7 +2117,7 @@ var cleanTree = function cleanTree ( node, config, preserveWS ) {
break;
}
}
data = data.replace( /^[ \t\r\n]+/g, sibling ? ' ' : '' );
data = data.replace( /^[ \r\n]+/g, sibling ? ' ' : '' );
}
if ( endsWithWS ) {
walker.currentNode = child;
@@ -2132,7 +2132,7 @@ var cleanTree = function cleanTree ( node, config, preserveWS ) {
break;
}
}
data = data.replace( /[ \t\r\n]+$/g, sibling ? ' ' : '' );
data = data.replace( /[ \r\n]+$/g, sibling ? ' ' : '' );
}
if ( data ) {
child.data = data;
@@ -2693,7 +2693,8 @@ var sanitizeToDOMFragment = function ( html, isPaste, self ) {
ALLOW_UNKNOWN_PROTOCOLS: true,
WHOLE_DOCUMENT: false,
RETURN_DOM: true,
RETURN_DOM_FRAGMENT: true
RETURN_DOM_FRAGMENT: true,
FORCE_BODY: false
}) : null;
return frag ? doc.importNode( frag, true ) : doc.createDocumentFragment();
};

View File

@@ -20,7 +20,7 @@
.image-placeholder {
width: 100%;
height: 100%;
max-height: 100px;
max-height: 75px;
}
.image-placeholder:after {
content: 'No Image';
@@ -89,7 +89,14 @@
<!-- image and text -->
<div class="six wide center aligned middle aligned column">
<a :href="linkUrl" target="_blank" >
<img v-if="item.file_location" class="attachment-image" :src="`/api/static/thumb_${item.file_location}`">
<img v-if="item.file_location" class="attachment-image"
onerror="
this.onerror=null;
this.src='/api/static/assets/marketing/void.svg';
this.classList.add('image-placeholder');
this.insertAdjacentText('afterend', 'Image not found');
"
:src="`/api/static/thumb_${item.file_location}`">
<span v-else>
<img class="image-placeholder" loading="lazy" src="/api/static/assets/marketing/void.svg">
No Image
@@ -110,11 +117,16 @@
<a class="link" :href="linkUrl" target="_blank">{{linkText}}</a>
<!-- Buttons -->
<div class="ui small compact basic button" v-on:click="openNote">
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openNote">
<i class="file outline icon"></i>
Open Note
</div>
<div class="ui small compact basic button" v-on:click="openEditAttachments"
<div v-if="!item.note_id" class="ui small compact basic disabled button">
<i class="angle double up icon"></i>
Pushed from Web
</div>
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openEditAttachments"
:class="{ 'disabled':this.searchParams.noteId }">
<i class="folder open outline icon"></i>
Note Files
@@ -171,6 +183,9 @@
this.checkKeyup()
})
},
updated: function(){
this.checkKeyup()
},
methods: {
checkKeyup(){
let elm = this.$refs.edit

View File

@@ -1,45 +1,51 @@
<template>
<div :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
<div class="ui basic segment">
<div>
<div class="ui grid">
<div class="ui sixteen wide center aligned column">
<div class="ui fluid button" v-on:click="clearStyles">
<div class="ui sixteen wide column">
<div class="ui dividing header">
Reset Background Color and Icon
</div>
<div class="ui labeled basic icon button" v-on:click="clearStyles">
<i class="refresh icon"></i>
Clear All Styles
Reset
</div>
</div>
<div class="row">
<div class="sixteen wide column">
<br>
<p>Note Color</p>
<div class="sixteen wide column rounded" :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
<div class="ui dividing header" :style="{ 'color':allStyles['noteText']}">
<i class="fill drip icon"></i>
Background Color
</div>
<div v-for="color in colors"
class="color-button"
:style="{ backgroundColor:color }"
v-on:click="chosenColor(color)"
></div>
</div>
</div>
<div class="row">
<div class="sixteen wide column">
<p>Note Icon
<div class="ui dividing header">
<span v-if="allStyles.noteIcon" >
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
</span>
</p>
<div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
<i :class="`large ${icon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
Note Icon
</div>
<div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
<i :class="`large ${icon} icon`"></i>
</div>
</div>
<div class="row">
<div class="sixteen wide column">
<p>Icon Color</p>
<div class="ui dividing header">
<span v-if="allStyles.noteIcon" >
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
</span>
Icon Color
</div>
<div v-for="color in getReducedColors()"
class="color-button"
:style="{ backgroundColor:color }"
@@ -47,8 +53,7 @@
>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -147,20 +152,24 @@
}
</script>
<style type="text/css" scoped>
.icon-button {
.icon-button, .color-button {
height: 40px;
width: calc(10% - 7px);
width: calc(15% - 1px);
display: inline-block;
cursor: pointer;
font-size: 1.3em;
border: 1px solid grey;
text-align: center;
padding: 5px 0px 0 0;
border-radius: 4px;
box-shadow: 0px 1px 3px 0px #3e3e3e;
margin: 2px 2px 0 0;
box-sizing: border-box;
}
.color-button {
display: inline-block;
width: calc(10% - 7px);
height: 30px;
border-radius: 30px;
box-shadow: 0px 1px 3px 0px #3e3e3e;
margin: 7px 7px 0 0;
cursor: pointer;
width: calc(10% - 4px);
}
.rounded {
border-radius: 5px;
}
</style>

View File

@@ -19,7 +19,7 @@
padding: 1em 5px;
cursor: pointer;
}
.popup-row > span {
.popup-row > p {
/*width: calc(100% - 50px);*/
display: inline-block;
text-align: left;
@@ -85,6 +85,18 @@
animation: progressBar 3s linear;
animation-fill-mode: both;
}
.time-display {
display: inline-block;
width: calc(100% - 25px);
/*text-align: right;*/
color: white;
font-size: 0.7em;
margin: 0 0 0 25px;
}
.text-display {
display: inline-block;
width: 100%;
}
@keyframes progressBar {
0% { width: 0; }
@@ -101,7 +113,11 @@
<div class="meter">
<span><span class="progress"></span></span>
</div>
<span><i class="small info circle icon"></i>{{ item }}</span>
<p class="text-display">
<i class="small info circle icon"></i>
{{ item.text }}
<span class="time-display">{{ item.time }}</span>
</p>
</div>
</div>
</template>
@@ -119,8 +135,8 @@
}
},
beforeMount(){
this.$bus.$on('notification', info => {
this.displayNotification(info)
this.$bus.$on('notification', notificationText => {
this.displayNotification(notificationText)
})
},
mounted(){
@@ -131,8 +147,17 @@
},
methods: {
displayNotification(newNotification){
this.notifications.push(newNotification)
displayNotification(notificationText){
const date = new Date()
const time = date.toLocaleTimeString()
const notification = {
text: notificationText,
time: time
}
this.notifications.unshift(notification)
clearTimeout(this.totalTimeout)
this.totalTimeout = setTimeout(() => {
this.dismiss()

View File

@@ -1,12 +1,13 @@
<style scoped>
.slotholder {
height: 100vh;
width: 155px;
width: 180px;
display: block;
float: left;
overflow: hidden;
}
.global-menu {
width: 155px;
width: 180px;
/* background: #221f2b; */
background: #221f2b;
margin: 0;
@@ -14,14 +15,14 @@
box-sizing: border-box;
display: block;
position: fixed;
z-index: 111;
z-index: 900;
top: 0;
left: 0;
bottom: 0;
}
.menu-logo-display {
width: 27px;
margin: 5px 0 0 41px;
margin: 5px 0 0 55px;
display: inline-block;
height: auto;
}
@@ -54,9 +55,6 @@
text-decoration: none;
}
.router-link-active i {
/*color: #16ab39;*/
}
.router-link-active {
background-color: #534c68;
}
@@ -68,7 +66,7 @@
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.7);
z-index: 100;
z-index: 899;
cursor: pointer;
}
.top-menu-bar {
@@ -89,6 +87,7 @@
margin: 0;
padding: 0;
overflow: hidden;
}
.place-holder {
width: 100%;
@@ -109,6 +108,7 @@
text-align: center;
color: #8c80ae;
cursor: pointer;
background-color: var(--menu-background);
}
.mobile-button {
@@ -141,6 +141,17 @@
.mobile-button.active {
background-color: transparent;
}
.single-line-text {
width: calc(100%);
/*margin: 5px 10px;*/
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
.faded {
color: var(--dark_border_color);
}
</style>
@@ -170,7 +181,6 @@
</span>
</div>
<!-- open straight to note -->
<router-link
v-if="loggedIn && $store.getters.totals && $store.getters.totals['quickNote']"
@@ -198,8 +208,8 @@
</router-link>
<!-- menu -->
<div class="mobile-button">
<i class="green link bars icon" v-on:click="collapseMenu"></i>
<div class="mobile-button" v-on:click="collapseMenu">
<i class="green link bars icon" ></i>
Menu
</div>
@@ -220,12 +230,12 @@
<div class="menu-section" v-if="loggedIn">
<div v-if="!disableNewNote" @click="createNote" class="menu-item menu-item menu-button">
<div class="ui green button">
<div class="ui green fluid compact button">
<i class="plus icon"></i>New Note
</div>
</div>
<div v-if="disableNewNote" class="menu-item menu-item menu-button">
<div class="ui basic button">
<div class="ui basic fluid compact button">
<i class="plus loading icon"></i>New Note
</div>
</div>
@@ -238,14 +248,19 @@
</router-link>
<div>
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(3)" v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)">
<i class="grey mail outline icon"></i>Inbox
<i class="grey paper plane outline icon"></i>Shared
<counter v-if="$store.getters.totals && $store.getters.totals['sharedToNotes']" class="float-right" number-id="sharedToNotes" />
</div>
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
<i class="grey archive icon"></i>Archived
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
<counter v-if="$store.getters.totals && $store.getters.totals['archivedNotes']" class="float-right" number-id="archivedNotes" />
</div>
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['trashedNotes'] > 0">
<i class="grey trash alternate outline icon"></i>Trashed
<counter v-if="$store.getters.totals && $store.getters.totals['trashedNotes']" class="float-right" number-id="trashedNotes" />
</div>
<!-- <div class="menu-item sub">Show Only <i class="caret down icon"></i></div> -->
<!-- <div v-on:click="updateFastFilters(0)" class="menu-item menu-button sub"><i class="grey linkify icon"></i>Links</div> -->
@@ -302,24 +317,49 @@
</div>
</div>
<div class="menu-section">
<router-link class="menu-item menu-button" exact-active-class="active" to="/help">
<i class="question circle outline icon"></i>Help
</router-link>
</div>
<div class="menu-section" v-if="loggedIn">
<router-link class="menu-item menu-button" exact-active-class="active" to="/settings">
<i class="cog icon"></i>Settings
</router-link>
</div>
<div class="menu-section" v-if="loggedIn">
<router-link class="menu-item menu-button" exact-active-class="active" to="/metrictrack">
<i class="calendar check outlin icon"></i>Metric Track
</router-link>
</div>
<div class="menu-section">
<router-link class="menu-item menu-button" exact-active-class="active" to="/help">
<i class="question circle outline icon"></i>Help
</router-link>
</div>
<div class="menu-section" v-if="loggedIn">
<div class="menu-item menu-button" v-on:click="logout()">
<i class="log out icon"></i>Log Out
</div>
</div>
<!-- Tags -->
<div class="menu-section" v-if="gotTags()">
<div class="menu-item">
<i class="green tags icon"></i>
Tags
</div>
</div>
<div v-if="gotTags()">
<div class="menu-section"
v-for="(data, tag) in $store.getters.totals['tags']">
<router-link class="menu-item menu-button" :to="`/search/tags/${tag}`">
<span class="single-line-text">
<!-- <i class="small grey tag icon"></i> -->
<span class="float-right">{{ data.uses }}</span>
<span class="faded"> #</span> {{ tag }}</span>
</router-link>
</div>
</div>
<div v-on:click="reloadPage" class="version-display" v-if="version != 0" >
<i :class="`${getVersionIcon()} icon`"></i> {{ version }}
</div>
@@ -379,6 +419,16 @@
},
},
methods: {
gotTags(){
if(this.loggedIn && this.$store.getters.totals && this.$store.getters.totals.tags
&& Object.keys(this.$store.getters.totals.tags).length
){
return true
}
return false
},
logout() {
this.$router.push('/')
@@ -477,8 +527,11 @@
location.reload(true)
},
getVersionIcon(){
if(!this.version){
return 'radiation alternate'
}
const icons = ['cat','crow','dog','dove','dragon','fish','frog','hippo','horse','kiwi bird','otter','spider', 'smile', 'robot', 'hat wizard', 'microchip', 'atom', 'grin tongue squint', 'radiation', 'ghost', 'dna', 'burn', 'brain', 'moon', 'torii gate']
const index = ( parseInt(this.version.replace(/\./g,'')) % (icons.length))
const index = ( parseInt(String(this.version).replace(/\./g,'')) % (icons.length))
return icons[index]
}

View File

@@ -39,9 +39,9 @@
.loading-container {
text-align: center;
width: 100%;
min-height: 100px;
/*min-height: 100px;*/
margin: 20px 0;
padding: 40px;
/*padding: 40px;*/
border-radius: 7px;
background-color: var(--small_element_bg_color);
}

View File

@@ -1,10 +1,10 @@
<template>
<div v-on:keyup.enter="login()">
<div>
<!-- thicc form display -->
<div v-if="!thin" class="ui large form">
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register">
<div class="field">
<div class="ui input">
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
@@ -15,6 +15,11 @@
<input v-model="password" type="password" name="password" placeholder="Password">
</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">
@@ -24,18 +29,11 @@
<div class="ui fluid buttons">
<div v-on:click="register()" class="ui button">
<div v-on:click="register" class="ui green button" :class="{ 'disabled':(username.length == 0 || password.length == 0)}">
<i class="plug icon"></i>
Sign Up
</div>
<div class="or"></div>
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="login()" class="ui green button">
<i class="power icon"></i>
Login
</div>
</div>
</div>
<div class="sixteen wide column">
@@ -49,12 +47,12 @@
</div>
<!-- Thin form display -->
<div v-if="thin" class="ui small form">
<div v-if="thin" class="ui small form" v-on:keyup.enter="login">
<div v-if="!require2FA" class="field"><!-- hide this field if someone is logging in with 2FA -->
<div 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>
@@ -87,7 +85,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>
@@ -128,6 +126,7 @@
enabled: false,
username: '',
password: '',
password2: '',
authToken: '',
require2FA: false,
}
@@ -160,13 +159,21 @@
},
register(){
if( this.username.length == 0 || this.password.length == 0 ){
let error = false
if(this.$route.name == 'LoginPage'){
this.$bus.$emit('notification', 'Both a Username and Password are Required')
return
if( this.username.length == 0 || this.password.length == 0 || this.password2.length == 0 ){
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
@@ -228,7 +235,6 @@
<style type="text/css" scoped="true">
.small-terms {
display: inline-block;
text-align: right;
width: 100%;
font-size: 0.9em;
}

View File

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

View File

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

View File

@@ -0,0 +1,164 @@
<style type="text/css" scoped>
.modal-content {
position: fixed;
top: 40%;
left: 50%;
/* bring your own prefixes */
transform: translate(-50%, -40%);
z-index: 300;
padding: 1em;
box-sizing: border-box;
width: 50%;
max-height: 100%;
/*overflow: hidden;*/
overflow-y: scroll;
font-weight: normal;
}
.modal-content.fullscreen {
width: 96%;
height: 100%;
max-height: 100%;
}
.close-container {
position: fixed;
top: 5px;
right: 5px;
z-index: 320;
}
/* Shrink button text for mobile */
@media only screen and (max-width: 740px) {
.modal-content {
width: 100%;
/* padding-bottom: 55px;*/
}
}
.modal-content.right-side {
width: 60%;
max-height: none;
height: 100vh;
padding: 0;
margin: 0;
top: 0;
bottom: 0;
left: 0;
left: auto;
transform: translate(0, 0);
}
.close-container-right-side {
position: fixed;
top: 5px;
left: calc(60% + 2px);
z-index: 320;
}
.shade {
position: fixed;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #0000007d;
z-index: 299;
backdrop-filter: blur(2px);
}
.fade-out-top {
animation: fade-out-top 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
}
.fade-out {
animation: fade-out 0.3s ease-out both;
}
@keyframes fade-out-top {
0% {
/*transform: translate(-50%, -50%);*/
opacity: 1;
}
100% {
/*transform: translate(-50%, -70%);*/
opacity: 0;
}
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.fade-in {
/*animation: fade-in 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;*/
}
@keyframes fade-in {
0% {
transform: translate(-50%, -70%);
opacity: 0;
}
100% {
transform: translate(-50%, -50%);
opacity: 1;
}
}
</style>
<template>
<div v-if="openModel">
<div class="modal-content" :class="{ 'fade-out-top':(animateOut), 'fade-in':(!animateOut), 'fullscreen':(fullscreen)}">
<slot></slot>
</div>
<!-- full screen close button -->
<div class="close-container" v-if="fullscreen && clickOutClose !== false">
<div class="ui green icon button" v-on:click="closeModel">
<i class="close icon"></i>
</div>
</div>
<div class="shade" v-on:click="closeModel" v-on:mouseenter=" hoverOutClose?closeModel():null " :class="{ 'fade-out':(animateOut) }"></div>
</div>
</template>
<script>
export default {
props: [
'fullscreen', //Make the model really big
'clickOutClose', //Set to false to prevent closing of modal by clicking out
'hoverOutClose', //Close if cursor leaves modal
],
data: function(){
return {
openModel:true,
animateOut:false,
}
},
methods: {
closeModel(){
//Don't allow closing by clicking out
if(this.clickOutClose === false){
return
}
//Set stups to close model, animate out
this.animateOut = true
setTimeout( () => {
this.openModel = false
this.$emit('close')
//Once close event is sent, reset to default state
this.animateOut = false
this.openModel = true
}, 800)
},
}
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,15 @@
<template>
<div class="note-title-display-card"
:style="{'background-color':color, 'color':fontColor, 'border-color':color }"
:class="{'currently-open':(currentlyOpen || showWorking), 'bgboy':triggerClosedAnimation, 'title-view':titleView }"
>
:class="{
'currently-open':(currentlyOpen || showWorking),
'ring':triggerClosedAnimation,
'title-view':titleView
}">
<!-- Show title and snippet below it -->
<div class="overflow-hidden note-card-text" @click="cardClicked" v-if="!titleView">
<div class="overflow-hidden note-card-text" @click.stop="cardClicked" v-if="!titleView">
<span v-if="note.title == '' && note.subtext == ''">
Empty Note
@@ -21,28 +24,10 @@
class="big-text"><p>{{ note.title }}</p></span>
<span class="tags" v-if="note.tags">
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}</span>
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click.stop="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}</span>
<br>
</span>
<!-- Sub text display -->
<span v-if="note.subtext.length > 0"
class="small-text"
v-html="note.subtext"></span>
<!-- Not indexed warning -->
<!-- <span v-if="note.indexed != 1">
<span class="green label">Not Indexed</span>
</span> -->
<div class="ui fluid basic button" v-if="note.encrypted == 1">
<i class="green lock icon"></i>
Locked
</div>
<!-- Shared Details -->
<span class="subtext" v-if="note.shared == 2">
<i class="green paper plane outline icon"></i> Shared
@@ -62,23 +47,85 @@
</span>
</span>
<!-- Sub text display -->
<span v-if="note.subtext.length > 0"
class="small-text"
v-html="note.subtext"></span>
<!-- Not indexed warning -->
<!-- <span v-if="note.indexed != 1">
<span class="green label">Not Indexed</span>
</span> -->
<!-- <div class="ui fluid basic button" v-if="note.encrypted == 1">
<i class="green lock icon"></i>
Locked
</div> -->
</div>
<div v-if="titleView" class="single-line-text" @click="cardClicked">
<span class="title-line" v-if="note.title.length > 0">{{ note.title }}<br></span>
<span class="sub-line" v-if="note.subtext.length > 0">{{ removeHtml(note.subtext) }}</span>
<span v-if="note.title.length == 0 && note.title.length == 0">Empty Note</span>
<!-- slim card view -->
<div v-if="titleView" class="thin-container" @click="cardClicked">
<!-- icon -->
<span v-if="noteIcon" class="thin-icon">
<i :class="`${noteIcon} icon`" :style="{ 'color':iconColor }"></i>
</span>
<!-- title -->
<span class="thin-title" v-if="note.title.length > 0">{{ note.title }}</span>
<!-- snippet -->
<span class="thick-sub" v-if="note.subtext.length > 0 && note.title.length == 0">
{{ removeHtml(note.subtext) }}
</span>
<span class="thin-sub" v-else-if="note.subtext.length > 0">
{{ removeHtml(note.subtext) }}
</span>
<span v-else-if="note.title.length == 0 && removeHtml(note.subtext).length == 0">
Empty Note
</span>
<!-- tags -->
<span v-if="note.tags" class="thin-tags" >
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}
</span>
</span>
<!-- edited -->
<span class="thin-right">
{{$helpers.timeAgo( note.updated )}}
<i class="green link ellipsis vertical icon"></i>
</span>
</div>
<!-- Toolbar on the bottom -->
<div class="tool-bar" @click.self="cardClicked" v-if="!titleView">
<div class="icon-bar">
<span class="time-ago-display" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
<div v-if="getThumbs.length > 0">
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
<img v-for="thumb in getThumbs"
class="tiny-thumb"
:src="`/api/static/thumb_${thumb}`"
onerror="
this.onerror=null;
this.src='/api/static/assets/marketing/void.svg';
"
/>
</div>
</div>
<div class="icon-bar" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
<span class="time-ago-display">
{{$helpers.timeAgo( note.updated )}}
</span>
<span class="teeny-buttons" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
<span class="teeny-buttons">
<span v-if="!note.trashed">
@@ -115,19 +162,13 @@
</i>
<delete-button class="teeny-button" :note-id="note.id" />
</span>
</span>
</div>
<div v-if="getThumbs.length > 0">
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
<img v-for="thumb in getThumbs" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`">
</div>
</div>
</div>
<!-- tag edit menu -->
<side-slide-menu v-if="showTagSlideMenu" v-on:close="toggleTags(false)" :full-shadow="true" :skip-history="true">
<div class="ui basic segment">
<note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/>
@@ -186,10 +227,12 @@
},
pinNote(){ //togglePinned() <- old name
this.showWorking = true
let postData = {'pinned': !this.note.pinned, 'noteId':this.note.id}
this.note.pinned = this.note.pinned == 1 ? 0:1
let postData = {'pinned': this.note.pinned, 'noteId':this.note.id}
axios.post('/api/note/setpinned', postData)
.then(data => {
this.showWorking = false
// this event is triggered by the server after note is saved
// this.$bus.$emit('update_single_note', this.note.id)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Pin Note') })
@@ -205,11 +248,10 @@
//Show message so no one worries where note went
let message = 'Moved to Archive'
if(postData.archived != 1){
message = 'Moved to main list'
message = 'Moved out of Archive'
}
this.$bus.$emit('notification', message)
// this.$bus.$emit('update_single_note', this.note.id)
this.$bus.$emit('update_single_note', this.note.id)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Archive Note') })
},
@@ -224,9 +266,10 @@
//Show message so no one worries where note went
let message = 'Moved to Trash'
if(postData.trashed == 0){
message = 'Moved to main list'
message = 'Moved out of Trash'
}
this.$bus.$emit('notification', message)
this.$bus.$emit('update_single_note', this.note.id)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Trash Note') })
@@ -242,23 +285,28 @@
},
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'
// })
//After scroll, trigger green outline animation
setTimeout(() => {
// this.$bus.$emit('notification','Note Saved')
this.triggerClosedAnimation = true
setTimeout(()=>{
//After 3 seconds, hide it
this.triggerClosedAnimation = false
}, 3000)
// //After scroll, trigger green outline animation
// setTimeout(() => {
}, 500)
// this.triggerClosedAnimation = true
// setTimeout(()=>{
// //After 3 seconds, hide it
// this.triggerClosedAnimation = false
// }, 1500)
// }, 500)
},
},
@@ -333,13 +381,11 @@
.teeny-buttons {
float: right;
width: 65%;
text-align: right;
}
.time-ago-display {
width: 35%;
float: left;
text-align: center;
font-size: 11px;
font-weight: bold;
}
.tags {
width: 100%;
@@ -364,9 +410,7 @@
/*Strict font sizes for card display*/
.small-text {
max-height: 267px;
width: 100%;
overflow: hidden;
display: inline-block;
}
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
@@ -414,10 +458,10 @@
.note-title-display-card {
position: relative;
background-color: var(--small_element_bg_color);
/*The subtle shadow*/
/*box-shadow: 0px 1px 2px 1px rgba(210, 211, 211, 0.46);*/
box-shadow: 2px 2px 6px 0 rgba(0,0,0,.15);
transition: box-shadow ease 0.5s, transform linear 0.1s;
transition: box-shadow, border-color ease 0.5s, transform linear 0.5s;
margin: 5px;
/*padding: 0.7em 1em;*/
border-radius: .28571429rem;
@@ -426,7 +470,7 @@
/*width: calc(33.333% - 10px);*/
width: calc(25% - 10px);
/*min-width: 190px;*/
min-height: 130px;
/*min-height: 130px;*/
/*transition: box-shadow 0.3s;*/
box-sizing: border-box;
cursor: pointer;
@@ -435,32 +479,72 @@
letter-spacing: 0.05rem;
display: flex;
flex-direction: column;
align-items: stretch;
text-align: left;
min-height: 100px;
max-height: 450px;
}
.note-title-display-card:hover {
/*box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8);*/
/*transform: translateY(-2px);*/
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
box-shadow: 0 8px 15px rgba(0,0,0,0.3);
border-color: var(--main-accent);
}
.note-title-display-card.title-view {
width: 100%;
min-height: 20px;
max-width: none;
padding: 10px;
margin: 0;
/*overflow: hidden;*/
border-radius: 0;
border: none;
/*box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);*/
}
.title-view + .title-view {
border-top: 1px solid var(--border_color);
}
.single-line-text {
.thin-container.single-line-text {
width: calc(100% - 25px);
margin: 5px 10px;
/*margin: 5px 10px;*/
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
}
.title-line {
.thin-container .thin-title {
font-weight: bold;
font-size: 1.2em;
padding: 0 20px 0 0;
}
.thin-container .thin-sub {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
line-clamp: 1;
-webkit-box-orient: vertical;
opacity: 0.85;
}
.thin-container .thick-sub {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
opacity: 0.85;
}
.thin-container .thin-tags {
float: left;
margin-top: 3px;
}
.thin-container .thin-right {
float: right;
color: var(--dark_border_color);
}
.thin-container .thin-icon {
float: right;
}
.icon-bar {
@@ -468,6 +552,7 @@
padding: 5px 10px 0;
opacity: 1;
width: 100%;
background-color: rgba(200, 200, 200, 0.2);
}
.hover-hide {
opacity: 0.0;
@@ -647,4 +732,36 @@
animation: bgin 4s cubic-bezier(0.19, 1, 0.22, 1) 1;
}
/*switch between ring or BG boy to change save animation*/
.ring {
position: relative;
}
.ring::after {
content: '';
width: 10px;
height: 10px;
border-radius: 100%;
border: 6px solid #00FFCB;
position: absolute;
z-index: 800;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: ring 1.5s 1;
}
@keyframes ring {
0% {
width: 10px;
height: 10px;
opacity: 1;
}
100% {
width: 420px;
height: 420px;
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,116 @@
<template>
<div class="button-fix">
<div class="ui right floated basic shrinking icon button" v-on:click="showPasteInputArea">
<i class="green paste icon"></i>
Paste
</div>
<div class="shade" v-if="showPasteArea" @click.prevent="close">
<div class="ui stackable grid full-height" @click.prevent="close">
<div class="four wide column"></div>
<div class="eight wide middle aligned center aligned column">
<div class="ui raised segment">
<div class="ui dividing header">
<i class="green paste icon"></i>
Paste & automatically Save
</div>
<div class="ui fluid action input">
<input
id="pastetextarea"
type="text"
ref="pastearea"
@paste.prevent="onPaste"
@keyup.enter.prevent="onEnter"
placeholder="Paste Here">
<button class="ui green labeled icon button" @click.prevent="onEnter">
<i class="save icon"></i>
Save
</button>
</div>
</div>
</div>
<div class="four wide column"></div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'PasteButton',
props: {},
data () {
return {
showPasteArea: false,
}
},
methods: {
close(){
this.showPasteArea = false
},
onEnter(e){
const text = this.$refs.pastearea.value
this.saveText(text)
},
onPaste(e){
// Get pasted data via clipboard API
const clipboardData = e.clipboardData || window.clipboardData
const pastedData = String(clipboardData.getData('Text')).trim()
this.saveText(pastedData)
},
saveText(text){
this.showPasteArea = false
if(!text){
this.$bus.$emit('notification', 'Nothing to save.')
return;
}
axios.post('/api/quick-note/update', { 'pushText':text } )
.then( response => {
this.$bus.$emit('notification', 'Saved To Scratch Pad')
})
.catch(error => {
this.$bus.$emit('notification', 'Failed to Save')
})
},
showPasteInputArea(){
// Show text area and focus its contents
this.showPasteArea = true
this.$nextTick(() => {
const aux = document.getElementById('pastetextarea')
aux.focus();
})
// auto hide after 1 Minute
setTimeout(() => {
this.showPasteArea = false
}, 60*1000)
},
}
}
</script>
<style scoped lang="css">
.paste-text-container {
background-color: green;
position: absolute;
width: 50vw;
height: 80vh;
display: inline-block;
}
.full-height {
height: 100vh;
}
</style>

View File

@@ -35,7 +35,6 @@
<i class="search icon"></i>
</div>
<div class="floating-button" v-if="searchTerm.length > 0">
<i class="big link grey close icon" v-on:click="clear()"></i>
</div>

View File

@@ -1,9 +1,9 @@
<style type="text/css" scoped>
.slide-container {
position: fixed;
position: absolute;
top: 0;
left: 0;
right: 50%;
right: 0;
bottom: 0;
z-index: 1020;
overflow: hidden;
@@ -98,11 +98,9 @@
<slot></slot>
</div>
</div>
<div class="slide-shadow" :class="{'full-shadow':fullShadow}" v-on:click="close"></div>
<!-- <div class="slide-shadow" :class="{'full-shadow':fullShadow}" v-on:click="close"></div> -->
</div>
<!-- </transition> -->

File diff suppressed because one or more lines are too long

View File

@@ -113,6 +113,7 @@
<style type="text/css">
.button-fix {
display: inline-block;
float: left;
}
.hover-row:hover {
cursor: pointer;

View File

@@ -2,22 +2,43 @@
.colors {
position: fixed;
z-index: 1023;
top: 5px;
top: 35px;
/*height: 100px;*/
width: 400px;
left: 20%;
}
.colors-container {
max-width: 370px;
/*max-width: 360px;*/
display: flex;
/*flex-direction: column;*/
flex-wrap: wrap;
justify-content: center;
align-items: stretch;
align-content: stretch;
height: 250px;
width: 100%;
}
.dot {
display: inline-block;
width: 30px;
height: 30px;
/*display: inline-block;*/
border-radius: 30px;
box-shadow: 0px 1px 3px 0px #3e3e3e;
margin: 7px 7px 0 0;
box-shadow: 0px 0px 0px 1px inset #3e3e3e;
margin: 0 0 2px 2px;
cursor: pointer;
flex-basis: 9%;
height: 30px;
text-align: center;
}
.dot > i {
margin: 9px 0 0 0;
color: white;
text-shadow:
1px 1px 2px #3e3e3e,
1px -1px 2px #3e3e3e,
-1px 1px 2px #3e3e3e,
-1px -1px 2px #3e3e3e
;
}
.shade {
position: fixed;
@@ -30,12 +51,16 @@
width: 100vw;
height: 100vh;
}
.big-shadow {
box-shadow: 0px 4px 5px 1px #a8a8a8;
}
@media only screen and (max-width: 740px) {
.colors {
position: fixed;
left: 0;
right: 0;
top: 0;
left: 5px;
right: -5px;
top: 5px;
width: 95%;
}
}
</style>
@@ -43,13 +68,15 @@
<template>
<div>
<div class="colors">
<div class="ui raised segment">
<div class="ui segment big-shadow">
<h3>Select Text Color</h3>
<div class="colors-container">
<span
v-for="(color,index) in colors"
class="dot"
v-on:click="onColorClick(index)"
:style="`background-color: ${color};`">
<i v-if="lastUsedColor == color" class="check icon"></i>
</span>
</div>
</div>
@@ -65,6 +92,7 @@
components:{
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
},
props: [ 'lastUsedColor' ],
data: function(){
return {
hover: false,

View File

@@ -13,19 +13,19 @@ import router from './router'
// import 'fomantic-ui-css/semantic.css';
//Required site and reset CSS
import 'fomantic-ui-css/components/reset.css'
import 'fomantic-ui-css/components/reset.min.css'
import 'fomantic-ui-css/components/site.css' //modified to remove included LATO fonts
//Only include parts that are used
import 'fomantic-ui-css/components/button.css'
import 'fomantic-ui-css/components/container.css'
import 'fomantic-ui-css/components/form.css'
import 'fomantic-ui-css/components/grid.css'
import 'fomantic-ui-css/components/header.css'
import 'fomantic-ui-css/components/button.min.css'
import 'fomantic-ui-css/components/container.min.css'
import 'fomantic-ui-css/components/form.min.css'
import 'fomantic-ui-css/components/grid.min.css'
import 'fomantic-ui-css/components/header.min.css'
import 'fomantic-ui-css/components/icon.css' //Modified to remove brand icons
import 'fomantic-ui-css/components/input.css'
import 'fomantic-ui-css/components/segment.css'
import 'fomantic-ui-css/components/label.css'
import 'fomantic-ui-css/components/input.min.css'
import 'fomantic-ui-css/components/segment.min.css'
import 'fomantic-ui-css/components/label.min.css'
//Overwrite and site styles and themes and good stuff

View File

@@ -9,6 +9,8 @@ const SquireButtonFunctions = {
activeList: false,
activeToDo: false,
activeColor: null,
activeCode: false,
activeSubTitle: false,
//
lastUsedColor: null,
}
@@ -28,6 +30,8 @@ const SquireButtonFunctions = {
this.activeToDo = false
this.activeColor = null
this.activeUnderline = false
this.activeCode = false
this.activeSubTitle = false
if(e.path.indexOf('>U>') > -1 || e.path.search(/U$/) > -1){
this.activeUnderline = true
@@ -38,15 +42,21 @@ const SquireButtonFunctions = {
if(e.path.indexOf('>I') > -1){
this.activeItalics = true
}
if(e.path.indexOf('fontSize') > -1){
if(e.path.indexOf('fontSize=1.4em') > -1){
this.activeTitle = true
}
if(e.path.indexOf('fontSize=0.9em') > -1){
this.activeSubTitle = true
}
if(e.path.indexOf('OL>LI') > -1){
this.activeList = true
}
if(e.path.indexOf('UL>LI') > -1){
this.activeToDo = true
}
if(e.path.indexOf('CODE') > -1){
this.activeCode= true
}
const colorIndex = e.path.indexOf('color=')
if(colorIndex > -1){
//Get all digigs after color index, then limit to 3
@@ -143,6 +153,12 @@ const SquireButtonFunctions = {
this.editor.italic()
}
},
modifyCode(){
this.selectLineIfNoSelect()
this.editor.toggleCode()
},
undoCustom(){
//The same as pressing CTRL + Z
// this.editor.focus()
@@ -156,15 +172,16 @@ 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');
})
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},600)
},
deleteCompletedListItems(){
//
@@ -174,6 +191,11 @@ 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)
setTimeout(()=>{
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
@@ -217,10 +239,9 @@ const SquireButtonFunctions = {
}
})
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
}, 600)
},
sortList(){
//
@@ -230,6 +251,11 @@ const SquireButtonFunctions = {
//Fetch the container
let container = document.getElementById('squire-id')
//Close menu if user is on mobile
this.$router.go(-1)
setTimeout(()=>{
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
@@ -281,10 +307,9 @@ const SquireButtonFunctions = {
}
})
//Close menu if user is on mobile
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},600)
},
calculateMath(){
//
@@ -294,6 +319,9 @@ 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
@@ -306,6 +334,8 @@ const SquireButtonFunctions = {
}
}
setTimeout(()=>{
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
@@ -333,17 +363,28 @@ const SquireButtonFunctions = {
}
})
},600)
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},
setText(inText){
this.editor.setHTML(inText)
// this.noteText = this.editor._getHTML()
// this.diffNoteText = this.editor._getHTML()
//Make sure all list items have draggable property
let container = document.getElementById('squire-id')
let listItems = container.getElementsByTagName('li')
for(let itemIndex in listItems){
// console.log(listItems[itemIndex])
// listItems[itemIndex].setAttribute('draggable','true')
}
// console.log(listItems)
},
getText(){
@@ -376,6 +417,26 @@ const SquireButtonFunctions = {
this.$router.go(-1)
},
indentText(){
// Lists use increase list level, increase quote breaks numbering
if(this.activeList || this.activeToDo){
this.editor.increaseListLevel()
return
}
this.editor.increaseQuoteLevel()
},
outdentText(){
// Lists use increase list level, increase quote breaks numbering
if(this.activeList || this.activeToDo){
this.editor.decreaseListLevel()
return
}
this.editor.decreaseQuoteLevel()
},
},
}

View File

@@ -8,6 +8,13 @@
<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>
@@ -36,6 +43,32 @@
Other Files
</router-link>
<router-link
v-if="$store.getters.totals && $store.getters.totals['archivedNotes']"
exact-active-class="green"
class="ui basic button shrinking"
to="/attachments/type/archived">
<i class="archive icon"></i>
Archived
</router-link>
<router-link
v-if="$store.getters.totals && $store.getters.totals['trashedNotes']"
exact-active-class="green"
class="ui basic button shrinking"
to="/attachments/type/trashed">
<i class="trash icon"></i>
Trashed
</router-link>
<router-link
v-if="$store.getters.totals && $store.getters.totals['sharedToNotes']"
exact-active-class="green"
class="ui basic button shrinking"
to="/attachments/type/shared">
<i class="send icon"></i>
Show Shared
</router-link>
</div>
<div class="sixteen wide column" v-if="searchParams.noteId">
@@ -99,6 +132,11 @@
//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()
},
@@ -106,6 +144,8 @@
//Remove scroll event on destroy
window.removeEventListener('scroll', this.onScroll)
this.$io.removeListener('update_note_attachments')
},
watch:{
$route (to, from){
@@ -165,6 +205,12 @@
this.searchParams.attachmentType = this.$route.params.type
}
// include files from shared notes or selected notes
this.searchParams.includeShared = false
if(this.$route.params.type == 'shared'){
this.searchParams.includeShared = true
}
//Set noteId in if in URL
if(this.$route.params.id){
this.searchParams.noteId = this.$route.params.id

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -12,7 +12,12 @@
animation: fadeorama 16s ease infinite;
height: 350px;
text-shadow: 1px 1px 2px black;
text-shadow:
1px 1px 1px rgba(69,69,69,0.1),
-1px -1px 1px rgba(69,69,69,0.1),
-1px 1px 1px rgba(69,69,69,0.1),
1px -1px 1px rgba(69,69,69,0.1)
;
}
.shine {
position: absolute;
@@ -149,23 +154,23 @@
<!-- All marketing images if you need to review -->
<div v-if="false" class="sixteen wide column">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/add.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/gardening.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/growth.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/icecream.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/investing.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/onboarding.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/robot.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/solution.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/watching.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/cloud.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/grandma.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/hamburger.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/idea.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/notebook.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/plan.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/secure.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/void.svg" alt="">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/add.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/gardening.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/growth.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/icecream.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/investing.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/onboarding.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/robot.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/solution.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/watching.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/cloud.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/grandma.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/hamburger.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/idea.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/notebook.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/plan.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/secure.svg">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/void.svg">
</div>
<!-- Go to notes button -->
@@ -191,11 +196,24 @@
</div>
</div>
<!-- Overview -->
<div class="middle aligned centered row">
<div class="six wide column">
<h2 class="ui dividing header">Powerful text editing and privacy</h2>
<h3>Easily edit, share and organize thousands of notes.</h3>
<h3>Feel safe knowing no one can read your notes but you.</h3>
<!-- <h3>Tools to organize and collaborate on thousands of notes while maintaining security and respecting your privacy.</h3> -->
</div>
<div class="four wide column">
<svg-displayer file="idea" alt="Explosion of New Ideas" />
</div>
</div>
<!-- theme selector -->
<div class="ui white row">
<div class="sixteen wide middle aligned column">
<div class="ui container">
<h2>
<h2 style="color: var(--main-accent);">
Pick your theme
</h2>
<h3 v-if="$parent.loggedIn">Go to settings to change theme</h3>
@@ -211,19 +229,6 @@
</div>
</div>
<!-- Overview -->
<div class="middle aligned centered row">
<div class="six wide column">
<h2 class="ui dividing header">Powerful text editing and privacy</h2>
<h3>Easily edit, share and organize thousands of notes.</h3>
<h3>Feel safe knowing no one can read your notes but you.</h3>
<!-- <h3>Tools to organize and collaborate on thousands of notes while maintaining security and respecting your privacy.</h3> -->
</div>
<div class="four wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/idea.svg" alt="Explosion of New Ideas">
</div>
</div>
<!-- features list -->
<div class="top aligned centered row">
@@ -355,7 +360,7 @@
<i class="grey lock icon"></i>
<i class="bottom left corner yellow key icon"></i>
</i>
All Note Text is Encrypted
Secure Notes
<div class="sub header">All note text is encrypted. No one can read your notes. None of your data is shared.</div>
</div>
</h2>
@@ -365,7 +370,7 @@
<i class="grey search icon"></i>
<i class="bottom left corner orange font icon"></i>
</i>
Note Search is Encrypted
Private Search
<div class="sub header">Search the contents of all your notes without compromising security.</div>
</div>
</h2>
@@ -375,7 +380,7 @@
<i class="grey share alternate icon"></i>
<i class="bottom left corner share icon"></i>
</i>
Encrypted Note Sharing
Encrypted Sharing
<div class="sub header">Shared notes are still encrypted, only readable by you and the shared users.</div>
</div>
</h2>
@@ -392,7 +397,7 @@
</div>
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="">
<svg-displayer file="onboarding" alt="Observe this chart" />
</div>
</div>
@@ -400,8 +405,7 @@
<div class="middle aligned centered row">
<div class="four wide right aligned column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
<svg-displayer file="secure" alt="So dang secure" />
</div>
<div class="six wide column">
<h2>Only you can read your notes. </h2>
@@ -415,13 +419,13 @@
<h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3>
</div>
<div class="four wide right aligned column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos">
<svg-displayer file="cloud" alt="Girl falling into the spiral of digital chaos" />
</div>
</div>
<div class="middle aligned centered row">
<div class="four wide right aligned column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/robot.svg" alt="Shrunken man near giant tablet">
<svg-displayer file="robot" alt="Murder Robot in office environment" />
</div>
<div class="six wide column">
<h2>Secure Data Sharing</h2>
@@ -440,7 +444,7 @@
<a href="https://pi-hole.net/" target="_blank">Pi-hole</a> on the network.</h3>
</div>
<div class="four wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/icecream.svg" alt="Emergence of a 4th dimensional being perceived as a large ice cream ">
<svg-displayer file="icecream" alt="Emergence of a 4th dimensional being perceived as a large ice cream" />
</div>
</div>
@@ -493,6 +497,8 @@
<a target="_blank" href="https://www.maxg.cc">Solid Scribe was created by Max Gialanella</a>
</h3>
<p><a target="_blank" href="https://www.maxg.cc">Check out my Resume</a></p>
<p>OR</p>
<p><a target="_blank" href="http://blog.maxg.cc">Check out my Programming Blog</a></p>
<p>
I was tired of all my data being owned by big companies, having it farmed out for marketing, and leaving the contents of my life exposed to corporations.
</p>
@@ -512,7 +518,7 @@
<p>Awesomely Generic Marketing Images - <a target="_blank" href="https://undraw.co/">https://unDraw.co/</a></p>
</div>
<div class="four wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/watching.svg" alt="Drinking the blood of the elderly">
<svg-displayer file="watching" alt="Drinking the blood of the elderly" />
</div>
</div>
@@ -531,6 +537,7 @@ export default {
components: {
'login-form':require('@/components/LoginFormComponent.vue').default,
'logo':require('@/components/LogoComponent.vue').default,
'svg-displayer':require('@/components/SvgDisplayer.vue').default,
},
data(){
return {
@@ -543,7 +550,7 @@ export default {
'#00b5ad', //Teal
'#2185d0', //Blue
'#7128b9', //Violet
'#a333c8', // "Purple"
'#a333c8', //Purple
'#e03997', //Pink
'#db2828', //Red
'#f2711c', //Orange

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
<template>
<div class="page-container" v-on:scroll="onScroll">
<div class="page-container">
<div class="ui grid" ref="content">
<div class="sixteen wide column">
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
<!-- :class="{ 'sixteen wide column':showOneColumn 'sixteen wide column':!showOneColumn}" -->
<div class="ui stackable grid">
@@ -12,6 +12,12 @@
<search-input />
</div>
<div class="sixteen wide column" v-if="$store.getters.totals && $store.getters.totals['showTrackMetricsButton']">
<router-link class="ui fluid green button" to="/metrictrack">
<i class="calendar check outlin icon"></i>Metric Track
</router-link>
</div>
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
<div class="ui basic button shrinking"
@@ -23,14 +29,13 @@
</div>
<tag-display
v-if="$store.getters.totals && Object.keys($store.getters.totals['tags'] || {}).length"
:user-tags="$store.getters.totals['tags']"
:active-tags="searchTags"
v-on:tagClick="tagId => toggleTagFilter(tagId)"
/>
<div class="ui basic shrinking icon button" v-on:click="toggleTitleView()" v-if="$store.getters.totals && $store.getters.totals['totalNotes'] > 0">
<i v-if="titleView" class="th icon"></i>
<i v-if="!titleView" class="bars icon"></i>
</div>
<paste-button />
</div>
@@ -45,7 +50,7 @@
</div>
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !loadingInProgress">
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !showLoading">
<h2 class="ui header">
<div class="content">
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}"
@@ -57,11 +62,15 @@
</div>
<div v-if="fastFilters['onlyArchived'] == 1" class="sixteen wide column">
<h2>Archived Notes</h2>
<h2>
<i class="green archive icon"></i>
Archived Notes</h2>
</div>
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
<h2>Trash
<h2>
<i class="green trash alternate outline icon"></i>
Trashed Notes
<span>({{ $store.getters.totals['trashedNotes'] }})</span>
<div class="ui right floated basic button" data-tooltip="This doesn't work yet">
<i class="poo storm icon"></i>
@@ -71,7 +80,8 @@
</div>
<div class="sixteen wide column" v-if="fastFilters['onlyShowSharedNotes'] == 1">
<h2>Shared Notes</h2>
<h2><i class="green paper plane outline icon"></i>
Shared Notes</h2>
</div>
<div class="sixteen wide column" v-if="tagSuggestions.length > 0">
@@ -82,6 +92,57 @@
</div>
</div>
<!-- Note title card display -->
<div class="sixteen wide column">
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0 && fastFilters['notesHome'] == 1">
No Notes Yet. <br>Thats ok.<br><br> <br>
<img loading="lazy" width="25%" src="/api/static/assets/marketing/hamburger.svg" alt="Create a new note"><br>
Create one when you feel ready.
</h3>
<!-- Go to one wide column, do not do this on mobile interface -->
<div :class="{'one-column':( showOneColumn), 'floating-list':( isFloatingList ), 'hidden-floating-list':(collapseFloatingList)}" v-on:scroll="onScroll">
<div class="ui basic fitted right aligned segment" v-if="isFloatingList">
<div class="ui small basic green left floated button" v-on:click="closeAllNotes()" v-if="openNotes.length >= 1">
<i class="close icon"></i>
Close Notes
</div>
<div class="ui small green button" v-on:click="collapseFloatingList = true">
<i class="caret square left outline icon"></i>
Hide List
</div>
</div>
<!-- render each section based on notes in set -->
<div v-for="section,index in noteSections" v-if="section.length > 0" class="note-card-section">
<h5 class="ui tiny dividing header"><i :class="`green ${sectionData[index][0]} icon`"></i>{{ sectionData[index][1] }}</h5>
<div class="note-card-display-area">
<note-title-display-card
v-on:tagClick="tagId => toggleTagFilter(tagId)"
v-for="note in section"
:ref="'note-'+note.id"
:onClick="openNote"
:data="note"
:title-view="titleView || isFloatingList"
:currently-open="openNotes.includes(note.id)"
:key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated + note.archived + note.pinned + note.trashed"
/>
</div>
</div>
<div class="loading-section" v-if="showLoading">
<loading-icon message="Decrypting Notes" />
</div>
</div>
</div>
<!-- found attachments -->
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
<h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
@@ -93,51 +154,24 @@
/>
</div>
<!-- Note title card display -->
<div class="sixteen wide column">
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0 && fastFilters['notesHome'] == 1">
No Notes Yet. <br>Thats ok.<br><br> <br>
<img loading="lazy" width="25%" src="/api/static/assets/marketing/hamburger.svg" alt="Create a new note"><br>
Create one when you feel ready.
</h3>
<!-- Go to one wide column, do not do this on mobile interface -->
<div :class="{'one-column':( showOneColumn() )}">
<!-- render each section based on notes in set -->
<div v-for="section,index in noteSections" v-if="section.length > 0" class="note-card-section">
<h5 class="ui tiny dividing header"><i :class="`green ${sectionData[index][0]} icon`"></i>{{ sectionData[index][1] }}</h5>
<div class="note-card-display-area">
<note-title-display-card
v-on:tagClick="tagId => toggleTagFilter(tagId)"
v-for="note in section"
:ref="'note-'+note.id"
:onClick="openNote"
:data="note"
:title-view="titleView"
:currently-open="activeNoteId1 == note.id"
:key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
/>
</div>
</div>
<loading-icon v-if="loadingInProgress" message="Decrypting Notes" />
</div>
<div class="show-hidden-note-list-button"
v-if="collapseFloatingList && openNotes.length > 0" v-on:click="collapseFloatingList = false">
<i class="caret square right outline icon"></i>
</div>
</div>
<!-- flexbox note container evenly spaces open notes -->
<div class="note-panel-container" :class="{ 'note-panel-fullwidth':collapseFloatingList}" v-if="openNotes.length">
<note-input-panel
v-if="activeNoteId1 != null"
:key="activeNoteId1"
:noteid="activeNoteId1"
v-for="noteId in openNotes"
v-if="noteId != null"
:key="noteId"
:noteid="noteId"
:url-data="$route.params"
:open-notes="openNotes.length"
/>
</div>
</div>
</template>
@@ -147,7 +181,7 @@
import axios from 'axios'
export default {
name: 'SearchBar',
name: 'NotesPage',
components: {
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
@@ -158,6 +192,7 @@
'attachment-display': require('@/components/AttachmentDisplayCard').default,
'tag-display':require('@/components/TagDisplayComponent.vue').default,
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
'paste-button':require('@/components/PasteButton.vue').default,
},
data () {
return {
@@ -167,6 +202,8 @@
searchResultsCount: 0,
searchTags: [],
notes: [],
openNotes: [],
collapseFloatingList: false,
highlights: [],
searchDebounce: null,
fastFilters: {},
@@ -174,10 +211,10 @@
//Load up notes in batches
firstLoadBatchSize: 10, //First set of rapidly loaded notes
batchSize: 25, //Size of batch loaded when user scrolls through current batch
batchSize: 20, //Size of batch loaded when user scrolls through current batch
batchOffset: 0, //Tracks the current batch that has been loaded
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
loadingInProgress: false,
showLoading: false,
scrollLoadEnabled: true,
//Clear button is not visible
@@ -223,37 +260,39 @@
this.$parent.loginGateway()
//If user is on title view,
this.titleView = this.$store.getters.getIsUserOnMobile
this.$io.on('new_note_created', noteId => {
//Do not update note if its open
if(this.activeNoteId1 != noteId){
// Push new note to top of list and animate
this.updateSingleNote(noteId)
this.$store.dispatch('fetchAndUpdateUserTotals')
this.updateSingleNote(noteId, false)
}
})
this.$io.on('note_attribute_modified', noteId => {
const drawFocus = !this.openNotes.includes(parseInt(noteId))
this.updateSingleNote(noteId, drawFocus)
//Do not update note if its open
if(this.activeNoteId1 != noteId){
if(this.openNotes.includes(parseInt(noteId))){
this.$store.dispatch('fetchAndUpdateUserTotals')
this.updateSingleNote(noteId, false)
}
})
//Update title cards when new note text is saved
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
//Do not update note if its open
if(this.activeNoteId1 != noteId){
this.updateSingleNote(noteId, true)
}
const drawFocus = !this.openNotes.includes(parseInt(noteId))
this.updateSingleNote(noteId, drawFocus)
})
this.$bus.$on('update_single_note', (noteId) => {
//Do not update note if its open
if(this.activeNoteId1 != noteId){
this.updateSingleNote(noteId)
}
const drawFocus = !this.openNotes.includes(parseInt(noteId))
this.updateSingleNote(noteId, drawFocus)
})
//Update totals for app
@@ -261,19 +300,7 @@
//Close note event
this.$bus.$on('close_active_note', ({noteId, modified}) => {
if(modified){
console.log('Just closed Note -> ' + noteId + ', modified -> ', modified)
}
//A note has been closed
if(this.$route.fullPath != '/notes'){
this.$router.push('/notes')
}
this.$store.dispatch('fetchAndUpdateUserTotals')
//Focus and animate if modified
this.updateSingleNote(noteId, modified)
this.closeNote(noteId, modified)
})
this.$bus.$on('note_deleted', (noteId) => {
@@ -320,11 +347,13 @@
//Reload page content - don't trigger if load is in progress
this.$bus.$on('note_reload', () => {
if(!this.loadingInProgress){
if(!this.showLoading){
this.reset()
}
})
// Window scroll needed when scrolling full page.
// second scroll event added on note-list for floating view scroll detection
window.addEventListener('scroll', this.onScroll)
//Close notes when back button is pressed
@@ -351,9 +380,9 @@
},
mounted() {
//Open note on load if ID is set
//Open note on PAGE LOAD if ID is set
if(this.$route.params.id > 1){
this.activeNoteId1 = this.$route.params.id
this.openNote(this.$route.params.id)
}
//Loads initial batch and tags
@@ -362,37 +391,123 @@
},
watch: {
'$route.params.id': function(id){
//Open note on ID, null id will close note
this.activeNoteId1 = id
this.openNote(id)
},
'$route' (to, from) {
// Reload the notes if returning to this page
if(to.fullPath == '/notes' && !from.fullPath.includes('/notes/open/')){
this.reset()
}
// Close all notes if returning to /notes page
if(to.fullPath == '/notes' && from.fullPath.includes('/notes/open/')){
this.closeAllNotes()
}
//Lookup tags set in URL
if(to.params.tag && this.$store.getters.totals && this.$store.getters.totals['tags'][to.params.tag]){
//Lookup tag in store by string
const tagObject = this.$store.getters.totals['tags'][to.params.tag]
//Pull key out of string and load tags for that key
this.toggleTagFilter(tagObject.id)
return
}
}
},
methods: {
onScroll(e){
console.log('Scroll')
},
toggleTitleView(){
this.titleView = !this.titleView
computed: {
isFloatingList(){
//If note 1 or 2 is open, show floating column
return (this.openNotes.length > 0)
},
showOneColumn(){
return this.$store.getters.getIsUserOnMobile
//If note 1 or 2 is open, show one column. Or if the user is on mobile
return (this.activeNoteId1 != null || this.activeNoteId2 != null) &&
!this.$store.getters.getIsUserOnMobile
}
},
methods: {
openNote(id, event = null){
//
const intId = parseInt(id)
if(this.openNotes.includes(intId)){
console.log('Open already open note?')
// const openIndex = this.openNotes.indexOf(intId)
// if(openIndex != -1){
// console.log('Open note and remove it ', intId + ' on index ' + openIndex)
// this.openNotes.splice(openIndex, 1)
// }
// this.$bus.$emit('close_note_by_id', intId)
return
}
//Don't open note if a link is clicked in display card
if(event && event.target && event.target.nodeName){
const nodeClick = event.target.nodeName
if(nodeClick == 'A'){ return }
}
//Open note if a link was not clicked
// Push note to stack if not open
if(Number.isInteger(intId) && !this.openNotes.includes(intId)){
this.openNotes.push(intId)
}
this.$nextTick(() => {
// change route if open ID is not the same as current ID
if(this.$route.params.id != id){
console.log('Open note, change route -> route id ' + this.$route.params.id + ' note id ->' + id + ', ' +(this.$route.params.id == id))
this.$router.push('/notes/open/'+id)
}
})
return
},
closeNote(noteId, modified){
console.log('close note', this.$route.fullPath)
const openIndex = this.openNotes.indexOf(noteId)
if(openIndex != -1){
console.log('Removing note id ', noteId + ' on index ' + openIndex)
this.openNotes.splice(openIndex, 1)
}
// //A note has been closed
// if(this.$route.fullPath != '/notes'){
// this.$router.push('/notes')
// }
if(this.openNotes.length == 0 && this.$route.fullPath != '/notes'){
this.$router.push('/notes')
}
if(modified){
console.log('Just closed Note -> ' + noteId + ', modified -> ', modified)
this.$store.dispatch('fetchAndUpdateUserTotals')
//Focus and animate if modified
this.updateSingleNote(noteId, modified)
}
console.log('closeNote(): Open notes length ', this.openNotes.length)
},
closeAllNotes(){
console.log('Close all notes ------------')
for (let i = this.openNotes.length - 1; i >= 0; i--) {
console.log('Close all notes -> ' + this.openNotes[i])
this.closeNote(this.openNotes[i])
}
console.log('----------------')
},
toggleTagFilter(tagId){
this.searchTags = [tagId]
@@ -409,6 +524,10 @@
},
onScroll(e){
if(!this.scrollLoadEnabled){
return
}
clearTimeout(this.loadingBatchTimeout)
this.loadingBatchTimeout = setTimeout(() => {
@@ -418,12 +537,12 @@
const height = document.getElementById('app').scrollHeight
//Load if less than 500px from the bottom
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled && !this.loadingInProgress){
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled){
this.search(false, this.batchSize, true)
this.search(true, this.batchSize, true)
}
}, 30)
}, 50)
return
@@ -444,21 +563,24 @@
}
this.lastVisibilityState = document.visibilityState
},
// @TODO Don't even trigger this if the note wasn't changed
updateSingleNote(noteId, focuseAndAnimate = true){
// console.log('updating single note', noteId)
noteId = parseInt(noteId)
//Find local note, if it exists; continue
let note = null
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
if(this.$refs['note-'+noteId]?.[0]?.note){
note = this.$refs['note-'+noteId][0].note
//Show that note is working on updating
this.$refs['note-'+noteId][0].showWorking = true
}
this.rebuildNoteCategorise()
// return
//Lookup one note using passed in ID
const postData = {
@@ -480,6 +602,7 @@
return
}
// if old note data and new note data exists
if(note && newNote){
//go through each prop and update it with new values
@@ -488,7 +611,7 @@
})
//Push new note to front if its modified or we want it to
if( focuseAndAnimate || note.updated != newNote.updated ){
if( note.updated != newNote.updated ){
// Find note, in section, move to front
Object.keys(this.noteSections).forEach( key => {
@@ -502,6 +625,9 @@
})
})
}
if( focuseAndAnimate ){
this.$nextTick( () => {
//Trigger close animation on note
this.$refs['note-'+noteId][0].justClosed()
@@ -544,19 +670,14 @@
return new Promise((resolve, reject) => {
//Don't double load note batches
if(this.loadingInProgress){
if(this.showLoading){
console.log('Loading already in progress')
return resolve(false)
}
//Reset a lot of stuff if we are not merging batches
if(!mergeExisting){
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key] = []
})
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
this.batchOffset = 0 // Reset batch offset if we are not merging note batches or new set will be offset from current and overwrite current set with second batch
}
this.searchResultsCount = 0
//Remove all filter limits from previous queries
delete this.fastFilters.limitSize
@@ -584,25 +705,40 @@
}
//Perform search - or die
this.loadingInProgress = true
this.showLoading = showLoading
this.scrollLoadEnabled = false
axios.post('/api/note/search', postData)
.then(response => {
//Reset a lot of stuff if we are not merging batches
if(!mergeExisting){
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key] = []
})
}
this.searchResultsCount = 0
// console.timeEnd('Fetch TitleCard Batch '+notesInNextLoad)
//Save the number of notes just loaded
this.batchOffset += response.data.notes.length
//Enable or disable scroll loading
//Enable scroll loading if endpoint retured notes
this.scrollLoadEnabled = response.data.notes.length > 0
if(response.data.total > 0){
this.searchResultsCount = response.data.total
}
this.loadingInProgress = false
this.showLoading = false
this.generateNoteCategories(response.data.notes, mergeExisting)
//cache initial notes for faster reloads
if(!mergeExisting && this.showClear == false){
const cachedNotesJson = JSON.stringify(response.data.notes)
localStorage.setItem('snippetCache', cachedNotesJson)
}
return resolve(true)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
@@ -717,7 +853,7 @@
//clear out tags
this.searchTags = []
this.tagSuggestions = []
this.loadingInProgress = false
this.showLoading = false
this.searchTerm = ''
this.$bus.$emit('reset_fast_filters') //Clear out search
@@ -734,15 +870,32 @@
filter[options[index]] = 1
this.fastFilters = filter
//If notes exist in cache, load them up
let showLoading = true
const cachedNotesJson = localStorage.getItem('snippetCache')
const cachedNotes = JSON.parse(cachedNotesJson)
if(cachedNotes && cachedNotes.length > 0 && !this.showClear){
//Load cache. do not merge existing
this.generateNoteCategories(cachedNotes, false)
showLoading = false
}
//Fetch First batch of notes with new filter
this.search(true, this.firstLoadBatchSize, false)
.then( r => this.search(false, this.batchSize, true))
this.search(showLoading, this.batchSize, false)
// .then( r => this.search(false, this.batchSize, true))
}
}
}
</script>
<style type="text/css" scoped>
.text-fix {
padding: 8px 0 0 15px;
display: inline-block;
color: var(--menu-accent);
}
.detail {
float: right;
}
@@ -760,4 +913,150 @@
.note-card-section + .note-card-section {
padding: 15px 0 0;
}
.loading-section {
color: var(--main-accent);
box-shadow: 0 1px 3px 0 var(--main-accent);
border-radius: 6px;
background-color: var(--small_element_bg_color);
display: inline-block;
width: 100%;
margin: 15px 0;
}
.floating-list {
z-index: 1000;
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 25%;
height: 100vh;
background-color: var(--small_element_bg_color);
padding: 15px 5px 0px 10px;
overflow-y: scroll;
overflow-x: hidden;
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
background-color: var(--border_color);
}
.floating-list::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
.note-panel-container {
position: fixed;
width: 75%;
height: 100vh;
background: gray;
top: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
align-items: stretch;
align-content: stretch;
z-index: 1000;
}
.note-panel-fullwidth {
width: 100% !important;
}
.note-panel-container > div {
flex: 1;
position: relative;
}
.hidden-floating-list {
left: -1000px !important;
}
.show-hidden-note-list-button {
position: fixed;
top: 25px;
left: 0;
min-width: 45px;
background-color: var(--main-accent);
color: var(--text_color);
display: block;
z-index: 1100;
cursor: pointer;
border-bottom-right-radius: 5px;
border-top-right-radius: 5px;
padding: 8px 0px 8px 13px;
text-align: left;
font-size: 1.4em;
}
@media (min-width:320px) { /* smartphones, iPhone, portrait 480x320 phones */
.floating-list {
left: -1000px;
}
.note-panel-container {
width: 100%;
}
}
@media (min-width:481px) { /* portrait e-readers (Nook/Kindle), smaller tablets @ 600 or @ 640 wide. */
.floating-list {
left: 0px;
}
.note-panel-container {
width: 75%;
}
}
@media (min-width:641px) { /* portrait tablets, portrait iPad, landscape e-readers, landscape 800x480 or 854x480 phones */
}
@media (min-width:961px) { /* tablet, landscape iPad, lo-res laptops ands desktops */
}
@media (min-width:1025px) { /* big landscape tablets, laptops, and desktops */
}
@media (min-width:1281px) { /* hi-res laptops and desktops */
}
@media (min-width:2000px) { /* BIG hi-res laptops and desktops */
.floating-list {
left: 180px;
width: calc(30% - 180px);
}
.note-panel-container {
width: 70%;
}
}
.master-note-edit {
position: absolute;
width: 100%;
background: var(--small_element_bg_color);
left: 0;
top: 0;
bottom: 0;
overflow: hidden;
}
.master-note-edit + .master-note-edit {
border-left: 2px solid var(--main-accent);
border-left: 5px solid var(--border_color);
}
/*html, body {
height: 100%;
}
.wrap {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}*/
</style>

View File

@@ -0,0 +1,761 @@
<template>
<div class="page-container">
<div class="ui grid" ref="content">
<div class="sixteen wide column">
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
<div class="ui stackable grid">
<div class="six wide column" v-if="$store.getters.totals && $store.getters.totals['totalNotes']">
<search-input />
</div>
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
<div class="ui basic button shrinking"
v-on:click="updateFastFilters(3)"
v-if="$store.getters.totals && ($store.getters.totals['youGotMailCount'] > 0)"
style="position: relative;">
<i class="green mail icon"></i>Inbox
<span class="tiny circular floating ui green label">+{{ $store.getters.totals['youGotMailCount'] }}</span>
</div>
<tag-display
:active-tags="searchTags"
v-on:tagClick="tagId => toggleTagFilter(tagId)"
/>
<div class="ui basic shrinking icon button" v-on:click="toggleTitleView()" v-if="$store.getters.totals && $store.getters.totals['totalNotes'] > 0">
<i v-if="titleView" class="th icon"></i>
<i v-if="!titleView" class="bars icon"></i>
</div>
</div>
<div class="eight wide column" v-if="showClear">
<!-- <fast-filters /> -->
<span class="ui fluid green button" @click="reset">
<i class="arrow circle left icon"></i>Show All Notes
</span>
</div>
</div>
</div>
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !loadingInProgress">
<h2 class="ui header">
<div class="content">
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}"
<div v-if="searchResultsCount == 0" class="sub header">
Search can only find key words. Try a single word search.
</div>
</div>
</h2>
</div>
<div v-if="fastFilters['onlyArchived'] == 1" class="sixteen wide column">
<h2>Archived Notes</h2>
</div>
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
<h2>Trash
<span>({{ $store.getters.totals['trashedNotes'] }})</span>
<div class="ui right floated basic button" data-tooltip="This doesn't work yet">
<i class="poo storm icon"></i>
Empty Trash
</div>
</h2>
</div>
<div class="sixteen wide column" v-if="fastFilters['onlyShowSharedNotes'] == 1">
<h2>Shared Notes</h2>
</div>
<div class="sixteen wide column" v-if="tagSuggestions.length > 0">
<h5 class="ui tiny dividing header"><i class="green tags icon"></i> Tags ({{ tagSuggestions.length }})</h5>
<div class="ui clickable green label" v-for="tag in tagSuggestions" v-on:click="tagId => toggleTagFilter(tag.id)">
<i class="tag icon"></i>
{{ tag.text }}
</div>
</div>
<!-- found attachments -->
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
<h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
<attachment-display
v-for="item in foundAttachments"
:item="item"
:key="item.id"
:search-params="{}"
/>
</div>
<!-- Note title card display -->
<div class="sixteen wide column">
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0 && fastFilters['notesHome'] == 1">
No Notes Yet. <br>Thats ok.<br><br> <br>
<img loading="lazy" width="25%" src="/api/static/assets/marketing/hamburger.svg" alt="Create a new note"><br>
Create one when you feel ready.
</h3>
<!-- Go to one wide column, do not do this on mobile interface -->
<div :class="{'one-column':( showOneColumn() )}">
<!-- render each section based on notes in set -->
<div v-for="section,index in noteSections" v-if="section.length > 0" class="note-card-section">
<h5 class="ui tiny dividing header"><i :class="`green ${sectionData[index][0]} icon`"></i>{{ sectionData[index][1] }}</h5>
<div class="note-card-display-area">
<note-title-display-card
v-on:tagClick="tagId => toggleTagFilter(tagId)"
v-for="note in section"
:ref="'note-'+note.id"
:onClick="openNote"
:data="note"
:title-view="titleView"
:currently-open="activeNoteId1 == note.id"
:key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
/>
</div>
</div>
<loading-icon v-if="loadingInProgress" message="Decrypting Notes" />
</div>
</div>
</div>
<note-input-panel
v-if="activeNoteId1 != null"
:key="activeNoteId1"
:noteid="activeNoteId1"
:url-data="$route.params"
/>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'SearchBar',
components: {
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
// 'fast-filters': require('@/components/FastFilters.vue').default,
'search-input': require('@/components/SearchInput.vue').default,
'attachment-display': require('@/components/AttachmentDisplayCard').default,
'counter':require('@/components/AnimatedCounterComponent.vue').default,
'tag-display':require('@/components/TagDisplayComponent.vue').default,
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
},
data () {
return {
initComponent: true,
tagSuggestions:[],
searchTerm: '',
searchResultsCount: 0,
searchTags: [],
notes: [],
highlights: [],
searchDebounce: null,
fastFilters: {},
titleView: false,
//Load up notes in batches
firstLoadBatchSize: 10, //First set of rapidly loaded notes
batchSize: 25, //Size of batch loaded when user scrolls through current batch
batchOffset: 0, //Tracks the current batch that has been loaded
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
loadingInProgress: false,
scrollLoadEnabled: true,
//Clear button is not visible
showClear: false,
initialPostData: null,
//Currently open notes in app
activeNoteId1: null,
activeNoteId2: null,
//Position determines how note is Positioned
activeNote1Position: 0,
activeNote2Position: 0,
lastVisibilityState: null,
foundAttachments: [],
sectionData: {
'pinned': ['thumbtack', 'Pinned'],
'archived': ['archive', 'Archived'],
'shared': ['envelope outline', 'Inbox'],
'sent': ['paper plane outline', 'Sent Notes'],
'notes': ['file','Notes'],
'highlights': ['paragraph', 'Found In Text'],
'trashed': ['poop', 'Trashed Notes'],
'tagged': ['tag', 'Tagged'],
},
noteSections: {
pinned: [],
archived: [],
shared:[],
sent:[],
notes: [],
highlights: [],
trashed: [],
tagged:[],
},
}
},
beforeMount(){
this.$parent.loginGateway()
this.$io.on('new_note_created', noteId => {
//Do not update note if its open
if(this.activeNoteId1 != noteId){
this.$store.dispatch('fetchAndUpdateUserTotals')
this.updateSingleNote(noteId, false)
}
})
this.$io.on('note_attribute_modified', noteId => {
//Do not update note if its open
if(this.activeNoteId1 != noteId){
this.$store.dispatch('fetchAndUpdateUserTotals')
this.updateSingleNote(noteId, false)
}
})
//Update title cards when new note text is saved
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
//Do not update note if its open
if(this.activeNoteId1 != noteId){
this.updateSingleNote(noteId, true)
}
})
this.$bus.$on('update_single_note', (noteId) => {
//Do not update note if its open
if(this.activeNoteId1 != noteId){
this.updateSingleNote(noteId)
}
})
//Update totals for app
this.$store.dispatch('fetchAndUpdateUserTotals')
//Close note event
this.$bus.$on('close_active_note', ({noteId, modified}) => {
if(modified){
console.log('Just closed Note -> ' + noteId + ', modified -> ', modified)
}
//A note has been closed
if(this.$route.fullPath != '/notes'){
this.$router.push('/notes')
}
this.$store.dispatch('fetchAndUpdateUserTotals')
//Focus and animate if modified
this.updateSingleNote(noteId, modified)
})
this.$bus.$on('note_deleted', (noteId) => {
//Remove deleted note from set, its deleted
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key].forEach( (note, index) => {
if(note.id == noteId){
this.noteSections[key].splice(index,1)
this.$store.dispatch('fetchAndUpdateUserTotals')
return
}
})
})
})
this.$bus.$on('update_fast_filters', filterIndex => {
this.updateFastFilters(filterIndex)
})
//Event to update search from other areas
this.$bus.$on('update_search_term', sentInSearchTerm => {
this.searchTerm = sentInSearchTerm
this.search(true, this.batchSize)
.then( () => {
this.searchAttachments()
const postData = {
'tagText':this.searchTerm.trim()
}
this.tagSuggestions = []
axios.post('/api/tag/suggest', postData)
.then( response => {
this.tagSuggestions = response.data
})
// return
})
})
//Reload page content - don't trigger if load is in progress
this.$bus.$on('note_reload', () => {
if(!this.loadingInProgress){
this.reset()
}
})
window.addEventListener('scroll', this.onScroll)
//Close notes when back button is pressed
// window.addEventListener('hashchange', this.hashChangeAction)
//update note on visibility change
// document.addEventListener('visibilitychange', this.visibiltyChangeAction);
},
beforeDestroy(){
window.removeEventListener('scroll', this.onScroll)
// document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
this.$bus.$off('note_reload')
this.$bus.$off('close_active_note')
// this.$bus.$off('update_single_note')
this.$bus.$off('note_deleted')
this.$bus.$off('update_fast_filters')
this.$bus.$off('update_search_term')
//We want to remove event listeners, but something here is messing them up and preventing ALL event listeners from working
// this.$off() // Remove all event listeners
// this.$bus.$off()
},
mounted() {
//Open note on load if ID is set
if(this.$route.params.id > 1){
this.activeNoteId1 = this.$route.params.id
}
//Loads initial batch and tags
this.reset()
},
watch: {
'$route.params.id': function(id){
//Open note on ID, null id will close note
this.activeNoteId1 = id
}
},
methods: {
toggleTitleView(){
this.titleView = !this.titleView
},
showOneColumn(){
return this.$store.getters.getIsUserOnMobile
//If note 1 or 2 is open, show one column. Or if the user is on mobile
return (this.activeNoteId1 != null || this.activeNoteId2 != null) &&
!this.$store.getters.getIsUserOnMobile
},
openNote(id, event = null){
//Don't open note if a link is clicked in display card
if(event && event.target && event.target.nodeName){
const nodeClick = event.target.nodeName
if(nodeClick == 'A'){ return }
}
//Open note if a link was not clicked
this.$router.push('/notes/open/'+id)
return
},
toggleTagFilter(tagId){
this.searchTags = [tagId]
//Reset note set and load up notes and tags
if(this.searchTags.length > 0){
this.search(true, this.batchSize)
return
}
//If no tags are selected, reset entire page
this.reset()
},
onScroll(e){
clearTimeout(this.loadingBatchTimeout)
this.loadingBatchTimeout = setTimeout(() => {
//Detect distance scrolled down the page
const scrolledDown = window.pageYOffset + window.innerHeight
//Get height of div to properly detect scroll distance down
const height = document.getElementById('app').scrollHeight
//Load if less than 500px from the bottom
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled && !this.loadingInProgress){
this.search(false, this.batchSize, true)
}
}, 30)
return
},
visibiltyChangeAction(event){
//Fuck this shit, just use web sockets
return
//@TODO - phase this out, update it via socket.io
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
//Load initial batch, then tags, then other batch
this.search(false, this.firstLoadBatchSize)
.then( () => {
// return
})
}
this.lastVisibilityState = document.visibilityState
},
// @TODO Don't even trigger this if the note wasn't changed
updateSingleNote(noteId, focuseAndAnimate = true){
noteId = parseInt(noteId)
//Find local note, if it exists; continue
let note = null
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
note = this.$refs['note-'+noteId][0].note
//Show that note is working on updating
this.$refs['note-'+noteId][0].showWorking = true
}
//Lookup one note using passed in ID
const postData = {
searchQuery: this.searchTerm,
searchTags: this.searchTags,
fastFilters:{
noteIdSet:[noteId]
}
}
//Note data must be fetched, then sorted into existing note data
axios.post('/api/note/search', postData)
.then(results => {
//Pull note data out of note set
let newNote = results.data.notes[0]
if(newNote === undefined){
return
}
if(note && newNote){
//go through each prop and update it with new values
Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop]
})
//Push new note to front if its modified or we want it to
if( focuseAndAnimate || note.updated != newNote.updated ){
// Find note, in section, move to front
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key].forEach( (searchNote, index) => {
if(searchNote.id == noteId){
//Remove note from location and push to front
this.noteSections[key].splice(index, 1)
this.noteSections[key].unshift(note)
return
}
})
})
this.$nextTick( () => {
//Trigger close animation on note
this.$refs['note-'+noteId][0].justClosed()
this.$refs['note-'+noteId][0].showWorking = false
})
}
}
//New notes don't exist in list, push them to the front
if(note == null){
this.noteSections.notes.unshift(newNote)
//Trigger close animation on note
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].justClosed()
this.$refs['note-'+noteId][0].showWorking = false
}
}
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].showWorking = false
}
//Trigger section rebuild
this.rebuildNoteCategorise()
})
.catch(error => {
console.log(error)
this.$bus.$emit('notification', 'Failed to Update Note')
})
},
searchAttachments(){
axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm})
.then(results => {
this.foundAttachments = results.data
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Attachments') })
},
search(showLoading = true, notesInNextLoad = 10, mergeExisting = false){
return new Promise((resolve, reject) => {
//Don't double load note batches
if(this.loadingInProgress){
console.log('Loading already in progress')
return resolve(false)
}
//Reset a lot of stuff if we are not merging batches
if(!mergeExisting){
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key] = []
})
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
}
this.searchResultsCount = 0
//Remove all filter limits from previous queries
delete this.fastFilters.limitSize
delete this.fastFilters.limitOffset
let postData = {
searchQuery: this.searchTerm,
searchTags: this.searchTags,
fastFilters: this.fastFilters,
}
//Save initial post data on first load
if(this.initialPostData == null){
this.initialPostData = JSON.stringify(postData)
}
//If post data is not the same as initial, show clear button
if(JSON.stringify(postData) != this.initialPostData){
this.showClear = true
}
if(notesInNextLoad && notesInNextLoad > 0){
//Create limit based off of the number of notes already loaded
postData.fastFilters.limitSize = notesInNextLoad
postData.fastFilters.limitOffset = this.batchOffset
}
//Perform search - or die
this.loadingInProgress = true
axios.post('/api/note/search', postData)
.then(response => {
// console.timeEnd('Fetch TitleCard Batch '+notesInNextLoad)
//Save the number of notes just loaded
this.batchOffset += response.data.notes.length
//Enable or disable scroll loading
this.scrollLoadEnabled = response.data.notes.length > 0
if(response.data.total > 0){
this.searchResultsCount = response.data.total
}
this.loadingInProgress = false
this.generateNoteCategories(response.data.notes, mergeExisting)
return resolve(true)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
})
},
rebuildNoteCategorise(){
let currentNotes = []
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key].forEach( note => {
currentNotes.push(note)
})
})
this.generateNoteCategories(currentNotes, false)
},
generateNoteCategories(notes, mergeExisting){
// Place each note in a category based on certain attributes and fast filters
//Reset all sections if we are not merging existing
if(!mergeExisting){
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key] = []
})
}
//Sort notes into defined sections
notes.forEach(note => {
if(this.searchTerm.length > 0){
if(note.pinned == 1){
this.noteSections.pinned.push(note)
return
}
//Push to default note section
this.noteSections.notes.push(note)
return
}
//Display all tags in tag section
if(this.searchTags.length >= 1){
this.noteSections.tagged.push(note)
return
}
//Only show trashed notes when trashed
if(this.fastFilters.onlyShowTrashed == 1){
if(note.trashed == 1){
this.noteSections.trashed.push(note)
}
return
}
if(note.trashed == 1){
return
}
//Show archived notes
if(this.fastFilters.onlyArchived == 1){
if(note.pinned == 1 && note.archived == 1){
this.noteSections.pinned.push(note)
return
}
if(note.archived == 1){
this.noteSections.archived.push(note)
}
return
}
if(note.archived == 1){ return }
//Only show sent notes section if shared is selected
if(this.fastFilters.onlyShowSharedNotes == 1){
if(note.shared == 2){
this.noteSections.sent.push(note)
}
if(note.shareUsername != null){
this.noteSections.shared.push(note)
}
return
}
//Show shared notes on main list but not notes shared with you
if(note.shareUsername != null){ return }
// Pinned notes are always first, they can appear in the archive
if(note.pinned == 1){
this.noteSections.pinned.push(note)
return
}
//Push to default note section
this.noteSections.notes.push(note)
return
})
},
reset(){
this.showClear = false
this.scrollLoadEnabled = true
this.searchTerm = ''
this.searchTags = []
this.tagSuggestions = []
this.fastFilters = {}
this.foundAttachments = [] //Remove all attachments
this.updateFastFilters(5) //This loads notes
},
updateFastFilters(index){
//clear out tags
this.searchTags = []
this.tagSuggestions = []
this.loadingInProgress = false
this.searchTerm = ''
this.$bus.$emit('reset_fast_filters') //Clear out search
const options = [
'withLinks', // 'Only Show Notes with Links'
'withTags', // 'Only Show Notes with Tags'
'onlyArchived', //'Only Show Archived Notes'
'onlyShowSharedNotes', //Only show shared notes
'onlyShowTrashed',
'notesHome',
]
let filter = {}
filter[options[index]] = 1
this.fastFilters = filter
//Fetch First batch of notes with new filter
this.search(true, this.firstLoadBatchSize, false)
.then( r => this.search(false, this.batchSize, true))
}
}
}
</script>
<style type="text/css" scoped>
.detail {
float: right;
}
.note-card-display-area {
display: flex;
flex-wrap: wrap;
}
.display-area-title {
width: 100%;
display: inline-block;
}
.note-card-section {
/*padding-bottom: 15px;*/
}
.note-card-section + .note-card-section {
padding: 15px 0 0;
}
</style>

View File

@@ -13,6 +13,7 @@ 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)
@@ -43,6 +44,12 @@ export default new Router({
meta: {title: 'Open Note'},
component: NotesPage,
},
{
path: '/search/tags/:tag',
name: 'Search Notes',
meta: {title: 'Search Notes'},
component: NotesPage,
},
{
path: '/notes/open/:id/menu/:openMenu',
name: 'Open Note Menu',
@@ -61,6 +68,12 @@ export default new Router({
meta: {title:'Terms'},
component: TermsPage
},
{
path: '/bookmarklet',
name: 'Bookmarklet',
meta: {title:'Bookmarklet'},
component: BookmarkletPage
},
{
path: '/settings',
name: 'Settings',
@@ -109,5 +122,12 @@ export default new Router({
meta: {title:'404 Page Not Found'},
component: NotFoundPage
},
// Cycle Tracking
{
path: '/metrictrack',
name: 'Metric Tracking',
meta: {title:'Metric Tracking'},
component: () => import(/* webpackChunkName: "MetrictrackingPage" */ '@/pages/MetrictrackingPage')
},
]
})

View File

@@ -9,7 +9,9 @@ export default new Vuex.Store({
username: null,
nightMode: false,
isUserOnMobile: false,
userTotals: null,
fetchTotalsTimeout: null,
userTotals: null, // {} // setting this to object breaks reactivity
activeSessions: 0,
},
mutations: {
setUsername(state, username){
@@ -24,6 +26,7 @@ export default new Vuex.Store({
localStorage.removeItem('loginToken')
localStorage.removeItem('username')
localStorage.removeItem('currentVersion')
localStorage.removeItem('snippetCache')
delete axios.defaults.headers.common['authorizationtoken']
state.username = null
state.userTotals = null
@@ -41,12 +44,12 @@ export default new Vuex.Store({
'menu-text': '#5e6268',
},
'black':{
'body_bg_color': 'linear-gradient(135deg, rgba(0,0,0,1) 0%, rgba(23,12,46,1) 100%)',
'body_bg_color': 'rgb(12 4 30)',
//'#0f0f0f',//'#000',
'small_element_bg_color': '#000',
'text_color': '#FFF',
'dark_border_color': '#555',//'#ACACAC', //Lighter color to accent elemnts user can interact with
'border_color': '#555',
'border_color': '#505050',
'menu-accent': '#626262',
'menu-text': '#d9d9d9',
},
@@ -98,8 +101,23 @@ export default new Vuex.Store({
state.socket = socket
},
setUserTotals(state, totalsObject){
//Save all the totals for the user
state.userTotals = totalsObject
if(!state.userTotals){
state.userTotals = {}
}
// retain old values loaded on initial, extended options load
let oldMissingValues = {}
Object.keys(state.userTotals).forEach(key => {
if(!totalsObject[key] && totalsObject[key] !== 0){
oldMissingValues[key] = state.userTotals[key]
}
})
// combine old settings with updated settings
let oldAndNew = Object.assign(oldMissingValues, totalsObject)
state.userTotals = oldAndNew
//Set computer version from server
const currentVersion = localStorage.getItem('currentVersion')
@@ -119,6 +137,15 @@ export default new Vuex.Store({
// Object.keys(totalsObject).forEach( key => {
// console.log(key + ' -- ' + totalsObject[key])
// })
},
setActiveSessions(state, countData){
//Count of the number of active socket.io sessions for this user
state.activeSessions = countData
},
hideMetricTrackingReminder(state){
if(state.userTotals){
state.userTotals['showTrackMetricsButton'] = false
}
}
},
getters: {
@@ -144,10 +171,19 @@ export default new Vuex.Store({
totals: state => {
return state.userTotals
},
getActiveSessions: state => {
return state.activeSessions
}
},
actions: {
fetchAndUpdateUserTotals ({ commit }) {
axios.post('/api/user/totals')
fetchAndUpdateUserTotals ({ commit, state }) {
clearTimeout(state.fetchTotalsTimeout)
state.fetchTotalsTimeout = setTimeout(() => {
// load extended options on initial load
let postData = {
extendedOptions: !state.userTotals
}
axios.post('/api/user/totals', postData)
.then( ({data}) => {
commit('setUserTotals', data)
})
@@ -157,6 +193,7 @@ export default new Vuex.Store({
location.reload()
}
})
}, 100)
}
}
})

View File

@@ -1,18 +0,0 @@
module.exports = {
pwa: {
name: 'SolidScribe',
iconPaths: {
favicon32: null,
favicon16: null,
appleTouchIcon: null,
maskIcon: null,
msTileImage: null,
},
},
devServer: {
disableHostCheck: true,
proxy: 'http://localhost:8081',
public: 'marvin.local',
},
}

View File

@@ -12,3 +12,4 @@ bundle.*
client/dist*
server/public/*
client/dist*
*_scrape*

11
jest.config.js Normal file
View File

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

8812
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,5 +1,7 @@
//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

@@ -6,6 +6,7 @@ const speakeasy = require('speakeasy')
let Auth = {}
const tokenSecretKey = process.env.JSON_KEY
const sessionTokenUses = 300 //Defines number of uses each session token has before being refreshed
//Creates session token
Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => {
@@ -26,7 +27,7 @@ Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) =>
return db.promise().query(
'INSERT INTO user_active_session (salt, encrypted_master_password, created, uses, user_hash, session_id) VALUES (?,?,?,?,?,?)',
[salt, encryptedMasterPass, created, 40, userHash, sessionId])
[salt, encryptedMasterPass, created, sessionTokenUses, userHash, sessionId])
})
.then((r,f) => {

View File

@@ -72,6 +72,8 @@ 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 = ['share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
const commonWords = ['just','start','what','these','how', 'was', 'being','can','way','share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
'give','use','find','tell','ask','work','seem','feel','try','leave','call','good','new','first','last','long','great','little','own','other','old',
'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',
@@ -54,7 +54,7 @@ SiteScrape.getCleanUrls = (textBlock) => {
SiteScrape.getHostName = (url) => {
var hostname = 'https://'+(new URL(url)).hostname;
console.log('hostname', hostname)
// console.log('hostname', hostname)
return hostname
}
@@ -63,36 +63,95 @@ SiteScrape.getDisplayImage = ($, url) => {
const hostname = SiteScrape.getHostName(url)
let metaImg = $('meta[property="og:image"]')
let shortcutIcon = $('link[rel="shortcut icon"]')
let favicon = $('link[rel="icon"]')
let metaImg = $('[property="og:image"]')
let shortcutIcon = $('[rel="shortcut icon"]')
let favicon = $('[rel="icon"]')
let randomImg = $('img')
console.log('----')
//Set of images we may want gathered from various places in source
let imagesWeWant = []
let thumbnail = ''
//Scrape metadata for page image
//Grab the first random image we find
if(randomImg && randomImg[0] && randomImg[0].attribs){
thumbnail = hostname + randomImg[0].attribs.src
console.log('random img '+thumbnail)
if(randomImg && randomImg.length > 0){
let imgSrcs = []
for (let i = 0; i < randomImg.length; i++) {
imgSrcs.push( randomImg[i].attribs.src )
}
//Grab the favicon of the site
const half = Math.ceil(imgSrcs.length / 2)
imagesWeWant = [...imgSrcs.slice(-half), ...imgSrcs.slice(0,half) ]
}
//Grab the shortcut icon
if(favicon && favicon[0] && favicon[0].attribs){
thumbnail = hostname + favicon[0].attribs.href
console.log('favicon '+thumbnail)
imagesWeWant.push(favicon[0].attribs.href)
}
//Grab the shortcut icon
if(shortcutIcon && shortcutIcon[0] && shortcutIcon[0].attribs){
thumbnail = hostname + shortcutIcon[0].attribs.href
console.log('shortcut '+thumbnail)
imagesWeWant.push(shortcutIcon[0].attribs.href)
}
//Grab the presentation image for the site
if(metaImg && metaImg[0] && metaImg[0].attribs){
thumbnail = metaImg[0].attribs.content
console.log('ogImg '+thumbnail)
imagesWeWant.unshift(metaImg[0].attribs.content)
}
// console.log(imagesWeWant)
//Remove everything that isn't an accepted file format
for (let i = imagesWeWant.length - 1; i >= 0; i--) {
let img = String(imagesWeWant[i])
if(
!img.includes('.jpg') &&
!img.includes('.jpeg') &&
!img.includes('.png') &&
!img.includes('.gif')
){
imagesWeWant.splice(i,1)
}
}
//Find if we have absolute thumbnails or not
let foundAbsolute = false
for (let i = imagesWeWant.length - 1; i >= 0; i--) {
let img = imagesWeWant[i]
//Add host name if its not included
if(String(img).includes('//') || String(img).includes('http')){
foundAbsolute = true
break
}
}
//Go through all found images. Grab the one closest to the top. Closer is better
for (let i = imagesWeWant.length - 1; i >= 0; i--) {
let img = imagesWeWant[i]
if(!String(img).includes('//') && foundAbsolute){
continue;
}
//Only add host to images if no absolute images were found
if(!String(img).includes('//') ){
if(img.indexOf('/') != 0){
img = '/' + img
}
img = hostname + img
}
if(img.indexOf('//') == 0){
img = 'https:' + img //Scrape breaks without protocol
}
thumbnail = img
}
console.log('-----')
return thumbnail
}
@@ -103,19 +162,28 @@ SiteScrape.getKeywords = ($) => {
majorContent += $('[class*=content]').text()
.replace(removeWhitespace, " ") //Remove all whitespace
.replace(/\W\s/g, '') //Remove all non alphanumeric characters
.substring(0,3000) //Limit to 3000 characters
// .replace(/\W\s/g, '') //Remove all non alphanumeric characters
.substring(0,6000) //Limit to 6000 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 => {
if(commonWords.includes(word)){
return //Exclude certain words
// Exclude short or common words
if(commonWords.includes(word) || word.length <= 2){
return
}
if(!frequency[word]){
frequency[word] = 0
}
// Skip some plurals
if(frequency[word+'s'] || frequency[word+'es']){
return
}
frequency[word]++
})
@@ -133,7 +201,7 @@ SiteScrape.getKeywords = ($) => {
});
let finalWords = []
for(let i=0; i<5; i++){
for(let i=0; i<6; i++){
if(sortable[i] && sortable[i][0]){
finalWords.push(sortable[i][0])
}

View File

@@ -1,7 +1,12 @@
//Set up environmental variables, pulled from .env file used as process.env.DB_HOST
//Set up environmental variables, pulled from ~/.env file used as process.env.DB_HOST
const os = require('os') //Used to get path of home directory
const result = require('dotenv').config({ path:(os.homedir()+'/.env') })
const ports = {
express: 3000,
socketIo: 3001
}
//Allow user of @ in in require calls. Config in package.json
require('module-alias/register')
@@ -15,7 +20,8 @@ const helmet = require('helmet')
const express = require('express')
const app = express()
app.use( helmet() )
const port = 3000
// allow for the parsing of url encoded forms
app.use(express.urlencoded({ extended: true }));
//
@@ -51,12 +57,31 @@ io.on('connection', function(socket){
Auth.decodeToken(token)
.then(userData => {
socket.join(userData.userId)
//Track active logged in user accounts
const usersInRoom = io.sockets.adapter.rooms[userData.userId]
io.to(userData.userId).emit('update_active_user_count', usersInRoom.length)
}).catch(error => {
//Don't add user to room if they are not logged in
// console.log(error)
})
})
socket.on('get_active_user_count', token => {
Auth.decodeToken(token)
.then(userData => {
socket.join(userData.userId)
//Track active logged in user accounts
const usersInRoom = io.sockets.adapter.rooms[userData.userId]
io.to(userData.userId).emit('update_active_user_count', usersInRoom.length)
}).catch(error => {
// console.log(error)
})
})
//Renew Session tokens when users request a new one
socket.on('renew_session_token', token => {
@@ -91,29 +116,12 @@ 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)
}
}
})
@@ -139,31 +147,13 @@ io.on('connection', function(socket){
noteDiffs[noteId].push(data)
//Remove duplicate diffs if they exist
for (var i = noteDiffs[noteId].length - 1; i >= 0; i--) {
let pastDiff = noteDiffs[noteId][i]
for (var j = noteDiffs[noteId].length - 1; j >= 0; j--) {
let currentDiff = noteDiffs[noteId][j]
if(i == j){
continue
}
if(currentDiff.diff == pastDiff.diff || currentDiff.time == pastDiff.time){
console.log('Removing Duplicate')
noteDiffs[noteId].splice(i,1)
}
}
}
//Each user joins a room when they open the app.
// Go over each user in this note-room
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)
}
@@ -190,7 +180,6 @@ io.on('connection', function(socket){
}
}
noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo)
if(noteDiffs[checkpoint.rawTextId].length == 0){
@@ -205,14 +194,14 @@ io.on('connection', function(socket){
}
})
socket.on('disconnect', function(){
socket.on('disconnect', function(socket){
// console.log('user disconnected');
});
});
http.listen(3001, function(){
console.log('socket.io liseting on port 3001');
http.listen(ports.socketIo, function(){
console.log(`Socke.io: Listening on port ${ports.socketIo}`)
});
//Enable json body parsing in requests. Allows me to post data in ajax calls
@@ -253,22 +242,24 @@ app.use(function(req, res, next){
// Test Area
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()
})
// 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)
// })
//Test
app.get('/api', (req, res) => res.send('Solidscribe API is up and running'))
app.get('/api', (req, res) => res.send('Solidscribe /API is up and running'))
//Serve up uploaded files
app.use('/api/static', express.static( __dirname+'/../staticFiles' ))
@@ -297,9 +288,13 @@ app.use('/api/attachment', attachment)
var quickNote = require('@routes/quicknoteController')
app.use('/api/quick-note', quickNote)
//cycle tracking endpoint
var metricTracking = require('@routes/metrictrackingController')
app.use('/api/metric-tracking', metricTracking)
//Output running status
app.listen(port, () => {
console.log(`Listening on port ${port}!`)
app.listen(ports.express, () => {
console.log(`Express: Listening on port ${ports.express}!`)
})
//

View File

@@ -1,6 +1,7 @@
let db = require('@config/database')
let SiteScrape = require('@helpers/SiteScrape')
const cs = require('@helpers/CryptoString')
let Attachment = module.exports = {}
@@ -46,16 +47,28 @@ Attachment.textSearch = (userId, searchTerm) => {
})
}
Attachment.search = (userId, noteId, attachmentType, offset, setSize) => {
Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeShared) => {
console.log([userId, noteId, attachmentType, offset, setSize, includeShared])
return new Promise((resolve, reject) => {
let params = [userId]
let query = 'SELECT * FROM attachment WHERE user_id = ? AND visible = 1 '
let query = `
SELECT attachment.*, note.share_user_id FROM attachment
LEFT JOIN note ON (attachment.note_id = note.id)
WHERE attachment.user_id = ? AND visible = 1
`
if(noteId && noteId > 0){
query += 'AND note_id = ? '
//
// Show everything if note ID is present
//
query += 'AND attachment.note_id = ? '
params.push(noteId)
}
} else {
//
// Other filters if NO note id
//
if(attachmentType == 'links'){
query += 'AND attachment_type = 1 '
@@ -64,13 +77,30 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize) => {
query += 'AND attachment_type > 1 '
}
query += `AND note.archived = ${ attachmentType == 'archived' ? '1':'0' } `
query += `AND note.trashed = ${ attachmentType == 'trashed' ? '1':'0' } `
if(!attachmentType){
// Null note ID means it was pushed by bookmarklet
query += 'OR attachment.note_id IS NULL '
}
}
if(!noteId){
const sharedOrNot = includeShared ? ' NOT ':' '
query += `AND note.share_user_id IS${sharedOrNot}NULL `
}
query += 'ORDER BY last_indexed DESC '
const limitOffset = parseInt(offset, 10) || 0 //Either parse int, or use zero
const parsedSetSize = parseInt(setSize, 10) || 20 //Either parse int, or use zero
const parsedSetSize = parseInt(setSize, 10) || 20
query += ` LIMIT ${limitOffset}, ${parsedSetSize}`
console.log(query)
db.promise()
.query(query, params)
.then((rows, fields) => {
@@ -80,18 +110,6 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize) => {
})
}
//Returns all attachments
Attachment.forNote = (userId, noteId) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND visible = 1 ORDER BY last_indexed DESC;`, [userId, noteId])
.then((rows, fields) => {
resolve(rows[0]) //Return all attachments found by query
})
.catch(console.log)
})
}
Attachment.urlForNote = (userId, noteId) => {
return new Promise((resolve, reject) => {
db.promise()
@@ -167,6 +185,7 @@ Attachment.delete = (userId, attachmentId, urlDelete = false) => {
.catch(console.log)
}
})
.catch(console.log)
})
}
@@ -283,9 +302,13 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
//Once everything is done being scraped, emit new attachment events
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)
})
})
}
@@ -313,9 +336,13 @@ Attachment.scrapeUrlsCreateAttachments = (userId, noteId, foundUrls) => {
//All URLs have been scraped, return data
if(processedCount == foundUrls.length){
resolve(scrapedText)
console.log('All urls scraped')
return resolve(scrapedText)
}
})
.catch(error => {
console.log('Site Scrape error', error)
})
})
})
}
@@ -325,17 +352,16 @@ Attachment.downloadFileFromUrl = (url) => {
return new Promise((resolve, reject) => {
if(url == null){
resolve(null)
if(!url){
return resolve(null)
}
const random = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
const extension = '.'+url.split('.').pop() //This is throwing an error
let fileName = random+'_scrape'+extension
const thumbPath = 'thumb_'+fileName
let extension = ''
let fileName = random+'_scrape'
let thumbPath = 'thumb_'+fileName
console.log('Scraping image url')
console.log(url)
console.log('Scraping image url', url)
console.log('Getting ready to scrape ', url)
@@ -347,6 +373,8 @@ Attachment.downloadFileFromUrl = (url) => {
.on('response', res => {
console.log(res.statusCode)
console.log(res.headers['content-type'])
//Get mime type from header content type
// extension = '.'+String(res.headers['content-type']).split('/').pop()
})
.pipe(fs.createWriteStream(filePath+thumbPath))
.on('close', () => {
@@ -354,21 +382,24 @@ Attachment.downloadFileFromUrl = (url) => {
//resize image if its real big
gm(filePath+thumbPath)
.resize(550) //Resize to width of 550 px
.quality(75) //compression level 0 - 100 (best)
.quality(85) //compression level 0 - 100 (best)
.write(filePath+thumbPath, function (err) {
if(err){ console.log(err) }
})
if(err){
console.log(err)
return resolve(null)
}
console.log('Saved Image')
resolve(fileName)
return resolve(fileName)
})
})
})
}
Attachment.processUrl = (userId, noteId, url) => {
const scrapeTime = 20*1000;
const scrapeTime = 5*1000;
return new Promise((resolve, reject) => {
@@ -396,7 +427,7 @@ Attachment.processUrl = (userId, noteId, url) => {
.query(`INSERT INTO attachment
(note_id, user_id, attachment_type, text, url, last_indexed, file_location)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[noteId, userId, 1, 'Processing...', url, created, null])
[noteId, userId, 1, url, url, created, null])
.then((rows, fields) => {
//Set two bigger variables then return request for processing
request = rp(options)
@@ -421,9 +452,12 @@ Attachment.processUrl = (userId, noteId, url) => {
const keywords = SiteScrape.getKeywords($)
var desiredSearchText = ''
desiredSearchText += pageTitle + "\n"
desiredSearchText += keywords
desiredSearchText += pageTitle
if(keywords){
desiredSearchText += "\n " + keywords
}
console.log('Results from site scrape-------------')
console.log({
pageTitle,
hostname,
@@ -473,40 +507,142 @@ Attachment.processUrl = (userId, noteId, url) => {
})
.catch(error => {
// console.log('Scrape pooped out')
// console.log('Issue with scrape')
console.log(error)
// resolve('')
console.log('Scrape pooped out')
console.log('Issue with scrape', error.statusCode)
clearTimeout(requestTimeout)
return resolve('No site text')
})
requestTimeout = setTimeout( () => {
console.log('Cancel the request, its taking to long.')
request.cancel()
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)
return resolve('Request Timeout')
}, 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

@@ -0,0 +1,71 @@
let db = require('@config/database')
let Note = require('@models/Note')
let MetricTracking = module.exports = {};
MetricTracking.get = (userId, masterKey) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`
SELECT note.id FROM note WHERE quick_note = 2 AND user_id = ? LIMIT 1`, [userId])
.then((rows, fields) => {
//Quick Note is set, return note object
if(rows[0][0] != undefined){
let noteId = rows[0][0].id
const note = Note.get(userId, noteId, masterKey)
.then(noteData => {
return resolve(noteData)
})
} else {
return resolve('no data')
}
})
.catch(console.log)
})
}
MetricTracking.create = (userId, masterKey) => {
return new Promise((resolve, reject) => {
let finalId = null
return Note.create(userId, 'Metric Tracking', '', masterKey)
.then(insertedId => {
finalId = insertedId
db.promise().query('UPDATE note SET quick_note = 2 WHERE id = ? AND user_id = ?',[insertedId, userId])
.then((rows, fields) => {
const note = Note.get(userId, finalId, masterKey)
.then(noteData => {
return resolve(noteData)
})
})
})
.catch(console.log)
})
}
MetricTracking.save = (userId, metricData, masterKey) => {
return new Promise((resolve, reject) => {
let finalId = null
MetricTracking.get(userId, masterKey)
.then(noteObject => {
return Note.update(userId, noteObject.id, metricData, noteObject.title, noteObject.color, noteObject.pinned, noteObject.archived, null, masterKey)
})
.then( saveResults => {
return resolve(saveResults)
})
})
}

View File

@@ -17,6 +17,7 @@ const fs = require('fs')
const gm = require('gm')
Note.test = (userId, masterKey, printResults) => {
return false;
return new Promise((resolve, reject) => {
@@ -162,6 +163,10 @@ Note.test = (userId, masterKey, printResults) => {
return resolve('Test: Complete ---')
})
.catch(error => {
console.log(error)
return reject(error)
})
})
}
@@ -193,7 +198,7 @@ Note.create = (userId, noteTitle = '', noteText = '', masterKey) => {
})
.then((rows, fields) => {
if(SocketIo){
if(typeof SocketIo != 'undefined'){
SocketIo.to(userId).emit('new_note_created', rows[0].insertId)
}
@@ -341,7 +346,7 @@ Note.reindex = (userId, masterKey, removeId = null) => {
setTimeout(() => {
if(masterKey == null || note.salt == null){
console.log('Error indexing note', note.id)
console.log('Error indexing note - master key or salt missing', note.id)
return resolve(true)
}
@@ -390,13 +395,13 @@ Note.reindex = (userId, masterKey, removeId = null) => {
return Promise.all(reindexQueue)
})
.then(rawSearchIndex => {
.then(updatePromiseResults => {
const created = Math.round((+new Date)/1000)
const jsonSearchIndex = JSON.stringify(searchIndex)
const encryptedJsonIndex = cs.encrypt(masterKey, searchIndexSalt, jsonSearchIndex)
return db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
[encryptedJsonIndex, created, userId])
.then((rows, fields) => {
@@ -406,6 +411,7 @@ 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)
})
@@ -442,6 +448,10 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
})
.then((rows, fields) => {
if(!rows[0] || !rows[0][0] || !rows[0][0]['note_raw_text_id']){
return reject(false)
}
const textId = rows[0][0]['note_raw_text_id']
let salt = rows[0][0]['salt']
let snippetSalt = rows[0][0]['snippet_salt']
@@ -503,12 +513,12 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
})
.then((rows, fields) => {
if(SocketIo){
if(typeof SocketIo != 'undefined'){
SocketIo.to(userId).emit('new_note_text_saved', {noteId, hash})
}
//Async attachment reindex
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
}
//Send back updated response
resolve(rows[0])
@@ -658,6 +668,9 @@ Note.delete = (userId, noteId, masterKey = null) => {
})
}
//
// Returns noteData
//
Note.get = (userId, noteId, masterKey) => {
return new Promise((resolve, reject) => {
@@ -681,6 +694,7 @@ Note.get = (userId, noteId, masterKey) => {
note_raw_text.text,
note_raw_text.salt,
note_raw_text.updated as updated,
GROUP_CONCAT(DISTINCT(tag.text) ORDER BY tag.text DESC) AS tags,
note.id,
note.user_id,
note.created,
@@ -697,7 +711,9 @@ Note.get = (userId, noteId, masterKey) => {
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
LEFT JOIN attachment ON (note.id = attachment.note_id)
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, noteId])
LEFT JOIN note_tag ON (note.id = note_tag.note_id AND note_tag.user_id = ?)
LEFT JOIN tag ON (note_tag.tag_id = tag.id)
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, userId, noteId])
})
.then((rows, fields) => {
@@ -729,12 +745,13 @@ 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)
})
})
.catch(error => {
@@ -985,6 +1002,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.visible = 1)
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
WHERE note.user_id = ?
AND note.quick_note <= 1
`
//If text search returned results, limit search to those ids

View File

@@ -13,11 +13,14 @@ QuickNote.get = (userId, masterKey) => {
SELECT note.id FROM note WHERE quick_note = 1 AND user_id = ? LIMIT 1`, [userId])
.then((rows, fields) => {
//Quick Note is set, return note text
//Quick Note is set, return note object
if(rows[0][0] != undefined){
let noteId = rows[0][0].id
return resolve({'noteId':noteId})
const note = Note.get(userId, noteId, masterKey)
.then(noteData => {
return resolve(noteData)
})
} else {
//Or create a new note and get the id
@@ -81,7 +84,7 @@ QuickNote.update = (userId, pushText, masterKey) => {
.replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities
.replace(/<[^>]+>/g, '') //Rip out all HTML tags
//Turn links into actual linx
//Turn links into actual link
clean = QuickNote.makeUrlLink(clean)
if(clean == ''){ clean = '&nbsp;' }
@@ -114,7 +117,7 @@ QuickNote.update = (userId, pushText, masterKey) => {
}
})
.then( saveResults => {
return resolve(true)
return resolve(saveResults)
})
})

View File

@@ -138,6 +138,33 @@ Tag.get = (userId, noteId) => {
})
}
//
// Get just tag string for note
//
Tag.fornote = (userId, noteId) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`SELECT GROUP_CONCAT(DISTINCT(tag.text) ORDER BY tag.text DESC) AS tags
FROM note_tag
LEFT JOIN tag ON (note_tag.tag_id = tag.id)
WHERE note_tag.note_id = ?
AND user_id = ?;
`, [noteId,userId])
.then((rows, fields) => {
//pull IDs out of returned results
// let ids = rows[0].map( item => {})
resolve( rows[0][0] ) //Return all tags found by query
})
.catch(console.log)
})
}
//
// Get all tags for a note and concatinate into a string 'all, tags, like, this'
//

View File

@@ -9,7 +9,8 @@ const speakeasy = require('speakeasy')
let User = module.exports = {}
const version = '3.3.1'
const version = '3.8.0'
// 3.7.3 - diff/patch update
//Login a user, if that user does not exist create them
//Issues login token
@@ -193,17 +194,19 @@ User.register = (username, password) => {
}
//Counts notes, pinned notes, archived notes, shared notes, unread notes, total files and types
User.getCounts = (userId) => {
User.getCounts = (userId, extendedOptions) => {
return new Promise((resolve, reject) => {
let countTotals = {}
const userHash = cs.hash(String(userId)).toString('base64')
let countTotals = {
tags: {}
}
// const userHash = cs.hash(String(userId)).toString('base64')
db.promise().query(
`SELECT
SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes,
SUM(trashed = 1) AS trashedNotes,
SUM(share_user_id IS NULL && trashed = 0) AS totalNotes,
SUM(share_user_id IS NULL && trashed = 0 AND quick_note < 2) AS totalNotes,
SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount,
SUM(share_user_id != ? && trashed = 0) AS sharedToNotes
FROM note
@@ -244,17 +247,73 @@ User.getCounts = (userId) => {
Object.assign(countTotals, rows[0][0]) //combine results
//Count usages of user tags, sort by most popular
return db.promise().query(`
SELECT
tag.text, COUNT(tag_id) AS uses, tag.id
FROM note_tag
JOIN tag ON (tag.id = note_tag.tag_id)
WHERE user_id = ?
GROUP BY tag_id
ORDER BY uses DESC
LIMIT 16
`, [userId])
}).then( (rows, fields) => {
//Convert everything to an int or 0
Object.keys(countTotals).forEach( key => {
const count = parseInt(countTotals[key])
countTotals[key] = count ? count : 0
})
//Build out tags object
let tagsObject = {}
rows[0].forEach(tagRow => {
tagsObject[tagRow['text']] = {'id':tagRow.id, 'uses':tagRow.uses}
})
//Assign after counts are updated
countTotals['tags'] = tagsObject
countTotals['currentVersion'] = version
// Allow for extended options set on page load
if(extendedOptions){
db.promise().query(
`SELECT updated FROM note
JOIN note_raw_text ON note_raw_text.id = note.note_raw_text_id
WHERE note.quick_note = 2
AND user_id = ?`, [userId])
.then( (rows, fields) => {
if(rows[0][0] && rows[0][0].updated){
const lastOpened = rows[0][0].updated
const timeDiff = Math.round(((+new Date) - (lastOpened))/1000)
const hoursInSeconds = (12 * 60 * 60) //12 hours
// Show metric tracking button if its been 12 hours since last entry
if(lastOpened && timeDiff > hoursInSeconds){
countTotals['showTrackMetricsButton'] = true
}
}
resolve(countTotals)
})
} else {
resolve(countTotals)
}
})
})
}
@@ -494,6 +553,12 @@ 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 = []
@@ -526,77 +591,3 @@ User.deleteUser = (userId, password) => {
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

@@ -26,7 +26,7 @@ router.use(function setUserId (req, res, next) {
})
router.post('/search', function (req, res) {
Attachment.search(userId, req.body.noteId, req.body.attachmentType, req.body.offset, req.body.setSize)
Attachment.search(userId, req.body.noteId, req.body.attachmentType, req.body.offset, req.body.setSize, req.body.includeShared)
.then( data => res.send(data) )
})
@@ -35,11 +35,6 @@ 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 => {
@@ -65,5 +60,26 @@ router.post('/upload', upload.single('file'), function (req, res, next) {
})
//
// Push URL to attachments
// push action on - public controller
//
// get push key
router.post('/getbookmarklet', function (req, res) {
Attachment.getPushkeyBookmarklet(userId)
.then( data => res.send(data) )
})
// generate new push key
router.post('/generatepushkey', function (req, res) {
})
// delete push key
router.post('/deletepushkey', function (req, res) {
})
module.exports = router

View File

@@ -0,0 +1,45 @@
//
// /api/metric-tracking
//
var express = require('express')
var router = express.Router()
let MetricTracking = require('@models/MetricTracking');
let userId = null
let masterKey = null
// middleware that is specific to this router
router.use(function setUserId (req, res, next) {
//Session key is required to continue
if(!req.headers.sessionId){
next('Unauthorized')
}
if(req.headers.userId){
userId = req.headers.userId
masterKey = req.headers.masterKey
next()
}
})
router.post('/get', function (req, res) {
MetricTracking.get(userId, masterKey)
.then( data => res.send(data) )
})
router.post('/create', function (req, res) {
MetricTracking.create(userId, masterKey)
.then( data => res.send(data) )
})
//Push text to quick note
router.post('/save', function (req, res) {
MetricTracking.save(userId, req.body.cycleData, masterKey)
.then( data => res.send(data) )
})
module.exports = router

View File

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

View File

@@ -50,6 +50,12 @@ router.post('/get', function (req, res) {
.then( data => res.send(data) )
})
//Get the latest notes the user has created
router.post('/fornote', function (req, res) {
Tags.fornote(userId, req.body.noteId)
.then( data => res.send(data) )
})
//Get all the tags for this user in order of usage
router.post('/usertags', function (req, res) {
Tags.userTags(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters)

View File

@@ -53,7 +53,7 @@ router.post('/revokesessions', function(req, res) {
// fetch counts of users notes
router.post('/totals', function (req, res) {
User.getCounts(req.headers.userId)
User.getCounts(req.headers.userId, req.body.extendedOptions)
.then( countsObject => res.send( countsObject ))
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,9 @@
cd /home/mab/ss
echo '::--:: Starting dev server. cd client; npm run serve -> 192.168.1.164:8081'
screen -dmS "NoteClientScreen" bash -c "cd /home/mab/ss/client; npm run serve"
screen -dmS "NoteClientScreen" bash -c "cd /home/mab/ss/client; npm run serve -- --port 8081 --https true"
echo '::--:: Starting API server (/api), watching for file changes...'
cd /home/mab/ss/server
pm2 flush
pm2 start ecosystem.config.js

View File

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

BIN
staticFiles/assets/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" id="svg8" version="1.1" viewBox="0 0 132.29166 132.29167" height="500" width="500">
<defs id="defs2"/>
<metadata id="metadata5">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g style="display:inline" transform="translate(0,-164.70832)" id="layer1">
<path id="path3813-4" d="m 56.22733,165.36641 -55.56249926,15.875 8e-7,63.5 47.62499846,11.90625 v 27.78125 l -47.76066333,-13.9757 0.13566407,10.00695 55.56249926,15.875 v -47.625 l -47.6249985,-11.90625 -8e-7,-47.625 47.7606633,-13.94121 c 0.135664,-2.30629 -0.135664,-9.87129 -0.135664,-9.87129 z" style="fill:#0f7425;fill-opacity:1;stroke:none;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;shape-rendering:crispedges"/>
<path id="path4563" d="m 20.508581,220.92891 c 15.265814,-14.23899 27.809717,-7.68002 39.687499,3.96875 v -7.9375 C 51.75093,200.8366 37.512584,206.01499 20.508581,205.05391 Z" style="fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;shape-rendering:crispedges"/>
<path id="path4563-6" d="m 111.78985,220.92891 c -15.265834,-14.23899 -27.809737,-7.68002 -39.68752,3.96875 v -7.9375 c 8.445151,-16.12356 22.683497,-10.94517 39.68752,-11.90625 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;shape-rendering:crispedges"/>
<path id="path3813-4-2" d="m 76.07108,165.36641 55.5625,15.875 v 63.5 l -47.625,11.90625 v 27.78125 l 47.76067,-13.9757 -0.13567,10.00695 -55.5625,15.875 v -47.625 l 47.625,-11.90626 V 189.17891 L 75.93542,175.2377 c -0.13567,-2.30629 0.13566,-9.87129 0.13566,-9.87129 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;shape-rendering:crispedges"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,24 @@
{
"theme_color":"#000",
"background_color": "#000",
"description": "Take Notes",
"display": "standalone",
"icons": [
{
"src": "/api/static/assets/logo.png",
"sizes": "496x496",
"type": "image/png",
"purpose": "any"
},
{
"src": "/api/static/assets/maskable_icon.png",
"sizes": "826x826",
"type": "image/png",
"purpose": "maskable"
}
],
"name": "Solid Scribe",
"short_name": "Solid Scribe",
"start_url": "/#/notes",
"author":"Max"
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" id="svg8" version="1.1" viewBox="0 0 132.29166 132.29167" height="500" width="500">
<defs id="defs2"/>
<metadata id="metadata5">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g style="display:inline" transform="translate(0,-164.70832)" id="layer1">
<path id="path3813-4" d="m 56.22733,165.36641 -55.56249926,15.875 8e-7,63.5 47.62499846,11.90625 v 27.78125 l -47.76066333,-13.9757 0.13566407,10.00695 55.56249926,15.875 v -47.625 l -47.6249985,-11.90625 -8e-7,-47.625 47.7606633,-13.94121 c 0.135664,-2.30629 -0.135664,-9.87129 -0.135664,-9.87129 z" style="fill:#0f7425;fill-opacity:1;stroke:none;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<path id="path4563" d="m 20.508581,220.92891 c 15.265814,-14.23899 27.809717,-7.68002 39.687499,3.96875 v -7.9375 C 51.75093,200.8366 37.512584,206.01499 20.508581,205.05391 Z" style="fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path id="path4563-6" d="m 111.78985,220.92891 c -15.265834,-14.23899 -27.809737,-7.68002 -39.68752,3.96875 v -7.9375 c 8.445151,-16.12356 22.683497,-10.94517 39.68752,-11.90625 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path id="path3813-4-2" d="m 76.07108,165.36641 55.5625,15.875 v 63.5 l -47.625,11.90625 v 27.78125 l 47.76067,-13.9757 -0.13567,10.00695 -55.5625,15.875 v -47.625 l 47.625,-11.90626 V 189.17891 L 75.93542,175.2377 c -0.13567,-2.30629 0.13566,-9.87129 0.13566,-9.87129 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,24 @@
{
"theme_color":"#000",
"background_color": "#000",
"description": "Take Notes",
"display": "standalone",
"icons": [
{
"src": "/api/static/assets/logo.png",
"sizes": "496x496",
"type": "image/png",
"purpose": "any"
},
{
"src": "/api/static/assets/maskable_icon.png",
"sizes": "826x826",
"type": "image/png",
"purpose": "maskable"
}
],
"name": "Solid Scribe",
"short_name": "Solid Scribe",
"start_url": "/#/notes",
"author":"Max"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -12,4 +12,4 @@
# z - Compress for speed
# h - Human Readable file sizes
rsync -e 'ssh' --exclude-from=dontSync.txt -havzC --update mab@marvin.local:/home/mab/pi/ .
rsync -e 'ssh' --exclude-from=dontSync.txt -havzC --update mab@marvin.local:/home/mab/ss/ .