Compare commits

..

61 Commits

Author SHA1 Message Date
Max G
cca89a60d8 Update
* Added more version icons
* Added working sign to notes when archived or tagged
* Big sexy marketing update
* Clicking Tags now opens them in their tag category
2020-07-03 03:25:38 +00:00
Max G
a56ade5b08 Updated all global and local client packages
* Tweaked sessions to be a little more reliable on mobile
2020-06-21 02:07:36 +00:00
Max G
39f9a16fff * Updated server packages 2020-06-21 01:07:27 +00:00
Max G
6740200a33 Removed some transitions from tooltips 2020-06-21 01:01:12 +00:00
Max G
e4fae23623 * Added Much better session Management, key updating and deleting
* Force reload of JS if app numbers dont match
* Added cool tag display on side of note
* Cleaned up a bunch of code and tweaked little things to be better
2020-06-15 09:02:20 +00:00
Max G
56d4664d0d * Added new token system to add more security to logins
* Added simple tag editing from note page
2020-06-10 04:41:52 +00:00
Max G
d349fb8328 * Adjusted theme colors to add more contrast on white theme while making black more OLED friendly
* Links now get an underline on hover
* Cleaned up CSS variable names, added another theme color for more control
* Cleaned up unused CSS, removed scrollbars popping up, tons of other little UI tweaks
* Renamed shared notes to inbox
* Tweaked form display, seperated login and create accouts
* Put login/sign up form on home page
* Created more legitimate marketing for home page
* Tons up updates to note page and note input panel
* Better support for two users editing a note
* MUCH better diff handling, web sockets restore notes with unsaved diffs
* Moved all squire text modifier functions into a mixin class
* It now says saving when closing a note
* Lots of cleanup and better handiling of events on mount and destroy
* Scroll behavior modified to load notes when closer to bottom of page
* Pretty decent shared notes and sharable link support
* Updated help text
* Search now includes tag suggestions and attachment suggestions
* Cleaned up scratch pad a ton, allow for users to create new scratch pads
* Created a 404 Page and a Shared note page
* So many other small improvements. Oh my god, what is wrong with me, not doing commits!?
2020-06-07 20:57:35 +00:00
Max G
09cccf1983 * Small hack to fix images not closing window on mobile
* Made note active text modifier buttons better
* Fixed Colored notes being to big on mobile
2020-05-22 07:08:45 +00:00
Max G
97e7b011d9 * Fixed cursor clicking ToDo lists clicking to early
* Added login form to home page with focus on load
* Tags update after editing tags from title card
* Fixed uploading of images/files
* Fixed images not appearing when opening images tab
* Search hits all categories on search, like archived
* Got rid of brand icons to reduce size
* Got rid of DiffPatchMatch and Crypto from note input panel to reduce size
* Disabled animation on io events so they don't annoy the shit out of people on other computers
2020-05-20 07:57:15 +00:00
Max G
fc1f3f81fe Bugfix Batch
* Animations disabled on remote events, closing note still triggers animation for local user
* Created save icons to fix display on mobile
* Hidden URLs are hidden until note is deleted or URL is removed from note
* Tags search all categories, but probably not trash
* Back to all notes button clears search
* Deleted Notes are removed from search index
2020-05-19 03:38:43 +00:00
Max G
9c4fff7913 * Removed arrows from notification
* Added trash can function
* Tweaked status text to always be the same
* Removed some open second note code
* Edior always focuses on text now
* Added some extra loading note messages
* Notes are now removed from search index when deleted
* Lots more things happen and update in real time on multiple machines
* Shared notes can be reverted
* WAY more tests
* Note Categories are much more reliable
* Lots of code is much cleaner
2020-05-18 07:45:35 +00:00
Max G
b0eee636b5 * Made splash page dark and updated description
* Cleaned up unused things
* Updated squire which had a comment typo update...thats it
* Background color picker has matching colors and styles to text color picker
* Added new black theme
* Moved search to main page, show it on mobile and added options to push things to notes from search with experimental tag searching
* Added active note menu buttons based on cursor location in text
* Added more instant updating if app is open in two locations for the same user Scratch Pad and home page update with new notes and new text in real time
2020-05-15 23:12:09 +00:00
Max G
2861042485 * Delete Crunch Menu Component
* Disabled Quick Note
* Note crunches over when menu is open
* Added a cool loader
* Remomoved locked notes
* Added full note encryption
* Added encrypted search index
* Added encrypted shared notes
* Made search bar have a clear and search button
* Tags only loade when clicking on the tags menu
* Tweaked home page to be a little more sane
* built out some gigantic test cases
* simplified a lot of things to make entire app easier to maintain
2020-05-10 21:15:59 +00:00
Max G
1005913c0b Fully Encrypted notes Beta
* Encrypts all notes going to the database
* Creates encrypted snippets for loading note title cards
* Creates an encrypted search index when note is changed
* Migrates users to encrypted notes on login
* Creates new encrypted master keys for newly logged in users
2020-05-06 07:10:27 +00:00
Max G
c8033588dd Major Update: Changed Text Input View
* Created new toolbar that moves on mobile
2020-05-02 19:10:20 +00:00
Max G
bcb31e9af5 Tweaked shrinking buttons for better display on mobile 2020-04-16 01:41:47 +00:00
Max G
596e57eaf0 * Tags Dropbown Beta...kinda crappy 2020-04-15 21:54:36 +00:00
Max G
d91b0735fd * Little Bug Fixes All Around 2020-04-15 20:44:24 +00:00
Max G
71f909fb76 * Made dispay of last edit smaller on note title display card
* Made note menu buttons look better on mobile
* Moved around some note menu buttons
* Added a color picker with a rip off of google colors
* Added a remove formatting button
* Hide pin and archive icons, they appear green on hover, in the buttons
* Further simplified display card logic, now it just adds an end tag and returns the data
* Changed highlight text color to show colors (works on chrome...)
2020-04-15 06:28:58 +00:00
Max G
a44bca204c * Added error display to every axios server call
* Added better destroy of login token if invalid
* Block users from opening notes they don't own, note closes automatically
* Beefed up login and home page a little to make them more appealing
2020-04-14 05:09:19 +00:00
Max G
7c15427b3d * Added placeholder text to site when loading JS
* Added hidden text to site for scraping
* Login token will be destroyed if fetch site totals is called and the token is bad
* Moved passwords out of application and into a .env file that is loaded on startup
* Changed prod database password for primary user (which is dev)
* Set up .env for dev and prod
2020-04-13 07:44:57 +00:00
Max G
ed4a5e5291 * Added some better base information to site for scrapers
* Updated help text
* Refactored a lot of the scrape code into a SiteScrape helper
2020-04-13 06:17:37 +00:00
Max G
c11f1b1b6f Big Update:
* Menus open and close based on URL, allowing for back button on note menus to close

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

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

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

6
.gitignore vendored
View File

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

View File

@@ -1,63 +0,0 @@
#!/bin/bash
echo '-------'
echo 'Starting Database Restore'
echo '-------'
#get Latest database backup
# Unzip File
# gzip -dk file.gz
BACKUPDIR="/home/mab/databaseBackupSolidScribe"
#DEVDBPASS="Crama!Lama*Jamma###88383!!!!!345345956245i"
#DEVDBPASS="RootPass1234!"
DEVDBPASS="ReallySecureRootPass123!"
# LazaLinga&33Can't!Do!That34
cd $BACKUPDIR
# -t sort by modification time, newest first
# -A --almost-all, do not list implied . and ..
LASTZIPPEDFILE=$(ls -At *.gz | head -n1)
# -k keep file after unzip
# -d Decompress
# -v verbose
echo "Unzipping $LASTZIPPEDFILE"
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'
#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/*
rsync -e 'ssh -p 13328' -hazC --update mab@solidscribe.com:/home/mab/pi/staticFiles /home/mab/ss/
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,34 +1,18 @@
#!/bin/bash #!/bin/bash
# Take all variables in .env and turn them into local variables for this script BACKUPDIR="/home/mab/databaseBackupPi"
source ~/.env
BACKUPDIR="/home/mab/databaseBackupSolidScribe"
mkdir -p $BACKUPDIR mkdir -p $BACKUPDIR
cd $BACKUPDIR cd $BACKUPDIR
NOW=$(date +"%Y-%m-%d_%H-%M") NOW=$(date +"%Y-%m-%d_%H-%M")
ssh mab@solidscribe.com -p 13328 "mysqldump --all-databases --single-transaction --user root -p$PROD_DB_PASS" > "backup-$NOW.sql" ssh mab@solidscribe.com -p 13328 "mysqldump --all-databases --user root -pRootPass1234!" > "backup-$NOW.sql"
gzip "backup-$NOW.sql"
# cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/backup-$NOW.sql" cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/backup-$NOW.sql"
echo "Database Backup Complete on $NOW" echo "Database Backup Complete on $NOW"
# Delete all but last 8 files #Restore DB
ls -tp | grep -v '/$' | tail -n +9 | tr '\n' '\0' | xargs -0 rm --
##
# Restore DB
##
# copy file over, run restore # copy file over, run restore
# scp -P 13328 backup-2019-12-04_03-00.sql mab@avidhabit.com:/home/mab # scp -P 13328 backup-2019-12-04_03-00.sql mab@avidhabit.com:/home/mab
# mysql -u root -p < backup-2019-12-04_03-00.sql # mysql -u root -p < backup-2019-12-04_03-00.sql
##
# Crontab setup
##
# 0 2 * * * /bin/bash /home/mab/ss/backupDatabase.sh 1> /home/mab/databaseBackupLog.txt

12
client/.babelrc Normal file
View File

@@ -0,0 +1,12 @@
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime"]
}

9
client/.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

16
client/.gitignore vendored
View File

@@ -1,20 +1,9 @@
.DS_Store .DS_Store
node_modules node_modules/
/dist /dist/
# local env files
.env.local
.env.*.local
*.pem
*.crt
*.key
# Log files
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log*
# Editor directories and files # Editor directories and files
.idea .idea
@@ -23,4 +12,3 @@ pnpm-debug.log*
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw?

10
client/.postcssrc.js Normal file
View File

@@ -0,0 +1,10 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-url": {},
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}

View File

@@ -1,19 +1,21 @@
# client # client2
## Project setup > client2
```
## Build Setup
``` bash
# install dependencies
npm install npm install
```
### Compiles and hot-reloads for development # serve with hot reload at localhost:8080
``` npm run dev
npm run serve
```
### Compiles and minifies for production # build for production with minification
```
npm run build npm run build
# build for production and view the bundle analyzer report
npm run build --report
``` ```
### Customize configuration For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

41
client/build/build.js Normal file
View File

@@ -0,0 +1,41 @@
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
const spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})

View File

@@ -0,0 +1,54 @@
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
}
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}

BIN
client/build/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

101
client/build/utils.js Normal file
View File

@@ -0,0 +1,101 @@
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}

View File

@@ -0,0 +1,22 @@
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}

View File

@@ -0,0 +1,82 @@
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}

View File

@@ -0,0 +1,96 @@
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
disableHostCheck: true,
hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// publish the new Port, necessary for e2e tests
process.env.PORT = port
// add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
resolve(devWebpackConfig)
}
})
})

View File

@@ -0,0 +1,145 @@
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const env = require('../config/prod.env')
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig

7
client/config/dev.env.js Normal file
View File

@@ -0,0 +1,7 @@
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

69
client/config/index.js Normal file
View File

@@ -0,0 +1,69 @@
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// Various Dev Server settings
host: '0.0.0.0', // can be overwritten by process.env.HOST
port: 8444, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-module-eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
cssSourceMap: true
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
/**
* Source Maps
*/
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
}
}

View File

@@ -0,0 +1,4 @@
'use strict'
module.exports = {
NODE_ENV: '"production"'
}

View File

@@ -1,9 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="icon" href="/api/static/assets/favicon.ico" type="image/ico"/> <link rel="icon" href="/api/static/assets/favicon.ico" type="image/ico"/>
<link rel="shortcut icon" href="/api/static/assets/favicon.ico" type="image/x-icon"/> <link rel="shortcut icon" href="/api/static/assets/favicon.ico" type="image/x-icon"/>
@@ -12,20 +11,14 @@
<link rel="manifest" href="/api/static/assets/manifest.json"> <link rel="manifest" href="/api/static/assets/manifest.json">
<title>Solid Scribe - An easy, encrypted Note App</title> <title>Solid Scribe - An easy, encrypted Note App</title>
<!-- <title><%= htmlWebpackPlugin.options.title %></title> -->
</head> </head>
<body> <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"> <div id="app">
<!-- placeholder data for scrapers with no JS --> <!-- placeholder data for scrapers with no JS -->
<style> <style>
body { body {
background-color: #212221; background-color: #212221;
color: #aeaeae; color: #aeaeae;
height: 100vh;
width: 100%;
} }
.centered { .centered {
position: fixed; position: fixed;

View File

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

25523
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,66 @@
{ {
"name": "solidscribe", "name": "client2",
"version": "0.1.0", "version": "1.0.0",
"description": "client2",
"author": "max",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"build": "vue-cli-service build" "start": "npm run dev",
"build": "node build/build.js"
}, },
"dependencies": { "dependencies": {
"axios": "^1.1.3", "axios": "^0.19.2",
"core-js": "^3.6.5",
"es6-promise": "^4.2.8", "es6-promise": "^4.2.8",
"fomantic-ui-css": "^2.9.0", "fomantic-ui-css": "^2.8.6",
"vue": "^2.6.11", "vue": "^2.5.2",
"vue-chartjs": "^5.0.1", "vue-router": "^3.3.4",
"vue-router": "^3.2.0",
"vuedraggable": "^2.24.3",
"vuex": "^3.4.0" "vuex": "^3.4.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^5.0.8", "autoprefixer": "^7.1.2",
"@vue/cli-plugin-router": "^5.0.8", "babel-core": "^6.22.1",
"@vue/cli-plugin-vuex": "^5.0.8", "babel-helper-vue-jsx-merge-props": "^2.0.3",
"@vue/cli-service": "^5.0.8", "babel-loader": "^7.1.1",
"vue-template-compiler": "^2.6.11" "babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"chalk": "^2.0.1",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.26",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
"not dead" "not ie <= 8"
] ]
} }

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow:

View File

