Compare commits
55 Commits
dev
...
d349fb8328
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d349fb8328 | ||
|
|
09cccf1983 | ||
|
|
97e7b011d9 | ||
|
|
fc1f3f81fe | ||
|
|
9c4fff7913 | ||
|
|
b0eee636b5 | ||
|
|
2861042485 | ||
|
|
1005913c0b | ||
|
|
c8033588dd | ||
|
|
bcb31e9af5 | ||
|
|
596e57eaf0 | ||
|
|
d91b0735fd | ||
|
|
71f909fb76 | ||
|
|
a44bca204c | ||
|
|
7c15427b3d | ||
|
|
ed4a5e5291 | ||
|
|
c11f1b1b6f | ||
|
|
0b5675e000 | ||
|
|
9309ea0821 | ||
|
|
5975ab6d68 | ||
|
|
3d6e527e3a | ||
|
|
88a0c7b26a | ||
|
|
1b14a8fd31 | ||
|
|
4cc6014581 | ||
|
|
196224d0b8 | ||
|
|
795f1b7d76 | ||
|
|
1600bd132c | ||
|
|
2a379f8a4e | ||
|
|
3ed26bcc03 | ||
|
|
282cbfe7bc | ||
|
|
b50aecdfca | ||
|
|
98f4695739 | ||
|
|
984ac6ccff | ||
|
|
f63c0c0d60 | ||
|
|
a478cbe11c | ||
|
|
99b69c234f | ||
|
|
f0b6d7b85e | ||
|
|
596703a963 | ||
|
|
21f606b480 | ||
|
|
b961a69a91 | ||
|
|
8d3762e106 | ||
|
|
b2f241dbba | ||
|
|
8833a213a7 | ||
|
|
f833845452 | ||
|
|
05152cd5a4 | ||
|
|
cf3289aac6 | ||
|
|
acf72ca67e | ||
|
|
7f93925f74 | ||
|
|
d2c1dedffb | ||
|
|
003c7e32b1 | ||
|
|
de646cf1de | ||
|
|
2828cc9462 | ||
|
|
f99d6ed430 | ||
|
|
4216c1825e | ||
|
|
8d07a8e11a |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -7,9 +7,3 @@ pids
|
|||||||
*.seed
|
*.seed
|
||||||
*.pid.lock
|
*.pid.lock
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# exclude everything
|
|
||||||
staticFiles/*
|
|
||||||
|
|
||||||
# exception to the rule
|
|
||||||
!staticFiles/assets/
|
|
||||||
@@ -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 '-------'
|
|
||||||
@@ -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
12
client/.babelrc
Normal 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
9
client/.editorconfig
Normal 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
16
client/.gitignore
vendored
@@ -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
10
client/.postcssrc.js
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/).
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
41
client/build/build.js
Normal file
41
client/build/build.js
Normal 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'
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
54
client/build/check-versions.js
Normal file
54
client/build/check-versions.js
Normal 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
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
101
client/build/utils.js
Normal 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')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
22
client/build/vue-loader.conf.js
Normal file
22
client/build/vue-loader.conf.js
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
82
client/build/webpack.base.conf.js
Normal file
82
client/build/webpack.base.conf.js
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
96
client/build/webpack.dev.conf.js
Executable file
96
client/build/webpack.dev.conf.js
Executable 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
145
client/build/webpack.prod.conf.js
Normal file
145
client/build/webpack.prod.conf.js
Normal 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
7
client/config/dev.env.js
Normal 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
69
client/config/index.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
4
client/config/prod.env.js
Normal file
4
client/config/prod.env.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
'use strict'
|
||||||
|
module.exports = {
|
||||||
|
NODE_ENV: '"production"'
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"module": "esnext",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lib": [
|
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25382
client/package-lock.json
generated
25382
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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.4",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.5.2",
|
||||||
"vue-chartjs": "^5.0.1",
|
"vue-router": "^3.0.1",
|
||||||
"vue-router": "^3.2.0",
|
"vuex": "^3.1.3"
|
||||||
"vuedraggable": "^2.24.3",
|
|
||||||
"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.13",
|
||||||
|
"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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
@@ -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,151 +16,53 @@
|
|||||||
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
|
|
||||||
blockUntilNextRequest: false, //If token was just renewed, don't fetch more until next request
|
|
||||||
|
|
||||||
requireAuth: false,
|
|
||||||
password: '',
|
|
||||||
otp: '',
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
//Axios response interceptor
|
|
||||||
// - Gets new session tokens from server and uses them in app
|
|
||||||
beforeCreate: function(){
|
beforeCreate: function(){
|
||||||
|
|
||||||
//Before all requests going out
|
|
||||||
axios.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
|
|
||||||
//Enable token fetching after another request is made
|
|
||||||
if(this.blockUntilNextRequest){
|
|
||||||
this.fetchingInProgress = false
|
|
||||||
this.blockUntilNextRequest = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add a response interceptor, token can be renewed on every response
|
|
||||||
axios.interceptors.response.use(
|
|
||||||
(response) => {
|
|
||||||
|
|
||||||
if(typeof response.headers.remaininguses !== 'undefined'){
|
|
||||||
|
|
||||||
// console.log(response.headers.remaininguses)
|
|
||||||
//Look at remaining uses of token, if its less than five, request a new one
|
|
||||||
if(response.headers.remaininguses < 15 && !this.fetchingInProgress && !this.blockUntilNextRequest){
|
|
||||||
this.fetchingInProgress = true
|
|
||||||
const currentToken = localStorage.getItem('loginToken')
|
|
||||||
this.$io.emit('renew_session_token', currentToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
},
|
|
||||||
(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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
//Puts token into state on page load
|
//Puts token into state on page load
|
||||||
let token = localStorage.getItem('loginToken')
|
let token = localStorage.getItem('loginToken')
|
||||||
let username = localStorage.getItem('username')
|
let username = localStorage.getItem('username')
|
||||||
|
|
||||||
//
|
// const socket = io({ path:'/socket' });
|
||||||
if(token && token.length > 0){
|
|
||||||
|
|
||||||
//setup username display
|
|
||||||
this.$store.commit('setUsername', username)
|
|
||||||
|
|
||||||
//Set session token on every request if set
|
|
||||||
axios.defaults.headers.common['authorizationtoken'] = token
|
|
||||||
|
|
||||||
//Setup websockets into vue instance
|
|
||||||
const socket = this.$io
|
const socket = this.$io
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
|
|
||||||
//Put user into personal event room for live note updates, etc
|
this.$store.commit('setSocketIoSocket', socket.id)
|
||||||
|
|
||||||
this.$io.emit('user_connect', token)
|
this.$io.emit('user_connect', token)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//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){
|
||||||
this.$store.commit('toggleNightMode', themeNumber)
|
this.$store.commit('toggleNightMode', themeNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Put user data into global store on load
|
||||||
|
if(token){
|
||||||
|
this.$store.commit('setLoginToken', {token, username})
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
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')
|
||||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$io.on('recievend_new_token', newToken => {
|
|
||||||
|
|
||||||
// console.log('Got a new token')
|
|
||||||
|
|
||||||
axios.defaults.headers.common['authorizationtoken'] = newToken
|
|
||||||
localStorage.setItem('loginToken', newToken)
|
|
||||||
|
|
||||||
//Disable getting new tokens until next request
|
|
||||||
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 +71,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>
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,28 +11,12 @@
|
|||||||
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: #21BA45;
|
|
||||||
|
|
||||||
/*theme colors */
|
|
||||||
--body_bg_color: #f5f6f7;
|
--body_bg_color: #f5f6f7;
|
||||||
--small_element_bg_color: #fff;
|
--small_element_bg_color: #fff;
|
||||||
--text_color: #3d3d3d;
|
--text_color: #3d3d3d;
|
||||||
@@ -50,11 +34,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 +42,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 +62,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 +81,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 +89,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 +101,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 +121,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 +211,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 +249,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 +267,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 +282,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 +291,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 +340,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 +393,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 +404,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 +432,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 +471,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 +489,10 @@ padding-right: 10px;
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
/*transition: opacity 0.2s ease;*/
|
-webkit-transition: opacity 0.1s ease, -webkit-transform 0.1s ease;
|
||||||
|
transition: opacity 0.1s ease, -webkit-transform 0.1s ease;
|
||||||
|
transition: transform 0.1s ease, opacity 0.1s ease;
|
||||||
|
transition: transform 0.1s ease, opacity 0.1s ease, -webkit-transform 0.1s ease;
|
||||||
}
|
}
|
||||||
[data-tooltip]:before {
|
[data-tooltip]:before {
|
||||||
-webkit-transform: rotate(45deg) scale(0) !important;
|
-webkit-transform: rotate(45deg) scale(0) !important;
|
||||||
@@ -772,13 +515,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 +619,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 +658,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 +677,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 +725,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 +743,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 +791,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;
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
|
||||||
if ( typeof sanitizeToDOMFragment === 'function' ) {
|
|
||||||
var frag = sanitizeToDOMFragment( html, false, this );
|
|
||||||
empty( node );
|
|
||||||
node.appendChild( frag );
|
|
||||||
} else {
|
|
||||||
node.innerHTML = html;
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,51 +1,45 @@
|
|||||||
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="sixteen wide column">
|
<div class="sixteen wide column">
|
||||||
<div class="ui dividing header">
|
<p>Note Icon
|
||||||
<span v-if="allStyles.noteIcon" >
|
<span v-if="allStyles.noteIcon" >
|
||||||
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
||||||
</span>
|
</span>
|
||||||
Note Icon
|
</p>
|
||||||
</div>
|
|
||||||
<div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
|
<div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
|
||||||
<i :class="`large ${icon} icon`"></i>
|
<i :class="`large ${icon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="sixteen wide column">
|
<div class="sixteen wide column">
|
||||||
<div class="ui dividing header">
|
<p>Icon Color</p>
|
||||||
<span v-if="allStyles.noteIcon" >
|
|
||||||
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
|
||||||
</span>
|
|
||||||
Icon Color
|
|
||||||
</div>
|
|
||||||
<div v-for="color in getReducedColors()"
|
<div v-for="color in getReducedColors()"
|
||||||
class="color-button"
|
class="color-button"
|
||||||
:style="{ backgroundColor:color }"
|
:style="{ backgroundColor:color }"
|
||||||
@@ -53,7 +47,8 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
@@ -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(){
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -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 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>
|
</router-link>
|
||||||
|
|
||||||
<!-- new note -->
|
</div>
|
||||||
<div v-if="loggedIn" class="mobile-button">
|
|
||||||
<span v-if="!disableNewNote" @click="createNote">
|
<div class="two wide center aligned bottom aligned column">
|
||||||
<i class="green plus icon"></i>
|
<img loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
|
||||||
New Note
|
</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>
|
||||||
<span v-if="disableNewNote">
|
<span v-if="disableNewNote" class="ui large basic compact icon button">
|
||||||
<i class="grey plus icon"></i>
|
<i class="grey plus icon"></i>
|
||||||
Working
|
</span>
|
||||||
</span>
|
</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>
|
||||||
|
|
||||||
<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">
|
||||||
|
<!-- <div class="menu-item menu-button" > -->
|
||||||
<i class="white angle left icon"></i>
|
<i class="white angle left icon"></i>
|
||||||
<logo class="menu-logo-display" color="var(--main-accent)" />
|
<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,54 +228,27 @@
|
|||||||
<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">
|
||||||
<i :class="`${getVersionIcon()} icon`"></i> {{ version }}
|
<i :class="`${getVersionIcon()} icon`"></i> {{ version }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -376,25 +264,19 @@
|
|||||||
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 {
|
||||||
version: '0',
|
version: '2.3.4',
|
||||||
username: '',
|
username: '',
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
mobile: false,
|
mobile: false,
|
||||||
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
|
||||||
@@ -406,64 +288,32 @@
|
|||||||
|
|
||||||
if(this.loggedIn){
|
if(this.loggedIn){
|
||||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
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 +332,25 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
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() {
|
||||||
|
this.$bus.$emit('notification', 'Logged Out')
|
||||||
|
this.$store.commit('destroyLoginToken')
|
||||||
|
this.$router.push('/')
|
||||||
|
},
|
||||||
toggleNightMode(){
|
toggleNightMode(){
|
||||||
this.$store.commit('toggleNightMode')
|
this.$store.commit('toggleNightMode')
|
||||||
},
|
},
|
||||||
@@ -527,11 +380,8 @@
|
|||||||
location.reload(true)
|
location.reload(true)
|
||||||
},
|
},
|
||||||
getVersionIcon(){
|
getVersionIcon(){
|
||||||
if(!this.version){
|
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']
|
||||||
return 'radiation alternate'
|
const index = ( parseInt(this.version.replace(/\./g,'')) % (icons.length))
|
||||||
}
|
|
||||||
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))
|
|
||||||
return icons[index]
|
return icons[index]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
@@ -143,15 +98,13 @@
|
|||||||
//Login user if we have a valid token
|
//Login user if we have a valid token
|
||||||
if(data && data.token && data.token.length > 0){
|
if(data && data.token && data.token.length > 0){
|
||||||
|
|
||||||
//Set username to local session
|
|
||||||
this.$store.commit('setUsername', this.username)
|
|
||||||
|
|
||||||
const token = data.token
|
const token = data.token
|
||||||
|
const username = this.username
|
||||||
|
|
||||||
|
this.$store.commit('setLoginToken', {token, username})
|
||||||
|
|
||||||
//Setup socket io after user logs in
|
//Setup socket io after user logs in
|
||||||
axios.defaults.headers.common['authorizationtoken'] = token
|
|
||||||
this.$io.emit('user_connect', token)
|
this.$io.emit('user_connect', token)
|
||||||
localStorage.setItem('loginToken', token)
|
|
||||||
|
|
||||||
//Redirect user to notes section after login
|
//Redirect user to notes section after login
|
||||||
this.$router.push('/notes')
|
this.$router.push('/notes')
|
||||||
@@ -159,83 +112,44 @@
|
|||||||
},
|
},
|
||||||
register(){
|
register(){
|
||||||
|
|
||||||
let error = false
|
if( this.username.length == 0 || this.password.length == 0 ){
|
||||||
|
this.$bus.$emit('notification', '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){
|
||||||
this.$bus.$emit('notification', 'Unable to Sign Up - Username already in use')
|
this.$bus.$emit('notification', 'Username already in use')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.finalizeLogin(data)
|
this.finalizeLogin(data)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.$bus.$emit('notification', 'Unable to Sign Up - Username already in use')
|
this.$bus.$emit('notification', 'Username already in use')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
login(){
|
login(){
|
||||||
|
|
||||||
if( this.username.length == 0 || this.password.length == 0 ){
|
if( this.username.length == 0 || this.password.length == 0 ){
|
||||||
this.$bus.$emit('notification', 'Unable to Login - Username and Password Required')
|
this.$bus.$emit('notification', 'Username and Password Required')
|
||||||
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', '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.$bus.$emit('notification', data.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if(data.success){
|
|
||||||
this.finalizeLogin(data)
|
this.finalizeLogin(data)
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.$bus.$emit('notification', error)
|
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style type="text/css" scoped="true">
|
|
||||||
.small-terms {
|
|
||||||
display: inline-block;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
@@ -1,17 +1,25 @@
|
|||||||
<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, '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 v-if="note.title == '' && note.subtext == ''">
|
<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 == '' && note.encrypted == 0">
|
||||||
Empty Note
|
Empty Note
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -23,109 +31,55 @@
|
|||||||
<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 && !isShowingSearchResults()"
|
||||||
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>
|
||||||
|
|
||||||
|
<span class="subtext" v-if="note.shared == 2">
|
||||||
|
You Shared this note
|
||||||
|
<span v-if="note.updated > note.opened && !beenClicked" class="ui tiny green compact right floated basic button">
|
||||||
|
Updated
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Display highlights from solr results -->
|
||||||
|
<span v-if="note.note_highlights.length > 0" class="term-usage">
|
||||||
|
<span
|
||||||
|
class="usage-row"
|
||||||
|
v-for="highlight in note.note_highlights"
|
||||||
|
:class="{ 'big-text':(highlight <= 100), 'small-text-title':(highlight >= 100) }"
|
||||||
|
v-html="cleanHighlight(highlight)"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- slim card view -->
|
<div v-if="titleView" class="single-line-text" @click="cardClicked">
|
||||||
<div v-if="titleView" class="thin-container" @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>
|
||||||
<!-- icon -->
|
<span v-if="note.title.length == 0 && note.title.length == 0">Empty Note</span>
|
||||||
<span v-if="noteIcon" class="thin-icon">
|
|
||||||
<i :class="`${noteIcon} icon`" :style="{ 'color':iconColor }"></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- title -->
|
|
||||||
<span class="thin-title" v-if="note.title.length > 0">{{ note.title }}</span>
|
|
||||||
|
|
||||||
<!-- snippet -->
|
|
||||||
<span class="thick-sub" v-if="note.subtext.length > 0 && note.title.length == 0">
|
|
||||||
{{ removeHtml(note.subtext) }}
|
|
||||||
</span>
|
|
||||||
<span class="thin-sub" v-else-if="note.subtext.length > 0">
|
|
||||||
{{ removeHtml(note.subtext) }}
|
|
||||||
</span>
|
|
||||||
<span v-else-if="note.title.length == 0 && removeHtml(note.subtext).length == 0">
|
|
||||||
Empty Note
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- tags -->
|
|
||||||
<span v-if="note.tags" class="thin-tags" >
|
|
||||||
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- edited -->
|
|
||||||
<span class="thin-right">
|
|
||||||
{{$helpers.timeAgo( note.updated )}}
|
|
||||||
|
|
||||||
<i class="green link ellipsis vertical icon"></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
</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">{{ tag }}</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 +116,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"/>
|
||||||
@@ -219,6 +179,12 @@
|
|||||||
|
|
||||||
return updated
|
return updated
|
||||||
},
|
},
|
||||||
|
isShowingSearchResults(){
|
||||||
|
if(this.note.note_highlights.length > 0 || this.note.attachment_highlights.length > 0 || this.note.tag_highlights.length > 0){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
splitTags(text){
|
splitTags(text){
|
||||||
return text.split(',')
|
return text.split(',')
|
||||||
},
|
},
|
||||||
@@ -226,21 +192,14 @@
|
|||||||
this.$router.push('/attachments/note/'+this.note.id)
|
this.$router.push('/attachments/note/'+this.note.id)
|
||||||
},
|
},
|
||||||
pinNote(){ //togglePinned() <- old name
|
pinNote(){ //togglePinned() <- old name
|
||||||
this.showWorking = true
|
let postData = {'pinned': !this.note.pinned, 'noteId':this.note.id}
|
||||||
this.note.pinned = this.note.pinned == 1 ? 0:1
|
|
||||||
let postData = {'pinned': this.note.pinned, 'noteId':this.note.id}
|
|
||||||
axios.post('/api/note/setpinned', postData)
|
axios.post('/api/note/setpinned', postData)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.showWorking = false
|
|
||||||
// this event is triggered by the server after note is saved
|
|
||||||
// this.$bus.$emit('update_single_note', this.note.id)
|
// 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') })
|
||||||
},
|
},
|
||||||
archiveNote(){ //toggleArchived() <- old name
|
archiveNote(){ //toggleArchived() <- old name
|
||||||
|
|
||||||
this.showWorking = true
|
|
||||||
|
|
||||||
let postData = {'archived': !this.note.archived, 'noteId':this.note.id}
|
let postData = {'archived': !this.note.archived, 'noteId':this.note.id}
|
||||||
axios.post('/api/note/setarchived', postData)
|
axios.post('/api/note/setarchived', postData)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -248,17 +207,15 @@
|
|||||||
//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') })
|
||||||
},
|
},
|
||||||
trashNote(){ //toggleArchived() <- old name
|
trashNote(){ //toggleArchived() <- old name
|
||||||
|
|
||||||
this.showWorking = true
|
|
||||||
|
|
||||||
let postData = {'trashed': !this.note.trashed, 'noteId':this.note.id}
|
let postData = {'trashed': !this.note.trashed, 'noteId':this.note.id}
|
||||||
axios.post('/api/note/settrashed', postData)
|
axios.post('/api/note/settrashed', postData)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -266,10 +223,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 +241,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)
|
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -320,7 +271,6 @@
|
|||||||
beenClicked: false,
|
beenClicked: false,
|
||||||
showTagSlideMenu: false,
|
showTagSlideMenu: false,
|
||||||
triggerClosedAnimation: false, //Show just closed animation
|
triggerClosedAnimation: false, //Show just closed animation
|
||||||
showWorking: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -381,11 +331,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 +362,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 +376,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 +411,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 +421,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 +464,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 +472,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 +482,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;
|
||||||
@@ -629,7 +539,7 @@
|
|||||||
height: calc(100% + 30px);
|
height: calc(100% + 30px);
|
||||||
}
|
}
|
||||||
.currently-open:after {
|
.currently-open:after {
|
||||||
content: '...';
|
content: 'Open';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -651,50 +561,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 +596,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 +619,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>
|
||||||
@@ -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>
|
|
||||||
@@ -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') })
|
||||||
|
|||||||
@@ -8,13 +8,8 @@
|
|||||||
<div class="ui grid" v-if="shareUsername == null">
|
<div class="ui grid" v-if="shareUsername == null">
|
||||||
|
|
||||||
<div v-if="!isNoteShared" class="sixteen wide column">
|
<div v-if="!isNoteShared" class="sixteen wide column">
|
||||||
<div class="ui button" v-on:click="makeShared()">Enable Sharing</div>
|
<div class="ui button" v-on:click="makeShared()">Enable Shared</div>
|
||||||
<ul>
|
<p>Shared notes are different and junk.</p>
|
||||||
<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>If you turn off sharing, no one else can read the note.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isNoteShared" class="sixteen wide column">
|
<div v-if="isNoteShared" class="sixteen wide column">
|
||||||
@@ -22,10 +17,14 @@
|
|||||||
<div class="ui button" v-on:click="removeShared()">Remove Shared</div>
|
<div class="ui button" v-on:click="removeShared()">Remove Shared</div>
|
||||||
|
|
||||||
<div class="ui button" v-on:click="getSharedUrl()">Get Shareable URL</div>
|
<div class="ui button" v-on:click="getSharedUrl()">Get Shareable URL</div>
|
||||||
</div>
|
|
||||||
<div class="sixteen wide column" v-if="isNoteShared && sharedUrl.length > 0">
|
<div v-if="sharedUrl.length > 0">
|
||||||
<p>Public Link - this link can be disabled by turning off sharing</p>
|
|
||||||
<a target="_blank" :href="sharedUrl">{{ sharedUrl }}</a>
|
<a target="_blank" :href="sharedUrl">{{ sharedUrl }}</a>
|
||||||
|
<div class="ui input">
|
||||||
|
<input type="text" v-model="sharedUrl">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 class="slide-shadow" :class="{'full-shadow':fullShadow}" v-on:click="close"></div> -->
|
</div>
|
||||||
|
|
||||||
|
<div class="slide-shadow" :class="{'full-shadow':fullShadow}" v-on:click="close"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- </transition> -->
|
<!-- </transition> -->
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,29 +14,29 @@ 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
|
|
||||||
require('./assets/semantic-helper.css')
|
require('./assets/semantic-helper.css')
|
||||||
// Fonts
|
// Fonts
|
||||||
require('./assets/roboto-latin.woff2')
|
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({
|
||||||
|
el: '#app',
|
||||||
router,
|
router,
|
||||||
store,
|
store,
|
||||||
render: h => h(App),
|
components: { App },
|
||||||
}).$mount('#app')
|
template: '<App/>'
|
||||||
|
})
|
||||||
@@ -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,32 +143,31 @@ 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 => {
|
Array.from( container.getElementsByClassName('active') ).forEach(item => {
|
||||||
item.classList.remove('active');
|
item.classList.remove('active');
|
||||||
})
|
})
|
||||||
|
|
||||||
},600)
|
|
||||||
|
|
||||||
},
|
},
|
||||||
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
|
|
||||||
this.$router.go(-1)
|
|
||||||
|
|
||||||
setTimeout(()=>{
|
|
||||||
|
|
||||||
//Go through each item, on first level, look for Unordered Lists
|
//Go through each item, on first level, look for Unordered Lists
|
||||||
container.childNodes.forEach( (node) => {
|
container.childNodes.forEach( (node) => {
|
||||||
if(node.nodeName == 'UL'){
|
if(node.nodeName == 'UL'){
|
||||||
@@ -238,24 +210,20 @@ const SquireButtonFunctions = {
|
|||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}, 600)
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
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
|
|
||||||
this.$router.go(-1)
|
|
||||||
|
|
||||||
setTimeout(()=>{
|
|
||||||
|
|
||||||
//Go through each item, on first level, look for Unordered Lists
|
//Go through each item, on first level, look for Unordered Lists
|
||||||
container.childNodes.forEach( (node) => {
|
container.childNodes.forEach( (node) => {
|
||||||
if(node.nodeName == 'UL'){
|
if(node.nodeName == 'UL'){
|
||||||
@@ -306,22 +274,20 @@ const SquireButtonFunctions = {
|
|||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
},600)
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
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,8 +300,6 @@ const SquireButtonFunctions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(()=>{
|
|
||||||
|
|
||||||
//Go through each item, on first level, look for Unordered Lists
|
//Go through each item, on first level, look for Unordered Lists
|
||||||
container.childNodes.forEach( (node) => {
|
container.childNodes.forEach( (node) => {
|
||||||
|
|
||||||
@@ -363,80 +327,17 @@ const SquireButtonFunctions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
},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()
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -10,33 +10,10 @@
|
|||||||
-webkit-animation: fadeorama 16s ease infinite;
|
-webkit-animation: fadeorama 16s ease infinite;
|
||||||
-moz-animation: fadeorama 16s ease infinite;
|
-moz-animation: fadeorama 16s ease infinite;
|
||||||
animation: fadeorama 16s ease infinite;
|
animation: fadeorama 16s ease infinite;
|
||||||
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: 50%;
|
||||||
height: auto;
|
max-width: 450px;
|
||||||
}
|
}
|
||||||
.lightly-padded {
|
.lightly-padded {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
@@ -46,14 +23,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; }
|
||||||
@@ -103,19 +76,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-main img {
|
.home-main img {
|
||||||
max-height: 250px;
|
max-height: 400px !important;
|
||||||
}
|
}
|
||||||
.white-link {
|
.white-link {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 740px) {
|
|
||||||
.column > img {
|
|
||||||
max-height: 125px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -124,59 +91,53 @@
|
|||||||
|
|
||||||
<div class="row hero fadeBg" :style="{ 'height':(height+'px') }">
|
<div class="row hero fadeBg" :style="{ 'height':(height+'px') }">
|
||||||
|
|
||||||
<!-- <div class="one wide large screen only column"></div> -->
|
<!-- All marketing images if you need to review -->
|
||||||
|
<div v-if="false" class="sixteen wide column">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/add.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/gardening.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/growth.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/icecream.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/investing.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/onboarding.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/robot.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/solution.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/watching.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/cloud.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/grandma.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/hamburger.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/idea.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/notebook.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/plan.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/secure.svg" alt="">
|
||||||
|
<img loading="lazy" width="10%" src="/api/static/assets/marketing/void.svg" alt="">
|
||||||
|
</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="seven wide middle aligned left 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>
|
||||||
|
|
||||||
<!-- <div class="eight wide middle aligned left aligned column">
|
<div class="eight wide middle aligned left aligned column">
|
||||||
<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 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 -->
|
|
||||||
<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/gardening.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/growth.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/icecream.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/investing.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/onboarding.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/robot.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/solution.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/watching.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/cloud.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/grandma.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/hamburger.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/idea.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/notebook.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/plan.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/secure.svg">
|
|
||||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/void.svg">
|
|
||||||
</div>
|
</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>
|
||||||
@@ -189,243 +150,90 @@
|
|||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
<h2>
|
<h2>
|
||||||
<i class="plug icon"></i>
|
<i class="plug icon"></i>
|
||||||
Sign Up Now - Only a Username and Password required
|
Sign Up Now - Only a Username and Password required</h2>
|
||||||
</h2>
|
|
||||||
<login-form :thin="true" />
|
<login-form :thin="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Overview -->
|
<!-- set -->
|
||||||
<div class="middle aligned centered row">
|
<div class="middle aligned centered row">
|
||||||
<div class="six wide column">
|
<div class="six wide right aligned column">
|
||||||
<h2 class="ui dividing header">Powerful text editing and privacy</h2>
|
<h2>Solid Scribe is a browser based note application that focuses on ease of use while keeping your data private</h2>
|
||||||
<h3>Easily edit, share and organize thousands of notes.</h3>
|
<h3>Tools to organize and collaborate on 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 class="four wide column">
|
|
||||||
<svg-displayer file="idea" alt="Explosion of New Ideas" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- theme selector -->
|
|
||||||
<div class="ui white row">
|
|
||||||
<div class="sixteen wide middle aligned column">
|
|
||||||
<div class="ui container">
|
|
||||||
<h2 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>
|
|
||||||
|
|
||||||
<!-- features list -->
|
|
||||||
<div class="top aligned centered row">
|
|
||||||
|
|
||||||
<!-- note features -->
|
|
||||||
<div class="six wide column">
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<i class="icons">
|
|
||||||
<i class="grey sticky note icon"></i>
|
|
||||||
<i class="bottom left corner teal plus icon"></i>
|
|
||||||
</i>
|
|
||||||
Create a million notes!
|
|
||||||
<div class="sub header">Create unlimited notes up to 5,000,000 characters long.</div>
|
|
||||||
</div>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<h2 class="ui header">
|
|
||||||
<div class="content">
|
|
||||||
<i class="icons">
|
|
||||||
<i class="grey tags icon"></i>
|
|
||||||
<i class="bottom left corner purple plus icon"></i>
|
|
||||||
</i>
|
|
||||||
Tag Notes
|
|
||||||
<div class="sub header">Add and edit tags on notes then search or sort by tag.</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>
|
|
||||||
Search Note Text
|
|
||||||
<div class="sub header">Search all notes, files, links and tags.</div>
|
|
||||||
</div>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<h2 class="ui header">
|
|
||||||
<div class="content">
|
|
||||||
<i class="icons">
|
|
||||||
<i class="grey search icon"></i>
|
|
||||||
<i class="bottom left corner pink paperclip icon"></i>
|
|
||||||
</i>
|
|
||||||
Search Attachments
|
|
||||||
<div class="sub header">Add text to Images and links than can be searched.</div>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- editing features -->
|
|
||||||
<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 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 class="middle aligned centered row">
|
|
||||||
<div class="four wide right aligned column">
|
|
||||||
<svg-displayer file="secure" alt="So dang secure" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<h2>Only you can read your notes. </h2>
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/idea.svg" alt="Explosion of New Ideas">
|
||||||
<h3>Employees can not <a target="_blank" href="https://www.forbes.com/sites/zakdoffman/2019/01/30/facebook-has-just-been-caught-spying-on-users-private-messages-and-data-again/#1e27e00a31ce"> snoop your account</a>. No one can <a target="_blank" href="https://mashable.com/article/google-reading-your-emails-response/">read your data for advertising</a>. Notes are completely unreadable without your password.</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="middle aligned centered green row">
|
<div class="middle aligned centered green row">
|
||||||
<div class="six wide column">
|
<div class="six wide right aligned column">
|
||||||
<h2>Extremely accessible - Nothing to install</h2>
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
|
||||||
<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="six wide column">
|
||||||
<svg-displayer file="cloud" alt="Girl falling into the spiral of digital chaos" />
|
<h2>All Note text is encrypted</h2>
|
||||||
|
<h3>Only you can read your notes. <a class="white-link" target="_blank" href="https://www.forbes.com/sites/zakdoffman/2019/01/30/facebook-has-just-been-caught-spying-on-users-private-messages-and-data-again/#1e27e00a31ce"> Employees can not snoop your account</a>. <a class="white-link" target="_blank" href="https://mashable.com/article/google-reading-your-emails-response/">No one can read your data for advertising</a>. Note text is completely unreadable without your password.</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- set -->
|
||||||
|
<div class="middle aligned centered row">
|
||||||
|
<div class="six wide column">
|
||||||
|
<h2>Organize your notes</h2>
|
||||||
|
<h3>Tag, Pin, Color, Archive, Attach Images, Share Encrypted Notes and Search</h3>
|
||||||
|
</div>
|
||||||
|
<div class="six wide column">
|
||||||
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/gardening.svg" alt="Pruning the mind garden">
|
||||||
</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="six wide right aligned column">
|
||||||
<svg-displayer file="robot" alt="Murder Robot in office environment" />
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos">
|
||||||
|
</div>
|
||||||
|
<div class="six wide column">
|
||||||
|
<h2>Extremely accessible - Nothing to install</h2>
|
||||||
|
<h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- set -->
|
||||||
|
<div class="middle aligned centered row">
|
||||||
|
<div class="six wide right aligned column">
|
||||||
|
<h2>Secure Search</h2>
|
||||||
|
<h3>Keyword search using an encrypted search index helps you find what you need without compromising security.</h3>
|
||||||
|
</div>
|
||||||
|
<div class="six wide column">
|
||||||
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/solution.svg" alt="Hypercube of Solutions">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="middle aligned centered row">
|
||||||
|
<div class="six wide right aligned column">
|
||||||
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/plan.svg" alt="Scheme for planetary destruction">
|
||||||
|
</div>
|
||||||
|
<div class="six wide column">
|
||||||
|
<h2>Create Lists with Check Boxes</h2>
|
||||||
|
<h3>Todo lists are supported. With options to removed checked items, sort by completed and un-check all.</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- set -->
|
||||||
|
<div class="middle aligned centered row">
|
||||||
|
<div class="six wide right aligned column">
|
||||||
|
<h2>Powerful Text Editing</h2>
|
||||||
|
<h3>A plethora of editing tools are provided for coloring, underlining, bolding, attaching images and more.</h3>
|
||||||
|
</div>
|
||||||
|
<div class="six wide column">
|
||||||
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/growth.svg" alt="Endless progress at the cost of sanity and health">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="middle aligned centered row">
|
||||||
|
<div class="six wide right aligned column">
|
||||||
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="Shrunken man near giant tablet">
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<h2>Secure Data Sharing</h2>
|
<h2>Secure Data Sharing</h2>
|
||||||
@@ -433,23 +241,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- set -->
|
<!-- set -->
|
||||||
<div class="middle aligned centered row">
|
<!-- <div class="middle aligned centered row">
|
||||||
|
<div class="six wide right aligned column">
|
||||||
|
<h2>Ice Cream</h2>
|
||||||
|
<h3>Get excited without all the screaming</h3>
|
||||||
|
</div>
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<h2>Leave your Ad Blockers turned on</h2>
|
<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 ">
|
||||||
<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://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://pi-hole.net/" target="_blank">Pi-hole</a> on the network.</h3>
|
|
||||||
</div>
|
|
||||||
<div class="four wide column">
|
|
||||||
<svg-displayer file="icecream" alt="Emergence of a 4th dimensional being perceived as a large ice cream" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
|
||||||
|
|
||||||
<div class="middle aligned centered row">
|
<div class="middle aligned centered row">
|
||||||
<div class="six wide right aligned column">
|
<div class="six wide right aligned column">
|
||||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/add.svg" alt="A shpere of newness">
|
<img loading="lazy" width="100%" src="/api/static/assets/marketing/add.svg" alt="A shpere of newness">
|
||||||
@@ -488,44 +290,31 @@
|
|||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
|
<span class="ui button" v-on:click="showRealInformation">About</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="true" class="middle aligned centered row">
|
<div v-if="realInformation" class="middle aligned centered row" ref="real">
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<h3>
|
<h2 class="ui center aligned">
|
||||||
<a target="_blank" href="https://www.maxg.cc">Solid Scribe was created by Max Gialanella</a>
|
Why Does this App exist?
|
||||||
</h3>
|
</h2>
|
||||||
<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.
|
This App exists because I was tired of all my data being owned by big companies, having it farmed out for marketing, and leaving the contents of my life exposed to corporations.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
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>Generic Marketing Images - <a target="_blank" href="https://undraw.co/">https://unDraw.co/</a></p>
|
||||||
<p>If you want to help me out with hosting this application, I would love a small Bitcoin donation.</p>
|
|
||||||
<p>
|
|
||||||
<a href="https://btc3.trezor.io/address/3QYnnNKnYTcU82F8NJ1BrmzGU2zRndTyEG" target="_blank">
|
|
||||||
<img loading="lazy" width="160px" src="/api/static/assets/marketing/wallet.png" alt="3QYnnNKnYTcU82F8NJ1BrmzGU2zRndTyEG">
|
|
||||||
</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 +325,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(){
|
||||||
@@ -569,42 +341,14 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeMount(){
|
beforeMount(){
|
||||||
|
|
||||||
|
//Don't change hero banner on mobile
|
||||||
|
if(!this.$store.getters.getIsUserOnMobile){
|
||||||
|
let windowHeight = window.innerHeight
|
||||||
|
this.height = windowHeight - (windowHeight * 0.18)
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
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(){
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,51 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<!-- Note title card display -->
|
||||||
|
<div class="sixteen wide column">
|
||||||
|
|
||||||
<div class="show-hidden-note-list-button"
|
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0 && fastFilters['notesHome'] == 1">
|
||||||
v-if="collapseFloatingList && openNotes.length > 0" v-on:click="collapseFloatingList = false">
|
No Notes Yet. <br>Thats ok.<br><br> <br>
|
||||||
<i class="caret square right outline icon"></i>
|
<img loading="lazy" width="25%" src="/api/static/assets/marketing/hamburger.svg" alt="Create a new note"><br>
|
||||||
</div>
|
Create one when you feel ready.
|
||||||
|
</h3>
|
||||||
|
|
||||||
<!-- flexbox note container evenly spaces open notes -->
|
<!-- Go to one wide column, do not do this on mobile interface -->
|
||||||
<div class="note-panel-container" :class="{ 'note-panel-fullwidth':collapseFloatingList}" v-if="openNotes.length">
|
<div :class="{'one-column':( showOneColumn() )}">
|
||||||
<note-input-panel
|
|
||||||
v-for="noteId in openNotes"
|
<!-- render each section based on notes in set -->
|
||||||
v-if="noteId != null"
|
<div v-for="section,index in noteSections" v-if="section.length > 0" class="note-card-section">
|
||||||
:key="noteId"
|
<h5 class="ui tiny dividing header"><i :class="`green ${sectionData[index][0]} icon`"></i>{{ sectionData[index][1] }}</h5>
|
||||||
:noteid="noteId"
|
|
||||||
:url-data="$route.params"
|
<div class="note-card-display-area">
|
||||||
:open-notes="openNotes.length"
|
<note-title-display-card
|
||||||
|
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.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<loading-icon v-if="loadingInProgress" message="Decrypting Notes" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<input-notes
|
||||||
|
v-if="activeNoteId1 != null"
|
||||||
|
:key="'active_note_'+activeNoteId1"
|
||||||
|
:noteid="activeNoteId1"
|
||||||
|
:position="activeNote1Position"
|
||||||
|
:url-data="$route.params"
|
||||||
|
ref="note1" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -181,18 +147,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 +168,6 @@
|
|||||||
searchResultsCount: 0,
|
searchResultsCount: 0,
|
||||||
searchTags: [],
|
searchTags: [],
|
||||||
notes: [],
|
notes: [],
|
||||||
openNotes: [],
|
|
||||||
collapseFloatingList: false,
|
|
||||||
highlights: [],
|
highlights: [],
|
||||||
searchDebounce: null,
|
searchDebounce: null,
|
||||||
fastFilters: {},
|
fastFilters: {},
|
||||||
@@ -211,10 +175,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 +224,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 +262,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 +311,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 +347,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 +355,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) &&
|
||||||
|
!this.$store.getters.getIsUserOnMobile
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
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) })
|
||||||
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
|
return
|
||||||
},
|
|
||||||
closeNote(noteId, modified){
|
|
||||||
|
|
||||||
console.log('close note', this.$route.fullPath)
|
|
||||||
|
|
||||||
const openIndex = this.openNotes.indexOf(noteId)
|
|
||||||
if(openIndex != -1){
|
|
||||||
console.log('Removing note id ', noteId + ' on index ' + openIndex)
|
|
||||||
this.openNotes.splice(openIndex, 1)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// //A note has been closed
|
closeNote(position){
|
||||||
// if(this.$route.fullPath != '/notes'){
|
this.activeNoteId1 = null
|
||||||
// this.$router.push('/notes')
|
|
||||||
// }
|
|
||||||
if(this.openNotes.length == 0 && this.$route.fullPath != '/notes'){
|
|
||||||
this.$router.push('/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 +409,6 @@
|
|||||||
},
|
},
|
||||||
onScroll(e){
|
onScroll(e){
|
||||||
|
|
||||||
if(!this.scrollLoadEnabled){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(this.loadingBatchTimeout)
|
clearTimeout(this.loadingBatchTimeout)
|
||||||
this.loadingBatchTimeout = setTimeout(() => {
|
this.loadingBatchTimeout = setTimeout(() => {
|
||||||
|
|
||||||
@@ -537,16 +418,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 +472,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 +505,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 +532,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 +546,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 +568,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 +608,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 +734,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 +743,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 +760,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 +786,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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -4,31 +4,19 @@
|
|||||||
<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" class="squire-box"></div>
|
<div v-if="text" v-html="text"></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">
|
||||||
|
|
||||||
<div class="ui segment">
|
|
||||||
|
|
||||||
<div class="ui grid">
|
|
||||||
<div class="three wide middle aligned center aligned column">
|
|
||||||
<img class="small-logo" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
|
|
||||||
</div>
|
|
||||||
<div class="thirteen wide column">
|
|
||||||
<!-- header -->
|
|
||||||
<h2 class="ui header">
|
<h2 class="ui header">
|
||||||
|
<img class="small-logo" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
Solid Scribe is an easy, free, secure Note App
|
Solid Scribe is an easy, free, secure Note App
|
||||||
<div class="sub header">
|
<div class="sub header">
|
||||||
@@ -36,7 +24,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
<!-- buttons -->
|
|
||||||
<div class="ui grid">
|
<div class="ui grid">
|
||||||
<div class="eight wide center aligned column">
|
<div class="eight wide center aligned column">
|
||||||
<router-link class="ui compact green button" to="/login">
|
<router-link class="ui compact green button" to="/login">
|
||||||
@@ -53,10 +40,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui sixteen wide center aligned column">
|
<div class="ui sixteen wide center aligned column">
|
||||||
<h4>{{ failText }}</h4>
|
<h4>{{ failText }}</h4>
|
||||||
</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
@@ -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')
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import Vuex from 'vuex'
|
|
||||||
|
|
||||||
Vue.use(Vuex)
|
|
||||||
|
|
||||||
export default new Vuex.Store({
|
|
||||||
state: {
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
},
|
|
||||||
modules: {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -6,18 +6,30 @@ Vue.use(Vuex);
|
|||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
|
token: null,
|
||||||
username: null,
|
username: null,
|
||||||
nightMode: false,
|
nightMode: false,
|
||||||
isUserOnMobile: false,
|
isUserOnMobile: false,
|
||||||
fetchTotalsTimeout: null,
|
isNoteSettingsOpen: false, //Little note settings pane
|
||||||
userTotals: null, // {} // setting this to object breaks reactivity
|
socket: null,
|
||||||
activeSessions: 0,
|
userTotals: null,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setUsername(state, username){
|
setLoginToken(state, userData){
|
||||||
|
|
||||||
|
const username = userData.username
|
||||||
|
const token = userData.token
|
||||||
|
|
||||||
|
localStorage.removeItem('loginToken') //We only want one login token per computer
|
||||||
|
localStorage.setItem('loginToken', token)
|
||||||
|
|
||||||
localStorage.removeItem('username') //We only want one login token per computer
|
localStorage.removeItem('username') //We only want one login token per computer
|
||||||
localStorage.setItem('username', username)
|
localStorage.setItem('username', username)
|
||||||
|
|
||||||
|
//Set default token to axios, every request will have header
|
||||||
|
axios.defaults.headers.common['authorizationtoken'] = token
|
||||||
|
|
||||||
|
state.token = token
|
||||||
state.username = username
|
state.username = username
|
||||||
},
|
},
|
||||||
destroyLoginToken(state){
|
destroyLoginToken(state){
|
||||||
@@ -25,34 +37,40 @@ export default new Vuex.Store({
|
|||||||
//Remove login token from local storage and from headers
|
//Remove login token from local storage and from headers
|
||||||
localStorage.removeItem('loginToken')
|
localStorage.removeItem('loginToken')
|
||||||
localStorage.removeItem('username')
|
localStorage.removeItem('username')
|
||||||
localStorage.removeItem('currentVersion')
|
|
||||||
localStorage.removeItem('snippetCache')
|
|
||||||
delete axios.defaults.headers.common['authorizationtoken']
|
delete axios.defaults.headers.common['authorizationtoken']
|
||||||
|
state.token = null
|
||||||
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 +99,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 +111,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,59 +122,24 @@ 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
|
|
||||||
const currentVersion = localStorage.getItem('currentVersion')
|
|
||||||
if(currentVersion == null){
|
|
||||||
localStorage.setItem('currentVersion', totalsObject.currentVersion)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//If version is already set and it doesn't match the server, reload app
|
|
||||||
if(currentVersion != totalsObject.currentVersion){
|
|
||||||
localStorage.setItem('currentVersion', totalsObject.currentVersion)
|
|
||||||
location.reload(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// console.log('-------------')
|
// console.log('-------------')
|
||||||
// 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: {
|
||||||
getUsername: state => {
|
getUsername: state => {
|
||||||
return state.username
|
return state.username
|
||||||
},
|
},
|
||||||
|
getLoginToken: state => {
|
||||||
|
return state.token
|
||||||
|
},
|
||||||
getLoggedIn: state => {
|
getLoggedIn: state => {
|
||||||
let weIn = (state.username && state.username.length > 0)
|
let weIn = (state.token !== null && state.token != undefined && state.token.length > 0)
|
||||||
return weIn
|
return weIn
|
||||||
},
|
},
|
||||||
getIsNightMode: state => {
|
getIsNightMode: state => {
|
||||||
@@ -171,19 +157,10 @@ 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(() => {
|
|
||||||
// load extended options on initial load
|
|
||||||
let postData = {
|
|
||||||
extendedOptions: !state.userTotals
|
|
||||||
}
|
|
||||||
axios.post('/api/user/totals', postData)
|
|
||||||
.then( ({data}) => {
|
.then( ({data}) => {
|
||||||
commit('setUserTotals', data)
|
commit('setUserTotals', data)
|
||||||
})
|
})
|
||||||
@@ -193,7 +170,6 @@ export default new Vuex.Store({
|
|||||||
location.reload()
|
location.reload()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
38
configs/nginx/default
Normal file
38
configs/nginx/default
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
##
|
||||||
|
#
|
||||||
|
# This is just a mock config file, describing what is needed to run the app
|
||||||
|
# The app currently only needs two paths / and /api
|
||||||
|
#
|
||||||
|
##
|
||||||
|
|
||||||
|
#
|
||||||
|
# This is needed to define any ports the app may use from node
|
||||||
|
#
|
||||||
|
upstream expressapp {
|
||||||
|
server 127.0.0.1:3000;
|
||||||
|
keepalive 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
|
||||||
|
#
|
||||||
|
# Needed to server up static, compiled JS files and index.html
|
||||||
|
#
|
||||||
|
location / {
|
||||||
|
autoindex on;
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# define the api route to connect to the backend and serve up static files
|
||||||
|
#
|
||||||
|
location /api {
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-NginX-Proxy true;
|
||||||
|
|
||||||
|
proxy_pass http://expressapp;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -12,4 +12,3 @@ bundle.*
|
|||||||
client/dist*
|
client/dist*
|
||||||
server/public/*
|
server/public/*
|
||||||
client/dist*
|
client/dist*
|
||||||
*_scrape*
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9371
package-lock.json
generated
9371
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -1,31 +1,30 @@
|
|||||||
{
|
{
|
||||||
"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.1",
|
||||||
"gm": "^1.23.1",
|
"gm": "^1.23.1",
|
||||||
"helmet": "^4.1.1",
|
"helmet": "^3.21.3",
|
||||||
"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.6.5",
|
||||||
"node-tesseract-ocr": "^2.0.0",
|
"node-tesseract-ocr": "^1.0.0",
|
||||||
"qrcode": "^1.4.4",
|
"request": "^2.88.0",
|
||||||
"request": "^2.88.2",
|
"request-promise": "^4.2.4",
|
||||||
"request-promise": "^4.2.6",
|
|
||||||
"socket.io": "^2.3.0",
|
"socket.io": "^2.3.0",
|
||||||
"speakeasy": "^2.0.0"
|
"solr-node": "^1.2.1"
|
||||||
},
|
},
|
||||||
"_moduleAliases": {
|
"_moduleAliases": {
|
||||||
"@root": ".",
|
"@root": ".",
|
||||||
@@ -33,8 +32,5 @@
|
|||||||
"@routes": "server/routes",
|
"@routes": "server/routes",
|
||||||
"@helpers": "server/helpers",
|
"@helpers": "server/helpers",
|
||||||
"@config": "server/config"
|
"@config": "server/config"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"jest": "^29.7.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -1,297 +1,25 @@
|
|||||||
const db = require('@config/database')
|
var jwt = require('jsonwebtoken');
|
||||||
const jwt = require('jsonwebtoken')
|
|
||||||
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) => {
|
||||||
Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => {
|
const signedData = {'id':userId, 'date':Date.now(), 'masterKey':masterKey}
|
||||||
return new Promise((resolve, reject) => {
|
const token = jwt.sign(signedData, tokenSecretKey)
|
||||||
|
return token
|
||||||
const created = pastCreatedDate ? pastCreatedDate : Math.floor(+new Date/1000)
|
|
||||||
const userHash = cs.hash(String(userId)).toString('base64')
|
|
||||||
|
|
||||||
//Encrypt Master Password and save it to the server
|
|
||||||
const sessionId = pastId ? pastId : cs.createSmallSalt().slice(0,9) //Use existing session id
|
|
||||||
const salt = cs.createSmallSalt()
|
|
||||||
const tempPass = cs.createSmallSalt()
|
|
||||||
const encryptedMasterPass = cs.encrypt(tempPass, salt, masterKey)
|
|
||||||
|
|
||||||
//Deactivate all other session keys, they delete after 30 seconds
|
|
||||||
db.promise().query('UPDATE user_active_session SET active = 0 WHERE session_id = ?', [sessionId])
|
|
||||||
.then((r,f) => {
|
|
||||||
|
|
||||||
return db.promise().query(
|
|
||||||
'INSERT INTO user_active_session (salt, encrypted_master_password, created, uses, user_hash, session_id) VALUES (?,?,?,?,?,?)',
|
|
||||||
[salt, encryptedMasterPass, created, sessionTokenUses, userHash, sessionId])
|
|
||||||
|
|
||||||
})
|
|
||||||
.then((r,f) => {
|
|
||||||
|
|
||||||
const sessionNum = r[0].insertId
|
|
||||||
|
|
||||||
//Required Data for JWT payload
|
|
||||||
const tokenPayload = {userId, tempPass, sessionNum}
|
|
||||||
|
|
||||||
//Return token
|
|
||||||
const token = jwt.sign(tokenPayload, tokenSecretKey)
|
|
||||||
return resolve(token)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
Auth.decodeToken = (token) => {
|
||||||
//Decodes session token
|
|
||||||
Auth.decodeToken = (token, request = null) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
let decodedToken = null
|
|
||||||
|
|
||||||
//Delete all tokens older than 20 days before continuing or inacive and older than 1 minute
|
|
||||||
const now = (Math.floor((+new Date)/1000))
|
|
||||||
const twentyDays = (Math.floor((+new Date)/1000)) - (86400 * 20)
|
|
||||||
const fourtyFiveSeconds = (Math.floor((+new Date)/1000)) - (45)
|
|
||||||
|
|
||||||
//Decode Json web token
|
|
||||||
jwt.verify(token, tokenSecretKey, function(err, decoded){
|
jwt.verify(token, tokenSecretKey, function(err, decoded){
|
||||||
if(err || decoded.tempPass == undefined || decoded.tempPass.length < 5){
|
if(err || decoded.id == undefined){
|
||||||
throw new Error('Bad Token')
|
reject('Bad Token')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
//Pass back decoded token
|
||||||
decodedToken = decoded
|
resolve(decoded)
|
||||||
|
return
|
||||||
db.promise().query(`DELETE from user_active_session WHERE
|
});
|
||||||
(created < ?) OR
|
|
||||||
(active = 0 AND last_used < ?) OR
|
|
||||||
(uses = 0)
|
|
||||||
`, [twentyDays, fourtyFiveSeconds])
|
|
||||||
.then((r,f) => {
|
|
||||||
|
|
||||||
//Lookup session data in database
|
|
||||||
db.promise().query('SELECT * FROM user_active_session WHERE id = ? LIMIT 1', [decodedToken.sessionNum])
|
|
||||||
.then((r,f) => {
|
|
||||||
|
|
||||||
if(r == undefined || r[0].length == 0){
|
|
||||||
throw new Error('Active Session not found for token')
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = r[0][0]
|
|
||||||
|
|
||||||
// console.log(decodedToken.sessionNum + ' uses -> ' + row.uses)
|
|
||||||
|
|
||||||
if(row.uses <= 0){
|
|
||||||
throw new Error('Token is used up')
|
|
||||||
}
|
|
||||||
|
|
||||||
//Decrypt master key from lookup
|
|
||||||
const masterKey = cs.decrypt(decodedToken.tempPass, row.salt, row.encrypted_master_password)
|
|
||||||
if(masterKey == null){
|
|
||||||
// console.log('Deleting invalid session')
|
|
||||||
Auth.terminateSession(row.session_id)
|
|
||||||
throw new Error ('Unable to decrypt password for session')
|
|
||||||
}
|
|
||||||
|
|
||||||
//Async update DB counts and disable session if needed
|
|
||||||
db.promise().query('UPDATE user_active_session SET uses = uses -1, last_used = ? WHERE id = ? LIMIT 1', [now, decodedToken.sessionNum])
|
|
||||||
.then((r,f) => {
|
|
||||||
|
|
||||||
let userData = {
|
|
||||||
'userId': decodedToken.userId,
|
|
||||||
'masterKey': masterKey,
|
|
||||||
'sessionId': row.session_id,
|
|
||||||
'created': row.created,
|
|
||||||
'remainingUses':(row.uses--),
|
|
||||||
'active': row.active
|
|
||||||
}
|
|
||||||
|
|
||||||
//Return token Data
|
|
||||||
return resolve(userData)
|
|
||||||
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
//Token errors result in having sessions deleted
|
|
||||||
// console.log('-- Auth Token Error --')
|
|
||||||
// console.log(error)
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Auth.terminateSession = (sessionId) => {
|
|
||||||
|
|
||||||
return db.promise().query('DELETE from user_active_session WHERE session_id = ?', [sessionId])
|
|
||||||
}
|
|
||||||
|
|
||||||
Auth.deletAllLoginKeys = (userId) => {
|
|
||||||
|
|
||||||
const userHash = cs.hash(String(userId)).toString('base64')
|
|
||||||
|
|
||||||
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 = () => {
|
|
||||||
|
|
||||||
const testUserId = 22
|
|
||||||
const testPass = cs.createSmallSalt()
|
|
||||||
Auth.createToken(testUserId, testPass)
|
|
||||||
.then(token => {
|
|
||||||
|
|
||||||
console.log('Test: Create JWT -> Pass')
|
|
||||||
|
|
||||||
return Auth.decodeToken(token)
|
|
||||||
})
|
|
||||||
.then(userData => {
|
|
||||||
|
|
||||||
console.log('Test: Decrypted key Match -> ' + (testPass == userData.masterKey))
|
|
||||||
return Auth.deletAllLoginKeys(testUserId)
|
|
||||||
})
|
|
||||||
.then(results => {
|
|
||||||
|
|
||||||
console.log('Test: Remove user Json Web Tokens - Pass')
|
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ CryptoString.encrypt = (password, salt64, rawText) => {
|
|||||||
//Decrypt base64 string cipher text,
|
//Decrypt base64 string cipher text,
|
||||||
CryptoString.decrypt = (password, salt64, cipherTextString) => {
|
CryptoString.decrypt = (password, salt64, cipherTextString) => {
|
||||||
|
|
||||||
if(!password || !salt64 || !cipherTextString){ return '' }
|
|
||||||
if(password.length == 0 || salt64.length == 0 || cipherTextString == 0){ return '' }
|
|
||||||
|
|
||||||
let cipherText = Buffer.from(cipherTextString, 'base64')
|
let cipherText = Buffer.from(cipherTextString, 'base64')
|
||||||
const salt = Buffer.from(salt64, 'base64')
|
const salt = Buffer.from(salt64, 'base64')
|
||||||
|
|
||||||
@@ -72,8 +69,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')
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ ProcessText.deduceNoteTitle = (inTitle, inString) => {
|
|||||||
//Remove inline styles that may be added by editor
|
//Remove inline styles that may be added by editor
|
||||||
// inString = inString.replace(/style=".*?"/g,'')
|
// inString = inString.replace(/style=".*?"/g,'')
|
||||||
|
|
||||||
// const tagFreeLength = ProcessText.removeHtml(inString).length
|
const tagFreeLength = ProcessText.removeHtml(inString).length
|
||||||
|
|
||||||
//
|
//
|
||||||
// Simplified attempt!
|
// Simplified attempt!
|
||||||
@@ -80,7 +80,7 @@ ProcessText.deduceNoteTitle = (inTitle, inString) => {
|
|||||||
// if(tagFreeLength > 200){
|
// if(tagFreeLength > 200){
|
||||||
// sub += '... <i class="green caret down icon"></i>'
|
// sub += '... <i class="green caret down icon"></i>'
|
||||||
// }
|
// }
|
||||||
// inString += '</end>'
|
inString += '</end>'
|
||||||
|
|
||||||
return {title, sub}
|
return {title, sub}
|
||||||
|
|
||||||
|
|||||||
@@ -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 )
|
|
||||||
}
|
}
|
||||||
|
//Grab the favicon of the site
|
||||||
const half = Math.ceil(imgSrcs.length / 2)
|
|
||||||
imagesWeWant = [...imgSrcs.slice(-half), ...imgSrcs.slice(0,half) ]
|
|
||||||
|
|
||||||
}
|
|
||||||
//Grab the shortcut icon
|
|
||||||
if(favicon && favicon[0] && favicon[0].attribs){
|
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])
|
||||||
}
|
}
|
||||||
|
|||||||
203
server/index.js
203
server/index.js
@@ -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'
|
||||||
@@ -56,54 +50,13 @@ io.on('connection', function(socket){
|
|||||||
socket.on('user_connect', token => {
|
socket.on('user_connect', token => {
|
||||||
Auth.decodeToken(token)
|
Auth.decodeToken(token)
|
||||||
.then(userData => {
|
.then(userData => {
|
||||||
socket.join(userData.userId)
|
socket.join(userData.id)
|
||||||
|
|
||||||
//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
|
|
||||||
socket.on('renew_session_token', token => {
|
|
||||||
|
|
||||||
//Decode the token they currently have
|
|
||||||
Auth.decodeToken(token)
|
|
||||||
.then(userData => {
|
|
||||||
|
|
||||||
if(userData.active == 1){
|
|
||||||
//Create a new one using credentials and session keys from current
|
|
||||||
Auth.createToken(userData.userId, userData.masterKey, userData.sessionId, userData.created)
|
|
||||||
.then(newToken => {
|
|
||||||
|
|
||||||
//Emit new token only to user on socket
|
|
||||||
socket.emit('recievend_new_token', newToken)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
//Attempting to reactivate disabled session, kills it all
|
|
||||||
Auth.terminateSession(userData.sessionId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('join_room', rawTextId => {
|
socket.on('join_room', rawTextId => {
|
||||||
// Join user to rawtextid room when they enter
|
// Join user to rawtextid room when they enter
|
||||||
socket.join(rawTextId)
|
socket.join(rawTextId)
|
||||||
@@ -116,12 +69,31 @@ 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
|
||||||
|
console.log('Note diff object')
|
||||||
|
console.log(noteDiffs)
|
||||||
|
|
||||||
|
|
||||||
|
let noteDiffKeys = Object.keys(noteDiffs)
|
||||||
|
let totalDiffs = 0
|
||||||
|
noteDiffKeys.forEach(diffSetKey => {
|
||||||
|
if(noteDiffs[diffSetKey]){
|
||||||
|
totalDiffs += noteDiffs[diffSetKey].length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Total notes in limbo -> ', noteDiffKeys.length)
|
||||||
|
console.log('Total Diffs for all notes -> ', totalDiffs)
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -147,13 +119,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 +170,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,46 +185,37 @@ 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
|
||||||
app.use(express.json({limit: '5mb'}))
|
app.use(express.json({limit: '5mb'}))
|
||||||
|
|
||||||
|
|
||||||
|
//Prefix defied by route in nginx config
|
||||||
|
const prefix = '/api'
|
||||||
|
|
||||||
//App Auth, all requests will come in with a token, decode the token and set global var
|
//App Auth, all requests will come in with a token, decode the token and set global var
|
||||||
app.use(function(req, res, next){
|
app.use(function(req, res, next){
|
||||||
|
|
||||||
//Always null out master key, never allow it set from outside
|
|
||||||
req.headers.userId = null
|
|
||||||
req.headers.masterKey = null
|
|
||||||
req.headers.sessionId = null
|
|
||||||
|
|
||||||
//auth token set by axios in headers
|
//auth token set by axios in headers
|
||||||
let token = req.headers.authorizationtoken
|
let token = req.headers.authorizationtoken
|
||||||
if(token !== undefined && token.length > 0){
|
if(token && token != null && typeof token === 'string'){
|
||||||
Auth.decodeToken(token, req)
|
Auth.decodeToken(token)
|
||||||
.then(userData => {
|
.then(userData => {
|
||||||
|
req.headers.userId = userData.id //Update headers for the rest of the application
|
||||||
//Update headers for the rest of the application
|
|
||||||
req.headers.userId = userData.userId
|
|
||||||
req.headers.masterKey = userData.masterKey
|
req.headers.masterKey = userData.masterKey
|
||||||
req.headers.sessionId = userData.sessionId
|
|
||||||
|
|
||||||
//Tell front end remaining uses on current token
|
|
||||||
res.set('remainingUses', userData.remainingUses)
|
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
}).catch(error => {
|
||||||
.catch(error => {
|
|
||||||
|
|
||||||
next('Unauthorized')
|
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,69 +224,48 @@ app.use(function(req, res, next){
|
|||||||
|
|
||||||
|
|
||||||
// Test Area
|
// Test Area
|
||||||
// const printResults = true
|
const printResults = false
|
||||||
// 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')
|
UserTest.keyPairTest('genMan12', '1', printResults)
|
||||||
// Auth.test()
|
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
|
||||||
// UserTest.keyPairTest('genMan30', '1', printResults)
|
.then( message => {
|
||||||
// .then( ({testUserId, masterKey}) =>
|
if(printResults) console.log(message)
|
||||||
// NoteTest.test(testUserId, masterKey, printResults))
|
})
|
||||||
// .then( message => {
|
// Test Area
|
||||||
// if(printResults) console.log(message)
|
|
||||||
// Auth.testTwoFactor()
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// console.log(error)
|
|
||||||
// })
|
|
||||||
|
|
||||||
//Test
|
//Test
|
||||||
app.get('/api', (req, res) => res.send('Solidscribe /API is up and running'))
|
app.get(prefix, (req, res) => res.send('The api is running'))
|
||||||
|
|
||||||
//Serve up uploaded files
|
//Serve up uploaded files
|
||||||
app.use('/api/static', express.static( __dirname+'/../staticFiles' ))
|
app.use(prefix+'/static', express.static( __dirname+'/../staticFiles' ))
|
||||||
|
|
||||||
//Public routes
|
//Public routes
|
||||||
var public = require('@routes/publicController')
|
var public = require('@routes/publicController')
|
||||||
app.use('/api/public', public)
|
app.use(prefix+'/public', public)
|
||||||
|
|
||||||
//user endpoint
|
//user endpoint
|
||||||
var user = require('@routes/userController')
|
var user = require('@routes/userController')
|
||||||
app.use('/api/user', user)
|
app.use(prefix+'/user', user)
|
||||||
|
|
||||||
//notes endpoint
|
//notes endpoint
|
||||||
var notes = require('@routes/noteController')
|
var notes = require('@routes/noteController')
|
||||||
app.use('/api/note', notes)
|
app.use(prefix+'/note', notes)
|
||||||
|
|
||||||
//tags endpoint
|
//tags endpoint
|
||||||
var tags = require('@routes/tagController')
|
var tags = require('@routes/tagController')
|
||||||
app.use('/api/tag', tags)
|
app.use(prefix+'/tag', tags)
|
||||||
|
|
||||||
//notes endpoint
|
//notes endpoint
|
||||||
var attachment = require('@routes/attachmentController')
|
var attachment = require('@routes/attachmentController')
|
||||||
app.use('/api/attachment', attachment)
|
app.use(prefix+'/attachment', attachment)
|
||||||
|
|
||||||
//quick notes endpoint
|
//quick notes endpoint
|
||||||
var quickNote = require('@routes/quicknoteController')
|
var quickNote = require('@routes/quicknoteController')
|
||||||
app.use('/api/quick-note', quickNote)
|
app.use(prefix+'/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()
|
|
||||||
})
|
})
|
||||||
@@ -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,28 +45,16 @@ 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'){
|
if(attachmentType == 'links'){
|
||||||
query += 'AND attachment_type = 1 '
|
query += 'AND attachment_type = 1 '
|
||||||
@@ -77,30 +63,13 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeSha
|
|||||||
query += 'AND attachment_type > 1 '
|
query += 'AND attachment_type > 1 '
|
||||||
}
|
}
|
||||||
|
|
||||||
query += `AND note.archived = ${ attachmentType == 'archived' ? '1':'0' } `
|
|
||||||
query += `AND note.trashed = ${ attachmentType == 'trashed' ? '1':'0' } `
|
|
||||||
|
|
||||||
if(!attachmentType){
|
|
||||||
// Null note ID means it was pushed by bookmarklet
|
|
||||||
query += 'OR attachment.note_id IS NULL '
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if(!noteId){
|
|
||||||
const sharedOrNot = includeShared ? ' NOT ':' '
|
|
||||||
query += `AND note.share_user_id IS${sharedOrNot}NULL `
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
query += 'ORDER BY last_indexed DESC '
|
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()
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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,16 +449,14 @@ 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 = ''
|
||||||
//Create encrypted snippet if its a long note
|
//Create encrypted snippet
|
||||||
let snippet = ''
|
const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
|
||||||
if(noteText.length > 500){
|
|
||||||
snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
|
|
||||||
noteSnippet = cs.encrypt(masterKey, snippetSalt, snippet)
|
noteSnippet = cs.encrypt(masterKey, snippetSalt, snippet)
|
||||||
}
|
|
||||||
|
|
||||||
//Encrypt note text
|
//Encrypt note text
|
||||||
const textObject = JSON.stringify([noteTitle, noteText])
|
const textObject = JSON.stringify([noteTitle, noteText])
|
||||||
@@ -484,14 +472,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,23 +486,20 @@ 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
|
//Async attachment reindex
|
||||||
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
|
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
|
||||||
}
|
|
||||||
|
|
||||||
//Send back updated response
|
//Send back updated response
|
||||||
resolve(rows[0])
|
resolve(rows[0])
|
||||||
@@ -531,7 +512,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 +529,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 +546,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 +649,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 +726,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 +742,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 +773,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 +859,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 = []
|
||||||
|
|
||||||
let partialWords = [] //For debugging
|
|
||||||
let exactWords = [] //For debugging
|
|
||||||
|
|
||||||
|
|
||||||
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 => {
|
Object.keys(searchIndex).forEach(wordIndex => {
|
||||||
if( wordIndex.indexOf(word) != -1 && wordIndex != word){
|
if( wordIndex.indexOf(word) != -1 && wordIndex != word){
|
||||||
// partialWords.push(wordIndex) //partialWords for debugging
|
partials.push(wordIndex)
|
||||||
partialMatchNoteIds.push(...searchIndex[wordIndex])
|
noteIds.push(...searchIndex[wordIndex])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
const exactArray = searchIndex[word] ? searchIndex[word] : []
|
||||||
|
|
||||||
//If more than one work was searched, remove notes that don't contain both
|
let searchData = {
|
||||||
if(words.length > 1 && exactWordIdSets.length > 0){
|
'word':word,
|
||||||
|
'exact': exactArray,
|
||||||
//Find ids that appear more than once, this means there was an exact match in more than one note
|
'partials': partials,
|
||||||
let overlappingIds = exactWordIdSets.filter((e, i, a) => a.indexOf(e) !== i)
|
'partial': [...new Set(noteIds) ],
|
||||||
overlappingIds = [...new Set(overlappingIds)]
|
|
||||||
|
|
||||||
//If there are notes that appear
|
|
||||||
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 {
|
||||||
@@ -977,10 +946,8 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
let searchParams = [userId]
|
let searchParams = [userId]
|
||||||
let noteSearchQuery = `
|
let noteSearchQuery = `
|
||||||
SELECT note.id,
|
SELECT note.id,
|
||||||
note.snippet as snippetText,
|
note.snippet as snippet,
|
||||||
note.snippet_salt as snippetSalt,
|
note.snippet_salt as salt,
|
||||||
note_raw_text.text as noteText,
|
|
||||||
note_raw_text.salt as noteSalt,
|
|
||||||
note_raw_text.updated as updated,
|
note_raw_text.updated as updated,
|
||||||
opened,
|
opened,
|
||||||
color,
|
color,
|
||||||
@@ -989,12 +956,11 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
note.pinned,
|
note.pinned,
|
||||||
note.archived,
|
note.archived,
|
||||||
note.trashed,
|
note.trashed,
|
||||||
GROUP_CONCAT(DISTINCT tag.text,":",tag.id) as tags,
|
GROUP_CONCAT(DISTINCT tag.text) as tags,
|
||||||
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 +968,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 +1043,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){
|
||||||
@@ -1127,39 +1092,26 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//Only long notes have snippets, decipher it if present
|
//Decrypt note text
|
||||||
let displayTitle = ''
|
if(note.snippet && note.salt){
|
||||||
let displayText = ''
|
const decipheredText = cs.decrypt(currentNoteKey, note.salt, note.snippet)
|
||||||
|
|
||||||
let encryptedText = note.noteText
|
|
||||||
let relatedSalt = note.noteSalt
|
|
||||||
|
|
||||||
//Default to note text, use snippet if set
|
|
||||||
if(note.snippetSalt && note.snippetText && note.snippetSalt.length > 0 && note.snippetText.length > 0){
|
|
||||||
encryptedText = note.snippetText
|
|
||||||
relatedSalt = note.snippetSalt
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const decipheredText = cs.decrypt(currentNoteKey, relatedSalt, encryptedText)
|
|
||||||
const textObject = JSON.parse(decipheredText)
|
const textObject = JSON.parse(decipheredText)
|
||||||
if(textObject != null && textObject.length == 2){
|
if(textObject != null && textObject.length == 2){
|
||||||
if(textObject[0] && textObject[0] != null && textObject[0].length > 0){
|
note.title = textObject[0]
|
||||||
displayTitle = textObject[0]
|
note.text = textObject[1]
|
||||||
}
|
}
|
||||||
if(textObject[1] && textObject[1] != null && textObject[1].length > 0){
|
|
||||||
displayText = textObject[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(err) {
|
|
||||||
console.log('Error opening note id -> '+note.id+' for userId -> '+userId)
|
|
||||||
console.log(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Deduce note title
|
||||||
|
const textData = ProcessText.deduceNoteTitle(note.title, note.text)
|
||||||
|
|
||||||
|
note.title = textData.title
|
||||||
|
note.subtext = textData.sub
|
||||||
|
|
||||||
note.title = displayTitle
|
//Remove these variables
|
||||||
note.subtext = ProcessText.stripDoubleBlankLines(displayText)
|
note.note_highlights = []
|
||||||
|
note.attachment_highlights = []
|
||||||
|
note.tag_highlights = []
|
||||||
|
|
||||||
//Limit number of attachment thumbs to 4
|
//Limit number of attachment thumbs to 4
|
||||||
if(note.thumbs){
|
if(note.thumbs){
|
||||||
@@ -1171,12 +1123,9 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Clear out note.text before sending it to front end, its being used in title and subtext
|
//Clear out note.text before sending it to front end, its being used in title and subtext
|
||||||
delete note.snippetText
|
delete note.snippet
|
||||||
delete note.snippetSalt
|
delete note.salt
|
||||||
delete note.noteText
|
|
||||||
delete note.noteSalt
|
|
||||||
delete note.encrypted_share_password_key
|
delete note.encrypted_share_password_key
|
||||||
delete note.text //Passed back as title and subtext
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 = ' ' }
|
if(clean == ''){ clean = ' ' }
|
||||||
@@ -117,7 +105,7 @@ QuickNote.update = (userId, pushText, masterKey) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then( saveResults => {
|
.then( saveResults => {
|
||||||
return resolve(saveResults)
|
return resolve(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -5,33 +5,20 @@ 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'
|
|
||||||
// 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,34 +26,8 @@ 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
|
|
||||||
const tokenValidates = speakeasy.totp.verify({
|
|
||||||
'secret': lookedUpUser['two_fa_secret'],
|
|
||||||
'encoding': 'base32',
|
|
||||||
'token': authToken,
|
|
||||||
'window': 2
|
|
||||||
})
|
|
||||||
|
|
||||||
if(lookedUpUser.two_fa_enabled == 1 && !authToken){
|
|
||||||
|
|
||||||
statusObject['verificationRequired'] = true
|
|
||||||
statusObject['message'] = '2FA authentication required.'
|
|
||||||
|
|
||||||
return resolve(statusObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(lookedUpUser.two_fa_enabled == 1 && !tokenValidates){
|
|
||||||
|
|
||||||
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
|
//hash the password and check for a match
|
||||||
|
// const salt = new Buffer(lookedUpUser.salt, 'binary')
|
||||||
const salt = Buffer.from(lookedUpUser.salt, 'binary')
|
const salt = Buffer.from(lookedUpUser.salt, 'binary')
|
||||||
crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
|
crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
|
||||||
if(delivered_key.toString('hex') === lookedUpUser.password){
|
if(delivered_key.toString('hex') === lookedUpUser.password){
|
||||||
@@ -79,36 +40,19 @@ User.login = (username, password, authToken = null) => {
|
|||||||
.then(({publicKey, privateKey}) => {
|
.then(({publicKey, privateKey}) => {
|
||||||
|
|
||||||
//Passback a json web token
|
//Passback a json web token
|
||||||
Auth.createToken(lookedUpUser.id, masterKey)
|
const token = Auth.createToken(lookedUpUser.id, masterKey)
|
||||||
.then(token => {
|
resolve({ token: token, userId:lookedUpUser.id })
|
||||||
|
|
||||||
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)
|
||||||
@@ -175,10 +119,7 @@ User.register = (username, password) => {
|
|||||||
})
|
})
|
||||||
.then(({publicKey, privateKey}) => {
|
.then(({publicKey, privateKey}) => {
|
||||||
|
|
||||||
return Auth.createToken(userId, newMasterKey)
|
const token = Auth.createToken(userId, newMasterKey)
|
||||||
})
|
|
||||||
.then(token => {
|
|
||||||
|
|
||||||
return resolve({token, userId})
|
return resolve({token, userId})
|
||||||
})
|
})
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
@@ -194,19 +135,16 @@ 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')
|
|
||||||
|
|
||||||
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,86 +179,16 @@ 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
|
|
||||||
|
|
||||||
// Allow for extended options set on page load
|
|
||||||
if(extendedOptions){
|
|
||||||
|
|
||||||
db.promise().query(
|
|
||||||
`SELECT updated FROM note
|
|
||||||
JOIN note_raw_text ON note_raw_text.id = note.note_raw_text_id
|
|
||||||
WHERE note.quick_note = 2
|
|
||||||
AND user_id = ?`, [userId])
|
|
||||||
.then( (rows, fields) => {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(rows[0][0] && rows[0][0].updated){
|
|
||||||
const lastOpened = rows[0][0].updated
|
|
||||||
const timeDiff = Math.round(((+new Date) - (lastOpened))/1000)
|
|
||||||
const hoursInSeconds = (12 * 60 * 60) //12 hours
|
|
||||||
|
|
||||||
// Show metric tracking button if its been 12 hours since last entry
|
|
||||||
if(lastOpened && timeDiff > hoursInSeconds){
|
|
||||||
countTotals['showTrackMetricsButton'] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
resolve(countTotals)
|
resolve(countTotals)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
} else {
|
|
||||||
resolve(countTotals)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//Log out user by deleting login token for that active session
|
|
||||||
User.logout = (sessionId) => {
|
|
||||||
console.log('Terminate Session -> ', sessionId)
|
|
||||||
return db.promise().query('DELETE FROM user_active_session WHERE (session_id = ?)', [sessionId])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
User.generateMasterKey = (userId, password) => {
|
User.generateMasterKey = (userId, password) => {
|
||||||
@@ -370,12 +238,13 @@ 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 +360,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 +374,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 +387,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})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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 => {
|
||||||
@@ -133,4 +136,19 @@ router.post('/disableshare', function (req, res) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Testing Action
|
||||||
|
//
|
||||||
|
//Reindex all Note. Not a very good function, not public
|
||||||
|
router.get('/reindex5yu43prchuj903mrc', function (req, res) {
|
||||||
|
|
||||||
|
Note.migrateNoteTextToNewTable().then(status => {
|
||||||
|
return res.send(status)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,86 +1,54 @@
|
|||||||
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
|
|
||||||
if(!req.headers.sessionId){
|
|
||||||
next('Unauthorized')
|
|
||||||
}
|
|
||||||
|
|
||||||
if(req.headers.userId){
|
|
||||||
userId = req.headers.userId
|
|
||||||
masterKey = req.headers.masterKey
|
|
||||||
next()
|
next()
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
// define the home page route
|
||||||
// Logout User
|
router.get('/', function (req, res) {
|
||||||
router.post('/logout', function (req, res) {
|
res.send('User Home Page ' + User.getUsername())
|
||||||
|
|
||||||
User.logout(req.headers.sessionId)
|
|
||||||
.then( returnData => {
|
|
||||||
res.send(true)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
// 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) {
|
||||||
|
|
||||||
// change password
|
User.login(req.body.username, req.body.password)
|
||||||
router.post('/changepassword', function (req, res) {
|
|
||||||
|
|
||||||
User.changePassword(req.headers.userId, req.body.currentPass, req.body.newPass)
|
|
||||||
.then( returnData => {
|
.then( returnData => {
|
||||||
|
|
||||||
res.send(returnData)
|
res.send(returnData)
|
||||||
})
|
})
|
||||||
|
.catch(e => {
|
||||||
|
res.send(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
// Login User
|
||||||
|
router.post('/register', function (req, res) {
|
||||||
|
|
||||||
//Revoke all active session keys for user
|
User.register(req.body.username, req.body.password)
|
||||||
router.post('/revokesessions', function(req, res) {
|
|
||||||
|
|
||||||
User.revokeActiveSessions(req.headers.userId, req.headers.sessionId)
|
|
||||||
.then( returnData => {
|
.then( returnData => {
|
||||||
|
|
||||||
res.send(returnData)
|
res.send(returnData)
|
||||||
})
|
})
|
||||||
|
.catch(e => {
|
||||||
|
res.send(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// fetch counts of users notes
|
// fetch counts of users notes
|
||||||
router.post('/totals', function (req, res) {
|
router.post('/totals', function (req, res) {
|
||||||
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
|
||||||
@@ -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()
|
|
||||||
})
|
|
||||||
@@ -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()
|
|
||||||
})
|
|
||||||
@@ -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()
|
|
||||||
})
|
|
||||||
@@ -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()
|
|
||||||
})
|
|
||||||
@@ -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
4
staticFiles/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
*
|
||||||
|
*/
|
||||||
|
!.gitignore
|
||||||
|
!assets
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -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 |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user