Compare commits
36 Commits
dev
...
5975ab6d68
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
@@ -6,10 +6,4 @@ pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
.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
|
||||
|
||||
# Take all variables in .env and turn them into local variables for this script
|
||||
source ~/.env
|
||||
|
||||
BACKUPDIR="/home/mab/databaseBackupSolidScribe"
|
||||
BACKUPDIR="/home/mab/databaseBackupPi"
|
||||
|
||||
mkdir -p $BACKUPDIR
|
||||
cd $BACKUPDIR
|
||||
|
||||
NOW=$(date +"%Y-%m-%d_%H-%M")
|
||||
ssh mab@solidscribe.com -p 13328 "mysqldump --all-databases --single-transaction --user root -p$PROD_DB_PASS" > "backup-$NOW.sql"
|
||||
gzip "backup-$NOW.sql"
|
||||
ssh mab@solidscribe.com -p 13328 "mysqldump --all-databases --user root -pRootPass1234!" > "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"
|
||||
|
||||
# Delete all but last 8 files
|
||||
ls -tp | grep -v '/$' | tail -n +9 | tr '\n' '\0' | xargs -0 rm --
|
||||
|
||||
##
|
||||
# Restore DB
|
||||
##
|
||||
|
||||
#Restore DB
|
||||
# copy file over, run restore
|
||||
# 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
|
||||
|
||||
##
|
||||
# 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
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.pem
|
||||
*.crt
|
||||
*.key
|
||||
|
||||
# Log files
|
||||
node_modules/
|
||||
/dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
@@ -23,4 +12,3 @@ pnpm-debug.log*
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.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
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
# serve with hot reload at localhost:8080
|
||||
npm run dev
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
# build for production with minification
|
||||
npm run build
|
||||
|
||||
# build for production and view the bundle analyzer report
|
||||
npm run build --report
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
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).
|
||||
|
||||
@@ -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"'
|
||||
}
|
||||
19
client/index.html
Normal file
19
client/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
|
||||
<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"/>
|
||||
|
||||
<meta name="theme-color" content="#000" />
|
||||
<link rel="manifest" href="/api/static/assets/manifest.json">
|
||||
|
||||
<title>Notes</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
25392
client/package-lock.json
generated
25392
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,32 +1,66 @@
|
||||
{
|
||||
"name": "solidscribe",
|
||||
"version": "0.1.0",
|
||||
"name": "client2",
|
||||
"version": "1.0.0",
|
||||
"description": "client2",
|
||||
"author": "max",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build"
|
||||
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
|
||||
"start": "npm run dev",
|
||||
"build": "node build/build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.1.3",
|
||||
"core-js": "^3.6.5",
|
||||
"axios": "^0.19.2",
|
||||
"es6-promise": "^4.2.8",
|
||||
"fomantic-ui-css": "^2.9.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue-chartjs": "^5.0.1",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.4.0"
|
||||
"fomantic-ui-css": "^2.8.4",
|
||||
"vue": "^2.5.2",
|
||||
"vue-router": "^3.0.1",
|
||||
"vuex": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^5.0.8",
|
||||
"@vue/cli-plugin-router": "^5.0.8",
|
||||
"@vue/cli-plugin-vuex": "^5.0.8",
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
"autoprefixer": "^7.1.2",
|
||||
"babel-core": "^6.22.1",
|
||||
"babel-helper-vue-jsx-merge-props": "^2.0.3",
|
||||
"babel-loader": "^7.1.1",
|
||||
"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": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
"not ie <= 8"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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="shortcut icon" href="/api/static/assets/favicon.ico" type="image/x-icon"/>
|
||||
|
||||
<meta name="theme-color" content="#000" />
|
||||
<link rel="manifest" href="/api/static/assets/manifest.json">
|
||||
|
||||
<title>Solid Scribe - An easy, encrypted Note App</title>
|
||||
<!-- <title><%= htmlWebpackPlugin.options.title %></title> -->
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but Solid Scribe doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<!-- placeholder data for scrapers with no JS -->
|
||||
<style>
|
||||
body {
|
||||
background-color: #212221;
|
||||
color: #aeaeae;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
.centered {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
.logo {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
}
|
||||
.scrape-info {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="centered">
|
||||
<img class="logo" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo - if you can read this your connection is really slow">
|
||||
<h1>Solid Scribe</h1>
|
||||
<h3>An easy, encrypted Note App</h3>
|
||||
<h4>Loading...</h4>
|
||||
</div>
|
||||
|
||||
<div class="scrape-info">
|
||||
<h1>Solid Scribe</h1>
|
||||
<h2>A note application that respects your privacy.</h2>
|
||||
<p>Take notes with a clean editor that works on desktop or mobile.</p>
|
||||
<p>Search notes, links and files to find what you need.</p>
|
||||
<p>Accessable everywhere.</p>
|
||||
<p>Categorize notes with tags.</p>
|
||||
<p>Share data with fellow users.</p>
|
||||
<p>Encrypt notes for additional security.</p>
|
||||
<b>This site requires Javascipt to run.</b>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -1,53 +1,9 @@
|
||||
<template>
|
||||
<div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode == 2) }">
|
||||
<div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode) }">
|
||||
|
||||
<div class="ui container" v-if="showFakeSite">
|
||||
<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>
|
||||
<global-site-menu />
|
||||
|
||||
<div class="auth-block" v-if="requireAuth">
|
||||
<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" />
|
||||
<router-view />
|
||||
|
||||
<global-notification />
|
||||
|
||||
@@ -60,151 +16,52 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
'global-site-menu': require('@/components/GlobalSiteMenu.vue').default,
|
||||
'global-notification':require('@/components/GlobalNotificationComponent.vue').default,
|
||||
'global-notification':require('@/components/GlobalNotificationComponent.vue').default
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
showFakeSite:false, //Incorrect domain detection
|
||||
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: '',
|
||||
// loggedIn:
|
||||
}
|
||||
},
|
||||
|
||||
//Axios response interceptor
|
||||
// - Gets new session tokens from server and uses them in app
|
||||
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
|
||||
let token = localStorage.getItem('loginToken')
|
||||
let username = localStorage.getItem('username')
|
||||
|
||||
//
|
||||
if(token && token.length > 0){
|
||||
// const socket = io({ path:'/socket' });
|
||||
const socket = this.$io
|
||||
socket.on('connect', () => {
|
||||
|
||||
//setup username display
|
||||
this.$store.commit('setUsername', username)
|
||||
this.$store.commit('setSocketIoSocket', socket.id)
|
||||
|
||||
//Set session token on every request if set
|
||||
axios.defaults.headers.common['authorizationtoken'] = token
|
||||
|
||||
//Setup websockets into vue instance
|
||||
const socket = this.$io
|
||||
socket.on('connect', () => {
|
||||
|
||||
//Put user into personal event room for live note updates, etc
|
||||
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
|
||||
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
|
||||
if(localStorage.getItem('nightMode') == 'true'){
|
||||
this.$store.commit('toggleNightMode')
|
||||
}
|
||||
|
||||
//Set color theme based on local storage
|
||||
const themeNumber = localStorage.getItem('nightMode')
|
||||
if(themeNumber != null){
|
||||
this.$store.commit('toggleNightMode', themeNumber)
|
||||
//Put user data into global store on load
|
||||
if(token){
|
||||
this.$store.commit('setLoginToken', {token, username})
|
||||
}
|
||||
|
||||
},
|
||||
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
|
||||
this.$io.on('update_counts', () => {
|
||||
console.log('Got event, update totals')
|
||||
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: {
|
||||
loggedIn () {
|
||||
@@ -213,23 +70,16 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
destroyLoginToken() {
|
||||
this.$store.commit('destroyLoginToken')
|
||||
},
|
||||
loginGateway() {
|
||||
if(!this.loggedIn){
|
||||
console.log('This user is not logged in')
|
||||
this.$router.push({'path':'/login'})
|
||||
return
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
|
||||
this.$router.push('/')
|
||||
axios.post('/api/user/logout')
|
||||
|
||||
setTimeout(() => {
|
||||
this.$store.commit('destroyLoginToken')
|
||||
this.$bus.$emit('notification', 'Logged Out')
|
||||
}, 200)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -3,15 +3,6 @@ import Vue from 'vue'
|
||||
const helpers = {}
|
||||
|
||||
helpers.timeAgo = (time) => {
|
||||
|
||||
if(time == null){
|
||||
time = Math.round(time/1000)
|
||||
}
|
||||
|
||||
if(time.toString().length >= 13){
|
||||
time = Math.round(time/1000)
|
||||
}
|
||||
|
||||
const time_formats = [
|
||||
[ 60, 'seconds', 1 ],
|
||||
[ 120, '1 minute ago', '1 minute from now' ],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
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;
|
||||
}
|
||||
/* latin */
|
||||
@@ -11,207 +11,121 @@
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
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;
|
||||
}
|
||||
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 {
|
||||
|
||||
/*main accent for all buttons, icons and logos*/
|
||||
--main-accent: #21BA45;
|
||||
|
||||
/*theme colors */
|
||||
--body_bg_color: #f5f6f7;
|
||||
--small_element_bg_color: #fff;
|
||||
--background_color: #fff;
|
||||
--text_color: #3d3d3d;
|
||||
--dark_border_color: #DFE1E6;
|
||||
--border_color: #DFE1E6;
|
||||
--outline_color: rgba(34,36,38,.15);
|
||||
--border_color: rgba(34,36,38,.20);
|
||||
|
||||
/* Global purple menu styles */
|
||||
/*Global purple menu styles */
|
||||
--menu-border: #534c68;
|
||||
--menu-background: #221f2b;
|
||||
|
||||
/* edit menu styles, text, accent */
|
||||
--menu-text: #5e6268;
|
||||
--menu-accent: #cecece;
|
||||
}
|
||||
|
||||
html {
|
||||
/*scrollbar-width: none;*/
|
||||
width: 100%;
|
||||
height:100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: var(--body_bg_color);
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
div.ui.basic.segment.no-fluf-segment {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
/*width: 100%;*/
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
div.ui.basic.segment.no-fluf-segment {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
/* Night mode modifiers */
|
||||
|
||||
/*Make images sepia in night mode */
|
||||
.night-mode img {
|
||||
filter: grayscale(50%) brightness(80%) sepia(80%);
|
||||
}
|
||||
.night-mode i.green {
|
||||
color: #877A61 !important;
|
||||
}
|
||||
.night-mode .green.label {
|
||||
background-color: #877A61 !important;
|
||||
border-color: #877A61 !important;
|
||||
}
|
||||
.night-mode .green.button {
|
||||
background-color: #534428 !important;
|
||||
}
|
||||
/*Make images sepia in night mode */
|
||||
.night-mode img {
|
||||
filter: grayscale(50%) brightness(80%) sepia(80%);
|
||||
|
||||
}
|
||||
|
||||
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
|
||||
body {
|
||||
color: var(--text_color);
|
||||
background: none;
|
||||
background-color: var(--background_color);
|
||||
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
}
|
||||
#app {
|
||||
/* background: var(--body_bg_color);*/
|
||||
}
|
||||
|
||||
.ui.segment {
|
||||
color: var(--text_color);
|
||||
background-color: var(--small_element_bg_color);
|
||||
border-color: var(--dark_border_color);
|
||||
background-color: var(--background_color);
|
||||
border-color: var(--border_color);
|
||||
}
|
||||
.button-sub {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 0.9em;
|
||||
color: grey;
|
||||
opacity: 0.9;
|
||||
padding: 4px 0 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ui.form input:not([type]),
|
||||
.ui.form input:not([type]):focus,
|
||||
.ui.form textarea:not([type]),
|
||||
.ui.form textarea:not([type]):focus {
|
||||
color: var(--text_color);
|
||||
background-color: var(--small_element_bg_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;
|
||||
background-color: var(--background_color);
|
||||
border-color: var(--border_color);
|
||||
}
|
||||
.ui.basic.label, .ui.header, .ui.header div.sub.header {
|
||||
color: var(--text_color);
|
||||
background-color: transparent;
|
||||
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);
|
||||
background-color: var(--background_color);
|
||||
border-color: var(--border_color);
|
||||
}
|
||||
.ui.icon.input > i.icon {
|
||||
color: var(--text_color);
|
||||
}
|
||||
div.ui.basic.green.label {
|
||||
background-color: var(--small_element_bg_color) !important;
|
||||
background-color: var(--background_color) !important;
|
||||
}
|
||||
.ui.basic.button, .ui.basic.buttons .button {
|
||||
background-color: var(--small_element_bg_color);
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
border: 1px solid;
|
||||
border-color: var(--dark_border_color) !important;
|
||||
border-color: var(--border_color) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
.ui.basic.button:focus, .ui.basic.button:hover {
|
||||
background-color: var(--small_element_bg_color) !important;
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
.ui.tabular.menu .item {
|
||||
background-color: var(--small_element_bg_color) !important;
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
}
|
||||
.ui.tabular.menu .item.active {
|
||||
background-color: var(--small_element_bg_color) !important;
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
border-color: var(--dark_border_color) !important;
|
||||
border-color: var(--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*/
|
||||
|
||||
/* Styles for public display pages */
|
||||
.fun {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
color: var(--text_color);
|
||||
}
|
||||
.fun h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
.fun h2 {
|
||||
font-size: 1.9em;
|
||||
}
|
||||
.fun h3 {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
.fun p {
|
||||
/*font-size: 1.5em;*/
|
||||
}
|
||||
.fun blockquote {
|
||||
border-left: 5px solid cornflowerblue;
|
||||
padding-left: 25px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
/* Styles for public display pages */
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*//
|
||||
// Purple Global Menu
|
||||
//*/
|
||||
@@ -259,11 +173,6 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
|
||||
/* Shrink button text for mobile */
|
||||
@media only screen and (max-width: 740px) {
|
||||
.note-menu > .nm-button {
|
||||
font-size: 1.4em;
|
||||
padding: 10px 1px;
|
||||
min-width: 40px;
|
||||
}
|
||||
.note-menu .nm-button span {
|
||||
font-size: 0.7em;
|
||||
line-height: 0.4em;
|
||||
@@ -300,46 +209,25 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
z-index: 100;
|
||||
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-box {
|
||||
border: none;
|
||||
/*height: calc(100% - 69px);*/
|
||||
|
||||
min-height: 300px;
|
||||
background-color: var(--small_element_bg_color);
|
||||
min-height: calc(100% - 0px);
|
||||
background-color: rgba(255,200,0,0.0);
|
||||
/*margin-bottom: 15px;*/
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: 10px 15px 10px;
|
||||
/*background: transparent;*/
|
||||
overflow: hidden;
|
||||
overflow-x: scroll;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.8em;
|
||||
line-height: 1.5em;
|
||||
word-wrap: break-word;
|
||||
/*border-bottom: 1px solid #ccc;*/
|
||||
scrollbar-width: none;
|
||||
scrollbar-color: transparent transparent;
|
||||
caret-color: var(--main-accent);
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 1100px;
|
||||
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
||||
|
||||
}
|
||||
.squire-box::selection,
|
||||
.squire-box::-moz-selection {
|
||||
background: #cce2ffa6;
|
||||
color: inherit;
|
||||
}
|
||||
/*Makes the first line real big */
|
||||
.squire-box:focus {
|
||||
@@ -351,34 +239,28 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
.squire-box a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.note-card-text i:not(.icon),
|
||||
.squire-box i {
|
||||
padding: 0.5em 0.99em;
|
||||
border-radius: 1px;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
background-color: rgba(113, 113, 113, 0.1);
|
||||
}
|
||||
.night-mode .note-card-text i:not(.icon),
|
||||
.night-mode .squire-box i:not(.icon) {
|
||||
.night-mode .squire-box i {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.note-card-text code,
|
||||
.squire-box code,
|
||||
.note-card-text pre,
|
||||
.squire-box pre {
|
||||
/*word-wrap: break-word;*/
|
||||
display: inline-block;
|
||||
border-left: 2px solid var(--main-accent);
|
||||
padding-left: 15px;
|
||||
}
|
||||
.note-card-text p,
|
||||
.squire-box p {
|
||||
margin-bottom: 0;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.note-card-text blockquote,
|
||||
.squire-box blockquote {
|
||||
margin: 0;
|
||||
padding: 0 0 0 2.5em;
|
||||
}
|
||||
.note-card-text u,
|
||||
.squire-box u {
|
||||
text-decoration-color: var(--main-accent);
|
||||
}
|
||||
.note-card-text img {
|
||||
max-width:100%;
|
||||
height: auto;
|
||||
@@ -394,40 +276,6 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
.squire-box li > p {
|
||||
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,
|
||||
.squire-box ul > li {
|
||||
@@ -437,129 +285,41 @@ padding-right: 10px;
|
||||
.note-card-text ul > li:before,
|
||||
.squire-box ul > li:before {
|
||||
|
||||
/*filled circle */
|
||||
content: "\f111";
|
||||
/*empty square*/
|
||||
/*content: "\F0C8";*/
|
||||
|
||||
font-family: 'Icons';
|
||||
/*font-family: 'outline-icons';*/
|
||||
backface-visibility: hidden;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-decoration: inherit;
|
||||
text-align: center;
|
||||
font-size: 0.25em;
|
||||
line-height: 1.4em;
|
||||
font-size: 0.75em;
|
||||
|
||||
height: 100%;
|
||||
width: 20px;
|
||||
height: 17px;
|
||||
width: 17px;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
|
||||
left: -25px;
|
||||
left: -30px;
|
||||
/*border: 2px solid #444;*/
|
||||
/*border-radius: 4px;*/
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
top: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
|
||||
color: var(--text_color);
|
||||
text-align: center;
|
||||
}
|
||||
/* filled in check circle */
|
||||
ul > li.active:before {
|
||||
|
||||
font-family: 'Icons';
|
||||
content: "\F14A";
|
||||
color: var(--main-accent);
|
||||
content: "\f058";
|
||||
color: #21BA45;
|
||||
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 */
|
||||
@media only screen and (max-width: 740px) {
|
||||
|
||||
.ui.button.shrinking {
|
||||
font-size: 0.85714286rem;
|
||||
margin: 0 3px;
|
||||
padding: 10px 7px !important;
|
||||
|
||||
}
|
||||
|
||||
.note-card-text ul > li,
|
||||
@@ -567,40 +327,30 @@ padding-right: 10px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
/*empty check box*/
|
||||
.note-card-text ul > li:before,
|
||||
.squire-box ul > li:before,
|
||||
.squire-box ul > li:hover:not(.active):before {
|
||||
|
||||
/*empty checkmark*/
|
||||
/*font-family: 'Icons';*/
|
||||
/*content: "\f058";*/
|
||||
.squire-box ul > li:before {
|
||||
|
||||
content: "\F0C8";
|
||||
font-family: 'outline-icons';
|
||||
content: "\f111";
|
||||
font-family: outline-icons;
|
||||
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
left: -40px;
|
||||
bottom: 0;
|
||||
top: 0px;
|
||||
cursor: pointer;
|
||||
|
||||
line-height: 0.9em;
|
||||
font-size: 1.4em;
|
||||
opacity: 0.2;
|
||||
}
|
||||
/*Filled check box */
|
||||
ul > li.active:before {
|
||||
|
||||
font-family: 'Icons';
|
||||
content: "\F14A";
|
||||
|
||||
color: var(--main-accent);
|
||||
content: "\f058";
|
||||
color: #21BA45;
|
||||
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 +380,6 @@ padding-right: 10px;
|
||||
.ui.white.button {
|
||||
background: #FFF;
|
||||
}
|
||||
.white.row {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.input-floating-button {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
@@ -645,27 +391,6 @@ padding-right: 10px;
|
||||
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
|
||||
@@ -680,324 +405,4 @@ padding-right: 10px;
|
||||
transform: translateZ(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*Fomantic Tooltips*/
|
||||
/* Content */
|
||||
[data-tooltip] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Arrow */
|
||||
[data-tooltip]:before {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
content: '';
|
||||
font-size: 1rem;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #1B1C1D;
|
||||
-webkit-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
z-index: 1901;
|
||||
}
|
||||
|
||||
/* Popup */
|
||||
[data-tooltip]:after {
|
||||
min-width: 40px;
|
||||
pointer-events: none;
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
text-transform: none;
|
||||
text-align: center;
|
||||
white-space: pre;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #D4D4D5;
|
||||
line-height: 1.4285em;
|
||||
max-width: none;
|
||||
background: #1B1C1D;
|
||||
padding: 0.5em;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
/*color: var(--main-accent);*/
|
||||
color: white;
|
||||
border-radius: 0.28571429rem;
|
||||
z-index: 1900;
|
||||
}
|
||||
|
||||
/* Default Position (Top Center) */
|
||||
[data-tooltip]:not([data-position]):before {
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
background: #1B1C1D;
|
||||
margin-left: -0.07142857rem;
|
||||
margin-bottom: 0.14285714rem;
|
||||
}
|
||||
[data-tooltip]:not([data-position]):after {
|
||||
left: 50%;
|
||||
-webkit-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
bottom: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/* Animation */
|
||||
[data-tooltip]:before,
|
||||
[data-tooltip]:after {
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
/*transition: opacity 0.2s ease;*/
|
||||
}
|
||||
[data-tooltip]:before {
|
||||
-webkit-transform: rotate(45deg) scale(0) !important;
|
||||
transform: rotate(45deg) scale(0) !important;
|
||||
-webkit-transform-origin: center top;
|
||||
transform-origin: center top;
|
||||
}
|
||||
[data-tooltip]:after {
|
||||
-webkit-transform-origin: center bottom;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
[data-tooltip]:hover:before,
|
||||
[data-tooltip]:hover:after {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
[data-tooltip]:hover:before {
|
||||
-webkit-transform: rotate(45deg) scale(1) !important;
|
||||
transform: rotate(45deg) scale(1) !important;
|
||||
}
|
||||
|
||||
|
||||
/*--------------
|
||||
Position
|
||||
---------------*/
|
||||
|
||||
[data-position~="top"][data-tooltip]:before {
|
||||
background: #1B1C1D;
|
||||
}
|
||||
|
||||
/* Top Center */
|
||||
[data-position="top center"][data-tooltip]:after {
|
||||
top: auto;
|
||||
right: auto;
|
||||
left: 50%;
|
||||
bottom: 100%;
|
||||
-webkit-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
[data-position="top center"][data-tooltip]:before {
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
background: #1B1C1D;
|
||||
margin-left: -0.07142857rem;
|
||||
margin-bottom: 0.14285714rem;
|
||||
}
|
||||
|
||||
/* Top Left */
|
||||
[data-position="top left"][data-tooltip]:after {
|
||||
top: auto;
|
||||
right: auto;
|
||||
left: 0;
|
||||
bottom: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
[data-position="top left"][data-tooltip]:before {
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: 100%;
|
||||
left: 1em;
|
||||
margin-left: -0.07142857rem;
|
||||
margin-bottom: 0.14285714rem;
|
||||
}
|
||||
|
||||
/* Top Right */
|
||||
[data-position="top right"][data-tooltip]:after {
|
||||
top: auto;
|
||||
left: auto;
|
||||
right: 0;
|
||||
bottom: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
[data-position="top right"][data-tooltip]:before {
|
||||
top: auto;
|
||||
left: auto;
|
||||
bottom: 100%;
|
||||
right: 1em;
|
||||
margin-left: -0.07142857rem;
|
||||
margin-bottom: 0.14285714rem;
|
||||
}
|
||||
[data-position~="bottom"][data-tooltip]:before {
|
||||
background: #1B1C1D;
|
||||
-webkit-box-shadow: -1px -1px 0 0 #bababc;
|
||||
box-shadow: -1px -1px 0 0 #bababc;
|
||||
}
|
||||
|
||||
/* Bottom Center */
|
||||
[data-position="bottom center"][data-tooltip]:after {
|
||||
bottom: auto;
|
||||
right: auto;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
-webkit-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
[data-position="bottom center"][data-tooltip]:before {
|
||||
bottom: auto;
|
||||
right: auto;
|
||||
top: 100%;
|
||||
left: 30%;
|
||||
margin-left: -0.07142857rem;
|
||||
margin-top: 0.14285714rem;
|
||||
}
|
||||
|
||||
/* Bottom Left */
|
||||
[data-position="bottom left"][data-tooltip]:after {
|
||||
left: 0;
|
||||
top: 100%;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
[data-position="bottom left"][data-tooltip]:before {
|
||||
bottom: auto;
|
||||
right: auto;
|
||||
top: 100%;
|
||||
left: 1em;
|
||||
margin-left: -0.07142857rem;
|
||||
margin-top: 0.14285714rem;
|
||||
}
|
||||
|
||||
/* Bottom Right */
|
||||
[data-position="bottom right"][data-tooltip]:after {
|
||||
right: 0;
|
||||
top: 100%;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
[data-position="bottom right"][data-tooltip]:before {
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
top: 100%;
|
||||
right: 1em;
|
||||
margin-left: -0.14285714rem;
|
||||
margin-top: 0.07142857rem;
|
||||
}
|
||||
|
||||
/* Left Center */
|
||||
[data-position="left center"][data-tooltip]:after {
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
margin-right: 0.5em;
|
||||
-webkit-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
[data-position="left center"][data-tooltip]:before {
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
margin-top: -0.14285714rem;
|
||||
margin-right: -0.07142857rem;
|
||||
background: #1B1C1D;
|
||||
}
|
||||
|
||||
/* Right Center */
|
||||
[data-position="right center"][data-tooltip]:after {
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
margin-left: 0.5em;
|
||||
-webkit-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
[data-position="right center"][data-tooltip]:before {
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
margin-top: -0.07142857rem;
|
||||
margin-left: 0.14285714rem;
|
||||
background: #1B1C1D;
|
||||
}
|
||||
|
||||
[data-position~="bottom"][data-tooltip]:before {
|
||||
-webkit-transform-origin: center bottom;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
[data-position~="bottom"][data-tooltip]:after {
|
||||
-webkit-transform-origin: center top;
|
||||
transform-origin: center top;
|
||||
}
|
||||
[data-position="left center"][data-tooltip]:before {
|
||||
-webkit-transform-origin: top center;
|
||||
transform-origin: top center;
|
||||
}
|
||||
[data-position="left center"][data-tooltip]:after {
|
||||
-webkit-transform-origin: right center;
|
||||
transform-origin: right center;
|
||||
}
|
||||
[data-position="right center"][data-tooltip]:before {
|
||||
-webkit-transform-origin: right center;
|
||||
transform-origin: right center;
|
||||
}
|
||||
[data-position="right center"][data-tooltip]:after {
|
||||
-webkit-transform-origin: left center;
|
||||
transform-origin: left center;
|
||||
}
|
||||
@media only screen and (max-width: 740px) {
|
||||
/*hide tooltips on mobile*/
|
||||
[data-tooltip]:hover:before,
|
||||
[data-tooltip]:hover:after {
|
||||
visibility: visible;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.glint:after {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -392,7 +392,7 @@ function createElement ( doc, tag, props, children ) {
|
||||
|
||||
function fixCursor ( node, root ) {
|
||||
// In Webkit and Gecko, block level elements are collapsed and
|
||||
// unfocusable if they have no content. To remedy this, a <BR> must be
|
||||
// unfocussable if they have no content. To remedy this, a <BR> must be
|
||||
// inserted. In Opera and IE, we just need a textnode in order for the
|
||||
// cursor to appear.
|
||||
var self = root.__squire__;
|
||||
@@ -460,6 +460,7 @@ function fixContainer ( container, root ) {
|
||||
var doc = container.ownerDocument;
|
||||
var wrapper = null;
|
||||
var i, l, child, isBR;
|
||||
var config = root.__squire__._config;
|
||||
|
||||
for ( i = 0, l = children.length; i < l; i += 1 ) {
|
||||
child = children[i];
|
||||
@@ -1095,9 +1096,6 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
|
||||
}
|
||||
|
||||
while ( true ) {
|
||||
if ( endContainer === endMax || endContainer === root ) {
|
||||
break;
|
||||
}
|
||||
if ( maySkipBR &&
|
||||
endContainer.nodeType !== TEXT_NODE &&
|
||||
endContainer.childNodes[ endOffset ] &&
|
||||
@@ -1105,7 +1103,9 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
|
||||
endOffset += 1;
|
||||
maySkipBR = false;
|
||||
}
|
||||
if ( endOffset !== getLength( endContainer ) ) {
|
||||
if ( endContainer === endMax ||
|
||||
endContainer === root ||
|
||||
endOffset !== getLength( endContainer ) ) {
|
||||
break;
|
||||
}
|
||||
parent = endContainer.parentNode;
|
||||
@@ -1117,20 +1117,6 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
|
||||
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,
|
||||
// or null if no block is contained by the range.
|
||||
var getStartBlockOfRange = function ( range, root ) {
|
||||
@@ -1265,9 +1251,7 @@ var keys = {
|
||||
37: 'left',
|
||||
39: 'right',
|
||||
46: 'delete',
|
||||
191: '/',
|
||||
219: '[',
|
||||
220: '\\',
|
||||
221: ']'
|
||||
};
|
||||
|
||||
@@ -1301,13 +1285,10 @@ var onKey = function ( event ) {
|
||||
if ( event.altKey ) { modifiers += 'alt-'; }
|
||||
if ( event.ctrlKey ) { modifiers += 'ctrl-'; }
|
||||
if ( event.metaKey ) { modifiers += 'meta-'; }
|
||||
if ( event.shiftKey ) { modifiers += 'shift-'; }
|
||||
}
|
||||
// However, on Windows, shift-delete is apparently "cut" (WTF right?), so
|
||||
// we want to let the browser handle shift-delete in this situation.
|
||||
if ( isWin && event.shiftKey && key === 'delete' ) {
|
||||
modifiers += 'shift-';
|
||||
}
|
||||
// we want to let the browser handle shift-delete.
|
||||
if ( event.shiftKey ) { modifiers += 'shift-'; }
|
||||
|
||||
key = modifiers + key;
|
||||
|
||||
@@ -1484,7 +1465,12 @@ var handleEnter = function ( self, shiftKey, range ) {
|
||||
// just play it safe and insert a <br>.
|
||||
if ( !block || shiftKey || /^T[HD]$/.test( block.nodeName ) ) {
|
||||
// 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' ) );
|
||||
range.collapse( false );
|
||||
self.setSelection( range );
|
||||
@@ -1763,7 +1749,7 @@ var keyHandlers = {
|
||||
}
|
||||
},
|
||||
space: function ( self, _, range ) {
|
||||
var node;
|
||||
var node, parent;
|
||||
var root = self._root;
|
||||
self._recordUndoState( range );
|
||||
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 + 'i' ] = mapKeyToFormat( 'I' );
|
||||
keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' );
|
||||
keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat( 'S' );
|
||||
keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat( 'SUB', { tag: 'SUP' } );
|
||||
keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat( 'SUP', { tag: 'SUB' } );
|
||||
keyHandlers[ ctrlKey + 'shift-8' ] =
|
||||
toggleList( /(?:^|>)UL/, 'makeUnorderedList' );
|
||||
keyHandlers[ ctrlKey + 'shift-9' ] =
|
||||
toggleList( /(?:^|>)OL/, 'makeOrderedList' );
|
||||
keyHandlers[ ctrlKey + '[' ] =
|
||||
changeIndentationLevel( 'decreaseQuoteLevel', 'decreaseListLevel' );
|
||||
keyHandlers[ ctrlKey + ']' ] =
|
||||
changeIndentationLevel( 'increaseQuoteLevel', 'increaseListLevel' );
|
||||
keyHandlers[ ctrlKey + 'shift-8' ] = mapKeyTo( 'makeUnorderedList' );
|
||||
keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' );
|
||||
keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' );
|
||||
keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' );
|
||||
keyHandlers[ ctrlKey + 'd' ] = mapKeyTo( 'toggleCode' );
|
||||
keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
|
||||
keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
|
||||
@@ -2117,7 +2074,7 @@ var cleanTree = function cleanTree ( node, config, preserveWS ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
data = data.replace( /^[ \r\n]+/g, sibling ? ' ' : '' );
|
||||
data = data.replace( /^[ \t\r\n]+/g, sibling ? ' ' : '' );
|
||||
}
|
||||
if ( endsWithWS ) {
|
||||
walker.currentNode = child;
|
||||
@@ -2132,7 +2089,7 @@ var cleanTree = function cleanTree ( node, config, preserveWS ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
data = data.replace( /[ \r\n]+$/g, sibling ? ' ' : '' );
|
||||
data = data.replace( /[ \t\r\n]+$/g, sibling ? ' ' : '' );
|
||||
}
|
||||
if ( 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
|
||||
// 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.
|
||||
var setClipboardData =
|
||||
function ( event, contents, root, willCutCopy, toPlainText, plainTextOnly ) {
|
||||
var clipboardData = event.clipboardData;
|
||||
var doc = event.target.ownerDocument;
|
||||
var body = doc.body;
|
||||
var node = createElement( doc, 'div' );
|
||||
var setClipboardData = function ( clipboardData, node, root, config ) {
|
||||
var body = node.ownerDocument.body;
|
||||
var willCutCopy = config.willCutCopy;
|
||||
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;
|
||||
text = node.innerText || node.textContent;
|
||||
|
||||
if ( willCutCopy ) {
|
||||
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.
|
||||
// If on Windows, normalise to \r\n, since Notepad and some other crappy
|
||||
// apps do not understand just \n.
|
||||
@@ -2262,18 +2210,18 @@ var setClipboardData =
|
||||
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 );
|
||||
event.preventDefault();
|
||||
|
||||
body.removeChild( node );
|
||||
};
|
||||
|
||||
var onCut = function ( event ) {
|
||||
var clipboardData = event.clipboardData;
|
||||
var range = this.getSelection();
|
||||
var root = this._root;
|
||||
var self = this;
|
||||
var startBlock, endBlock, copyRoot, contents, parent, newContents;
|
||||
var startBlock, endBlock, copyRoot, contents, parent, newContents, node;
|
||||
|
||||
// Nothing to do
|
||||
if ( range.collapsed ) {
|
||||
@@ -2285,7 +2233,7 @@ var onCut = function ( event ) {
|
||||
this.saveUndoState( range );
|
||||
|
||||
// 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
|
||||
// parents up to root if selection across blocks
|
||||
startBlock = getStartBlockOfRange( range, root );
|
||||
@@ -2305,8 +2253,10 @@ var onCut = function ( event ) {
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
// Set clipboard data
|
||||
setClipboardData(
|
||||
event, contents, root, this._config.willCutCopy, null, false );
|
||||
node = this.createElement( 'div' );
|
||||
node.appendChild( contents );
|
||||
setClipboardData( clipboardData, node, root, this._config );
|
||||
event.preventDefault();
|
||||
} else {
|
||||
setTimeout( function () {
|
||||
try {
|
||||
@@ -2321,10 +2271,14 @@ var onCut = function ( event ) {
|
||||
this.setSelection( range );
|
||||
};
|
||||
|
||||
var _onCopy = function ( event, range, root, willCutCopy, toPlainText, plainTextOnly ) {
|
||||
var startBlock, endBlock, copyRoot, contents, parent, newContents;
|
||||
var onCopy = function ( event ) {
|
||||
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.
|
||||
if ( !isEdge && event.clipboardData ) {
|
||||
if ( !isEdge && clipboardData ) {
|
||||
// Clipboard content should include all parents within block, or all
|
||||
// parents up to root if selection across blocks
|
||||
startBlock = getStartBlockOfRange( range, root );
|
||||
@@ -2349,21 +2303,13 @@ var _onCopy = function ( event, range, root, willCutCopy, toPlainText, plainText
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
// 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
|
||||
// in paste event.
|
||||
function monitorShiftKey ( event ) {
|
||||
@@ -2669,8 +2615,6 @@ function Squire ( root, config ) {
|
||||
this.setConfig( config );
|
||||
|
||||
root.setAttribute( 'contenteditable', 'true' );
|
||||
// Grammarly breaks the editor, *sigh*
|
||||
root.setAttribute( 'data-gramm', 'false' );
|
||||
|
||||
// Remove Firefox's built-in controls
|
||||
try {
|
||||
@@ -2693,8 +2637,7 @@ var sanitizeToDOMFragment = function ( html, isPaste, self ) {
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||
WHOLE_DOCUMENT: false,
|
||||
RETURN_DOM: true,
|
||||
RETURN_DOM_FRAGMENT: true,
|
||||
FORCE_BODY: false
|
||||
RETURN_DOM_FRAGMENT: true
|
||||
}) : null;
|
||||
return frag ? doc.importNode( frag, true ) : doc.createDocumentFragment();
|
||||
};
|
||||
@@ -2978,6 +2921,16 @@ proto.setSelection = function ( range ) {
|
||||
// needing restore on focus.
|
||||
if ( !this._isFocused ) {
|
||||
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 {
|
||||
// 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
|
||||
@@ -2987,15 +2940,7 @@ proto.setSelection = function ( range ) {
|
||||
this._win.focus();
|
||||
}
|
||||
var sel = getWindowSelection( this );
|
||||
if ( sel && sel.setBaseAndExtent ) {
|
||||
sel.setBaseAndExtent(
|
||||
range.startContainer,
|
||||
range.startOffset,
|
||||
range.endContainer,
|
||||
range.endOffset,
|
||||
);
|
||||
} else if ( sel ) {
|
||||
// This is just for IE11
|
||||
if ( sel ) {
|
||||
sel.removeAllRanges();
|
||||
sel.addRange( range );
|
||||
}
|
||||
@@ -3171,7 +3116,7 @@ proto._updatePath = function ( range, force ) {
|
||||
// selectionchange is fired synchronously in IE when removing current selection
|
||||
// and when setting new selection; keyup/mouseup may have processing we want
|
||||
// to do first. Either way, send to next event loop.
|
||||
proto._updatePathOnEvent = function () {
|
||||
proto._updatePathOnEvent = function ( event ) {
|
||||
var self = this;
|
||||
if ( self._isFocused && !self._willUpdatePath ) {
|
||||
self._willUpdatePath = true;
|
||||
@@ -3891,9 +3836,10 @@ var increaseBlockQuoteLevel = function ( frag ) {
|
||||
};
|
||||
|
||||
var decreaseBlockQuoteLevel = function ( frag ) {
|
||||
var root = this._root;
|
||||
var blockquotes = frag.querySelectorAll( 'blockquote' );
|
||||
Array.prototype.filter.call( blockquotes, function ( el ) {
|
||||
return !getNearest( el.parentNode, frag, 'BLOCKQUOTE' );
|
||||
return !getNearest( el.parentNode, root, 'BLOCKQUOTE' );
|
||||
}).forEach( function ( el ) {
|
||||
replaceWith( el, empty( el ) );
|
||||
});
|
||||
@@ -4174,14 +4120,7 @@ proto._getHTML = function () {
|
||||
proto._setHTML = function ( html ) {
|
||||
var root = this._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 {
|
||||
fixCursor( node, root );
|
||||
} while ( node = getNextBlock( node, root ) );
|
||||
@@ -4189,7 +4128,8 @@ proto._setHTML = function ( html ) {
|
||||
};
|
||||
|
||||
proto.getHTML = function ( withBookMark ) {
|
||||
var html, range;
|
||||
var brs = [],
|
||||
root, node, fixer, html, l, range;
|
||||
if ( withBookMark && ( range = this.getSelection() ) ) {
|
||||
this._saveRangeToBookmark( range );
|
||||
}
|
||||
@@ -4475,12 +4415,6 @@ proto.insertHTML = function ( html, isPaste ) {
|
||||
this._docWasChanged();
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -4984,7 +4918,6 @@ Squire.rangeDoesEndAtBlockBoundary = rangeDoesEndAtBlockBoundary;
|
||||
Squire.expandRangeToBlockBoundaries = expandRangeToBlockBoundaries;
|
||||
|
||||
// Clipboard.js exports
|
||||
Squire.onCopy = _onCopy;
|
||||
Squire.onPaste = onPaste;
|
||||
|
||||
// Editor.js exports
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
display: inline-block;
|
||||
border: 1px solid;
|
||||
border-color: var(--border_color);
|
||||
background-color: var(--small_element_bg_color);
|
||||
border-radius: 4px;
|
||||
margin: 0 0 15px;
|
||||
max-height: 10000px;
|
||||
@@ -20,7 +19,7 @@
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 75px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.image-placeholder:after {
|
||||
content: 'No Image';
|
||||
@@ -89,14 +88,7 @@
|
||||
<!-- image and text -->
|
||||
<div class="six wide center aligned middle aligned column">
|
||||
<a :href="linkUrl" target="_blank" >
|
||||
<img v-if="item.file_location" class="attachment-image"
|
||||
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}`">
|
||||
<img v-if="item.file_location" class="attachment-image" :src="`/api/static/thumb_${item.file_location}`">
|
||||
<span v-else>
|
||||
<img class="image-placeholder" loading="lazy" src="/api/static/assets/marketing/void.svg">
|
||||
No Image
|
||||
@@ -117,16 +109,11 @@
|
||||
<a class="link" :href="linkUrl" target="_blank">{{linkText}}</a>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openNote">
|
||||
<div class="ui small compact basic button" v-on:click="openNote">
|
||||
<i class="file outline icon"></i>
|
||||
Open Note
|
||||
</div>
|
||||
<div v-if="!item.note_id" class="ui small compact basic disabled button">
|
||||
<i class="angle double up icon"></i>
|
||||
Pushed from Web
|
||||
</div>
|
||||
|
||||
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openEditAttachments"
|
||||
<div class="ui small compact basic button" v-on:click="openEditAttachments"
|
||||
:class="{ 'disabled':this.searchParams.noteId }">
|
||||
<i class="folder open outline icon"></i>
|
||||
Note Files
|
||||
@@ -183,9 +170,6 @@
|
||||
this.checkKeyup()
|
||||
})
|
||||
},
|
||||
updated: function(){
|
||||
this.checkKeyup()
|
||||
},
|
||||
methods: {
|
||||
checkKeyup(){
|
||||
let elm = this.$refs.edit
|
||||
@@ -218,7 +202,6 @@
|
||||
}, 600)
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to delete attachment') })
|
||||
},
|
||||
saveIt(){
|
||||
|
||||
@@ -237,7 +220,6 @@
|
||||
|
||||
//Save it, and don't think about it.
|
||||
axios.post('/api/attachment/update', data)
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Save') })
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,59 +1,54 @@
|
||||
<template>
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
<div :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
|
||||
<div class="ui basic segment">
|
||||
<div class="ui grid">
|
||||
|
||||
<div class="ui sixteen wide column">
|
||||
<div class="ui dividing header">
|
||||
Reset Background Color and Icon
|
||||
</div>
|
||||
<div class="ui labeled basic icon button" v-on:click="clearStyles">
|
||||
<div class="ui sixteen wide center aligned column">
|
||||
<div class="ui fluid button" v-on:click="clearStyles">
|
||||
<i class="refresh icon"></i>
|
||||
Reset
|
||||
Clear All Styles
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column rounded" :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
|
||||
<div class="ui dividing header" :style="{ 'color':allStyles['noteText']}">
|
||||
<i class="fill drip icon"></i>
|
||||
Background Color
|
||||
</div>
|
||||
<div v-for="color in colors"
|
||||
class="color-button"
|
||||
:style="{ backgroundColor:color }"
|
||||
v-on:click="chosenColor(color)"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<div class="ui dividing header">
|
||||
<span v-if="allStyles.noteIcon" >
|
||||
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
||||
</span>
|
||||
Note Icon
|
||||
</div>
|
||||
<div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
|
||||
<i :class="`large ${icon} icon`"></i>
|
||||
<div class="row">
|
||||
<div class="sixteen wide column">
|
||||
<br>
|
||||
<p>Note Color</p>
|
||||
<div v-for="color in getReducedColors()"
|
||||
class="color-button"
|
||||
:style="{ backgroundColor:color }"
|
||||
v-on:click="chosenColor(color)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<div class="ui dividing header">
|
||||
<span v-if="allStyles.noteIcon" >
|
||||
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
||||
</span>
|
||||
Icon Color
|
||||
</div>
|
||||
<div v-for="color in getReducedColors()"
|
||||
class="color-button"
|
||||
:style="{ backgroundColor:color }"
|
||||
v-on:click="chooseIconColor(color)"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="sixteen wide column">
|
||||
<p>Note Icon
|
||||
<span v-if="allStyles.noteIcon" >
|
||||
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
||||
</span>
|
||||
</p>
|
||||
<div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
|
||||
<i :class="`large ${icon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="sixteen wide column">
|
||||
<p>Icon Color</p>
|
||||
<div v-for="color in getReducedColors()"
|
||||
class="color-button"
|
||||
:style="{ backgroundColor:color }"
|
||||
v-on:click="chooseIconColor(color)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -69,9 +64,9 @@
|
||||
return {
|
||||
allStyles:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
|
||||
blankStyle:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':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)'],
|
||||
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']
|
||||
colors: [
|
||||
"#ffebee","#ffcdd2","#ef9a9a","#e57373","#ef5350","#f44336","#e53935","#d32f2f","#c62828","#b71c1c","#fce4ec","#f8bbd0","#f48fb1","#f06292","#ec407a","#e91e63","#d81b60","#c2185b","#ad1457","#880e4f","#f3e5f5","#e1bee7","#ce93d8","#ba68c8","#ab47bc","#9c27b0","#8e24aa","#7b1fa2","#6a1b9a","#4a148c","#ede7f6","#d1c4e9","#b39ddb","#9575cd","#7e57c2","#673ab7","#5e35b1","#512da8","#4527a0","#311b92","#e8eaf6","#c5cae9","#9fa8da","#7986cb","#5c6bc0","#3f51b5","#3949ab","#303f9f","#283593","#1a237e","#e3f2fd","#bbdefb","#90caf9","#64b5f6","#42a5f5","#2196f3","#1e88e5","#1976d2","#1565c0","#0d47a1","#e1f5fe","#b3e5fc","#81d4fa","#4fc3f7","#29b6f6","#03a9f4","#039be5","#0288d1","#0277bd","#01579b","#e0f7fa","#b2ebf2","#80deea","#4dd0e1","#26c6da","#00bcd4","#00acc1","#0097a7","#00838f","#006064","#e0f2f1","#b2dfdb","#80cbc4","#4db6ac","#26a69a","#009688","#00897b","#00796b","#00695c","#004d40","#e8f5e9","#c8e6c9","#a5d6a7","#81c784","#66bb6a","#4caf50","#43a047","#388e3c","#2e7d32","#1b5e20","#f1f8e9","#dcedc8","#c5e1a5","#aed581","#9ccc65","#8bc34a","#7cb342","#689f38","#558b2f","#33691e","#f9fbe7","#f0f4c3","#e6ee9c","#dce775","#d4e157","#cddc39","#c0ca33","#afb42b","#9e9d24","#827717","#fffde7","#fff9c4","#fff59d","#fff176","#ffee58","#ffeb3b","#fdd835","#fbc02d","#f9a825","#f57f17","#fff8e1","#ffecb3","#ffe082","#ffd54f","#ffca28","#ffc107","#ffb300","#ffa000","#ff8f00","#ff6f00","#fff3e0","#ffe0b2","#ffcc80","#ffb74d","#ffa726","#ff9800","#fb8c00","#f57c00","#ef6c00","#e65100","#fbe9e7","#ffccbc","#ffab91","#ff8a65","#ff7043","#ff5722","#f4511e","#e64a19","#d84315","#bf360c","#efebe9","#d7ccc8","#bcaaa4","#a1887f","#8d6e63","#795548","#6d4c41","#5d4037","#4e342e","#3e2723","#fafafa","#f5f5f5","#eeeeee","#e0e0e0","#bdbdbd","#9e9e9e","#757575","#616161","#424242","#212121","#eceff1","#cfd8dc","#b0bec5","#90a4ae","#78909c","#607d8b","#546e7a","#455a64","#37474f","#263238","#ffffff","#000000"],
|
||||
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:{
|
||||
@@ -88,11 +83,20 @@
|
||||
let reduced = []
|
||||
|
||||
this.colors.forEach((color,i) => {
|
||||
if(i < 20 || i > 69){
|
||||
|
||||
if(i%20 <= 10){
|
||||
return
|
||||
}
|
||||
|
||||
let mod = (i % 10)+1 //1 - 10
|
||||
let lines = [3, 5, 8, 9, 10]
|
||||
if(lines.includes(mod)){
|
||||
reduced.push(color)
|
||||
}
|
||||
})
|
||||
|
||||
reduced.push("#000")
|
||||
|
||||
return reduced
|
||||
},
|
||||
clearStyles(){
|
||||
@@ -106,24 +110,14 @@
|
||||
//Set not background to color that was chosen
|
||||
this.allStyles.noteBackground = inColor
|
||||
|
||||
if(inColor == null){
|
||||
this.$emit('changeColor', this.allStyles)
|
||||
return
|
||||
}
|
||||
|
||||
//Automatically select note text color
|
||||
|
||||
//If you are using hex colors, use this
|
||||
// Convert hex color to RGB - http://gist.github.com/983661
|
||||
// let color = +("0x" + inColor.slice(1).replace(inColor.length < 5 && /./g, '$&$&'));
|
||||
// let r = color >> 16;
|
||||
// let g = color >> 8 & 255;
|
||||
// let b = color & 255;
|
||||
let color = +("0x" + inColor.slice(1).replace(inColor.length < 5 && /./g, '$&$&'));
|
||||
|
||||
const set = inColor.match(/\d+/g)
|
||||
const r = parseInt(set[0])
|
||||
const g = parseInt(set[1])
|
||||
const b = parseInt(set[2])
|
||||
let r = color >> 16;
|
||||
let g = color >> 8 & 255;
|
||||
let b = color & 255;
|
||||
|
||||
//Convert RGB to HSP
|
||||
const hsp = Math.sqrt( 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) );
|
||||
@@ -152,24 +146,18 @@
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
.icon-button, .color-button {
|
||||
.icon-button {
|
||||
height: 40px;
|
||||
width: calc(15% - 1px);
|
||||
width: 14.2%;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
font-size: 1.3em;
|
||||
border: 1px solid grey;
|
||||
text-align: center;
|
||||
padding: 5px 0px 0 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 1px 3px 0px #3e3e3e;
|
||||
margin: 2px 2px 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.color-button {
|
||||
width: calc(10% - 4px);
|
||||
}
|
||||
.rounded {
|
||||
border-radius: 5px;
|
||||
height: 50px;
|
||||
width: 20%;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
}
|
||||
</style>
|
||||
43
client/src/components/CrunchMenu.vue
Normal file
43
client/src/components/CrunchMenu.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>Crunch Menu</p>
|
||||
<div v-for="(item, index) in items">
|
||||
<slot :name="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'CrunchMenu',
|
||||
data () {
|
||||
return {
|
||||
items: []
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
},
|
||||
mounted(){
|
||||
console.log(this)
|
||||
// console.log(this.$slots.default)
|
||||
this.$slots.default.forEach( vnode => {
|
||||
if(vnode.tag && vnode.tag.length > 0){
|
||||
this.items.push(vnode)
|
||||
}
|
||||
})
|
||||
|
||||
console.log(this.items)
|
||||
},
|
||||
methods: {
|
||||
onClickTag(index){
|
||||
console.log('yup')
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
</style>
|
||||
@@ -24,9 +24,9 @@
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
// this.$bus.$on('reset_fast_filters', () => {
|
||||
// this.orderString = 'Order by Last Edited'
|
||||
// })
|
||||
this.$bus.$on('reset_fast_filters', () => {
|
||||
this.orderString = 'Order by Last Edited'
|
||||
})
|
||||
},
|
||||
methods:{
|
||||
displayString(){
|
||||
@@ -40,7 +40,7 @@
|
||||
let filter = {}
|
||||
filter[option] = 1
|
||||
|
||||
// this.$bus.$emit('update_fast_filters', filter)
|
||||
this.$bus.$emit('update_fast_filters', filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@
|
||||
.filter-menu {
|
||||
|
||||
color: var(--text_color);
|
||||
background-color: var(--small_element_bg_color);
|
||||
background-color: var(--background_color);
|
||||
|
||||
|
||||
border: 1px solid;
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
<template>
|
||||
<form data-tooltip="Upload File" data-inverted>
|
||||
<label :for="`upfile-${noteId}`" class="clickable">
|
||||
<!-- <nm-button icon="upload" :text="uploadStatusText"/> -->
|
||||
<i class="file upload icon"></i>
|
||||
<nm-button icon="upload" :text="uploadStatusText"/>
|
||||
</label>
|
||||
<input class="hidden-up" type="file" :id="`upfile-${noteId}`" ref="file" v-on:change="handleFileUpload()" />
|
||||
<!-- <button v-if="file" v-on:click="uploadFileToServer()">Submit</button> -->
|
||||
@@ -74,7 +73,6 @@
|
||||
})
|
||||
.catch(results => {
|
||||
this.uploadStatusText = 0
|
||||
this.$bus.$emit('notification', 'Failed to Upload')
|
||||
})
|
||||
},
|
||||
handleFileUpload() {
|
||||
|
||||
@@ -2,122 +2,45 @@
|
||||
|
||||
.popup-body {
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
min-height: 50px;
|
||||
min-width: 200px;
|
||||
max-width: calc(100% - 30px);
|
||||
z-index: 1020;
|
||||
|
||||
max-width: calc(100% - 20px);
|
||||
z-index: 1002;
|
||||
border-top: 2px solid #21ba45;
|
||||
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);
|
||||
background-color: #21ba45;
|
||||
}
|
||||
.popup-row {
|
||||
padding: 1em 5px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.popup-row > p {
|
||||
/*width: calc(100% - 50px);*/
|
||||
.popup-row > span {
|
||||
width: calc(100% - 50px);
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px 0;
|
||||
font-size: 1.25em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.popup-row + .popup-row {
|
||||
border-top: 1px solid #FFF;
|
||||
}
|
||||
|
||||
.slide-in-bottom {
|
||||
animation: slide-in-bottom 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
}
|
||||
@keyframes slide-in-bottom {
|
||||
0% {
|
||||
transform: translateY(-1000px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.color-fade {
|
||||
animation: color-fade-animation 0.7s ease-out both;
|
||||
}
|
||||
@keyframes color-fade-animation {
|
||||
0% {
|
||||
color: black;
|
||||
background-color: black;
|
||||
}
|
||||
50% {
|
||||
color: black;
|
||||
background-color: black;
|
||||
}
|
||||
100% {
|
||||
color: white;
|
||||
background-color: #21ba45;
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
<template>
|
||||
<div class="popup-body slide-in-bottom" v-on:click="dismiss" v-if="notifications.length > 0">
|
||||
<div class="popup-body" v-on:click="dismiss" v-if="notifications.length > 0">
|
||||
<div class="popup-row" v-for="item in notifications">
|
||||
<div class="meter">
|
||||
<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>
|
||||
<i class="disabled angle left icon"></i>
|
||||
<span>{{ item }}</span>
|
||||
<i class="disabled angle right icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -135,33 +58,24 @@
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
this.$bus.$on('notification', notificationText => {
|
||||
this.displayNotification(notificationText)
|
||||
this.$bus.$on('notification', info => {
|
||||
this.displayNotification(info)
|
||||
})
|
||||
},
|
||||
mounted(){
|
||||
|
||||
// this.$bus.$emit('notification', 'Password Protection Removed Login did not succeed')
|
||||
// this.$bus.$emit('notification', 'Password Protection Removed your life is exposed to the internet')
|
||||
// this.$bus.$emit('notification', 'Password Protection Removed everyone can see everything')
|
||||
// this.$bus.$emit('notification', 'Password Protection Removed')
|
||||
// this.$bus.$emit('notification', 'Password Protection Removed')
|
||||
// this.$bus.$emit('notification', 'Password Protection Removed')
|
||||
|
||||
},
|
||||
methods: {
|
||||
displayNotification(notificationText){
|
||||
|
||||
const date = new Date()
|
||||
const time = date.toLocaleTimeString()
|
||||
|
||||
const notification = {
|
||||
text: notificationText,
|
||||
time: time
|
||||
}
|
||||
|
||||
this.notifications.unshift(notification)
|
||||
displayNotification(newNotification){
|
||||
this.notifications.push(newNotification)
|
||||
clearTimeout(this.totalTimeout)
|
||||
this.totalTimeout = setTimeout(() => {
|
||||
this.dismiss()
|
||||
}, 3000)
|
||||
}, 4000)
|
||||
},
|
||||
dismiss(){
|
||||
this.notifications = []
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
<style scoped>
|
||||
.slotholder {
|
||||
height: 100vh;
|
||||
width: 180px;
|
||||
width: 140px;
|
||||
display: block;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
.global-menu {
|
||||
width: 180px;
|
||||
/* background: #221f2b; */
|
||||
width: 140px;
|
||||
background: #221f2b;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 900;
|
||||
z-index: 111;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.menu-logo-display {
|
||||
width: 27px;
|
||||
margin: 5px 0 0 55px;
|
||||
width: 25px;
|
||||
margin: 5px 0 0 34px;
|
||||
display: inline-block;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
color: #fff;
|
||||
padding: 9px 10px;
|
||||
padding: 0.8em 10px 0.8em 10px;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 1.1em;
|
||||
font-size: 1.15em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.menu-item i.icon {
|
||||
@@ -44,8 +41,7 @@
|
||||
|
||||
.menu-section {}
|
||||
.menu-section + .menu-section {
|
||||
/* border-top: 1px solid #534c68; */
|
||||
border-top: 1px solid #534c68e3;
|
||||
border-top: 1px solid #534c68;
|
||||
}
|
||||
.menu-button {
|
||||
cursor: pointer;
|
||||
@@ -55,6 +51,9 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.router-link-active i {
|
||||
/*color: #16ab39;*/
|
||||
}
|
||||
.router-link-active {
|
||||
background-color: #534c68;
|
||||
}
|
||||
@@ -66,91 +65,29 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
z-index: 899;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
}
|
||||
.top-menu-bar {
|
||||
/*color: var(--text_color);*/
|
||||
/*width: 100%;*/
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: var(--small_element_bg_color);
|
||||
/*padding: 5px 1rem 5px;*/
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 100vw;
|
||||
border-top: 1px solid var(--dark_border_color);
|
||||
display: flex;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--background_color);
|
||||
border-bottom: 1px solid;
|
||||
border-color: var(--border_color);
|
||||
padding: 5px 1rem 5px;
|
||||
}
|
||||
.place-holder {
|
||||
width: 100%;
|
||||
/*height: 40px;*/
|
||||
height: 0;
|
||||
height: 50px;
|
||||
}
|
||||
.logo-display {
|
||||
width: 27px;
|
||||
height: auto;
|
||||
}
|
||||
.version-display {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -20px;
|
||||
right: 0;
|
||||
.top-menu-bar img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
color: #8c80ae;
|
||||
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>
|
||||
@@ -162,58 +99,47 @@
|
||||
|
||||
<!-- collapsed menu, appears as a bar -->
|
||||
<div class="top-menu-bar" v-if="(collapsed || mobile) && !menuOpen">
|
||||
<div class="ui grid">
|
||||
|
||||
<!-- logo -->
|
||||
<router-link v-if="loggedIn" class="mobile-button" exact-active-class="active" to="/notes" v-on:click.native="emitReloadEvent()">
|
||||
<logo class="logo-display" color="var(--main-accent)" />
|
||||
Notes
|
||||
</router-link>
|
||||
<div class="seven wide column">
|
||||
<div class="ui large basic compact icon button" v-on:click="collapseMenu">
|
||||
<i class="green bars icon"></i>
|
||||
</div>
|
||||
|
||||
<router-link v-if="loggedIn" class="ui large basic compact icon button" to="/notes" v-on:click.native="emitReloadEvent()">
|
||||
<i class="green home icon"></i>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="loggedIn" class="ui basic icon button" exact-active-class="active" to="/attachments">
|
||||
<i class="open folder outline icon"></i>
|
||||
</router-link>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="two wide center aligned bottom aligned column">
|
||||
<img loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
|
||||
</div>
|
||||
|
||||
<div class="seven wide right aligned column">
|
||||
|
||||
<div v-on:click="toggleNightMode" class="ui large basic compact icon button">
|
||||
<i class="green moon outline icon"></i>
|
||||
</div>
|
||||
|
||||
<search-input v-if="loggedIn && mobile"></search-input>
|
||||
<!-- mobile create note button -->
|
||||
<span v-if="loggedIn">
|
||||
<span v-if="!disableNewNote" @click="createNote" class="ui large basic compact icon button">
|
||||
<i class="green plus icon"></i>
|
||||
</span>
|
||||
<span v-if="disableNewNote" class="ui large basic compact icon button">
|
||||
<i class="grey plus icon"></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- new note -->
|
||||
<div v-if="loggedIn" class="mobile-button">
|
||||
<span v-if="!disableNewNote" @click="createNote">
|
||||
<i class="green plus icon"></i>
|
||||
New Note
|
||||
</span>
|
||||
<span v-if="disableNewNote">
|
||||
<i class="grey plus icon"></i>
|
||||
Working
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 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 class="shade" v-if="mobile && !collapsed" v-on:click="collapseMenu"></div>
|
||||
@@ -224,47 +150,31 @@
|
||||
<div class="global-menu" v-if="!collapsed" v-on:click="menuClicked">
|
||||
|
||||
<div class="menu-section" v-on:click="collapseMenu">
|
||||
<i class="white angle left icon"></i>
|
||||
<logo class="menu-logo-display" color="var(--main-accent)" />
|
||||
<!-- <div class="menu-item menu-button" > -->
|
||||
<i class="white angle left icon"></i>
|
||||
<img class="menu-logo-display" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<div v-if="!disableNewNote" @click="createNote" class="menu-item menu-item menu-button">
|
||||
<div class="ui green fluid compact button">
|
||||
<i class="plus icon"></i>New Note
|
||||
</div>
|
||||
<i class="green plus icon"></i>New Note
|
||||
</div>
|
||||
<div v-if="disableNewNote" class="menu-item menu-item menu-button">
|
||||
<div class="ui basic fluid compact button">
|
||||
<i class="plus loading icon"></i>New Note
|
||||
</div>
|
||||
<i class="purple plus icon"></i>Creating
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<router-link exact-active-class="active" class="menu-item menu-button" to="/notes" v-on:click.native="emitReloadEvent()">
|
||||
<i class="file outline icon"></i>Notes
|
||||
<counter v-if="$store.getters.totals && $store.getters.totals['totalNotes']" class="float-right" number-id="totalNotes" />
|
||||
<counter class="float-right" number-id="totalNotes" />
|
||||
</router-link>
|
||||
<div>
|
||||
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(3)" v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)">
|
||||
<i class="grey paper plane outline icon"></i>Shared
|
||||
|
||||
<counter v-if="$store.getters.totals && $store.getters.totals['sharedToNotes']" class="float-right" number-id="sharedToNotes" />
|
||||
</div>
|
||||
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
|
||||
<i class="grey archive icon"></i>Archived
|
||||
|
||||
<counter v-if="$store.getters.totals && $store.getters.totals['archivedNotes']" class="float-right" number-id="archivedNotes" />
|
||||
</div>
|
||||
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['trashedNotes'] > 0">
|
||||
<i class="grey trash alternate outline icon"></i>Trashed
|
||||
|
||||
<counter v-if="$store.getters.totals && $store.getters.totals['trashedNotes']" class="float-right" number-id="trashedNotes" />
|
||||
</div>
|
||||
<!-- <div class="menu-item sub">Show Only <i class="caret down icon"></i></div> -->
|
||||
<!-- <div v-on:click="updateFastFilters(0)" class="menu-item menu-button sub"><i class="grey linkify icon"></i>Links</div> -->
|
||||
<!-- <div v-on:click="updateFastFilters(1)" class="menu-item menu-button sub"><i class="grey tags icon"></i>Tags</div> -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -276,26 +186,9 @@
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
|
||||
|
||||
<!-- 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
|
||||
<router-link v-if="loggedIn" exact-active-class="active" class="menu-item menu-button" to="/quick">
|
||||
<i class="paper plane outline icon"></i>Quick
|
||||
</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 class="menu-section" v-if="!loggedIn">
|
||||
@@ -310,60 +203,22 @@
|
||||
|
||||
<div class="menu-section">
|
||||
<div v-on:click="toggleNightMode" class="menu-item menu-button">
|
||||
<span v-if="$store.getters.getIsNightMode == 0">
|
||||
<i class="moon outline icon"></i>Black Theme</span>
|
||||
<span v-if="$store.getters.getIsNightMode == 1">
|
||||
<span v-if="$store.getters.getIsNightMode">
|
||||
<i class="moon outline icon"></i>Light Theme</span>
|
||||
<span v-else>
|
||||
<i class="moon outline icon"></i>Dark Theme</span>
|
||||
</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">
|
||||
<router-link class="menu-item menu-button" exact-active-class="active" to="/help">
|
||||
<i class="question circle outline icon"></i>Help
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<div class="menu-item menu-button" v-on:click="logout()">
|
||||
<i class="log out icon"></i>Log Out
|
||||
<div class="menu-section" v-if="loggedIn" data-tooltip="Click to log out" data-inverted="" data-position="right center">
|
||||
<div v-if="loggedIn" v-on:click="destroyLoginToken" class="menu-item menu-button">
|
||||
<i v-if="userIcon" class="user outline icon"></i>{{ usernameDisplay }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="menu-section" v-if="gotTags()">
|
||||
<div class="menu-item">
|
||||
<i class="green tags icon"></i>
|
||||
Tags
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="gotTags()">
|
||||
<div class="menu-section"
|
||||
v-for="(data, tag) in $store.getters.totals['tags']">
|
||||
<router-link class="menu-item menu-button" :to="`/search/tags/${tag}`">
|
||||
<span class="single-line-text">
|
||||
<!-- <i class="small grey tag icon"></i> -->
|
||||
<span class="float-right">{{ data.uses }}</span>
|
||||
<span class="faded"> #</span> {{ tag }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-on:click="reloadPage" class="version-display" v-if="version != 0" >
|
||||
<i :class="`${getVersionIcon()} icon`"></i> {{ version }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <router-link class="ui basic compact button" exact-active-class="active" to="/help">
|
||||
<i class="question mark icon"></i>Help
|
||||
</router-link> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -376,25 +231,18 @@
|
||||
components: {
|
||||
'search-input': require('@/components/SearchInput.vue').default,
|
||||
'counter':require('@/components/AnimatedCounterComponent.vue').default,
|
||||
'logo':require('@/components/LogoComponent.vue').default,
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
version: '0',
|
||||
username: '',
|
||||
collapsed: false,
|
||||
mobile: false,
|
||||
disableNewNote: false,
|
||||
menuOpen: true,
|
||||
userIcon: true,
|
||||
resizeDebounce: null,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
window.addEventListener('resize', this.resizeEventHandler)
|
||||
},
|
||||
beforeDestroy(){
|
||||
window.removeEventListener('resize', this.resizeEventHandler)
|
||||
beforeCreate: function(){
|
||||
},
|
||||
mounted: function(){
|
||||
this.mobile = this.$store.getters.getIsUserOnMobile
|
||||
@@ -406,10 +254,7 @@
|
||||
|
||||
if(this.loggedIn){
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
this.version = localStorage.getItem('currentVersion')
|
||||
}
|
||||
|
||||
this.resizeEventHandler() //Trigger resize event
|
||||
|
||||
},
|
||||
computed: {
|
||||
@@ -417,53 +262,20 @@
|
||||
//Map logged in from state
|
||||
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
|
||||
}
|
||||
|
||||
return this.ucWords(name.substring(0, 16))
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
gotTags(){
|
||||
|
||||
if(this.loggedIn && this.$store.getters.totals && this.$store.getters.totals.tags
|
||||
&& Object.keys(this.$store.getters.totals.tags).length
|
||||
){
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
logout() {
|
||||
|
||||
this.$router.push('/')
|
||||
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(){
|
||||
//Collapse menu when item is clicked in mobile
|
||||
if(this.mobile && !this.collapsed){
|
||||
@@ -482,21 +294,23 @@
|
||||
|
||||
},
|
||||
createNote(event){
|
||||
|
||||
const title = ''
|
||||
this.disableNewNote = true
|
||||
|
||||
axios.post('/api/note/create', {title:''})
|
||||
axios.post('/api/note/create', {title})
|
||||
.then(response => {
|
||||
|
||||
if(response.data && response.data.id){
|
||||
|
||||
//Push new note to url and it will open
|
||||
this.$router.push('/notes/open/'+response.data.id)
|
||||
|
||||
this.$bus.$emit('open_note', response.data.id)
|
||||
this.disableNewNote = false
|
||||
}
|
||||
})
|
||||
.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(){
|
||||
this.$store.commit('toggleNightMode')
|
||||
@@ -511,29 +325,27 @@
|
||||
//Reloads note page to initial state
|
||||
this.$bus.$emit('note_reload')
|
||||
},
|
||||
updateFastFilters(filterIndex){
|
||||
updateFastFilters(index){
|
||||
|
||||
//A little hacky, brings user to notes page then filters on click
|
||||
if(this.$route.name != 'Note Page'){
|
||||
if(this.$route.name != 'NotesPage'){
|
||||
this.$router.push('/notes')
|
||||
setTimeout( () => {
|
||||
this.$bus.$emit('update_fast_filters', filterIndex)
|
||||
this.updateFastFilters(index)
|
||||
}, 500 )
|
||||
} else {
|
||||
this.$bus.$emit('update_fast_filters', filterIndex)
|
||||
}
|
||||
},
|
||||
reloadPage(){
|
||||
location.reload(true)
|
||||
},
|
||||
getVersionIcon(){
|
||||
if(!this.version){
|
||||
return 'radiation alternate'
|
||||
}
|
||||
const icons = ['cat','crow','dog','dove','dragon','fish','frog','hippo','horse','kiwi bird','otter','spider', 'smile', 'robot', 'hat wizard', 'microchip', 'atom', 'grin tongue squint', 'radiation', 'ghost', 'dna', 'burn', 'brain', 'moon', 'torii gate']
|
||||
const index = ( parseInt(String(this.version).replace(/\./g,'')) % (icons.length))
|
||||
return icons[index]
|
||||
|
||||
const options = [
|
||||
'withLinks', // 'Only Show Notes with Links'
|
||||
'withTags', // 'Only Show Notes with Tags'
|
||||
'onlyArchived', //'Only Show Archived Notes'
|
||||
'onlyShowSharedNotes', //Only show shared notes
|
||||
]
|
||||
|
||||
let filter = {}
|
||||
filter[options[index]] = 1
|
||||
|
||||
this.$bus.$emit('update_fast_filters', filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,55 +0,0 @@
|
||||
<template>
|
||||
<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">
|
||||
<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">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="0.5s"
|
||||
from="0 50 50"
|
||||
to="180 50 50"
|
||||
type="rotate"
|
||||
id="strokeBox"
|
||||
attributeType="XML"
|
||||
begin="rectBox.end"/>
|
||||
</rect>
|
||||
<rect x="25" y="25" :fill="$store.getters.getIsNightMode > 0 ? '#FFF':'var(--main-accent)'" width="50" height="50">
|
||||
<animate
|
||||
attributeName="height"
|
||||
dur="1.3s"
|
||||
attributeType="XML"
|
||||
from="50"
|
||||
to="0"
|
||||
id="rectBox"
|
||||
fill="freeze"
|
||||
begin="0s;strokeBox.end"/>
|
||||
</rect>
|
||||
</svg>
|
||||
<div class="loading-message" v-if="message">{{ message }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'LoadingIcon',
|
||||
props:[ 'message' ],
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
/*min-height: 100px;*/
|
||||
margin: 20px 0;
|
||||
/*padding: 40px;*/
|
||||
border-radius: 7px;
|
||||
background-color: var(--small_element_bg_color);
|
||||
}
|
||||
.loading-container svg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.loading-message {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,241 +0,0 @@
|
||||
|
||||
<template>
|
||||
|
||||
<div>
|
||||
|
||||
<!-- thicc form display -->
|
||||
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register">
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="password" type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="password2" type="password" name="password2" placeholder="Re-type Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" v-if="require2FA">
|
||||
<div class="ui input">
|
||||
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sixteen wide field">
|
||||
<div class="ui fluid buttons">
|
||||
|
||||
|
||||
<div v-on:click="register" class="ui green button" :class="{ 'disabled':(username.length == 0 || password.length == 0)}">
|
||||
<i class="plug icon"></i>
|
||||
Sign Up
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Thin form display -->
|
||||
<div v-if="thin" class="ui small form" v-on:keyup.enter="login">
|
||||
|
||||
<div v-if="!require2FA" class="field"><!-- hide this field if someone is logging in with 2FA -->
|
||||
<div class="ui grid">
|
||||
<div class="ui sixteen wide center aligned column">
|
||||
<div v-on:click="register" class="ui green button">
|
||||
<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">
|
||||
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="password" type="password" name="password" placeholder="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="field">
|
||||
<div v-on:click="login" class="ui fluid button">
|
||||
<i class="power icon"></i>
|
||||
Login
|
||||
</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>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
props:[ 'thin' ],
|
||||
mounted() {
|
||||
|
||||
//Focus on login form on desktop
|
||||
if(!this.$store.getters.getIsUserOnMobile){
|
||||
this.$refs.nameForm.focus()
|
||||
}
|
||||
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
enabled: false,
|
||||
username: '',
|
||||
password: '',
|
||||
password2: '',
|
||||
authToken: '',
|
||||
require2FA: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
finalizeLogin(data){
|
||||
|
||||
//Destroy local data if there is an error
|
||||
if(data == false){
|
||||
this.$store.commit('destroyLoginToken')
|
||||
return
|
||||
}
|
||||
|
||||
//Login user if we have a valid token
|
||||
if(data && data.token && data.token.length > 0){
|
||||
|
||||
//Set username to local session
|
||||
this.$store.commit('setUsername', this.username)
|
||||
|
||||
const token = data.token
|
||||
|
||||
//Setup socket io after user logs in
|
||||
axios.defaults.headers.common['authorizationtoken'] = token
|
||||
this.$io.emit('user_connect', token)
|
||||
localStorage.setItem('loginToken', token)
|
||||
|
||||
//Redirect user to notes section after login
|
||||
this.$router.push('/notes')
|
||||
}
|
||||
},
|
||||
register(){
|
||||
|
||||
let error = false
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
axios.post('/api/public/register', {'username': this.username, 'password': this.password})
|
||||
.then(({data}) => {
|
||||
|
||||
if(data == false){
|
||||
this.$bus.$emit('notification', 'Unable to Sign Up - Username already in use')
|
||||
}
|
||||
|
||||
this.finalizeLogin(data)
|
||||
})
|
||||
.catch(error => {
|
||||
this.$bus.$emit('notification', 'Unable to Sign Up - Username already in use')
|
||||
})
|
||||
},
|
||||
login(){
|
||||
|
||||
if( this.username.length == 0 || this.password.length == 0 ){
|
||||
this.$bus.$emit('notification', 'Unable to Login - Username and Password Required')
|
||||
return
|
||||
}
|
||||
|
||||
axios.post('/api/public/login', {'username': this.username, 'password': this.password, 'authToken':this.authToken })
|
||||
.then(({data}) => {
|
||||
|
||||
//Enable 2FA on form
|
||||
if(data.success == false && data.verificationRequired == true && this.require2FA == false){
|
||||
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)
|
||||
return
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.$bus.$emit('notification', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<span>
|
||||
<span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete Forever" data-inverted="" data-position="top right">
|
||||
<span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete" data-inverted="" data-position="top right">
|
||||
<i class="trash alternate icon"></i>
|
||||
</span>
|
||||
<span class="clickable" @click="actuallyDelete()" @mouseleave="reset" v-if="click == 1" data-tooltip="Click again to delete." data-position="top right" data-inverted="">
|
||||
@@ -33,7 +33,6 @@
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Delete Note') })
|
||||
},
|
||||
reset(){
|
||||
this.click = 0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -91,7 +91,6 @@
|
||||
this.allTags = data.allTags
|
||||
this.noteTagIds = data.noteTagIds
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Get Tags') })
|
||||
},
|
||||
isTagOnNote(id){
|
||||
for (let i = 0; i < this.noteTagIds.length; i++) {
|
||||
@@ -171,7 +170,6 @@
|
||||
vm.suggestions = response.data
|
||||
vm.selection = -1 //Nothing selected
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
|
||||
}
|
||||
}, 300)
|
||||
},
|
||||
@@ -228,7 +226,6 @@
|
||||
//Trigger focus event to reload tag suggestions
|
||||
vm.onFocus()
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Add Tag') })
|
||||
},
|
||||
onFocus(){
|
||||
return
|
||||
@@ -243,7 +240,6 @@
|
||||
vm.suggestions = response.data
|
||||
vm.selection = -1 //Nothing selected
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Latest Tags') })
|
||||
},
|
||||
onKeyup(){
|
||||
|
||||
@@ -272,7 +268,6 @@
|
||||
.then(response => {
|
||||
this.getTags()
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Remove Tag') })
|
||||
},
|
||||
clearSuggestions(){
|
||||
this.suggestions = []
|
||||
@@ -323,7 +318,7 @@
|
||||
height: 40px;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
background-color: var(--small_element_bg_color);
|
||||
background-color: var(--background_color);
|
||||
color: var(--text_color);
|
||||
}
|
||||
.suggestion-item.active {
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
<template>
|
||||
<div class="note-title-display-card"
|
||||
:style="{'background-color':color, 'color':fontColor, 'border-color':color }"
|
||||
:class="{
|
||||
'currently-open':(currentlyOpen || showWorking),
|
||||
'ring':triggerClosedAnimation,
|
||||
'title-view':titleView
|
||||
}">
|
||||
:class="{'currently-open':currentlyOpen}"
|
||||
>
|
||||
|
||||
|
||||
<!-- 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">
|
||||
|
||||
<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 class="subtext" v-if="note.shared == 2">
|
||||
You Shared
|
||||
<span v-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
|
||||
</span>
|
||||
|
||||
@@ -21,155 +36,84 @@
|
||||
|
||||
<!-- Title display -->
|
||||
<span v-if="note.title.length > 0"
|
||||
data-test-id="title"
|
||||
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 -->
|
||||
<span v-if="note.subtext.length > 0"
|
||||
<span v-if="note.subtext.length > 0 && !isShowingSearchResults()"
|
||||
data-test-id="subtext"
|
||||
class="small-text"
|
||||
v-html="note.subtext"></span>
|
||||
|
||||
|
||||
<!-- Not indexed warning -->
|
||||
<!-- <span v-if="note.indexed != 1">
|
||||
<span class="green label">Not Indexed</span>
|
||||
</span> -->
|
||||
|
||||
|
||||
<!-- <div class="ui fluid basic button" v-if="note.encrypted == 1">
|
||||
<p v-if="note.encrypted == 1">
|
||||
<i class="green lock icon"></i>
|
||||
Locked
|
||||
</div> -->
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- slim card view -->
|
||||
<div v-if="titleView" class="thin-container" @click="cardClicked">
|
||||
|
||||
<!-- icon -->
|
||||
<span v-if="noteIcon" class="thin-icon">
|
||||
<i :class="`${noteIcon} icon`" :style="{ 'color':iconColor }"></i>
|
||||
</span>
|
||||
|
||||
<!-- title -->
|
||||
<span class="thin-title" v-if="note.title.length > 0">{{ note.title }}</span>
|
||||
|
||||
<!-- snippet -->
|
||||
<span class="thick-sub" v-if="note.subtext.length > 0 && note.title.length == 0">
|
||||
{{ removeHtml(note.subtext) }}
|
||||
</span>
|
||||
<span class="thin-sub" v-else-if="note.subtext.length > 0">
|
||||
{{ removeHtml(note.subtext) }}
|
||||
</span>
|
||||
<span v-else-if="note.title.length == 0 && removeHtml(note.subtext).length == 0">
|
||||
Empty Note
|
||||
</span>
|
||||
|
||||
<!-- tags -->
|
||||
<span v-if="note.tags" class="thin-tags" >
|
||||
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- edited -->
|
||||
<span class="thin-right">
|
||||
{{$helpers.timeAgo( note.updated )}}
|
||||
|
||||
<i class="green link ellipsis vertical icon"></i>
|
||||
<!-- 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>
|
||||
|
||||
|
||||
|
||||
<!-- Toolbar on the bottom -->
|
||||
<div class="tool-bar" @click.self="cardClicked" v-if="!titleView">
|
||||
<div class="tool-bar" @click.self="cardClicked">
|
||||
<div class="icon-bar">
|
||||
<!-- {{$helpers.timeAgo(note.updated)}} -->
|
||||
|
||||
<span v-if="note.pinned == 1" data-position="top left" data-tooltip="Pinned" data-inverted>
|
||||
<i class="green pin icon"></i>
|
||||
</span>
|
||||
<span v-if="note.archived == 1" data-position="top left" data-tooltip="Archived" data-inverted>
|
||||
<i class="green archive icon"></i>
|
||||
</span>
|
||||
|
||||
<span v-if="note.tags">
|
||||
<span v-for="tag in (note.tags.split(','))" class="little-tag">{{ tag }}</span>
|
||||
</span>
|
||||
|
||||
<!-- :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }" -->
|
||||
<span class="float-right" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
|
||||
|
||||
<i class="teeny-button" data-tooltip="Tags" data-inverted v-on:click="toggleTags(true)">
|
||||
<i class="tags icon"></i>
|
||||
</i>
|
||||
|
||||
<i class="teeny-button"
|
||||
data-tooltip="Archive"
|
||||
:data-tooltip="note.archived ? 'Un-Archive':'Archive' "
|
||||
data-inverted v-on:click="archiveNote">
|
||||
<i class="archive icon"></i>
|
||||
</i>
|
||||
|
||||
<i class="teeny-button"
|
||||
:data-tooltip="note.pinned ? 'Un-Pin':'Pin' "
|
||||
data-inverted v-on:click="pinNote">
|
||||
<i class="pin icon"></i>
|
||||
</i>
|
||||
|
||||
<delete-button class="teeny-button" :note-id="note.id" />
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="getThumbs.length > 0">
|
||||
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
|
||||
<img v-for="thumb in getThumbs"
|
||||
class="tiny-thumb"
|
||||
:src="`/api/static/thumb_${thumb}`"
|
||||
onerror="
|
||||
this.onerror=null;
|
||||
this.src='/api/static/assets/marketing/void.svg';
|
||||
"
|
||||
/>
|
||||
<img v-for="thumb in getThumbs" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="icon-bar" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
|
||||
|
||||
<span class="time-ago-display">
|
||||
{{$helpers.timeAgo( note.updated )}}
|
||||
</span>
|
||||
|
||||
<span class="teeny-buttons">
|
||||
|
||||
<span v-if="!note.trashed">
|
||||
|
||||
<i class="teeny-button" data-tooltip="Tags" data-inverted v-on:click="toggleTags(true)">
|
||||
<i class="tags icon"></i>
|
||||
</i>
|
||||
|
||||
<i class="teeny-button"
|
||||
data-tooltip="Archive"
|
||||
:data-tooltip="note.archived ? 'Un-Archive':'Archive' "
|
||||
data-inverted v-on:click="archiveNote">
|
||||
<i class="archive icon" :class="{'green':note.archived}"></i>
|
||||
</i>
|
||||
|
||||
<i class="teeny-button"
|
||||
:data-tooltip="note.pinned ? 'Un-Pin':'Pin' "
|
||||
data-inverted v-on:click="pinNote">
|
||||
<i class="pin icon" :class="{'green':note.pinned}"></i>
|
||||
</i>
|
||||
|
||||
<i class="teeny-button"
|
||||
data-tooltip="Move to Trash"
|
||||
data-inverted v-on:click="trashNote()">
|
||||
<i class="trash icon"></i>
|
||||
</i>
|
||||
</span>
|
||||
|
||||
<!-- Trash note options -->
|
||||
<span v-if="note.trashed">
|
||||
<i class="teeny-button"
|
||||
data-tooltip="Un-Trash"
|
||||
data-inverted v-on:click="trashNote()">
|
||||
<i class="reply icon"></i>
|
||||
</i>
|
||||
<delete-button class="teeny-button" :note-id="note.id" />
|
||||
</span>
|
||||
</span>
|
||||
</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">
|
||||
<div class="ui basic segment">
|
||||
<note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/>
|
||||
</div>
|
||||
@@ -189,7 +133,7 @@
|
||||
|
||||
export default {
|
||||
name: 'NoteTitleDisplayCard',
|
||||
props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults', 'titleView' ],
|
||||
props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults' ],
|
||||
components: {
|
||||
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
|
||||
'note-tag-edit': require('@/components/NoteTagEdit.vue').default,
|
||||
@@ -201,17 +145,6 @@
|
||||
this.beenClicked = true
|
||||
this.onClick(this.note.id)
|
||||
},
|
||||
removeHtml(string){
|
||||
if(string == undefined || string == null || string.length == 0){
|
||||
return ''
|
||||
}
|
||||
|
||||
return string
|
||||
.replace(/&[[#A-Za-z0-9]+A-Za-z0-9]+;/g,' ') //Rip out all HTML entities
|
||||
.replace(/<[^>]+>/g, ' ') //Rip out all HTML tags
|
||||
.replace(/\s+/g, ' ') //Remove all whitespace
|
||||
.trim()
|
||||
},
|
||||
cleanHighlight(text){
|
||||
//Basically just remove whitespace
|
||||
let updated = text.replace(/ /g, '').replace(/<br>/g,'')
|
||||
@@ -219,60 +152,31 @@
|
||||
|
||||
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){
|
||||
return text.split(',')
|
||||
},
|
||||
openEditAttachment(){
|
||||
this.$router.push('/attachments/note/'+this.note.id)
|
||||
},
|
||||
pinNote(){ //togglePinned() <- old name
|
||||
this.showWorking = true
|
||||
this.note.pinned = this.note.pinned == 1 ? 0:1
|
||||
let postData = {'pinned': this.note.pinned, 'noteId':this.note.id}
|
||||
pinNote(){
|
||||
let postData = {'pinned': !this.note.pinned, 'noteId':this.note.id}
|
||||
axios.post('/api/note/setpinned', postData)
|
||||
.then(data => {
|
||||
this.showWorking = false
|
||||
// this event is triggered by the server after note is saved
|
||||
// this.$bus.$emit('update_single_note', this.note.id)
|
||||
this.$bus.$emit('update_single_note', this.note.id)
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Pin Note') })
|
||||
},
|
||||
archiveNote(){ //toggleArchived() <- old name
|
||||
|
||||
this.showWorking = true
|
||||
|
||||
archiveNote(){
|
||||
let postData = {'archived': !this.note.archived, 'noteId':this.note.id}
|
||||
axios.post('/api/note/setarchived', postData)
|
||||
.then(data => {
|
||||
|
||||
//Show message so no one worries where note went
|
||||
let message = 'Moved to Archive'
|
||||
if(postData.archived != 1){
|
||||
message = 'Moved out of Archive'
|
||||
}
|
||||
this.$bus.$emit('notification', message)
|
||||
this.$bus.$emit('update_single_note', this.note.id)
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Archive Note') })
|
||||
},
|
||||
trashNote(){ //toggleArchived() <- old name
|
||||
|
||||
this.showWorking = true
|
||||
|
||||
let postData = {'trashed': !this.note.trashed, 'noteId':this.note.id}
|
||||
axios.post('/api/note/settrashed', postData)
|
||||
.then(data => {
|
||||
|
||||
//Show message so no one worries where note went
|
||||
let message = 'Moved to Trash'
|
||||
if(postData.trashed == 0){
|
||||
message = 'Moved out of Trash'
|
||||
}
|
||||
this.$bus.$emit('notification', message)
|
||||
this.$bus.$emit('update_single_note', this.note.id)
|
||||
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Trash Note') })
|
||||
},
|
||||
toggleTags(state){
|
||||
|
||||
@@ -283,32 +187,6 @@
|
||||
}
|
||||
|
||||
},
|
||||
justClosed(){
|
||||
|
||||
// Dont do anything when not is closed.
|
||||
// Its already saved, this will make interface feel snappy
|
||||
|
||||
// Scroll note into view
|
||||
// this.$el.scrollIntoView({
|
||||
// behavior: 'smooth',
|
||||
// block: 'center',
|
||||
// inline: 'center'
|
||||
// })
|
||||
|
||||
// this.$bus.$emit('notification','Note Saved')
|
||||
|
||||
// //After scroll, trigger green outline animation
|
||||
// setTimeout(() => {
|
||||
|
||||
// this.triggerClosedAnimation = true
|
||||
// setTimeout(()=>{
|
||||
// //After 3 seconds, hide it
|
||||
// this.triggerClosedAnimation = false
|
||||
// }, 1500)
|
||||
|
||||
// }, 500)
|
||||
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -319,8 +197,6 @@
|
||||
iconColor: null,
|
||||
beenClicked: false,
|
||||
showTagSlideMenu: false,
|
||||
triggerClosedAnimation: false, //Show just closed animation
|
||||
showWorking: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -379,19 +255,6 @@
|
||||
</script>
|
||||
<style type="text/css">
|
||||
|
||||
.teeny-buttons {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
.time-ago-display {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tags {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.teeny-button {
|
||||
border: 1px solid var(--border_color);
|
||||
border-radius: 5px;
|
||||
@@ -401,21 +264,13 @@
|
||||
display: inline-block;
|
||||
min-width: 30px;
|
||||
color: var(--text_color);
|
||||
background-color: var(--small_element_bg_color);
|
||||
}
|
||||
.subtext {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
background-color: var(--background_color);
|
||||
}
|
||||
|
||||
/*Strict font sizes for card display*/
|
||||
.small-text {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
|
||||
/*font-size: 1.0em !important;*/
|
||||
font-size: 14px !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
.small-text > p, , .small-text > h1, .small-text > h2 {
|
||||
margin-bottom: 0.5em;
|
||||
@@ -423,7 +278,7 @@
|
||||
.big-text > p:first-child,
|
||||
.big-text > h1, .big-text > h2 {
|
||||
/*font-size: 1.3em !important;*/
|
||||
font-size: 20px !important;
|
||||
font-size: 17px !important;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
@@ -457,110 +312,43 @@
|
||||
|
||||
.note-title-display-card {
|
||||
position: relative;
|
||||
background-color: var(--small_element_bg_color);
|
||||
|
||||
/*The subtle shadow*/
|
||||
box-shadow: 2px 2px 6px 0 rgba(0,0,0,.15);
|
||||
transition: box-shadow, border-color ease 0.5s, transform linear 0.5s;
|
||||
/*box-shadow: 0 1px 2px 0 rgba(34,36,38,.15);*/
|
||||
/*box-shadow: 0 0px 5px 1px rgba(34,36,38,0);*/
|
||||
box-shadow: 0 1px 2px 0 rgba(34,36,38,.15);
|
||||
margin: 5px;
|
||||
/*padding: 0.7em 1em;*/
|
||||
border-radius: .28571429rem;
|
||||
border: 1px solid transparent;
|
||||
border: 1px solid;
|
||||
border-color: var(--border_color);
|
||||
/*width: calc(33.333% - 10px);*/
|
||||
width: calc(25% - 10px);
|
||||
/*min-width: 190px;*/
|
||||
/*min-height: 130px;*/
|
||||
/*transition: box-shadow 0.3s;*/
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
line-height: 1.8rem;
|
||||
letter-spacing: 0.05rem;
|
||||
letter-spacing: 0.02rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
text-align: left;
|
||||
|
||||
min-height: 100px;
|
||||
max-height: 450px;
|
||||
}
|
||||
.note-title-display-card:hover {
|
||||
box-shadow: 0 8px 15px rgba(0,0,0,0.3);
|
||||
border-color: var(--main-accent);
|
||||
/*box-shadow: 0 3px 6px -0 rgba(34,36,38,.50);*/
|
||||
/*box-shadow: 0 0px 5px 1px rgba(34,36,38,0.3);*/
|
||||
}
|
||||
.note-title-display-card.title-view {
|
||||
width: 100%;
|
||||
min-height: 20px;
|
||||
max-width: none;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
/*overflow: hidden;*/
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
/*box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);*/
|
||||
}
|
||||
.title-view + .title-view {
|
||||
border-top: 1px solid var(--border_color);
|
||||
}
|
||||
|
||||
.thin-container.single-line-text {
|
||||
width: calc(100% - 25px);
|
||||
/*margin: 5px 10px;*/
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.thin-container .thin-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.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 {
|
||||
display: inline-block;
|
||||
padding: 5px 10px 0;
|
||||
padding: 0 10px 0;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
background-color: rgba(200, 200, 200, 0.2);
|
||||
/*margin-top: -2.2rem;*/
|
||||
}
|
||||
.hover-hide {
|
||||
opacity: 0.0;
|
||||
transition: opacity ease 0.6s;
|
||||
}
|
||||
.little-tag {
|
||||
font-size: 0.7em;
|
||||
padding: 5px 5px;
|
||||
border: 1px solid var(--border_color);
|
||||
margin: 0 3px 5px 0;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
@@ -570,8 +358,6 @@
|
||||
line-height: 0.8em;
|
||||
text-overflow: ellipsis;
|
||||
float: left;
|
||||
color: var(--main-accent);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.tiny-thumb-box {
|
||||
max-height: 70px;
|
||||
@@ -615,9 +401,9 @@
|
||||
|
||||
|
||||
.one-column .note-title-display-card {
|
||||
/*margin-right: 65%;*/
|
||||
/*width: 33%;*/
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
/*margin: 0px -5px 10px -5px;*/
|
||||
}
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
@@ -629,7 +415,7 @@
|
||||
height: calc(100% + 30px);
|
||||
}
|
||||
.currently-open:after {
|
||||
content: '...';
|
||||
content: 'Open';
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
top: 0;
|
||||
@@ -651,117 +437,11 @@
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Break points determine when display cards shrink */
|
||||
@media only screen and (max-width: 700px) {
|
||||
/* Tweak mobile display to show only one column */
|
||||
@media only screen and (max-width: 740px) {
|
||||
.note-title-display-card {
|
||||
width: calc(100% + 10px);
|
||||
/*margin: 0px -5px 10px -5px;*/
|
||||
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 {
|
||||
width: calc(20% - 10px);
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1800px) {
|
||||
.note-title-display-card {
|
||||
width: calc(16.66666% - 10px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*Animations for cool border effects*/
|
||||
@keyframes bgin {
|
||||
0% {
|
||||
background-image:
|
||||
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* TopLeft to Right */
|
||||
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%), /* TopRight to Bottom */
|
||||
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* BottomLeft to Right*/
|
||||
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%); /* TopLeft to Bottom */
|
||||
/*Initial state, no BG*/
|
||||
background-size: 0 4px, 4px 0, 0 4px, 4px 0;
|
||||
}
|
||||
10% {
|
||||
/*Middre state, some filled */
|
||||
background-size: 100% 4px, 4px 0, 100% 4px, 4px 0;
|
||||
}
|
||||
20% {
|
||||
/*final state, all filled */
|
||||
background-size: 100% 4px, 4px 100%, 100% 4px, 4px 100%;
|
||||
}
|
||||
30% {
|
||||
background-size: 100% 4px, 4px 100%, 100% 4px, 4px 100%;
|
||||
background-image:
|
||||
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* TopLeft to Right */
|
||||
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%), /* TopRight to Bottom */
|
||||
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* BottomLeft to Right*/
|
||||
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%); /* TopLeft to Bottom */
|
||||
}
|
||||
100% {
|
||||
background-image:
|
||||
linear-gradient(to right, transparent 50%, transparent 100%), /* TopLeft to Right */
|
||||
linear-gradient(to bottom, transparent 50%, transparent 100%), /* TopRight to Bottom */
|
||||
linear-gradient(to right, transparent 50%, transparent 100%), /* BottomLeft to Right*/
|
||||
linear-gradient(to bottom, transparent 50%, transparent 100%); /* TopLeft to Bottom */
|
||||
}
|
||||
}
|
||||
|
||||
.bgboy {
|
||||
|
||||
overflow: hidden;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 0, 0 100%, 100% 0, 0 100%;
|
||||
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -6,70 +6,35 @@
|
||||
right: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
.floating-button {
|
||||
position: absolute;
|
||||
right: 7px;
|
||||
top: 5px;
|
||||
z-index: 2;
|
||||
}
|
||||
.floating-note-options {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 35px;
|
||||
z-index: 2;
|
||||
}
|
||||
.floating-note-options > .segment {
|
||||
border-top: none;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<span>
|
||||
|
||||
<div class="ui form" v-on:mouseenter="extraMenuHover = true" v-on:mouseleave="extraMenuHover = false">
|
||||
<div class="ui form" v-if="!$store.getters.getIsUserOnMobile">
|
||||
<!-- normal search menu -->
|
||||
<div class="ui left icon fluid input">
|
||||
<input ref="desktopSearch" v-on:blur="focused = false" v-on:focus="focused = true" v-model="searchTerm" @keydown="onKeyDown" @keyup="onKeyUp" placeholder="Search or Start Typing New Note" />
|
||||
<input v-model="searchTerm" @keyup="searchKeyUp" @keyup.enter="search" placeholder="Search Notes and Files" ref="searchInput"/>
|
||||
<i class="search icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="floating-button" v-if="searchTerm.length > 0">
|
||||
<i class="big link grey close icon" v-on:click="clear()"></i>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="floating-note-options"
|
||||
v-if="(searchTerm.length > 0 || tagSuggestions.length > 0) && (extraMenuHover)">
|
||||
<div class="ui segment">
|
||||
<div class="ui very compact grid">
|
||||
<div class="five wide column">
|
||||
<div class="ui mini green fluid compact shrinking button" v-on:click="search()">
|
||||
<i class="search icon"></i>Search
|
||||
</div>
|
||||
<span class="button-sub">
|
||||
( Enter )
|
||||
</span>
|
||||
</div>
|
||||
<div class="five wide middle aligned column">
|
||||
<div class="ui mini green fluid compact shrinking button" v-on:click="pushToNewNote()">
|
||||
<i class="plus icon"></i>A New Note
|
||||
</div>
|
||||
<span class="button-sub">
|
||||
( Tab )
|
||||
</span>
|
||||
</div>
|
||||
<div class="six wide right aligned column">
|
||||
<div class="ui mini green fluid compact shrinking button" v-on:click="pushToQuickNote()">
|
||||
<i class="sticky note outline icon"></i>The Scratch Pad
|
||||
</div>
|
||||
<span class="button-sub">
|
||||
( CTRL + ENTER )
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="ui basic icon button" v-on:click="openFloatingSearch" v-if="$store.getters.getIsUserOnMobile">
|
||||
<i class="green search icon"></i>
|
||||
</span>
|
||||
|
||||
<div class="fixed-search" v-if="showFixedSearch">
|
||||
<div class="ui raised segment">
|
||||
<h2 class="ui center aligned header">Search!</h2>
|
||||
<div class="ui form">
|
||||
<div class="ui left icon fluid input">
|
||||
<input
|
||||
ref="fixedSearch"
|
||||
v-model="searchTerm"
|
||||
@keyup.enter="search"
|
||||
v-on:blur="showFixedSearch = false"
|
||||
placeholder="Press Enter to Search" />
|
||||
<i class="search icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,142 +45,48 @@
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
|
||||
data: function(){
|
||||
return {
|
||||
searchTerm:'',
|
||||
searched: false,
|
||||
|
||||
tagSuggestions: [],
|
||||
tagSearchDebounce: null,
|
||||
|
||||
extraMenuHover: false,
|
||||
searchTerm: '',
|
||||
searchTimeout: null,
|
||||
searchDebounceDuration: 300,
|
||||
showFixedSearch: false,
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
},
|
||||
beforeMount(){
|
||||
mounted: function(){
|
||||
|
||||
//search clear
|
||||
this.$bus.$on('reset_fast_filters', () => {
|
||||
this.searchTerm = ''
|
||||
this.tagSuggestions = []
|
||||
})
|
||||
},
|
||||
beforeDestroy(){
|
||||
this.$bus.$off('reset_fast_filters')
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
},
|
||||
methods: {
|
||||
tagClick(tagId){
|
||||
openFloatingSearch(){
|
||||
this.showFixedSearch = !this.showFixedSearch
|
||||
|
||||
this.$emit('tagClick', tagId)
|
||||
this.tagSuggestions = []
|
||||
this.searchTerm = ''
|
||||
|
||||
},
|
||||
clear(){
|
||||
this.searched = false
|
||||
this.searchTerm = ''
|
||||
this.tagSuggestions = []
|
||||
if(!this.$store.getters.getIsUserOnMobile){
|
||||
this.$refs.desktopSearch.focus()
|
||||
if(this.showFixedSearch){
|
||||
this.$nextTick( () => {
|
||||
this.searchTerm = ''
|
||||
this.$refs.fixedSearch.focus()
|
||||
})
|
||||
}
|
||||
this.$bus.$emit('note_reload')
|
||||
},
|
||||
pushToQuickNote(){
|
||||
|
||||
const text = this.searchTerm
|
||||
this.searchTerm = ''
|
||||
this.tagSuggestions = []
|
||||
|
||||
axios.post('/api/quick-note/update', { 'pushText':text.trim() } )
|
||||
.then( response => {
|
||||
|
||||
this.$bus.$emit('notification', 'Saved To Scratch Pad')
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Update The Scratch Pad') })
|
||||
},
|
||||
pushToNewNote(){
|
||||
|
||||
const text = this.searchTerm
|
||||
this.searchTerm = ''
|
||||
this.tagSuggestions = []
|
||||
|
||||
axios.post('/api/note/create', { text })
|
||||
.then(response => {
|
||||
|
||||
if(response.data && response.data.id){
|
||||
this.$router.push('/notes/open/'+response.data.id)
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to create note') })
|
||||
},
|
||||
onKeyUp(event){
|
||||
//Search Tags
|
||||
const postData = {
|
||||
'tagText':this.searchTerm.trim()
|
||||
}
|
||||
|
||||
clearTimeout(this.tagSearchDebounce)
|
||||
|
||||
// if(this.searchTerm.length == 0){
|
||||
// this.tagSuggestions = []
|
||||
// return
|
||||
// }
|
||||
|
||||
// this.tagSearchDebounce = setTimeout(() => {
|
||||
// this.tagSuggestions = []
|
||||
// axios.post('/api/tag/suggest', postData)
|
||||
// .then( response => {
|
||||
|
||||
// this.tagSuggestions = response.data
|
||||
// })
|
||||
// .catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
|
||||
// }, 800)
|
||||
},
|
||||
onKeyDown(event){
|
||||
|
||||
//Tab
|
||||
if(event.keyCode == 9){
|
||||
this.pushToNewNote()
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
//Commant + Enter
|
||||
if((event.metaKey || event.ctrlKey) && event.keyCode == 13 ){
|
||||
this.pushToQuickNote()
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
//Enter
|
||||
if(event.keyCode == 13){
|
||||
searchKeyUp(){
|
||||
//This event is not triggered on mobile
|
||||
clearTimeout(this.searchTimeout)
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.search()
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
}, this.searchDebounceDuration)
|
||||
},
|
||||
search(){
|
||||
|
||||
if(this.searchTerm.length == 0){
|
||||
this.clear()
|
||||
return
|
||||
}
|
||||
|
||||
this.searched = true
|
||||
|
||||
this.$refs.desktopSearch.focus()
|
||||
//Blur after search on mobile
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
this.$refs.desktopSearch.blur()
|
||||
this.$refs.fixedSearch.blur()
|
||||
}
|
||||
|
||||
this.$bus.$emit('update_search_term', this.searchTerm)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,31 +5,7 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div class="ui grid" v-if="shareUsername == null">
|
||||
|
||||
<div v-if="!isNoteShared" class="sixteen wide column">
|
||||
<div class="ui button" v-on:click="makeShared()">Enable Sharing</div>
|
||||
<ul>
|
||||
<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 v-if="isNoteShared" class="sixteen wide column">
|
||||
<p>Generating a shared URL will expose the password of this note.</p>
|
||||
<div class="ui button" v-on:click="removeShared()">Remove Shared</div>
|
||||
|
||||
<div class="ui button" v-on:click="getSharedUrl()">Get Shareable URL</div>
|
||||
</div>
|
||||
<div class="sixteen wide column" v-if="isNoteShared && sharedUrl.length > 0">
|
||||
<p>Public Link - this link can be disabled by turning off sharing</p>
|
||||
<a target="_blank" :href="sharedUrl">{{ sharedUrl }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui grid" v-if="shareUsername == null">
|
||||
<div class="ui grid" v-if="this.shareUsername == null">
|
||||
|
||||
<div class="row">
|
||||
<div class="eight wide column">
|
||||
@@ -62,7 +38,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="ui grid" v-if="shareUsername != null">
|
||||
<div class="ui grid" v-if="this.shareUsername != null">
|
||||
<div class="sixteen wide column">
|
||||
Shared with you by <h3><i class="green user circle icon"></i>{{shareUsername}}</h3>
|
||||
</div>
|
||||
@@ -80,12 +56,10 @@
|
||||
props: [ 'noteId', 'rawTextId', 'shareUsername' ],
|
||||
data () {
|
||||
return {
|
||||
isNoteShared: false,
|
||||
sharedWithUsers: [],
|
||||
shareUserInput: '',
|
||||
debounce: null,
|
||||
enableSubmitShare: false,
|
||||
sharedUrl: '',
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
@@ -93,8 +67,6 @@
|
||||
},
|
||||
mounted(){
|
||||
|
||||
// this.isNoteShared = this.noteShared
|
||||
|
||||
if(this.shareUsername == null){
|
||||
this.loadShareList()
|
||||
}
|
||||
@@ -102,54 +74,19 @@
|
||||
},
|
||||
methods: {
|
||||
loadShareList(){
|
||||
axios.post('/api/note/getshareinfo', {'noteId':this.noteId, 'rawTextId':this.rawTextId })
|
||||
axios.post('/api/note/getshareusers', {'rawTextId':this.rawTextId })
|
||||
.then( ({data}) => {
|
||||
|
||||
this.isNoteShared = (data.shareStatus == 2)
|
||||
this.sharedWithUsers = data.shareUsers
|
||||
|
||||
this.sharedWithUsers = data
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Load Shared') })
|
||||
},
|
||||
makeShared(){
|
||||
axios.post('/api/note/enableshare', {'noteId':this.noteId })
|
||||
.then( ({data}) => {
|
||||
this.isNoteShared = true
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to fetch Shared URL') })
|
||||
},
|
||||
removeShared(){
|
||||
axios.post('/api/note/disableshare', {'noteId':this.noteId })
|
||||
.then( ({data}) => {
|
||||
this.isNoteShared = false
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to remove share status') })
|
||||
},
|
||||
getSharedUrl(){
|
||||
axios.post('/api/note/getsharekey', {'noteId':this.noteId })
|
||||
.then( ({data}) => {
|
||||
|
||||
const encodedKey = encodeURIComponent(data)
|
||||
|
||||
this.sharedUrl = `${window.location.protocol}//${window.location.hostname}/#/public/note/${this.noteId}/${encodedKey}`
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to fetch Shared URL') })
|
||||
},
|
||||
onRevokeAccess(sharedNoteId){
|
||||
|
||||
const postData = {
|
||||
'noteId': this.noteId,
|
||||
'shareUserNoteId': sharedNoteId
|
||||
}
|
||||
|
||||
axios.post('/api/note/shareremoveuser', postData)
|
||||
onRevokeAccess(noteId){
|
||||
axios.post('/api/note/shareremoveuser', {'noteId':noteId})
|
||||
.then( ({data}) => {
|
||||
console.log(data)
|
||||
if(data == true){
|
||||
this.loadShareList()
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Remove Share User') })
|
||||
},
|
||||
onKeyup(event){
|
||||
if(event.keyCode == 13){
|
||||
@@ -168,7 +105,6 @@
|
||||
this.$bus.$emit('notification', 'User not found')
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Add User') })
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<style type="text/css" scoped>
|
||||
.slide-container {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
right: 55%;
|
||||
bottom: 0;
|
||||
z-index: 1020;
|
||||
z-index: 400;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
color: var(--text_color);
|
||||
background-color: var(--small_element_bg_color);
|
||||
background-color: var(--background_color);
|
||||
}
|
||||
.slide-content {
|
||||
box-sizing: border-box;
|
||||
/*padding: 1em 1.5em;*/
|
||||
height: calc(100% - 43px);
|
||||
border-right: 1px solid var(--menu-border);
|
||||
/*background-color: var(--small_element_bg_color);*/
|
||||
/*background-color: var(--background_color);*/
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.slide-shadow {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
right: 50%;
|
||||
bottom: 0;
|
||||
color: red;
|
||||
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%);*/
|
||||
z-index: 1019;
|
||||
z-index: 399;
|
||||
overflow: hidden;
|
||||
/*cursor: pointer;*/
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slide-shadow.full-shadow {
|
||||
@@ -83,33 +83,33 @@
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<!-- <transition name="fade"> -->
|
||||
<transition name="fade">
|
||||
<div>
|
||||
|
||||
<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 -->
|
||||
<div class="slide-content">
|
||||
<slot></slot>
|
||||
</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 class="slide-shadow" :class="{'full-shadow':fullShadow}" v-on:click="close"></div>
|
||||
|
||||
</div>
|
||||
<!-- </transition> -->
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SideSlideMenu',
|
||||
props: [ 'name', 'styleObject', 'fullShadow', 'skipHistory' ],
|
||||
props: [ 'name', 'styleObject', 'fullShadow' ],
|
||||
components: {
|
||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||
},
|
||||
@@ -145,16 +145,15 @@
|
||||
|
||||
//Close all other panels that are not this one
|
||||
this.$nextTick( () => {
|
||||
// this.$bus.$emit('destroy_all_other_side_panels', this.name)
|
||||
this.$bus.$emit('destroy_all_other_side_panels', this.name)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
onClickTag(index){
|
||||
console.log('yup')
|
||||
},
|
||||
close() {
|
||||
if(this.skipHistory != true){
|
||||
this.$router.go(-1)
|
||||
}
|
||||
|
||||
this.$emit('close'); //Close menu via event
|
||||
this.$emit('close');
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
.img-row {
|
||||
height: 20vh;
|
||||
height: 30vh;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -87,24 +87,21 @@
|
||||
|
||||
},
|
||||
mounted(){
|
||||
this.loadImages()
|
||||
|
||||
axios.post('/api/attachment/search', {'attachmentType':'files', 'setSize':1000})
|
||||
.then( ({data}) => {
|
||||
|
||||
//Sort files into two categories
|
||||
data.forEach(file => {
|
||||
if(file['note_id'] == this.noteId){
|
||||
this.uploadedToNote.push(file)
|
||||
} else {
|
||||
this.files.push(file)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
loadImages(){
|
||||
axios.post('/api/attachment/search', {'attachmentType':'files', 'setSize':1000})
|
||||
.then( ({data}) => {
|
||||
|
||||
//Sort files into two categories
|
||||
data.forEach(file => {
|
||||
if(file['note_id'] == this.noteId){
|
||||
this.uploadedToNote.push(file)
|
||||
} else {
|
||||
this.files.push(file)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Load Attachments') })
|
||||
},
|
||||
onFileClick(file){
|
||||
|
||||
const imageCode = `<img alt="image" src="/api/static/thumb_${file.file_location}">`
|
||||
@@ -112,7 +109,7 @@
|
||||
this.$bus.$emit('new_file_upload', {noteId: this.noteId, imageCode})
|
||||
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
window.history.back();
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
close() {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<div class="button-fix">
|
||||
|
||||
|
||||
<!-- Dropdown Button -->
|
||||
<span v-if="activeTags.length == 0" v-on:click="openMenu()" class="ui basic button shrinking">
|
||||
<i class="green tags icon"></i>
|
||||
Tags
|
||||
<i class="caret down icon"></i>
|
||||
</span>
|
||||
<!-- Remove Tag button -->
|
||||
<span v-if="activeTags.length > 0" v-on:click="openMenu()" class="ui basic button shrinking">
|
||||
<i class="green tag icon"></i>
|
||||
{{ getActiveTag() }}
|
||||
<i class="caret right icon"></i>
|
||||
</span>
|
||||
|
||||
<!-- hidden dropdown menu -->
|
||||
<div class="dropdown-menu" v-if="menuOpen">
|
||||
<div class="ui raised segment">
|
||||
<div class="ui very tight grid">
|
||||
<div class="fourteen wide column">
|
||||
<h2 class="ui header"><i class="small green tags icon"></i>Tags</h2>
|
||||
</div>
|
||||
<div class="two wide middle aligned center aligned column" v-on:click="closeMenu()">
|
||||
<i class="link grey close icon"></i>
|
||||
</div>
|
||||
<div class="sixteen wide middle aligned column" v-if="loadedTags.length == 0">
|
||||
Tags added to Notes will appear here.
|
||||
</div>
|
||||
<div class="row hover-row" v-for="tag in loadedTags" v-on:click="onClick(tag.id)" :class="{'green':(activeTags[0] == tag.id)}">
|
||||
<div class="two wide center aligned column">
|
||||
<i class="grey tag icon"></i>
|
||||
</div>
|
||||
<div class="twelve wide column">
|
||||
{{ ucWords(tag.text) }}
|
||||
</div>
|
||||
<div class="two wide center aligned column">
|
||||
{{tag.usages}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shade" v-if="menuOpen" v-on:click="closeMenu()"></div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
export default {
|
||||
name: 'TagDisplay',
|
||||
props: [ 'activeTags' ],
|
||||
data () {
|
||||
return {
|
||||
loadedTags: [],
|
||||
menuOpen: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
},
|
||||
methods:{
|
||||
closeMenu(){
|
||||
this.menuOpen = false
|
||||
},
|
||||
openMenu(){
|
||||
this.menuOpen = true
|
||||
axios.post('/api/tag/usertags')
|
||||
.then( ({data}) => {
|
||||
this.loadedTags = data
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Tags') })
|
||||
},
|
||||
toggleActive(){
|
||||
this.menuOpen = false
|
||||
const current = this.activeTags[0]
|
||||
this.onClick( current )
|
||||
},
|
||||
onClick(tagId){
|
||||
this.menuOpen = false
|
||||
this.$emit('tagClick', tagId)
|
||||
},
|
||||
ucWords(str){
|
||||
return (str + '')
|
||||
.replace(/^(.)|\s+(.)/g, function ($1) {
|
||||
return $1.toUpperCase()
|
||||
})
|
||||
},
|
||||
getActiveTag(){
|
||||
let text = 'Tags'
|
||||
|
||||
if(this.activeTags.length == 0){
|
||||
return text
|
||||
}
|
||||
|
||||
this.loadedTags.forEach(tag => {
|
||||
if( this.activeTags.includes(tag.id) ){
|
||||
text = this.ucWords(tag.text)
|
||||
}
|
||||
})
|
||||
|
||||
return text
|
||||
},
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.button-fix {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
.hover-row:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--menu-accent);
|
||||
}
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
/*width: 70vw;*/
|
||||
top: 50px;
|
||||
z-index: 1005;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
/*min-width: 200px;*/
|
||||
/*max-width: 100%;*/
|
||||
width: 340px;
|
||||
text-align: left;
|
||||
}
|
||||
.dropdown-menu .button {
|
||||
margin: 0 5px 5px 0;
|
||||
}
|
||||
.shade {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1004;
|
||||
background-color: #0000008a;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@@ -1,120 +0,0 @@
|
||||
<style type="text/css" scoped>
|
||||
.colors {
|
||||
position: fixed;
|
||||
z-index: 1023;
|
||||
top: 35px;
|
||||
/*height: 100px;*/
|
||||
width: 400px;
|
||||
left: 20%;
|
||||
}
|
||||
.colors-container {
|
||||
/*max-width: 360px;*/
|
||||
display: flex;
|
||||
/*flex-direction: column;*/
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
align-content: stretch;
|
||||
|
||||
height: 250px;
|
||||
width: 100%;
|
||||
}
|
||||
.dot {
|
||||
/*display: inline-block;*/
|
||||
|
||||
border-radius: 30px;
|
||||
box-shadow: 0px 0px 0px 1px inset #3e3e3e;
|
||||
margin: 0 0 2px 2px;
|
||||
cursor: pointer;
|
||||
flex-basis: 9%;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.dot > i {
|
||||
margin: 9px 0 0 0;
|
||||
color: white;
|
||||
text-shadow:
|
||||
1px 1px 2px #3e3e3e,
|
||||
1px -1px 2px #3e3e3e,
|
||||
-1px 1px 2px #3e3e3e,
|
||||
-1px -1px 2px #3e3e3e
|
||||
;
|
||||
}
|
||||
.shade {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1022;
|
||||
background-color: transparent;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.big-shadow {
|
||||
box-shadow: 0px 4px 5px 1px #a8a8a8;
|
||||
}
|
||||
@media only screen and (max-width: 740px) {
|
||||
.colors {
|
||||
position: fixed;
|
||||
left: 5px;
|
||||
right: -5px;
|
||||
top: 5px;
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="colors">
|
||||
<div class="ui segment big-shadow">
|
||||
<h3>Select Text Color</h3>
|
||||
<div class="colors-container">
|
||||
<span
|
||||
v-for="(color,index) in colors"
|
||||
class="dot"
|
||||
v-on:click="onColorClick(index)"
|
||||
:style="`background-color: ${color};`">
|
||||
<i v-if="lastUsedColor == color" class="check icon"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shade" v-on:click="close"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
export default {
|
||||
components:{
|
||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||
},
|
||||
props: [ 'lastUsedColor' ],
|
||||
data: function(){
|
||||
return {
|
||||
hover: false,
|
||||
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)'
|
||||
],
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
},
|
||||
mounted: function(){
|
||||
},
|
||||
methods: {
|
||||
close(){
|
||||
this.$emit('close')
|
||||
},
|
||||
onColorClick(index){
|
||||
|
||||
this.$emit('color',this.colors[index])
|
||||
this.$nextTick( () => {
|
||||
this.close()
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -4,38 +4,20 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import Vuex from 'vuex'
|
||||
import 'es6-promise/auto' //Vuex likes promises
|
||||
import store from './stores/mainStore';
|
||||
|
||||
import App from './App'
|
||||
import router from './router'
|
||||
|
||||
//Include entire fomantic ui library
|
||||
// import 'fomantic-ui-css/semantic.css';
|
||||
|
||||
//Required site and reset CSS
|
||||
import 'fomantic-ui-css/components/reset.min.css'
|
||||
import 'fomantic-ui-css/components/site.css' //modified to remove included LATO fonts
|
||||
|
||||
//Only include parts that are used
|
||||
import 'fomantic-ui-css/components/button.min.css'
|
||||
import 'fomantic-ui-css/components/container.min.css'
|
||||
import 'fomantic-ui-css/components/form.min.css'
|
||||
import 'fomantic-ui-css/components/grid.min.css'
|
||||
import 'fomantic-ui-css/components/header.min.css'
|
||||
import 'fomantic-ui-css/components/icon.css' //Modified to remove brand icons
|
||||
import 'fomantic-ui-css/components/input.min.css'
|
||||
import 'fomantic-ui-css/components/segment.min.css'
|
||||
import 'fomantic-ui-css/components/label.min.css'
|
||||
|
||||
|
||||
//Overwrite and site styles and themes and good stuff
|
||||
import 'fomantic-ui-css/semantic.css';
|
||||
require('./assets/semantic-helper.css')
|
||||
// Fonts
|
||||
require('./assets/roboto-latin.woff2')
|
||||
require('./assets/roboto-latin-bold.woff2')
|
||||
|
||||
|
||||
|
||||
require('./assets/squire.js')
|
||||
|
||||
//Import socket io, init using nginx configured socket path
|
||||
import io from 'socket.io-client';
|
||||
@@ -53,7 +35,7 @@ Object.defineProperties(Vue.prototype, {
|
||||
// This callback runs before every route change, including on page load.
|
||||
// Sets the title of the page using vue router
|
||||
router.beforeEach((to, from, next) => {
|
||||
document.title = to.meta.title + ' - Solid Scribe';
|
||||
document.title = to.meta.title;
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -64,8 +46,11 @@ import Helpers from './Helpers'
|
||||
Vue.use(Vuex)
|
||||
Vue.config.productionTip = false
|
||||
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
||||
el: '#app',
|
||||
router,
|
||||
store,
|
||||
components: { App },
|
||||
template: '<App/>'
|
||||
})
|
||||
@@ -1,443 +0,0 @@
|
||||
const SquireButtonFunctions = {
|
||||
data(){
|
||||
return {
|
||||
//active button states
|
||||
activeBold: false,
|
||||
activeItalics: false,
|
||||
activeUnderline: false,
|
||||
activeTitle: false,
|
||||
activeList: false,
|
||||
activeToDo: false,
|
||||
activeColor: null,
|
||||
activeCode: false,
|
||||
activeSubTitle: false,
|
||||
//
|
||||
lastUsedColor: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
//
|
||||
// Inside squire init function
|
||||
//
|
||||
|
||||
pathChangeEvent(e){
|
||||
|
||||
//Reset all button states
|
||||
this.activeBold = false
|
||||
this.activeTitle = false
|
||||
this.activeItalics = false
|
||||
this.activeList = false
|
||||
this.activeToDo = false
|
||||
this.activeColor = null
|
||||
this.activeUnderline = false
|
||||
this.activeCode = false
|
||||
this.activeSubTitle = false
|
||||
|
||||
if(e.path.indexOf('>U>') > -1 || e.path.search(/U$/) > -1){
|
||||
this.activeUnderline = true
|
||||
}
|
||||
if(e.path.indexOf('>B>') > -1 || e.path.search(/B$/) > -1){
|
||||
this.activeBold = true
|
||||
}
|
||||
if(e.path.indexOf('>I') > -1){
|
||||
this.activeItalics = true
|
||||
}
|
||||
if(e.path.indexOf('fontSize=1.4em') > -1){
|
||||
this.activeTitle = true
|
||||
}
|
||||
if(e.path.indexOf('fontSize=0.9em') > -1){
|
||||
this.activeSubTitle = true
|
||||
}
|
||||
if(e.path.indexOf('OL>LI') > -1){
|
||||
this.activeList = true
|
||||
}
|
||||
if(e.path.indexOf('UL>LI') > -1){
|
||||
this.activeToDo = true
|
||||
}
|
||||
if(e.path.indexOf('CODE') > -1){
|
||||
this.activeCode= true
|
||||
}
|
||||
const colorIndex = e.path.indexOf('color=')
|
||||
if(colorIndex > -1){
|
||||
//Get all digigs after color index, then limit to 3
|
||||
let colors = e.path.substring(colorIndex).match(/\d+/g).slice(0,3)
|
||||
|
||||
const lastColor = `rgb(${colors.join(',')})`
|
||||
this.activeColor = lastColor
|
||||
this.lastUsedColor = lastColor
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
//Inside Squire Init
|
||||
//
|
||||
|
||||
removeFormatting(){
|
||||
this.selectLineIfNoSelect()
|
||||
this.editor.removeAllFormatting()
|
||||
},
|
||||
//If nothing is selected, select the entire line
|
||||
selectLineIfNoSelect(){
|
||||
|
||||
//Select entire line if range is not set
|
||||
let selection = this.editor.getSelection()
|
||||
|
||||
if(selection.startOffset == selection.endOffset && selection.startContainer == selection.endContainer){
|
||||
|
||||
let squireRange = this.editor.createRange(
|
||||
selection.startContainer, 0,
|
||||
selection.endContainer, selection.commonAncestorContainer.textContent.length)
|
||||
|
||||
this.editor.setSelection(squireRange)
|
||||
}
|
||||
},
|
||||
modifyFont(inSize){
|
||||
|
||||
this.selectLineIfNoSelect()
|
||||
|
||||
let fontInfo = this.editor.getFontInfo()
|
||||
//Toggle font size between large and normal
|
||||
if(fontInfo.size){
|
||||
this.editor.setFontSize(null)
|
||||
} else {
|
||||
this.editor.setFontSize(inSize)
|
||||
}
|
||||
},
|
||||
modifyColor(color){
|
||||
|
||||
this.selectLineIfNoSelect()
|
||||
//Set color of font
|
||||
this.editor.setTextColour(color)
|
||||
|
||||
this.lastUsedColor = color
|
||||
},
|
||||
applyLastUsedColor(){
|
||||
this.modifyColor(this.lastUsedColor)
|
||||
},
|
||||
toggleList(type){
|
||||
|
||||
//Undo list if its already a lits
|
||||
if(this.editor.hasFormat(type)){
|
||||
this.editor.removeList()
|
||||
return
|
||||
}
|
||||
|
||||
if(type == 'ol'){
|
||||
this.editor.makeOrderedList()
|
||||
}
|
||||
if(type == 'ul'){
|
||||
this.editor.makeUnorderedList()
|
||||
}
|
||||
},
|
||||
toggleUnderline(){
|
||||
this.selectLineIfNoSelect()
|
||||
if( this.editor.hasFormat('u') ){
|
||||
this.editor.removeUnderline()
|
||||
} else {
|
||||
this.editor.underline()
|
||||
}
|
||||
},
|
||||
toggleBold(){
|
||||
this.selectLineIfNoSelect()
|
||||
if( this.editor.hasFormat('b') ){
|
||||
this.editor.removeBold()
|
||||
} else {
|
||||
this.editor.bold()
|
||||
}
|
||||
},
|
||||
toggleItalic(){
|
||||
this.selectLineIfNoSelect()
|
||||
if( this.editor.hasFormat('i') ){
|
||||
this.editor.removeItalic()
|
||||
} else {
|
||||
this.editor.italic()
|
||||
}
|
||||
},
|
||||
modifyCode(){
|
||||
|
||||
this.selectLineIfNoSelect()
|
||||
|
||||
this.editor.toggleCode()
|
||||
},
|
||||
undoCustom(){
|
||||
//The same as pressing CTRL + Z
|
||||
// this.editor.focus()
|
||||
// document.execCommand("undo", false, null)
|
||||
this.editor.undo()
|
||||
},
|
||||
uncheckAllListItems(){
|
||||
//
|
||||
// Uncheck All List Items
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
this.$router.go(-1)
|
||||
|
||||
setTimeout(()=>{
|
||||
|
||||
Array.from( container.getElementsByClassName('active') ).forEach(item => {
|
||||
item.classList.remove('active');
|
||||
})
|
||||
|
||||
},600)
|
||||
|
||||
},
|
||||
deleteCompletedListItems(){
|
||||
//
|
||||
// Delete Completed List Items
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
//Close menu if user is on mobile, then sort list
|
||||
this.$router.go(-1)
|
||||
|
||||
setTimeout(()=>{
|
||||
|
||||
//Go through each item, on first level, look for Unordered Lists
|
||||
container.childNodes.forEach( (node) => {
|
||||
if(node.nodeName == 'UL'){
|
||||
|
||||
//Create two categories, done and not done list items
|
||||
let undoneElements = document.createDocumentFragment()
|
||||
|
||||
//Go through each item in each list we found
|
||||
node.childNodes.forEach( (checkListItem, index) => {
|
||||
|
||||
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
|
||||
if(checkListItem.nodeName == 'UL'){
|
||||
return
|
||||
}
|
||||
|
||||
//Check if list item has active class
|
||||
const checkedItem = checkListItem.classList.contains('active')
|
||||
|
||||
//Check if the next item is a list, Keep lists with intented items together
|
||||
let sublist = null
|
||||
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
|
||||
sublist = node.childNodes[index+1]
|
||||
}
|
||||
|
||||
//Push checked items and their sub lists to the done set
|
||||
if(!checkedItem){
|
||||
|
||||
undoneElements.appendChild( checkListItem.cloneNode(true) )
|
||||
if(sublist){
|
||||
undoneElements.appendChild( sublist.cloneNode(true) )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
//Remove all HTML from node, push unfinished items, then finished below them
|
||||
node.innerHTML = null
|
||||
node.appendChild(undoneElements)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}, 600)
|
||||
|
||||
|
||||
},
|
||||
sortList(){
|
||||
//
|
||||
// Sort list, checked at the bottom, unchecked at the top
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
//Close menu if user is on mobile
|
||||
this.$router.go(-1)
|
||||
|
||||
setTimeout(()=>{
|
||||
|
||||
//Go through each item, on first level, look for Unordered Lists
|
||||
container.childNodes.forEach( (node) => {
|
||||
if(node.nodeName == 'UL'){
|
||||
|
||||
//Create two categories, done and not done list items
|
||||
let doneElements = document.createDocumentFragment()
|
||||
let undoneElements = document.createDocumentFragment()
|
||||
|
||||
//Go through each item in each list we found
|
||||
node.childNodes.forEach( (checkListItem, index) => {
|
||||
|
||||
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
|
||||
if(checkListItem.nodeName == 'UL'){
|
||||
return
|
||||
}
|
||||
|
||||
//Check if list item has active class
|
||||
const checkedItem = checkListItem.classList.contains('active')
|
||||
|
||||
//Check if the next item is a list, Keep lists with intented items together
|
||||
let sublist = null
|
||||
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
|
||||
sublist = node.childNodes[index+1]
|
||||
}
|
||||
|
||||
//Push checked items and their sub lists to the done set
|
||||
if(checkedItem){
|
||||
|
||||
doneElements.appendChild( checkListItem.cloneNode(true) )
|
||||
if(sublist){
|
||||
doneElements.appendChild( sublist.cloneNode(true) )
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
undoneElements.appendChild( checkListItem.cloneNode(true) )
|
||||
if(sublist){
|
||||
undoneElements.appendChild( sublist.cloneNode(true) )
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
//Remove all HTML from node, push unfinished items, then finished below them
|
||||
node.innerHTML = null
|
||||
node.appendChild(undoneElements)
|
||||
node.appendChild(doneElements)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
},600)
|
||||
|
||||
|
||||
},
|
||||
calculateMath(){
|
||||
//
|
||||
// Find math in note and calculate the outcome
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
//Close menu if user is on mobile, then sort list
|
||||
this.$router.go(-1)
|
||||
|
||||
// simple function that trys to evaluate javascript
|
||||
const shittyMath = (string) => {
|
||||
//Remove all chars but math chars
|
||||
const cleanString = String(string).replace(/[a-zA-Z\s]*/g,'')
|
||||
try {
|
||||
return Function('"use strict"; return (' + cleanString + ')')();
|
||||
} catch (error) {
|
||||
console.log('Math Error: ', string)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(()=>{
|
||||
|
||||
//Go through each item, on first level, look for Unordered Lists
|
||||
container.childNodes.forEach( (node) => {
|
||||
|
||||
const line = node.innerText.trim()
|
||||
|
||||
// = sign exists and its the last character in the string
|
||||
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
|
||||
|
||||
//Pull out everything before the formula and try to evaluate it
|
||||
const formula = line.split('=').shift()
|
||||
const output = shittyMath(formula)
|
||||
|
||||
//If its a number and didn't throw an error, update the line
|
||||
if(!isNaN(output) && output != null){
|
||||
|
||||
//Since there is HTML in the line, splice in the number after the = sign
|
||||
let equalLocation = node.innerHTML.indexOf('=')
|
||||
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
|
||||
newLine += ` ${output}`
|
||||
newLine += node.innerHTML.slice(equalLocation+1).trim()
|
||||
|
||||
//Slam in that new HTML with the output
|
||||
node.innerHTML = newLine
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
},600)
|
||||
|
||||
|
||||
|
||||
},
|
||||
setText(inText){
|
||||
|
||||
|
||||
|
||||
this.editor.setHTML(inText)
|
||||
// this.noteText = this.editor._getHTML()
|
||||
// this.diffNoteText = this.editor._getHTML()
|
||||
|
||||
//Make sure all list items have draggable property
|
||||
let container = document.getElementById('squire-id')
|
||||
let listItems = container.getElementsByTagName('li')
|
||||
for(let itemIndex in listItems){
|
||||
// console.log(listItems[itemIndex])
|
||||
// listItems[itemIndex].setAttribute('draggable','true')
|
||||
}
|
||||
// console.log(listItems)
|
||||
|
||||
},
|
||||
getText(){
|
||||
|
||||
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()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default SquireButtonFunctions
|
||||
@@ -8,13 +8,6 @@
|
||||
<div class="content">
|
||||
Files
|
||||
<div class="sub header">Uploaded Files and Websites from notes.</div>
|
||||
<div class="sub header">
|
||||
<i class="green angle double up icon icon"></i>
|
||||
<router-link
|
||||
to="/bookmarklet">
|
||||
Push any website to solid scribe
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
@@ -43,32 +36,6 @@
|
||||
Other Files
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="$store.getters.totals && $store.getters.totals['archivedNotes']"
|
||||
exact-active-class="green"
|
||||
class="ui basic button shrinking"
|
||||
to="/attachments/type/archived">
|
||||
<i class="archive icon"></i>
|
||||
Archived
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="$store.getters.totals && $store.getters.totals['trashedNotes']"
|
||||
exact-active-class="green"
|
||||
class="ui basic button shrinking"
|
||||
to="/attachments/type/trashed">
|
||||
<i class="trash icon"></i>
|
||||
Trashed
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="$store.getters.totals && $store.getters.totals['sharedToNotes']"
|
||||
exact-active-class="green"
|
||||
class="ui basic button shrinking"
|
||||
to="/attachments/type/shared">
|
||||
<i class="send icon"></i>
|
||||
Show Shared
|
||||
</router-link>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="searchParams.noteId">
|
||||
@@ -132,11 +99,6 @@
|
||||
//Load more attachments on scroll
|
||||
window.addEventListener('scroll', this.onScroll)
|
||||
|
||||
this.$io.on('update_note_attachments', () => {
|
||||
this.reset()
|
||||
this.searchAttachments()
|
||||
})
|
||||
|
||||
//Mount notes on load if note ID is set
|
||||
this.searchAttachments()
|
||||
},
|
||||
@@ -144,8 +106,6 @@
|
||||
|
||||
//Remove scroll event on destroy
|
||||
window.removeEventListener('scroll', this.onScroll)
|
||||
|
||||
this.$io.removeListener('update_note_attachments')
|
||||
},
|
||||
watch:{
|
||||
$route (to, from){
|
||||
@@ -205,12 +165,6 @@
|
||||
this.searchParams.attachmentType = this.$route.params.type
|
||||
}
|
||||
|
||||
// include files from shared notes or selected notes
|
||||
this.searchParams.includeShared = false
|
||||
if(this.$route.params.type == 'shared'){
|
||||
this.searchParams.includeShared = true
|
||||
}
|
||||
|
||||
//Set noteId in if in URL
|
||||
if(this.$route.params.id){
|
||||
this.searchParams.noteId = this.$route.params.id
|
||||
@@ -240,7 +194,6 @@
|
||||
//Grab the next batch
|
||||
this.loadedAttachmentsOffset += results.data.length
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Attachments') })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,9 @@
|
||||
-webkit-animation: fadeorama 16s ease infinite;
|
||||
-moz-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 {
|
||||
width: 140px;
|
||||
height: auto;
|
||||
width: 50%;
|
||||
}
|
||||
.lightly-padded {
|
||||
margin-top: 10px;
|
||||
@@ -46,20 +22,17 @@
|
||||
font-size: 4rem;
|
||||
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 {
|
||||
animation:blinkingText 1.5s linear infinite;
|
||||
}
|
||||
@keyframes blinkingText {
|
||||
@keyframes blinkingText{
|
||||
0%{ opacity: 0.9; }
|
||||
50%{ opacity: 0; }
|
||||
100%{ opacity: 0.9; }
|
||||
}
|
||||
.subtext {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid white;
|
||||
border-right: 1px solid white;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
padding: 0 0 0 10px;
|
||||
@@ -102,357 +75,157 @@
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.home-main img {
|
||||
max-height: 250px;
|
||||
}
|
||||
.white-link {
|
||||
text-decoration: underline;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 740px) {
|
||||
.column > img {
|
||||
max-height: 125px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="lightly-padded home-main">
|
||||
<div class="lightly-padded">
|
||||
<div class="ui centered vertically divided stackable grid">
|
||||
|
||||
<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 -->
|
||||
<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">
|
||||
<!-- <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" />
|
||||
<img class="logo-display" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
|
||||
<br>
|
||||
Solid Scribe
|
||||
</h2>
|
||||
|
||||
<h3 class="subtext">
|
||||
A free, secure, online note taking application<i class="i cursor icon blinking"></i>
|
||||
Take Notes Like Never Before<i class="i cursor icon blinking"></i>
|
||||
</h3>
|
||||
<p class="green-text">Assuming you have never used a note application previously in your life.</p>
|
||||
|
||||
</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">
|
||||
</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>
|
||||
|
||||
<!-- Go to notes button -->
|
||||
<div class="row" v-if="$parent.loggedIn">
|
||||
<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">
|
||||
<i class="external alternate icon"></i>Go to Notes
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Small login form -->
|
||||
<div class="row" v-if="!$parent.loggedIn">
|
||||
<div class="sixteen wide middle algined column">
|
||||
<div class="ui text container">
|
||||
<h2>
|
||||
<i class="plug icon"></i>
|
||||
Sign Up Now - Only a Username and Password required
|
||||
</h2>
|
||||
<login-form :thin="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overview -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2 class="ui dividing header">Powerful text editing and privacy</h2>
|
||||
<h3>Easily edit, share and organize thousands of notes.</h3>
|
||||
<h3>Feel safe knowing no one can read your notes but you.</h3>
|
||||
<!-- <h3>Tools to organize and collaborate on thousands of notes while maintaining security and respecting your privacy.</h3> -->
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<svg-displayer file="idea" alt="Explosion of New Ideas" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- theme selector -->
|
||||
<div class="ui white row">
|
||||
<div class="sixteen wide middle aligned column">
|
||||
<div class="ui container">
|
||||
<h2 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 class="six wide column">
|
||||
<h2>Only you can read your notes. </h2>
|
||||
<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 class="middle aligned centered green row">
|
||||
<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 class="four wide right aligned column">
|
||||
<svg-displayer file="cloud" alt="Girl falling into the spiral of digital chaos" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="four wide right aligned column">
|
||||
<svg-displayer file="robot" alt="Murder Robot in office environment" />
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Secure Data Sharing</h2>
|
||||
<h3>Share notes with friends without compromising privacy. The data remains encrypted with a shared password for you and people you invite to view it.</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- set -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2>Leave your Ad Blockers turned on</h2>
|
||||
<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>
|
||||
<h2>Everyone has knowledge that need to be expressed</h2>
|
||||
<h3>Utilize action potential to create notes by encoding raw brainwaves converted to written language</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 class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/idea.svg" alt="Explosion of New Ideas">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/gardening.svg" alt="Pruning the mind garden">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Dream it, then do it</h2>
|
||||
<h3>Easily record your unlimited imagination. Ideas, stories, notes, plays, poems anything, that can reasonably be put into text</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- set -->
|
||||
<div class="middle aligned centered green row">
|
||||
<div class="six wide column">
|
||||
<h2>Unbridled Input</h2>
|
||||
<h3>Revolutionary technology allows the use of any keyboard with up to 395 keys</h3>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/add.svg" alt="A shpere of newness">
|
||||
</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/add.svg" alt="A shpere of newness">
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/solution.svg" alt="Hypercube of Solutions">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Solutions with the Internet</h2>
|
||||
<h3>With the power to save any combination of letters, you can easily inscribe thoughts</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- set -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2>Search your data</h2>
|
||||
<h3>Type in a word and find that same word but somewhere else</h3>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide 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>Embrace the Void</h2>
|
||||
<h3>Remove unnecessary clutter for your brain and save it to the cloud, allowing you to easily embrace the gaping abyss</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- set -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2>Space for Growth</h2>
|
||||
<h3>Groom a clear path for new expressions and innovations. Elevate your being and lower your cholesterol</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 column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="Shrunken man near giant tablet">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Become your Data</h2>
|
||||
<h3>We exist as electrical impulses, no different from data on a computer</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- set -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2>Ice Cream</h2>
|
||||
<h3>Get excited without all the screaming</h3>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/icecream.svg" alt="Emergence of a 4th dimensional being perceived as a large ice cream ">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Data Backups</h2>
|
||||
@@ -461,14 +234,14 @@
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide right aligned column">
|
||||
<div class="six wide column">
|
||||
<h2>Freedom to unleash yourself</h2>
|
||||
<h3>Imagine an awakening of what could be</h3>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/grandma.svg" alt="Drinking the blood of the elderly">
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- final slide -->
|
||||
<div class="middle aligned centered green row">
|
||||
@@ -485,47 +258,36 @@
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
OR
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<span class="ui button" v-on:click="showRealInformation">View real information about this site</span>
|
||||
</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">
|
||||
<h3>
|
||||
<a target="_blank" href="https://www.maxg.cc">Solid Scribe was created by Max Gialanella</a>
|
||||
</h3>
|
||||
<p><a target="_blank" href="https://www.maxg.cc">Check out my Resume</a></p>
|
||||
<p>OR</p>
|
||||
<p><a target="_blank" href="http://blog.maxg.cc">Check out my Programming Blog</a></p>
|
||||
<h2 class="ui center aligned">
|
||||
What is this really?
|
||||
</h2>
|
||||
<h3>Its just a little web app for taking notes. This page is mocking the "over the top" marketing sites use to sell their products.</h3>
|
||||
<p>
|
||||
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>
|
||||
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>
|
||||
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>Email me at <a href="mailto:maxgialanella@pm.me">Max.Gialanella@pm.me</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>
|
||||
<p>Generic Marketing Images - <a target="_blank" href="https://undraw.co/">https://unDraw.co/</a></p>
|
||||
</div>
|
||||
<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 class="center aligned sixteen wide column">
|
||||
<router-link to="/terms">Solid Scribe Terms of Use</router-link>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -534,30 +296,10 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'WelcomePage',
|
||||
components: {
|
||||
'login-form':require('@/components/LoginFormComponent.vue').default,
|
||||
'logo':require('@/components/LogoComponent.vue').default,
|
||||
'svg-displayer':require('@/components/SvgDisplayer.vue').default,
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
height: null,
|
||||
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(){
|
||||
@@ -569,42 +311,14 @@ export default {
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
//Don't change hero banner on mobile
|
||||
if(!this.$store.getters.getIsUserOnMobile){
|
||||
let windowHeight = window.innerHeight
|
||||
this.height = windowHeight - (windowHeight * 0.10)
|
||||
}
|
||||
|
||||
},
|
||||
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(){
|
||||
|
||||
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
<template>
|
||||
<div class="ui basic segment no-fluf-segment">
|
||||
<div class="ui stackable grid">
|
||||
|
||||
<div class="sixteen wide center aligned column">
|
||||
<h2>Login / Register</h2>
|
||||
</div>
|
||||
|
||||
<div class="ui grid">
|
||||
<div class="ui sixteen wide column">
|
||||
<div class="ui text container">
|
||||
|
||||
<p><b>Create an account:</b> type in the username you want to use followed by the password.</p>
|
||||
|
||||
<div class="ui segment">
|
||||
|
||||
<h4 class="ui header">
|
||||
<i class="plug icon"></i>
|
||||
<div class="content">
|
||||
To Register
|
||||
<div class="sub header">Choose Any Username & password</div>
|
||||
<div class="ui segment" v-on:keyup.enter="submit">
|
||||
<div class="ui large form">
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="username" type="text" name="email" placeholder="Username or E-mail address" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
|
||||
<login-form />
|
||||
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="password" type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="submit" class="ui massive compact fluid green submit button">Login</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>You will remain logged in on this browser, for 20 days or until you log out.</p>
|
||||
</div>
|
||||
<p>You will remain logged in on this browser, until you log out.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,16 +34,51 @@
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
components: {
|
||||
'login-form':require('@/components/LoginFormComponent.vue').default,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
enabled: false,
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit(){
|
||||
|
||||
//Both fields are required
|
||||
if(this.username <= 0){
|
||||
return false
|
||||
}
|
||||
if(this.password <= 0){
|
||||
return false
|
||||
}
|
||||
|
||||
let vm = this
|
||||
|
||||
let data = {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
}
|
||||
|
||||
axios.post('/api/user/login', data)
|
||||
.then(response => {
|
||||
if(response.data.success){
|
||||
|
||||
const token = response.data.token
|
||||
const username = response.data.username
|
||||
|
||||
vm.$store.commit('setLoginToken', {token, username})
|
||||
|
||||
//Redirect user to notes section after login
|
||||
vm.$router.push('/notes')
|
||||
} else {
|
||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
||||
vm.$store.commit('destroyLoginToken')
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div class="ui basic segment">
|
||||
<div class="ui grid">
|
||||
<div class="sixteen wide column">
|
||||
<div class="ui text container">
|
||||
|
||||
<h2 class="ui dividing header">
|
||||
Page Not Found
|
||||
</h2>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NotFoundPage',
|
||||
props:[ 'message' ],
|
||||
data () {
|
||||
return {
|
||||
items: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -4,32 +4,31 @@
|
||||
|
||||
<div class="ui sixteen wide column">
|
||||
<h2 class="ui header">
|
||||
<i class="sticky note outline icon"></i>
|
||||
<i class="paper plane outline icon"></i>
|
||||
<div class="content">
|
||||
The Scratch Pad
|
||||
<div class="sub header">One place to put random junk</div>
|
||||
Quick
|
||||
<div class="sub header">Rapidly save text</div>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide middle aligned column" v-if="quickNoteId > 0">
|
||||
<div class="sixteen wide middle aligned column">
|
||||
|
||||
<div class="ui compact basic button"
|
||||
v-on:click="enterToSubmit = !enterToSubmit">
|
||||
<i v-if="enterToSubmit" class="green toggle on icon"></i>
|
||||
<i v-else class="toggle off icon"></i>
|
||||
|
||||
<span v-if="enterToSubmit">Save after Enter press</span>
|
||||
<span v-else>CTRL + Enter to Save</span>
|
||||
|
||||
<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">
|
||||
<i class="sync alternate reload icon"></i>
|
||||
New Scratch Pad
|
||||
</div>
|
||||
<div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="showNewNoteConfirm = false">
|
||||
<i class="close icon"></i>
|
||||
Cancel
|
||||
</div>
|
||||
<div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="newQuickNote()">
|
||||
<i class="green thumbs up icon"></i>
|
||||
Confirm
|
||||
<div class="ui compact basic button"
|
||||
v-on:click="pasteToSubmit = !pasteToSubmit">
|
||||
<i v-if="pasteToSubmit" class="green check circle outline icon"></i>
|
||||
<i v-else class="circle outline icon"></i>
|
||||
Save after Pasting
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,24 +41,25 @@
|
||||
ref="fastInput"
|
||||
v-model="newText"
|
||||
v-on:keydown="checkKeyup"
|
||||
v-on:paste="onPaste"
|
||||
placeholder="Push to the top of the quick note."
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div v-on:click="appendQuickNote" class="ui green button">Save (Enter)</div>
|
||||
<div v-on:click="appendQuickNote" class="ui green button">Save</div>
|
||||
<div v-if="quickNoteId" class="ui right floated basic button" v-on:click="$router.push('/attachments/note/'+quickNoteId)">
|
||||
<i class="folder open outline icon"></i>
|
||||
Files
|
||||
</div>
|
||||
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button">
|
||||
<i class="file outline icon"></i>
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="one wide column"></div>
|
||||
<div class="fourteen wide column">
|
||||
<div class="note-card-text" v-html="savedQuickNoteText"></div>
|
||||
</div>
|
||||
<div class="one wide column"></div>
|
||||
<div class="fun" v-html="savedQuickNoteText"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -79,7 +79,8 @@
|
||||
newText: '',
|
||||
savedQuickNoteText: '',
|
||||
quickNoteId: null,
|
||||
showNewNoteConfirm: false,
|
||||
pasteToSubmit: true,
|
||||
enterToSubmit: true,
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
@@ -88,16 +89,6 @@
|
||||
//
|
||||
this.$parent.loginGateway()
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
this.$io.on('new_note_created', noteId => {
|
||||
this.getQuickNote()
|
||||
})
|
||||
|
||||
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
|
||||
this.getQuickNote()
|
||||
})
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
if(this.$refs.fastInput){
|
||||
@@ -116,17 +107,6 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
newQuickNote(){
|
||||
|
||||
this.showNewNoteConfirm = false
|
||||
|
||||
axios.post('/api/quick-note/new')
|
||||
.then( ({data}) => {
|
||||
this.savedQuickNoteText = ''
|
||||
this.quickNoteId = ''
|
||||
})
|
||||
|
||||
},
|
||||
openNoteEdit(){
|
||||
this.$router.push({'path':'/notes/open/'+this.quickNoteId})
|
||||
},
|
||||
@@ -139,10 +119,17 @@
|
||||
element.style.height = (element.scrollHeight + padding) +'px';
|
||||
|
||||
//Enter Key submits by default
|
||||
if(event.keyCode == 13){
|
||||
if(event.keyCode == 13 && this.enterToSubmit == true){
|
||||
this.appendQuickNote()
|
||||
return
|
||||
}
|
||||
|
||||
//Alternate submit
|
||||
//If command+enter or control+enter is pressed, submit
|
||||
if((event.metaKey || event.ctrlKey) && [13].includes(event.keyCode) && this.enterToSubmit == false){
|
||||
this.appendQuickNote()
|
||||
return
|
||||
}
|
||||
},
|
||||
appendQuickNote(){
|
||||
|
||||
@@ -155,16 +142,25 @@
|
||||
this.newText = '' //Clear text area
|
||||
this.$refs.fastInput.style.height = 'auto' //Back to normal size
|
||||
|
||||
this.savedQuickNoteText = results.data.text
|
||||
this.quickNoteId = results.data.id
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Update Quick Note') })
|
||||
},
|
||||
getQuickNote(){
|
||||
getQuickNote (){
|
||||
axios.post('/api/quick-note/get')
|
||||
.then( ({data}) => {
|
||||
this.savedQuickNoteText = data.text
|
||||
this.quickNoteId = data.id
|
||||
.then( results => {
|
||||
this.savedQuickNoteText = results.data.text
|
||||
this.quickNoteId = results.data.id
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Quick Note') })
|
||||
},
|
||||
onPaste(event){
|
||||
|
||||
if(this.pasteToSubmit == true){
|
||||
setTimeout( () => {
|
||||
this.appendQuickNote()
|
||||
}, 10)
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -1,66 +1,9 @@
|
||||
<template>
|
||||
<div class="ui grid">
|
||||
|
||||
<div class="sixteen wide column"></div>
|
||||
|
||||
<div class="sixteen wide column" v-if="text.length > 0 || title.length > 0">
|
||||
<div class="ui text container">
|
||||
|
||||
<div class="ui segment" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
|
||||
|
||||
<h1 v-if="title">{{title}}</h1>
|
||||
|
||||
<div v-if="text" v-html="text" class="squire-box"></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="ui basic segment">
|
||||
<div class="ui container">
|
||||
<div class="fun" :style="{'color':color}" v-if="noteText" v-html="noteText"></div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="!$store.getters.getLoggedIn">
|
||||
<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">
|
||||
<div class="content">
|
||||
Solid Scribe is an easy, free, secure Note App
|
||||
<div class="sub header">
|
||||
Encrypted notes, only readable by you. Unless you share them.
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<!-- buttons -->
|
||||
<div class="ui grid">
|
||||
<div class="eight wide center aligned column">
|
||||
<router-link class="ui compact green button" to="/login">
|
||||
<i class="plug icon"></i>Sign Up
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="eight wide center aligned column">
|
||||
<router-link class="ui compact green button" to="/">
|
||||
<i class="comment outline icon"></i>
|
||||
Learn More
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui sixteen wide center aligned column">
|
||||
<h4>{{ failText }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="ui basic segment"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -72,51 +15,35 @@
|
||||
name: 'SharePage',
|
||||
data(){
|
||||
return {
|
||||
title: '',
|
||||
text: '',
|
||||
failText: '',
|
||||
styleObject:{},
|
||||
noteText: null,
|
||||
color: '#000'
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
//You can put something here for live updates
|
||||
// this.$io.on
|
||||
|
||||
this.openNote()
|
||||
|
||||
//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)
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
fail(){
|
||||
this.failText = 'Failed to open Shared Note'
|
||||
this.$bus.$emit('notification', 'Failed to Open Shared Note')
|
||||
},
|
||||
openNote(){
|
||||
openNote(noteId){
|
||||
axios.post('/api/public/note', {'noteId': noteId})
|
||||
.then( response => {
|
||||
|
||||
const noteId = this.$route.params.id
|
||||
const sharedKey = this.$route.params.token
|
||||
let colors = JSON.parse(response.data.color)
|
||||
|
||||
axios.post('/api/public/opensharednote', {noteId, sharedKey})
|
||||
.then( ({data}) => {
|
||||
|
||||
if(data.success){
|
||||
this.title = data.title
|
||||
this.text = data.text
|
||||
this.styleObject = data.styleObject
|
||||
} else {
|
||||
this.fail()
|
||||
if(colors && colors.noteBackground){
|
||||
document.body.style.background = colors.noteBackground
|
||||
}
|
||||
if(colors && colors.noteText){
|
||||
this.color = colors.noteText
|
||||
}
|
||||
|
||||
this.noteText = response.data.text
|
||||
})
|
||||
.catch(error => { this.fail() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style type="text/css" scoped>
|
||||
.small-logo {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
File diff suppressed because one or more lines are too long
@@ -2,19 +2,22 @@ import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
|
||||
//Breaking components into function sections allows webpack to load them dynamically
|
||||
//import HomePage from '@/pages/HomePage'
|
||||
const HomePage = () => import('@/pages/HomePage')
|
||||
|
||||
const HomePage = () => import(/* webpackChunkName: "HomePage" */ '@/pages/HomePage')
|
||||
const LoginPage = () => import(/* webpackChunkName: "LoginPage" */ '@/pages/LoginPage')
|
||||
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 NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage')
|
||||
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
|
||||
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
|
||||
const OverviewPage = () => import(/* webpackChunkName: "OverviewPage" */ '@/pages/OverviewPage')
|
||||
const BookmarkletPage = () => import(/* webpackChunkName: "BookmarkletPage" */ '@/pages/BookmarkletPage')
|
||||
const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
|
||||
// import LoginPage from '@/pages/LoginPage'
|
||||
const LoginPage = () => import('@/pages/LoginPage')
|
||||
|
||||
// import HelpPage from '@/pages/HelpPage'
|
||||
const HelpPage = () => import('@/pages/HelpPage')
|
||||
|
||||
// import SharePage from '@/pages/SharePage'
|
||||
const SharePage = () => import('@/pages/SharePage')
|
||||
|
||||
//These guys can all be loaded as a chunk
|
||||
import NotesPage from '@/pages/NotesPage'
|
||||
import QuickPage from '@/pages/QuickPage'
|
||||
import AttachmentsPage from '@/pages/AttachmentsPage'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
@@ -34,27 +37,15 @@ export default new Router({
|
||||
},
|
||||
{
|
||||
path: '/notes',
|
||||
name: 'Note Page', //don't change this
|
||||
name: 'NotesPage',
|
||||
meta: {title:'Notes'},
|
||||
component: NotesPage
|
||||
},
|
||||
{
|
||||
path: '/notes/open/:id',
|
||||
name: 'Open Note',
|
||||
meta: {title: 'Open Note'},
|
||||
component: NotesPage,
|
||||
},
|
||||
{
|
||||
path: '/search/tags/:tag',
|
||||
name: 'Search Notes',
|
||||
meta: {title: 'Search Notes'},
|
||||
component: NotesPage,
|
||||
},
|
||||
{
|
||||
path: '/notes/open/:id/menu/:openMenu',
|
||||
name: 'Open Note Menu',
|
||||
meta: {title: 'Open Note Menu'},
|
||||
component: NotesPage,
|
||||
name: 'NotesPage',
|
||||
meta: {title:'Notes'},
|
||||
component: NotesPage
|
||||
},
|
||||
{
|
||||
path: '/help',
|
||||
@@ -63,25 +54,7 @@ export default new Router({
|
||||
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: '/share/:id',
|
||||
name: 'Share',
|
||||
meta: {title:'Shared'},
|
||||
component: SharePage
|
||||
@@ -89,7 +62,7 @@ export default new Router({
|
||||
{
|
||||
path: '/quick',
|
||||
name: 'Quick',
|
||||
meta: {title:'Scratch Pad'},
|
||||
meta: {title:'Quick'},
|
||||
component: QuickPage
|
||||
},
|
||||
{
|
||||
@@ -110,24 +83,5 @@ export default new Router({
|
||||
meta: {title:'Attachments by Type'},
|
||||
component: AttachmentsPage
|
||||
},
|
||||
{
|
||||
path: '/overview',
|
||||
name: 'Overview of Notes',
|
||||
meta: {title:'Overview of Notes'},
|
||||
component: OverviewPage
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
name: 'Page Not Found',
|
||||
meta: {title:'404 Page Not Found'},
|
||||
component: NotFoundPage
|
||||
},
|
||||
// Cycle Tracking
|
||||
{
|
||||
path: '/metrictrack',
|
||||
name: 'Metric Tracking',
|
||||
meta: {title:'Metric Tracking'},
|
||||
component: () => import(/* webpackChunkName: "MetrictrackingPage" */ '@/pages/MetrictrackingPage')
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
state: {
|
||||
token: null,
|
||||
username: null,
|
||||
nightMode: false,
|
||||
isUserOnMobile: false,
|
||||
fetchTotalsTimeout: null,
|
||||
userTotals: null, // {} // setting this to object breaks reactivity
|
||||
activeSessions: 0,
|
||||
isNoteSettingsOpen: false, //Little note settings pane
|
||||
socket: null,
|
||||
userTotals: null,
|
||||
},
|
||||
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.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
|
||||
},
|
||||
destroyLoginToken(state){
|
||||
@@ -25,63 +37,38 @@ export default new Vuex.Store({
|
||||
//Remove login token from local storage and from headers
|
||||
localStorage.removeItem('loginToken')
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('currentVersion')
|
||||
localStorage.removeItem('snippetCache')
|
||||
delete axios.defaults.headers.common['authorizationtoken']
|
||||
state.token = null
|
||||
state.username = null
|
||||
state.userTotals = null
|
||||
},
|
||||
toggleNightMode(state, pastTheme){
|
||||
|
||||
const themes = {
|
||||
'white':{
|
||||
'body_bg_color': '#f1f1f1',//'#f5f6f7',
|
||||
'small_element_bg_color': '#fff',
|
||||
'text_color': '#3d3d3d',
|
||||
'dark_border_color': '#d9d9d9',//'#DFE1E6',
|
||||
'border_color': '#DFE1E6',
|
||||
'menu-accent': '#cecece',
|
||||
'menu-text': '#5e6268',
|
||||
},
|
||||
'black':{
|
||||
'body_bg_color': 'rgb(12 4 30)',
|
||||
//'#0f0f0f',//'#000',
|
||||
'small_element_bg_color': '#000',
|
||||
'text_color': '#FFF',
|
||||
'dark_border_color': '#555',//'#ACACAC', //Lighter color to accent elemnts user can interact with
|
||||
'border_color': '#505050',
|
||||
'menu-accent': '#626262',
|
||||
'menu-text': '#d9d9d9',
|
||||
},
|
||||
}
|
||||
|
||||
//Catch values not in set
|
||||
|
||||
const totalThemes = Object.keys(themes).length
|
||||
state.nightMode++
|
||||
if(state.nightMode > totalThemes-1){
|
||||
state.nightMode = 0
|
||||
}
|
||||
if(pastTheme != null){
|
||||
state.nightMode = pastTheme
|
||||
}
|
||||
|
||||
//Final catch for numbers
|
||||
if(Number.isInteger(parseInt(state.nightMode)) == false){
|
||||
state.nightMode = 0
|
||||
}
|
||||
|
||||
const currentTheme = Object.keys(themes)[state.nightMode]
|
||||
toggleNightMode(state){
|
||||
|
||||
//Toggle state and save to local storage
|
||||
state.nightMode = !(state.nightMode)
|
||||
localStorage.setItem('nightMode', state.nightMode)
|
||||
|
||||
//Default theme colors
|
||||
let themeColors = {
|
||||
'background_color': '#fff',
|
||||
'text_color': '#3d3d3d',
|
||||
'outline_color': 'rgba(34,36,38,0.15)',
|
||||
'border_color': 'rgba(34,36,38,0.20)',
|
||||
}
|
||||
//Night mode colors
|
||||
if(state.nightMode){
|
||||
themeColors = {
|
||||
'background_color': '#000',
|
||||
'text_color': '#a98457',
|
||||
'outline_color': '#a98457',
|
||||
'border_color': 'rgba(255, 255, 255, 0.31)',
|
||||
}
|
||||
}
|
||||
|
||||
//Go through each color and set CSS variable
|
||||
let root = document.documentElement
|
||||
Object.keys( themes[currentTheme] ).forEach( attribute => {
|
||||
root.style.setProperty('--'+attribute, themes[currentTheme][attribute])
|
||||
Object.keys(themeColors).forEach( attribute => {
|
||||
root.style.setProperty('--'+attribute, themeColors[attribute])
|
||||
})
|
||||
|
||||
},
|
||||
detectIsUserOnMobile(state){
|
||||
|
||||
@@ -94,6 +81,9 @@ export default new Vuex.Store({
|
||||
}
|
||||
})(navigator.userAgent||navigator.vendor||window.opera, state);
|
||||
},
|
||||
toggleNoteSettingsPane(state){
|
||||
state.isNoteSettingsOpen = !state.isNoteSettingsOpen
|
||||
},
|
||||
setSocketIoSocket(state, socket){
|
||||
|
||||
//Put socket id in axios headers
|
||||
@@ -101,59 +91,24 @@ export default new Vuex.Store({
|
||||
state.socket = socket
|
||||
},
|
||||
setUserTotals(state, totalsObject){
|
||||
//Save all the totals for the user
|
||||
state.userTotals = totalsObject
|
||||
|
||||
if(!state.userTotals){
|
||||
state.userTotals = {}
|
||||
}
|
||||
|
||||
// retain old values loaded on initial, extended options load
|
||||
let oldMissingValues = {}
|
||||
Object.keys(state.userTotals).forEach(key => {
|
||||
if(!totalsObject[key] && totalsObject[key] !== 0){
|
||||
oldMissingValues[key] = state.userTotals[key]
|
||||
}
|
||||
})
|
||||
|
||||
// combine old settings with updated settings
|
||||
let oldAndNew = Object.assign(oldMissingValues, totalsObject)
|
||||
|
||||
state.userTotals = oldAndNew
|
||||
|
||||
//Set computer version from server
|
||||
const currentVersion = localStorage.getItem('currentVersion')
|
||||
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('-------------')
|
||||
// Object.keys(totalsObject).forEach( key => {
|
||||
// console.log(key + ' -- ' + totalsObject[key])
|
||||
// })
|
||||
},
|
||||
setActiveSessions(state, countData){
|
||||
//Count of the number of active socket.io sessions for this user
|
||||
state.activeSessions = countData
|
||||
},
|
||||
hideMetricTrackingReminder(state){
|
||||
if(state.userTotals){
|
||||
state.userTotals['showTrackMetricsButton'] = false
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
getters: {
|
||||
getUsername: state => {
|
||||
return state.username
|
||||
},
|
||||
getLoginToken: state => {
|
||||
return state.token
|
||||
},
|
||||
getLoggedIn: state => {
|
||||
let weIn = (state.username && state.username.length > 0)
|
||||
let weIn = (state.token !== null && state.token != undefined && state.token.length > 0)
|
||||
return weIn
|
||||
},
|
||||
getIsNightMode: state => {
|
||||
@@ -171,29 +126,13 @@ export default new Vuex.Store({
|
||||
totals: state => {
|
||||
return state.userTotals
|
||||
},
|
||||
getActiveSessions: state => {
|
||||
return state.activeSessions
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
fetchAndUpdateUserTotals ({ commit, state }) {
|
||||
clearTimeout(state.fetchTotalsTimeout)
|
||||
state.fetchTotalsTimeout = setTimeout(() => {
|
||||
// load extended options on initial load
|
||||
let postData = {
|
||||
extendedOptions: !state.userTotals
|
||||
}
|
||||
axios.post('/api/user/totals', postData)
|
||||
.then( ({data}) => {
|
||||
commit('setUserTotals', data)
|
||||
})
|
||||
.catch( error => {
|
||||
if(error.response && error.response.status == 400){
|
||||
commit('destroyLoginToken')
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
}, 100)
|
||||
fetchAndUpdateUserTotals ({ commit }) {
|
||||
axios.post('/api/user/totals')
|
||||
.then( ({data}) => {
|
||||
commit('setUserTotals', data)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,5 +11,4 @@ common.js
|
||||
bundle.*
|
||||
client/dist*
|
||||
server/public/*
|
||||
client/dist*
|
||||
*_scrape*
|
||||
client/dist*
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
10729
package-lock.json
generated
10729
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -1,31 +1,29 @@
|
||||
{
|
||||
"name": "personal-internet",
|
||||
"version": "1.0.0",
|
||||
"description": "Encrypted note taking applications",
|
||||
"description": "Personal or Private net",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Max",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.19.0",
|
||||
"body-parser": "^1.18.3",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^5.1.3",
|
||||
"express": "^4.16.4",
|
||||
"express-rate-limit": "^5.1.1",
|
||||
"gm": "^1.23.1",
|
||||
"helmet": "^4.1.1",
|
||||
"helmet": "^3.21.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"module-alias": "^2.2.2",
|
||||
"multer": "^1.4.2",
|
||||
"mysql2": "^2.2.5",
|
||||
"node-tesseract-ocr": "^2.0.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"request": "^2.88.2",
|
||||
"request-promise": "^4.2.6",
|
||||
"mysql2": "^1.6.5",
|
||||
"node-tesseract-ocr": "^1.0.0",
|
||||
"request": "^2.88.0",
|
||||
"request-promise": "^4.2.4",
|
||||
"socket.io": "^2.3.0",
|
||||
"speakeasy": "^2.0.0"
|
||||
"solr-node": "^1.2.1"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"@root": ".",
|
||||
@@ -33,8 +31,5 @@
|
||||
"@routes": "server/routes",
|
||||
"@helpers": "server/helpers",
|
||||
"@config": "server/config"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
//Import mysql2 package
|
||||
const mysql = require('mysql2');
|
||||
const os = require('os') //Used to get path of home directory
|
||||
const result = require('dotenv').config({ path:(os.homedir()+'/.env') })
|
||||
|
||||
// Create the connection pool.
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
host: 'localhost',
|
||||
user: 'dev',
|
||||
password: "LazaLinga&33Can't!Do!That34",
|
||||
database: 'application',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 20,
|
||||
|
||||
@@ -1,297 +1,25 @@
|
||||
const db = require('@config/database')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const cs = require('@helpers/CryptoString')
|
||||
const speakeasy = require('speakeasy')
|
||||
var jwt = require('jsonwebtoken');
|
||||
|
||||
let Auth = {}
|
||||
|
||||
const tokenSecretKey = process.env.JSON_KEY
|
||||
const sessionTokenUses = 300 //Defines number of uses each session token has before being refreshed
|
||||
const secretKey = '@TODO define secret constant its important!!!'
|
||||
|
||||
//Creates session token
|
||||
Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => {
|
||||
Auth.createToken = (userId) => {
|
||||
const signedData = {'id': userId, 'date':Date.now()}
|
||||
const token = jwt.sign(signedData, secretKey)
|
||||
return token
|
||||
}
|
||||
Auth.decodeToken = (token) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//Decodes session token
|
||||
Auth.decodeToken = (token, request = null) => {
|
||||
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){
|
||||
if(err || decoded.tempPass == undefined || decoded.tempPass.length < 5){
|
||||
throw new Error('Bad Token')
|
||||
}
|
||||
|
||||
decodedToken = decoded
|
||||
|
||||
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')
|
||||
jwt.verify(token, secretKey, function(err, decoded){
|
||||
if(err || decoded.id == undefined){
|
||||
reject('Bad Token')
|
||||
return
|
||||
}
|
||||
|
||||
})
|
||||
.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')
|
||||
|
||||
//Pass back decoded token
|
||||
resolve(decoded)
|
||||
return
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,6 @@ CryptoString.encrypt = (password, salt64, rawText) => {
|
||||
//Decrypt base64 string cipher text,
|
||||
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')
|
||||
const salt = Buffer.from(salt64, 'base64')
|
||||
|
||||
@@ -73,12 +70,6 @@ CryptoString.createSalt = () => {
|
||||
return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64')
|
||||
}
|
||||
|
||||
// Creates a small random salt
|
||||
CryptoString.createSmallSalt = () => {
|
||||
|
||||
return crypto.randomBytes(20).toString('base64')
|
||||
}
|
||||
|
||||
CryptoString.hash = (hashString) => {
|
||||
|
||||
return crypto.createHash('sha256').update(hashString).digest()
|
||||
|
||||
@@ -67,9 +67,9 @@ ProcessText.deduceNoteTitle = (inTitle, inString) => {
|
||||
}
|
||||
|
||||
//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!
|
||||
@@ -77,15 +77,14 @@ ProcessText.deduceNoteTitle = (inTitle, inString) => {
|
||||
// Still needs, links to open in a new window.
|
||||
|
||||
sub = ProcessText.stripDoubleBlankLines(inString)
|
||||
// if(tagFreeLength > 200){
|
||||
// sub += '... <i class="green caret down icon"></i>'
|
||||
// }
|
||||
// inString += '</end>'
|
||||
if(tagFreeLength > 200){
|
||||
sub += '... <i class="green caret down icon"></i>'
|
||||
}
|
||||
|
||||
return {title, sub}
|
||||
|
||||
//Emergency ending tag if truncated. This will help regex find all the lines
|
||||
|
||||
inString += '</end>'
|
||||
|
||||
//Match full line and closing tag or just closing tag
|
||||
let lines = inString.match(/[<[a-zA-Z0-9]+>(.*?)<\/[a-zA-Z0-9]+>|<\/[a-zA-Z0-9>]+?>/gms)
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
let SiteScrape = module.exports = {}
|
||||
|
||||
//
|
||||
// $ = the cheerio scrape object
|
||||
//
|
||||
|
||||
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',
|
||||
'give','use','find','tell','ask','work','seem','feel','try','leave','call','good','new','first','last','long','great','little','own','other','old',
|
||||
'right','big','high','different','small','large','next','early','young','important','few','public','bad','same','able','to','of','in','for','on',
|
||||
'with','at','by','from','up','about','into','over','after','the','and','a','that','I','it','not','he','as','you','this','but','his','they','her',
|
||||
'she','or','an','will','my','one','all','would','there','their','and','that','but','or','as','if','when','than','because','while','where','after',
|
||||
'so','though','since','until','whether','before','although','nor','like','once','unless','now','except','are','also','is','your','its']
|
||||
|
||||
SiteScrape.getTitle = ($) => {
|
||||
|
||||
let title = $('title').text().replace(removeWhitespace, " ")
|
||||
return title
|
||||
|
||||
}
|
||||
|
||||
//Finds all urls in text, removes duplicates, makes sure they have https://
|
||||
SiteScrape.getCleanUrls = (textBlock) => {
|
||||
//Find all URLs in text
|
||||
//@TODO - Use the process text library for this function
|
||||
const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm
|
||||
let allUrls = textBlock.match(urlPattern)
|
||||
|
||||
if(allUrls == null){
|
||||
return []
|
||||
}
|
||||
|
||||
//Every URL needs HTTPS!!!
|
||||
let foundUrls = []
|
||||
allUrls.forEach( (item, index) => {
|
||||
//add protocol if it is missing
|
||||
if(item.indexOf('https://') == -1 && item.indexOf('http://') == -1){
|
||||
allUrls[index] = 'https://'+item
|
||||
}
|
||||
//convert http to https
|
||||
if(item.indexOf('http://') >= 0){
|
||||
allUrls[index] = item.replace('http://','https://')
|
||||
}
|
||||
})
|
||||
|
||||
//Remove all duplicates
|
||||
foundUrls = [...new Set(allUrls)]
|
||||
|
||||
return foundUrls
|
||||
}
|
||||
|
||||
//Site hostname with https:// eg: https://www.google.com
|
||||
SiteScrape.getHostName = (url) => {
|
||||
|
||||
var hostname = 'https://'+(new URL(url)).hostname;
|
||||
// console.log('hostname', hostname)
|
||||
return hostname
|
||||
}
|
||||
|
||||
// URL for image that can be downloaded to represent website
|
||||
SiteScrape.getDisplayImage = ($, url) => {
|
||||
|
||||
const hostname = SiteScrape.getHostName(url)
|
||||
|
||||
let metaImg = $('[property="og:image"]')
|
||||
let shortcutIcon = $('[rel="shortcut icon"]')
|
||||
let favicon = $('[rel="icon"]')
|
||||
let randomImg = $('img')
|
||||
|
||||
//Set of images we may want gathered from various places in source
|
||||
let imagesWeWant = []
|
||||
let thumbnail = ''
|
||||
|
||||
//Scrape metadata for page image
|
||||
if(randomImg && randomImg.length > 0){
|
||||
|
||||
let imgSrcs = []
|
||||
for (let i = 0; i < randomImg.length; i++) {
|
||||
imgSrcs.push( randomImg[i].attribs.src )
|
||||
}
|
||||
|
||||
const half = Math.ceil(imgSrcs.length / 2)
|
||||
imagesWeWant = [...imgSrcs.slice(-half), ...imgSrcs.slice(0,half) ]
|
||||
|
||||
}
|
||||
//Grab the shortcut icon
|
||||
if(favicon && favicon[0] && favicon[0].attribs){
|
||||
imagesWeWant.push(favicon[0].attribs.href)
|
||||
}
|
||||
//Grab the shortcut icon
|
||||
if(shortcutIcon && shortcutIcon[0] && shortcutIcon[0].attribs){
|
||||
imagesWeWant.push(shortcutIcon[0].attribs.href)
|
||||
}
|
||||
//Grab the presentation image for the site
|
||||
if(metaImg && metaImg[0] && metaImg[0].attribs){
|
||||
imagesWeWant.unshift(metaImg[0].attribs.content)
|
||||
}
|
||||
|
||||
// console.log(imagesWeWant)
|
||||
|
||||
//Remove everything that isn't an accepted file format
|
||||
for (let i = imagesWeWant.length - 1; i >= 0; i--) {
|
||||
|
||||
let img = String(imagesWeWant[i])
|
||||
|
||||
if(
|
||||
!img.includes('.jpg') &&
|
||||
!img.includes('.jpeg') &&
|
||||
!img.includes('.png') &&
|
||||
!img.includes('.gif')
|
||||
){
|
||||
imagesWeWant.splice(i,1)
|
||||
}
|
||||
}
|
||||
|
||||
//Find if we have absolute thumbnails or not
|
||||
let foundAbsolute = false
|
||||
for (let i = imagesWeWant.length - 1; i >= 0; i--) {
|
||||
|
||||
let img = imagesWeWant[i]
|
||||
|
||||
//Add host name if its not included
|
||||
if(String(img).includes('//') || String(img).includes('http')){
|
||||
foundAbsolute = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//Go through all found images. Grab the one closest to the top. Closer is better
|
||||
for (let i = imagesWeWant.length - 1; i >= 0; i--) {
|
||||
|
||||
let img = imagesWeWant[i]
|
||||
|
||||
if(!String(img).includes('//') && foundAbsolute){
|
||||
continue;
|
||||
}
|
||||
|
||||
//Only add host to images if no absolute images were found
|
||||
if(!String(img).includes('//') ){
|
||||
if(img.indexOf('/') != 0){
|
||||
img = '/' + img
|
||||
}
|
||||
img = hostname + img
|
||||
}
|
||||
|
||||
if(img.indexOf('//') == 0){
|
||||
img = 'https:' + img //Scrape breaks without protocol
|
||||
}
|
||||
|
||||
thumbnail = img
|
||||
|
||||
}
|
||||
|
||||
return thumbnail
|
||||
}
|
||||
|
||||
// Get all the site text and parse out the words that appear most
|
||||
SiteScrape.getKeywords = ($) => {
|
||||
|
||||
let majorContent = ''
|
||||
|
||||
majorContent += $('[class*=content]').text()
|
||||
.replace(removeWhitespace, " ") //Remove all whitespace
|
||||
// .replace(/\W\s/g, '') //Remove all non alphanumeric characters
|
||||
.substring(0,6000) //Limit to 6000 characters
|
||||
.toLowerCase()
|
||||
.replace(/[^A-Za-z0-9- ]/g, '');
|
||||
|
||||
|
||||
console.log(majorContent)
|
||||
|
||||
//Count frequency of each word in scraped text
|
||||
let frequency = {}
|
||||
majorContent.split(' ').forEach(word => {
|
||||
// Exclude short or common words
|
||||
if(commonWords.includes(word) || word.length <= 2){
|
||||
return
|
||||
}
|
||||
if(!frequency[word]){
|
||||
frequency[word] = 0
|
||||
}
|
||||
// Skip some plurals
|
||||
if(frequency[word+'s'] || frequency[word+'es']){
|
||||
return
|
||||
}
|
||||
frequency[word]++
|
||||
})
|
||||
|
||||
//Create a sortable array
|
||||
var sortable = [];
|
||||
for (var index in frequency) {
|
||||
if(frequency[index] > 1){
|
||||
sortable.push([index, frequency[index]]);
|
||||
}
|
||||
}
|
||||
|
||||
//Sort them by most used words in the list
|
||||
sortable.sort(function(a, b) {
|
||||
return b[1] - a[1];
|
||||
});
|
||||
|
||||
let finalWords = []
|
||||
for(let i=0; i<6; i++){
|
||||
if(sortable[i] && sortable[i][0]){
|
||||
finalWords.push(sortable[i][0])
|
||||
}
|
||||
}
|
||||
|
||||
if(finalWords.length > 0){
|
||||
return 'Keywords: ' + finalWords.join(', ')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
SiteScrape.getMainText = ($) => {}
|
||||
|
||||
|
||||
|
||||
248
server/index.js
248
server/index.js
@@ -1,127 +1,66 @@
|
||||
//Set up environmental variables, pulled from ~/.env file used as process.env.DB_HOST
|
||||
const os = require('os') //Used to get path of home directory
|
||||
const result = require('dotenv').config({ path:(os.homedir()+'/.env') })
|
||||
|
||||
const ports = {
|
||||
express: 3000,
|
||||
socketIo: 3001
|
||||
}
|
||||
|
||||
//Allow user of @ in in require calls. Config in package.json
|
||||
require('module-alias/register')
|
||||
|
||||
//Auth helper, used for decoding users web token
|
||||
let Auth = require('@helpers/Auth')
|
||||
|
||||
//Helmet adds additional security to express server
|
||||
const helmet = require('helmet')
|
||||
|
||||
//Setup express server
|
||||
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
app.use( helmet() )
|
||||
// allow for the parsing of url encoded forms
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
const port = 3000
|
||||
|
||||
|
||||
//
|
||||
// Request Rate Limiter
|
||||
//
|
||||
const rateLimit = require('express-rate-limit')
|
||||
//Limiter for the entire app
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const limiter = rateLimit({
|
||||
windowMs: 10 * 60 * 1000, // 10 minutes
|
||||
max: 1000 // limit each IP to 1000 requests per windowMs
|
||||
})
|
||||
windowMs: 10 * 60 * 1000, // minutes
|
||||
max: 1000 // limit each IP to 100 requests per windowMs
|
||||
});
|
||||
|
||||
// apply to all requests
|
||||
app.use(limiter);
|
||||
|
||||
|
||||
|
||||
var http = require('http').createServer(app);
|
||||
var io = require('socket.io')(http, {
|
||||
path:'/socket'
|
||||
});
|
||||
|
||||
//Set socket IO as a global in the app
|
||||
global.SocketIo = io
|
||||
|
||||
let noteDiffs = {}
|
||||
// Make io accessible to our router
|
||||
app.use(function(req,res,next){
|
||||
req.io = io;
|
||||
next();
|
||||
});
|
||||
|
||||
io.on('connection', function(socket){
|
||||
|
||||
// console.log('New user ', socket.id)
|
||||
|
||||
//When a user connects, add them to their own room
|
||||
// This allows the server to emit events to that specific user
|
||||
// access socket.io in the controller with SocketIo global
|
||||
// access socket.io in the controller with req.io
|
||||
socket.on('user_connect', 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)
|
||||
|
||||
socket.join(userData.id)
|
||||
}).catch(error => {
|
||||
//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)
|
||||
socket.on('join_room', roomId => {
|
||||
// console.log('Join room ', roomId)
|
||||
socket.join(roomId)
|
||||
|
||||
//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 => {
|
||||
// Join user to rawtextid room when they enter
|
||||
socket.join(rawTextId)
|
||||
|
||||
//If there are past diffs for this note, send them to the user
|
||||
if(noteDiffs[rawTextId] != undefined){
|
||||
|
||||
//Sort all note diffs by when they were created.
|
||||
noteDiffs[rawTextId].sort((a,b) => { return a.time - b.time })
|
||||
|
||||
//Emit all sorted diffs to user
|
||||
socket.emit('past_diffs', noteDiffs[rawTextId])
|
||||
}
|
||||
|
||||
const usersInRoom = io.sockets.adapter.rooms[rawTextId]
|
||||
const usersInRoom = io.sockets.adapter.rooms[roomId]
|
||||
if(usersInRoom){
|
||||
//Update users in room count
|
||||
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
|
||||
// console.log('Users in room', usersInRoom.length)
|
||||
io.to(roomId).emit('update_user_count', usersInRoom.length)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -139,23 +78,13 @@ io.on('connection', function(socket){
|
||||
|
||||
socket.on('note_diff', data => {
|
||||
|
||||
//Log each diff for note
|
||||
const noteId = data.id
|
||||
delete data.id
|
||||
if(noteDiffs[noteId] == undefined){ noteDiffs[noteId] = [] }
|
||||
data.time = +new Date
|
||||
|
||||
noteDiffs[noteId].push(data)
|
||||
|
||||
// Go over each user in this note-room
|
||||
io.in(noteId).clients((error, clients) => {
|
||||
//Each user joins a room when they open the app.
|
||||
io.in(data.id).clients((error, clients) => {
|
||||
if (error) throw error;
|
||||
|
||||
//Go through each client in note-room and send them the diff
|
||||
//Go through each client in note room and send them the diff
|
||||
clients.forEach(socketId => {
|
||||
// only send off diff if user
|
||||
if(socketId != socket.id){
|
||||
io.to(socketId).emit('incoming_diff', data)
|
||||
io.to(socketId).emit('incoming_diff', data.diff)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -163,148 +92,71 @@ io.on('connection', function(socket){
|
||||
|
||||
})
|
||||
|
||||
socket.on('truncate_diffs_at_save', checkpoint => {
|
||||
|
||||
let diffSet = noteDiffs[checkpoint.rawTextId]
|
||||
if(diffSet && diffSet.length > 0){
|
||||
|
||||
//Make sure all diffs are sorted before cleaning
|
||||
noteDiffs[checkpoint.rawTextId].sort((a,b) => { return a.time - b.time })
|
||||
|
||||
// Remove all diffs until it reaches the current hash
|
||||
let sliceTo = 0
|
||||
for (var i = 0; i < diffSet.length; i++) {
|
||||
if(diffSet[i].hash == checkpoint){
|
||||
sliceTo = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo)
|
||||
|
||||
if(noteDiffs[checkpoint.rawTextId].length == 0){
|
||||
delete noteDiffs[checkpoint.rawTextId]
|
||||
}
|
||||
//Debugging
|
||||
else {
|
||||
console.log('Diffset after save')
|
||||
console.log(noteDiffs[checkpoint.rawTextId])
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('disconnect', function(socket){
|
||||
socket.on('disconnect', function(){
|
||||
// console.log('user disconnected');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
http.listen(ports.socketIo, function(){
|
||||
console.log(`Socke.io: Listening on port ${ports.socketIo}`)
|
||||
http.listen(3001, function(){
|
||||
console.log('socket.io liseting on port 3001');
|
||||
});
|
||||
|
||||
//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: '2mb'}))
|
||||
|
||||
|
||||
//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.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
|
||||
let token = req.headers.authorizationtoken
|
||||
if(token !== undefined && token.length > 0){
|
||||
Auth.decodeToken(token, req)
|
||||
if(token && token != null && typeof token === 'string'){
|
||||
Auth.decodeToken(token)
|
||||
.then(userData => {
|
||||
|
||||
//Update headers for the rest of the application
|
||||
req.headers.userId = userData.userId
|
||||
req.headers.masterKey = userData.masterKey
|
||||
req.headers.sessionId = userData.sessionId
|
||||
|
||||
//Tell front end remaining uses on current token
|
||||
res.set('remainingUses', userData.remainingUses)
|
||||
|
||||
req.headers.userId = userData.id //Update headers for the rest of the application
|
||||
next()
|
||||
})
|
||||
.catch(error => {
|
||||
}).catch(error => {
|
||||
|
||||
next('Unauthorized')
|
||||
res.statusMessage = error //Throw 400 error if token is bad
|
||||
res.status(400).end()
|
||||
})
|
||||
} else {
|
||||
next() //No token. Move along.
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// Test Area
|
||||
// const printResults = true
|
||||
// let UserTest = require('@models/User')
|
||||
// let NoteTest = require('@models/Note')
|
||||
// let AuthTest = require('@helpers/Auth')
|
||||
// Auth.test()
|
||||
// UserTest.keyPairTest('genMan30', '1', printResults)
|
||||
// .then( ({testUserId, masterKey}) =>
|
||||
// NoteTest.test(testUserId, masterKey, printResults))
|
||||
// .then( message => {
|
||||
// if(printResults) console.log(message)
|
||||
// Auth.testTwoFactor()
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.log(error)
|
||||
// })
|
||||
|
||||
//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
|
||||
app.use('/api/static', express.static( __dirname+'/../staticFiles' ))
|
||||
app.use(prefix+'/static', express.static( __dirname+'/../staticFiles' ))
|
||||
|
||||
//Public routes
|
||||
var public = require('@routes/publicController')
|
||||
app.use('/api/public', public)
|
||||
app.use(prefix+'/public', public)
|
||||
|
||||
//user endpoint
|
||||
var user = require('@routes/userController')
|
||||
app.use('/api/user', user)
|
||||
app.use(prefix+'/user', user)
|
||||
|
||||
//notes endpoint
|
||||
var notes = require('@routes/noteController')
|
||||
app.use('/api/note', notes)
|
||||
app.use(prefix+'/note', notes)
|
||||
|
||||
//tags endpoint
|
||||
var tags = require('@routes/tagController')
|
||||
app.use('/api/tag', tags)
|
||||
app.use(prefix+'/tag', tags)
|
||||
|
||||
//notes endpoint
|
||||
var attachment = require('@routes/attachmentController')
|
||||
app.use('/api/attachment', attachment)
|
||||
app.use(prefix+'/attachment', attachment)
|
||||
|
||||
//quick notes endpoint
|
||||
var quickNote = require('@routes/quicknoteController')
|
||||
app.use('/api/quick-note', quickNote)
|
||||
|
||||
//cycle tracking endpoint
|
||||
var metricTracking = require('@routes/metrictrackingController')
|
||||
app.use('/api/metric-tracking', metricTracking)
|
||||
app.use(prefix+'/quick-note', quickNote)
|
||||
|
||||
//Output running status
|
||||
app.listen(ports.express, () => {
|
||||
console.log(`Express: Listening on port ${ports.express}!`)
|
||||
})
|
||||
|
||||
//
|
||||
//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()
|
||||
})
|
||||
app.listen(port, () => console.log(`Listening on port ${port}!`))
|
||||
@@ -1,8 +1,5 @@
|
||||
let db = require('@config/database')
|
||||
|
||||
let SiteScrape = require('@helpers/SiteScrape')
|
||||
const cs = require('@helpers/CryptoString')
|
||||
|
||||
let Attachment = module.exports = {}
|
||||
|
||||
const cheerio = require('cheerio')
|
||||
@@ -33,7 +30,6 @@ Attachment.textSearch = (userId, searchTerm) => {
|
||||
) as snippet
|
||||
FROM attachment
|
||||
WHERE user_id = ?
|
||||
AND visible != 0
|
||||
AND MATCH(text)
|
||||
AGAINST(? IN NATURAL LANGUAGE MODE)
|
||||
LIMIT 1000`
|
||||
@@ -47,60 +43,31 @@ Attachment.textSearch = (userId, searchTerm) => {
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeShared) => {
|
||||
console.log([userId, noteId, attachmentType, offset, setSize, includeShared])
|
||||
Attachment.search = (userId, noteId, attachmentType, offset, setSize) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let params = [userId]
|
||||
let query = `
|
||||
SELECT attachment.*, note.share_user_id FROM attachment
|
||||
LEFT JOIN note ON (attachment.note_id = note.id)
|
||||
WHERE attachment.user_id = ? AND visible = 1
|
||||
`
|
||||
let query = 'SELECT * FROM attachment WHERE user_id = ? AND visible = 1 '
|
||||
|
||||
if(noteId && noteId > 0){
|
||||
//
|
||||
// Show everything if note ID is present
|
||||
//
|
||||
query += 'AND attachment.note_id = ? '
|
||||
query += 'AND note_id = ? '
|
||||
params.push(noteId)
|
||||
|
||||
} else {
|
||||
//
|
||||
// Other filters if NO note id
|
||||
//
|
||||
|
||||
if(attachmentType == 'links'){
|
||||
query += 'AND attachment_type = 1 '
|
||||
}
|
||||
if(attachmentType == 'files'){
|
||||
query += 'AND attachment_type > 1 '
|
||||
}
|
||||
|
||||
query += `AND note.archived = ${ attachmentType == 'archived' ? '1':'0' } `
|
||||
query += `AND note.trashed = ${ attachmentType == 'trashed' ? '1':'0' } `
|
||||
|
||||
if(!attachmentType){
|
||||
// Null note ID means it was pushed by bookmarklet
|
||||
query += 'OR attachment.note_id IS NULL '
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(!noteId){
|
||||
const sharedOrNot = includeShared ? ' NOT ':' '
|
||||
query += `AND note.share_user_id IS${sharedOrNot}NULL `
|
||||
if(attachmentType == 'links'){
|
||||
query += 'AND attachment_type = 1 '
|
||||
}
|
||||
if(attachmentType == 'files'){
|
||||
query += 'AND attachment_type > 1 '
|
||||
}
|
||||
|
||||
|
||||
query += 'ORDER BY last_indexed DESC '
|
||||
|
||||
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}`
|
||||
|
||||
console.log(query)
|
||||
|
||||
db.promise()
|
||||
.query(query, params)
|
||||
.then((rows, fields) => {
|
||||
@@ -110,6 +77,18 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeSha
|
||||
})
|
||||
}
|
||||
|
||||
//Returns all attachments
|
||||
Attachment.forNote = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND visible = 1 ORDER BY last_indexed DESC;`, [userId, noteId])
|
||||
.then((rows, fields) => {
|
||||
resolve(rows[0]) //Return all attachments found by query
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.urlForNote = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
@@ -136,9 +115,6 @@ Attachment.update = (userId, attachmentId, updatedText, noteId) => {
|
||||
|
||||
Attachment.delete = (userId, attachmentId, urlDelete = false) => {
|
||||
|
||||
let attachment = null
|
||||
let noteExists = true
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query('SELECT * FROM attachment WHERE id = ? AND user_id = ? LIMIT 1', [attachmentId, userId])
|
||||
@@ -149,43 +125,36 @@ Attachment.delete = (userId, attachmentId, urlDelete = false) => {
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
attachment = rows[0][0]
|
||||
|
||||
return db.promise().query('SELECT count(id) as `exists` FROM note WHERE id = ?', [attachment.note_id])
|
||||
|
||||
})
|
||||
|
||||
.then((rows, fields) => {
|
||||
|
||||
noteExists = (rows[0][0]['exists'] > 0)
|
||||
|
||||
let url = attachment.url
|
||||
const noteId = attachment.note_id
|
||||
//Pull data we want out of
|
||||
let row = rows[0][0]
|
||||
let url = row.url
|
||||
const noteId = row.note_id
|
||||
|
||||
//Try to delete file and thumbnail
|
||||
try {
|
||||
fs.unlinkSync(filePath+attachment.file_location)
|
||||
fs.unlinkSync(filePath+row.file_location)
|
||||
} catch(err) { console.error('File Does not exist') }
|
||||
try {
|
||||
fs.unlinkSync(filePath+'thumb_'+attachment.file_location)
|
||||
fs.unlinkSync(filePath+'thumb_'+row.file_location)
|
||||
} catch(err) { console.error('Thumbnail Does not exist') }
|
||||
|
||||
//Do not delete link attachments, just hide them. They will be deleted if removed from note or if note is deleted
|
||||
if(attachment.attachment_type == 1 && !urlDelete && noteExists){
|
||||
//Do not delete link attachments, just hide them. They will be deleted if removed from note
|
||||
if(row.attachment_type == 1 && !urlDelete){
|
||||
db.promise()
|
||||
.query(`UPDATE attachment SET visible = 0 WHERE id = ?`, [attachmentId])
|
||||
.then((rows, fields) => resolve(true))
|
||||
.then((rows, fields) => { })
|
||||
.catch(console.log)
|
||||
|
||||
return resolve(true)
|
||||
} else {
|
||||
db.promise()
|
||||
.query(`DELETE FROM attachment WHERE id = ?`, [attachmentId])
|
||||
.then((rows, fields) => resolve(true))
|
||||
.catch(console.log)
|
||||
}
|
||||
|
||||
db.promise()
|
||||
.query(`DELETE FROM attachment WHERE id = ?`, [attachmentId])
|
||||
.then((rows, fields) => { })
|
||||
.catch(console.log)
|
||||
|
||||
return resolve(true)
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -273,8 +242,32 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
|
||||
|
||||
Attachment.urlForNote(userId, noteId).then(attachments => {
|
||||
|
||||
//Pull all the URLs out of the text
|
||||
let foundUrls = SiteScrape.getCleanUrls(noteText)
|
||||
//Find all URLs in text
|
||||
//@TODO - Use the process text library for this function
|
||||
const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm
|
||||
let allUrls = noteText.match(urlPattern)
|
||||
|
||||
if(allUrls == null){
|
||||
allUrls = []
|
||||
}
|
||||
|
||||
//Every URL needs HTTPS!!!
|
||||
let foundUrls = []
|
||||
allUrls.forEach( (item, index) => {
|
||||
//Every URL should have HTTPS
|
||||
if(item.indexOf('https://') == -1 && item.indexOf('http://') == -1){
|
||||
allUrls[index] = 'https://'+item
|
||||
}
|
||||
//URLs should all have HTTPS!!!
|
||||
if(item.indexOf('http://') >= 0){
|
||||
allUrls[index] = item.replace('http://','https://')
|
||||
}
|
||||
})
|
||||
|
||||
//Remove all duplicates
|
||||
foundUrls = [...new Set(allUrls)]
|
||||
|
||||
|
||||
|
||||
//Go through each saved URL, remove new URLs from saved URLs
|
||||
//If a URL is not found, delete it
|
||||
@@ -300,15 +293,13 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
|
||||
Attachment.scrapeUrlsCreateAttachments(userId, noteId, foundUrls).then( freshlyScrapedText => {
|
||||
|
||||
//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')
|
||||
if(io){
|
||||
io.to(userId).emit('update_counts')
|
||||
}
|
||||
|
||||
solrAttachmentText += freshlyScrapedText
|
||||
resolve(solrAttachmentText)
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -336,13 +327,9 @@ Attachment.scrapeUrlsCreateAttachments = (userId, noteId, foundUrls) => {
|
||||
|
||||
//All URLs have been scraped, return data
|
||||
if(processedCount == foundUrls.length){
|
||||
console.log('All urls scraped')
|
||||
return resolve(scrapedText)
|
||||
resolve(scrapedText)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Site Scrape error', error)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -352,16 +339,17 @@ Attachment.downloadFileFromUrl = (url) => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if(!url){
|
||||
return resolve(null)
|
||||
if(url == null){
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
const random = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
||||
let extension = ''
|
||||
let fileName = random+'_scrape'
|
||||
let thumbPath = 'thumb_'+fileName
|
||||
const extension = '.'+url.split('.').pop() //This is throwing an error
|
||||
let fileName = random+'_scrape'+extension
|
||||
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)
|
||||
|
||||
@@ -373,8 +361,6 @@ Attachment.downloadFileFromUrl = (url) => {
|
||||
.on('response', res => {
|
||||
console.log(res.statusCode)
|
||||
console.log(res.headers['content-type'])
|
||||
//Get mime type from header content type
|
||||
// extension = '.'+String(res.headers['content-type']).split('/').pop()
|
||||
})
|
||||
.pipe(fs.createWriteStream(filePath+thumbPath))
|
||||
.on('close', () => {
|
||||
@@ -382,27 +368,32 @@ Attachment.downloadFileFromUrl = (url) => {
|
||||
//resize image if its real big
|
||||
gm(filePath+thumbPath)
|
||||
.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) {
|
||||
if(err){
|
||||
console.log(err)
|
||||
return resolve(null)
|
||||
}
|
||||
|
||||
console.log('Saved Image')
|
||||
return resolve(fileName)
|
||||
if(err){ console.log(err) }
|
||||
})
|
||||
|
||||
|
||||
console.log('Saved Image')
|
||||
resolve(fileName)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.processUrl = (userId, noteId, url) => {
|
||||
|
||||
const scrapeTime = 5*1000;
|
||||
const scrapeTime = 20*1000;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const excludeWords = ['share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
|
||||
'give','use','find','tell','ask','work','seem','feel','try','leave','call','good','new','first','last','long','great','little','own','other','old',
|
||||
'right','big','high','different','small','large','next','early','young','important','few','public','bad','same','able','to','of','in','for','on',
|
||||
'with','at','by','from','up','about','into','over','after','the','and','a','that','I','it','not','he','as','you','this','but','his','they','her',
|
||||
'she','or','an','will','my','one','all','would','there','their','and','that','but','or','as','if','when','than','because','while','where','after',
|
||||
'so','though','since','until','whether','before','although','nor','like','once','unless','now','except','are','also','is','your','its']
|
||||
|
||||
var removeWhitespace = /\s+/g
|
||||
|
||||
const options = {
|
||||
uri: url,
|
||||
@@ -427,7 +418,7 @@ Attachment.processUrl = (userId, noteId, url) => {
|
||||
.query(`INSERT INTO attachment
|
||||
(note_id, user_id, attachment_type, text, url, last_indexed, file_location)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[noteId, userId, 1, url, url, created, null])
|
||||
[noteId, userId, 1, 'Processing...', url, created, null])
|
||||
.then((rows, fields) => {
|
||||
//Set two bigger variables then return request for processing
|
||||
request = rp(options)
|
||||
@@ -437,36 +428,70 @@ Attachment.processUrl = (userId, noteId, url) => {
|
||||
})
|
||||
.then($ => {
|
||||
|
||||
//Clear timeout that would end this function
|
||||
clearTimeout(requestTimeout)
|
||||
|
||||
var desiredSearchText = ''
|
||||
|
||||
let pageTitle = $('title').text().replace(removeWhitespace, " ")
|
||||
desiredSearchText += pageTitle + "\n"
|
||||
|
||||
// let header = $('h1').text().replace(removeWhitespace, " ")
|
||||
// desiredSearchText += header + "\n"
|
||||
|
||||
const pageTitle = SiteScrape.getTitle($)
|
||||
|
||||
const hostname = SiteScrape.getHostName(url)
|
||||
|
||||
const thumbnail = SiteScrape.getDisplayImage($, url)
|
||||
|
||||
const keywords = SiteScrape.getKeywords($)
|
||||
|
||||
var desiredSearchText = ''
|
||||
desiredSearchText += pageTitle
|
||||
if(keywords){
|
||||
desiredSearchText += "\n " + keywords
|
||||
//Scrape metadata for page image
|
||||
let metadata = $('meta[property="og:image"]')
|
||||
if(metadata && metadata[0] && metadata[0].attribs){
|
||||
thumbnail = metadata[0].attribs.content
|
||||
}
|
||||
|
||||
console.log('Results from site scrape-------------')
|
||||
console.log({
|
||||
pageTitle,
|
||||
hostname,
|
||||
thumbnail,
|
||||
keywords
|
||||
})
|
||||
|
||||
|
||||
// throw new Error('Ending this function early.')
|
||||
let majorContent = ''
|
||||
majorContent += $('[class*=content]').text()
|
||||
.replace(removeWhitespace, " ") //Remove all whitespace
|
||||
.replace(/\W\s/g, '') //Remove all non alphanumeric characters
|
||||
.substring(0,3000)
|
||||
.toLowerCase()
|
||||
majorContent += $('[id*=content]').text().replace(removeWhitespace, " ")
|
||||
.replace(removeWhitespace, " ") //Remove all whitespace
|
||||
.replace(/\W\s/g, '') //Remove all non alphanumeric characters
|
||||
.substring(0,3000) //Limit characters
|
||||
.toLowerCase()
|
||||
|
||||
//Count frequency of each word in scraped text
|
||||
let frequency = {}
|
||||
majorContent.split(' ').forEach(word => {
|
||||
if(excludeWords.includes(word)){
|
||||
return //Exclude certain words
|
||||
}
|
||||
if(!frequency[word]){
|
||||
frequency[word] = 0
|
||||
}
|
||||
frequency[word]++
|
||||
})
|
||||
|
||||
//Create a sortable array
|
||||
var sortable = [];
|
||||
for (var index in frequency) {
|
||||
if(frequency[index] > 1){
|
||||
sortable.push([index, frequency[index]]);
|
||||
}
|
||||
}
|
||||
|
||||
//Sort them by most used words in the list
|
||||
sortable.sort(function(a, b) {
|
||||
return b[1] - a[1];
|
||||
});
|
||||
|
||||
let finalWords = []
|
||||
for(let i=0; i<5; i++){
|
||||
if(sortable[i] && sortable[i][0]){
|
||||
finalWords.push(sortable[i][0])
|
||||
}
|
||||
}
|
||||
|
||||
if(finalWords.length > 0){
|
||||
desiredSearchText += 'Keywords: ' + finalWords.join(', ')
|
||||
}
|
||||
|
||||
|
||||
// console.log('TexT Scraped')
|
||||
@@ -507,142 +532,39 @@ Attachment.processUrl = (userId, noteId, url) => {
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Scrape pooped out')
|
||||
console.log('Issue with scrape', error.statusCode)
|
||||
clearTimeout(requestTimeout)
|
||||
return resolve('No site text')
|
||||
console.log('Issue with scrape')
|
||||
console.log(error)
|
||||
resolve('')
|
||||
})
|
||||
|
||||
requestTimeout = setTimeout( () => {
|
||||
console.log('Cancel the request, its taking to long.')
|
||||
request.cancel()
|
||||
return resolve('Request Timeout')
|
||||
|
||||
desiredSearchText = 'No Description for -> '+url
|
||||
|
||||
created = Math.round((+new Date)/1000)
|
||||
db.promise()
|
||||
.query(`UPDATE attachment SET
|
||||
text = ?,
|
||||
last_indexed = ?,
|
||||
WHERE id = ?
|
||||
`, [desiredSearchText, created, insertedId])
|
||||
.then((rows, fields) => {
|
||||
resolve(desiredSearchText) //Return found text
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
//Create attachment in DB with scrape text and provided data
|
||||
// db.promise()
|
||||
// .query(`INSERT INTO attachment
|
||||
// (note_id, user_id, attachment_type, text, url, last_indexed)
|
||||
// VALUES (?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created])
|
||||
// .then((rows, fields) => {
|
||||
// resolve(desiredSearchText) //Return found text
|
||||
// })
|
||||
// .catch(console.log)
|
||||
|
||||
}, scrapeTime )
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.generatePushKey = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
db.promise()
|
||||
.query("SELECT pushkey FROM user WHERE id = ? LIMIT 1", [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
const pushKey = rows[0][0].pushkey
|
||||
|
||||
// push key exists
|
||||
if(pushKey && pushKey.length > 0){
|
||||
|
||||
return resolve(pushKey)
|
||||
|
||||
} else {
|
||||
|
||||
// generate and save a new key
|
||||
const newPushKey = cs.createSmallSalt()
|
||||
db.promise()
|
||||
.query('UPDATE user SET pushkey = ? WHERE id = ? LIMIT 1', [newPushKey,userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
return resolve(newPushKey)
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.deletePushKey = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
db.promise()
|
||||
.query('UPDATE user SET pushkey = null WHERE id = ? LIMIT 1', [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
return resolve(rows[0].affectedRows == 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.getPushkeyBookmarklet = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
Attachment.generatePushKey(userId)
|
||||
.then( pushKey => {
|
||||
|
||||
let bookmarklet = Attachment.generateBookmarkletText(pushKey)
|
||||
return resolve(bookmarklet)
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.pushUrl = (pushkey,url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let userId = null
|
||||
pushkey = pushkey.replace(/ /g, '+')
|
||||
|
||||
db.promise()
|
||||
.query("SELECT id FROM user WHERE pushkey = ? LIMIT 1", [pushkey])
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(rows[0].length == 0){
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
userId = rows[0][0].id
|
||||
return Attachment.scrapeUrlsCreateAttachments(userId, null, [url])
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
if(typeof SocketIo != 'undefined'){
|
||||
//Once everything is done being scraped, emit new attachment events
|
||||
SocketIo.to(userId).emit('update_counts')
|
||||
|
||||
// Tell user to update attachments with scraped text
|
||||
SocketIo.to(userId).emit('update_note_attachments')
|
||||
}
|
||||
|
||||
return resolve(true)
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.generateBookmarkletText = (pushKey) => {
|
||||
|
||||
const endpoint = '/api/public/pushmebaby'
|
||||
let url = 'https://www.solidscribe.com' + endpoint
|
||||
if(process.env.NODE_ENV === 'development'){
|
||||
// url = 'https://192.168.1.164' + endpoint
|
||||
}
|
||||
|
||||
// Terminate each line with a semi-colon, super important, since spaces are removed.
|
||||
|
||||
// document.getElementById(id).remove();
|
||||
url += '?pushkey='+encodeURIComponent(pushKey)
|
||||
const bookmarkletV3 = `
|
||||
javascript: (() => {
|
||||
var p = encodeURIComponent(window.location.href);
|
||||
var n = "`+url+`&url="+p;
|
||||
window.open(n, '_blank', 'noopener=noopener');
|
||||
window.focus();
|
||||
|
||||
var k = document.createElement("div");
|
||||
k.setAttribute("style", "position:fixed;right:10px;top:10px;z-index:222222;border-radius:4px;font-size:1.3em;padding:20px 15px;background: #8f51be;color:white;");
|
||||
k.innerHTML = "Posted URL to your Solid Scribe account";
|
||||
|
||||
document.body.appendChild(k);
|
||||
|
||||
setTimeout(()=>{
|
||||
k.remove();
|
||||
},5000);
|
||||
|
||||
})();
|
||||
`
|
||||
|
||||
return bookmarkletV3
|
||||
.replace(/\t|\r|\n/gm, "") // Remove tabs, new lines, returns
|
||||
.replace(/\s+/g, ' ') // remove double spaces
|
||||
.trim()
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,88 +5,45 @@ let Note = require('@models/Note')
|
||||
let QuickNote = module.exports = {}
|
||||
|
||||
|
||||
QuickNote.get = (userId, masterKey) => {
|
||||
QuickNote.get = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
db.promise()
|
||||
.query(`
|
||||
SELECT note.id FROM note WHERE quick_note = 1 AND user_id = ? LIMIT 1`, [userId])
|
||||
SELECT note.id, text FROM note
|
||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
WHERE quick_note = 1 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)
|
||||
//Quick Note is set, return note text
|
||||
if(rows[0].length == 1){
|
||||
resolve({
|
||||
id: rows[0][0].id,
|
||||
text: rows[0][0].text
|
||||
})
|
||||
|
||||
} else {
|
||||
//Or create a new note and get the id
|
||||
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})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
resolve({
|
||||
id: null,
|
||||
text: 'Enter something to create a quick note.'
|
||||
})
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
QuickNote.newNote = (userId) => {
|
||||
QuickNote.update = (userId, pushText) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise().query('UPDATE note SET quick_note = 0 WHERE quick_note = 1 AND user_id = ?',[userId])
|
||||
.then((rows, fields) => {
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
QuickNote.makeUrlLink = (inputText) => {
|
||||
var replacedText, replacePattern1, replacePattern2, replacePattern3;
|
||||
|
||||
//URLs starting with http://, https://, or ftp://
|
||||
replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
|
||||
replacedText = inputText.replace(replacePattern1, '<a href="$1" target="_blank">$1</a>');
|
||||
|
||||
//URLs starting with "www." (without // before it, or it'd re-link the ones done above).
|
||||
replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
|
||||
replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
|
||||
|
||||
//Change email addresses to mailto:: links.
|
||||
replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
|
||||
replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
|
||||
|
||||
return replacedText;
|
||||
}
|
||||
|
||||
QuickNote.update = (userId, pushText, masterKey) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let finalId = null
|
||||
let finalText = ''
|
||||
|
||||
//Process pushText, split at \n (new lines), put <p> tags around each new line
|
||||
let broken = '<p>' +
|
||||
let broken = '<blockquote>' +
|
||||
pushText.split(/\r?\n/).map( (line, index) => {
|
||||
|
||||
let clean = line
|
||||
.replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities
|
||||
.replace(/<[^>]+>/g, '') //Rip out all HTML tags
|
||||
|
||||
//Turn links into actual link
|
||||
clean = QuickNote.makeUrlLink(clean)
|
||||
|
||||
if(clean == ''){ clean = ' ' }
|
||||
let newLine = ''
|
||||
if(index > 0){ newLine = '<br>' }
|
||||
@@ -94,31 +51,51 @@ QuickNote.update = (userId, pushText, masterKey) => {
|
||||
//Return line wrapped in p tags
|
||||
return `${newLine}<span>${clean}</span>`
|
||||
|
||||
}).join('') + '</p><p><br></p>'
|
||||
}).join('') + '</blockquote>'
|
||||
|
||||
QuickNote.get(userId, masterKey)
|
||||
.then(noteObject => {
|
||||
db.promise()
|
||||
.query(`
|
||||
SELECT note.id, text, color, pinned, archived
|
||||
FROM note
|
||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
WHERE quick_note = 1 AND user_id = ? LIMIT 1
|
||||
`, [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(noteObject == null){
|
||||
//Quick Note is set, push it the front of existing note
|
||||
if(rows[0].length == 1){
|
||||
|
||||
finalText += broken
|
||||
let d = rows[0][0] //Get row data
|
||||
|
||||
return Note.create(userId, 'Quick Note', finalText, masterKey)
|
||||
.then(insertedId => {
|
||||
finalId = insertedId
|
||||
return db.promise().query('UPDATE note SET quick_note = 1 WHERE id = ? AND user_id = ?',[insertedId, userId])
|
||||
//Push old text behind fresh new text
|
||||
let newText = broken +''+ d.text
|
||||
|
||||
//Save that, then return the new text
|
||||
Note.update(null, userId, d.id, newText, '', d.color, d.pinned, d.archived)
|
||||
.then( saveResults => {
|
||||
resolve({
|
||||
id:d.id,
|
||||
text:newText
|
||||
})
|
||||
})
|
||||
|
||||
} else {
|
||||
|
||||
finalText += (broken + noteObject.text)
|
||||
finalId = noteObject.id
|
||||
return Note.update(userId, noteObject.id, finalText, noteObject.title, noteObject.color, noteObject.pinned, noteObject.archived, null, masterKey)
|
||||
//Create a new note with the quick text submitted.
|
||||
Note.create(userId, broken, 1)
|
||||
.then( insertId => {
|
||||
resolve({
|
||||
id:insertId,
|
||||
text:broken
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
.then( saveResults => {
|
||||
return resolve(saveResults)
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
|
||||
//Lookup quick note,
|
||||
|
||||
//Note.create(userId, 'Quick Note', 1)
|
||||
|
||||
}
|
||||
@@ -9,283 +9,86 @@ const Note = require('@models/Note')
|
||||
|
||||
let ShareNote = module.exports = {}
|
||||
|
||||
const crypto = require('crypto')
|
||||
const cs = require('@helpers/CryptoString')
|
||||
|
||||
ShareNote.addUserToSharedNote = (userId, noteId, shareUserId, masterKey) => {
|
||||
// Share a note with a user, given the correct username
|
||||
ShareNote.addUser = (userId, noteId, rawTextId, username) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const Note = require('@models/Note')
|
||||
const User = require('@models/User')
|
||||
let shareUserId = null
|
||||
let newNoteShare = null
|
||||
const cleanUser = username.toLowerCase().trim()
|
||||
|
||||
//generate new random salts and password
|
||||
let sharedNoteMasterKey = null
|
||||
|
||||
let encryptedSharedKey = null //new key for note encrypted with shared users pubic key
|
||||
|
||||
//Current note object
|
||||
let noteObject = null
|
||||
let publicKey = null
|
||||
|
||||
db.promise().query('SELECT id FROM user WHERE id = ?', [shareUserId])
|
||||
//Check that user actually exists
|
||||
db.promise().query(`SELECT id FROM user WHERE LOWER(username) = ?`, [cleanUser])
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(rows[0].length == 0){
|
||||
throw new Error('User Does Not Exist')
|
||||
}
|
||||
|
||||
return ShareNote.migrateToShared(userId, noteId, masterKey)
|
||||
|
||||
})
|
||||
.then(({note, sharedNoteKey})=> {
|
||||
|
||||
sharedNoteMasterKey = sharedNoteKey
|
||||
noteObject = note
|
||||
shareUserId = rows[0][0]['id']
|
||||
|
||||
//Check if note has already been added for user
|
||||
return db.promise()
|
||||
.query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, note.rawTextId])
|
||||
.query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, rawTextId])
|
||||
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(rows[0].length >= 1){
|
||||
throw new Error('User Already has this note shared with them')
|
||||
if(rows[0].length != 0){
|
||||
throw new Error('User Already Has Note')
|
||||
}
|
||||
|
||||
return User.getPublicKey(shareUserId)
|
||||
|
||||
})
|
||||
.then(shareUserPublicKey => {
|
||||
|
||||
//New Encrypted snippet, using new shared password
|
||||
const newSnippetSalt = cs.createSmallSalt()
|
||||
const snippet = JSON.stringify([noteObject.title, noteObject.text.substring(0, 500)])
|
||||
const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, newSnippetSalt, snippet)
|
||||
|
||||
//Encrypt shared password for this user
|
||||
const encryptedSharedKey = crypto.publicEncrypt(shareUserPublicKey, Buffer.from(sharedNoteMasterKey, 'utf8')).toString('base64')
|
||||
|
||||
//Insert new note for shared user
|
||||
return db.promise().query(`
|
||||
INSERT INTO note (user_id, note_raw_text_id, created, color, share_user_id, snippet, snippet_salt, encrypted_share_password_key) VALUES (?,?,?,?,?,?,?,?);
|
||||
`, [shareUserId, noteObject.rawTextId, noteObject.created, noteObject.color, userId, encryptedSnippet, newSnippetSalt, encryptedSharedKey])
|
||||
|
||||
//Lookup note to share with user, clone this data to create users new note
|
||||
return db.promise()
|
||||
.query(`SELECT * FROM note WHERE id = ? LIMIT 1`, [noteId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
const sharedUserNoteId = rows[0]['insertId']
|
||||
newNoteShare = rows[0][0]
|
||||
|
||||
//Emit update count event to user shared with - so they see the note in real time
|
||||
SocketIo.to(shareUserId).emit('update_counts')
|
||||
//Modify note with the share attributes we want
|
||||
delete newNoteShare['id']
|
||||
delete newNoteShare['opened']
|
||||
newNoteShare['share_user_id'] = userId //User who shared the note
|
||||
newNoteShare['user_id'] = shareUserId //User who gets note
|
||||
|
||||
let success = true
|
||||
return resolve({success, shareUserId, sharedUserNoteId})
|
||||
//Setup db colums, db values and number of '?' to put into prepared statement
|
||||
let dbColumns = []
|
||||
let dbValues = []
|
||||
let escapeChars = []
|
||||
|
||||
//Pull out all the data we need from object to create prepared statemnt
|
||||
Object.keys(newNoteShare).forEach( key => {
|
||||
escapeChars.push('?')
|
||||
dbColumns.push(key)
|
||||
dbValues.push(newNoteShare[key])
|
||||
})
|
||||
|
||||
//Stick all the note value back into query, insert updated note
|
||||
return db.promise()
|
||||
.query(`INSERT INTO note (${dbColumns.join()}) VALUES (${escapeChars.join()})`, dbValues)
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Update note share status to 2
|
||||
return db.promise()
|
||||
.query('UPDATE note SET shared = 2 WHERE id = ?', [noteId])
|
||||
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
//Success!
|
||||
return resolve({'success':true, shareUserId})
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Shared Note Error')
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
ShareNote.removeUserFromSharedNote = (userId, noteId, shareNoteUserId, masterKey) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let rawTextId = null
|
||||
let removeUserId = null
|
||||
|
||||
db.promise()
|
||||
.query('SELECT note_raw_text_id, user_id FROM note WHERE id = ? AND share_user_id = ?', [shareNoteUserId, userId])
|
||||
.then( (rows, fields) => {
|
||||
|
||||
rawTextId = rows[0][0]['note_raw_text_id']
|
||||
removeUserId = rows[0][0]['user_id']
|
||||
|
||||
//Delete note entry for other user - remove users access
|
||||
//@TODO - this won't remove the note from their search index, it needs to
|
||||
return Note.delete(removeUserId, shareNoteUserId)
|
||||
|
||||
})
|
||||
.then(results => {
|
||||
|
||||
return db.promise().query('SELECT count(*) as count FROM note WHERE note_raw_text_id = ?', [rawTextId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Convert back to normal note if there is only one person with this note
|
||||
if(rows[0][0]['count'] == 1){
|
||||
|
||||
return ShareNote.migrateToNormal(userId, noteId, masterKey)
|
||||
.then(results => {
|
||||
resolve(true)
|
||||
})
|
||||
|
||||
} else {
|
||||
//Keep note shared
|
||||
return resolve(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//Encrypt note with private shared key
|
||||
ShareNote.migrateToShared = (userId, noteId, masterKey) => {
|
||||
|
||||
const User = require('@models/User')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
//generate new random password
|
||||
const sharedNoteMasterKey = cs.createSmallSalt()
|
||||
let userPublicKey = null
|
||||
let userPrivateKey = null
|
||||
let note = null
|
||||
|
||||
User.generateKeypair(userId, masterKey)
|
||||
.then( ({publicKey, privateKey}) => {
|
||||
|
||||
//Get users public key
|
||||
userPublicKey = publicKey
|
||||
userPrivateKey = privateKey
|
||||
|
||||
return Note.get(userId, noteId, masterKey)
|
||||
})
|
||||
.then(noteObject => {
|
||||
|
||||
note = noteObject
|
||||
|
||||
if(note.shared == 2){
|
||||
|
||||
//Note is already shared, decrypt sharedKey
|
||||
let sharedNoteKey = null
|
||||
const encryptedShareKey = note.encrypted_share_password_key
|
||||
if(encryptedShareKey != null){
|
||||
sharedNoteKey = crypto.privateDecrypt(userPrivateKey,
|
||||
Buffer.from(encryptedShareKey, 'base64') )
|
||||
}
|
||||
|
||||
return resolve({note, sharedNoteKey})
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
//
|
||||
// Update raw_text to have a shared password, encrypt text with this password
|
||||
//
|
||||
const sharedNoteSalt = cs.createSmallSalt()
|
||||
|
||||
//Encrypt note text with new password
|
||||
const textObject = JSON.stringify([note.title, note.text])
|
||||
const encryptedText = cs.encrypt(sharedNoteMasterKey, sharedNoteSalt, textObject)
|
||||
|
||||
//Update note raw text with new data
|
||||
db.promise()
|
||||
.query("UPDATE `application`.`note_raw_text` SET `text` = ?, `salt` = ? WHERE (`id` = ?)",
|
||||
[encryptedText, sharedNoteSalt, note.rawTextId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//
|
||||
// Update snippet using new shared password
|
||||
// + Save shared password (encrypted with public key)
|
||||
//
|
||||
const sharedNoteSnippetSalt = cs.createSmallSalt()
|
||||
const snippet = JSON.stringify([note.title, note.text.substring(0, 500)])
|
||||
const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, sharedNoteSnippetSalt, snippet)
|
||||
|
||||
//Encrypt shared password for this user
|
||||
const encryptedSharedKey = crypto.publicEncrypt(userPublicKey, Buffer.from(sharedNoteMasterKey, 'utf8')).toString('base64')
|
||||
|
||||
//Update note snippet for current user with public key encoded snippet
|
||||
return db.promise().query('UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 2 WHERE id = ? AND user_id = ?',
|
||||
[encryptedSnippet, sharedNoteSnippetSalt, encryptedSharedKey, noteId, userId])
|
||||
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
return resolve({note, 'sharedNoteKey':sharedNoteMasterKey})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
//Remove private shared key, encrypt note with users master password
|
||||
ShareNote.migrateToNormal = (userId, noteId, masterKey) => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
Note.get(userId, noteId, masterKey)
|
||||
.then(noteObject => {
|
||||
|
||||
const salt = cs.createSmallSalt()
|
||||
const snippetSalt = cs.createSmallSalt()
|
||||
|
||||
const snippetObj = JSON.stringify([noteObject.title, noteObject.text.substring(0, 500)])
|
||||
const snippet = cs.encrypt(masterKey, snippetSalt, snippetObj)
|
||||
|
||||
const textObject = JSON.stringify([noteObject.title, noteObject.text])
|
||||
const encryptedText = cs.encrypt(masterKey, salt, textObject)
|
||||
|
||||
db.promise()
|
||||
.query(`UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 0 WHERE id = ? AND user_id = ?`,
|
||||
[snippet, snippetSalt, null, noteId, userId])
|
||||
.then((r,f) => {
|
||||
|
||||
db.promise()
|
||||
.query('UPDATE note_raw_text SET text = ?, salt = ? WHERE id = ?',
|
||||
[encryptedText, salt, noteObject.rawTextId])
|
||||
.then(() => {
|
||||
return resolve(true)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ShareNote.decryptSharedKey = (userId, noteId, masterKey) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let userPrivateKey = null
|
||||
|
||||
const User = require('@models/User')
|
||||
User.generateKeypair(userId, masterKey)
|
||||
.then( ({publicKey, privateKey}) => {
|
||||
|
||||
userPrivateKey = privateKey
|
||||
|
||||
return Note.get(userId, noteId, masterKey)
|
||||
})
|
||||
.then(noteObject => {
|
||||
|
||||
//Shared notes use encrypted key - decrypt key then return it.
|
||||
const encryptedShareKey = noteObject.encrypted_share_password_key
|
||||
|
||||
if(encryptedShareKey != null && userPrivateKey != null){
|
||||
const currentNoteKey = crypto.privateDecrypt(userPrivateKey,
|
||||
Buffer.from(encryptedShareKey, 'base64') )
|
||||
|
||||
return resolve(currentNoteKey)
|
||||
|
||||
} else {
|
||||
return resolve(null)
|
||||
}
|
||||
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Get users who see a shared note
|
||||
ShareNote.getShareInfo = (userId, noteId, rawTextId) => {
|
||||
ShareNote.getUsers = (userId, rawTextId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let shareUsers = []
|
||||
let shareStatus = 0
|
||||
|
||||
db.promise()
|
||||
.query(`
|
||||
SELECT username, note.id as noteId
|
||||
@@ -298,14 +101,47 @@ ShareNote.getShareInfo = (userId, noteId, rawTextId) => {
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Return a list of user names
|
||||
shareUsers = rows[0]
|
||||
return db.promise().query('SELECT shared FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
shareStatus = rows[0][0]['shared']
|
||||
|
||||
return resolve({ shareStatus, shareUsers })
|
||||
return resolve (rows[0])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Remove a user from a shared note
|
||||
ShareNote.removeUser = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const Note = require('@models/Note')
|
||||
|
||||
let rawTextId = null
|
||||
let removeUserId = null
|
||||
|
||||
//note.id = noteId, share_user_id = userId
|
||||
db.promise()
|
||||
.query('SELECT note_raw_text_id, user_id FROM note WHERE id = ? AND share_user_id = ?', [noteId, userId])
|
||||
.then( (rows, fields) => {
|
||||
|
||||
rawTextId = rows[0][0]['note_raw_text_id']
|
||||
removeUserId = rows[0][0]['user_id']
|
||||
|
||||
//Delete note entry for other user - remove users access
|
||||
if(removeUserId && Number.isInteger(removeUserId)){
|
||||
//Delete this users access to the note
|
||||
return Note.delete(removeUserId, noteId)
|
||||
|
||||
} else {
|
||||
|
||||
return new Promise((resolve, reject) => { resolve(true) })
|
||||
}
|
||||
|
||||
})
|
||||
.then(stuff => {
|
||||
resolve(true)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
resolve(false)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
@@ -17,11 +17,29 @@ Tag.userTags = (userId, searchQuery, searchTags, fastFilters) => {
|
||||
FROM tag
|
||||
JOIN note_tag ON tag.id = note_tag.tag_id
|
||||
JOIN note ON note.id = note_tag.note_id
|
||||
WHERE note_tag.user_id = ? AND note.trashed = 0
|
||||
WHERE note_tag.user_id = ?
|
||||
`
|
||||
|
||||
//Show shared notes
|
||||
if(fastFilters && fastFilters.onlyShowSharedNotes == 1){
|
||||
query += ' AND note.share_user_id IS NOT NULL' //Show Archived
|
||||
} else {
|
||||
query += ' AND note.share_user_id IS NULL'
|
||||
}
|
||||
|
||||
if(fastFilters && fastFilters.onlyShowEncrypted == 1){
|
||||
query += ' AND note.encrypted = 1' //Show Archived
|
||||
}
|
||||
|
||||
//Show archived notes, only if fast filter is set, default to not archived
|
||||
if(fastFilters && fastFilters.onlyArchived == 1){
|
||||
query += ' AND note.archived = 1' //Show Archived
|
||||
} else {
|
||||
query += ' AND note.archived = 0' //Exclude archived
|
||||
}
|
||||
|
||||
query += ` GROUP BY tag.id
|
||||
ORDER BY LOWER(TRIM(text)) ASC`
|
||||
ORDER BY usages DESC, text ASC`
|
||||
|
||||
|
||||
db.promise()
|
||||
@@ -57,8 +75,6 @@ Tag.addToNote = (userId, noteId, tagText) => {
|
||||
.then( newTagId => {
|
||||
Tag.associateWithNote(userId, noteId, newTagId)
|
||||
.then( result => {
|
||||
|
||||
SocketIo.to(userId).emit('new_note_text_saved', {noteId})
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
@@ -138,33 +154,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'
|
||||
//
|
||||
@@ -205,24 +194,16 @@ Tag.suggest = (userId, noteId, tagText) => {
|
||||
tagText += '%'
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let params = [userId, tagText]
|
||||
let query = `SELECT tag.id, text FROM note_tag
|
||||
db.promise()
|
||||
.query(`SELECT text FROM note_tag
|
||||
JOIN tag ON note_tag.tag_id = tag.id
|
||||
WHERE note_tag.user_id = ?
|
||||
AND tag.text LIKE ?`
|
||||
|
||||
if(noteId && noteId > 0){
|
||||
params.push(noteId)
|
||||
query += `AND note_tag.tag_id NOT IN
|
||||
(SELECT note_tag.tag_id FROM note_tag WHERE note_tag.note_id = ?)`
|
||||
}
|
||||
|
||||
query += ` GROUP BY text, id LIMIT 6`
|
||||
|
||||
|
||||
db.promise()
|
||||
.query(query, params)
|
||||
AND tag.text LIKE ?
|
||||
AND note_tag.tag_id NOT IN (
|
||||
SELECT note_tag.tag_id FROM note_tag WHERE note_tag.note_id = ?
|
||||
)
|
||||
GROUP BY text
|
||||
LIMIT 6`, [userId, tagText, noteId])
|
||||
.then((rows, fields) => {
|
||||
resolve(rows[0]) //Return new ID
|
||||
})
|
||||
|
||||
@@ -1,115 +1,47 @@
|
||||
const crypto = require('crypto')
|
||||
var crypto = require('crypto')
|
||||
|
||||
const Note = require('@models/Note')
|
||||
|
||||
const db = require('@config/database')
|
||||
const Auth = require('@helpers/Auth')
|
||||
const cs = require('@helpers/CryptoString')
|
||||
const speakeasy = require('speakeasy')
|
||||
let db = require('@config/database')
|
||||
let Auth = require('@helpers/Auth')
|
||||
|
||||
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
|
||||
//Issues login token
|
||||
User.login = (username, password, authToken = null) => {
|
||||
User.login = (username, password) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const lowerName = username.toLowerCase()
|
||||
|
||||
let statusObject = {
|
||||
success: false,
|
||||
token: null,
|
||||
userId: null,
|
||||
verificationRequired: false,
|
||||
message: 'Incorrect Username or Password'
|
||||
}
|
||||
const lowerName = username.toLowerCase();
|
||||
|
||||
db.promise()
|
||||
.query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//
|
||||
// Login User
|
||||
//
|
||||
if(rows[0].length == 1){
|
||||
//Pull out user data from database results
|
||||
const lookedUpUser = rows[0][0];
|
||||
|
||||
//Pull out user data from database results
|
||||
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
|
||||
//User not found, create a new account with set data
|
||||
if(rows[0].length == 0){
|
||||
User.create(lowerName, password)
|
||||
.then(loginToken => {
|
||||
resolve(loginToken)
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
const salt = Buffer.from(lookedUpUser.salt, 'binary')
|
||||
crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
|
||||
if(delivered_key.toString('hex') === lookedUpUser.password){
|
||||
|
||||
User.generateMasterKey(lookedUpUser.id, password)
|
||||
.then( result => User.getMasterKey(lookedUpUser.id, password))
|
||||
.then(masterKey => {
|
||||
|
||||
User.generateKeypair(lookedUpUser.id, masterKey)
|
||||
.then(({publicKey, privateKey}) => {
|
||||
|
||||
//Passback a json web token
|
||||
Auth.createToken(lookedUpUser.id, masterKey)
|
||||
.then(token => {
|
||||
|
||||
statusObject['token'] = token
|
||||
statusObject['userId'] = lookedUpUser.id
|
||||
statusObject['success'] = true
|
||||
|
||||
return resolve(statusObject)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
} else {
|
||||
return resolve(statusObject)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
//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)
|
||||
return
|
||||
}
|
||||
|
||||
//hash the password and check for a match
|
||||
const salt = new Buffer(lookedUpUser.salt, 'binary')
|
||||
crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
|
||||
if(delivered_key.toString('hex') === lookedUpUser.password){
|
||||
|
||||
//Passback a json web token
|
||||
const token = Auth.createToken(lookedUpUser.id)
|
||||
resolve(token)
|
||||
|
||||
} else {
|
||||
|
||||
reject('Password does not match database')
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
@@ -118,7 +50,7 @@ User.login = (username, password, authToken = null) => {
|
||||
|
||||
//Create user account
|
||||
//Issues login token
|
||||
User.register = (username, password) => {
|
||||
User.create = (username, password) => {
|
||||
|
||||
//For some reason, username won't get into the promise. But password will @TODO figure this out
|
||||
const lowerName = username.toLowerCase().trim()
|
||||
@@ -139,7 +71,7 @@ User.register = (username, password) => {
|
||||
shasum.update(''+otherRandomInt) //Update Hasd
|
||||
|
||||
const saltString = shasum.digest('hex')
|
||||
const salt = Buffer.from(saltString, 'binary') //Generate Salt hash
|
||||
const salt = new Buffer(saltString, 'binary') //Generate Salt hash
|
||||
const iterations = 25000
|
||||
|
||||
crypto.pbkdf2(password, salt, iterations, 512, 'sha512', function(err, delivered_key) {
|
||||
@@ -155,36 +87,25 @@ User.register = (username, password) => {
|
||||
created: currentDate
|
||||
};
|
||||
|
||||
let userId = null
|
||||
let newMasterKey = null
|
||||
|
||||
db.promise()
|
||||
.query('INSERT INTO user SET ?', new_user)
|
||||
.then((rows, fields) => {
|
||||
|
||||
userId = rows[0].insertId
|
||||
return User.generateMasterKey(userId, password)
|
||||
})
|
||||
.then( result => {
|
||||
if(rows[0].affectedRows == 1){
|
||||
|
||||
return User.getMasterKey(userId, password)
|
||||
})
|
||||
.then(masterKey => {
|
||||
newMasterKey = masterKey
|
||||
return User.generateKeypair(userId, newMasterKey)
|
||||
})
|
||||
.then(({publicKey, privateKey}) => {
|
||||
const newUserId = rows[0].insertId
|
||||
const loginToken = Auth.createToken(newUserId)
|
||||
resolve(loginToken)
|
||||
|
||||
return Auth.createToken(userId, newMasterKey)
|
||||
})
|
||||
.then(token => {
|
||||
|
||||
return resolve({token, userId})
|
||||
} else {
|
||||
//Emit Error to user
|
||||
reject('New user could not be created')
|
||||
}
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
} else {
|
||||
return reject('Username already in use.')
|
||||
reject('Username already in use.')
|
||||
}//END user create
|
||||
})
|
||||
.catch(console.log)
|
||||
@@ -194,34 +115,29 @@ User.register = (username, password) => {
|
||||
}
|
||||
|
||||
//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) => {
|
||||
|
||||
let countTotals = {
|
||||
tags: {}
|
||||
}
|
||||
// const userHash = cs.hash(String(userId)).toString('base64')
|
||||
let countTotals = {}
|
||||
|
||||
db.promise().query(
|
||||
`SELECT
|
||||
SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes,
|
||||
SUM(trashed = 1) AS trashedNotes,
|
||||
SUM(share_user_id IS NULL && trashed = 0 AND quick_note < 2) AS totalNotes,
|
||||
SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount,
|
||||
SUM(share_user_id != ? && trashed = 0) AS sharedToNotes
|
||||
SUM(pinned = 1 && archived = 0 && share_user_id IS NULL) AS pinnedNotes,
|
||||
SUM(archived = 1 && share_user_id IS NULL) AS archivedNotes,
|
||||
SUM(encrypted = 1) AS encryptedNotes,
|
||||
SUM(share_user_id IS NULL) AS totalNotes,
|
||||
SUM(share_user_id != ?) AS sharedToNotes,
|
||||
SUM( (share_user_id != ? && opened IS null) || (share_user_id != ? && note_raw_text.updated > opened) ) AS unreadNotes
|
||||
FROM note
|
||||
LEFT JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
WHERE user_id = ?`, [userId, userId, userId, userId])
|
||||
.then( (rows, fields) => {
|
||||
|
||||
Object.assign(countTotals, rows[0][0]) //combine results
|
||||
//
|
||||
// @TODO - Figured out if this is useful
|
||||
// We want, notes shared with user and note user has shared
|
||||
//
|
||||
|
||||
return db.promise().query(
|
||||
`SELECT count(id) AS sharedFromNotes
|
||||
FROM note WHERE shared = 2 AND user_id = ? AND trashed = 0`, [userId]
|
||||
FROM note WHERE share_user_id = ?`, [userId]
|
||||
)
|
||||
})
|
||||
.then( (rows, fields) => {
|
||||
@@ -241,353 +157,14 @@ User.getCounts = (userId, extendedOptions) => {
|
||||
|
||||
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
|
||||
Object.keys(countTotals).forEach( key => {
|
||||
const count = parseInt(countTotals[key])
|
||||
countTotals[key] = count ? count : 0
|
||||
})
|
||||
|
||||
//Build out tags object
|
||||
let tagsObject = {}
|
||||
rows[0].forEach(tagRow => {
|
||||
tagsObject[tagRow['text']] = {'id':tagRow.id, 'uses':tagRow.uses}
|
||||
})
|
||||
|
||||
//Assign after counts are updated
|
||||
countTotals['tags'] = tagsObject
|
||||
|
||||
countTotals['currentVersion'] = version
|
||||
|
||||
// Allow for extended options set on page load
|
||||
if(extendedOptions){
|
||||
|
||||
db.promise().query(
|
||||
`SELECT updated FROM note
|
||||
JOIN note_raw_text ON note_raw_text.id = note.note_raw_text_id
|
||||
WHERE note.quick_note = 2
|
||||
AND user_id = ?`, [userId])
|
||||
.then( (rows, fields) => {
|
||||
|
||||
|
||||
|
||||
if(rows[0][0] && rows[0][0].updated){
|
||||
const lastOpened = rows[0][0].updated
|
||||
const timeDiff = Math.round(((+new Date) - (lastOpened))/1000)
|
||||
const hoursInSeconds = (12 * 60 * 60) //12 hours
|
||||
|
||||
// Show metric tracking button if its been 12 hours since last entry
|
||||
if(lastOpened && timeDiff > hoursInSeconds){
|
||||
countTotals['showTrackMetricsButton'] = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resolve(countTotals)
|
||||
})
|
||||
|
||||
|
||||
} else {
|
||||
resolve(countTotals)
|
||||
}
|
||||
|
||||
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) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if(!userId || !password){
|
||||
reject('Need userId and password to generate key')
|
||||
}
|
||||
|
||||
db.promise()
|
||||
.query('SELECT count(id) as total FROM user_key WHERE user_id = ?', [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Entry already exists, you good.
|
||||
if(rows[0][0]['total'] > 0){
|
||||
return resolve(true)
|
||||
// throw new Error('User Encryption key already exists')
|
||||
} else {
|
||||
// Generate user key, its big and random
|
||||
const masterPassword = cs.createSmallSalt()
|
||||
|
||||
//Generate a salt because it wants it
|
||||
const salt = cs.createSmallSalt()
|
||||
|
||||
// Encrypt master password
|
||||
const encryptedMasterPassword = cs.encrypt(password, salt, masterPassword)
|
||||
|
||||
const created = Math.round((+new Date)/1000)
|
||||
|
||||
db.promise()
|
||||
.query(
|
||||
'INSERT INTO user_key (`user_id`, `salt`, `key`, `created`) VALUES (?, ?, ?, ?);',
|
||||
[userId, salt, encryptedMasterPassword, created]
|
||||
)
|
||||
.then(results => {
|
||||
return resolve(true)
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Create Master Password Error')
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
User.getMasterKey = (userId, password) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if(!userId || !password){
|
||||
reject('Need userId and password to fetch key')
|
||||
}
|
||||
|
||||
db.promise().query('SELECT * FROM user_key WHERE user_id = ? LIMIT 1', [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
const row = rows[0][0]
|
||||
|
||||
if(!rows[0] || rows[0].length == 0 || rows[0][0] == undefined){
|
||||
return reject('Row or salt or something not set')
|
||||
}
|
||||
|
||||
const masterKey = cs.decrypt(password, row['salt'], row['key'])
|
||||
|
||||
if(masterKey == null){
|
||||
return reject('Unable to decrypt key')
|
||||
}
|
||||
|
||||
return resolve(masterKey)
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
User.generateKeypair = (userId, masterKey) => {
|
||||
|
||||
let publicKey = null
|
||||
let privateKey = null
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise().query('SELECT * FROM user_key WHERE user_id = ?', [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
const row = rows[0][0]
|
||||
|
||||
const salt = row['salt']
|
||||
publicKey = row['public_key']
|
||||
privateKey = row['private_key_encrypted']
|
||||
|
||||
if(row['public_key'] == null){
|
||||
const keyPair = crypto.generateKeyPairSync('rsa', {
|
||||
modulusLength: 1024,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem'
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem'
|
||||
}
|
||||
})
|
||||
|
||||
publicKey = keyPair.publicKey
|
||||
privateKey = keyPair.privateKey
|
||||
const privateKeyEncrypted = cs.encrypt(masterKey, salt, privateKey)
|
||||
|
||||
db.promise()
|
||||
.query(
|
||||
'UPDATE user_key SET `public_key` = ?, `private_key_encrypted` = ? WHERE user_id = ?;',
|
||||
[publicKey, privateKeyEncrypted, userId]
|
||||
)
|
||||
.then((rows, fields)=>{
|
||||
|
||||
return resolve({publicKey, privateKey})
|
||||
|
||||
})
|
||||
|
||||
} else {
|
||||
|
||||
//Decrypt private key
|
||||
privateKey = cs.decrypt(masterKey, salt, privateKey)
|
||||
|
||||
return resolve({publicKey, privateKey})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
User.getPublicKey = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise().query('SELECT public_key FROM user_key WHERE user_id = ?', [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
const row = rows[0][0]
|
||||
return resolve(row['public_key'])
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
User.getPrivateKey = (userId, masterKey) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise().query('SELECT salt, private_key_encrypted FROM user_key WHERE user_id = ?', [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
const row = rows[0][0]
|
||||
|
||||
const salt = row['salt']
|
||||
privateKey = row['private_key_encrypted']
|
||||
|
||||
//Decrypt private key
|
||||
privateKey = cs.decrypt(masterKey, salt, privateKey)
|
||||
|
||||
return resolve(privateKey)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
User.getByUserName = (username) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise().query('SELECT * FROM user WHERE username = ? LIMIT 1', [username.toLowerCase()])
|
||||
.then((rows, fields) => {
|
||||
resolve(rows[0][0])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
if(!userId || !password){
|
||||
return new Promise((resolve, reject) => {
|
||||
return resolve('Missing User ID or Password. No Action Taken.')
|
||||
})
|
||||
}
|
||||
|
||||
//Verify user is correct by decryptig master key with password
|
||||
|
||||
let deletePromises = []
|
||||
|
||||
//Delete all notes and raw text
|
||||
let noteDelete = db.promise().query(`
|
||||
DELETE note, note_raw_text
|
||||
FROM note
|
||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
WHERE note.user_id = ?
|
||||
`,[userId])
|
||||
deletePromises.push(noteDelete)
|
||||
|
||||
//Delete user entry
|
||||
let userDelete = db.promise().query(`
|
||||
DELETE FROM user WHERE id = ?
|
||||
`,[userId])
|
||||
deletePromises.push(userDelete)
|
||||
|
||||
//Delete user_key, encrypted search index
|
||||
let tables = ['user_key', 'user_encrypted_search_index']
|
||||
tables.forEach(tableName => {
|
||||
|
||||
const query = `DELETE FROM ${tableName} WHERE user_id = ?`
|
||||
const deleteQuery = db.promise().query(query, [userId])
|
||||
deletePromises.push(deleteQuery)
|
||||
})
|
||||
|
||||
//Remove all note attachments and files
|
||||
|
||||
return Promise.all(deletePromises)
|
||||
}
|
||||
@@ -4,29 +4,21 @@ var multer = require('multer')
|
||||
var upload = multer({ dest: '../staticFiles/' }) //@TODO make this a global value
|
||||
let router = express.Router()
|
||||
|
||||
let Attachment = require('@models/Attachment')
|
||||
let Attachment = require('@models/Attachment');
|
||||
let Note = require('@models/Note')
|
||||
|
||||
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){
|
||||
if(userId = req.headers.userId){
|
||||
userId = req.headers.userId
|
||||
masterKey = req.headers.masterKey
|
||||
next()
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
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) )
|
||||
})
|
||||
|
||||
@@ -35,6 +27,11 @@ router.post('/textsearch', function (req, res) {
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
router.post('/get', function (req, res) {
|
||||
Attachment.forNote(userId, req.body.noteId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
router.post('/update', function (req, res) {
|
||||
Attachment.update(userId, req.body.attachmentId, req.body.updatedText, req.body.noteId)
|
||||
.then( result => {
|
||||
@@ -55,31 +52,12 @@ router.post('/upload', upload.single('file'), function (req, res, next) {
|
||||
|
||||
Attachment.processUploadedFile(userId, noteId, req.file)
|
||||
.then( uploadResults => {
|
||||
res.send(uploadResults)
|
||||
//Reindex note, attachment may have had text
|
||||
Note.reindex(userId, noteId)
|
||||
.then( data => {res.send(uploadResults)})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
//
|
||||
// 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
|
||||
@@ -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
|
||||
@@ -1,90 +1,78 @@
|
||||
var express = require('express')
|
||||
var router = express.Router()
|
||||
|
||||
let Note = require('@models/Note')
|
||||
let User = require('@models/User')
|
||||
let ShareNote = require('@models/ShareNote')
|
||||
let Notes = require('@models/Note');
|
||||
let ShareNote = require('@models/ShareNote');
|
||||
|
||||
let userId = null
|
||||
let masterKey = null
|
||||
let socket = 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()
|
||||
}
|
||||
if(req.headers.socket){
|
||||
// socket = req.
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
//
|
||||
// Note actions
|
||||
//
|
||||
router.post('/get', function (req, res) {
|
||||
Note.get(userId, req.body.noteId, masterKey)
|
||||
.then( noteObject => {
|
||||
|
||||
delete noteObject.snippet_salt
|
||||
delete noteObject.salt
|
||||
delete noteObject.encrypted_share_password_key
|
||||
|
||||
res.send(noteObject)
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/delete', function (req, res) {
|
||||
Note.delete(userId, req.body.noteId, masterKey)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
router.post('/create', function (req, res) {
|
||||
Note.create(userId, req.body.title, req.body.text, masterKey)
|
||||
.then( id => res.send({id}) )
|
||||
})
|
||||
|
||||
router.post('/update', function (req, res) {
|
||||
Note.update(userId, req.body.noteId, req.body.text, req.body.title, req.body.color, req.body.pinned, req.body.archived, req.body.hash, masterKey)
|
||||
.then( id => res.send({id}) )
|
||||
})
|
||||
|
||||
router.post('/search', function (req, res) {
|
||||
Note.search(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters, masterKey)
|
||||
.then( NoteAndTags => {
|
||||
res.send(NoteAndTags)
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/reindex', function (req, res) {
|
||||
Note.reindex(userId, masterKey)
|
||||
// req.io.emit('welcome_homie', 'Welcome, dont poop from excitement')
|
||||
Notes.get(userId, req.body.noteId, req.body.password)
|
||||
.then( data => {
|
||||
//Join room when user opens note
|
||||
// req.io.join('note_room')
|
||||
res.send(data)
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/delete', function (req, res) {
|
||||
Notes.delete(userId, req.body.noteId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
router.post('/create', function (req, res) {
|
||||
Notes.create(userId, req.body.title, req.body.text)
|
||||
.then( id => res.send({id}) )
|
||||
})
|
||||
|
||||
router.post('/update', function (req, res) {
|
||||
Notes.update(req.io, userId, req.body.noteId, req.body.text, req.body.title, req.body.color, req.body.pinned, req.body.archived, req.body.password, req.body.hint)
|
||||
.then( id => res.send({id}) )
|
||||
})
|
||||
|
||||
router.post('/search', function (req, res) {
|
||||
Notes.search(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters)
|
||||
.then( notesAndTags => {
|
||||
res.send(notesAndTags)
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/difftext', function (req, res) {
|
||||
Notes.getDiffText(userId, req.body.noteId, req.body.text, req.body.updated)
|
||||
.then( fullDiffText => {
|
||||
//Response should be full diff text
|
||||
res.send(fullDiffText)
|
||||
})
|
||||
})
|
||||
|
||||
//
|
||||
// Update single note attributes
|
||||
//
|
||||
router.post('/setpinned', function (req, res) {
|
||||
Note.setPinned(userId, req.body.noteId, req.body.pinned)
|
||||
Notes.setPinned(userId, req.body.noteId, req.body.pinned)
|
||||
.then( results => {
|
||||
res.send(results)
|
||||
})
|
||||
})
|
||||
router.post('/setarchived', function (req, res) {
|
||||
Note.setArchived(userId, req.body.noteId, req.body.archived)
|
||||
.then( results => {
|
||||
res.send(results)
|
||||
})
|
||||
})
|
||||
router.post('/settrashed', function (req, res) {
|
||||
Note.setTrashed(userId, req.body.noteId, req.body.trashed, masterKey)
|
||||
Notes.setArchived(userId, req.body.noteId, req.body.archived)
|
||||
.then( results => {
|
||||
res.send(results)
|
||||
})
|
||||
@@ -93,44 +81,40 @@ router.post('/settrashed', function (req, res) {
|
||||
//
|
||||
// Share Note Actions
|
||||
//
|
||||
router.post('/getshareinfo', function (req, res) {
|
||||
ShareNote.getShareInfo(userId, req.body.noteId, req.body.rawTextId)
|
||||
router.post('/getshareusers', function (req, res) {
|
||||
ShareNote.getUsers(userId, req.body.rawTextId)
|
||||
.then(results => res.send(results))
|
||||
})
|
||||
|
||||
router.post('/shareadduser', function (req, res) {
|
||||
// ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username, masterKey)
|
||||
User.getByUserName(req.body.username)
|
||||
.then( user => {
|
||||
return ShareNote.addUserToSharedNote(userId, req.body.noteId, user.id, masterKey)
|
||||
})
|
||||
ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username)
|
||||
.then( ({success, shareUserId}) => {
|
||||
|
||||
//Emit update count event to user shared with - so they see the note in real time
|
||||
req.io.to(shareUserId).emit('update_counts')
|
||||
|
||||
res.send(success)
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/shareremoveuser', function (req, res) {
|
||||
// (userId, noteId, shareNoteUserId, shareUserId, masterKey)
|
||||
ShareNote.removeUserFromSharedNote(userId, req.body.noteId, req.body.shareUserNoteId, masterKey)
|
||||
ShareNote.removeUser(userId, req.body.noteId)
|
||||
.then(results => res.send(results))
|
||||
})
|
||||
|
||||
router.post('/enableshare', function (req, res) {
|
||||
//Create Shared Encryption Key for Note
|
||||
ShareNote.migrateToShared(userId, req.body.noteId, masterKey)
|
||||
.then(results => res.send(true))
|
||||
})
|
||||
router.post('/getsharekey', function (req, res) {
|
||||
//Get Shared Key for a note
|
||||
ShareNote.decryptSharedKey(userId, req.body.noteId, masterKey)
|
||||
.then(results => res.send(results))
|
||||
})
|
||||
router.post('/disableshare', function (req, res) {
|
||||
//Removed shared encryption key from note
|
||||
ShareNote.migrateToNormal(userId, req.body.noteId, masterKey)
|
||||
.then(results => res.send(true))
|
||||
|
||||
//
|
||||
// Testing Action
|
||||
//
|
||||
//Reindex all notes. Not a very good function, not public
|
||||
router.get('/reindex5yu43prchuj903mrc', function (req, res) {
|
||||
|
||||
Notes.migrateNoteTextToNewTable().then(status => {
|
||||
return res.send(status)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
module.exports = router
|
||||
@@ -1,85 +1,15 @@
|
||||
var express = require('express')
|
||||
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 Notes = require('@models/Note')
|
||||
|
||||
router.post('/note', function (req, res) {
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Public Note action
|
||||
//
|
||||
const sharedNoteLimiter = rateLimit({
|
||||
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)
|
||||
.then(results => res.send(results))
|
||||
Notes.getShared(req.body.noteId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
//
|
||||
// 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
|
||||
@@ -2,40 +2,32 @@ var express = require('express')
|
||||
var router = express.Router()
|
||||
|
||||
let QuickNote = require('@models/QuickNote');
|
||||
|
||||
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){
|
||||
if(userId = req.headers.userId){
|
||||
userId = req.headers.userId
|
||||
masterKey = req.headers.masterKey
|
||||
next()
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
//Get quick note text
|
||||
router.post('/get', function (req, res) {
|
||||
QuickNote.get(userId, masterKey)
|
||||
QuickNote.get(userId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
//Push text to quick note
|
||||
router.post('/update', function (req, res) {
|
||||
QuickNote.update(userId, req.body.pushText, masterKey)
|
||||
QuickNote.update(userId, req.body.pushText)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
//Push text to quick note
|
||||
router.post('/new', function (req, res) {
|
||||
QuickNote.newNote(userId)
|
||||
//Change quick note to a new note
|
||||
router.post('/change', function (req, res) {
|
||||
QuickNote.change(userId, req.body.noteId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
var express = require('express')
|
||||
var router = express.Router()
|
||||
|
||||
let Tags = require('@models/Tag')
|
||||
|
||||
let Tags = require('@models/Tag');
|
||||
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){
|
||||
if(userId = req.headers.userId){
|
||||
userId = req.headers.userId
|
||||
masterKey = req.headers.masterKey
|
||||
next()
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
//Get the latest notes the user has created
|
||||
@@ -50,12 +42,6 @@ router.post('/get', function (req, res) {
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
//Get the latest notes the user has created
|
||||
router.post('/fornote', function (req, res) {
|
||||
Tags.fornote(userId, req.body.noteId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
//Get all the tags for this user in order of usage
|
||||
router.post('/usertags', function (req, res) {
|
||||
Tags.userTags(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters)
|
||||
|
||||
@@ -1,86 +1,56 @@
|
||||
var express = require('express')
|
||||
var router = express.Router()
|
||||
|
||||
const User = require('@models/User')
|
||||
const Auth = require('@helpers/Auth')
|
||||
const cs = require('@helpers/CryptoString')
|
||||
|
||||
let userId = null
|
||||
let masterKey = null
|
||||
let User = require('@models/User');
|
||||
|
||||
// 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())
|
||||
next()
|
||||
})
|
||||
// define the home page route
|
||||
router.get('/', function (req, res) {
|
||||
res.send('User Home Page ' + User.getUsername())
|
||||
})
|
||||
// define the about route
|
||||
router.get('/about', function (req, res) {
|
||||
User.getUsername(req.headers.userId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
// define the login route
|
||||
router.post('/login', function (req, res) {
|
||||
|
||||
//Session key is required to continue
|
||||
if(!req.headers.sessionId){
|
||||
next('Unauthorized')
|
||||
//Pull out variables we want
|
||||
const username = req.body.username
|
||||
const password = req.body.password
|
||||
|
||||
let returnData = {
|
||||
success: false,
|
||||
token: '',
|
||||
username: ''
|
||||
}
|
||||
|
||||
if(req.headers.userId){
|
||||
userId = req.headers.userId
|
||||
masterKey = req.headers.masterKey
|
||||
next()
|
||||
}
|
||||
})
|
||||
User.login(username, password)
|
||||
.then(function(loginToken){
|
||||
|
||||
// Logout User
|
||||
router.post('/logout', function (req, res) {
|
||||
|
||||
User.logout(req.headers.sessionId)
|
||||
.then( returnData => {
|
||||
res.send(true)
|
||||
})
|
||||
})
|
||||
|
||||
// change password
|
||||
router.post('/changepassword', function (req, res) {
|
||||
|
||||
User.changePassword(req.headers.userId, req.body.currentPass, req.body.newPass)
|
||||
.then( returnData => {
|
||||
res.send(returnData)
|
||||
})
|
||||
})
|
||||
|
||||
//Revoke all active session keys for user
|
||||
router.post('/revokesessions', function(req, res) {
|
||||
|
||||
User.revokeActiveSessions(req.headers.userId, req.headers.sessionId)
|
||||
.then( returnData => {
|
||||
res.send(returnData)
|
||||
})
|
||||
//Return json web token to user
|
||||
returnData['success'] = true
|
||||
returnData['token'] = loginToken
|
||||
returnData['username'] = username
|
||||
|
||||
res.send(returnData)
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
res.send(returnData)
|
||||
})
|
||||
})
|
||||
|
||||
// fetch counts of users notes
|
||||
router.post('/totals', function (req, res) {
|
||||
User.getCounts(req.headers.userId, req.body.extendedOptions)
|
||||
User.getCounts(req.headers.userId)
|
||||
.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
|
||||
@@ -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()
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user