@@ -1,53 +1,9 @@
<template> <template>
<div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode == 2) }"> <div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode == 2) }">
<div class="ui container" v-if="showFakeSite"> <global-site-menu />
<div class="ui basic very padded segment">
<div class="ui inverted red segment">
<h1>WARNING - False site detected</h1>
<h2>The Domain for this website is not correct.</h2>
<h2>Only trust <a class="ui button" href="https://www.solidscribe.com">https://www.solidscribe.com</a></h2>
<h2>Do not any enter any personal information into this website.</h2>
<h2>You will be redirected to the correct domain in {{redirectSeconds}}</h2>
</div>
</div>
</div>
<div class="auth-block" v-if="requireAuth"> <router-view />
<div class="ui raised inverted segment">
<div class="ui centered header">
Authentication Required
</div>
<div class="ui small inverted centered header" v-if="$store.getters.getUsername">
<i class="green user outline icon"></i>
{{ $store.getters.getUsername }}
</div>
<div class="ui large form">
<div class="field">
<div class="ui small inverted header">Password</div>
<div class="ui input">
<input type="password" v-model="password" placeholder="Password">
</div>
</div>
<div class="field">
<div class="ui small inverted header">One Time Password</div>
<div class="ui input">
<input type="password" v-model="otp" placeholder="One Time Password">
</div>
</div>
<div class="ui fluid inverted black button">
<i class="unlock icon"></i>
Submit</div>
</div>
</div>
</div>
<global-site-menu v-if="!showFakeSite" />
<router-view v-if="!showFakeSite" />
<global-notification /> <global-notification />
@@ -60,21 +16,15 @@
import axios from 'axios' import axios from 'axios'
export default { export default {
name: 'App',
components: { components: {
'global-site-menu': require('@/components/GlobalSiteMenu.vue').default, 'global-site-menu': require('@/components/GlobalSiteMenu.vue').default,
'global-notification':require('@/components/GlobalNotificationComponent.vue').default, 'global-notification':require('@/components/GlobalNotificationComponent.vue').default
}, },
data: function(){ data: function(){
return { return {
showFakeSite:false, //Incorrect domain detection // loggedIn:
redirectSeconds: 15,
fetchingInProgress: false, //Prevent start getting token while fetch is in progress fetchingInProgress: false, //Prevent start getting token while fetch is in progress
blockUntilNextRequest: false, //If token was just renewed, don't fetch more until next request blockUntilNextRequest: false //If token was just renewed, don't fetch more until next request
requireAuth: false,
password: '',
otp: '',
} }
}, },
@@ -117,16 +67,6 @@ export default {
return response return response
}, },
(error) => { (error) => {
//Catch all authorization errors, log user out if we encounter one
if(error.response && error.response.status == 401){
this.$router.push('/')
this.$store.commit('destroyLoginToken')
this.$bus.$emit('notification', 'Error: You have been logged out.')
}
return Promise.reject(error) return Promise.reject(error)
} }
) )
@@ -157,12 +97,6 @@ export default {
//Detect if user is on a mobile browser and set a flag in store //Detect if user is on a mobile browser and set a flag in store
this.$store.commit('detectIsUserOnMobile') this.$store.commit('detectIsUserOnMobile')
//Set Main theme color
const accentColor = localStorage.getItem('main-accent')
if(accentColor){
document.documentElement.style.setProperty('--main-accent', accentColor)
}
//Set color theme based on local storage //Set color theme based on local storage
const themeNumber = localStorage.getItem('nightMode') const themeNumber = localStorage.getItem('nightMode')
if(themeNumber != null){ if(themeNumber != null){
@@ -172,17 +106,6 @@ export default {
}, },
mounted: function(){ mounted: function(){
const isDev = process.env['NODE_ENV'] == 'development'
if(window.location.hostname.toLowerCase().replace('www.','') != "solidscribe.com" && !isDev){
this.showFakeSite = true
setInterval(() => {
this.redirectSeconds--
if(this.redirectSeconds == 0){
window.location.href = 'https://www.solidscribe.com'
}
}, 1000)
}
//Update totals for entire app on event //Update totals for entire app on event
this.$io.on('update_counts', () => { this.$io.on('update_counts', () => {
console.log('Got event, update totals') console.log('Got event, update totals')
@@ -200,11 +123,6 @@ export default {
this.blockUntilNextRequest = true this.blockUntilNextRequest = true
}) })
//Track users active sessions
this.$io.on('update_active_user_count', countData => {
this.$store.commit('setActiveSessions', countData)
})
}, },
computed: { computed: {
loggedIn () { loggedIn () {
@@ -213,23 +131,16 @@ export default {
} }
}, },
methods: { methods: {
destroyLoginToken() {
this.$store.commit('destroyLoginToken')
},
loginGateway() { loginGateway() {
if(!this.loggedIn){ if(!this.loggedIn){
console.log('This user is not logged in') console.log('This user is not logged in')
this.$router.push({'path':'/login'}) this.$router.push({'path':'/login'})
return return
} }
}, }
logout() {
this.$router.push('/')
axios.post('/api/user/logout')
setTimeout(() => {
this.$store.commit('destroyLoginToken')
this.$bus.$emit('notification', 'Logged Out')
}, 200)
},
} }
} }
</script> </script>

View File

@@ -4,10 +4,6 @@ const helpers = {}
helpers.timeAgo = (time) => { helpers.timeAgo = (time) => {
if(time == null){
time = Math.round(time/1000)
}
if(time.toString().length >= 13){ if(time.toString().length >= 13){
time = Math.round(time/1000) time = Math.round(time/1000)
} }
@@ -53,7 +49,7 @@ helpers.timeAgo = (time) => {
if (typeof format[2] == 'string') { if (typeof format[2] == 'string') {
return format[list_choice] return format[list_choice]
} else { } 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-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(./roboto-latin.woff2) format('woff2'); src: local('Roboto'), local('Roboto-Regular'), url(/api/static/assets/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; 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 */ /* latin */
@@ -11,26 +11,14 @@
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(./roboto-latin-bold.woff2) format('woff2'); src: local('Roboto Bold'), local('Roboto-Bold'), url(/api/static/assets/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; 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 { :root {
/*main accent for all buttons, icons and logos*/ --main-accent: #16ab39;
--main-accent: #21BA45;
/*theme colors */ /*theme colors */
--body_bg_color: #f5f6f7; --body_bg_color: #f5f6f7;
@@ -50,11 +38,6 @@ body {
html { html {
/*scrollbar-width: none;*/ /*scrollbar-width: none;*/
width: 100%;
height:100%;
padding: 0;
margin: 0;
background: var(--body_bg_color);
} }
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
@@ -63,15 +46,6 @@ div.ui.basic.segment.no-fluf-segment {
margin-top: 0px; margin-top: 0px;
} }
.page-container {
/*width: 100%;*/
display: block;
margin: 0;
padding: 0.5rem;
box-sizing: border-box;
overflow: hidden;
}
/* Night mode modifiers */ /* Night mode modifiers */
/*Make images sepia in night mode */ /*Make images sepia in night mode */
@@ -92,12 +66,9 @@ div.ui.basic.segment.no-fluf-segment {
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/ /* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
body { body {
color: var(--text_color); color: var(--text_color);
background: none; background-color: var(--body_bg_color);
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif; font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
} }
#app {
/* background: var(--body_bg_color);*/
}
.ui.segment { .ui.segment {
color: var(--text_color); color: var(--text_color);
@@ -114,8 +85,6 @@ body {
text-align: center; text-align: center;
} }
.ui.form input:not([type]), .ui.form input:not([type]),
.ui.form input:not([type]):focus, .ui.form input:not([type]):focus,
.ui.form textarea:not([type]), .ui.form textarea:not([type]),
@@ -124,32 +93,11 @@ body {
background-color: var(--small_element_bg_color); background-color: var(--small_element_bg_color);
border-color: var(--dark_border_color); border-color: var(--dark_border_color);
} }
.ui.form input[type="password"],
.ui.form input[type="text"],
.ui.input > input {
color: var(--text_color);
background-color: var(--small_element_bg_color);
border-color: var(--dark_border_color);
}
.ui.form input[type="password"]:focus, .ui.form input[type="password"]:active,
.ui.form input[type="text"]:focus, .ui.form input[type="text"]:active,
.ui.input > input:focus, .ui.input > input:active {
color: var(--text_color);
background-color: var(--small_element_bg_color);
border-color: var(--main-accent);
border-right-color: var(--main-accent) !important;
}
.ui.basic.label, .ui.header, .ui.header div.sub.header { .ui.basic.label, .ui.header, .ui.header div.sub.header {
color: var(--text_color); color: var(--text_color);
background-color: transparent; background-color: transparent;
border-color: var(--dark_border_color); border-color: var(--dark_border_color);
} }
.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 { .ui.icon.input > i.icon {
color: var(--text_color); color: var(--text_color);
} }
@@ -157,7 +105,7 @@ div.ui.basic.green.label {
background-color: var(--small_element_bg_color) !important; background-color: var(--small_element_bg_color) !important;
} }
.ui.basic.button, .ui.basic.buttons .button { .ui.basic.button, .ui.basic.buttons .button {
background-color: var(--small_element_bg_color); background-color: var(--small_element_bg_color) !important;
color: var(--text_color) !important; color: var(--text_color) !important;
border: 1px solid; border: 1px solid;
border-color: var(--dark_border_color) !important; border-color: var(--dark_border_color) !important;
@@ -177,39 +125,6 @@ div.ui.basic.green.label {
color: var(--text_color) !important; color: var(--text_color) !important;
border-color: var(--dark_border_color) !important; border-color: var(--dark_border_color) !important;
} }
/*Overwrites for modifiable theme color */
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);
}
.ui.basic.green.button, .ui.basic.green.buttons .button:hover, .ui.basic.green.button:hover, .ui.basic.green.button:focus {
box-shadow: var(--main-accent) 0px 0px 0px 1px inset;
}
.ui.green.labels .label, .ui.ui.ui.green.label {
background-color: var(--main-accent);
border-color: var(--main-accent);
}
.ui.grid > .green.row, .ui.grid > .green.column, .ui.grid > .row > .green.column {
background-color: var(--main-accent);
}
.ui.green.header {
color: var(--main-accent);
}
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/ /* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
/*// /*//
@@ -300,41 +215,27 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
z-index: 100; z-index: 100;
cursor: pointer; cursor: pointer;
} }
.text-container {
max-width: 1000px;
display: block;
margin-left: auto;
margin-right: auto;
background-color: var(--small_element_bg_color) !important;
}
/* squire text styles */ /* squire text styles */
.squire-box { .squire-box {
border: none; border: none;
/*height: calc(100% - 69px);*/ /*height: calc(100% - 69px);*/
min-height: 300px; min-height: 500px;
background-color: var(--small_element_bg_color); background-color: rgba(255,200,0,0.0);
/*margin-bottom: 15px;*/ /*margin-bottom: 15px;*/
box-sizing: border-box; box-sizing: border-box;
padding: 10px 15px 10px; padding: 10px 15px 10px;
/*background: transparent;*/ /*background: transparent;*/
overflow: hidden; overflow-x: scroll;
font-size: 1.2em; font-size: 1.2em;
line-height: 1.8em; line-height: 1.8em;
word-wrap: break-word; word-wrap: break-word;
/*border-bottom: 1px solid #ccc;*/ /*border-bottom: 1px solid #ccc;*/
scrollbar-width: none; scrollbar-width: none;
scrollbar-color: transparent transparent; scrollbar-color: transparent transparent;
caret-color: var(--main-accent); caret-color: #21BA45;
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::selection,
.squire-box::-moz-selection { .squire-box::-moz-selection {
@@ -352,18 +253,13 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
cursor: pointer; cursor: pointer;
} }
.night-mode .note-card-text i:not(.icon), .night-mode .note-card-text i:not(.icon),
.night-mode .squire-box i:not(.icon) { .night-mode .squire-box i {
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.2);
} }
.note-card-text code,
.squire-box code,
.note-card-text pre, .note-card-text pre,
.squire-box pre { .squire-box pre {
/*word-wrap: break-word;*/ word-wrap: break-word;
display: inline-block;
border-left: 2px solid var(--main-accent);
padding-left: 15px;
} }
.note-card-text p, .note-card-text p,
.squire-box p { .squire-box p {
@@ -375,10 +271,6 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
margin: 0; margin: 0;
padding: 0 0 0 2.5em; padding: 0 0 0 2.5em;
} }
.note-card-text u,
.squire-box u {
text-decoration-color: var(--main-accent);
}
.note-card-text img { .note-card-text img {
max-width:100%; max-width:100%;
height: auto; height: auto;
@@ -394,40 +286,6 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
.squire-box li > p { .squire-box li > p {
margin-bottom: 0; margin-bottom: 0;
} }
.note-card-text ol,
.squire-box ol,
.note-card-text ul,
.squire-box ul {
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, .note-card-text ul > li,
.squire-box ul > li { .squire-box ul > li {
@@ -437,124 +295,43 @@ padding-right: 10px;
.note-card-text ul > li:before, .note-card-text ul > li:before,
.squire-box ul > li:before { .squire-box ul > li:before {
/*filled circle */
content: "\f111"; content: "\f111";
/*empty square*/
/*content: "\F0C8";*/
font-family: 'Icons'; font-family: 'Icons';
/*font-family: 'outline-icons';*/
backface-visibility: hidden; backface-visibility: hidden;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
text-decoration: inherit; text-decoration: inherit;
text-align: center; text-align: center;
font-size: 0.25em; line-height: 1.4em;
font-size: 0.75em;
height: 100%; height: 17px;
width: 20px; width: 17px;
display: inline-block; display: inline-block;
position: absolute; position: absolute;
left: -25px; left: -30px;
/*border: 2px solid #444;*/ /*border: 2px solid #444;*/
/*border-radius: 4px;*/ /*border-radius: 4px;*/
bottom: 0; bottom: 0;
top: 0; top: 4px;
cursor: pointer; cursor: pointer;
opacity: 0.7; opacity: 0.7;
color: var(--text_color);
text-align: center;
} }
/* filled in check circle */
ul > li.active:before { ul > li.active:before {
font-family: 'Icons'; font-family: 'Icons';
content: "\F14A"; content: "\f058";
color: var(--main-accent); color: #21BA45;
opacity: 1; opacity: 1;
font-size: 1em;
} }
/* hover - transparent icon */
.squire-box ul > li:hover:not(.active):before {
font-family: 'outline-icons';
content: "\f14a";
opacity: 0.4;
font-size: 1em;
}
.note-title-display-card .divide,
.squire-box .divide {
width: 100%;
display: inline-block;
height: 2px;
background-color: var(--main-accent);
}
table {
width: 100%;
border-collapse: collapse;
}
tr {
display: flex;
}
th, td {
border: 1px solid #ddd;
border-bottom: 1px solid #ddd;
font-weight: normal;
flex: 1;
}
/* table:hover th, table:hover td {
border: 1px solid black;
}*/
th, td {
padding: 3px;
text-align: left;
}
.table-tic-table {
}
.table-tic-table > div {
height: 21px;
margin: 0;
padding: 0;
}
.tabletic {
display: inline-block;
border: 1px solid black;
border-radius: 2px;
width: 20px;
height: 20px;
margin: 0 1px 1px 0;
cursor: pointer;
}
.t-table {
width: 100%;
display: inline-block;
border: 1px solid black;
}
.t-table > span,
.t-table > div {
display: flex; /* aligns all child elements (flex items) in a row */
}
.t-table > span > span,
.t-table > div > div {
flex: 1; /* distributes space on the line equally among items */
border: 1px solid #DDD;
}
/* adjust checkboxes for mobile. Make them a little bigger, easier to click */ /* adjust checkboxes for mobile. Make them a little bigger, easier to click */
@media only screen and (max-width: 740px) { @media only screen and (max-width: 740px) {
.squire-box {
min-height: calc(100vh - 120px);
}
.ui.button.shrinking { .ui.button.shrinking {
font-size: 0.85714286rem; font-size: 0.85714286rem;
margin: 0 3px; margin: 0 3px;
@@ -567,40 +344,30 @@ padding-right: 10px;
min-height: 30px; min-height: 30px;
} }
/*empty check box*/
.note-card-text ul > li:before, .note-card-text ul > li:before,
.squire-box ul > li:before, .squire-box ul > li:before {
.squire-box ul > li:hover:not(.active):before {
/*empty checkmark*/ content: "\f111";
/*font-family: 'Icons';*/ font-family: outline-icons;
/*content: "\f058";*/
content: "\F0C8"; height: 24px;
font-family: 'outline-icons'; width: 24px;
left: -40px; left: -40px;
bottom: 0;
top: 0px;
cursor: pointer;
line-height: 0.9em;
font-size: 1.4em; font-size: 1.4em;
opacity: 0.2;
} }
/*Filled check box */
ul > li.active:before { ul > li.active:before {
font-family: 'Icons'; font-family: 'Icons';
content: "\F14A"; content: "\f058";
color: #21BA45;
color: var(--main-accent);
opacity: 1; 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;
}
} }
@@ -630,10 +397,6 @@ padding-right: 10px;
.ui.white.button { .ui.white.button {
background: #FFF; background: #FFF;
} }
.white.row {
background-color: rgba(255, 255, 255, 0.9);
}
.input-floating-button { .input-floating-button {
position: absolute; position: absolute;
top: 19px; top: 19px;
@@ -645,27 +408,6 @@ padding-right: 10px;
animation: fade-in-fwd 0.8s both; animation: fade-in-fwd 0.8s both;
} }
/* div that comes up, blocking interaction annd requiring authentication */
.auth-block {
background-color: rgba(0,0,0,0.9);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100vh;
width: 100%;
z-index: 200;
}
.auth-block > div {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -40%);
width: 300px;
}
/** /**
* ---------------------------------------- * ----------------------------------------
* animation fade-in-fwd * animation fade-in-fwd
@@ -694,34 +436,36 @@ padding-right: 10px;
position: absolute; position: absolute;
content: ''; content: '';
font-size: 1rem; font-size: 1rem;
width: 10px; width: 0.71428571em;
height: 10px; height: 0.71428571em;
background: #1B1C1D; background: #FFFFFF;
-webkit-transform: rotate(45deg); -webkit-transform: rotate(45deg);
transform: rotate(45deg); transform: rotate(45deg);
z-index: 1901; z-index: 1901;
-webkit-box-shadow: 1px 1px 0 0 #bababc;
box-shadow: 1px 1px 0 0 #bababc;
} }
/* Popup */ /* Popup */
[data-tooltip]:after { [data-tooltip]:after {
min-width: 40px;
pointer-events: none; pointer-events: none;
content: attr(data-tooltip); content: attr(data-tooltip);
position: absolute; position: absolute;
text-transform: none; text-transform: none;
text-align: center; text-align: left;
white-space: pre; white-space: nowrap;
font-size: 1rem; font-size: 1rem;
border: 1px solid #D4D4D5; border: 1px solid #D4D4D5;
line-height: 1.4285em; line-height: 1.4285em;
max-width: none; max-width: none;
background: #1B1C1D; background: #FFFFFF;
padding: 0.5em; padding: 0.833em 1em;
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
/*color: var(--main-accent);*/ color: rgba(0, 0, 0, 0.87);
color: white;
border-radius: 0.28571429rem; border-radius: 0.28571429rem;
-webkit-box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
z-index: 1900; z-index: 1900;
} }
@@ -731,7 +475,7 @@ padding-right: 10px;
right: auto; right: auto;
bottom: 100%; bottom: 100%;
left: 50%; left: 50%;
background: #1B1C1D; background: #FFFFFF;
margin-left: -0.07142857rem; margin-left: -0.07142857rem;
margin-bottom: 0.14285714rem; margin-bottom: 0.14285714rem;
} }
@@ -749,7 +493,7 @@ padding-right: 10px;
pointer-events: none; pointer-events: none;
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
/*transition: opacity 0.2s ease;*/ transition: opacity 0.2s ease;
} }
[data-tooltip]:before { [data-tooltip]:before {
-webkit-transform: rotate(45deg) scale(0) !important; -webkit-transform: rotate(45deg) scale(0) !important;
@@ -772,13 +516,93 @@ padding-right: 10px;
transform: rotate(45deg) scale(1) !important; transform: rotate(45deg) scale(1) !important;
} }
/* Animation Position */
[data-tooltip]:after,
[data-tooltip][data-position="top center"]:after,
[data-tooltip][data-position="bottom center"]:after {
-webkit-transform: translateX(-50%) scale(0) !important;
transform: translateX(-50%) scale(0) !important;
}
[data-tooltip]:hover:after,
[data-tooltip][data-position="bottom center"]:hover:after {
-webkit-transform: translateX(-50%) scale(1) !important;
transform: translateX(-50%) scale(1) !important;
}
[data-tooltip][data-position="left center"]:after,
[data-tooltip][data-position="right center"]:after {
-webkit-transform: translateY(-50%) scale(0) !important;
transform: translateY(-50%) scale(0) !important;
}
[data-tooltip][data-position="left center"]:hover:after,
[data-tooltip][data-position="right center"]:hover:after {
-webkit-transform: translateY(-50%) scale(1) !important;
transform: translateY(-50%) scale(1) !important;
}
[data-tooltip][data-position="top left"]:after,
[data-tooltip][data-position="top right"]:after,
[data-tooltip][data-position="bottom left"]:after,
[data-tooltip][data-position="bottom right"]:after {
-webkit-transform: scale(0) !important;
transform: scale(0) !important;
}
[data-tooltip][data-position="top left"]:hover:after,
[data-tooltip][data-position="top right"]:hover:after,
[data-tooltip][data-position="bottom left"]:hover:after,
[data-tooltip][data-position="bottom right"]:hover:after {
-webkit-transform: scale(1) !important;
transform: scale(1) !important;
}
[data-tooltip][data-variation~="fixed"]:after {
white-space: normal;
width: 250px;
}
[data-tooltip][data-variation*="wide fixed"]:after {
width: 350px;
}
[data-tooltip][data-variation*="very wide fixed"]:after {
width: 550px;
}
@media only screen and (max-width: 767.98px) {
[data-tooltip][data-variation~="fixed"]:after {
width: 250px;
}
}
/*--------------
Inverted
---------------*/
/* Arrow */
[data-tooltip][data-inverted]:before {
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
/* Arrow Position */
[data-tooltip][data-inverted]:before {
background: #1B1C1D;
}
/* Popup */
[data-tooltip][data-inverted]:after {
background: #1B1C1D;
color: #FFFFFF;
border: none;
-webkit-box-shadow: none;
box-shadow: none;
}
[data-tooltip][data-inverted]:after .header {
background: none;
color: #FFFFFF;
}
/*-------------- /*--------------
Position Position
---------------*/ ---------------*/
[data-position~="top"][data-tooltip]:before { [data-position~="top"][data-tooltip]:before {
background: #1B1C1D; background: #FFFFFF;
} }
/* Top Center */ /* Top Center */
@@ -796,7 +620,7 @@ padding-right: 10px;
right: auto; right: auto;
bottom: 100%; bottom: 100%;
left: 50%; left: 50%;
background: #1B1C1D; background: #FFFFFF;
margin-left: -0.07142857rem; margin-left: -0.07142857rem;
margin-bottom: 0.14285714rem; margin-bottom: 0.14285714rem;
} }
@@ -835,7 +659,7 @@ padding-right: 10px;
margin-bottom: 0.14285714rem; margin-bottom: 0.14285714rem;
} }
[data-position~="bottom"][data-tooltip]:before { [data-position~="bottom"][data-tooltip]:before {
background: #1B1C1D; background: #FFFFFF;
-webkit-box-shadow: -1px -1px 0 0 #bababc; -webkit-box-shadow: -1px -1px 0 0 #bababc;
box-shadow: -1px -1px 0 0 #bababc; box-shadow: -1px -1px 0 0 #bababc;
} }
@@ -854,7 +678,7 @@ padding-right: 10px;
bottom: auto; bottom: auto;
right: auto; right: auto;
top: 100%; top: 100%;
left: 30%; left: 50%;
margin-left: -0.07142857rem; margin-left: -0.07142857rem;
margin-top: 0.14285714rem; margin-top: 0.14285714rem;
} }
@@ -902,7 +726,9 @@ padding-right: 10px;
top: 50%; top: 50%;
margin-top: -0.14285714rem; margin-top: -0.14285714rem;
margin-right: -0.07142857rem; margin-right: -0.07142857rem;
background: #1B1C1D; background: #FFFFFF;
-webkit-box-shadow: 1px -1px 0 0 #bababc;
box-shadow: 1px -1px 0 0 #bababc;
} }
/* Right Center */ /* Right Center */
@@ -918,9 +744,30 @@ padding-right: 10px;
top: 50%; top: 50%;
margin-top: -0.07142857rem; margin-top: -0.07142857rem;
margin-left: 0.14285714rem; margin-left: 0.14285714rem;
background: #1B1C1D; background: #FFFFFF;
-webkit-box-shadow: -1px 1px 0 0 #bababc;
box-shadow: -1px 1px 0 0 #bababc;
} }
/* Inverted Arrow Color */
[data-inverted][data-position~="bottom"][data-tooltip]:before {
background: #1B1C1D;
-webkit-box-shadow: -1px -1px 0 0 #bababc;
box-shadow: -1px -1px 0 0 #bababc;
}
[data-inverted][data-position="left center"][data-tooltip]:before {
background: #1B1C1D;
-webkit-box-shadow: 1px -1px 0 0 #bababc;
box-shadow: 1px -1px 0 0 #bababc;
}
[data-inverted][data-position="right center"][data-tooltip]:before {
background: #1B1C1D;
-webkit-box-shadow: -1px 1px 0 0 #bababc;
box-shadow: -1px 1px 0 0 #bababc;
}
[data-inverted][data-position~="top"][data-tooltip]:before {
background: #1B1C1D;
}
[data-position~="bottom"][data-tooltip]:before { [data-position~="bottom"][data-tooltip]:before {
-webkit-transform-origin: center bottom; -webkit-transform-origin: center bottom;
transform-origin: center bottom; transform-origin: center bottom;
@@ -945,59 +792,11 @@ padding-right: 10px;
-webkit-transform-origin: left center; -webkit-transform-origin: left center;
transform-origin: left center; transform-origin: left center;
} }
@media only screen and (max-width: 740px) {
/*hide tooltips on mobile*/ /*--------------
[data-tooltip]:hover:before, Basic
[data-tooltip]:hover:after { ---------------*/
visibility: visible;
opacity: 0; [data-tooltip][data-variation~="basic"]:before {
} display: none;
}
.glint:after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
opacity: 0;
pointer-events: none;
z-index: 1;
background: linear-gradient(
130deg,
rgba(255,255,255,0) 45%,
rgba(255,255,255,1) 50%,
var(--main-accent) 55%,
rgba(255,255,255,0) 60%
);
animation: glint-animation 0.8s linear 1;
animation-delay: 0.9s;
}
@keyframes glint-animation {
0% {
left: -100%;
opacity: 1;
}
100% {
left: 100%;
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

@@ -460,6 +460,7 @@ function fixContainer ( container, root ) {
var doc = container.ownerDocument; var doc = container.ownerDocument;
var wrapper = null; var wrapper = null;
var i, l, child, isBR; var i, l, child, isBR;
var config = root.__squire__._config;
for ( i = 0, l = children.length; i < l; i += 1 ) { for ( i = 0, l = children.length; i < l; i += 1 ) {
child = children[i]; child = children[i];
@@ -1095,9 +1096,6 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
} }
while ( true ) { while ( true ) {
if ( endContainer === endMax || endContainer === root ) {
break;
}
if ( maySkipBR && if ( maySkipBR &&
endContainer.nodeType !== TEXT_NODE && endContainer.nodeType !== TEXT_NODE &&
endContainer.childNodes[ endOffset ] && endContainer.childNodes[ endOffset ] &&
@@ -1105,7 +1103,9 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
endOffset += 1; endOffset += 1;
maySkipBR = false; maySkipBR = false;
} }
if ( endOffset !== getLength( endContainer ) ) { if ( endContainer === endMax ||
endContainer === root ||
endOffset !== getLength( endContainer ) ) {
break; break;
} }
parent = endContainer.parentNode; parent = endContainer.parentNode;
@@ -1117,20 +1117,6 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
range.setEnd( endContainer, endOffset ); range.setEnd( endContainer, endOffset );
}; };
var moveRangeBoundaryOutOf = function ( range, nodeName, root ) {
var parent = getNearest( range.endContainer, root, 'A' );
if ( parent ) {
var clone = range.cloneRange();
parent = parent.parentNode;
moveRangeBoundariesUpTree( clone, parent, parent, root );
if ( clone.endContainer === parent ) {
range.setStart( clone.endContainer, clone.endOffset );
range.setEnd( clone.endContainer, clone.endOffset );
}
}
return range;
};
// Returns the first block at least partially contained by the range, // Returns the first block at least partially contained by the range,
// or null if no block is contained by the range. // or null if no block is contained by the range.
var getStartBlockOfRange = function ( range, root ) { var getStartBlockOfRange = function ( range, root ) {
@@ -1265,9 +1251,7 @@ var keys = {
37: 'left', 37: 'left',
39: 'right', 39: 'right',
46: 'delete', 46: 'delete',
191: '/',
219: '[', 219: '[',
220: '\\',
221: ']' 221: ']'
}; };
@@ -1301,13 +1285,10 @@ var onKey = function ( event ) {
if ( event.altKey ) { modifiers += 'alt-'; } if ( event.altKey ) { modifiers += 'alt-'; }
if ( event.ctrlKey ) { modifiers += 'ctrl-'; } if ( event.ctrlKey ) { modifiers += 'ctrl-'; }
if ( event.metaKey ) { modifiers += 'meta-'; } if ( event.metaKey ) { modifiers += 'meta-'; }
if ( event.shiftKey ) { modifiers += 'shift-'; }
} }
// However, on Windows, shift-delete is apparently "cut" (WTF right?), so // However, on Windows, shift-delete is apparently "cut" (WTF right?), so
// we want to let the browser handle shift-delete in this situation. // we want to let the browser handle shift-delete.
if ( isWin && event.shiftKey && key === 'delete' ) { if ( event.shiftKey ) { modifiers += 'shift-'; }
modifiers += 'shift-';
}
key = modifiers + key; key = modifiers + key;
@@ -1484,7 +1465,12 @@ var handleEnter = function ( self, shiftKey, range ) {
// just play it safe and insert a <br>. // just play it safe and insert a <br>.
if ( !block || shiftKey || /^T[HD]$/.test( block.nodeName ) ) { if ( !block || shiftKey || /^T[HD]$/.test( block.nodeName ) ) {
// If inside an <a>, move focus out // If inside an <a>, move focus out
moveRangeBoundaryOutOf( range, 'A', root ); parent = getNearest( range.endContainer, root, 'A' );
if ( parent ) {
parent = parent.parentNode;
moveRangeBoundariesUpTree( range, parent, parent, root );
range.collapse( false );
}
insertNodeInRange( range, self.createElement( 'BR' ) ); insertNodeInRange( range, self.createElement( 'BR' ) );
range.collapse( false ); range.collapse( false );
self.setSelection( range ); self.setSelection( range );
@@ -1763,7 +1749,7 @@ var keyHandlers = {
} }
}, },
space: function ( self, _, range ) { space: function ( self, _, range ) {
var node; var node, parent;
var root = self._root; var root = self._root;
self._recordUndoState( range ); self._recordUndoState( range );
if ( self._config.addLinks ) { if ( self._config.addLinks ) {
@@ -1835,45 +1821,16 @@ if ( !isMac ) {
}; };
} }
const changeIndentationLevel = function ( methodIfInQuote, methodIfInList ) {
return function ( self, event ) {
event.preventDefault();
var path = self.getPath();
if ( /(?:^|>)BLOCKQUOTE/.test( path ) ||
!/(?:^|>)[OU]L/.test( path ) ) {
self[ methodIfInQuote ]();
} else {
self[ methodIfInList ]();
}
};
};
const toggleList = function ( listRegex, methodIfNotInList ) {
return function ( self, event ) {
event.preventDefault();
var path = self.getPath();
if ( !listRegex.test( path ) ) {
self[ methodIfNotInList ]();
} else {
self.removeList();
}
};
};
keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat( 'B' ); keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat( 'B' );
keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' ); keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' );
keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' ); keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' );
keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat( 'S' ); keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat( 'S' );
keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat( 'SUB', { tag: 'SUP' } ); keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat( 'SUB', { tag: 'SUP' } );
keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat( 'SUP', { tag: 'SUB' } ); keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat( 'SUP', { tag: 'SUB' } );
keyHandlers[ ctrlKey + 'shift-8' ] = keyHandlers[ ctrlKey + 'shift-8' ] = mapKeyTo( 'makeUnorderedList' );
toggleList( /(?:^|>)UL/, 'makeUnorderedList' ); keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' );
keyHandlers[ ctrlKey + 'shift-9' ] = keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' );
toggleList( /(?:^|>)OL/, 'makeOrderedList' ); keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' );
keyHandlers[ ctrlKey + '[' ] =
changeIndentationLevel( 'decreaseQuoteLevel', 'decreaseListLevel' );
keyHandlers[ ctrlKey + ']' ] =
changeIndentationLevel( 'increaseQuoteLevel', 'increaseListLevel' );
keyHandlers[ ctrlKey + 'd' ] = mapKeyTo( 'toggleCode' ); keyHandlers[ ctrlKey + 'd' ] = mapKeyTo( 'toggleCode' );
keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' ); keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' ); keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
@@ -2117,7 +2074,7 @@ var cleanTree = function cleanTree ( node, config, preserveWS ) {
break; break;
} }
} }
data = data.replace( /^[ \r\n]+/g, sibling ? ' ' : '' ); data = data.replace( /^[ \t\r\n]+/g, sibling ? ' ' : '' );
} }
if ( endsWithWS ) { if ( endsWithWS ) {
walker.currentNode = child; walker.currentNode = child;
@@ -2132,7 +2089,7 @@ var cleanTree = function cleanTree ( node, config, preserveWS ) {
break; break;
} }
} }
data = data.replace( /[ \r\n]+$/g, sibling ? ' ' : '' ); data = data.replace( /[ \t\r\n]+$/g, sibling ? ' ' : '' );
} }
if ( data ) { if ( data ) {
child.data = data; child.data = data;
@@ -2226,35 +2183,26 @@ var cleanupBRs = function ( node, root, keepForBlankLine ) {
// The (non-standard but supported enough) innerText property is based on the // The (non-standard but supported enough) innerText property is based on the
// render tree in Firefox and possibly other browsers, so we must insert the // render tree in Firefox and possibly other browsers, so we must insert the
// DOM node into the document to ensure the text part is correct. // DOM node into the document to ensure the text part is correct.
var setClipboardData = var setClipboardData = function ( clipboardData, node, root, config ) {
function ( event, contents, root, willCutCopy, toPlainText, plainTextOnly ) { var body = node.ownerDocument.body;
var clipboardData = event.clipboardData; var willCutCopy = config.willCutCopy;
var doc = event.target.ownerDocument;
var body = doc.body;
var node = createElement( doc, 'div' );
var html, text; var html, text;
node.appendChild( contents ); // Firefox will add an extra new line for BRs at the end of block when
// calculating innerText, even though they don't actually affect display.
// So we need to remove them first.
cleanupBRs( node, root, true );
node.setAttribute( 'style',
'position:fixed;overflow:hidden;bottom:100%;right:100%;' );
body.appendChild( node );
html = node.innerHTML; html = node.innerHTML;
text = node.innerText || node.textContent;
if ( willCutCopy ) { if ( willCutCopy ) {
html = willCutCopy( html ); html = willCutCopy( html );
} }
if ( toPlainText ) {
text = toPlainText( html );
} else {
// Firefox will add an extra new line for BRs at the end of block when
// calculating innerText, even though they don't actually affect
// display, so we need to remove them first.
cleanupBRs( node, root, true );
node.setAttribute( 'style',
'position:fixed;overflow:hidden;bottom:100%;right:100%;' );
body.appendChild( node );
text = node.innerText || node.textContent;
text = text.replace( / /g, ' ' ); // Replace nbsp with regular space
body.removeChild( node );
}
// Firefox (and others?) returns unix line endings (\n) even on Windows. // Firefox (and others?) returns unix line endings (\n) even on Windows.
// If on Windows, normalise to \r\n, since Notepad and some other crappy // If on Windows, normalise to \r\n, since Notepad and some other crappy
// apps do not understand just \n. // apps do not understand just \n.
@@ -2262,18 +2210,18 @@ var setClipboardData =
text = text.replace( /\r?\n/g, '\r\n' ); text = text.replace( /\r?\n/g, '\r\n' );
} }
if ( !plainTextOnly && text !== html ) { clipboardData.setData( 'text/html', html );
clipboardData.setData( 'text/html', html );
}
clipboardData.setData( 'text/plain', text ); clipboardData.setData( 'text/plain', text );
event.preventDefault();
body.removeChild( node );
}; };
var onCut = function ( event ) { var onCut = function ( event ) {
var clipboardData = event.clipboardData;
var range = this.getSelection(); var range = this.getSelection();
var root = this._root; var root = this._root;
var self = this; var self = this;
var startBlock, endBlock, copyRoot, contents, parent, newContents; var startBlock, endBlock, copyRoot, contents, parent, newContents, node;
// Nothing to do // Nothing to do
if ( range.collapsed ) { if ( range.collapsed ) {
@@ -2285,7 +2233,7 @@ var onCut = function ( event ) {
this.saveUndoState( range ); this.saveUndoState( range );
// Edge only seems to support setting plain text as of 2016-03-11. // Edge only seems to support setting plain text as of 2016-03-11.
if ( !isEdge && event.clipboardData ) { if ( !isEdge && clipboardData ) {
// Clipboard content should include all parents within block, or all // Clipboard content should include all parents within block, or all
// parents up to root if selection across blocks // parents up to root if selection across blocks
startBlock = getStartBlockOfRange( range, root ); startBlock = getStartBlockOfRange( range, root );
@@ -2305,8 +2253,10 @@ var onCut = function ( event ) {
parent = parent.parentNode; parent = parent.parentNode;
} }
// Set clipboard data // Set clipboard data
setClipboardData( node = this.createElement( 'div' );
event, contents, root, this._config.willCutCopy, null, false ); node.appendChild( contents );
setClipboardData( clipboardData, node, root, this._config );
event.preventDefault();
} else { } else {
setTimeout( function () { setTimeout( function () {
try { try {
@@ -2321,10 +2271,14 @@ var onCut = function ( event ) {
this.setSelection( range ); this.setSelection( range );
}; };
var _onCopy = function ( event, range, root, willCutCopy, toPlainText, plainTextOnly ) { var onCopy = function ( event ) {
var startBlock, endBlock, copyRoot, contents, parent, newContents; var clipboardData = event.clipboardData;
var range = this.getSelection();
var root = this._root;
var startBlock, endBlock, copyRoot, contents, parent, newContents, node;
// Edge only seems to support setting plain text as of 2016-03-11. // Edge only seems to support setting plain text as of 2016-03-11.
if ( !isEdge && event.clipboardData ) { if ( !isEdge && clipboardData ) {
// Clipboard content should include all parents within block, or all // Clipboard content should include all parents within block, or all
// parents up to root if selection across blocks // parents up to root if selection across blocks
startBlock = getStartBlockOfRange( range, root ); startBlock = getStartBlockOfRange( range, root );
@@ -2349,21 +2303,13 @@ var _onCopy = function ( event, range, root, willCutCopy, toPlainText, plainText
parent = parent.parentNode; parent = parent.parentNode;
} }
// Set clipboard data // Set clipboard data
setClipboardData( event, contents, root, willCutCopy, toPlainText, plainTextOnly ); node = this.createElement( 'div' );
node.appendChild( contents );
setClipboardData( clipboardData, node, root, this._config );
event.preventDefault();
} }
}; };
var onCopy = function ( event ) {
_onCopy(
event,
this.getSelection(),
this._root,
this._config.willCutCopy,
null,
false
);
};
// Need to monitor for shift key like this, as event.shiftKey is not available // Need to monitor for shift key like this, as event.shiftKey is not available
// in paste event. // in paste event.
function monitorShiftKey ( event ) { function monitorShiftKey ( event ) {
@@ -2693,8 +2639,7 @@ var sanitizeToDOMFragment = function ( html, isPaste, self ) {
ALLOW_UNKNOWN_PROTOCOLS: true, ALLOW_UNKNOWN_PROTOCOLS: true,
WHOLE_DOCUMENT: false, WHOLE_DOCUMENT: false,
RETURN_DOM: true, RETURN_DOM: true,
RETURN_DOM_FRAGMENT: true, RETURN_DOM_FRAGMENT: true
FORCE_BODY: false
}) : null; }) : null;
return frag ? doc.importNode( frag, true ) : doc.createDocumentFragment(); return frag ? doc.importNode( frag, true ) : doc.createDocumentFragment();
}; };
@@ -2978,6 +2923,16 @@ proto.setSelection = function ( range ) {
// needing restore on focus. // needing restore on focus.
if ( !this._isFocused ) { if ( !this._isFocused ) {
enableRestoreSelection.call( this ); enableRestoreSelection.call( this );
} else if ( isAndroid && !this._restoreSelection ) {
// Android closes the keyboard on removeAllRanges() and doesn't
// open it again when addRange() is called, sigh.
// Since Android doesn't trigger a focus event in setSelection(),
// use a blur/focus dance to work around this by letting the
// selection be restored on focus.
// Need to check for !this._restoreSelection to avoid infinite loop
enableRestoreSelection.call( this );
this.blur();
this.focus();
} else { } else {
// iOS bug: if you don't focus the iframe before setting the // iOS bug: if you don't focus the iframe before setting the
// selection, you can end up in a state where you type but the input // selection, you can end up in a state where you type but the input
@@ -2987,15 +2942,7 @@ proto.setSelection = function ( range ) {
this._win.focus(); this._win.focus();
} }
var sel = getWindowSelection( this ); var sel = getWindowSelection( this );
if ( sel && sel.setBaseAndExtent ) { if ( sel ) {
sel.setBaseAndExtent(
range.startContainer,
range.startOffset,
range.endContainer,
range.endOffset,
);
} else if ( sel ) {
// This is just for IE11
sel.removeAllRanges(); sel.removeAllRanges();
sel.addRange( range ); sel.addRange( range );
} }
@@ -3171,7 +3118,7 @@ proto._updatePath = function ( range, force ) {
// selectionchange is fired synchronously in IE when removing current selection // selectionchange is fired synchronously in IE when removing current selection
// and when setting new selection; keyup/mouseup may have processing we want // and when setting new selection; keyup/mouseup may have processing we want
// to do first. Either way, send to next event loop. // to do first. Either way, send to next event loop.
proto._updatePathOnEvent = function () { proto._updatePathOnEvent = function ( event ) {
var self = this; var self = this;
if ( self._isFocused && !self._willUpdatePath ) { if ( self._isFocused && !self._willUpdatePath ) {
self._willUpdatePath = true; self._willUpdatePath = true;
@@ -3891,9 +3838,10 @@ var increaseBlockQuoteLevel = function ( frag ) {
}; };
var decreaseBlockQuoteLevel = function ( frag ) { var decreaseBlockQuoteLevel = function ( frag ) {
var root = this._root;
var blockquotes = frag.querySelectorAll( 'blockquote' ); var blockquotes = frag.querySelectorAll( 'blockquote' );
Array.prototype.filter.call( blockquotes, function ( el ) { Array.prototype.filter.call( blockquotes, function ( el ) {
return !getNearest( el.parentNode, frag, 'BLOCKQUOTE' ); return !getNearest( el.parentNode, root, 'BLOCKQUOTE' );
}).forEach( function ( el ) { }).forEach( function ( el ) {
replaceWith( el, empty( el ) ); replaceWith( el, empty( el ) );
}); });
@@ -4174,14 +4122,7 @@ proto._getHTML = function () {
proto._setHTML = function ( html ) { proto._setHTML = function ( html ) {
var root = this._root; var root = this._root;
var node = root; var node = root;
var sanitizeToDOMFragment = this._config.sanitizeToDOMFragment; node.innerHTML = html;
if ( typeof sanitizeToDOMFragment === 'function' ) {
var frag = sanitizeToDOMFragment( html, false, this );
empty( node );
node.appendChild( frag );
} else {
node.innerHTML = html;
}
do { do {
fixCursor( node, root ); fixCursor( node, root );
} while ( node = getNextBlock( node, root ) ); } while ( node = getNextBlock( node, root ) );
@@ -4189,7 +4130,8 @@ proto._setHTML = function ( html ) {
}; };
proto.getHTML = function ( withBookMark ) { proto.getHTML = function ( withBookMark ) {
var html, range; var brs = [],
root, node, fixer, html, l, range;
if ( withBookMark && ( range = this.getSelection() ) ) { if ( withBookMark && ( range = this.getSelection() ) ) {
this._saveRangeToBookmark( range ); this._saveRangeToBookmark( range );
} }
@@ -4475,12 +4417,6 @@ proto.insertHTML = function ( html, isPaste ) {
this._docWasChanged(); this._docWasChanged();
} }
range.collapse( false ); range.collapse( false );
// After inserting the fragment, check whether the cursor is inside
// an <a> element and if so if there is an equivalent cursor
// position after the <a> element. If there is, move it there.
moveRangeBoundaryOutOf( range, 'A', root );
this._ensureBottomLine(); this._ensureBottomLine();
} }
@@ -4984,7 +4920,6 @@ Squire.rangeDoesEndAtBlockBoundary = rangeDoesEndAtBlockBoundary;
Squire.expandRangeToBlockBoundaries = expandRangeToBlockBoundaries; Squire.expandRangeToBlockBoundaries = expandRangeToBlockBoundaries;
// Clipboard.js exports // Clipboard.js exports
Squire.onCopy = _onCopy;
Squire.onPaste = onPaste; Squire.onPaste = onPaste;
// Editor.js exports // Editor.js exports

View File

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

View File

@@ -1,59 +1,54 @@
<template> <template>
<div> <div :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
<div class="ui basic segment">
<div class="ui grid"> <div class="ui grid">
<div class="ui sixteen wide column"> <div class="ui sixteen wide center aligned column">
<div class="ui dividing header"> <div class="ui fluid button" v-on:click="clearStyles">
Reset Background Color and Icon
</div>
<div class="ui labeled basic icon button" v-on:click="clearStyles">
<i class="refresh icon"></i> <i class="refresh icon"></i>
Reset Clear All Styles
</div> </div>
</div> </div>
<div class="sixteen wide column rounded" :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}"> <div class="row">
<div class="ui dividing header" :style="{ 'color':allStyles['noteText']}"> <div class="sixteen wide column">
<i class="fill drip icon"></i> <br>
Background Color <p>Note Color</p>
</div> <div v-for="color in getReducedColors()"
<div v-for="color in colors" class="color-button"
class="color-button" :style="{ backgroundColor:color }"
:style="{ backgroundColor:color }" v-on:click="chosenColor(color)"
v-on:click="chosenColor(color)" ></div>
></div>
</div>
<div class="sixteen wide column">
<div class="ui dividing header">
<span v-if="allStyles.noteIcon" >
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
</span>
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> </div>
<div class="sixteen wide column"> <div class="row">
<div class="ui dividing header"> <div class="sixteen wide column">
<span v-if="allStyles.noteIcon" > <p>Note Icon
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i> <span v-if="allStyles.noteIcon" >
</span> <i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
Icon Color </span>
</div> </p>
<div v-for="color in getReducedColors()" <div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
class="color-button" <i :class="`large ${icon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
:style="{ backgroundColor:color }" </div>
v-on:click="chooseIconColor(color)"
>
</div> </div>
</div> </div>
<div class="row">
<div class="sixteen wide column">
<p>Icon Color</p>
<div v-for="color in getReducedColors()"
class="color-button"
:style="{ backgroundColor:color }"
v-on:click="chooseIconColor(color)"
>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
@@ -71,7 +66,7 @@
blankStyle:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, blankStyle:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
colors: [null, colors: [null,
'rgb(67,67,67)','rgb(102,102,102)','rgb(153,153,153)','rgb(183,183,183)','rgb(204,204,204)','rgb(217,217,217)','rgb(239,239,239)','rgb(243,243,243)','rgb(255,255,255)','rgb(152,0,0)','rgb(255,0,0)','rgb(255,153,0)','rgb(255,255,0)','rgb(0,255,0)','rgb(0,255,255)','rgb(74,134,232)','rgb(0,0,255)','rgb(153,0,255)','rgb(255,0,255)','rgb(230,184,175)','rgb(244,204,204)','rgb(252,229,205)','rgb(255,242,204)','rgb(217,234,211)','rgb(208,224,227)','rgb(201,218,248)','rgb(207,226,243)','rgb(217,210,233)','rgb(234,209,220)','rgb(221,126,107)','rgb(234,153,153)','rgb(249,203,156)','rgb(255,229,153)','rgb(182,215,168)','rgb(162,196,201)','rgb(164,194,244)','rgb(159,197,232)','rgb(180,167,214)','rgb(213,166,189)','rgb(204,65,37)','rgb(224,102,102)','rgb(246,178,107)','rgb(255,217,102)','rgb(147,196,125)','rgb(118,165,175)','rgb(109,158,235)','rgb(111,168,220)','rgb(142,124,195)','rgb(194,123,160)','rgb(166,28,0)','rgb(204,0,0)','rgb(230,145,56)','rgb(241,194,50)','rgb(106,168,79)','rgb(69,129,142)','rgb(60,120,216)','rgb(61,133,198)','rgb(103,78,167)','rgb(166,77,121)','rgb(133,32,12)','rgb(153,0,0)','rgb(180,95,6)','rgb(191,144,0)','rgb(56,118,29)','rgb(19,79,92)','rgb(17,85,204)','rgb(11,83,148)','rgb(53,28,117)','rgb(116,27,71)','rgb(91,15,0)','rgb(102,0,0)','rgb(120,63,4)','rgb(127,96,0)','rgb(39,78,19)','rgb(12,52,61)','rgb(28,69,135)','rgb(7,55,99)','rgb(32,18,77)','rgb(76,17,48)'], 'rgb(67,67,67)','rgb(102,102,102)','rgb(153,153,153)','rgb(183,183,183)','rgb(204,204,204)','rgb(217,217,217)','rgb(239,239,239)','rgb(243,243,243)','rgb(255,255,255)','rgb(152,0,0)','rgb(255,0,0)','rgb(255,153,0)','rgb(255,255,0)','rgb(0,255,0)','rgb(0,255,255)','rgb(74,134,232)','rgb(0,0,255)','rgb(153,0,255)','rgb(255,0,255)','rgb(230,184,175)','rgb(244,204,204)','rgb(252,229,205)','rgb(255,242,204)','rgb(217,234,211)','rgb(208,224,227)','rgb(201,218,248)','rgb(207,226,243)','rgb(217,210,233)','rgb(234,209,220)','rgb(221,126,107)','rgb(234,153,153)','rgb(249,203,156)','rgb(255,229,153)','rgb(182,215,168)','rgb(162,196,201)','rgb(164,194,244)','rgb(159,197,232)','rgb(180,167,214)','rgb(213,166,189)','rgb(204,65,37)','rgb(224,102,102)','rgb(246,178,107)','rgb(255,217,102)','rgb(147,196,125)','rgb(118,165,175)','rgb(109,158,235)','rgb(111,168,220)','rgb(142,124,195)','rgb(194,123,160)','rgb(166,28,0)','rgb(204,0,0)','rgb(230,145,56)','rgb(241,194,50)','rgb(106,168,79)','rgb(69,129,142)','rgb(60,120,216)','rgb(61,133,198)','rgb(103,78,167)','rgb(166,77,121)','rgb(133,32,12)','rgb(153,0,0)','rgb(180,95,6)','rgb(191,144,0)','rgb(56,118,29)','rgb(19,79,92)','rgb(17,85,204)','rgb(11,83,148)','rgb(53,28,117)','rgb(116,27,71)','rgb(91,15,0)','rgb(102,0,0)','rgb(120,63,4)','rgb(127,96,0)','rgb(39,78,19)','rgb(12,52,61)','rgb(28,69,135)','rgb(7,55,99)','rgb(32,18,77)','rgb(76,17,48)'],
icons: ['cat','crow','dog','dove','dragon','feather','feather alternate','fish','frog','hippo','horse','horse head','kiwi bird','otter','paw','spider','video','headphones','motorcycle','truck','monster truck','campground','cloud sun','drumstick bite','football ball','fruit-apple','hiking','mountain','tractor','tree','wind','wine bottle','coffee','flask','glass cheers','glass martini','beer','toilet paper','gift','globe','hand holding heart','comment','graduation cap','hat cowboy','hat wizard','mitten','user tie','laptop code','microchip','shield alternate','mouse','plug','power off','satellite','hammer','wrench','bell','eye','marker','paperclip','atom','award','theater masks','music','grin alternate','grin tongue squint outline','laugh wink','fire','fire alternate','poop','sun','money bill alternate','piggy bank','heart outline','heartbeat','running','walking','bacon','bone','bread slice','candy cane','carrot','cheese','cloud meatball','cookie','egg','hamburger','hotdog','ice cream','lemon','lemon outline','pepper hot','pizza slice','seedling','stroopwafel','leaf','book dead','broom','cloud moon','ghost','mask','skull crossbones','certificate','check','check circle','joint','cannabis','bong','gem','futbol','brain','dna','hand spock','hand spock outline','meteor','moon','moon outline','robot','rocket','satellite dish','space shuttle','user astronaut','fingerprint','thumbs up','thumbs down'] icons: ['ambulance','anchor','balance scale','bath','bed','beer','bell','bell slash','bell slash outline','bicycle','binoculars','birthday cake','blind','bomb','book','bookmark','briefcase','building','car','coffee','crosshairs','dollar sign','eye','eye slash','fighter jet','fire','fire extinguisher','flag','flag checkered','flask','gamepad','gavel','gift','glass martini','globe','graduation cap','h square','heart','heart outline','heartbeat','home','hospital','hospital outline','image','image outline','images','images outline','industry','info','info circle','key','leaf','lemon','lemon outline','life ring','life ring outline','lightbulb','lightbulb outline','location arrow','low vision','magnet','male','map','map outline','map marker','map marker alternate','map pin','map signs','medkit','money bill alternate','money bill alternate outline','motorcycle','music','newspaper','newspaper outline','paw','phone','phone square','phone volume','plane','plug','plus','plus square','plus square outline','print','recycle','road','rocket','search','search minus','search plus','ship','shopping bag','shopping basket','shopping cart','shower','street view','subway','suitcase','tag','tags','taxi','thumbtack','ticket alternate','tint','train','tree','trophy','truck','tty','umbrella','university','utensil spoon','utensils','wheelchair','wifi','wrench']
} }
}, },
watch:{ watch:{
@@ -88,11 +83,17 @@
let reduced = [] let reduced = []
this.colors.forEach((color,i) => { this.colors.forEach((color,i) => {
if(i < 20 || i > 69){
let mod = (i % 10)+1 //1 - 10
let lines = [3, 5, 8, 9, 10]
// if(lines.includes(mod)){
reduced.push(color) reduced.push(color)
} // }
}) })
reduced.push("#000")
return reduced return reduced
}, },
clearStyles(){ clearStyles(){
@@ -113,17 +114,12 @@
//Automatically select note text color //Automatically select note text color
//If you are using hex colors, use this
// Convert hex color to RGB - http://gist.github.com/983661 // Convert hex color to RGB - http://gist.github.com/983661
// let color = +("0x" + inColor.slice(1).replace(inColor.length < 5 && /./g, '$&$&')); let color = +("0x" + inColor.slice(1).replace(inColor.length < 5 && /./g, '$&$&'));
// let r = color >> 16;
// let g = color >> 8 & 255;
// let b = color & 255;
const set = inColor.match(/\d+/g) let r = color >> 16;
const r = parseInt(set[0]) let g = color >> 8 & 255;
const g = parseInt(set[1]) let b = color & 255;
const b = parseInt(set[2])
//Convert RGB to HSP //Convert RGB to HSP
const hsp = Math.sqrt( 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) ); const hsp = Math.sqrt( 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) );
@@ -152,24 +148,20 @@
} }
</script> </script>
<style type="text/css" scoped> <style type="text/css" scoped>
.icon-button, .color-button { .icon-button {
height: 40px; height: 40px;
width: calc(15% - 1px); width: calc(10% - 7px);
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
font-size: 1.3em; 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 { .color-button {
width: calc(10% - 4px); display: inline-block;
} width: calc(10% - 7px);
.rounded { height: 30px;
border-radius: 5px; border-radius: 30px;
box-shadow: 0px 1px 3px 0px #3e3e3e;
margin: 7px 7px 0 0;
cursor: pointer;
} }
</style> </style>

View File

@@ -24,9 +24,9 @@
} }
}, },
beforeMount(){ beforeMount(){
// this.$bus.$on('reset_fast_filters', () => { this.$bus.$on('reset_fast_filters', () => {
// this.orderString = 'Order by Last Edited' this.orderString = 'Order by Last Edited'
// }) })
}, },
methods:{ methods:{
displayString(){ displayString(){

View File

@@ -2,31 +2,30 @@
.popup-body { .popup-body {
position: fixed; position: fixed;
top: 15px; bottom: 15px;
left: 15px; left: 15px;
min-height: 50px; min-height: 50px;
min-width: 200px; min-width: 200px;
max-width: calc(100% - 30px); max-width: calc(100% - 20px);
z-index: 1020; z-index: 1002;
border-top: 2px solid #21ba45;
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1); box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
border-radius: 4px; border-top-right-radius: 4px;
border-top-left-radius: 4px;
color: white;
background-color: var(--main-accent);
} }
.popup-row { .popup-row {
padding: 1em 5px; padding: 1em 5px;
cursor: pointer; cursor: pointer;
white-space: nowrap;
} }
.popup-row > p { .popup-row > span {
/*width: calc(100% - 50px);*/ width: calc(100% - 50px);
display: inline-block; display: inline-block;
text-align: left; text-align: center;
box-sizing: border-box; box-sizing: border-box;
padding: 0 10px 0; padding: 0 10px 0;
font-size: 1.25em; font-size: 1.25em;
border-radius: 4px;
} }
.popup-row + .popup-row { .popup-row + .popup-row {
border-top: 1px solid #FFF; border-top: 1px solid #FFF;
@@ -37,10 +36,12 @@
} }
@keyframes slide-in-bottom { @keyframes slide-in-bottom {
0% { 0% {
transform: translateY(-1000px); transform: translateY(1000px);
opacity: 0;
} }
100% { 100% {
transform: translateY(0); transform: translateY(0);
opacity: 1;
} }
} }
@@ -62,62 +63,14 @@
} }
} }
.meter {
height: 2px;
display: inline-block;
width: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
overflow: hidden;
border-top-right-radius: 4px;
border-top-left-radius: 4px;
}
.meter span {
display: block;
height: 100%;
}
.progress {
background-color: white;
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; }
100% { width: 100%; }
}
</style> </style>
<template> <template>
<div class="popup-body slide-in-bottom" v-on:click="dismiss" v-if="notifications.length > 0"> <div class="popup-body slide-in-bottom" v-on:click="dismiss" v-if="notifications.length > 0">
<div class="popup-row" v-for="item in notifications"> <div class="popup-row color-fade" v-for="item in notifications">
<div class="meter"> <span>{{ item }}</span>
<span><span class="progress"></span></span>
</div>
<p class="text-display">
<i class="small info circle icon"></i>
{{ item.text }}
<span class="time-display">{{ item.time }}</span>
</p>
</div> </div>
</div> </div>
</template> </template>
@@ -135,33 +88,24 @@
} }
}, },
beforeMount(){ beforeMount(){
this.$bus.$on('notification', notificationText => { this.$bus.$on('notification', info => {
this.displayNotification(notificationText) this.displayNotification(info)
}) })
}, },
mounted(){ mounted(){
// this.$bus.$emit('notification', 'Password Protection Removed Login did not succeed') // this.$bus.$emit('notification', 'Password Protection Removed')
// this.$bus.$emit('notification', 'Password Protection Removed your life is exposed to the internet') // this.$bus.$emit('notification', 'Password Protection Removed')
// this.$bus.$emit('notification', 'Password Protection Removed everyone can see everything') // this.$bus.$emit('notification', 'Password Protection Removed')
}, },
methods: { methods: {
displayNotification(notificationText){ displayNotification(newNotification){
this.notifications.push(newNotification)
const date = new Date()
const time = date.toLocaleTimeString()
const notification = {
text: notificationText,
time: time
}
this.notifications.unshift(notification)
clearTimeout(this.totalTimeout) clearTimeout(this.totalTimeout)
this.totalTimeout = setTimeout(() => { this.totalTimeout = setTimeout(() => {
this.dismiss() this.dismiss()
}, 3000) }, 4000)
}, },
dismiss(){ dismiss(){
this.notifications = [] this.notifications = []

View File

@@ -1,30 +1,27 @@
<style scoped> <style scoped>
.slotholder { .slotholder {
height: 100vh; height: 100vh;
width: 180px; width: 155px;
display: block; display: block;
float: left; float: left;
overflow: hidden;
} }
.global-menu { .global-menu {
width: 180px; width: 155px;
/* background: #221f2b; */
background: #221f2b; background: #221f2b;
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
position: fixed; position: fixed;
z-index: 900; z-index: 111;
top: 0; top: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;
} }
.menu-logo-display { .menu-logo-display {
width: 27px; width: 25px;
margin: 5px 0 0 55px; margin: 5px 0 0 42px;
display: inline-block; display: inline-block;
height: auto;
} }
.menu-item { .menu-item {
@@ -44,8 +41,7 @@
.menu-section {} .menu-section {}
.menu-section + .menu-section { .menu-section + .menu-section {
/* border-top: 1px solid #534c68; */ border-top: 1px solid #534c68;
border-top: 1px solid #534c68e3;
} }
.menu-button { .menu-button {
cursor: pointer; cursor: pointer;
@@ -55,6 +51,9 @@
text-decoration: none; text-decoration: none;
} }
.router-link-active i {
/*color: #16ab39;*/
}
.router-link-active { .router-link-active {
background-color: #534c68; background-color: #534c68;
} }
@@ -66,37 +65,29 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: rgba(0,0,0,0.7); background-color: rgba(0,0,0,0.7);
z-index: 899; z-index: 100;
cursor: pointer; cursor: pointer;
} }
.top-menu-bar { .top-menu-bar {
/*color: var(--text_color);*/ /*color: var(--text_color);*/
/*width: 100%;*/ /*width: 100%;*/
position: fixed; position: fixed;
bottom: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
z-index: 999; z-index: 999;
background-color: var(--small_element_bg_color); background-color: var(--small_element_bg_color);
/*padding: 5px 1rem 5px;*/ border-bottom: 1px solid;
display: flex; border-color: var(--border_color);
justify-content: space-around; padding: 5px 1rem 5px;
width: 100vw;
border-top: 1px solid var(--dark_border_color);
display: flex;
margin: 0;
padding: 0;
overflow: hidden;
} }
.place-holder { .place-holder {
width: 100%; width: 100%;
/*height: 40px;*/ height: 50px;
height: 0;
} }
.logo-display { .top-menu-bar img {
width: 27px; width: 30px;
height: auto; height: 30px;
} }
.version-display { .version-display {
position: absolute; position: absolute;
@@ -108,49 +99,6 @@
text-align: center; text-align: center;
color: #8c80ae; color: #8c80ae;
cursor: pointer; cursor: pointer;
background-color: var(--menu-background);
}
.mobile-button {
padding: 5px 0 0;
margin: 0;
cursor: pointer;
font-size: 0.6em;
color: var(--menu-text);
text-align: center;
flex-basis: 100%;
line-height: 1.8em;
}
.mobile-button + .mobile-button {
border-left: 1px solid var(--dark_border_color);
}
.mobile-button i {
font-size: 2em;
margin: 0 auto;
padding: 0;
width: 100%;
}
.mobile-button svg {
margin: 0 46% 0;
display: inline-block;
width: 15px;
}
.mobile-button:active, .mobile-button:focus, .mobile-button:hover {
text-decoration: none;
}
.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> </style>
@@ -162,58 +110,45 @@
<!-- collapsed menu, appears as a bar --> <!-- collapsed menu, appears as a bar -->
<div class="top-menu-bar" v-if="(collapsed || mobile) && !menuOpen"> <div class="top-menu-bar" v-if="(collapsed || mobile) && !menuOpen">
<div class="ui grid">
<!-- logo --> <div class="seven wide column">
<router-link v-if="loggedIn" class="mobile-button" exact-active-class="active" to="/notes" v-on:click.native="emitReloadEvent()"> <div class="ui large basic compact icon button" v-on:click="collapseMenu">
<logo class="logo-display" color="var(--main-accent)" /> <i class="green bars icon"></i>
Notes </div>
</router-link>
<!-- <router-link v-if="loggedIn" class="ui large basic compact icon button" to="/notes" v-on:click.native="emitReloadEvent()">
<i class="green home icon"></i>
</router-link> -->
<router-link v-if="loggedIn" class="ui basic icon button" exact-active-class="active" to="/attachments">
<i class="open folder outline icon"></i>
</router-link>
</div>
<div class="two wide center aligned bottom aligned column">
<img loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
</div>
<div class="seven wide right aligned column">
<div v-on:click="toggleNightMode" class="ui large basic compact icon button">
<i class="green moon outline icon"></i>
</div>
<!-- mobile create note button -->
<span v-if="loggedIn">
<span v-if="!disableNewNote" @click="createNote" class="ui large green compact icon button">
<i class="plus icon"></i>
</span>
<span v-if="disableNewNote" class="ui large basic compact icon button">
<i class="grey plus icon"></i>
</span>
</span>
</div>
<!-- new note -->
<div v-if="loggedIn" class="mobile-button">
<span v-if="!disableNewNote" @click="createNote">
<i class="green plus icon"></i>
New Note
</span>
<span v-if="disableNewNote">
<i class="grey plus icon"></i>
Working
</span>
</div> </div>
<!-- open straight to note -->
<router-link
v-if="loggedIn && $store.getters.totals && $store.getters.totals['quickNote']"
exact-active-class="active"
class="mobile-button"
:to="`/notes/open/${$store.getters.totals['quickNote']}`">
<i class="green sticky note outline icon"></i>
Scratch Pad
</router-link>
<!-- create new and redirect to new note id -->
<a
v-if="loggedIn && $store.getters.totals && !$store.getters.totals['quickNote']"
v-on:click="newQuickNote()"
exact-active-class="active"
class="mobile-button">
<i class="green sticky note outline icon"></i>
Scratch Pad
</a>
<router-link v-if="loggedIn" class="mobile-button" exact-active-class="active" to="/attachments">
<i class="green open folder outline icon"></i>
Files
</router-link>
<!-- menu -->
<div class="mobile-button" v-on:click="collapseMenu">
<i class="green link bars icon" ></i>
Menu
</div>
</div> </div>
<div class="shade" v-if="mobile && !collapsed" v-on:click="collapseMenu"></div> <div class="shade" v-if="mobile && !collapsed" v-on:click="collapseMenu"></div>
@@ -224,18 +159,20 @@
<div class="global-menu" v-if="!collapsed" v-on:click="menuClicked"> <div class="global-menu" v-if="!collapsed" v-on:click="menuClicked">
<div class="menu-section" v-on:click="collapseMenu"> <div class="menu-section" v-on:click="collapseMenu">
<i class="white angle left icon"></i> <!-- <div class="menu-item menu-button" > -->
<logo class="menu-logo-display" color="var(--main-accent)" /> <i class="white angle left icon"></i>
<img class="menu-logo-display" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
<!-- </div> -->
</div> </div>
<div class="menu-section" v-if="loggedIn"> <div class="menu-section" v-if="loggedIn">
<div v-if="!disableNewNote" @click="createNote" class="menu-item menu-item menu-button"> <div v-if="!disableNewNote" @click="createNote" class="menu-item menu-item menu-button">
<div class="ui green fluid compact button"> <div class="ui green button">
<i class="plus icon"></i>New Note <i class="plus icon"></i>New Note
</div> </div>
</div> </div>
<div v-if="disableNewNote" class="menu-item menu-item menu-button"> <div v-if="disableNewNote" class="menu-item menu-item menu-button">
<div class="ui basic fluid compact button"> <div class="ui basic button">
<i class="plus loading icon"></i>New Note <i class="plus loading icon"></i>New Note
</div> </div>
</div> </div>
@@ -248,19 +185,14 @@
</router-link> </router-link>
<div> <div>
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(3)" v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)"> <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 paper plane outline icon"></i>Shared <i class="grey mail outline icon"></i>Inbox
<counter v-if="$store.getters.totals && $store.getters.totals['sharedToNotes']" class="float-right" number-id="sharedToNotes" />
</div> </div>
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0"> <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 <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>
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['trashedNotes'] > 0"> <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 <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>
<!-- <div class="menu-item sub">Show Only <i class="caret down icon"></i></div> --> <!-- <div class="menu-item sub">Show Only <i class="caret down icon"></i></div> -->
<!-- <div v-on:click="updateFastFilters(0)" class="menu-item menu-button sub"><i class="grey linkify icon"></i>Links</div> --> <!-- <div v-on:click="updateFastFilters(0)" class="menu-item menu-button sub"><i class="grey linkify icon"></i>Links</div> -->
@@ -276,26 +208,9 @@
</div> </div>
<div class="menu-section" v-if="loggedIn"> <div class="menu-section" v-if="loggedIn">
<router-link v-if="loggedIn" exact-active-class="active" class="menu-item menu-button" to="/quick">
<!-- open straight to note -->
<router-link
v-if="loggedIn && $store.getters.totals && $store.getters.totals['quickNote']"
exact-active-class="active"
class="menu-item menu-button"
:to="`/notes/open/${$store.getters.totals['quickNote']}`">
<i class="sticky note outline icon"></i>Scratch Pad <i class="sticky note outline icon"></i>Scratch Pad
</router-link> </router-link>
<!-- create new and redirect to new note id -->
<a
v-if="loggedIn && $store.getters.totals && !$store.getters.totals['quickNote']"
v-on:click="newQuickNote()"
exact-active-class="active"
class="menu-item menu-button">
<i class="sticky note outline icon"></i>Scratch Pad
</a>
</div> </div>
<div class="menu-section" v-if="!loggedIn"> <div class="menu-section" v-if="!loggedIn">
@@ -313,52 +228,25 @@
<span v-if="$store.getters.getIsNightMode == 0"> <span v-if="$store.getters.getIsNightMode == 0">
<i class="moon outline icon"></i>Black Theme</span> <i class="moon outline icon"></i>Black Theme</span>
<span v-if="$store.getters.getIsNightMode == 1"> <span v-if="$store.getters.getIsNightMode == 1">
<i class="moon outline icon"></i>Flux Theme</span>
<span v-if="$store.getters.getIsNightMode == 2">
<i class="moon outline icon"></i>Light Theme</span> <i class="moon outline icon"></i>Light Theme</span>
</div> </div>
</div> </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"> <div class="menu-section">
<router-link class="menu-item menu-button" exact-active-class="active" to="/help"> <router-link class="menu-item menu-button" exact-active-class="active" to="/help">
<i class="question circle outline icon"></i>Help <i class="question circle outline icon"></i>Help
</router-link> </router-link>
</div> </div>
<div class="menu-section" v-if="loggedIn"> <div class="menu-section" v-if="loggedIn" :data-tooltip="`Logout ${this.$store.getters.getUsername}`" data-inverted="" data-position="right center">
<div class="menu-item menu-button" v-on:click="logout()"> <div v-on:click="destroyLoginToken" class="menu-item menu-button">
<i class="log out icon"></i>Log Out <i v-if="userIcon" class="user outline icon"></i>{{ usernameDisplay }}
</div> </div>
</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" > <div v-on:click="reloadPage" class="version-display" v-if="version != 0" >
<i :class="`${getVersionIcon()} icon`"></i> {{ version }} <i :class="`${getVersionIcon()} icon`"></i> {{ version }}
@@ -376,7 +264,6 @@
components: { components: {
'search-input': require('@/components/SearchInput.vue').default, 'search-input': require('@/components/SearchInput.vue').default,
'counter':require('@/components/AnimatedCounterComponent.vue').default, 'counter':require('@/components/AnimatedCounterComponent.vue').default,
'logo':require('@/components/LogoComponent.vue').default,
}, },
data: function(){ data: function(){
return { return {
@@ -387,14 +274,10 @@
disableNewNote: false, disableNewNote: false,
menuOpen: true, menuOpen: true,
userIcon: true, userIcon: true,
resizeDebounce: null,
} }
}, },
beforeMount(){ beforeCreate: function(){
window.addEventListener('resize', this.resizeEventHandler)
},
beforeDestroy(){
window.removeEventListener('resize', this.resizeEventHandler)
}, },
mounted: function(){ mounted: function(){
this.mobile = this.$store.getters.getIsUserOnMobile this.mobile = this.$store.getters.getIsUserOnMobile
@@ -409,61 +292,30 @@
this.version = localStorage.getItem('currentVersion') this.version = localStorage.getItem('currentVersion')
} }
this.resizeEventHandler() //Trigger resize event
}, },
computed: { computed: {
loggedIn () { loggedIn () {
//Map logged in from state //Map logged in from state
return this.$store.getters.getLoggedIn return this.$store.getters.getLoggedIn
}, },
usernameDisplay() {
//Remove Emails from username, limit length to 16 chars
let name = this.$store.getters.getUsername
let splitName = name.split('@')
if(splitName.length > 1){
name = splitName.shift()
this.userIcon = false
}
if(name.length > 16){
this.userIcon = false
}
return this.ucWords(name.substring(0, 16))
},
}, },
methods: { 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('/')
axios.post('/api/user/logout')
setTimeout(() => {
this.$store.commit('destroyLoginToken')
this.$bus.$emit('notification', 'Logged Out')
}, 200)
},
newQuickNote(){
axios.post('/api/quick-note/get')
.then( ({data}) => {
this.$router.push({'path':'/notes/open/'+data.noteId})
})
},
resizeEventHandler(e) {
clearTimeout(this.resizeDebounce)
this.resizeDebounce = setTimeout(() => {
this.mobile = false
this.menuOpen = false
this.collapsed = false
if(window.innerWidth < 700){
this.collapsed = true
this.mobile = true
}
}, 100)
},
menuClicked(){ menuClicked(){
//Collapse menu when item is clicked in mobile //Collapse menu when item is clicked in mobile
if(this.mobile && !this.collapsed){ if(this.mobile && !this.collapsed){
@@ -482,22 +334,28 @@
}, },
createNote(event){ createNote(event){
const title = ''
this.disableNewNote = true this.disableNewNote = true
axios.post('/api/note/create', {title:''}) axios.post('/api/note/create', {title})
.then(response => { .then(response => {
if(response.data && response.data.id){ if(response.data && response.data.id){
//Redirect to note page if user is not on it
//Push new note to url and it will open this.$bus.$emit('open_note', response.data.id)
this.$router.push('/notes/open/'+response.data.id)
this.disableNewNote = false this.disableNewNote = false
} }
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to create note') }) .catch(error => { this.$bus.$emit('notification', 'Failed to create note') })
}, },
destroyLoginToken() {
axios.post('/api/user/logout')
setTimeout(() => {
this.$bus.$emit('notification', 'Logged Out')
this.$store.commit('destroyLoginToken')
this.$router.push('/')
}, 200)
},
toggleNightMode(){ toggleNightMode(){
this.$store.commit('toggleNightMode') this.$store.commit('toggleNightMode')
}, },
@@ -527,11 +385,8 @@
location.reload(true) location.reload(true)
}, },
getVersionIcon(){ 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 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(String(this.version).replace(/\./g,'')) % (icons.length)) const index = ( parseInt(this.version.replace(/\./g,'')) % (icons.length))
return icons[index] return icons[index]
} }

View File

@@ -1,60 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank" rel="noopener">pwa</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="loading-container"> <div class="loading-container">
<svg version="1.1" id="L6" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"> <svg version="1.1" id="L6" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
<rect fill="none" :stroke="$store.getters.getIsNightMode > 0 ? '#FFF':'var(--main-accent)'" stroke-width="4" x="25" y="25" width="50" height="50" rx="5"> <rect fill="none" :stroke="$store.getters.getIsNightMode > 0 ? '#FFF':'#16ab39'" stroke-width="4" x="25" y="25" width="50" height="50" rx="5">
<animateTransform <animateTransform
attributeName="transform" attributeName="transform"
dur="0.5s" dur="0.5s"
@@ -12,7 +12,7 @@
attributeType="XML" attributeType="XML"
begin="rectBox.end"/> begin="rectBox.end"/>
</rect> </rect>
<rect x="25" y="25" :fill="$store.getters.getIsNightMode > 0 ? '#FFF':'var(--main-accent)'" width="50" height="50"> <rect x="25" y="25" :fill="$store.getters.getIsNightMode > 0 ? '#FFF':'#16ab39'" width="50" height="50">
<animate <animate
attributeName="height" attributeName="height"
dur="1.3s" dur="1.3s"
@@ -39,9 +39,9 @@
.loading-container { .loading-container {
text-align: center; text-align: center;
width: 100%; width: 100%;
/*min-height: 100px;*/ min-height: 100px;
margin: 20px 0; margin: 20px 0;
/*padding: 40px;*/ padding: 40px;
border-radius: 7px; border-radius: 7px;
background-color: var(--small_element_bg_color); background-color: var(--small_element_bg_color);
} }

View File

@@ -1,10 +1,9 @@
<template> <template>
<div> <div v-on:keyup.enter="login()">
<!-- thicc form display --> <!-- thicc form display -->
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register"> <div v-if="!thin" class="ui large form">
<div class="field"> <div class="field">
<div class="ui input"> <div class="ui input">
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail"> <input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
@@ -15,88 +14,47 @@
<input v-model="password" type="password" name="password" placeholder="Password"> <input v-model="password" type="password" name="password" placeholder="Password">
</div> </div>
</div> </div>
<div class="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">
</div>
</div>
<div class="sixteen wide field"> <div class="sixteen wide field">
<div class="ui fluid buttons"> <div class="ui fluid buttons">
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="login()" class="ui green button">
<i class="power icon"></i>
<div v-on:click="register" class="ui green button" :class="{ 'disabled':(username.length == 0 || password.length == 0)}"> Login
</div>
<div class="or"></div>
<div v-on:click="register()" class="ui button">
<i class="plug icon"></i> <i class="plug icon"></i>
Sign Up Sign Up
</div> </div>
</div> </div>
</div> </div>
<div class="sixteen wide column">
<span class="small-terms">
By signing up you agree to Solid Scribe's
<router-link to="/terms">
Terms of Use
</router-link>
</span>
</div>
</div> </div>
<!-- Thin form display --> <!-- Thin form display -->
<div v-if="thin" class="ui small form" v-on:keyup.enter="login"> <div v-if="thin" class="ui small form">
<div class="fields">
<div v-if="!require2FA" class="field"><!-- hide this field if someone is logging in with 2FA --> <div class="four wide field">
<div class="ui grid">
<div class="ui sixteen wide center aligned column">
<div v-on:click="register" class="ui green button">
<i class="plug icon"></i>
Sign Up Now!
</div>
</div>
</div>
</div>
<div class="field"><!-- hide this field if someone is logging in with 2FA -->
<div class="ui grid">
<div class="ui sixteen wide center aligned column">
Or Login
</div>
</div>
</div>
<div class="equal width fields">
<div class="field">
<div class="ui input"> <div class="ui input">
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail"> <input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
</div> </div>
</div> </div>
<div class="field"> <div class="four wide field">
<div class="ui input"> <div class="ui input">
<input v-model="password" type="password" name="password" placeholder="Password"> <input v-model="password" type="password" name="password" placeholder="Password">
</div> </div>
</div> </div>
<div class="field" v-if="require2FA"> <div class="four wide field">
<div class="ui input"> <div v-on:click="register()" class="ui fluid green button">
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token"> <i class="plug icon"></i>
Sign Up
</div> </div>
</div> </div>
<div class="field"> <div class="four wide field">
<div v-on:click="login" class="ui fluid button"> <div v-on:click="login()" class="ui fluid button">
<i class="power icon"></i> <i class="power icon"></i>
Login Login
</div> </div>
</div> </div>
</div> </div>
<span class="small-terms">
By signing up you agree to Solid Scribe's
<router-link to="/terms">
Terms of Use
</router-link>
</span>
</div> </div>
@@ -125,10 +83,7 @@
return { return {
enabled: false, enabled: false,
username: '', username: '',
password: '', password: ''
password2: '',
authToken: '',
require2FA: false,
} }
}, },
methods: { methods: {
@@ -159,27 +114,12 @@
}, },
register(){ register(){
let error = false if( this.username.length == 0 || this.password.length == 0 ){
this.$bus.$emit('notification', 'Unable to Sign Up - Username and Password Required')
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 return
} }
axios.post('/api/public/register', {'username': this.username, 'password': this.password}) axios.post('/api/user/register', {'username': this.username, 'password': this.password})
.then(({data}) => { .then(({data}) => {
if(data == false){ if(data == false){
@@ -199,43 +139,19 @@
return return
} }
axios.post('/api/public/login', {'username': this.username, 'password': this.password, 'authToken':this.authToken }) axios.post('/api/user/login', {'username': this.username, 'password': this.password})
.then(({data}) => { .then(({data}) => {
//Enable 2FA on form if(data == false){
if(data.success == false && data.verificationRequired == true && this.require2FA == false){ this.$bus.$emit('notification', 'Unable to Login - Incorrect Username or Password')
this.$bus.$emit('notification', data.message)
this.require2FA = true
this.$nextTick(() => {
this.$refs.authForm.focus()
})
return
} }
if(data.success == false){ this.finalizeLogin(data)
this.$bus.$emit('notification', data.message)
return
}
if(data.success){
this.finalizeLogin(data)
return
}
}) })
.catch(error => { .catch(error => {
this.$bus.$emit('notification', error) this.$bus.$emit('notification', 'Unable to Login - Incorrect Username or Password')
}) })
} }
} }
} }
</script> </script>
<style type="text/css" scoped="true">
.small-terms {
display: inline-block;
width: 100%;
font-size: 0.9em;
}
</style>

View File

@@ -1,100 +0,0 @@
<template>
<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></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
style="display:inline"
transform="translate(0,-164.70832)"
id="layer1">
<path
class="darken-accent"
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:${displayColor};fill-opacity:1;stroke:${strokeColor};stroke-width:${strokeWidth};stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1`" />
<path
class="brighten-accent"
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:${displayColor};fill-opacity:1;stroke:${strokeColor};stroke-width:${strokeWidth}px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1`" />
<path
class="brighten-accent"
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:${displayColor};fill-opacity:1;stroke:${strokeColor};stroke-width:${strokeWidth}px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1`" />
<path
class="brighten-accent"
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:${displayColor};fill-opacity:1;stroke:${strokeColor};stroke-width:${strokeWidth};stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1`" />
</g>
</svg>
</template>
<script>
export default {
name: 'LoadingIcon',
props:[
'color', // hex value for setting colorr
'stroke' // enable or disable stroke
],
data(){
return {
displayColor: '#21BA45', //Default green color
strokeWidth: '0.5',
strokeColor: 'none',
}
},
beforeCreate(){
},
created(){
if(this.stroke){
this.strokeWidth = 0.4
this.strokeColor = 'rgba(0,0,0,0.9)'
}
//Set color if passed
if(this.color){
this.displayColor = this.color
}
},
}
</script>
<style type="text/css" scoped>
.darken-accent {
filter: brightness(62%);
-webkit-filter: brightness(62%);
}
.brighten-accent {
filter: saturate(145%);
-webkit-filter: saturate(145%);
}
g > path {
filter: drop-shadow(1px 1px 1px black);
}
</style>

View File

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

View File

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

View File

@@ -1,164 +0,0 @@
<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,15 +1,23 @@
<template> <template>
<div class="note-title-display-card" <div class="note-title-display-card"
:style="{'background-color':color, 'color':fontColor, 'border-color':color }" :style="{'background-color':color, 'color':fontColor, 'border-color':color }"
:class="{ :class="{'currently-open':(currentlyOpen || showWorking), 'bgboy':triggerClosedAnimation, 'title-view':titleView }"
'currently-open':(currentlyOpen || showWorking), >
'ring':triggerClosedAnimation,
'title-view':titleView
}">
<!-- Show title and snippet below it --> <!-- Show title and snippet below it -->
<div class="overflow-hidden note-card-text" @click.stop="cardClicked" v-if="!titleView"> <div class="overflow-hidden note-card-text" @click="cardClicked" v-if="!titleView">
<span class="subtext" v-if="note.shareUsername">
Shared by {{ note.shareUsername }}
<span v-if="note.opened == null && !beenClicked" class="ui tiny green compact right floated button">
New
</span>
<span v-else-if="note.updated > note.opened && !beenClicked" class="ui tiny green compact right floated basic button">
Updated
</span>
</span>
<span v-if="note.title == '' && note.subtext == ''"> <span v-if="note.title == '' && note.subtext == ''">
Empty Note Empty Note
@@ -23,109 +31,46 @@
<span v-if="note.title.length > 0" <span v-if="note.title.length > 0"
class="big-text"><p>{{ note.title }}</p></span> 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.stop="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}</span>
<br>
</span>
<!-- Shared Details -->
<span class="subtext" v-if="note.shared == 2">
<i class="green paper plane outline icon"></i> Shared
<span v-if="note.updated/1000 > note.opened && !beenClicked" class="ui tiny green compact right floated basic button">
Updated
</span>
</span>
<span class="subtext" v-if="note.shareUsername">
<i class="green paper plane outline icon"></i> Shared by {{ note.shareUsername }}
<span v-if="note.opened == null && !beenClicked" class="ui tiny green compact right floated button">
New
</span>
<span v-else-if="note.updated/1000 > note.opened && !beenClicked" class="ui tiny green compact right floated basic button">
Updated
</span>
</span>
<!-- Sub text display --> <!-- Sub text display -->
<span v-if="note.subtext.length > 0" <span v-if="note.subtext.length > 0"
class="small-text" class="small-text"
v-html="note.subtext"></span> v-html="note.subtext"></span>
<!-- Not indexed warning --> <div class="ui fluid basic button" v-if="note.encrypted == 1">
<!-- <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> <i class="green lock icon"></i>
Locked Locked
</div> --> </div>
</div> <span class="subtext" v-if="note.shared == 2">
You Shared this note
<!-- slim card view --> <span v-if="note.updated > note.opened && !beenClicked" class="ui tiny green compact right floated basic button">
<div v-if="titleView" class="thin-container" @click="cardClicked"> Updated
<!-- 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>
</span> </span>
<!-- edited --> </div>
<span class="thin-right">
{{$helpers.timeAgo( note.updated )}}
<i class="green link ellipsis vertical icon"></i>
</span>
<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>
</div> </div>
<!-- Toolbar on the bottom --> <!-- Toolbar on the bottom -->
<div class="tool-bar" @click.self="cardClicked" v-if="!titleView"> <div class="tool-bar" @click.self="cardClicked" v-if="!titleView">
<div class="icon-bar">
<div v-if="getThumbs.length > 0"> <span class="tags" v-if="note.tags">
<div class="tiny-thumb-box" v-on:click="openEditAttachment"> <span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click="$emit('tagClick', tag.split(':')[1] )">{{ tag.split(':')[0] }}</span>
<img v-for="thumb in getThumbs" <br>
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>
<span class="teeny-buttons"> <span class="time-ago-display" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
{{$helpers.timeAgo(note.updated)}}
</span>
<span class="teeny-buttons" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
<span v-if="!note.trashed"> <span v-if="!note.trashed">
@@ -162,13 +107,19 @@
</i> </i>
<delete-button class="teeny-button" :note-id="note.id" /> <delete-button class="teeny-button" :note-id="note.id" />
</span> </span>
</span> </span>
</div> </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> </div>
<!-- tag edit menu -->
<side-slide-menu v-if="showTagSlideMenu" v-on:close="toggleTags(false)" :full-shadow="true" :skip-history="true"> <side-slide-menu v-if="showTagSlideMenu" v-on:close="toggleTags(false)" :full-shadow="true" :skip-history="true">
<div class="ui basic segment"> <div class="ui basic segment">
<note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/> <note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/>
@@ -227,12 +178,10 @@
}, },
pinNote(){ //togglePinned() <- old name pinNote(){ //togglePinned() <- old name
this.showWorking = true this.showWorking = true
this.note.pinned = this.note.pinned == 1 ? 0:1 let postData = {'pinned': !this.note.pinned, 'noteId':this.note.id}
let postData = {'pinned': this.note.pinned, 'noteId':this.note.id}
axios.post('/api/note/setpinned', postData) axios.post('/api/note/setpinned', postData)
.then(data => { .then(data => {
this.showWorking = false this.showWorking = false
// this event is triggered by the server after note is saved
// 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 Pin Note') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Pin Note') })
@@ -248,10 +197,11 @@
//Show message so no one worries where note went //Show message so no one worries where note went
let message = 'Moved to Archive' let message = 'Moved to Archive'
if(postData.archived != 1){ if(postData.archived != 1){
message = 'Moved out of Archive' message = 'Moved to main list'
} }
this.$bus.$emit('notification', message) 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') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Archive Note') })
}, },
@@ -266,10 +216,9 @@
//Show message so no one worries where note went //Show message so no one worries where note went
let message = 'Moved to Trash' let message = 'Moved to Trash'
if(postData.trashed == 0){ if(postData.trashed == 0){
message = 'Moved out of Trash' message = 'Moved to main list'
} }
this.$bus.$emit('notification', message) this.$bus.$emit('notification', message)
this.$bus.$emit('update_single_note', this.note.id)
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to Trash Note') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Trash Note') })
@@ -285,28 +234,23 @@
}, },
justClosed(){ justClosed(){
// Dont do anything when not is closed. //Scroll note into view
// Its already saved, this will make interface feel snappy
// Scroll note into view
// this.$el.scrollIntoView({ // this.$el.scrollIntoView({
// behavior: 'smooth', // behavior: 'smooth',
// block: 'center', // block: 'center',
// inline: 'center' // inline: 'center'
// }) // })
// this.$bus.$emit('notification','Note Saved') //After scroll, trigger green outline animation
setTimeout(() => {
// //After scroll, trigger green outline animation this.triggerClosedAnimation = true
// setTimeout(() => { setTimeout(()=>{
//After 3 seconds, hide it
this.triggerClosedAnimation = false
}, 3000)
// this.triggerClosedAnimation = true }, 500)
// setTimeout(()=>{
// //After 3 seconds, hide it
// this.triggerClosedAnimation = false
// }, 1500)
// }, 500)
}, },
}, },
@@ -381,11 +325,13 @@
.teeny-buttons { .teeny-buttons {
float: right; float: right;
width: 65%;
text-align: right; text-align: right;
} }
.time-ago-display { .time-ago-display {
font-size: 11px; width: 35%;
font-weight: bold; float: left;
text-align: center;
} }
.tags { .tags {
width: 100%; width: 100%;
@@ -410,12 +356,13 @@
/*Strict font sizes for card display*/ /*Strict font sizes for card display*/
.small-text { .small-text {
width: 100%; max-height: 261px;
overflow: hidden;
display: inline-block; display: inline-block;
} }
.small-text, .small-text > p, .small-text > h1, .small-text > h2 { .small-text, .small-text > p, .small-text > h1, .small-text > h2 {
/*font-size: 1.0em !important;*/ /*font-size: 1.0em !important;*/
font-size: 14px !important; font-size: 16px !important;
} }
.small-text > p, , .small-text > h1, .small-text > h2 { .small-text > p, , .small-text > h1, .small-text > h2 {
margin-bottom: 0.5em; margin-bottom: 0.5em;
@@ -423,7 +370,7 @@
.big-text > p:first-child, .big-text > p:first-child,
.big-text > h1, .big-text > h2 { .big-text > h1, .big-text > h2 {
/*font-size: 1.3em !important;*/ /*font-size: 1.3em !important;*/
font-size: 20px !important; font-size: 17px !important;
font-weight: bold; font-weight: bold;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
@@ -458,10 +405,9 @@
.note-title-display-card { .note-title-display-card {
position: relative; position: relative;
background-color: var(--small_element_bg_color); background-color: var(--small_element_bg_color);
/*The subtle shadow*/ /*The subtle shadow*/
box-shadow: 2px 2px 6px 0 rgba(0,0,0,.15); /*box-shadow: 0px 1px 2px 1px rgba(210, 211, 211, 0.46);*/
transition: box-shadow, border-color ease 0.5s, transform linear 0.5s; transition: box-shadow ease 0.5s, transform linear 0.1s;
margin: 5px; margin: 5px;
/*padding: 0.7em 1em;*/ /*padding: 0.7em 1em;*/
border-radius: .28571429rem; border-radius: .28571429rem;
@@ -469,82 +415,42 @@
border-color: var(--border_color); border-color: var(--border_color);
/*width: calc(33.333% - 10px);*/ /*width: calc(33.333% - 10px);*/
width: calc(25% - 10px); width: calc(25% - 10px);
/*min-width: 190px;*/ min-width: 190px;
/*min-height: 130px;*/ min-height: 130px;
/*transition: box-shadow 0.3s;*/ /*transition: box-shadow 0.3s;*/
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;
line-height: 1.8rem; line-height: 1.8rem;
letter-spacing: 0.05rem; letter-spacing: 0.02rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch;
text-align: left; text-align: left;
min-height: 100px;
max-height: 450px;
} }
.note-title-display-card:hover { .note-title-display-card:hover {
box-shadow: 0 8px 15px rgba(0,0,0,0.3); /*box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8);*/
border-color: var(--main-accent); /*transform: translateY(-2px);*/
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
} }
.note-title-display-card.title-view { .note-title-display-card.title-view {
width: 100%; width: 100%;
min-height: 20px; min-height: 20px;
max-width: none; 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);*/ /*box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);*/
} }
.title-view + .title-view {
border-top: 1px solid var(--border_color);
}
.thin-container.single-line-text { .single-line-text {
width: calc(100% - 25px); width: calc(100% - 25px);
/*margin: 5px 10px;*/ margin: 5px 10px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
box-sizing: border-box; box-sizing: border-box;
} }
.title-line {
.thin-container .thin-title {
font-weight: bold; font-weight: bold;
font-size: 1.2em; 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 { .icon-bar {
@@ -552,7 +458,6 @@
padding: 5px 10px 0; padding: 5px 10px 0;
opacity: 1; opacity: 1;
width: 100%; width: 100%;
background-color: rgba(200, 200, 200, 0.2);
} }
.hover-hide { .hover-hide {
opacity: 0.0; opacity: 0.0;
@@ -561,6 +466,7 @@
.little-tag { .little-tag {
font-size: 0.7em; font-size: 0.7em;
padding: 5px 5px; padding: 5px 5px;
border: 1px solid var(--border_color);
margin: 0 3px 5px 0; margin: 0 3px 5px 0;
border-radius: 3px; border-radius: 3px;
white-space: nowrap; white-space: nowrap;
@@ -570,8 +476,6 @@
line-height: 0.8em; line-height: 0.8em;
text-overflow: ellipsis; text-overflow: ellipsis;
float: left; float: left;
color: var(--main-accent);
opacity: 0.8;
} }
.tiny-thumb-box { .tiny-thumb-box {
max-height: 70px; max-height: 70px;
@@ -651,50 +555,27 @@
float: right; float: right;
} }
/* Break points determine when display cards shrink */ /* Tweak mobile display to show only one column */
@media only screen and (max-width: 700px) { @media only screen and (min-width: 1500px) {
.note-title-display-card {
width: calc(100% + 10px);
/*margin: 0px -5px 10px -5px;*/
}
}
@media only screen and (min-width: 700px) and (max-width: 900px) {
.note-title-display-card {
width: calc(50% - 10px);
}
}
@media only screen and (min-width: 900px) and (max-width: 1100px) {
.note-title-display-card {
width: calc(33.33333% - 10px);
}
}
@media only screen and (min-width: 1100px) and (max-width: 1300px) {
.note-title-display-card {
width: calc(25% - 10px);
}
}
@media only screen and (min-width: 1300px) and (max-width: 1800px) {
.note-title-display-card { .note-title-display-card {
width: calc(20% - 10px); width: calc(20% - 10px);
} }
} }
@media only screen and (min-width: 1800px) { @media only screen and (max-width: 740px) {
.note-title-display-card { .note-title-display-card {
width: calc(16.66666% - 10px); width: calc(100% + 10px);
margin: 0px -5px 10px -5px;
} }
} }
/*Animations for cool border effects*/ /*Animations for cool border effects*/
@keyframes bgin { @keyframes bgin {
0% { 0% {
background-image: background-image:
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* TopLeft to Right */ linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* TopLeft to Right */
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%), /* TopRight to Bottom */ linear-gradient(to bottom, #21BA45 50%, #21BA45 100%), /* TopRight to Bottom */
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* BottomLeft to Right*/ linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* BottomLeft to Right*/
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%); /* TopLeft to Bottom */ linear-gradient(to bottom, #21BA45 50%, #21BA45 100%); /* TopLeft to Bottom */
/*Initial state, no BG*/ /*Initial state, no BG*/
background-size: 0 4px, 4px 0, 0 4px, 4px 0; background-size: 0 4px, 4px 0, 0 4px, 4px 0;
} }
@@ -709,10 +590,10 @@
30% { 30% {
background-size: 100% 4px, 4px 100%, 100% 4px, 4px 100%; background-size: 100% 4px, 4px 100%, 100% 4px, 4px 100%;
background-image: background-image:
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* TopLeft to Right */ linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* TopLeft to Right */
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%), /* TopRight to Bottom */ linear-gradient(to bottom, #21BA45 50%, #21BA45 100%), /* TopRight to Bottom */
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* BottomLeft to Right*/ linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* BottomLeft to Right*/
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%); /* TopLeft to Bottom */ linear-gradient(to bottom, #21BA45 50%, #21BA45 100%); /* TopLeft to Bottom */
} }
100% { 100% {
background-image: background-image:
@@ -732,36 +613,4 @@
animation: bgin 4s cubic-bezier(0.19, 1, 0.22, 1) 1; 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> </style>

View File

@@ -1,116 +0,0 @@
<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,6 +35,7 @@
<i class="search icon"></i> <i class="search icon"></i>
</div> </div>
<div class="floating-button" v-if="searchTerm.length > 0"> <div class="floating-button" v-if="searchTerm.length > 0">
<i class="big link grey close icon" v-on:click="clear()"></i> <i class="big link grey close icon" v-on:click="clear()"></i>
</div> </div>
@@ -97,17 +98,13 @@
}, },
beforeCreate: function(){ beforeCreate: function(){
}, },
beforeMount(){ mounted: function(){
//search clear //search clear
this.$bus.$on('reset_fast_filters', () => { this.$bus.$on('reset_fast_filters', () => {
this.searchTerm = '' this.searchTerm = ''
this.tagSuggestions = [] this.tagSuggestions = []
}) })
},
beforeDestroy(){
this.$bus.$off('reset_fast_filters')
},
mounted: function(){
}, },
methods: { methods: {
@@ -151,6 +148,7 @@
if(response.data && response.data.id){ if(response.data && response.data.id){
this.$router.push('/notes/open/'+response.data.id) this.$router.push('/notes/open/'+response.data.id)
this.$bus.$emit('open_note', response.data.id)
} }
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to create note') }) .catch(error => { this.$bus.$emit('notification', 'Failed to create note') })

View File

@@ -12,7 +12,6 @@
<ul> <ul>
<li>Shared notes can be read and edited by you and all shared users.</li> <li>Shared notes can be read and edited by you and all shared users.</li>
<li>Shared notes can only be shared by the creator of the note.</li> <li>Shared notes can only be shared by the creator of the note.</li>
<li>If you turn off sharing, no one else can read the note.</li>
</ul> </ul>
</div> </div>

View File

@@ -1,9 +1,9 @@
<style type="text/css" scoped> <style type="text/css" scoped>
.slide-container { .slide-container {
position: absolute; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 50%;
bottom: 0; bottom: 0;
z-index: 1020; z-index: 1020;
overflow: hidden; overflow: hidden;
@@ -27,7 +27,7 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
color: red; color: red;
background-color: rgba(0,0,0,0.5); /*background-color: rgba(0,0,0,0.5);*/
/*background: linear-gradient(90deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 55%);*/ /*background: linear-gradient(90deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 55%);*/
z-index: 1019; z-index: 1019;
overflow: hidden; overflow: hidden;
@@ -88,19 +88,19 @@
<div class="slide-container" :style="{ 'background-color':bgColor, 'color':textColor}"> <div class="slide-container" :style="{ 'background-color':bgColor, 'color':textColor}">
<!-- close menu on bottom -->
<div class="note-menu">
<nm-button more-class="right" icon="close" text="close" :show-text="true" v-on:click.native="close" />
</div>
<!-- content of the editor --> <!-- content of the editor -->
<div class="slide-content"> <div class="slide-content">
<slot></slot> <slot></slot>
</div> </div>
<!-- close menu on bottom -->
<div class="note-menu">
<nm-button more-class="right" icon="close" text="close" :show-text="true" v-on:click.native="close" />
</div>
</div> </div>
<!-- <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> </div>
<!-- </transition> --> <!-- </transition> -->

View File

@@ -6,7 +6,7 @@
} }
.img-row { .img-row {
height: 20vh; height: 30vh;
flex-grow: 1; flex-grow: 1;
} }

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import 'es6-promise/auto' //Vuex likes promises
import store from './stores/mainStore'; import store from './stores/mainStore';
import App from './App' import App from './App'
@@ -13,19 +14,19 @@ import router from './router'
// import 'fomantic-ui-css/semantic.css'; // import 'fomantic-ui-css/semantic.css';
//Required site and reset CSS //Required site and reset CSS
import 'fomantic-ui-css/components/reset.min.css' import 'fomantic-ui-css/components/reset.css'
import 'fomantic-ui-css/components/site.css' //modified to remove included LATO fonts import 'fomantic-ui-css/components/site.css' //modified to remove included LATO fonts
//Only include parts that are used //Only include parts that are used
import 'fomantic-ui-css/components/button.min.css' import 'fomantic-ui-css/components/button.css'
import 'fomantic-ui-css/components/container.min.css' import 'fomantic-ui-css/components/container.css'
import 'fomantic-ui-css/components/form.min.css' import 'fomantic-ui-css/components/form.css'
import 'fomantic-ui-css/components/grid.min.css' import 'fomantic-ui-css/components/grid.css'
import 'fomantic-ui-css/components/header.min.css' import 'fomantic-ui-css/components/header.css'
import 'fomantic-ui-css/components/icon.css' //Modified to remove brand icons import 'fomantic-ui-css/components/icon.css' //Modified to remove brand icons
import 'fomantic-ui-css/components/input.min.css' import 'fomantic-ui-css/components/input.css'
import 'fomantic-ui-css/components/segment.min.css' import 'fomantic-ui-css/components/segment.css'
import 'fomantic-ui-css/components/label.min.css' import 'fomantic-ui-css/components/label.css'
//Overwrite and site styles and themes and good stuff //Overwrite and site styles and themes and good stuff
@@ -35,7 +36,7 @@ require('./assets/roboto-latin.woff2')
require('./assets/roboto-latin-bold.woff2') require('./assets/roboto-latin-bold.woff2')
require('./assets/squire.js')
//Import socket io, init using nginx configured socket path //Import socket io, init using nginx configured socket path
import io from 'socket.io-client'; import io from 'socket.io-client';
@@ -65,7 +66,9 @@ Vue.use(Vuex)
Vue.config.productionTip = false Vue.config.productionTip = false
new Vue({ new Vue({
router, el: '#app',
store, router,
render: h => h(App), store,
}).$mount('#app') components: { App },
template: '<App/>'
})

View File

@@ -9,10 +9,6 @@ const SquireButtonFunctions = {
activeList: false, activeList: false,
activeToDo: false, activeToDo: false,
activeColor: null, activeColor: null,
activeCode: false,
activeSubTitle: false,
//
lastUsedColor: null,
} }
}, },
methods: { methods: {
@@ -21,7 +17,6 @@ const SquireButtonFunctions = {
// //
pathChangeEvent(e){ pathChangeEvent(e){
//Reset all button states //Reset all button states
this.activeBold = false this.activeBold = false
this.activeTitle = false this.activeTitle = false
@@ -30,8 +25,6 @@ const SquireButtonFunctions = {
this.activeToDo = false this.activeToDo = false
this.activeColor = null this.activeColor = null
this.activeUnderline = false this.activeUnderline = false
this.activeCode = false
this.activeSubTitle = false
if(e.path.indexOf('>U>') > -1 || e.path.search(/U$/) > -1){ if(e.path.indexOf('>U>') > -1 || e.path.search(/U$/) > -1){
this.activeUnderline = true this.activeUnderline = true
@@ -42,29 +35,20 @@ const SquireButtonFunctions = {
if(e.path.indexOf('>I') > -1){ if(e.path.indexOf('>I') > -1){
this.activeItalics = true this.activeItalics = true
} }
if(e.path.indexOf('fontSize=1.4em') > -1){ if(e.path.indexOf('fontSize') > -1){
this.activeTitle = true this.activeTitle = true
} }
if(e.path.indexOf('fontSize=0.9em') > -1){
this.activeSubTitle = true
}
if(e.path.indexOf('OL>LI') > -1){ if(e.path.indexOf('OL>LI') > -1){
this.activeList = true this.activeList = true
} }
if(e.path.indexOf('UL>LI') > -1){ if(e.path.indexOf('UL>LI') > -1){
this.activeToDo = true this.activeToDo = true
} }
if(e.path.indexOf('CODE') > -1){
this.activeCode= true
}
const colorIndex = e.path.indexOf('color=') const colorIndex = e.path.indexOf('color=')
if(colorIndex > -1){ if(colorIndex > -1){
//Get all digigs after color index, then limit to 3 //Get all digigs after color index, then limit to 3
let colors = e.path.substring(colorIndex).match(/\d+/g).slice(0,3) let colors = e.path.substring(colorIndex).match(/\d+/g).slice(0,3)
this.activeColor=`rgb(${colors.join(',')})`
const lastColor = `rgb(${colors.join(',')})`
this.activeColor = lastColor
this.lastUsedColor = lastColor
} }
}, },
@@ -108,11 +92,6 @@ const SquireButtonFunctions = {
this.selectLineIfNoSelect() this.selectLineIfNoSelect()
//Set color of font //Set color of font
this.editor.setTextColour(color) this.editor.setTextColour(color)
this.lastUsedColor = color
},
applyLastUsedColor(){
this.modifyColor(this.lastUsedColor)
}, },
toggleList(type){ toggleList(type){
@@ -153,12 +132,6 @@ const SquireButtonFunctions = {
this.editor.italic() this.editor.italic()
} }
}, },
modifyCode(){
this.selectLineIfNoSelect()
this.editor.toggleCode()
},
undoCustom(){ undoCustom(){
//The same as pressing CTRL + Z //The same as pressing CTRL + Z
// this.editor.focus() // this.editor.focus()
@@ -170,158 +143,151 @@ const SquireButtonFunctions = {
// Uncheck All List Items // Uncheck All List Items
// //
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container //Fetch the container
let container = document.getElementById('squire-id') let container = document.getElementById('squire-id')
this.$router.go(-1)
setTimeout(()=>{
Array.from( container.getElementsByClassName('active') ).forEach(item => {
item.classList.remove('active');
})
},600)
Array.from( container.getElementsByClassName('active') ).forEach(item => {
item.classList.remove('active');
})
}, },
deleteCompletedListItems(){ deleteCompletedListItems(){
// //
// Delete Completed List Items // Delete Completed List Items
// //
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container //Fetch the container
let container = document.getElementById('squire-id') let container = document.getElementById('squire-id')
//Close menu if user is on mobile, then sort list //Go through each item, on first level, look for Unordered Lists
this.$router.go(-1) container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
setTimeout(()=>{ //Create two categories, done and not done list items
let undoneElements = document.createDocumentFragment()
//Go through each item, on first level, look for Unordered Lists //Go through each item in each list we found
container.childNodes.forEach( (node) => { node.childNodes.forEach( (checkListItem, index) => {
if(node.nodeName == 'UL'){
//Create two categories, done and not done list items //Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
let undoneElements = document.createDocumentFragment() if(checkListItem.nodeName == 'UL'){
return
}
//Go through each item in each list we found //Check if list item has active class
node.childNodes.forEach( (checkListItem, index) => { const checkedItem = checkListItem.classList.contains('active')
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together //Check if the next item is a list, Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){ let sublist = null
return if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(!checkedItem){
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
} }
//Check if list item has active class }
const checkedItem = checkListItem.classList.contains('active')
//Check if the next item is a list, Keep lists with intented items together })
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(!checkedItem){
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
})
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
}
})
}, 600)
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
}
})
}, },
sortList(){ sortList(){
// //
// Sort list, checked at the bottom, unchecked at the top // Sort list, checked at the bottom, unchecked at the top
// //
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container //Fetch the container
let container = document.getElementById('squire-id') let container = document.getElementById('squire-id')
//Close menu if user is on mobile //Go through each item, on first level, look for Unordered Lists
this.$router.go(-1) container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
setTimeout(()=>{ //Create two categories, done and not done list items
let doneElements = document.createDocumentFragment()
let undoneElements = document.createDocumentFragment()
//Go through each item, on first level, look for Unordered Lists //Go through each item in each list we found
container.childNodes.forEach( (node) => { node.childNodes.forEach( (checkListItem, index) => {
if(node.nodeName == 'UL'){
//Create two categories, done and not done list items //Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
let doneElements = document.createDocumentFragment() if(checkListItem.nodeName == 'UL'){
let undoneElements = document.createDocumentFragment() return
}
//Go through each item in each list we found //Check if list item has active class
node.childNodes.forEach( (checkListItem, index) => { const checkedItem = checkListItem.classList.contains('active')
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together //Check if the next item is a list, Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){ let sublist = null
return if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(checkedItem){
doneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
doneElements.appendChild( sublist.cloneNode(true) )
} }
//Check if list item has active class } else {
const checkedItem = checkListItem.classList.contains('active')
//Check if the next item is a list, Keep lists with intented items together undoneElements.appendChild( checkListItem.cloneNode(true) )
let sublist = null if(sublist){
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){ undoneElements.appendChild( sublist.cloneNode(true) )
sublist = node.childNodes[index+1]
} }
}
//Push checked items and their sub lists to the done set })
if(checkedItem){
doneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
doneElements.appendChild( sublist.cloneNode(true) )
}
} else {
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
})
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
node.appendChild(doneElements)
}
})
},600)
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
node.appendChild(doneElements)
}
})
}, },
calculateMath(){ calculateMath(){
// //
// Find math in note and calculate the outcome // Find math in note and calculate the outcome
// //
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container //Fetch the container
let container = document.getElementById('squire-id') 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 // simple function that trys to evaluate javascript
const shittyMath = (string) => { const shittyMath = (string) => {
//Remove all chars but math chars //Remove all chars but math chars
@@ -334,109 +300,44 @@ const SquireButtonFunctions = {
} }
} }
setTimeout(()=>{ //Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
//Go through each item, on first level, look for Unordered Lists const line = node.innerText.trim()
container.childNodes.forEach( (node) => {
const line = node.innerText.trim() // = sign exists and its the last character in the string
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
// = sign exists and its the last character in the string //Pull out everything before the formula and try to evaluate it
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){ const formula = line.split('=').shift()
const output = shittyMath(formula)
//Pull out everything before the formula and try to evaluate it //If its a number and didn't throw an error, update the line
const formula = line.split('=').shift() if(!isNaN(output) && output != null){
const output = shittyMath(formula)
//If its a number and didn't throw an error, update the line //Since there is HTML in the line, splice in the number after the = sign
if(!isNaN(output) && output != null){ let equalLocation = node.innerHTML.indexOf('=')
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
newLine += ` ${output}`
newLine += node.innerHTML.slice(equalLocation+1).trim()
//Since there is HTML in the line, splice in the number after the = sign //Slam in that new HTML with the output
let equalLocation = node.innerHTML.indexOf('=') node.innerHTML = newLine
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
newLine += ` ${output}`
newLine += node.innerHTML.slice(equalLocation+1).trim()
//Slam in that new HTML with the output
node.innerHTML = newLine
}
} }
}
}) })
},600)
}, },
setText(inText){ setText(inText){
this.editor.setHTML(inText) this.editor.setHTML(inText)
// this.noteText = this.editor._getHTML() // this.noteText = this.editor._getHTML()
// this.diffNoteText = 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(){ getText(){
return this.editor.getHTML() return this.editor.getHTML()
}, },
insertDivide(){
this.editor.insertHTML(`<p><div class='divide'></div><br></p>`)
this.editor.focus()
this.editor.moveCursorToEnd()
},
insertTable(tall, wide){
console.log(`Table: ${wide} x ${tall}`)
//Insert a table
let tableSyntax = '<div>'
tableSyntax += '<table>'
for (let i = 0; i < tall; i++) {
tableSyntax += '<tr>'
for (let j = 0; j < wide; j++) {
tableSyntax += '<td><p><br></p></td>'
}
tableSyntax += '</tr>'
}
tableSyntax += '</table></div><p><br></p>'
this.editor.insertHTML(tableSyntax)
this.editor.focus()
this.editor.moveCursorToEnd()
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,13 +8,6 @@
<div class="content"> <div class="content">
Files Files
<div class="sub header">Uploaded Files and Websites from notes.</div> <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> </div>
</h2> </h2>
@@ -43,32 +36,6 @@
Other Files Other Files
</router-link> </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>
<div class="sixteen wide column" v-if="searchParams.noteId"> <div class="sixteen wide column" v-if="searchParams.noteId">
@@ -132,11 +99,6 @@
//Load more attachments on scroll //Load more attachments on scroll
window.addEventListener('scroll', this.onScroll) window.addEventListener('scroll', this.onScroll)
this.$io.on('update_note_attachments', () => {
this.reset()
this.searchAttachments()
})
//Mount notes on load if note ID is set //Mount notes on load if note ID is set
this.searchAttachments() this.searchAttachments()
}, },
@@ -144,8 +106,6 @@
//Remove scroll event on destroy //Remove scroll event on destroy
window.removeEventListener('scroll', this.onScroll) window.removeEventListener('scroll', this.onScroll)
this.$io.removeListener('update_note_attachments')
}, },
watch:{ watch:{
$route (to, from){ $route (to, from){
@@ -205,12 +165,6 @@
this.searchParams.attachmentType = this.$route.params.type 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 //Set noteId in if in URL
if(this.$route.params.id){ if(this.$route.params.id){
this.searchParams.noteId = this.$route.params.id this.searchParams.noteId = this.$route.params.id

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -11,29 +11,7 @@
-moz-animation: fadeorama 16s ease infinite; -moz-animation: fadeorama 16s ease infinite;
animation: fadeorama 16s ease infinite; animation: fadeorama 16s ease infinite;
height: 350px; height: 350px;
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;
width: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: none;
}
.spotlight {
background: rgba(255,255,255,0);
background: radial-gradient(circle at bottom, var(--main-accent) 0%, rgba(255,255,255,0) 60%);
z-index: 200;
}
.logo-display { .logo-display {
width: 140px; width: 140px;
height: auto; height: auto;
@@ -46,14 +24,10 @@
font-size: 4rem; font-size: 4rem;
text-align: center; text-align: center;
} }
div#app div.lightly-padded.home-main div.ui.centered.vertically.divided.stackable.grid div.row.hero.fadeBg div.sixteen.wide.middle.aligned.center.column h2.massive-text svg.logo-display path {
stroke: black !important;
stroke-width: 1px !important;
}
.blinking { .blinking {
animation:blinkingText 1.5s linear infinite; animation:blinkingText 1.5s linear infinite;
} }
@keyframes blinkingText { @keyframes blinkingText{
0%{ opacity: 0.9; } 0%{ opacity: 0.9; }
50%{ opacity: 0; } 50%{ opacity: 0; }
100%{ opacity: 0.9; } 100%{ opacity: 0.9; }
@@ -127,17 +101,16 @@
<!-- <div class="one wide large screen only column"></div> --> <!-- <div class="one wide large screen only column"></div> -->
<!-- desktop column - large screen only --> <!-- desktop column - large screen only -->
<div class="sixteen wide middle aligned center aligned column" style="z-index: 500;"> <div class="sixteen wide middle aligned center aligned column">
<h2 class="massive-text"> <h2 class="massive-text">
<!-- <img class="logo-display" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo"> --> <img class="logo-display" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
<logo class="logo-display" color="var(--main-accent)" stroke="true" />
<br> <br>
Solid Scribe Solid Scribe
</h2> </h2>
<h3 class="subtext"> <h3 class="subtext">
A free, secure, online note taking application<i class="i cursor icon blinking"></i> A free, secure Note App<i class="i cursor icon blinking"></i>
</h3> </h3>
</div> </div>
@@ -146,37 +119,32 @@
<img loading="lazy" width="90%" src="/api/static/assets/marketing/notebook.svg" alt="The Venus fly laptop about to capture another victim"> <img loading="lazy" width="90%" src="/api/static/assets/marketing/notebook.svg" alt="The Venus fly laptop about to capture another victim">
</div> --> </div> -->
<div v-for="i in jewelFacets" class="shine" :style="shineStyle(i)" v-bind:key="i"></div>
<div class="shine spotlight"></div>
</div> </div>
<!-- All marketing images if you need to review --> <!-- All marketing images if you need to review -->
<div v-if="false" class="sixteen wide column"> <div v-if="false" class="sixteen wide column">
<img loading="lazy" width="10%" src="/api/static/assets/marketing/add.svg"> <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"> <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"> <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"> <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"> <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"> <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"> <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"> <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"> <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"> <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"> <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"> <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"> <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"> <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"> <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"> <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"> <img loading="lazy" width="10%" src="/api/static/assets/marketing/void.svg" alt="">
</div> </div>
<!-- Go to notes button --> <!-- Go to notes button -->
<div class="row" v-if="$parent.loggedIn"> <div class="row" v-if="$parent.loggedIn">
<div class="sixteen wide middle algined center aligned column"> <div class="sixteen wide middle algined center aligned column">
<h3>You are already logged in</h3>
<router-link class="ui huge green labeled icon button" to="/notes"> <router-link class="ui huge green labeled icon button" to="/notes">
<i class="external alternate icon"></i>Go to Notes <i class="external alternate icon"></i>Go to Notes
</router-link> </router-link>
@@ -199,78 +167,120 @@
<!-- Overview --> <!-- Overview -->
<div class="middle aligned centered row"> <div class="middle aligned centered row">
<div class="six wide column"> <div class="six wide column">
<h2 class="ui dividing header">Powerful text editing and privacy</h2> <h2>Solid Scribe focuses on powerful text editing and user privacy</h2>
<h3>Easily edit, share and organize thousands of notes.</h3> <h3>Tools to organize and collaborate on thousands of notes while maintaining security and respecting your privacy.</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>
<div class="four wide column"> <div class="four wide column">
<svg-displayer file="idea" alt="Explosion of New Ideas" /> <img loading="lazy" width="100%" src="/api/static/assets/marketing/idea.svg" 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 style="color: var(--main-accent);">
Pick your theme
</h2>
<h3 v-if="$parent.loggedIn">Go to settings to change theme</h3>
<div
v-for="color in themeColors"
v-bind:key="color"
class="ui small basic button"
:style="`background: linear-gradient(0deg, ${color} 4%, rgba(0,0,0,0) 5%);`"
v-on:click="setAccentColor(color)">
<logo style="width: 20px; height: auto;" :color="color" />
</div>
</div>
</div> </div>
</div> </div>
<!-- features list --> <!-- features list -->
<div class="top aligned centered row"> <div class="middle aligned centered row">
<div class="sixteen wide column">
<!-- note features --> <h1 class="ui center aligned header"><i class="sliders horizontal icon"></i>Features</h1>
</div>
<div class="six wide column"> <div class="six wide column">
<h2 class="ui dividing header">
<h1 class="ui center aligned dividing header"><i class="small green sliders horizontal icon"></i>App Features</h1>
<h2 class="ui header">
<div class="content"> <div class="content">
<i class="icons"> <i class="icons">
<i class="grey sticky note icon"></i> <i class="grey lock icon"></i>
<i class="bottom left corner teal plus icon"></i> <i class="bottom left corner yellow key icon"></i>
</i> </i>
Create a million notes! Privacy Focused
<div class="sub header">Create unlimited notes up to 5,000,000 characters long.</div> <div class="sub header">All note text is encrypted. No one can read your notes. None of your data is shared.</div>
</div> </div>
</h2> </h2>
<h2 class="ui header"> <h2 class="ui dividing header">
<div class="content">
<i class="icons">
<i class="grey list icon"></i>
<i class="bottom left corner green check icon"></i>
</i>
To Do Lists
<div class="sub header">Create To Do lists that are always synced, work on mobile and can be sorted.</div>
</div>
</h2>
<h2 class="ui dividing header">
<div class="content">
<i class="icons">
<i class="grey file icon"></i>
<i class="bottom left corner blue pen icon"></i>
</i>
Document Editing Tools
<div class="sub header">Bold, Underline, Title, Add Links, Add Tables Color Text, Color Background and more.</div>
</div>
</h2>
<h2 class="ui dividing header">
<div class="content"> <div class="content">
<i class="icons"> <i class="icons">
<i class="grey tags icon"></i> <i class="grey tags icon"></i>
<i class="bottom left corner purple plus icon"></i> <i class="bottom left corner purple plus icon"></i>
</i> </i>
Tag Notes Tag Notes
<div class="sub header">Add and edit tags on notes then search or sort by tag.</div> <div class="sub header">Easily add and edit tags on notes then sort notes by tag.</div>
</div> </div>
</h2> </h2>
<h2 class="ui header"> <h2 class="ui dividing header">
<div class="content">
<i class="icons">
<i class="grey images icon"></i>
<i class="bottom left corner teal paperclip icon"></i>
</i>
Add Images
<div class="sub header">Upload images to notes, add search text to the images to find them later.</div>
</div>
</h2>
<h2 class="ui dividing header">
<div class="content">
<i class="icons">
<i class="grey cloud moon icon"></i>
<i class="bottom left corner red eye icon"></i>
</i>
Night Mode
<div class="sub header">Pure black night theme with an even darker flux theme.</div>
</div>
</h2>
<h2 class="ui dividing header">
<div class="content">
<i class="icons">
<i class="grey share alternate icon"></i>
<i class="bottom left corner share icon"></i>
</i>
Share Encrypted Notes
<div class="sub header">Share notes with friends without compromising security. And its easy to disable sharing.</div>
</div>
</h2>
<h2 class="ui dividing header">
<div class="content">
<i class="icons">
<i class="grey users icon"></i>
<i class="bottom left corner olive exchange icon"></i>
</i>
Collaborative Note Editing
<div class="sub header">Notes instantly update in real time everywhere the note is open.</div>
</div>
</h2>
<h2 class="ui dividing header">
<div class="content"> <div class="content">
<i class="icons"> <i class="icons">
<i class="grey search icon"></i> <i class="grey search icon"></i>
<i class="bottom left corner orange font icon"></i> <i class="bottom left corner orange font icon"></i>
</i> </i>
Search Note Text Keyword Search
<div class="sub header">Search all notes, files, links and tags.</div> <div class="sub header">Easily search all notes. Encrypted search index ensures privacy and convenience.</div>
</div> </div>
</h2> </h2>
<h2 class="ui header"> <h2 class="ui dividing header">
<div class="content"> <div class="content">
<i class="icons"> <i class="icons">
<i class="grey search icon"></i> <i class="grey search icon"></i>
@@ -281,131 +291,17 @@
</div> </div>
</h2> </h2>
<h2 class="ui header">
<div class="content">
<i class="icons">
<i class="grey cloud moon icon"></i>
<i class="bottom left corner red eye icon"></i>
</i>
Night Mode
<div class="sub header">Pure black night theme with an even darker flux theme.</div>
</div>
</h2>
</div> </div>
<div class="four wide column">
<!-- editing features --> <img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="">
<div class="six wide column">
<h1 class="ui center aligned dividing header"><i class="small green sliders horizontal icon"></i>Editing Features</h1>
<h2 class="ui header">
<div class="content">
<i class="icons">
<i class="grey list icon"></i>
<i class="bottom left corner green check icon"></i>
</i>
Create To Do Lists
<div class="sub header">Create To Do lists that are always synced, work on mobile and can be sorted.</div>
</div>
</h2>
<h2 class="ui header">
<div class="content">
<i class="icons">
<i class="grey file icon"></i>
<i class="bottom left corner blue pen icon"></i>
</i>
Formatting Tools
<div class="sub header">Bold, Underline, Title, Add Links, Add Tables, Color Text, Color Background and more.</div>
</div>
</h2>
<h2 class="ui header">
<div class="content">
<i class="icons">
<i class="grey file icon"></i>
<i class="bottom left corner orange paint brush icon"></i>
</i>
Customized Colorful Notes
<div class="sub header">Color the background of notes and add colored icons to make them stand out.</div>
</div>
</h2>
<h2 class="ui header">
<div class="content">
<i class="icons">
<i class="grey images icon"></i>
<i class="bottom left corner teal paperclip icon"></i>
</i>
Add Images
<div class="sub header">Upload images to notes, add search text to the images to find them later.</div>
</div>
</h2>
<h2 class="ui header">
<div class="content">
<i class="icons">
<i class="grey users icon"></i>
<i class="bottom left corner olive exchange icon"></i>
</i>
Collaborative Note Editing
<div class="sub header">Notes instantly update in real time everywhere its open and anywhere its shared.</div>
</div>
</h2>
</div> </div>
</div>
<div class="middle aligned centered row">
<!-- privacy features -->
<div class="six wide column">
<h1 class="ui center aligned dividing header"><i class="small green sliders horizontal icon"></i>Privacy Features</h1>
<h2 class="ui header">
<div class="content">
<i class="icons">
<i class="grey lock icon"></i>
<i class="bottom left corner yellow key icon"></i>
</i>
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>
<h2 class="ui header">
<div class="content">
<i class="icons">
<i class="grey search icon"></i>
<i class="bottom left corner orange font icon"></i>
</i>
Private Search
<div class="sub header">Search the contents of all your notes without compromising security.</div>
</div>
</h2>
<h2 class="ui header">
<div class="content">
<i class="icons">
<i class="grey share alternate icon"></i>
<i class="bottom left corner share icon"></i>
</i>
Encrypted Sharing
<div class="sub header">Shared notes are still encrypted, only readable by you and the shared users.</div>
</div>
</h2>
<h2 class="ui header">
<div class="content">
<i class="icons">
<i class="grey tv icon"></i>
<i class="bottom left corner blue mobile icon"></i>
</i>
Two Factor Authentication
<div class="sub header">Enable two factor authentication for added peace of mind.</div>
</div>
</h2>
</div>
<div class="six wide column">
<svg-displayer file="onboarding" alt="Observe this chart" />
</div>
</div> </div>
<div class="middle aligned centered row"> <div class="middle aligned centered row">
<div class="four wide right aligned column"> <div class="four wide right aligned column">
<svg-displayer file="secure" alt="So dang secure" /> <img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
</div> </div>
<div class="six wide column"> <div class="six wide column">
<h2>Only you can read your notes. </h2> <h2>Only you can read your notes. </h2>
@@ -419,13 +315,13 @@
<h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3> <h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3>
</div> </div>
<div class="four wide right aligned column"> <div class="four wide right aligned column">
<svg-displayer file="cloud" alt="Girl falling into the spiral of digital chaos" /> <img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos">
</div> </div>
</div> </div>
<div class="middle aligned centered row"> <div class="middle aligned centered row">
<div class="four wide right aligned column"> <div class="four wide right aligned column">
<svg-displayer file="robot" alt="Murder Robot in office environment" /> <img loading="lazy" width="100%" src="/api/static/assets/marketing/robot.svg" alt="Shrunken man near giant tablet">
</div> </div>
<div class="six wide column"> <div class="six wide column">
<h2>Secure Data Sharing</h2> <h2>Secure Data Sharing</h2>
@@ -440,11 +336,11 @@
<h2>Leave your Ad Blockers turned on</h2> <h2>Leave your Ad Blockers turned on</h2>
<h3>SolidScribe doesn't load any trackers or ads. It was designed to run on <h3>SolidScribe doesn't load any trackers or ads. It was designed to run on
<a href="https://www.mozilla.org/en-US/firefox/new/" target="_blank">Firefox</a>, with <a href="https://www.mozilla.org/en-US/firefox/new/" target="_blank">Firefox</a>, with
<a href="https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/" target="_blank">an Ad Blocker</a> turned on. It even works with a <a href="https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/" target="_blank">uBlock Origin</a> and a
<a href="https://pi-hole.net/" target="_blank">Pi-hole</a> on the network.</h3> <a href="https://pi-hole.net/" target="_blank">Pi-hole</a> on the network.</h3>
</div> </div>
<div class="four wide column"> <div class="four wide column">
<svg-displayer file="icecream" alt="Emergence of a 4th dimensional being perceived as a large ice cream" /> <img loading="lazy" width="100%" src="/api/static/assets/marketing/icecream.svg" alt="Emergence of a 4th dimensional being perceived as a large ice cream ">
</div> </div>
</div> </div>
@@ -493,12 +389,7 @@
<div v-if="true" class="middle aligned centered row"> <div v-if="true" class="middle aligned centered row">
<div class="six wide column"> <div class="six wide column">
<h3> <h2>Solid Scribe was created by one passionate developer</h2>
<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> <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. I was tired of all my data being owned by big companies, having it farmed out for marketing, and leaving the contents of my life exposed to corporations.
</p> </p>
@@ -506,10 +397,9 @@
If you want to give it a shot, feel free to make an account. There are no ads. None of this data is shared or public. I don't make any money. If you want to give it a shot, feel free to make an account. There are no ads. None of this data is shared or public. I don't make any money.
</p> </p>
<p> <p>
If you see anything broken or want to see a feature implemented; I'm open to suggestions. <i class="thumbs up icon"></i> If you see anything broken or want to see a feature implemented, I'm open to suggestions. <i class="thumbs up icon"></i>
</p> </p>
<p>Email me at <a href="mailto:maxgialanella@pm.me">Max.Gialanella@pm.me</a></p> <p>If you want to help me out, I would love a small Bitcoin donation.</p>
<p>If you want to help me out with hosting this application, I would love a small Bitcoin donation.</p>
<p> <p>
<a href="https://btc3.trezor.io/address/3QYnnNKnYTcU82F8NJ1BrmzGU2zRndTyEG" target="_blank"> <a href="https://btc3.trezor.io/address/3QYnnNKnYTcU82F8NJ1BrmzGU2zRndTyEG" target="_blank">
<img loading="lazy" width="160px" src="/api/static/assets/marketing/wallet.png" alt="3QYnnNKnYTcU82F8NJ1BrmzGU2zRndTyEG"> <img loading="lazy" width="160px" src="/api/static/assets/marketing/wallet.png" alt="3QYnnNKnYTcU82F8NJ1BrmzGU2zRndTyEG">
@@ -518,14 +408,10 @@
<p>Awesomely Generic Marketing Images - <a target="_blank" href="https://undraw.co/">https://unDraw.co/</a></p> <p>Awesomely Generic Marketing Images - <a target="_blank" href="https://undraw.co/">https://unDraw.co/</a></p>
</div> </div>
<div class="four wide column"> <div class="four wide column">
<svg-displayer file="watching" alt="Drinking the blood of the elderly" /> <img loading="lazy" width="100%" src="/api/static/assets/marketing/watching.svg" alt="Drinking the blood of the elderly">
</div> </div>
</div> </div>
<div class="center aligned sixteen wide column">
<router-link to="/terms">Solid Scribe Terms of Use</router-link>
</div>
</div> </div>
</div> </div>
@@ -536,28 +422,11 @@ export default {
name: 'WelcomePage', name: 'WelcomePage',
components: { components: {
'login-form':require('@/components/LoginFormComponent.vue').default, 'login-form':require('@/components/LoginFormComponent.vue').default,
'logo':require('@/components/LogoComponent.vue').default,
'svg-displayer':require('@/components/SvgDisplayer.vue').default,
}, },
data(){ data(){
return { return {
height: null, height: null,
realInformation: false, realInformation: false,
jewelFacets: 15,
themeColors: [
'#21BA45', //Green
'#b5cc18', //Lime
'#00b5ad', //Teal
'#2185d0', //Blue
'#7128b9', //Violet
'#a333c8', //Purple
'#e03997', //Pink
'#db2828', //Red
'#f2711c', //Orange
'#fbbd08', //Yellow
'#767676', //Grey
'#303030', //Black-almost
],
} }
}, },
beforeCreate(){ beforeCreate(){
@@ -571,40 +440,6 @@ export default {
}, },
methods: { methods: {
shineStyle(i){
const farMax = 95 //85
const farMin = 83
const farOut = (Math.floor(Math.random() * (farMax - farMin + 1)) + farMin)
// const rotation = 360/this.jewelFacets
const rotMax = 360/this.jewelFacets
const rotMin = 320/this.jewelFacets
const rotation = (Math.floor(Math.random() * (rotMax - rotMin + 1)) + rotMin)
let style = `
background: linear-gradient(
${(i+1)*(rotation)}deg,
rgba(255,255,255,0) ${farOut}%,
rgba(255,255,255,0.1) ${farOut+1}%,
rgba(255,255,255,0.0) ${farOut+10}%
)
;`
// Remove whitespace - Make it 1 line
return style.replace(/\s+/g, '')
},
setAccentColor(color){
let root = document.documentElement
root.style.setProperty('--main-accent', color)
localStorage.setItem('main-accent', color)
if(!color || color == '#21BA45'){
localStorage.removeItem('main-accent')
}
},
showRealInformation(){ showRealInformation(){

View File

@@ -25,7 +25,7 @@
</div> </div>
<p>You will remain logged in on this browser, for 20 days or until you log out.</p> <p>You will remain logged in on this browser, for 30 days or until you log out.</p>
</div> </div>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="page-container"> <div class="ui basic segment no-fluf-segment">
<div class="ui grid" ref="content"> <div class="ui grid" ref="content">
<div class="sixteen wide column"> <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"> <div class="ui stackable grid">
@@ -12,12 +12,6 @@
<search-input /> <search-input />
</div> </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="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
<div class="ui basic button shrinking" <div class="ui basic button shrinking"
@@ -25,17 +19,18 @@
v-if="$store.getters.totals && ($store.getters.totals['youGotMailCount'] > 0)" v-if="$store.getters.totals && ($store.getters.totals['youGotMailCount'] > 0)"
style="position: relative;"> style="position: relative;">
<i class="green mail icon"></i>Inbox <i class="green mail icon"></i>Inbox
<span class="tiny circular floating ui green label">+{{ $store.getters.totals['youGotMailCount'] }}</span> +{{ $store.getters.totals['youGotMailCount'] }}
</div> </div>
<tag-display <tag-display
v-if="$store.getters.totals && Object.keys($store.getters.totals['tags'] || {}).length"
:user-tags="$store.getters.totals['tags']"
:active-tags="searchTags" :active-tags="searchTags"
v-on:tagClick="tagId => toggleTagFilter(tagId)" v-on:tagClick="tagId => toggleTagFilter(tagId)"
/> />
<paste-button /> <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>
@@ -50,7 +45,7 @@
</div> </div>
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !showLoading"> <div class="sixteen wide column" v-if="searchTerm.length > 0 && !loadingInProgress">
<h2 class="ui header"> <h2 class="ui header">
<div class="content"> <div class="content">
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}" {{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}"
@@ -62,15 +57,11 @@
</div> </div>
<div v-if="fastFilters['onlyArchived'] == 1" class="sixteen wide column"> <div v-if="fastFilters['onlyArchived'] == 1" class="sixteen wide column">
<h2> <h2>Archived Notes</h2>
<i class="green archive icon"></i>
Archived Notes</h2>
</div> </div>
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1"> <div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
<h2> <h2>Trash
<i class="green trash alternate outline icon"></i>
Trashed Notes
<span>({{ $store.getters.totals['trashedNotes'] }})</span> <span>({{ $store.getters.totals['trashedNotes'] }})</span>
<div class="ui right floated basic button" data-tooltip="This doesn't work yet"> <div class="ui right floated basic button" data-tooltip="This doesn't work yet">
<i class="poo storm icon"></i> <i class="poo storm icon"></i>
@@ -80,8 +71,7 @@
</div> </div>
<div class="sixteen wide column" v-if="fastFilters['onlyShowSharedNotes'] == 1"> <div class="sixteen wide column" v-if="fastFilters['onlyShowSharedNotes'] == 1">
<h2><i class="green paper plane outline icon"></i> <h2>Shared Notes</h2>
Shared Notes</h2>
</div> </div>
<div class="sixteen wide column" v-if="tagSuggestions.length > 0"> <div class="sixteen wide column" v-if="tagSuggestions.length > 0">
@@ -92,57 +82,6 @@
</div> </div>
</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 --> <!-- found attachments -->
<div class="sixteen wide column" v-if="foundAttachments.length > 0"> <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> <h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
@@ -154,24 +93,52 @@
/> />
</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() )}">
<!-- 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 || activeNoteId2 == 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> </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>
<!-- flexbox note container evenly spaces open notes --> <input-notes
<div class="note-panel-container" :class="{ 'note-panel-fullwidth':collapseFloatingList}" v-if="openNotes.length"> v-if="activeNoteId1 != null"
<note-input-panel :key="'active_note_'+activeNoteId1"
v-for="noteId in openNotes" :noteid="activeNoteId1"
v-if="noteId != null" :position="activeNote1Position"
:key="noteId" :url-data="$route.params"
:noteid="noteId" ref="note1" />
:url-data="$route.params"
:open-notes="openNotes.length"
/>
</div>
</div> </div>
</template> </template>
@@ -181,18 +148,18 @@
import axios from 'axios' import axios from 'axios'
export default { export default {
name: 'NotesPage', name: 'SearchBar',
components: { components: {
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'), 'input-notes': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default, 'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
// 'fast-filters': require('@/components/FastFilters.vue').default, // 'fast-filters': require('@/components/FastFilters.vue').default,
'search-input': require('@/components/SearchInput.vue').default, 'search-input': require('@/components/SearchInput.vue').default,
'attachment-display': require('@/components/AttachmentDisplayCard').default, 'attachment-display': require('@/components/AttachmentDisplayCard').default,
'counter':require('@/components/AnimatedCounterComponent.vue').default,
'tag-display':require('@/components/TagDisplayComponent.vue').default, 'tag-display':require('@/components/TagDisplayComponent.vue').default,
'loading-icon':require('@/components/LoadingIconComponent.vue').default, 'loading-icon':require('@/components/LoadingIconComponent.vue').default,
'paste-button':require('@/components/PasteButton.vue').default,
}, },
data () { data () {
return { return {
@@ -202,8 +169,6 @@
searchResultsCount: 0, searchResultsCount: 0,
searchTags: [], searchTags: [],
notes: [], notes: [],
openNotes: [],
collapseFloatingList: false,
highlights: [], highlights: [],
searchDebounce: null, searchDebounce: null,
fastFilters: {}, fastFilters: {},
@@ -211,10 +176,10 @@
//Load up notes in batches //Load up notes in batches
firstLoadBatchSize: 10, //First set of rapidly loaded notes firstLoadBatchSize: 10, //First set of rapidly loaded notes
batchSize: 20, //Size of batch loaded when user scrolls through current batch batchSize: 25, //Size of batch loaded when user scrolls through current batch
batchOffset: 0, //Tracks the current batch that has been loaded batchOffset: 0, //Tracks the current batch that has been loaded
loadingBatchTimeout: null, //Limit how quickly batches can be loaded loadingBatchTimeout: null, //Limit how quickly batches can be loaded
showLoading: false, loadingInProgress: false,
scrollLoadEnabled: true, scrollLoadEnabled: true,
//Clear button is not visible //Clear button is not visible
@@ -260,39 +225,37 @@
this.$parent.loginGateway() this.$parent.loginGateway()
//If user is on title view,
this.titleView = this.$store.getters.getIsUserOnMobile
this.$io.on('new_note_created', noteId => { this.$io.on('new_note_created', noteId => {
// Push new note to top of list and animate //Do not update note if its open
this.updateSingleNote(noteId) if(this.activeNoteId1 != noteId){
this.$store.dispatch('fetchAndUpdateUserTotals') this.$store.dispatch('fetchAndUpdateUserTotals')
this.updateSingleNote(noteId, false)
}
}) })
this.$io.on('note_attribute_modified', noteId => { 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 //Do not update note if its open
if(this.openNotes.includes(parseInt(noteId))){ if(this.activeNoteId1 != noteId){
this.$store.dispatch('fetchAndUpdateUserTotals') this.$store.dispatch('fetchAndUpdateUserTotals')
this.updateSingleNote(noteId, false)
} }
}) })
//Update title cards when new note text is saved //Update title cards when new note text is saved
this.$io.on('new_note_text_saved', ({noteId, hash}) => { this.$io.on('new_note_text_saved', ({noteId, hash}) => {
const drawFocus = !this.openNotes.includes(parseInt(noteId)) //Do not update note if its open
this.updateSingleNote(noteId, drawFocus) if(this.activeNoteId1 != noteId){
this.updateSingleNote(noteId, false)
}
}) })
this.$bus.$on('update_single_note', (noteId) => { this.$bus.$on('update_single_note', (noteId) => {
//Do not update note if its open
const drawFocus = !this.openNotes.includes(parseInt(noteId)) if(this.activeNoteId1 != noteId){
this.updateSingleNote(noteId, drawFocus) this.updateSingleNote(noteId)
}
}) })
//Update totals for app //Update totals for app
@@ -300,7 +263,11 @@
//Close note event //Close note event
this.$bus.$on('close_active_note', ({noteId, modified}) => { this.$bus.$on('close_active_note', ({noteId, modified}) => {
this.closeNote(noteId, modified)
this.closeNote()
this.$store.dispatch('fetchAndUpdateUserTotals')
//Focus and animate if modified
this.updateSingleNote(parseInt(noteId), modified)
}) })
this.$bus.$on('note_deleted', (noteId) => { this.$bus.$on('note_deleted', (noteId) => {
@@ -345,27 +312,35 @@
}) })
}) })
//Reload page content - don't trigger if load is in progress //New note button pushes open note event
this.$bus.$on('note_reload', () => { this.$bus.$on('open_note', noteId => {
if(!this.showLoading){ this.openNote(noteId)
this.reset()
}
}) })
// Window scroll needed when scrolling full page. //Reload page content
// second scroll event added on note-list for floating view scroll detection this.$bus.$on('note_reload', () => {
this.reset()
})
//Mount notes on load if note ID is set
if(this.$route.params && this.$route.params.id){
const id = this.$route.params.id
this.openNote(id)
}
window.addEventListener('scroll', this.onScroll) window.addEventListener('scroll', this.onScroll)
//Close notes when back button is pressed //Close notes when back button is pressed
// window.addEventListener('hashchange', this.hashChangeAction) window.addEventListener('hashchange', this.hashChangeAction)
//update note on visibility change //update note on visibility change
// document.addEventListener('visibilitychange', this.visibiltyChangeAction); document.addEventListener('visibilitychange', this.visibiltyChangeAction);
}, },
beforeDestroy(){ beforeDestroy(){
window.removeEventListener('scroll', this.onScroll) window.removeEventListener('scroll', this.onScroll)
// document.removeEventListener('visibilitychange', this.visibiltyChangeAction) window.removeEventListener('hashchange', this.hashChangeAction)
document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
this.$bus.$off('note_reload') this.$bus.$off('note_reload')
this.$bus.$off('close_active_note') this.$bus.$off('close_active_note')
@@ -373,6 +348,7 @@
this.$bus.$off('note_deleted') this.$bus.$off('note_deleted')
this.$bus.$off('update_fast_filters') this.$bus.$off('update_fast_filters')
this.$bus.$off('update_search_term') this.$bus.$off('update_search_term')
this.$bus.$off('open_note')
//We want to remove event listeners, but something here is messing them up and preventing ALL event listeners from working //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.$off() // Remove all event listeners
@@ -380,133 +356,43 @@
}, },
mounted() { mounted() {
//Open note on PAGE LOAD if ID is set
if(this.$route.params.id > 1){
this.openNote(this.$route.params.id)
}
//Loads initial batch and tags //Loads initial batch and tags
this.reset() this.reset()
// this.search(true, this.firstLoadBatchSize, false)
// .then( r => this.search(false, this.batchSize, true))
}, },
watch: { methods: {
'$route.params.id': function(id){ toggleTitleView(){
this.openNote(id) this.titleView = !this.titleView
},
'$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
}
}
},
computed: {
isFloatingList(){
//If note 1 or 2 is open, show floating column
return (this.openNotes.length > 0)
}, },
showOneColumn(){ showOneColumn(){
return this.$store.getters.getIsUserOnMobile 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) &&
methods: { !this.$store.getters.getIsUserOnMobile
},
openNote(id, event = null){ 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 //Don't open note if a link is clicked in display card
if(event && event.target && event.target.nodeName){ if(event && event.target && event.target.nodeName){
const nodeClick = event.target.nodeName const nodeClick = event.target.nodeName
if(nodeClick == 'A'){ return } if(nodeClick == 'A'){ return }
} }
// Push note to stack if not open //1 note open
if(Number.isInteger(intId) && !this.openNotes.includes(intId)){ if(this.activeNoteId1 == null){
this.openNotes.push(intId) this.activeNoteId1 = id
this.activeNote1Position = 0 //Middel of page
this.$router.push('/notes/open/'+this.activeNoteId1).catch(e => { console.log(e) })
return
} }
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){ closeNote(position){
this.activeNoteId1 = null
console.log('close note', this.$route.fullPath) this.$router.push('/notes')
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){ toggleTagFilter(tagId){
@@ -524,10 +410,6 @@
}, },
onScroll(e){ onScroll(e){
if(!this.scrollLoadEnabled){
return
}
clearTimeout(this.loadingBatchTimeout) clearTimeout(this.loadingBatchTimeout)
this.loadingBatchTimeout = setTimeout(() => { this.loadingBatchTimeout = setTimeout(() => {
@@ -537,16 +419,44 @@
const height = document.getElementById('app').scrollHeight const height = document.getElementById('app').scrollHeight
//Load if less than 500px from the bottom //Load if less than 500px from the bottom
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled){ if(((height - scrolledDown) < 500) && this.scrollLoadEnabled && !this.loadingInProgress){
this.search(true, this.batchSize, true) this.search(false, this.batchSize, true)
} }
}, 50) }, 30)
return return
}, },
//Try to close notes on URL hash change /notes/open/123 to /notes - parse 123, close note id 123
hashChangeAction(event){
//Clean up path of hash change
let path = window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.hash
let newPath = event.newURL.replace(path,'')
let oldPath = event.oldURL.replace(path,'')
// console.log(this.$route.params)
// console.log(this.$router)
//Open note if user goes forward to a note id
if(this.$route.params && this.$route.params.id){
this.openNote(this.$route.params.id)
}
//If we go from open note ID to no note ID, close the note
if(newPath == '' && oldPath.indexOf('/open/') != -1){
//Pull note ID out of URL
const noteIdToClose = oldPath.split('/').pop()
// console.log(noteIdToClose)
if(this.$refs.note1 && this.$refs.note1.currentNoteId == noteIdToClose){
// this.$refs.note1.close()
}
}
},
visibiltyChangeAction(event){ visibiltyChangeAction(event){
//Fuck this shit, just use web sockets //Fuck this shit, just use web sockets
@@ -563,25 +473,19 @@
} }
this.lastVisibilityState = document.visibilityState this.lastVisibilityState = document.visibilityState
}, },
// @TODO Don't even trigger this if the note wasn't changed // @TODO Don't even trigger this if the note wasn't changed
updateSingleNote(noteId, focuseAndAnimate = true){ updateSingleNote(noteId, focuseAndAnimate = true){
// console.log('updating single note', noteId)
noteId = parseInt(noteId) noteId = parseInt(noteId)
//Find local note, if it exists; continue //Find local note, if it exists; continue
let note = null let note = null
if(this.$refs['note-'+noteId]?.[0]?.note){ if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
note = 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 //Lookup one note using passed in ID
const postData = { const postData = {
searchQuery: this.searchTerm, searchQuery: this.searchTerm,
@@ -602,16 +506,20 @@
return return
} }
// if old note data and new note data exists
if(note && newNote){ if(note && newNote){
//Don't move notes that were not changed
if(note.updated == newNote.updated){
// return
}
//go through each prop and update it with new values //go through each prop and update it with new values
Object.keys(newNote).forEach(prop => { Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop] note[prop] = newNote[prop]
}) })
//Push new note to front if its modified or we want it to //Push new note to front if its modified
if( note.updated != newNote.updated ){ if(focuseAndAnimate){
// Find note, in section, move to front // Find note, in section, move to front
Object.keys(this.noteSections).forEach( key => { Object.keys(this.noteSections).forEach( key => {
@@ -625,13 +533,9 @@
}) })
}) })
}
if( focuseAndAnimate ){
this.$nextTick( () => { this.$nextTick( () => {
//Trigger close animation on note //Trigger close animation on note
this.$refs['note-'+noteId][0].justClosed() this.$refs['note-'+noteId][0].justClosed()
this.$refs['note-'+noteId][0].showWorking = false
}) })
} }
@@ -643,14 +547,9 @@
//Trigger close animation on note //Trigger close animation on note
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){ if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].justClosed() 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 //Trigger section rebuild
this.rebuildNoteCategorise() this.rebuildNoteCategorise()
}) })
@@ -670,14 +569,19 @@
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
//Don't double load note batches //Don't double load note batches
if(this.showLoading){ if(this.loadingInProgress){
console.log('Loading already in progress') console.log('Loading already in progress')
return resolve(false) return resolve(false)
} }
//Reset a lot of stuff if we are not merging batches
if(!mergeExisting){ if(!mergeExisting){
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 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 //Remove all filter limits from previous queries
delete this.fastFilters.limitSize delete this.fastFilters.limitSize
@@ -705,40 +609,26 @@
} }
//Perform search - or die //Perform search - or die
this.showLoading = showLoading this.loadingInProgress = true
this.scrollLoadEnabled = false // console.time('Fetch TitleCard Batch '+notesInNextLoad)
axios.post('/api/note/search', postData) axios.post('/api/note/search', postData)
.then(response => { .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) // console.timeEnd('Fetch TitleCard Batch '+notesInNextLoad)
//Save the number of notes just loaded //Save the number of notes just loaded
this.batchOffset += response.data.notes.length this.batchOffset += response.data.notes.length
//Enable scroll loading if endpoint retured notes //Enable or disable scroll loading
this.scrollLoadEnabled = response.data.notes.length > 0 this.scrollLoadEnabled = response.data.notes.length > 0
if(response.data.total > 0){ if(response.data.total > 0){
this.searchResultsCount = response.data.total this.searchResultsCount = response.data.total
} }
this.showLoading = false this.loadingInProgress = false
this.generateNoteCategories(response.data.notes, mergeExisting) 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) return resolve(true)
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
@@ -845,6 +735,7 @@
this.fastFilters = {} this.fastFilters = {}
this.foundAttachments = [] //Remove all attachments this.foundAttachments = [] //Remove all attachments
this.$bus.$emit('reset_fast_filters')
this.updateFastFilters(5) //This loads notes this.updateFastFilters(5) //This loads notes
}, },
@@ -853,7 +744,7 @@
//clear out tags //clear out tags
this.searchTags = [] this.searchTags = []
this.tagSuggestions = [] this.tagSuggestions = []
this.showLoading = false this.loadingInProgress = false
this.searchTerm = '' this.searchTerm = ''
this.$bus.$emit('reset_fast_filters') //Clear out search this.$bus.$emit('reset_fast_filters') //Clear out search
@@ -870,32 +761,15 @@
filter[options[index]] = 1 filter[options[index]] = 1
this.fastFilters = filter 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 //Fetch First batch of notes with new filter
this.search(showLoading, this.batchSize, false) this.search(true, this.firstLoadBatchSize, false)
// .then( r => this.search(false, this.batchSize, true)) .then( r => this.search(false, this.batchSize, true))
} }
} }
} }
</script> </script>
<style type="text/css" scoped> <style type="text/css" scoped>
.text-fix {
padding: 8px 0 0 15px;
display: inline-block;
color: var(--menu-accent);
}
.detail { .detail {
float: right; float: right;
} }
@@ -913,150 +787,4 @@
.note-card-section + .note-card-section { .note-card-section + .note-card-section {
padding: 15px 0 0; 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> </style>

View File

@@ -1,761 +0,0 @@
<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

@@ -14,11 +14,6 @@
<div class="sixteen wide middle aligned column" v-if="quickNoteId > 0"> <div class="sixteen wide middle aligned column" v-if="quickNoteId > 0">
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui compact basic button">
<i class="file outline icon"></i>
Open Note
</div>
<div class="ui compact basic right floated button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true"> <div class="ui compact basic right floated button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
<i class="sync alternate reload icon"></i> <i class="sync alternate reload icon"></i>
New Scratch Pad New Scratch Pad
@@ -51,6 +46,10 @@
<i class="folder open outline icon"></i> <i class="folder open outline icon"></i>
Files Files
</div> </div>
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button">
<i class="file outline icon"></i>
Open Note
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,283 +0,0 @@
<template>
<div class="squire-box">
<div>
<h3 class="ui dividing header">
<i class="inline green cog icon"></i>
Settings for {{ $store.getters.getUsername }}
</h3>
<h4>New Scratch Pad</h4>
<div class="ui segment">
<p>Create a new scratch pad. Old scratch pad will turn into a normal note.</p>
<div class="ui compact basic button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
<i class="sync alternate reload icon"></i>
New Scratch Pad
</div>
<div v-if="showNewNoteConfirm" class="ui compact basic button shrinking" v-on:click="showNewNoteConfirm = false">
<i class="close icon"></i>
Cancel
</div>
<div v-if="showNewNoteConfirm" class="ui compact basic button shrinking" v-on:click="newQuickNote()">
<i class="green thumbs up icon"></i>
Confirm
</div>
</div>
<!-- Accent Color -->
<h4 class="ui header">
Accent Color
</h4>
<div class="ui segment">
<div class="ui doubling grid">
<div class="sixteen wide column">
<p>Theme changes are only saved to this browser.</p>
<div
v-for="color in themeColors"
class="ui compact basic button"
:style="`background: linear-gradient(0deg, ${color} 4%, rgba(0,0,0,0) 5%);`"
v-on:click="setAccentColor(color)">
<logo style="width: 33px; height: auto;" :color="color" />
</div>
</div>
</div>
</div>
<!-- Enable Two Factor -->
<h4>Two Factor Authentication</h4>
<div class="ui segment">
<div class="ui stackable grid">
<div class="six wide column">
<div class="ui tiny dividing header">1. Enter Password and get QR</div>
<div class="ui fluid action input">
<input type="password" placeholder="Current Password" v-model="password">
<div v-if="password.length == 0" class="ui disabled button">
Get QR code
</div>
<div v-if="password.length > 0" class="ui green button" v-on:click="getQrCode()">
Get QR code
</div>
</div>
</div>
<div class="four wide column">
<div class="ui tiny dividing header">2. Scan QR Code</div>
<p v-if="qrCode == ''">(QR Code will appear here)</p>
<img v-if="qrCode != ''" :src="qrCode" class="ui image" alt="QR Code">
</div>
<div class="six wide column">
<div class="ui tiny dividing header">3. Verify with code</div>
<div class="ui fluid action input" v-if="qrCode != ''">
<input type="text" placeholder="Verification Code" v-model="verificationToken" v-on:keyup.enter="verifyQrCode()">
<div class="ui green button">
Verify!
</div>
</div>
<div class="ui fluid action input" v-if="qrCode == ''">
<input type="text" placeholder="Verification Code" >
<div class="ui disabled button">
Verify!
</div>
</div>
</div>
</div>
</div>
<!-- change password -->
<h4>Change Password</h4>
<div class="ui segment">
<div class="ui stackable grid">
<div class="five wide column">
<p>Current Password</p>
<div class="ui fluid input">
<input v-model="change1" type="password" placeholder="Current Password">
</div>
</div>
<div class="five wide column">
<p>New Password</p>
<div class="ui fluid input">
<input v-model="change2" type="password" placeholder="New Password">
</div>
</div>
<div class="six wide column">
<p>Rereat New Password</p>
<div class="ui fluid action input">
<input v-model="change3" type="password" placeholder="Repeat Password">
<div v-on:click="passwordChange()" class="ui button" :class="{'green':(change1.length > 0 && change2 == change3)}">
Change it!
</div>
</div>
</div>
</div>
</div>
<!-- log out -->
<h4>Revoke Other Active Sessions</h4>
<div class="ui segment">
<div class="ui stackable grid">
<div class="sixteen wide column">
<p>Revoke access on any logged in device, except for the one you are currently using.<br><br></p>
<div class="ui button" v-on:click="revokeAllSessions()">
<i class="sign out icon"></i>
Log Out all other devices
</div>
</div>
</div>
</div>
<h4>Export All Data (In Development)</h4>
<div class="ui segment">
<p>Download all files and notes in raw text or html</p>
<div class="ui button">Export all Data</div>
</div>
<h4>Delete Account (In Development)</h4>
<div class="ui segment">
<div class="ui stackable grid">
<div class="eight wide column">
<p>Delete all data. This can not be undone.</p>
<div class="ui fluid input">
<input type="password" placeholder="Current Password" v-model="password">
</div>
</div>
<div class="four wide bottom aligned column">
<div class="ui fluid button">Verify</div>
</div>
<div class="four wide bottom aligned column">
<div class="ui disabled fluid button">Delete Account</div>
</div>
</div>
</div>
<div class="ui grid">
<div class="center aligned sixteen wide column">
<router-link to="/terms"></i>Solid Scribe Terms of Use</router-link>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'SettingsPage',
components: {
'logo':require('@/components/LogoComponent.vue').default,
},
data () {
return {
password: '',
qrCode: '',
verificationToken: '',
showNewNoteConfirm: false,
themeColors: [
'#21BA45', //Green
'#b5cc18', //Lime
'#00b5ad', //Teal
'#2185d0', //Blue
'#7128b9', //Violet
'#a333c8', // "Purple"
'#e03997', //Pink
'#db2828', //Red
'#f2711c', //Orange
'#fbbd08', //Yellow
'#767676', //Grey
'#303030', //Black-almost
],
change1: '',
change2: '',
change3: '',
}
},
methods: {
newQuickNote(){
this.showNewNoteConfirm = true
axios.post('/api/quick-note/new')
.then( ({data}) => {
this.showNewNoteConfirm = false
this.$store.dispatch('fetchAndUpdateUserTotals')
this.$bus.$emit('notification', 'New Scratch Pad Created')
})
},
setAccentColor(color){
let root = document.documentElement
root.style.setProperty('--main-accent', color)
localStorage.setItem('main-accent', color)
if(!color || color == '#21BA45'){
localStorage.removeItem('main-accent')
}
},
getQrCode(){
axios.post('/api/user/twofactorsetup', { password:this.password })
.then(({data}) => {
this.qrCode = data
})
},
verifyQrCode(){
axios.post('/api/user/verifytwofactorsetuptoken', { password:this.password, token: this.verificationToken })
.then(({data}) => {
if(data == true){
//Two FA is set up
} else {
//It failed
}
})
},
passwordChange(){
if(this.change1 == '' || this.change2 == '' || this.change3 == ''){
this.$bus.$emit('notification', 'All Password Fields Required')
return
}
if(this.change1 == this.change2){
this.$bus.$emit('notification', 'Old password matches new password')
return
}
if(this.change2 != this.change3){
this.$bus.$emit('notification', 'New Passwords do not match')
return
}
const postData = {
'currentPass':this.change1,
'newPass':this.change3
}
axios.post('/api/user/changepassword', postData)
.then(({data}) => {
if(data){
this.$bus.$emit('notification', 'Success: Password Changed')
this.change1 = ''
this.change2 = ''
this.change3 = ''
} else {
this.$bus.$emit('notification', 'Failed to change password')
this.change1 = ''
}
})
},
revokeAllSessions(){
axios.post('/api/user/revokesessions')
.then(({data}) => {
this.$bus.$emit('notification', 'All other active sessions revoked.')
})
}
}
}
</script>

View File

@@ -4,55 +4,38 @@
<div class="sixteen wide column"></div> <div class="sixteen wide column"></div>
<div class="sixteen wide column" v-if="text.length > 0 || title.length > 0"> <div class="sixteen wide column" v-if="text.length > 0 || title.length > 0">
<div class="ui text container"> <div class="ui text container squire-box" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<div class="ui segment" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}"> <h1 v-if="title">{{title}}</h1>
<h1 v-if="title">{{title}}</h1> <div v-if="text" v-html="text"></div>
<div v-if="text" v-html="text" class="squire-box"></div>
</div>
</div> </div>
</div> </div>
<div class="sixteen wide column" v-if="!$store.getters.getLoggedIn"> <div class="sixteen wide column" v-if="!$store.getters.getLoggedIn">
<div class="ui text container"> <div class="ui text container">
<h2 class="ui header">
<div class="ui segment"> <img class="small-logo" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
<div class="content">
<div class="ui grid"> Solid Scribe is an easy, free, secure Note App
<div class="three wide middle aligned center aligned column"> <div class="sub header">
<img class="small-logo" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo"> Encrypted notes, only readable by you. Unless you share them.
</div>
<div class="thirteen wide column">
<!-- header -->
<h2 class="ui header">
<div class="content">
Solid Scribe is an easy, free, secure Note App
<div class="sub header">
Encrypted notes, only readable by you. Unless you share them.
</div>
</div>
</h2>
<!-- buttons -->
<div class="ui grid">
<div class="eight wide center aligned column">
<router-link class="ui compact green button" to="/login">
<i class="plug icon"></i>Sign Up
</router-link>
</div>
<div class="eight wide center aligned column">
<router-link class="ui compact green button" to="/">
<i class="comment outline icon"></i>
Learn More
</router-link>
</div>
</div>
</div> </div>
</div> </div>
</h2>
<div class="ui grid">
<div class="eight wide center aligned column">
<router-link class="ui compact green button" to="/login">
<i class="plug icon"></i>Sign Up
</router-link>
</div>
<div class="eight wide center aligned column">
<router-link class="ui compact green button" to="/">
<i class="comment outline icon"></i>
Learn More
</router-link>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -116,7 +99,7 @@
<style type="text/css" scoped> <style type="text/css" scoped>
.small-logo { .small-logo {
width: 100%; width: 30px;
height: auto; height: auto;
} }
</style> </style>

File diff suppressed because one or more lines are too long

View File

@@ -6,14 +6,10 @@ import Router from 'vue-router'
const HomePage = () => import(/* webpackChunkName: "HomePage" */ '@/pages/HomePage') const HomePage = () => import(/* webpackChunkName: "HomePage" */ '@/pages/HomePage')
const LoginPage = () => import(/* webpackChunkName: "LoginPage" */ '@/pages/LoginPage') const LoginPage = () => import(/* webpackChunkName: "LoginPage" */ '@/pages/LoginPage')
const HelpPage = () => import(/* webpackChunkName: "HelpPage" */ '@/pages/HelpPage') const HelpPage = () => import(/* webpackChunkName: "HelpPage" */ '@/pages/HelpPage')
const TermsPage = () => import(/* webpackChunkName: "TermsPage" */ '@/pages/TermsPage')
const SettingsPage = () => import(/* webpackChunkName: "SettingsPage" */ '@/pages/SettingsPage')
const SharePage = () => import(/* webpackChunkName: "SharePage" */ '@/pages/SharePage') const SharePage = () => import(/* webpackChunkName: "SharePage" */ '@/pages/SharePage')
const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage') const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage')
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage') const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage') const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
const OverviewPage = () => import(/* webpackChunkName: "OverviewPage" */ '@/pages/OverviewPage')
const BookmarkletPage = () => import(/* webpackChunkName: "BookmarkletPage" */ '@/pages/BookmarkletPage')
const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage') const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
Vue.use(Router) Vue.use(Router)
@@ -44,12 +40,6 @@ export default new Router({
meta: {title: 'Open Note'}, meta: {title: 'Open Note'},
component: NotesPage, component: NotesPage,
}, },
{
path: '/search/tags/:tag',
name: 'Search Notes',
meta: {title: 'Search Notes'},
component: NotesPage,
},
{ {
path: '/notes/open/:id/menu/:openMenu', path: '/notes/open/:id/menu/:openMenu',
name: 'Open Note Menu', name: 'Open Note Menu',
@@ -62,24 +52,6 @@ export default new Router({
meta: {title:'Help'}, meta: {title:'Help'},
component: HelpPage component: HelpPage
}, },
{
path: '/terms',
name: 'Terms',
meta: {title:'Terms'},
component: TermsPage
},
{
path: '/bookmarklet',
name: 'Bookmarklet',
meta: {title:'Bookmarklet'},
component: BookmarkletPage
},
{
path: '/settings',
name: 'Settings',
meta: {title:'Settings'},
component: SettingsPage
},
{ {
path: '/public/note/:id/:token', path: '/public/note/:id/:token',
name: 'Share', name: 'Share',
@@ -110,24 +82,11 @@ export default new Router({
meta: {title:'Attachments by Type'}, meta: {title:'Attachments by Type'},
component: AttachmentsPage component: AttachmentsPage
}, },
{
path: '/overview',
name: 'Overview of Notes',
meta: {title:'Overview of Notes'},
component: OverviewPage
},
{ {
path: '*', path: '*',
name: 'Page Not Found', name: 'Page Not Found',
meta: {title:'404 Page Not Found'}, meta: {title:'404 Page Not Found'},
component: NotFoundPage component: NotFoundPage
}, },
// Cycle Tracking
{
path: '/metrictrack',
name: 'Metric Tracking',
meta: {title:'Metric Tracking'},
component: () => import(/* webpackChunkName: "MetrictrackingPage" */ '@/pages/MetrictrackingPage')
},
] ]
}) })

View File

@@ -1,15 +0,0 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})

View File

@@ -9,9 +9,7 @@ export default new Vuex.Store({
username: null, username: null,
nightMode: false, nightMode: false,
isUserOnMobile: false, isUserOnMobile: false,
fetchTotalsTimeout: null, userTotals: null,
userTotals: null, // {} // setting this to object breaks reactivity
activeSessions: 0,
}, },
mutations: { mutations: {
setUsername(state, username){ setUsername(state, username){
@@ -26,33 +24,39 @@ export default new Vuex.Store({
localStorage.removeItem('loginToken') localStorage.removeItem('loginToken')
localStorage.removeItem('username') localStorage.removeItem('username')
localStorage.removeItem('currentVersion') localStorage.removeItem('currentVersion')
localStorage.removeItem('snippetCache')
delete axios.defaults.headers.common['authorizationtoken'] delete axios.defaults.headers.common['authorizationtoken']
state.username = null state.username = null
state.userTotals = null
}, },
toggleNightMode(state, pastTheme){ toggleNightMode(state, pastTheme){
const themes = { const themes = {
'white':{ 'white':{
'body_bg_color': '#f1f1f1',//'#f5f6f7', 'body_bg_color': '#f5f6f7',
'small_element_bg_color': '#fff', 'small_element_bg_color': '#fff',
'text_color': '#3d3d3d', 'text_color': '#3d3d3d',
'dark_border_color': '#d9d9d9',//'#DFE1E6', 'dark_border_color': '#DFE1E6',
'border_color': '#DFE1E6', 'border_color': '#DFE1E6',
'menu-accent': '#cecece', 'menu-accent': '#cecece',
'menu-text': '#5e6268', 'menu-text': '#5e6268',
}, },
'black':{ 'black':{
'body_bg_color': 'rgb(12 4 30)', 'body_bg_color': '#000',
//'#0f0f0f',//'#000',
'small_element_bg_color': '#000', 'small_element_bg_color': '#000',
'text_color': '#FFF', 'text_color': '#FFF',
'dark_border_color': '#555',//'#ACACAC', //Lighter color to accent elemnts user can interact with 'dark_border_color': '#ACACAC', //Lighter color to accent elemnts user can interact with
'border_color': '#505050', 'border_color': '#555',
'menu-accent': '#626262', 'menu-accent': '#626262',
'menu-text': '#d9d9d9', 'menu-text': '#d9d9d9',
}, },
'night':{
'body_bg_color': '#000',
'small_element_bg_color': '#000',
'text_color': '#a98457',
'dark_border_color': '#a98457',
'border_color': '#555',
'menu-accent': '#626262',
'menu-text': '#a69682',
},
} }
//Catch values not in set //Catch values not in set
@@ -81,7 +85,6 @@ export default new Vuex.Store({
Object.keys( themes[currentTheme] ).forEach( attribute => { Object.keys( themes[currentTheme] ).forEach( attribute => {
root.style.setProperty('--'+attribute, themes[currentTheme][attribute]) root.style.setProperty('--'+attribute, themes[currentTheme][attribute])
}) })
}, },
detectIsUserOnMobile(state){ detectIsUserOnMobile(state){
@@ -94,6 +97,10 @@ export default new Vuex.Store({
} }
})(navigator.userAgent||navigator.vendor||window.opera, state); })(navigator.userAgent||navigator.vendor||window.opera, state);
}, },
toggleNoteSettingsPane(state){
state.isNoteSettingsOpen = !state.isNoteSettingsOpen
},
setSocketIoSocket(state, socket){ setSocketIoSocket(state, socket){
//Put socket id in axios headers //Put socket id in axios headers
@@ -101,23 +108,8 @@ export default new Vuex.Store({
state.socket = socket state.socket = socket
}, },
setUserTotals(state, totalsObject){ setUserTotals(state, totalsObject){
//Save all the totals for the user
if(!state.userTotals){ state.userTotals = totalsObject
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 //Set computer version from server
const currentVersion = localStorage.getItem('currentVersion') const currentVersion = localStorage.getItem('currentVersion')
@@ -137,15 +129,6 @@ export default new Vuex.Store({
// Object.keys(totalsObject).forEach( key => { // Object.keys(totalsObject).forEach( key => {
// console.log(key + ' -- ' + totalsObject[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: { getters: {
@@ -171,29 +154,19 @@ export default new Vuex.Store({
totals: state => { totals: state => {
return state.userTotals return state.userTotals
}, },
getActiveSessions: state => {
return state.activeSessions
}
}, },
actions: { actions: {
fetchAndUpdateUserTotals ({ commit, state }) { fetchAndUpdateUserTotals ({ commit }) {
clearTimeout(state.fetchTotalsTimeout) axios.post('/api/user/totals')
state.fetchTotalsTimeout = setTimeout(() => { .then( ({data}) => {
// load extended options on initial load commit('setUserTotals', data)
let postData = { })
extendedOptions: !state.userTotals .catch( error => {
if(error.response && error.response.status == 400){
commit('destroyLoginToken')
location.reload()
} }
axios.post('/api/user/totals', postData) })
.then( ({data}) => {
commit('setUserTotals', data)
})
.catch( error => {
if(error.response && error.response.status == 400){
commit('destroyLoginToken')
location.reload()
}
})
}, 100)
} }
} }
}) })

View File

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

View File

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

9240
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,29 @@
{ {
"name": "personal-internet", "name": "personal-internet",
"version": "1.0.0", "version": "1.0.0",
"description": "Encrypted note taking applications", "description": "Personal or Private net",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "jest" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Max", "author": "Max",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"body-parser": "^1.19.0", "body-parser": "^1.18.3",
"cheerio": "^1.0.0-rc.3", "cheerio": "^1.0.0-rc.3",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.16.4",
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.1.3",
"gm": "^1.23.1", "gm": "^1.23.1",
"helmet": "^4.1.1", "helmet": "^3.23.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"multer": "^1.4.2", "multer": "^1.4.2",
"mysql2": "^2.2.5", "mysql2": "^1.7.0",
"node-tesseract-ocr": "^2.0.0", "node-tesseract-ocr": "^1.0.0",
"qrcode": "^1.4.4",
"request": "^2.88.2", "request": "^2.88.2",
"request-promise": "^4.2.6", "request-promise": "^4.2.5",
"socket.io": "^2.3.0", "socket.io": "^2.3.0"
"speakeasy": "^2.0.0"
}, },
"_moduleAliases": { "_moduleAliases": {
"@root": ".", "@root": ".",
@@ -33,8 +31,5 @@
"@routes": "server/routes", "@routes": "server/routes",
"@helpers": "server/helpers", "@helpers": "server/helpers",
"@config": "server/config" "@config": "server/config"
},
"devDependencies": {
"jest": "^29.7.0"
} }
} }

View File

@@ -1,7 +1,5 @@
//Import mysql2 package //Import mysql2 package
const mysql = require('mysql2'); 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. // Create the connection pool.
const pool = mysql.createPool({ const pool = mysql.createPool({

View File

@@ -1,14 +1,11 @@
const db = require('@config/database') const db = require('@config/database')
const jwt = require('jsonwebtoken') const jwt = require('jsonwebtoken')
const cs = require('@helpers/CryptoString') const cs = require('@helpers/CryptoString')
const speakeasy = require('speakeasy')
let Auth = {} let Auth = {}
const tokenSecretKey = process.env.JSON_KEY 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) => { Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -27,7 +24,7 @@ Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) =>
return db.promise().query( return db.promise().query(
'INSERT INTO user_active_session (salt, encrypted_master_password, created, uses, user_hash, session_id) VALUES (?,?,?,?,?,?)', 'INSERT INTO user_active_session (salt, encrypted_master_password, created, uses, user_hash, session_id) VALUES (?,?,?,?,?,?)',
[salt, encryptedMasterPass, created, sessionTokenUses, userHash, sessionId]) [salt, encryptedMasterPass, created, 40, userHash, sessionId])
}) })
.then((r,f) => { .then((r,f) => {
@@ -44,7 +41,6 @@ Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) =>
}) })
} }
//Decodes session token
Auth.decodeToken = (token, request = null) => { Auth.decodeToken = (token, request = null) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -124,7 +120,6 @@ Auth.decodeToken = (token, request = null) => {
} }
Auth.terminateSession = (sessionId) => { Auth.terminateSession = (sessionId) => {
return db.promise().query('DELETE from user_active_session WHERE session_id = ?', [sessionId]) return db.promise().query('DELETE from user_active_session WHERE session_id = ?', [sessionId])
} }
@@ -135,143 +130,6 @@ Auth.deletAllLoginKeys = (userId) => {
return db.promise().query('DELETE FROM user_active_session WHERE user_hash = ?', [userHash]) return db.promise().query('DELETE FROM user_active_session WHERE user_hash = ?', [userHash])
} }
//Generate two factor secret key, if key is not verified, return a new one
//Only return QR code if user is not verified, only show unique QR code, once
Auth.generateTwoFactorSecretKey = (userId, password) => {
return new Promise((resolve, reject) => {
const QRCode = require('qrcode')
const User = require('@models/User')
User.getMasterKey(userId, password)
.then(masterKey => {
return db.promise().query('SELECT username, two_fa_enabled FROM user WHERE id = ?', [userId])
})
.then((r,f) => {
const tfaEnabled = r[0][0]['two_fa_enabled'] == 1
const username = r[0][0]['username']
if(!tfaEnabled){
var secret = speakeasy.generateSecret({length: 20, name: username+' - solidscribe.com'})
const twoFaSecretToken = secret.base32
const otpauthUrl = secret.otpauth_url
//Generate test Token
var token = speakeasy.totp({
secret: twoFaSecretToken,
encoding: 'base32'
})
db.promise().query('UPDATE user SET two_fa_secret = ? WHERE id = ? LIMIT 1', [twoFaSecretToken, userId])
.then((r,f) => {
QRCode.toDataURL(otpauthUrl, function(err, qrCode) {
//Return A QR code for the user, one time use
return resolve({qrCode, token})
})
})
} else {
return reject('Two factor already enabled for user')
}
})
.catch(error => {
console.log('Key auth error')
console.log(error)
return reject(false)
})
})
}
Auth.setTwoFactorEnabled = (userId, password, token, enable) => {
return new Promise((resolve, reject) => {
Auth.validateTwoFactorToken(userId, password, token)
.then(isValid => {
if(isValid){
db.promise().query('UPDATE user SET two_fa_enabled = ? WHERE id = ? LIMIT 1', [enable, userId])
.then((r, f) => {
return resolve(true)
})
} else {
return resolve(false)
}
})
})
}
Auth.validateTwoFactorToken = (userId, password, token) => {
return new Promise((resolve, reject) => {
const User = require('@models/User')
User.getMasterKey(userId, password)
.then(masterKey => {
return db.promise().query('SELECT two_fa_secret FROM user WHERE id = ?', [userId])
})
.then((r,f) => {
//Verify Token
const tokenValidates = speakeasy.totp.verify({
'secret': r[0][0]['two_fa_secret'],
'encoding': 'base32',
'token': token,
'window': 6
})
return resolve(tokenValidates)
})
.catch(error => {
console.log('Token Validation Error')
return resolve(false)
})
})
}
Auth.testTwoFactor = () => {
const userId = 93
const pass = '1'
let tfaToken = null
console.log('Test Two Factor')
Auth.generateTwoFactorSecretKey(userId, pass)
.then( ({qrCode, token}) => {
tfaToken = token
Auth.validateTwoFactorToken(userId, pass, tfaToken)
.then(validToken => {
console.log('Is Token Valid:', validToken)
})
return Auth.setTwoFactorEnabled(userId, pass, tfaToken, true)
})
.then(twoFactorEnbled => {
console.log('Was it enabled?', twoFactorEnbled)
return Auth.setTwoFactorEnabled(userId, pass, tfaToken, false)
})
.then(twoFactorEnbled => {
console.log('Was it disabled?', twoFactorEnbled)
})
.catch(error => {
console.log(error)
})
}
Auth.test = () => { Auth.test = () => {
const testUserId = 22 const testUserId = 22

View File

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

View File

@@ -6,7 +6,7 @@ let SiteScrape = module.exports = {}
const removeWhitespace = /\s+/g const removeWhitespace = /\s+/g
const commonWords = ['just','start','what','these','how', 'was', 'being','can','way','share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want', const commonWords = ['share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
'give','use','find','tell','ask','work','seem','feel','try','leave','call','good','new','first','last','long','great','little','own','other','old', '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', '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', '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) => { SiteScrape.getHostName = (url) => {
var hostname = 'https://'+(new URL(url)).hostname; var hostname = 'https://'+(new URL(url)).hostname;
// console.log('hostname', hostname) console.log('hostname', hostname)
return hostname return hostname
} }
@@ -63,95 +63,36 @@ SiteScrape.getDisplayImage = ($, url) => {
const hostname = SiteScrape.getHostName(url) const hostname = SiteScrape.getHostName(url)
let metaImg = $('[property="og:image"]') let metaImg = $('meta[property="og:image"]')
let shortcutIcon = $('[rel="shortcut icon"]') let shortcutIcon = $('link[rel="shortcut icon"]')
let favicon = $('[rel="icon"]') let favicon = $('link[rel="icon"]')
let randomImg = $('img') let randomImg = $('img')
//Set of images we may want gathered from various places in source console.log('----')
let imagesWeWant = []
let thumbnail = ''
//Scrape metadata for page image //Scrape metadata for page image
if(randomImg && randomImg.length > 0){ //Grab the first random image we find
if(randomImg && randomImg[0] && randomImg[0].attribs){
let imgSrcs = [] thumbnail = hostname + randomImg[0].attribs.src
for (let i = 0; i < randomImg.length; i++) { console.log('random img '+thumbnail)
imgSrcs.push( randomImg[i].attribs.src )
}
const half = Math.ceil(imgSrcs.length / 2)
imagesWeWant = [...imgSrcs.slice(-half), ...imgSrcs.slice(0,half) ]
} }
//Grab the shortcut icon //Grab the favicon of the site
if(favicon && favicon[0] && favicon[0].attribs){ if(favicon && favicon[0] && favicon[0].attribs){
imagesWeWant.push(favicon[0].attribs.href) thumbnail = hostname + favicon[0].attribs.href
console.log('favicon '+thumbnail)
} }
//Grab the shortcut icon //Grab the shortcut icon
if(shortcutIcon && shortcutIcon[0] && shortcutIcon[0].attribs){ if(shortcutIcon && shortcutIcon[0] && shortcutIcon[0].attribs){
imagesWeWant.push(shortcutIcon[0].attribs.href) thumbnail = hostname + shortcutIcon[0].attribs.href
console.log('shortcut '+thumbnail)
} }
//Grab the presentation image for the site //Grab the presentation image for the site
if(metaImg && metaImg[0] && metaImg[0].attribs){ if(metaImg && metaImg[0] && metaImg[0].attribs){
imagesWeWant.unshift(metaImg[0].attribs.content) thumbnail = metaImg[0].attribs.content
} console.log('ogImg '+thumbnail)
// 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 return thumbnail
} }
@@ -162,28 +103,19 @@ SiteScrape.getKeywords = ($) => {
majorContent += $('[class*=content]').text() majorContent += $('[class*=content]').text()
.replace(removeWhitespace, " ") //Remove all whitespace .replace(removeWhitespace, " ") //Remove all whitespace
// .replace(/\W\s/g, '') //Remove all non alphanumeric characters .replace(/\W\s/g, '') //Remove all non alphanumeric characters
.substring(0,6000) //Limit to 6000 characters .substring(0,3000) //Limit to 3000 characters
.toLowerCase() .toLowerCase()
.replace(/[^A-Za-z0-9- ]/g, '');
console.log(majorContent)
//Count frequency of each word in scraped text //Count frequency of each word in scraped text
let frequency = {} let frequency = {}
majorContent.split(' ').forEach(word => { majorContent.split(' ').forEach(word => {
// Exclude short or common words if(commonWords.includes(word)){
if(commonWords.includes(word) || word.length <= 2){ return //Exclude certain words
return
} }
if(!frequency[word]){ if(!frequency[word]){
frequency[word] = 0 frequency[word] = 0
} }
// Skip some plurals
if(frequency[word+'s'] || frequency[word+'es']){
return
}
frequency[word]++ frequency[word]++
}) })
@@ -201,7 +133,7 @@ SiteScrape.getKeywords = ($) => {
}); });
let finalWords = [] let finalWords = []
for(let i=0; i<6; i++){ for(let i=0; i<5; i++){
if(sortable[i] && sortable[i][0]){ if(sortable[i] && sortable[i][0]){
finalWords.push(sortable[i][0]) finalWords.push(sortable[i][0])
} }

View File

@@ -1,12 +1,7 @@
//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 os = require('os') //Used to get path of home directory
const result = require('dotenv').config({ path:(os.homedir()+'/.env') }) 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 //Allow user of @ in in require calls. Config in package.json
require('module-alias/register') require('module-alias/register')
@@ -20,24 +15,23 @@ const helmet = require('helmet')
const express = require('express') const express = require('express')
const app = express() const app = express()
app.use( helmet() ) app.use( helmet() )
// allow for the parsing of url encoded forms const port = 3000
app.use(express.urlencoded({ extended: true }));
// //
// Request Rate Limiter // Request Rate Limiter
// //
const rateLimit = require('express-rate-limit') const rateLimit = require('express-rate-limit');
//Limiter for the entire app
const limiter = rateLimit({ const limiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minutes windowMs: 10 * 60 * 1000, // minutes
max: 1000 // limit each IP to 1000 requests per windowMs max: 1000 // limit each IP to 100 requests per windowMs
}) });
// apply to all requests // apply to all requests
app.use(limiter); app.use(limiter);
var http = require('http').createServer(app); var http = require('http').createServer(app);
var io = require('socket.io')(http, { var io = require('socket.io')(http, {
path:'/socket' path:'/socket'
@@ -57,31 +51,12 @@ io.on('connection', function(socket){
Auth.decodeToken(token) Auth.decodeToken(token)
.then(userData => { .then(userData => {
socket.join(userData.userId) 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 => { }).catch(error => {
//Don't add user to room if they are not logged in //Don't add user to room if they are not logged in
// console.log(error) // 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 //Renew Session tokens when users request a new one
socket.on('renew_session_token', token => { socket.on('renew_session_token', token => {
@@ -116,12 +91,29 @@ io.on('connection', function(socket){
//Emit all sorted diffs to user //Emit all sorted diffs to user
socket.emit('past_diffs', noteDiffs[rawTextId]) socket.emit('past_diffs', noteDiffs[rawTextId])
} else {
socket.emit('past_diffs', null)
} }
const usersInRoom = io.sockets.adapter.rooms[rawTextId] const usersInRoom = io.sockets.adapter.rooms[rawTextId]
if(usersInRoom){ if(usersInRoom){
//Update users in room count //Update users in room count
io.to(rawTextId).emit('update_user_count', usersInRoom.length) io.to(rawTextId).emit('update_user_count', usersInRoom.length)
//Debugging text - prints out notes in limbo
let noteDiffKeys = Object.keys(noteDiffs)
let totalDiffs = 0
noteDiffKeys.forEach(diffSetKey => {
if(noteDiffs[diffSetKey]){
totalDiffs += noteDiffs[diffSetKey].length
}
})
//Debugging Text
if(noteDiffKeys.length > 0){
console.log('Total notes in limbo -> ', noteDiffKeys.length)
console.log('Total Diffs for all notes -> ', totalDiffs)
}
} }
}) })
@@ -147,13 +139,31 @@ io.on('connection', function(socket){
noteDiffs[noteId].push(data) noteDiffs[noteId].push(data)
// Go over each user in this note-room //Remove duplicate diffs if they exist
for (var i = noteDiffs[noteId].length - 1; i >= 0; i--) {
let pastDiff = noteDiffs[noteId][i]
for (var j = noteDiffs[noteId].length - 1; j >= 0; j--) {
let currentDiff = noteDiffs[noteId][j]
if(i == j){
continue
}
if(currentDiff.diff == pastDiff.diff || currentDiff.time == pastDiff.time){
console.log('Removing Duplicate')
noteDiffs[noteId].splice(i,1)
}
}
}
//Each user joins a room when they open the app.
io.in(noteId).clients((error, clients) => { io.in(noteId).clients((error, clients) => {
if (error) throw error; if (error) throw error;
//Go through each client in note-room and send them the diff //Go through each client in note room and send them the diff
clients.forEach(socketId => { clients.forEach(socketId => {
// only send off diff if user
if(socketId != socket.id){ if(socketId != socket.id){
io.to(socketId).emit('incoming_diff', data) io.to(socketId).emit('incoming_diff', data)
} }
@@ -180,6 +190,7 @@ io.on('connection', function(socket){
} }
} }
noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo) noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo)
if(noteDiffs[checkpoint.rawTextId].length == 0){ if(noteDiffs[checkpoint.rawTextId].length == 0){
@@ -194,14 +205,14 @@ io.on('connection', function(socket){
} }
}) })
socket.on('disconnect', function(socket){ socket.on('disconnect', function(){
// console.log('user disconnected'); // console.log('user disconnected');
}); });
}); });
http.listen(ports.socketIo, function(){ http.listen(3001, function(){
console.log(`Socke.io: Listening on port ${ports.socketIo}`) // console.log('socket.io liseting on port 3001');
}); });
//Enable json body parsing in requests. Allows me to post data in ajax calls //Enable json body parsing in requests. Allows me to post data in ajax calls
@@ -211,7 +222,6 @@ app.use(express.json({limit: '5mb'}))
app.use(function(req, res, next){ app.use(function(req, res, next){
//Always null out master key, never allow it set from outside //Always null out master key, never allow it set from outside
req.headers.userId = null
req.headers.masterKey = null req.headers.masterKey = null
req.headers.sessionId = null req.headers.sessionId = null
@@ -233,7 +243,10 @@ app.use(function(req, res, next){
}) })
.catch(error => { .catch(error => {
next('Unauthorized') console.log(error)
res.statusMessage = error //Throw 400 error if token is bad
res.status(400).end()
}) })
} else { } else {
next() //No token. Move along. next() //No token. Move along.
@@ -242,24 +255,21 @@ app.use(function(req, res, next){
// Test Area // Test Area
// const printResults = true const printResults = true
// let UserTest = require('@models/User') let UserTest = require('@models/User')
// let NoteTest = require('@models/Note') let NoteTest = require('@models/Note')
// let AuthTest = require('@helpers/Auth') let AuthTest = require('@helpers/Auth')
// Auth.test() Auth.test()
// UserTest.keyPairTest('genMan30', '1', printResults) UserTest.keyPairTest('genMan15', '1', printResults)
// .then( ({testUserId, masterKey}) => .then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
// NoteTest.test(testUserId, masterKey, printResults)) .then( message => {
// .then( message => { if(printResults) console.log(message)
// if(printResults) console.log(message) })
// Auth.testTwoFactor() // Test Area
// })
// .catch((error) => {
// console.log(error)
// })
//Test //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 //Serve up uploaded files
app.use('/api/static', express.static( __dirname+'/../staticFiles' )) app.use('/api/static', express.static( __dirname+'/../staticFiles' ))
@@ -288,23 +298,7 @@ app.use('/api/attachment', attachment)
var quickNote = require('@routes/quicknoteController') var quickNote = require('@routes/quicknoteController')
app.use('/api/quick-note', quickNote) app.use('/api/quick-note', quickNote)
//cycle tracking endpoint
var metricTracking = require('@routes/metrictrackingController')
app.use('/api/metric-tracking', metricTracking)
//Output running status //Output running status
app.listen(ports.express, () => { app.listen(port, () => {
console.log(`Express: Listening on port ${ports.express}!`) // console.log(`Listening on port ${port}!`)
})
//
//Error handlers
//
//Default error handler just say unauthorized for everything
app.use(function (err, req, res, next) {
if (err) {
res.status(401).send('Unauthorized')
return
}
next()
}) })

View File

@@ -1,7 +1,6 @@
let db = require('@config/database') let db = require('@config/database')
let SiteScrape = require('@helpers/SiteScrape') let SiteScrape = require('@helpers/SiteScrape')
const cs = require('@helpers/CryptoString')
let Attachment = module.exports = {} let Attachment = module.exports = {}
@@ -33,7 +32,6 @@ Attachment.textSearch = (userId, searchTerm) => {
) as snippet ) as snippet
FROM attachment FROM attachment
WHERE user_id = ? WHERE user_id = ?
AND visible != 0
AND MATCH(text) AND MATCH(text)
AGAINST(? IN NATURAL LANGUAGE MODE) AGAINST(? IN NATURAL LANGUAGE MODE)
LIMIT 1000` LIMIT 1000`
@@ -47,60 +45,31 @@ Attachment.textSearch = (userId, searchTerm) => {
}) })
} }
Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeShared) => { Attachment.search = (userId, noteId, attachmentType, offset, setSize) => {
console.log([userId, noteId, attachmentType, offset, setSize, includeShared])
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let params = [userId] let params = [userId]
let query = ` let query = 'SELECT * FROM attachment WHERE user_id = ? AND visible = 1 '
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){ if(noteId && noteId > 0){
// query += 'AND note_id = ? '
// Show everything if note ID is present
//
query += 'AND attachment.note_id = ? '
params.push(noteId) params.push(noteId)
} else {
//
// Other filters if NO note id
//
if(attachmentType == 'links'){
query += 'AND attachment_type = 1 '
}
if(attachmentType == 'files'){
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(attachmentType == 'links'){
if(!noteId){ query += 'AND attachment_type = 1 '
const sharedOrNot = includeShared ? ' NOT ':' ' }
query += `AND note.share_user_id IS${sharedOrNot}NULL ` if(attachmentType == 'files'){
query += 'AND attachment_type > 1 '
} }
query += 'ORDER BY last_indexed DESC ' query += 'ORDER BY last_indexed DESC '
const limitOffset = parseInt(offset, 10) || 0 //Either parse int, or use zero const limitOffset = parseInt(offset, 10) || 0 //Either parse int, or use zero
const parsedSetSize = parseInt(setSize, 10) || 20 const parsedSetSize = parseInt(setSize, 10) || 20 //Either parse int, or use zero
query += ` LIMIT ${limitOffset}, ${parsedSetSize}` query += ` LIMIT ${limitOffset}, ${parsedSetSize}`
console.log(query)
db.promise() db.promise()
.query(query, params) .query(query, params)
.then((rows, fields) => { .then((rows, fields) => {
@@ -110,6 +79,18 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeSha
}) })
} }
//Returns all attachments
Attachment.forNote = (userId, noteId) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND visible = 1 ORDER BY last_indexed DESC;`, [userId, noteId])
.then((rows, fields) => {
resolve(rows[0]) //Return all attachments found by query
})
.catch(console.log)
})
}
Attachment.urlForNote = (userId, noteId) => { Attachment.urlForNote = (userId, noteId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise() db.promise()
@@ -185,7 +166,6 @@ Attachment.delete = (userId, attachmentId, urlDelete = false) => {
.catch(console.log) .catch(console.log)
} }
}) })
.catch(console.log)
}) })
} }
@@ -302,13 +282,9 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
//Once everything is done being scraped, emit new attachment events //Once everything is done being scraped, emit new attachment events
SocketIo.to(userId).emit('update_counts') SocketIo.to(userId).emit('update_counts')
// Tell user to update attachments with scraped text
SocketIo.to(userId).emit('update_note_attachments')
solrAttachmentText += freshlyScrapedText solrAttachmentText += freshlyScrapedText
resolve(solrAttachmentText) resolve(solrAttachmentText)
}) })
.catch(console.log)
}) })
}) })
} }
@@ -336,13 +312,9 @@ Attachment.scrapeUrlsCreateAttachments = (userId, noteId, foundUrls) => {
//All URLs have been scraped, return data //All URLs have been scraped, return data
if(processedCount == foundUrls.length){ if(processedCount == foundUrls.length){
console.log('All urls scraped') resolve(scrapedText)
return resolve(scrapedText)
} }
}) })
.catch(error => {
console.log('Site Scrape error', error)
})
}) })
}) })
} }
@@ -352,16 +324,17 @@ Attachment.downloadFileFromUrl = (url) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if(!url){ if(url == null){
return resolve(null) resolve(null)
} }
const random = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) const random = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
let extension = '' const extension = '.'+url.split('.').pop() //This is throwing an error
let fileName = random+'_scrape' let fileName = random+'_scrape'+extension
let thumbPath = 'thumb_'+fileName const thumbPath = 'thumb_'+fileName
console.log('Scraping image url', url) console.log('Scraping image url')
console.log(url)
console.log('Getting ready to scrape ', url) console.log('Getting ready to scrape ', url)
@@ -373,8 +346,6 @@ Attachment.downloadFileFromUrl = (url) => {
.on('response', res => { .on('response', res => {
console.log(res.statusCode) console.log(res.statusCode)
console.log(res.headers['content-type']) 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)) .pipe(fs.createWriteStream(filePath+thumbPath))
.on('close', () => { .on('close', () => {
@@ -382,24 +353,21 @@ Attachment.downloadFileFromUrl = (url) => {
//resize image if its real big //resize image if its real big
gm(filePath+thumbPath) gm(filePath+thumbPath)
.resize(550) //Resize to width of 550 px .resize(550) //Resize to width of 550 px
.quality(85) //compression level 0 - 100 (best) .quality(75) //compression level 0 - 100 (best)
.write(filePath+thumbPath, function (err) { .write(filePath+thumbPath, function (err) {
if(err){ if(err){ console.log(err) }
console.log(err)
return resolve(null)
}
console.log('Saved Image')
return resolve(fileName)
}) })
console.log('Saved Image')
resolve(fileName)
}) })
}) })
} }
Attachment.processUrl = (userId, noteId, url) => { Attachment.processUrl = (userId, noteId, url) => {
const scrapeTime = 5*1000; const scrapeTime = 20*1000;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -427,7 +395,7 @@ Attachment.processUrl = (userId, noteId, url) => {
.query(`INSERT INTO attachment .query(`INSERT INTO attachment
(note_id, user_id, attachment_type, text, url, last_indexed, file_location) (note_id, user_id, attachment_type, text, url, last_indexed, file_location)
VALUES (?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?)`,
[noteId, userId, 1, url, url, created, null]) [noteId, userId, 1, 'Processing...', url, created, null])
.then((rows, fields) => { .then((rows, fields) => {
//Set two bigger variables then return request for processing //Set two bigger variables then return request for processing
request = rp(options) request = rp(options)
@@ -452,12 +420,9 @@ Attachment.processUrl = (userId, noteId, url) => {
const keywords = SiteScrape.getKeywords($) const keywords = SiteScrape.getKeywords($)
var desiredSearchText = '' var desiredSearchText = ''
desiredSearchText += pageTitle desiredSearchText += pageTitle + "\n"
if(keywords){ desiredSearchText += keywords
desiredSearchText += "\n " + keywords
}
console.log('Results from site scrape-------------')
console.log({ console.log({
pageTitle, pageTitle,
hostname, hostname,
@@ -507,142 +472,40 @@ Attachment.processUrl = (userId, noteId, url) => {
}) })
.catch(error => { .catch(error => {
console.log('Scrape pooped out') // console.log('Scrape pooped out')
console.log('Issue with scrape', error.statusCode) // console.log('Issue with scrape')
clearTimeout(requestTimeout) console.log(error)
return resolve('No site text') // resolve('')
}) })
requestTimeout = setTimeout( () => { requestTimeout = setTimeout( () => {
console.log('Cancel the request, its taking to long.') console.log('Cancel the request, its taking to long.')
request.cancel() request.cancel()
return resolve('Request Timeout')
desiredSearchText = 'No Description for -> '+url
created = Math.round((+new Date)/1000)
db.promise()
.query(`UPDATE attachment SET
text = ?,
last_indexed = ?,
WHERE id = ?
`, [desiredSearchText, created, insertedId])
.then((rows, fields) => {
resolve(desiredSearchText) //Return found text
})
.catch(console.log)
//Create attachment in DB with scrape text and provided data
// db.promise()
// .query(`INSERT INTO attachment
// (note_id, user_id, attachment_type, text, url, last_indexed)
// VALUES (?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created])
// .then((rows, fields) => {
// resolve(desiredSearchText) //Return found text
// })
// .catch(console.log)
}, scrapeTime ) }, 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

@@ -1,71 +0,0 @@
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,7 +17,6 @@ const fs = require('fs')
const gm = require('gm') const gm = require('gm')
Note.test = (userId, masterKey, printResults) => { Note.test = (userId, masterKey, printResults) => {
return false;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -43,7 +42,7 @@ Note.test = (userId, masterKey, printResults) => {
testNoteId = newNoteId testNoteId = newNoteId
return Note.update return Note.update
(userId, testNoteId, 'Note text', 'Test Note beans barns Title', 0, 0, 0, 'hash', masterKey) (userId, testNoteId, 'Note text', 'Test Note beans Title', 0, 0, 0, 'hash', masterKey)
}) })
.then(() => { .then(() => {
@@ -64,14 +63,14 @@ Note.test = (userId, masterKey, printResults) => {
if(printResults) console.log('Test: Reindex normal note - Pass') if(printResults) console.log('Test: Reindex normal note - Pass')
return Note.encryptedIndexSearch(userId, 'beans barns', null, masterKey) return Note.encryptedIndexSearch(userId, 'beans', null, masterKey)
}) })
.then(textSearchResults => { .then(textSearchResults => {
if(textSearchResults['ids'] && textSearchResults['ids'].length >= 1){ if(textSearchResults['ids'] && textSearchResults['ids'].length >= 1){
if(printResults) console.log('Test: Normal Note Search Index - Pass') if(printResults) console.log('Test: Normal Note Search Index - Pass')
} else { console.log('Test: Search Index - Fail-------------> 🥱') } } else { console.log('Test: Search Index - Fail') }
return ShareNote.addUserToSharedNote(userId, testNoteId, shareUserId, masterKey) return ShareNote.addUserToSharedNote(userId, testNoteId, shareUserId, masterKey)
}) })
@@ -163,10 +162,6 @@ Note.test = (userId, masterKey, printResults) => {
return resolve('Test: Complete ---') return resolve('Test: Complete ---')
}) })
.catch(error => {
console.log(error)
return reject(error)
})
}) })
} }
@@ -187,7 +182,7 @@ Note.create = (userId, noteTitle = '', noteText = '', masterKey) => {
const encryptedText = cs.encrypt(masterKey, salt, textObject) const encryptedText = cs.encrypt(masterKey, salt, textObject)
db.promise() db.promise()
.query(`INSERT INTO note_raw_text (text, salt, updated) VALUE (?, ?, ?)`, [encryptedText, salt, (+new Date)]) .query(`INSERT INTO note_raw_text (text, salt, updated) VALUE (?, ?, ?)`, [encryptedText, salt, created])
.then( (rows, fields) => { .then( (rows, fields) => {
const rawTextId = rows[0].insertId const rawTextId = rows[0].insertId
@@ -198,7 +193,7 @@ Note.create = (userId, noteTitle = '', noteText = '', masterKey) => {
}) })
.then((rows, fields) => { .then((rows, fields) => {
if(typeof SocketIo != 'undefined'){ if(SocketIo){
SocketIo.to(userId).emit('new_note_created', rows[0].insertId) SocketIo.to(userId).emit('new_note_created', rows[0].insertId)
} }
@@ -346,7 +341,7 @@ Note.reindex = (userId, masterKey, removeId = null) => {
setTimeout(() => { setTimeout(() => {
if(masterKey == null || note.salt == null){ if(masterKey == null || note.salt == null){
console.log('Error indexing note - master key or salt missing', note.id) console.log('Error indexing note', note.id)
return resolve(true) return resolve(true)
} }
@@ -395,13 +390,13 @@ Note.reindex = (userId, masterKey, removeId = null) => {
return Promise.all(reindexQueue) return Promise.all(reindexQueue)
}) })
.then(updatePromiseResults => { .then(rawSearchIndex => {
const created = Math.round((+new Date)/1000) const created = Math.round((+new Date)/1000)
const jsonSearchIndex = JSON.stringify(searchIndex) const jsonSearchIndex = JSON.stringify(searchIndex)
const encryptedJsonIndex = cs.encrypt(masterKey, searchIndexSalt, jsonSearchIndex) const encryptedJsonIndex = cs.encrypt(masterKey, searchIndexSalt, jsonSearchIndex)
db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1", return db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
[encryptedJsonIndex, created, userId]) [encryptedJsonIndex, created, userId])
.then((rows, fields) => { .then((rows, fields) => {
@@ -411,7 +406,6 @@ Note.reindex = (userId, masterKey, removeId = null) => {
.then((rows, fields) => { .then((rows, fields) => {
// console.log('Indexd Note Count: ' + rows[0]['affectedRows']) // console.log('Indexd Note Count: ' + rows[0]['affectedRows'])
// @TODO - Return number of reindexed notes
resolve(true) resolve(true)
}) })
@@ -448,10 +442,6 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
}) })
.then((rows, fields) => { .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'] const textId = rows[0][0]['note_raw_text_id']
let salt = rows[0][0]['salt'] let salt = rows[0][0]['salt']
let snippetSalt = rows[0][0]['snippet_salt'] let snippetSalt = rows[0][0]['snippet_salt']
@@ -459,7 +449,8 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
//Shared notes use encrypted key - decrypt key then decrypt note //Shared notes use encrypted key - decrypt key then decrypt note
const encryptedShareKey = rows[0][0].encrypted_share_password_key const encryptedShareKey = rows[0][0].encrypted_share_password_key
if(encryptedShareKey != null){ if(encryptedShareKey != null){
masterKey = crypto.privateDecrypt(userPrivateKey, Buffer.from(encryptedShareKey, 'base64') ) masterKey = crypto.privateDecrypt(userPrivateKey,
Buffer.from(encryptedShareKey, 'base64') )
} }
let encryptedNoteText = '' let encryptedNoteText = ''
@@ -484,14 +475,10 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
for (var i = 0; i < rows[0].length; i++) { for (var i = 0; i < rows[0].length; i++) {
const otherNote = rows[0][i] const otherNote = rows[0][i]
//Re-encrypt for other user //Re-encrypt for other user
let updatedSnippet = '' //Default to no snippet const updatedSnippet = cs.encrypt(masterKey, otherNote.snippet_salt, snippet)
if(noteText.length > 500){
updatedSnippet = cs.encrypt(masterKey, otherNote.snippet_salt, snippet)
}
db.promise().query('UPDATE note SET snippet = ? WHERE id = ?', [updatedSnippet, otherNote.id]) db.promise().query('UPDATE note SET snippet = ? WHERE id = ?', [updatedSnippet, otherNote.id])
.then((rows, fields) => {
SocketIo.to(otherNote['user_id']).emit('new_note_text_saved', {'noteId':otherNote.id, hash}) SocketIo.to(otherNote['user_id']).emit('new_note_text_saved', {'noteId':otherNote.id, hash})
})
} }
}) })
@@ -502,24 +489,21 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
}) })
.then( (rows, fields) => { .then( (rows, fields) => {
//Set openend time to a minute ago
const theFuture = Math.round((+new Date)/1000) + 10
//Update other note attributes //Update other note attributes
return db.promise() return db.promise()
.query('UPDATE note SET pinned = ?, archived = ?, color = ?, snippet = ?, indexed = 0, opened = ? WHERE id = ? AND user_id = ? LIMIT 1', .query('UPDATE note SET pinned = ?, archived = ?, color = ?, snippet = ?, indexed = 0 WHERE id = ? AND user_id = ? LIMIT 1',
[pinned, archived, color, noteSnippet, theFuture, noteId, userId]) [pinned, archived, color, noteSnippet, noteId, userId])
}) })
.then((rows, fields) => { .then((rows, fields) => {
if(typeof SocketIo != 'undefined'){ if(SocketIo){
SocketIo.to(userId).emit('new_note_text_saved', {noteId, hash}) SocketIo.to(userId).emit('new_note_text_saved', {noteId, hash})
//Async attachment reindex
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
} }
//Async attachment reindex
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
//Send back updated response //Send back updated response
resolve(rows[0]) resolve(rows[0])
}) })
@@ -531,7 +515,7 @@ Note.setPinned = (userId, noteId, pinnedBoolean) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const pinned = pinnedBoolean ? 1:0 const pinned = pinnedBoolean ? 1:0
const now = (+new Date) const now = Math.round((+new Date)/1000)
//Update other note attributes //Update other note attributes
return db.promise() return db.promise()
@@ -548,7 +532,7 @@ Note.setArchived = (userId, noteId, archivedBoolead) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const archived = archivedBoolead ? 1:0 const archived = archivedBoolead ? 1:0
const now = (+new Date) const now = Math.round((+new Date)/1000)
//Update other note attributes //Update other note attributes
return db.promise() return db.promise()
@@ -565,7 +549,7 @@ Note.setTrashed = (userId, noteId, trashedBoolean, masterKey) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const trashed = trashedBoolean ? 1:0 const trashed = trashedBoolean ? 1:0
const now = (+new Date) const now = Math.round((+new Date)/1000)
//Update other note attributes //Update other note attributes
return db.promise() return db.promise()
@@ -668,9 +652,60 @@ Note.delete = (userId, noteId, masterKey = null) => {
}) })
} }
// //text is the current text for the note that will be compared to the text in the database
// Returns noteData Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
// return new Promise((resolve, reject) => {
Note.get(userId, noteId)
.then(noteObject => {
if(!noteObject.text || !usersCurrentText || noteObject.encrypted == 1){
return resolve(null)
}
let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
let newText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
if(noteObject.updated == lastUpdated){
// console.log('No note diff')
return resolve(null)
}
if(noteObject.updated > lastUpdated){
newText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
oldText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
}
const dmp = new DiffMatchPatch.diff_match_patch()
const diff = dmp.diff_main(oldText, newText)
dmp.diff_cleanupSemantic(diff)
const patch_list = dmp.patch_make(oldText, newText, diff);
const patch_text = dmp.patch_toText(patch_list);
//Patch text - shows a list of changes
var patches = dmp.patch_fromText(patch_text);
// console.log(patch_text)
//results[1] - contains diagnostic data for patch apply, its possible it can fail
var results = dmp.patch_apply(patches, oldText);
//Compile return data for front end
const returnData = {
updatedText: results[0],
diffs: results[1].length, //Only use length for now
updated: Math.max(noteObject.updated,lastUpdated) //Return most recently updated date
}
//Final change in notes
// console.log(returnData)
resolve(returnData)
})
})
}
Note.get = (userId, noteId, masterKey) => { Note.get = (userId, noteId, masterKey) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -694,7 +729,6 @@ Note.get = (userId, noteId, masterKey) => {
note_raw_text.text, note_raw_text.text,
note_raw_text.salt, note_raw_text.salt,
note_raw_text.updated as updated, note_raw_text.updated as updated,
GROUP_CONCAT(DISTINCT(tag.text) ORDER BY tag.text DESC) AS tags,
note.id, note.id,
note.user_id, note.user_id,
note.created, note.created,
@@ -711,13 +745,12 @@ Note.get = (userId, noteId, masterKey) => {
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) 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 attachment ON (note.id = attachment.note_id)
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id) LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
LEFT JOIN note_tag ON (note.id = note_tag.note_id AND note_tag.user_id = ?) WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, noteId])
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) => { .then((rows, fields) => {
const nowTime = Math.round((+new Date)/1000)
let noteLockedOut = false let noteLockedOut = false
let noteData = rows[0][0] let noteData = rows[0][0]
// const rawTextId = noteData['rawTextId'] // const rawTextId = noteData['rawTextId']
@@ -743,15 +776,13 @@ Note.get = (userId, noteId, masterKey) => {
noteData.title = textObject[0] noteData.title = textObject[0]
noteData.text = textObject[1] noteData.text = textObject[1]
const nowTime = Math.round((+new Date)/1000)
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId]) db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
.then(results => {
//Return note data //Return note data
// delete noteData.salt //remove salt from return data // delete noteData.salt //remove salt from return data
// delete noteData.encrypted_share_password_key // delete noteData.encrypted_share_password_key
noteData.lockedOut = noteLockedOut noteData.lockedOut = noteLockedOut
resolve(noteData) resolve(noteData)
})
}) })
.catch(error => { .catch(error => {
@@ -831,98 +862,39 @@ Note.encryptedIndexSearch = (userId, searchQuery, searchTags, masterKey) => {
const decipheredSearchIndex = cs.decrypt(masterKey, row.salt, row.index) const decipheredSearchIndex = cs.decrypt(masterKey, row.salt, row.index)
const searchIndex = JSON.parse(decipheredSearchIndex) const searchIndex = JSON.parse(decipheredSearchIndex)
//Clean up search word, leave in spaces, split to array //Clean up search word
const words = searchQuery.toLowerCase().replace(/[^a-z0-9 ]/g, '').split(' ') const word = searchQuery.toLowerCase().replace(/[^a-z0-9]/g, '')
let wordSearchCount = 0; let noteIds = []
let partials = []
Object.keys(searchIndex).forEach(wordIndex => {
let partialWords = [] //For debugging if( wordIndex.indexOf(word) != -1 && wordIndex != word){
let exactWords = [] //For debugging partials.push(wordIndex)
noteIds.push(...searchIndex[wordIndex])
let exactWordIdSets = []
let partialMatchNoteIds = []
words.forEach(word => {
//Skip short words
if(word.length <= 2){
return
} }
//count all words being searched
wordSearchCount++
//Save all exact match sets if found
if(searchIndex[word]){
// exactWords.push(word) //Words for debugging
exactWordIdSets.push(...searchIndex[word])
}
//Find all partial word matches in index
Object.keys(searchIndex).forEach(wordIndex => {
if( wordIndex.indexOf(word) != -1 && wordIndex != word){
// partialWords.push(wordIndex) //partialWords for debugging
partialMatchNoteIds.push(...searchIndex[wordIndex])
}
})
}) })
//If more than one work was searched, remove notes that don't contain both const exactArray = searchIndex[word] ? searchIndex[word] : []
if(words.length > 1 && exactWordIdSets.length > 0){
//Find ids that appear more than once, this means there was an exact match in more than one note let searchData = {
let overlappingIds = exactWordIdSets.filter((e, i, a) => a.indexOf(e) !== i) 'word':word,
overlappingIds = [...new Set(overlappingIds)] 'exact': exactArray,
'partials': partials,
//If there are notes that appear 'partial': [...new Set(noteIds) ],
if(overlappingIds.length > 0){
exactWordIdSets = overlappingIds
}
//If note appears in partial and exact, show only that set
const partialIntersect = exactWordIdSets.filter(x => partialMatchNoteIds.includes(x))
if(partialIntersect.length > 0){
exactWordIdSets = partialIntersect
partialMatchNoteIds = []
}
} }
//Remove duplicates from final id sets
let finalExact = [ ...new Set(exactWordIdSets) ]
let finalPartial = [ ...new Set(partialMatchNoteIds) ]
//Remove exact matches from partials set if there is overlap //Remove exact matches from partials set if there is overlap
if(finalExact.length > 0 && finalPartial.length > 0){ if(searchData['exact'].length > 0 && searchData['partial'].length > 0){
finalPartial = finalPartial searchData['partial'] = searchData['partial']
.filter( ( el ) => !finalExact.includes( el ) ) .filter( ( el ) => !searchData['exact'].includes( el ) )
} }
//Combine the two filtered sets searchData['ids'] = searchData['exact'].concat(searchData['partial'])
let finalIdSearchSet = finalExact.concat(finalPartial) searchData['total'] = searchData['ids'].length
// let searchData = { // console.log(searchData['total'])
// 'query':searchQuery,
// 'words_count': words.length,
// 'exact_matches': exactWordIdSets.length,
// 'word_search_count': wordSearchCount,
// 'exactWords': exactWords,
// 'exact': finalExact,
// 'partialWords': partialWords,
// 'partial': finalPartial,
// }
// //Lump all found note ids into one array return resolve({ 'ids':searchData['ids'] })
// searchData['ids'] = finalIdSearchSet
// searchData['total'] = searchData['ids'].length
// console.log('-----------------')
// console.log(searchData)
// console.log('-----------------')
return resolve({ 'ids':finalIdSearchSet })
} else { } else {
@@ -993,8 +965,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs, GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs,
shareUser.username as shareUsername, shareUser.username as shareUsername,
note.shared, note.shared,
note.encrypted_share_password_key, note.encrypted_share_password_key
note.indexed
FROM note FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
LEFT JOIN note_tag ON (note.id = note_tag.note_id) LEFT JOIN note_tag ON (note.id = note_tag.note_id)
@@ -1002,7 +973,6 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.visible = 1) 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) LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
WHERE note.user_id = ? WHERE note.user_id = ?
AND note.quick_note <= 1
` `
//If text search returned results, limit search to those ids //If text search returned results, limit search to those ids
@@ -1078,7 +1048,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
// Always prioritize pinned notes in searches. // Always prioritize pinned notes in searches.
//Default Sort, order by last updated //Default Sort, order by last updated
let defaultOrderBy = ' ORDER BY note.pinned DESC, updated DESC, note.created DESC, note.opened DESC' let defaultOrderBy = ' ORDER BY note.pinned DESC, updated DESC, note.created DESC, note.opened DESC, id DESC'
//Order by Last Created Date //Order by Last Created Date
if(fastFilters.lastCreated == 1){ if(fastFilters.lastCreated == 1){
@@ -1152,7 +1122,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
} }
} }
} catch(err) { } catch(err) {
console.log('Error opening note id -> '+note.id+' for userId -> '+userId) console.log('Error opening note id -> ', note.id)
console.log(err) console.log(err)
} }

View File

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

View File

@@ -138,33 +138,6 @@ 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' // Get all tags for a note and concatinate into a string 'all, tags, like, this'
// //

View File

@@ -5,33 +5,22 @@ const Note = require('@models/Note')
const db = require('@config/database') const db = require('@config/database')
const Auth = require('@helpers/Auth') const Auth = require('@helpers/Auth')
const cs = require('@helpers/CryptoString') const cs = require('@helpers/CryptoString')
const speakeasy = require('speakeasy')
let User = module.exports = {} let User = module.exports = {}
const version = '3.8.0' const version = '3.0.1'
// 3.7.3 - diff/patch update
//Login a user, if that user does not exist create them //Login a user, if that user does not exist create them
//Issues login token //Issues login token
User.login = (username, password, authToken = null) => { User.login = (username, password) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const lowerName = username.toLowerCase() const lowerName = username.toLowerCase();
let statusObject = {
success: false,
token: null,
userId: null,
verificationRequired: false,
message: 'Incorrect Username or Password'
}
db.promise() db.promise()
.query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName]) .query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName])
.then((rows, fields) => { .then((rows, fields) => {
//
// Login User // Login User
// //
if(rows[0].length == 1){ if(rows[0].length == 1){
@@ -39,76 +28,34 @@ User.login = (username, password, authToken = null) => {
//Pull out user data from database results //Pull out user data from database results
const lookedUpUser = rows[0][0] const lookedUpUser = rows[0][0]
//Verify Token if set //hash the password and check for a match
const tokenValidates = speakeasy.totp.verify({ // const salt = new Buffer(lookedUpUser.salt, 'binary')
'secret': lookedUpUser['two_fa_secret'], const salt = Buffer.from(lookedUpUser.salt, 'binary')
'encoding': 'base32', crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
'token': authToken, if(delivered_key.toString('hex') === lookedUpUser.password){
'window': 2
})
if(lookedUpUser.two_fa_enabled == 1 && !authToken){ User.generateMasterKey(lookedUpUser.id, password)
.then( result => User.getMasterKey(lookedUpUser.id, password))
.then(masterKey => {
statusObject['verificationRequired'] = true User.generateKeypair(lookedUpUser.id, masterKey)
statusObject['message'] = '2FA authentication required.' .then(({publicKey, privateKey}) => {
return resolve(statusObject) //Passback a json web token
} Auth.createToken(lookedUpUser.id, masterKey)
.then(token => {
if(lookedUpUser.two_fa_enabled == 1 && !tokenValidates){ return resolve({ token: token, userId:lookedUpUser.id })
statusObject['verificationRequired'] = true
statusObject['message'] = 'Invalid Authorization Token.'
return resolve(statusObject)
}
if(lookedUpUser.two_fa_enabled == 0 || (lookedUpUser.two_fa_enabled == 1 && tokenValidates) ){
//hash the password and check for a match
const salt = Buffer.from(lookedUpUser.salt, 'binary')
crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
if(delivered_key.toString('hex') === lookedUpUser.password){
User.generateMasterKey(lookedUpUser.id, password)
.then( result => User.getMasterKey(lookedUpUser.id, password))
.then(masterKey => {
User.generateKeypair(lookedUpUser.id, masterKey)
.then(({publicKey, privateKey}) => {
//Passback a json web token
Auth.createToken(lookedUpUser.id, masterKey)
.then(token => {
statusObject['token'] = token
statusObject['userId'] = lookedUpUser.id
statusObject['success'] = true
return resolve(statusObject)
})
}) })
}) })
})
} else { } else {
return resolve(statusObject)
}
})
}
reject('Password does not match database')
}
})
} else { } else {
return reject('Incorrect Username or Password')
//If user is not found, say two factor authentication is required
statusObject['verificationRequired'] = true
statusObject['message'] = '2FA authentication required.'
//Show fake auth token message
if(authToken){
statusObject['message'] = 'Invalid Authorization Token.'
}
return resolve(statusObject)
} }
}) })
.catch(console.log) .catch(console.log)
@@ -194,19 +141,17 @@ User.register = (username, password) => {
} }
//Counts notes, pinned notes, archived notes, shared notes, unread notes, total files and types //Counts notes, pinned notes, archived notes, shared notes, unread notes, total files and types
User.getCounts = (userId, extendedOptions) => { User.getCounts = (userId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let countTotals = { let countTotals = {}
tags: {} const userHash = cs.hash(String(userId)).toString('base64')
}
// const userHash = cs.hash(String(userId)).toString('base64')
db.promise().query( db.promise().query(
`SELECT `SELECT
SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes, SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes,
SUM(trashed = 1) AS trashedNotes, SUM(trashed = 1) AS trashedNotes,
SUM(share_user_id IS NULL && trashed = 0 AND quick_note < 2) AS totalNotes, SUM(share_user_id IS NULL && trashed = 0) AS totalNotes,
SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount, SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount,
SUM(share_user_id != ? && trashed = 0) AS sharedToNotes SUM(share_user_id != ? && trashed = 0) AS sharedToNotes
FROM note FROM note
@@ -241,77 +186,15 @@ User.getCounts = (userId, extendedOptions) => {
Object.assign(countTotals, rows[0][0]) //combine results Object.assign(countTotals, rows[0][0]) //combine results
return db.promise().query('SELECT id AS quickNote FROM note WHERE quick_note = 1 AND user_id = ?', [userId])
}).then( (rows, fields) => {
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 //Convert everything to an int or 0
Object.keys(countTotals).forEach( key => { Object.keys(countTotals).forEach( key => {
const count = parseInt(countTotals[key]) const count = parseInt(countTotals[key])
countTotals[key] = count ? count : 0 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 countTotals['currentVersion'] = version
// Allow for extended options set on page load resolve(countTotals)
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)
}
}) })
}) })
@@ -370,11 +253,12 @@ User.generateMasterKey = (userId, password) => {
} }
User.getMasterKey = (userId, password) => { User.getMasterKey = (userId, password) => {
return new Promise((resolve, reject) => {
if(!userId || !password){ if(!userId || !password){
reject('Need userId and password to fetch key') reject('Need userId and password to fetch key')
} }
return new Promise((resolve, reject) => {
db.promise().query('SELECT * FROM user_key WHERE user_id = ? LIMIT 1', [userId]) db.promise().query('SELECT * FROM user_key WHERE user_id = ? LIMIT 1', [userId])
.then((rows, fields) => { .then((rows, fields) => {
@@ -491,79 +375,12 @@ User.getByUserName = (username) => {
}) })
} }
User.changePassword = (userId, oldPass, newPass) => {
return new Promise((resolve, reject) => {
User.getMasterKey(userId, oldPass)
.then(masterKey => {
User.getPrivateKey(userId, masterKey)
.then(privateKey => {
//If success, user has correct password
// Generate new master pass, encrypt with new password
// const masterPassword = cs.createSmallSalt()
const salt = cs.createSmallSalt()
const encryptedMasterPassword = cs.encrypt(newPass, salt, masterKey)
const encryptedPrivateKey = cs.encrypt(masterKey, salt, privateKey)
db.promise()
.query(
'UPDATE user_key SET salt = ?, `key` = ?, private_key_encrypted = ? WHERE user_id = ? LIMIT 1',
[salt, encryptedMasterPassword, encryptedPrivateKey, userId]
).then((r,f) => {
//Create login using password
let shasum = crypto.createHash('sha512') //Prepare Hash
const saltString = shasum.digest('hex')
const passwordSalt = Buffer.from(saltString, 'binary') //Generate Salt hash
const iterations = 25000
crypto.pbkdf2(newPass, passwordSalt, iterations, 512, 'sha512', function(err, delivered_key) {
const deliveredPass = delivered_key.toString('hex')
db.promise().query('UPDATE user SET password = ?, salt = ? WHERE id = ? LIMIT 1', [deliveredPass, passwordSalt, userId])
.then((r,f) => {
return resolve(true)
})
})
})
})
})
.catch(error => {
resolve(false)
})
})
}
User.revokeActiveSessions = (userId, sessionId) => {
return new Promise((resolve, reject) => {
const userHash = cs.hash(String(userId)).toString('base64')
db.promise().query('DELETE FROM user_active_session WHERE user_hash = ? AND session_id != ?', [userHash, sessionId])
.then((r,f) => {
resolve(true)
})
})
}
User.deleteUser = (userId, password) => { 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 //Verify user is correct by decryptig master key with password
let deletePromises = [] let deletePromises = []
//Delete all notes and raw text
let noteDelete = db.promise().query(` let noteDelete = db.promise().query(`
DELETE note, note_raw_text DELETE note, note_raw_text
FROM note FROM note
@@ -572,14 +389,12 @@ User.deleteUser = (userId, password) => {
`,[userId]) `,[userId])
deletePromises.push(noteDelete) deletePromises.push(noteDelete)
//Delete user entry
let userDelete = db.promise().query(` let userDelete = db.promise().query(`
DELETE FROM user WHERE id = ? DELETE FROM user WHERE id = ?
`,[userId]) `,[userId])
deletePromises.push(userDelete) deletePromises.push(userDelete)
//Delete user_key, encrypted search index let tables = ['user_key', 'user_encrypted_search_index', 'attachment']
let tables = ['user_key', 'user_encrypted_search_index']
tables.forEach(tableName => { tables.forEach(tableName => {
const query = `DELETE FROM ${tableName} WHERE user_id = ?` const query = `DELETE FROM ${tableName} WHERE user_id = ?`
@@ -587,7 +402,58 @@ User.deleteUser = (userId, password) => {
deletePromises.push(deleteQuery) deletePromises.push(deleteQuery)
}) })
//Remove all note attachments and files
return Promise.all(deletePromises) 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'
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')
resolve({testUserId, masterKey})
})
})
}

View File

@@ -6,27 +6,20 @@ let router = express.Router()
let Attachment = require('@models/Attachment') let Attachment = require('@models/Attachment')
let Note = require('@models/Note') let Note = require('@models/Note')
let userId = null let userId = null
let masterKey = null
// middleware that is specific to this router // middleware that is specific to this router
router.use(function setUserId (req, res, next) { router.use(function setUserId (req, res, next) {
if(userId = req.headers.userId){
//Session key is required to continue
if(!req.headers.sessionId){
next('Unauthorized')
}
if(req.headers.userId){
userId = req.headers.userId userId = req.headers.userId
masterKey = req.headers.masterKey masterKey = req.headers.masterKey
next()
} }
next()
}) })
router.post('/search', function (req, res) { router.post('/search', function (req, res) {
Attachment.search(userId, req.body.noteId, req.body.attachmentType, req.body.offset, req.body.setSize, req.body.includeShared) Attachment.search(userId, req.body.noteId, req.body.attachmentType, req.body.offset, req.body.setSize)
.then( data => res.send(data) ) .then( data => res.send(data) )
}) })
@@ -35,6 +28,11 @@ router.post('/textsearch', function (req, res) {
.then( data => res.send(data) ) .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) { router.post('/update', function (req, res) {
Attachment.update(userId, req.body.attachmentId, req.body.updatedText, req.body.noteId) Attachment.update(userId, req.body.attachmentId, req.body.updatedText, req.body.noteId)
.then( result => { .then( result => {
@@ -60,26 +58,5 @@ router.post('/upload', upload.single('file'), function (req, res, next) {
}) })
//
// Push URL to attachments
// push action on - public controller
//
// get push key
router.post('/getbookmarklet', function (req, res) {
Attachment.getPushkeyBookmarklet(userId)
.then( data => res.send(data) )
})
// generate new push key
router.post('/generatepushkey', function (req, res) {
})
// delete push key
router.post('/deletepushkey', function (req, res) {
})
module.exports = router module.exports = router

View File

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

@@ -10,17 +10,12 @@ let masterKey = null
// middleware that is specific to this router // middleware that is specific to this router
router.use(function setUserId (req, res, next) { router.use(function setUserId (req, res, next) {
//Session key is required to continue
if(!req.headers.sessionId){
next('Unauthorized')
}
if(req.headers.userId){ if(req.headers.userId){
userId = req.headers.userId userId = req.headers.userId
masterKey = req.headers.masterKey masterKey = req.headers.masterKey
next()
} }
next()
}) })
// //
@@ -60,6 +55,14 @@ router.post('/search', function (req, res) {
}) })
}) })
router.post('/difftext', function (req, res) {
Note.getDiffText(userId, req.body.noteId, req.body.text, req.body.updated)
.then( fullDiffText => {
//Response should be full diff text
res.send(fullDiffText)
})
})
router.post('/reindex', function (req, res) { router.post('/reindex', function (req, res) {
Note.reindex(userId, masterKey) Note.reindex(userId, masterKey)
.then( data => { .then( data => {

View File

@@ -1,85 +1,18 @@
var express = require('express') var express = require('express')
var router = express.Router() var router = express.Router()
const rateLimit = require('express-rate-limit')
const Note = require('@models/Note')
const User = require('@models/User')
const Attachment = require('@models/Attachment')
let Note = require('@models/Note')
// //
// Public Note action // Public Note action
// //
const sharedNoteLimiter = rateLimit({ router.post('/opensharednote', function (req, res) {
windowMs: 30 * 60 * 1000, //30 min window
max: 50, // start blocking after 50 requests
message:'Unable to open that many shared notes'
})
router.post('/opensharednote', sharedNoteLimiter, function (req, res) {
Note.getShared(req.body.noteId, req.body.sharedKey) Note.getShared(req.body.noteId, req.body.sharedKey)
.then(results => res.send(results)) .then(results => res.send(results))
}) })
//
// Login User
//
const loginLimiter = rateLimit({
windowMs: 30 * 60 * 1000, // 30 min window
max: 25, // start blocking after 25 requests
message:'Please try to login again later'
})
router.post('/login', loginLimiter, function (req, res) {
User.login(req.body.username, req.body.password, req.body.authToken)
.then( returnData => {
res.send(returnData)
})
})
//
// Register User
//
const registerLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour window
max: 5, // start blocking after 5 requests
message:'Please try again to create an acount in an hour'
})
router.post('/register', registerLimiter, function (req, res) {
User.register(req.body.username, req.body.password)
.then( returnData => {
res.send(returnData)
})
})
//
// 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 module.exports = router

View File

@@ -8,17 +8,12 @@ let masterKey = null
// middleware that is specific to this router // middleware that is specific to this router
router.use(function setUserId (req, res, next) { router.use(function setUserId (req, res, next) {
if(userId = req.headers.userId){
//Session key is required to continue
if(!req.headers.sessionId){
next('Unauthorized')
}
if(req.headers.userId){
userId = req.headers.userId userId = req.headers.userId
masterKey = req.headers.masterKey masterKey = req.headers.masterKey
next()
} }
next()
}) })
//Get quick note text //Get quick note text

View File

@@ -1,24 +1,16 @@
var express = require('express') var express = require('express')
var router = express.Router() var router = express.Router()
let Tags = require('@models/Tag') let Tags = require('@models/Tag');
let userId = null let userId = null
let masterKey = null
// middleware that is specific to this router // middleware that is specific to this router
router.use(function setUserId (req, res, next) { router.use(function setUserId (req, res, next) {
if(userId = req.headers.userId){
//Session key is required to continue
if(!req.headers.sessionId){
next('Unauthorized')
}
if(req.headers.userId){
userId = req.headers.userId userId = req.headers.userId
masterKey = req.headers.masterKey
next()
} }
next()
}) })
//Get the latest notes the user has created //Get the latest notes the user has created
@@ -50,12 +42,6 @@ router.post('/get', function (req, res) {
.then( data => res.send(data) ) .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 //Get all the tags for this user in order of usage
router.post('/usertags', function (req, res) { router.post('/usertags', function (req, res) {
Tags.userTags(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters) Tags.userTags(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters)

View File

@@ -1,28 +1,35 @@
var express = require('express') var express = require('express')
var router = express.Router() var router = express.Router()
const User = require('@models/User') let User = require('@models/User');
const Auth = require('@helpers/Auth')
const cs = require('@helpers/CryptoString') const cs = require('@helpers/CryptoString')
let userId = null
let masterKey = null
// middleware that is specific to this router // middleware that is specific to this router
router.use(function setUserId (req, res, next) { router.use(function timeLog (req, res, next) {
// console.log('Time: ', Date.now())
//Session key is required to continue next()
if(!req.headers.sessionId){
next('Unauthorized')
}
if(req.headers.userId){
userId = req.headers.userId
masterKey = req.headers.masterKey
next()
}
}) })
// define the home page route
router.get('/', function (req, res) {
res.send('User Home Page ' + User.getUsername())
})
// define the about route
router.get('/about', function (req, res) {
User.getUsername(req.headers.userId)
.then( data => res.send(data) )
})
// Login User
router.post('/login', function (req, res) {
User.login(req.body.username, req.body.password)
.then( returnData => {
res.send(returnData)
})
.catch(e => {
res.send(false)
})
})
// Logout User // Logout User
router.post('/logout', function (req, res) { router.post('/logout', function (req, res) {
@@ -32,55 +39,28 @@ router.post('/logout', function (req, res) {
}) })
}) })
// change password
router.post('/changepassword', function (req, res) {
User.changePassword(req.headers.userId, req.body.currentPass, req.body.newPass)
// Login User
router.post('/register', function (req, res) {
User.register(req.body.username, req.body.password)
.then( returnData => { .then( returnData => {
res.send(returnData) res.send(returnData)
}) })
.catch(e => {
res.send(false)
})
}) })
//Revoke all active session keys for user
router.post('/revokesessions', function(req, res) {
User.revokeActiveSessions(req.headers.userId, req.headers.sessionId)
.then( returnData => {
res.send(returnData)
})
})
// fetch counts of users notes // fetch counts of users notes
router.post('/totals', function (req, res) { router.post('/totals', function (req, res) {
User.getCounts(req.headers.userId, req.body.extendedOptions) User.getCounts(req.headers.userId)
.then( countsObject => res.send( countsObject )) .then( countsObject => res.send( countsObject ))
}) })
//
// Two Factor Auth Setup
//
router.post('/twofactorsetup', function (req, res) {
//Send QR code to user for 2FA setup
Auth.generateTwoFactorSecretKey(req.headers.userId, req.body.password)
.then( ({ qrCode }) => { res.send( qrCode ) })
})
router.post('/verifytwofactorsetuptoken', function (req, res) {
//Verify Users QR code with token
Auth.setTwoFactorEnabled(req.headers.userId, req.body.password, req.body.token, true)
.then( ( results ) => { res.send( results ) })
})
router.post('/validatetwofactortoken', function (req, res) {
//Verify Users QR code with token
Auth.validateTwoFactorToken(req.headers.userId, req.body.password, req.body.token)
.then( ( results ) => { res.send( results ) })
})
module.exports = router module.exports = router

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
#!/bin/bash #!/bin/bash
cd /home/mab/ss echo 'Make sure this is being run from root folder of project'
echo '::--:: Starting dev server. cd client; npm run serve -> 192.168.1.164:8081' echo 'Starting Client webpack dev server (/app), in a screen, watching for file changes...'
screen -dmS "NoteClientScreen" bash -c "cd /home/mab/ss/client; npm run serve -- --port 8081 --https true" screen -dm bash -c "cd client/; npm run watch"
echo '::--:: Starting API server (/api), watching for file changes...' echo 'Starting API server (/api), watching for file changes...'
cd /home/mab/ss/server cd server
pm2 flush
pm2 start ecosystem.config.js pm2 start ecosystem.config.js

4
staticFiles/.gitignore vendored Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,19 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,24 +0,0 @@
{
"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

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

Some files were not shown because too many files have changed in this diff Show More