Compare commits
63 Commits
dev
...
e7d1cc7bc9
Author | SHA1 | Date | |
---|---|---|---|
|
e7d1cc7bc9 | ||
|
47fff0e1ee | ||
|
cca89a60d8 | ||
|
a56ade5b08 | ||
|
39f9a16fff | ||
|
6740200a33 | ||
|
e4fae23623 | ||
|
56d4664d0d | ||
|
d349fb8328 | ||
|
09cccf1983 | ||
|
97e7b011d9 | ||
|
fc1f3f81fe | ||
|
9c4fff7913 | ||
|
b0eee636b5 | ||
|
2861042485 | ||
|
1005913c0b | ||
|
c8033588dd | ||
|
bcb31e9af5 | ||
|
596e57eaf0 | ||
|
d91b0735fd | ||
|
71f909fb76 | ||
|
a44bca204c | ||
|
7c15427b3d | ||
|
ed4a5e5291 | ||
|
c11f1b1b6f | ||
|
0b5675e000 | ||
|
9309ea0821 | ||
|
5975ab6d68 | ||
|
3d6e527e3a | ||
|
88a0c7b26a | ||
|
1b14a8fd31 | ||
|
4cc6014581 | ||
|
196224d0b8 | ||
|
795f1b7d76 | ||
|
1600bd132c | ||
|
2a379f8a4e | ||
|
3ed26bcc03 | ||
|
282cbfe7bc | ||
|
b50aecdfca | ||
|
98f4695739 | ||
|
984ac6ccff | ||
|
f63c0c0d60 | ||
|
a478cbe11c | ||
|
99b69c234f | ||
|
f0b6d7b85e | ||
|
596703a963 | ||
|
21f606b480 | ||
|
b961a69a91 | ||
|
8d3762e106 | ||
|
b2f241dbba | ||
|
8833a213a7 | ||
|
f833845452 | ||
|
05152cd5a4 | ||
|
cf3289aac6 | ||
|
acf72ca67e | ||
|
7f93925f74 | ||
|
d2c1dedffb | ||
|
003c7e32b1 | ||
|
de646cf1de | ||
|
2828cc9462 | ||
|
f99d6ed430 | ||
|
4216c1825e | ||
|
8d07a8e11a |
6
.gitignore
vendored
@@ -7,9 +7,3 @@ pids
|
||||
*.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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
|
||||
'use strict'
|
||||
module.exports = {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
@@ -1,9 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<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"/>
|
||||
@@ -12,20 +11,14 @@
|
||||
<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;
|
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
25523
client/package-lock.json
generated
@@ -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",
|
||||
"fomantic-ui-css": "^2.8.6",
|
||||
"vue": "^2.5.2",
|
||||
"vue-router": "^3.3.4",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"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.26",
|
||||
"postcss-import": "^11.0.0",
|
||||
"postcss-loader": "^2.0.8",
|
||||
"postcss-url": "^7.2.1",
|
||||
"rimraf": "^2.6.0",
|
||||
"semver": "^5.3.0",
|
||||
"shelljs": "^0.7.6",
|
||||
"uglifyjs-webpack-plugin": "^1.1.1",
|
||||
"url-loader": "^0.5.8",
|
||||
"vue-loader": "^13.3.0",
|
||||
"vue-style-loader": "^3.0.1",
|
||||
"vue-template-compiler": "^2.5.2",
|
||||
"webpack": "^3.6.0",
|
||||
"webpack-bundle-analyzer": "^2.9.0",
|
||||
"webpack-dev-server": "^2.9.1",
|
||||
"webpack-merge": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
"not ie <= 8"
|
||||
]
|
||||
}
|
||||
|
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow:
|
@@ -1,53 +1,9 @@
|
||||
<template>
|
||||
<div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode == 2) }">
|
||||
|
||||
<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,21 +16,15 @@
|
||||
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,
|
||||
// loggedIn:
|
||||
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: '',
|
||||
blockUntilNextRequest: false //If token was just renewed, don't fetch more until next request
|
||||
}
|
||||
},
|
||||
|
||||
@@ -117,16 +67,6 @@ export default {
|
||||
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)
|
||||
}
|
||||
)
|
||||
@@ -172,17 +112,6 @@ export default {
|
||||
},
|
||||
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')
|
||||
@@ -200,11 +129,6 @@ export default {
|
||||
this.blockUntilNextRequest = true
|
||||
})
|
||||
|
||||
//Track users active sessions
|
||||
this.$io.on('update_active_user_count', countData => {
|
||||
this.$store.commit('setActiveSessions', countData)
|
||||
})
|
||||
|
||||
},
|
||||
computed: {
|
||||
loggedIn () {
|
||||
@@ -213,23 +137,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>
|
@@ -53,7 +53,7 @@ helpers.timeAgo = (time) => {
|
||||
if (typeof format[2] == 'string') {
|
||||
return format[list_choice]
|
||||
} else {
|
||||
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token
|
||||
return Math.floor(seconds / format[2]) + ' ' + format[1]// + ' ' + token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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,21 +11,10 @@
|
||||
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 {
|
||||
|
||||
@@ -50,11 +39,6 @@ body {
|
||||
|
||||
html {
|
||||
/*scrollbar-width: none;*/
|
||||
width: 100%;
|
||||
height:100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: var(--body_bg_color);
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
@@ -63,15 +47,6 @@ 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;
|
||||
}
|
||||
|
||||
/* Night mode modifiers */
|
||||
|
||||
/*Make images sepia in night mode */
|
||||
@@ -92,12 +67,9 @@ div.ui.basic.segment.no-fluf-segment {
|
||||
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
|
||||
body {
|
||||
color: var(--text_color);
|
||||
background: none;
|
||||
background-color: var(--body_bg_color);
|
||||
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
}
|
||||
#app {
|
||||
/* background: var(--body_bg_color);*/
|
||||
}
|
||||
|
||||
.ui.segment {
|
||||
color: var(--text_color);
|
||||
@@ -144,12 +116,6 @@ body {
|
||||
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);
|
||||
}
|
||||
.ui.icon.input > i.icon {
|
||||
color: var(--text_color);
|
||||
}
|
||||
@@ -178,21 +144,9 @@ div.ui.basic.green.label {
|
||||
border-color: var(--dark_border_color) !important;
|
||||
}
|
||||
/*Overwrites for modifiable theme color */
|
||||
i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
i.green.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);
|
||||
}
|
||||
@@ -206,9 +160,6 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
.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*/
|
||||
|
||||
@@ -300,27 +251,20 @@ 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: 500px;
|
||||
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;
|
||||
word-wrap: break-word;
|
||||
@@ -328,13 +272,6 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
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 {
|
||||
@@ -356,14 +293,9 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.note-card-text code,
|
||||
.squire-box code,
|
||||
.note-card-text pre,
|
||||
.squire-box pre {
|
||||
/*word-wrap: break-word;*/
|
||||
display: inline-block;
|
||||
border-left: 2px solid var(--main-accent);
|
||||
padding-left: 15px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.note-card-text p,
|
||||
.squire-box p {
|
||||
@@ -375,10 +307,6 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
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 +322,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,53 +331,35 @@ 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";
|
||||
content: "\f058";
|
||||
color: var(--main-accent);
|
||||
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,
|
||||
@@ -555,6 +431,10 @@ padding-right: 10px;
|
||||
/* adjust checkboxes for mobile. Make them a little bigger, easier to click */
|
||||
@media only screen and (max-width: 740px) {
|
||||
|
||||
.squire-box {
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.ui.button.shrinking {
|
||||
font-size: 0.85714286rem;
|
||||
margin: 0 3px;
|
||||
@@ -567,40 +447,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 {
|
||||
.squire-box ul > li:before {
|
||||
|
||||
/*empty checkmark*/
|
||||
/*font-family: 'Icons';*/
|
||||
/*content: "\f058";*/
|
||||
content: "\f111";
|
||||
font-family: outline-icons;
|
||||
|
||||
content: "\F0C8";
|
||||
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";
|
||||
|
||||
content: "\f058";
|
||||
color: var(--main-accent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Remove indent line on mobile */
|
||||
.note-card-text > ol > ol,
|
||||
.squire-box > ol > ol,
|
||||
.note-card-text > ul > ul,
|
||||
.squire-box > ul > ul
|
||||
{
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -630,10 +500,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 +511,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
|
||||
@@ -694,34 +539,36 @@ padding-right: 10px;
|
||||
position: absolute;
|
||||
content: '';
|
||||
font-size: 1rem;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #1B1C1D;
|
||||
width: 0.71428571em;
|
||||
height: 0.71428571em;
|
||||
background: #FFFFFF;
|
||||
-webkit-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
z-index: 1901;
|
||||
-webkit-box-shadow: 1px 1px 0 0 #bababc;
|
||||
box-shadow: 1px 1px 0 0 #bababc;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #D4D4D5;
|
||||
line-height: 1.4285em;
|
||||
max-width: none;
|
||||
background: #1B1C1D;
|
||||
padding: 0.5em;
|
||||
background: #FFFFFF;
|
||||
padding: 0.833em 1em;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
/*color: var(--main-accent);*/
|
||||
color: white;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
border-radius: 0.28571429rem;
|
||||
-webkit-box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
|
||||
box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
|
||||
z-index: 1900;
|
||||
}
|
||||
|
||||
@@ -731,7 +578,7 @@ padding-right: 10px;
|
||||
right: auto;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
background: #1B1C1D;
|
||||
background: #FFFFFF;
|
||||
margin-left: -0.07142857rem;
|
||||
margin-bottom: 0.14285714rem;
|
||||
}
|
||||
@@ -749,7 +596,7 @@ padding-right: 10px;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
/*transition: opacity 0.2s ease;*/
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
[data-tooltip]:before {
|
||||
-webkit-transform: rotate(45deg) scale(0) !important;
|
||||
@@ -772,13 +619,93 @@ padding-right: 10px;
|
||||
transform: rotate(45deg) scale(1) !important;
|
||||
}
|
||||
|
||||
/* Animation Position */
|
||||
[data-tooltip]:after,
|
||||
[data-tooltip][data-position="top center"]:after,
|
||||
[data-tooltip][data-position="bottom center"]:after {
|
||||
-webkit-transform: translateX(-50%) scale(0) !important;
|
||||
transform: translateX(-50%) scale(0) !important;
|
||||
}
|
||||
[data-tooltip]:hover:after,
|
||||
[data-tooltip][data-position="bottom center"]:hover:after {
|
||||
-webkit-transform: translateX(-50%) scale(1) !important;
|
||||
transform: translateX(-50%) scale(1) !important;
|
||||
}
|
||||
[data-tooltip][data-position="left center"]:after,
|
||||
[data-tooltip][data-position="right center"]:after {
|
||||
-webkit-transform: translateY(-50%) scale(0) !important;
|
||||
transform: translateY(-50%) scale(0) !important;
|
||||
}
|
||||
[data-tooltip][data-position="left center"]:hover:after,
|
||||
[data-tooltip][data-position="right center"]:hover:after {
|
||||
-webkit-transform: translateY(-50%) scale(1) !important;
|
||||
transform: translateY(-50%) scale(1) !important;
|
||||
}
|
||||
[data-tooltip][data-position="top left"]:after,
|
||||
[data-tooltip][data-position="top right"]:after,
|
||||
[data-tooltip][data-position="bottom left"]:after,
|
||||
[data-tooltip][data-position="bottom right"]:after {
|
||||
-webkit-transform: scale(0) !important;
|
||||
transform: scale(0) !important;
|
||||
}
|
||||
[data-tooltip][data-position="top left"]:hover:after,
|
||||
[data-tooltip][data-position="top right"]:hover:after,
|
||||
[data-tooltip][data-position="bottom left"]:hover:after,
|
||||
[data-tooltip][data-position="bottom right"]:hover:after {
|
||||
-webkit-transform: scale(1) !important;
|
||||
transform: scale(1) !important;
|
||||
}
|
||||
[data-tooltip][data-variation~="fixed"]:after {
|
||||
white-space: normal;
|
||||
width: 250px;
|
||||
}
|
||||
[data-tooltip][data-variation*="wide fixed"]:after {
|
||||
width: 350px;
|
||||
}
|
||||
[data-tooltip][data-variation*="very wide fixed"]:after {
|
||||
width: 550px;
|
||||
}
|
||||
@media only screen and (max-width: 767.98px) {
|
||||
[data-tooltip][data-variation~="fixed"]:after {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
/*--------------
|
||||
Inverted
|
||||
---------------*/
|
||||
|
||||
|
||||
/* Arrow */
|
||||
[data-tooltip][data-inverted]:before {
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Arrow Position */
|
||||
[data-tooltip][data-inverted]:before {
|
||||
background: #1B1C1D;
|
||||
}
|
||||
|
||||
/* Popup */
|
||||
[data-tooltip][data-inverted]:after {
|
||||
background: #1B1C1D;
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
[data-tooltip][data-inverted]:after .header {
|
||||
background: none;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/*--------------
|
||||
Position
|
||||
---------------*/
|
||||
|
||||
[data-position~="top"][data-tooltip]:before {
|
||||
background: #1B1C1D;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
/* Top Center */
|
||||
@@ -796,7 +723,7 @@ padding-right: 10px;
|
||||
right: auto;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
background: #1B1C1D;
|
||||
background: #FFFFFF;
|
||||
margin-left: -0.07142857rem;
|
||||
margin-bottom: 0.14285714rem;
|
||||
}
|
||||
@@ -835,7 +762,7 @@ padding-right: 10px;
|
||||
margin-bottom: 0.14285714rem;
|
||||
}
|
||||
[data-position~="bottom"][data-tooltip]:before {
|
||||
background: #1B1C1D;
|
||||
background: #FFFFFF;
|
||||
-webkit-box-shadow: -1px -1px 0 0 #bababc;
|
||||
box-shadow: -1px -1px 0 0 #bababc;
|
||||
}
|
||||
@@ -854,7 +781,7 @@ padding-right: 10px;
|
||||
bottom: auto;
|
||||
right: auto;
|
||||
top: 100%;
|
||||
left: 30%;
|
||||
left: 50%;
|
||||
margin-left: -0.07142857rem;
|
||||
margin-top: 0.14285714rem;
|
||||
}
|
||||
@@ -902,7 +829,9 @@ padding-right: 10px;
|
||||
top: 50%;
|
||||
margin-top: -0.14285714rem;
|
||||
margin-right: -0.07142857rem;
|
||||
background: #1B1C1D;
|
||||
background: #FFFFFF;
|
||||
-webkit-box-shadow: 1px -1px 0 0 #bababc;
|
||||
box-shadow: 1px -1px 0 0 #bababc;
|
||||
}
|
||||
|
||||
/* Right Center */
|
||||
@@ -918,9 +847,30 @@ padding-right: 10px;
|
||||
top: 50%;
|
||||
margin-top: -0.07142857rem;
|
||||
margin-left: 0.14285714rem;
|
||||
background: #1B1C1D;
|
||||
background: #FFFFFF;
|
||||
-webkit-box-shadow: -1px 1px 0 0 #bababc;
|
||||
box-shadow: -1px 1px 0 0 #bababc;
|
||||
}
|
||||
|
||||
/* Inverted Arrow Color */
|
||||
[data-inverted][data-position~="bottom"][data-tooltip]:before {
|
||||
background: #1B1C1D;
|
||||
-webkit-box-shadow: -1px -1px 0 0 #bababc;
|
||||
box-shadow: -1px -1px 0 0 #bababc;
|
||||
}
|
||||
[data-inverted][data-position="left center"][data-tooltip]:before {
|
||||
background: #1B1C1D;
|
||||
-webkit-box-shadow: 1px -1px 0 0 #bababc;
|
||||
box-shadow: 1px -1px 0 0 #bababc;
|
||||
}
|
||||
[data-inverted][data-position="right center"][data-tooltip]:before {
|
||||
background: #1B1C1D;
|
||||
-webkit-box-shadow: -1px 1px 0 0 #bababc;
|
||||
box-shadow: -1px 1px 0 0 #bababc;
|
||||
}
|
||||
[data-inverted][data-position~="top"][data-tooltip]:before {
|
||||
background: #1B1C1D;
|
||||
}
|
||||
[data-position~="bottom"][data-tooltip]:before {
|
||||
-webkit-transform-origin: center bottom;
|
||||
transform-origin: center bottom;
|
||||
@@ -945,59 +895,11 @@ padding-right: 10px;
|
||||
-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;
|
||||
|
||||
/*--------------
|
||||
Basic
|
||||
---------------*/
|
||||
|
||||
[data-tooltip][data-variation~="basic"]:before {
|
||||
display: none;
|
||||
}
|
@@ -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];
|
||||
@@ -1265,9 +1266,7 @@ var keys = {
|
||||
37: 'left',
|
||||
39: 'right',
|
||||
46: 'delete',
|
||||
191: '/',
|
||||
219: '[',
|
||||
220: '\\',
|
||||
221: ']'
|
||||
};
|
||||
|
||||
@@ -1763,7 +1762,7 @@ var keyHandlers = {
|
||||
}
|
||||
},
|
||||
space: function ( self, _, range ) {
|
||||
var node;
|
||||
var node, parent;
|
||||
var root = self._root;
|
||||
self._recordUndoState( range );
|
||||
if ( self._config.addLinks ) {
|
||||
@@ -2117,7 +2116,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 +2131,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 +2225,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 +2252,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 +2275,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 +2295,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 +2313,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 +2345,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 ) {
|
||||
@@ -2693,8 +2681,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 +2965,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 +2984,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 +3160,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 +3880,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 +4164,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 +4172,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 );
|
||||
}
|
||||
@@ -4984,7 +4968,6 @@ Squire.rangeDoesEndAtBlockBoundary = rangeDoesEndAtBlockBoundary;
|
||||
Squire.expandRangeToBlockBoundaries = expandRangeToBlockBoundaries;
|
||||
|
||||
// Clipboard.js exports
|
||||
Squire.onCopy = _onCopy;
|
||||
Squire.onPaste = onPaste;
|
||||
|
||||
// Editor.js exports
|
||||
|
@@ -20,7 +20,7 @@
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 75px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.image-placeholder:after {
|
||||
content: 'No Image';
|
||||
@@ -89,14 +89,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 +110,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 +171,6 @@
|
||||
this.checkKeyup()
|
||||
})
|
||||
},
|
||||
updated: function(){
|
||||
this.checkKeyup()
|
||||
},
|
||||
methods: {
|
||||
checkKeyup(){
|
||||
let elm = this.$refs.edit
|
||||
|
@@ -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>
|
||||
@@ -71,7 +66,7 @@
|
||||
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']
|
||||
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,17 @@
|
||||
let reduced = []
|
||||
|
||||
this.colors.forEach((color,i) => {
|
||||
if(i < 20 || i > 69){
|
||||
|
||||
|
||||
let mod = (i % 10)+1 //1 - 10
|
||||
let lines = [3, 5, 8, 9, 10]
|
||||
// if(lines.includes(mod)){
|
||||
reduced.push(color)
|
||||
}
|
||||
// }
|
||||
})
|
||||
|
||||
reduced.push("#000")
|
||||
|
||||
return reduced
|
||||
},
|
||||
clearStyles(){
|
||||
@@ -113,17 +114,12 @@
|
||||
|
||||
//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 +148,20 @@
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
.icon-button, .color-button {
|
||||
.icon-button {
|
||||
height: 40px;
|
||||
width: calc(15% - 1px);
|
||||
width: calc(10% - 7px);
|
||||
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;
|
||||
display: inline-block;
|
||||
width: calc(10% - 7px);
|
||||
height: 30px;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0px 1px 3px 0px #3e3e3e;
|
||||
margin: 7px 7px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
@@ -7,7 +7,7 @@
|
||||
min-height: 50px;
|
||||
min-width: 200px;
|
||||
max-width: calc(100% - 30px);
|
||||
z-index: 1020;
|
||||
z-index: 1002;
|
||||
|
||||
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
|
||||
border-radius: 4px;
|
||||
@@ -19,7 +19,7 @@
|
||||
padding: 1em 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.popup-row > p {
|
||||
.popup-row > span {
|
||||
/*width: calc(100% - 50px);*/
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
@@ -85,18 +85,6 @@
|
||||
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; }
|
||||
@@ -113,11 +101,7 @@
|
||||
<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>
|
||||
<span><i class="small info circle icon"></i>{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -135,8 +119,8 @@
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
this.$bus.$on('notification', notificationText => {
|
||||
this.displayNotification(notificationText)
|
||||
this.$bus.$on('notification', info => {
|
||||
this.displayNotification(info)
|
||||
})
|
||||
},
|
||||
mounted(){
|
||||
@@ -147,17 +131,8 @@
|
||||
|
||||
},
|
||||
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()
|
||||
|
@@ -1,28 +1,26 @@
|
||||
<style scoped>
|
||||
.slotholder {
|
||||
height: 100vh;
|
||||
width: 180px;
|
||||
width: 155px;
|
||||
display: block;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
.global-menu {
|
||||
width: 180px;
|
||||
/* background: #221f2b; */
|
||||
width: 155px;
|
||||
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;
|
||||
margin: 5px 0 0 41px;
|
||||
display: inline-block;
|
||||
height: auto;
|
||||
}
|
||||
@@ -44,8 +42,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 +52,9 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.router-link-active i {
|
||||
/*color: #16ab39;*/
|
||||
}
|
||||
.router-link-active {
|
||||
background-color: #534c68;
|
||||
}
|
||||
@@ -66,33 +66,27 @@
|
||||
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);
|
||||
border-bottom: 1px solid;
|
||||
border-color: var(--border_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;
|
||||
}
|
||||
.place-holder {
|
||||
width: 100%;
|
||||
/*height: 40px;*/
|
||||
height: 0;
|
||||
height: 50px;
|
||||
}
|
||||
.logo-display {
|
||||
width: 27px;
|
||||
@@ -108,49 +102,19 @@
|
||||
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;
|
||||
font-size: 2em;
|
||||
padding: 6px 3px 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.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);
|
||||
.mobile-button i {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -163,24 +127,12 @@
|
||||
<!-- collapsed menu, appears as a bar -->
|
||||
<div class="top-menu-bar" v-if="(collapsed || mobile) && !menuOpen">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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 class="mobile-button">
|
||||
<i class="green link bars icon" v-on:click="collapseMenu"></i>
|
||||
</div>
|
||||
|
||||
<div class="mobile-button"></div>
|
||||
|
||||
<!-- open straight to note -->
|
||||
<router-link
|
||||
v-if="loggedIn && $store.getters.totals && $store.getters.totals['quickNote']"
|
||||
@@ -188,7 +140,6 @@
|
||||
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 -->
|
||||
@@ -198,21 +149,27 @@
|
||||
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="/notes" v-on:click.native="emitReloadEvent()">
|
||||
<logo class="logo-display" color="var(--main-accent)" />
|
||||
</router-link>
|
||||
|
||||
<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 class="mobile-button"></div>
|
||||
|
||||
<!-- mobile create note button -->
|
||||
<span v-if="loggedIn">
|
||||
<span v-if="!disableNewNote" @click="createNote" class="mobile-button">
|
||||
<i class="green plus icon"></i>
|
||||
</span>
|
||||
<span v-if="disableNewNote" class="mobile-button">
|
||||
<i class="grey plus icon"></i>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -230,12 +187,12 @@
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<div v-if="!disableNewNote" @click="createNote" class="menu-item menu-item menu-button">
|
||||
<div class="ui green fluid compact button">
|
||||
<div class="ui green button">
|
||||
<i class="plus icon"></i>New Note
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="disableNewNote" class="menu-item menu-item menu-button">
|
||||
<div class="ui basic fluid compact button">
|
||||
<div class="ui basic button">
|
||||
<i class="plus loading icon"></i>New Note
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,19 +205,14 @@
|
||||
</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" />
|
||||
<i class="grey mail outline icon"></i>Inbox
|
||||
</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" />
|
||||
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
|
||||
</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> -->
|
||||
@@ -313,52 +265,25 @@
|
||||
<span v-if="$store.getters.getIsNightMode == 0">
|
||||
<i class="moon outline icon"></i>Black Theme</span>
|
||||
<span v-if="$store.getters.getIsNightMode == 1">
|
||||
<i class="moon outline icon"></i>Flux Theme</span>
|
||||
<span v-if="$store.getters.getIsNightMode == 2">
|
||||
<i class="moon outline icon"></i>Light Theme</span>
|
||||
</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
|
||||
<i class="question circle outline icon"></i>Help | Terms
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<div class="menu-item menu-button" v-on:click="logout()">
|
||||
<i class="log out icon"></i>Log Out
|
||||
</div>
|
||||
<div class="menu-section" v-if="loggedIn" :data-tooltip="`Settings for ${this.$store.getters.getUsername}`" data-inverted="" data-position="right center">
|
||||
<router-link class="menu-item menu-button" exact-active-class="active" to="/settings">
|
||||
<i v-if="userIcon" class="cog icon"></i>{{ usernameDisplay }}
|
||||
</router-link>
|
||||
</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 }}
|
||||
@@ -417,33 +342,30 @@
|
||||
//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
|
||||
}
|
||||
|
||||
if(name.length > 16){
|
||||
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}) => {
|
||||
|
||||
console.log(data)
|
||||
this.$router.push({'path':'/notes/open/'+data.noteId})
|
||||
})
|
||||
|
||||
@@ -527,11 +449,8 @@
|
||||
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))
|
||||
const index = ( parseInt(this.version.replace(/\./g,'')) % (icons.length))
|
||||
return icons[index]
|
||||
|
||||
}
|
||||
|
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br>
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank" rel="noopener">pwa</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HelloWorld',
|
||||
props: {
|
||||
msg: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
@@ -39,9 +39,9 @@
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
/*min-height: 100px;*/
|
||||
min-height: 100px;
|
||||
margin: 20px 0;
|
||||
/*padding: 40px;*/
|
||||
padding: 40px;
|
||||
border-radius: 7px;
|
||||
background-color: var(--small_element_bg_color);
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
|
||||
<template>
|
||||
|
||||
<div>
|
||||
<div v-on:keyup.enter="login()">
|
||||
|
||||
<!-- thicc form display -->
|
||||
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register">
|
||||
<div v-if="!thin" class="ui large form">
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
|
||||
@@ -15,11 +15,6 @@
|
||||
<input v-model="password" type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="password2" type="password" name="password2" placeholder="Re-type Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" v-if="require2FA">
|
||||
<div class="ui input">
|
||||
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
|
||||
@@ -27,19 +22,21 @@
|
||||
</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)}">
|
||||
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="login()" class="ui green button">
|
||||
<i class="power icon"></i>
|
||||
Login
|
||||
</div>
|
||||
<div class="or"></div>
|
||||
<div v-on:click="register()" class="ui button">
|
||||
<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">
|
||||
<router-link to="/help">
|
||||
Terms of Use
|
||||
</router-link>
|
||||
</span>
|
||||
@@ -47,27 +44,7 @@
|
||||
</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 v-if="thin" class="ui small form">
|
||||
<div class="equal width fields">
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
@@ -84,8 +61,15 @@
|
||||
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
|
||||
</div>
|
||||
</div>
|
||||
<!-- hide this field if someone is logging in with 2FA -->
|
||||
<div class="field" v-if="!require2FA">
|
||||
<div v-on:click="register()" class="ui fluid green button">
|
||||
<i class="plug icon"></i>
|
||||
Sign Up
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div v-on:click="login" class="ui fluid button">
|
||||
<div v-on:click="login()" class="ui fluid button">
|
||||
<i class="power icon"></i>
|
||||
Login
|
||||
</div>
|
||||
@@ -93,7 +77,7 @@
|
||||
</div>
|
||||
<span class="small-terms">
|
||||
By signing up you agree to Solid Scribe's
|
||||
<router-link to="/terms">
|
||||
<router-link to="/help">
|
||||
Terms of Use
|
||||
</router-link>
|
||||
</span>
|
||||
@@ -126,7 +110,6 @@
|
||||
enabled: false,
|
||||
username: '',
|
||||
password: '',
|
||||
password2: '',
|
||||
authToken: '',
|
||||
require2FA: false,
|
||||
}
|
||||
@@ -159,27 +142,12 @@
|
||||
},
|
||||
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')
|
||||
if( this.username.length == 0 || this.password.length == 0 ){
|
||||
this.$bus.$emit('notification', 'Unable to Sign Up - Username and Password Required')
|
||||
return
|
||||
}
|
||||
|
||||
axios.post('/api/public/register', {'username': this.username, 'password': this.password})
|
||||
axios.post('/api/user/register', {'username': this.username, 'password': this.password})
|
||||
.then(({data}) => {
|
||||
|
||||
if(data == false){
|
||||
@@ -199,7 +167,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
axios.post('/api/public/login', {'username': this.username, 'password': this.password, 'authToken':this.authToken })
|
||||
axios.post('/api/user/login', {'username': this.username, 'password': this.password, 'authToken':this.authToken })
|
||||
.then(({data}) => {
|
||||
|
||||
//Enable 2FA on form
|
||||
@@ -225,7 +193,7 @@
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.$bus.$emit('notification', error)
|
||||
this.$bus.$emit('notification', 'Unable to Login - Incorrect Username or Password')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -235,6 +203,7 @@
|
||||
<style type="text/css" scoped="true">
|
||||
.small-terms {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
@@ -32,22 +32,22 @@
|
||||
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`" />
|
||||
:style="`fill:${displayColor};fill-opacity:1;stroke:none;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1`" />
|
||||
<path
|
||||
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`" />
|
||||
:style="`fill:${displayColor};fill-opacity:1;stroke:none;stroke-width:0.26458332px;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`" />
|
||||
:style="`display:inline;fill:${displayColor};fill-opacity:1;stroke:none;stroke-width:0.26458332px;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`" />
|
||||
:style="`display:inline;fill:${displayColor};fill-opacity:1;stroke:none;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1`" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -56,28 +56,13 @@
|
||||
|
||||
export default {
|
||||
name: 'LoadingIcon',
|
||||
props:[
|
||||
'color', // hex value for setting colorr
|
||||
'stroke' // enable or disable stroke
|
||||
],
|
||||
props:[ 'color' ],
|
||||
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
|
||||
@@ -88,13 +73,8 @@
|
||||
<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,15 +1,23 @@
|
||||
<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 || showWorking), 'bgboy':triggerClosedAnimation, 'title-view':titleView }"
|
||||
>
|
||||
|
||||
|
||||
<!-- Show title and snippet below it -->
|
||||
<div class="overflow-hidden note-card-text" @click.stop="cardClicked" v-if="!titleView">
|
||||
<div class="overflow-hidden note-card-text" @click="cardClicked" v-if="!titleView">
|
||||
|
||||
<span class="subtext" v-if="note.shareUsername">
|
||||
Shared by {{ note.shareUsername }}
|
||||
|
||||
<span v-if="note.opened == null && !beenClicked" class="ui tiny green compact right floated button">
|
||||
New
|
||||
</span>
|
||||
<span v-else-if="note.updated > note.opened && !beenClicked" class="ui tiny green compact right floated basic button">
|
||||
Updated
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span v-if="note.title == '' && note.subtext == ''">
|
||||
Empty Note
|
||||
@@ -23,109 +31,46 @@
|
||||
<span v-if="note.title.length > 0"
|
||||
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"
|
||||
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">
|
||||
<div class="ui fluid basic button" v-if="note.encrypted == 1">
|
||||
<i class="green lock icon"></i>
|
||||
Locked
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
</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 class="subtext" v-if="note.shared == 2">
|
||||
You Shared this note
|
||||
<span v-if="note.updated > note.opened && !beenClicked" class="ui tiny green compact right floated basic button">
|
||||
Updated
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- edited -->
|
||||
<span class="thin-right">
|
||||
{{$helpers.timeAgo( note.updated )}}
|
||||
|
||||
<i class="green link ellipsis vertical icon"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="titleView" class="single-line-text" @click="cardClicked">
|
||||
<span class="title-line" v-if="note.title.length > 0">{{ note.title }}<br></span>
|
||||
<span class="sub-line" v-if="note.subtext.length > 0">{{ removeHtml(note.subtext) }}</span>
|
||||
<span v-if="note.title.length == 0 && note.title.length == 0">Empty Note</span>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar on the bottom -->
|
||||
<div class="tool-bar" @click.self="cardClicked" v-if="!titleView">
|
||||
<div class="icon-bar">
|
||||
|
||||
<div v-if="getThumbs.length > 0">
|
||||
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
|
||||
<img v-for="thumb in getThumbs"
|
||||
class="tiny-thumb"
|
||||
:src="`/api/static/thumb_${thumb}`"
|
||||
onerror="
|
||||
this.onerror=null;
|
||||
this.src='/api/static/assets/marketing/void.svg';
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="tags" v-if="note.tags">
|
||||
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click="$emit('tagClick', tag.split(':')[1] )">{{ tag.split(':')[0] }}</span>
|
||||
<br>
|
||||
</span>
|
||||
|
||||
<div class="icon-bar" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
|
||||
|
||||
<span class="time-ago-display">
|
||||
<span class="time-ago-display" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
|
||||
{{$helpers.timeAgo( note.updated )}}
|
||||
</span>
|
||||
|
||||
<span class="teeny-buttons">
|
||||
<span class="teeny-buttons" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
|
||||
|
||||
<span v-if="!note.trashed">
|
||||
|
||||
@@ -162,13 +107,19 @@
|
||||
</i>
|
||||
<delete-button class="teeny-button" :note-id="note.id" />
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="getThumbs.length > 0">
|
||||
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
|
||||
<img v-for="thumb in getThumbs" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tag edit menu -->
|
||||
<side-slide-menu v-if="showTagSlideMenu" v-on:close="toggleTags(false)" :full-shadow="true" :skip-history="true">
|
||||
<div class="ui basic segment">
|
||||
<note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/>
|
||||
@@ -227,12 +178,10 @@
|
||||
},
|
||||
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}
|
||||
let postData = {'pinned': !this.note.pinned, 'noteId':this.note.id}
|
||||
axios.post('/api/note/setpinned', postData)
|
||||
.then(data => {
|
||||
this.showWorking = false
|
||||
// this event is triggered by the server after note is saved
|
||||
// this.$bus.$emit('update_single_note', this.note.id)
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Pin Note') })
|
||||
@@ -248,10 +197,11 @@
|
||||
//Show message so no one worries where note went
|
||||
let message = 'Moved to Archive'
|
||||
if(postData.archived != 1){
|
||||
message = 'Moved out of Archive'
|
||||
message = 'Moved to main list'
|
||||
}
|
||||
this.$bus.$emit('notification', message)
|
||||
this.$bus.$emit('update_single_note', this.note.id)
|
||||
|
||||
// this.$bus.$emit('update_single_note', this.note.id)
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Archive Note') })
|
||||
},
|
||||
@@ -266,10 +216,9 @@
|
||||
//Show message so no one worries where note went
|
||||
let message = 'Moved to Trash'
|
||||
if(postData.trashed == 0){
|
||||
message = 'Moved out of Trash'
|
||||
message = 'Moved to main list'
|
||||
}
|
||||
this.$bus.$emit('notification', message)
|
||||
this.$bus.$emit('update_single_note', this.note.id)
|
||||
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Trash Note') })
|
||||
@@ -285,28 +234,23 @@
|
||||
},
|
||||
justClosed(){
|
||||
|
||||
// Dont do anything when not is closed.
|
||||
// Its already saved, this will make interface feel snappy
|
||||
|
||||
// Scroll note into view
|
||||
// this.$el.scrollIntoView({
|
||||
// behavior: 'smooth',
|
||||
// block: 'center',
|
||||
// inline: 'center'
|
||||
// })
|
||||
this.$el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
})
|
||||
|
||||
// this.$bus.$emit('notification','Note Saved')
|
||||
//After scroll, trigger green outline animation
|
||||
setTimeout(() => {
|
||||
|
||||
// //After scroll, trigger green outline animation
|
||||
// setTimeout(() => {
|
||||
this.triggerClosedAnimation = true
|
||||
setTimeout(()=>{
|
||||
//After 3 seconds, hide it
|
||||
this.triggerClosedAnimation = false
|
||||
}, 3000)
|
||||
|
||||
// this.triggerClosedAnimation = true
|
||||
// setTimeout(()=>{
|
||||
// //After 3 seconds, hide it
|
||||
// this.triggerClosedAnimation = false
|
||||
// }, 1500)
|
||||
|
||||
// }, 500)
|
||||
}, 500)
|
||||
|
||||
},
|
||||
},
|
||||
@@ -381,11 +325,13 @@
|
||||
|
||||
.teeny-buttons {
|
||||
float: right;
|
||||
width: 65%;
|
||||
text-align: right;
|
||||
}
|
||||
.time-ago-display {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
width: 35%;
|
||||
float: left;
|
||||
text-align: center;
|
||||
}
|
||||
.tags {
|
||||
width: 100%;
|
||||
@@ -410,7 +356,9 @@
|
||||
|
||||
/*Strict font sizes for card display*/
|
||||
.small-text {
|
||||
max-height: 267px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
|
||||
@@ -458,10 +406,9 @@
|
||||
.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: 0px 1px 2px 1px rgba(210, 211, 211, 0.46);*/
|
||||
transition: box-shadow ease 0.5s, transform linear 0.1s;
|
||||
margin: 5px;
|
||||
/*padding: 0.7em 1em;*/
|
||||
border-radius: .28571429rem;
|
||||
@@ -470,7 +417,7 @@
|
||||
/*width: calc(33.333% - 10px);*/
|
||||
width: calc(25% - 10px);
|
||||
/*min-width: 190px;*/
|
||||
/*min-height: 130px;*/
|
||||
min-height: 130px;
|
||||
/*transition: box-shadow 0.3s;*/
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
@@ -479,72 +426,32 @@
|
||||
letter-spacing: 0.05rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
text-align: left;
|
||||
|
||||
min-height: 100px;
|
||||
max-height: 450px;
|
||||
}
|
||||
.note-title-display-card:hover {
|
||||
box-shadow: 0 8px 15px rgba(0,0,0,0.3);
|
||||
border-color: var(--main-accent);
|
||||
/*box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8);*/
|
||||
/*transform: translateY(-2px);*/
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
||||
}
|
||||
.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 {
|
||||
.single-line-text {
|
||||
width: calc(100% - 25px);
|
||||
/*margin: 5px 10px;*/
|
||||
margin: 5px 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.thin-container .thin-title {
|
||||
.title-line {
|
||||
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;
|
||||
padding: 0 20px 0 0;
|
||||
}
|
||||
|
||||
.icon-bar {
|
||||
@@ -552,7 +459,6 @@
|
||||
padding: 5px 10px 0;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
background-color: rgba(200, 200, 200, 0.2);
|
||||
}
|
||||
.hover-hide {
|
||||
opacity: 0.0;
|
||||
@@ -561,6 +467,7 @@
|
||||
.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 +477,6 @@
|
||||
line-height: 0.8em;
|
||||
text-overflow: ellipsis;
|
||||
float: left;
|
||||
color: var(--main-accent);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.tiny-thumb-box {
|
||||
max-height: 70px;
|
||||
@@ -655,7 +560,7 @@
|
||||
@media only screen and (max-width: 700px) {
|
||||
.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) {
|
||||
@@ -732,36 +637,4 @@
|
||||
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>
|
@@ -35,6 +35,7 @@
|
||||
<i class="search icon"></i>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="floating-button" v-if="searchTerm.length > 0">
|
||||
<i class="big link grey close icon" v-on:click="clear()"></i>
|
||||
</div>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<style type="text/css" scoped>
|
||||
.slide-container {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
right: 50%;
|
||||
bottom: 0;
|
||||
z-index: 1020;
|
||||
overflow: hidden;
|
||||
@@ -27,7 +27,7 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
color: red;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
/*background-color: rgba(0,0,0,0.5);*/
|
||||
/*background: linear-gradient(90deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 55%);*/
|
||||
z-index: 1019;
|
||||
overflow: hidden;
|
||||
@@ -88,19 +88,19 @@
|
||||
|
||||
<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> -->
|
||||
|
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
.img-row {
|
||||
height: 20vh;
|
||||
height: 30vh;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
@@ -113,7 +113,6 @@
|
||||
<style type="text/css">
|
||||
.button-fix {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
.hover-row:hover {
|
||||
cursor: pointer;
|
||||
|
@@ -1,44 +1,23 @@
|
||||
<style type="text/css" scoped>
|
||||
.colors {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
z-index: 1023;
|
||||
top: 35px;
|
||||
top: 42px;
|
||||
/*height: 100px;*/
|
||||
width: 400px;
|
||||
left: 20%;
|
||||
/*width: 415px;*/
|
||||
left: 0;
|
||||
}
|
||||
.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%;
|
||||
max-width: 370px;
|
||||
}
|
||||
.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%;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
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
|
||||
;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0px 1px 3px 0px #3e3e3e;
|
||||
margin: 7px 7px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.shade {
|
||||
position: fixed;
|
||||
@@ -51,16 +30,12 @@
|
||||
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%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -68,15 +43,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="colors">
|
||||
<div class="ui segment big-shadow">
|
||||
<h3>Select Text Color</h3>
|
||||
<div class="ui raised segment">
|
||||
<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>
|
||||
@@ -92,7 +65,6 @@
|
||||
components:{
|
||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||
},
|
||||
props: [ 'lastUsedColor' ],
|
||||
data: function(){
|
||||
return {
|
||||
hover: false,
|
||||
|
@@ -4,6 +4,7 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import Vuex from 'vuex'
|
||||
import 'es6-promise/auto' //Vuex likes promises
|
||||
import store from './stores/mainStore';
|
||||
|
||||
import App from './App'
|
||||
@@ -13,19 +14,19 @@ import router from './router'
|
||||
// import 'fomantic-ui-css/semantic.css';
|
||||
|
||||
//Required site and reset CSS
|
||||
import 'fomantic-ui-css/components/reset.min.css'
|
||||
import 'fomantic-ui-css/components/reset.css'
|
||||
import 'fomantic-ui-css/components/site.css' //modified to remove included LATO fonts
|
||||
|
||||
//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/button.css'
|
||||
import 'fomantic-ui-css/components/container.css'
|
||||
import 'fomantic-ui-css/components/form.css'
|
||||
import 'fomantic-ui-css/components/grid.css'
|
||||
import 'fomantic-ui-css/components/header.css'
|
||||
import 'fomantic-ui-css/components/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'
|
||||
import 'fomantic-ui-css/components/input.css'
|
||||
import 'fomantic-ui-css/components/segment.css'
|
||||
import 'fomantic-ui-css/components/label.css'
|
||||
|
||||
|
||||
//Overwrite and site styles and themes and good stuff
|
||||
@@ -35,7 +36,7 @@ require('./assets/roboto-latin.woff2')
|
||||
require('./assets/roboto-latin-bold.woff2')
|
||||
|
||||
|
||||
|
||||
require('./assets/squire.js')
|
||||
|
||||
//Import socket io, init using nginx configured socket path
|
||||
import io from 'socket.io-client';
|
||||
@@ -65,7 +66,9 @@ 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/>'
|
||||
})
|
@@ -9,10 +9,6 @@ const SquireButtonFunctions = {
|
||||
activeList: false,
|
||||
activeToDo: false,
|
||||
activeColor: null,
|
||||
activeCode: false,
|
||||
activeSubTitle: false,
|
||||
//
|
||||
lastUsedColor: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -30,8 +26,6 @@ const SquireButtonFunctions = {
|
||||
this.activeToDo = false
|
||||
this.activeColor = null
|
||||
this.activeUnderline = false
|
||||
this.activeCode = false
|
||||
this.activeSubTitle = false
|
||||
|
||||
if(e.path.indexOf('>U>') > -1 || e.path.search(/U$/) > -1){
|
||||
this.activeUnderline = true
|
||||
@@ -42,29 +36,20 @@ const SquireButtonFunctions = {
|
||||
if(e.path.indexOf('>I') > -1){
|
||||
this.activeItalics = true
|
||||
}
|
||||
if(e.path.indexOf('fontSize=1.4em') > -1){
|
||||
if(e.path.indexOf('fontSize') > -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
|
||||
this.activeColor=`rgb(${colors.join(',')})`
|
||||
}
|
||||
},
|
||||
|
||||
@@ -108,11 +93,6 @@ const SquireButtonFunctions = {
|
||||
this.selectLineIfNoSelect()
|
||||
//Set color of font
|
||||
this.editor.setTextColour(color)
|
||||
|
||||
this.lastUsedColor = color
|
||||
},
|
||||
applyLastUsedColor(){
|
||||
this.modifyColor(this.lastUsedColor)
|
||||
},
|
||||
toggleList(type){
|
||||
|
||||
@@ -153,12 +133,6 @@ const SquireButtonFunctions = {
|
||||
this.editor.italic()
|
||||
}
|
||||
},
|
||||
modifyCode(){
|
||||
|
||||
this.selectLineIfNoSelect()
|
||||
|
||||
this.editor.toggleCode()
|
||||
},
|
||||
undoCustom(){
|
||||
//The same as pressing CTRL + Z
|
||||
// this.editor.focus()
|
||||
@@ -172,16 +146,15 @@ const SquireButtonFunctions = {
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
this.$router.go(-1)
|
||||
|
||||
setTimeout(()=>{
|
||||
|
||||
Array.from( container.getElementsByClassName('active') ).forEach(item => {
|
||||
item.classList.remove('active');
|
||||
})
|
||||
|
||||
},600)
|
||||
Array.from( container.getElementsByClassName('active') ).forEach(item => {
|
||||
item.classList.remove('active');
|
||||
})
|
||||
|
||||
//Close menu if user is on mobile, then sort list
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
this.$router.go(-1)
|
||||
}
|
||||
},
|
||||
deleteCompletedListItems(){
|
||||
//
|
||||
@@ -191,57 +164,53 @@ const SquireButtonFunctions = {
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
//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)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
//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)
|
||||
|
||||
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
this.$router.go(-1)
|
||||
}
|
||||
},
|
||||
sortList(){
|
||||
//
|
||||
@@ -251,65 +220,61 @@ const SquireButtonFunctions = {
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
//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)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
//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)
|
||||
|
||||
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
this.$router.go(-1)
|
||||
}
|
||||
},
|
||||
calculateMath(){
|
||||
//
|
||||
@@ -319,9 +284,6 @@ const SquireButtonFunctions = {
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
//Close menu if user is on mobile, then sort list
|
||||
this.$router.go(-1)
|
||||
|
||||
// simple function that trys to evaluate javascript
|
||||
const shittyMath = (string) => {
|
||||
//Remove all chars but math chars
|
||||
@@ -334,57 +296,44 @@ const SquireButtonFunctions = {
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(()=>{
|
||||
//Go through each item, on first level, look for Unordered Lists
|
||||
container.childNodes.forEach( (node) => {
|
||||
|
||||
//Go through each item, on first level, look for Unordered Lists
|
||||
container.childNodes.forEach( (node) => {
|
||||
const line = node.innerText.trim()
|
||||
|
||||
const line = node.innerText.trim()
|
||||
// = sign exists and its the last character in the string
|
||||
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
|
||||
|
||||
// = sign exists and its the last character in the string
|
||||
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
|
||||
//Pull out everything before the formula and try to evaluate it
|
||||
const formula = line.split('=').shift()
|
||||
const output = shittyMath(formula)
|
||||
|
||||
//Pull out everything before the formula and try to evaluate it
|
||||
const formula = line.split('=').shift()
|
||||
const output = shittyMath(formula)
|
||||
//If its a number and didn't throw an error, update the line
|
||||
if(!isNaN(output) && output != null){
|
||||
|
||||
//If its a number and didn't throw an error, update the line
|
||||
if(!isNaN(output) && output != null){
|
||||
//Since there is HTML in the line, splice in the number after the = sign
|
||||
let equalLocation = node.innerHTML.indexOf('=')
|
||||
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
|
||||
newLine += ` ${output}`
|
||||
newLine += node.innerHTML.slice(equalLocation+1).trim()
|
||||
|
||||
//Since there is HTML in the line, splice in the number after the = sign
|
||||
let equalLocation = node.innerHTML.indexOf('=')
|
||||
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
|
||||
newLine += ` ${output}`
|
||||
newLine += node.innerHTML.slice(equalLocation+1).trim()
|
||||
|
||||
//Slam in that new HTML with the output
|
||||
node.innerHTML = newLine
|
||||
}
|
||||
//Slam in that new HTML with the output
|
||||
node.innerHTML = newLine
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
},600)
|
||||
|
||||
|
||||
})
|
||||
|
||||
//Close menu if user is on mobile, then sort list
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
this.$router.go(-1)
|
||||
}
|
||||
},
|
||||
setText(inText){
|
||||
|
||||
|
||||
|
||||
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(){
|
||||
|
||||
@@ -417,26 +366,6 @@ const SquireButtonFunctions = {
|
||||
|
||||
this.$router.go(-1)
|
||||
},
|
||||
indentText(){
|
||||
|
||||
// Lists use increase list level, increase quote breaks numbering
|
||||
if(this.activeList || this.activeToDo){
|
||||
|
||||
this.editor.increaseListLevel()
|
||||
return
|
||||
}
|
||||
this.editor.increaseQuoteLevel()
|
||||
},
|
||||
outdentText(){
|
||||
|
||||
// Lists use increase list level, increase quote breaks numbering
|
||||
if(this.activeList || this.activeToDo){
|
||||
|
||||
this.editor.decreaseListLevel()
|
||||
return
|
||||
}
|
||||
this.editor.decreaseQuoteLevel()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
@@ -11,29 +11,7 @@
|
||||
-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;
|
||||
@@ -46,14 +24,10 @@
|
||||
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; }
|
||||
@@ -127,11 +101,10 @@
|
||||
<!-- <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="sixteen wide middle aligned center 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>
|
||||
@@ -146,37 +119,32 @@
|
||||
<img loading="lazy" width="90%" src="/api/static/assets/marketing/notebook.svg" alt="The Venus fly laptop about to capture another victim">
|
||||
</div> -->
|
||||
|
||||
<div v-for="i in jewelFacets" class="shine" :style="shineStyle(i)" v-bind:key="i"></div>
|
||||
|
||||
<div class="shine spotlight"></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">
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
@@ -199,78 +167,120 @@
|
||||
<!-- 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> -->
|
||||
<h2>Solid Scribe focuses on powerful text editing and user privacy</h2>
|
||||
<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>
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/idea.svg" alt="Explosion of New Ideas">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- features list -->
|
||||
<div class="top aligned centered row">
|
||||
|
||||
<!-- note features -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="sixteen wide column">
|
||||
<h1 class="ui center aligned header"><i class="sliders horizontal icon"></i>Features</h1>
|
||||
</div>
|
||||
<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">
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey sticky note icon"></i>
|
||||
<i class="bottom left corner teal plus icon"></i>
|
||||
<i class="grey lock icon"></i>
|
||||
<i class="bottom left corner yellow key icon"></i>
|
||||
</i>
|
||||
Create a million notes!
|
||||
<div class="sub header">Create unlimited notes up to 5,000,000 characters long.</div>
|
||||
Privacy Focused
|
||||
<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">
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey list icon"></i>
|
||||
<i class="bottom left corner green check icon"></i>
|
||||
</i>
|
||||
To Do Lists
|
||||
<div class="sub header">Create To Do lists that are always synced, work on mobile and can be sorted.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey file icon"></i>
|
||||
<i class="bottom left corner blue pen icon"></i>
|
||||
</i>
|
||||
Document Editing Tools
|
||||
<div class="sub header">Bold, Underline, Title, Add Links, Add Tables, Color Text, Color Background and more.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey images icon"></i>
|
||||
<i class="bottom left corner teal paperclip icon"></i>
|
||||
</i>
|
||||
Add Images
|
||||
<div class="sub header">Upload images to notes, add search text to the images to find them later.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey 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 class="sub header">Easily add and edit tags on notes then sort notes by tag.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui header">
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey cloud moon icon"></i>
|
||||
<i class="bottom left corner red eye icon"></i>
|
||||
</i>
|
||||
Night Mode
|
||||
<div class="sub header">Pure black night theme with an even darker flux theme.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey share alternate icon"></i>
|
||||
<i class="bottom left corner share icon"></i>
|
||||
</i>
|
||||
Share Encrypted Notes
|
||||
<div class="sub header">Share encrypted notes with friends without compromising security.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey users icon"></i>
|
||||
<i class="bottom left corner olive exchange icon"></i>
|
||||
</i>
|
||||
Collaborative Note Editing
|
||||
<div class="sub header">Notes instantly update in real time everywhere the note is open.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<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>
|
||||
Keyword Search
|
||||
<div class="sub header">Easily search all notes. Encrypted search index ensures privacy and convenience.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui header">
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey search icon"></i>
|
||||
@@ -281,110 +291,7 @@
|
||||
</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">
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey tv icon"></i>
|
||||
@@ -394,18 +301,18 @@
|
||||
<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 class="four wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="four wide right aligned column">
|
||||
<svg-displayer file="secure" alt="So dang secure" />
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
|
||||
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Only you can read your notes. </h2>
|
||||
@@ -419,13 +326,13 @@
|
||||
<h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3>
|
||||
</div>
|
||||
<div class="four wide right aligned column">
|
||||
<svg-displayer file="cloud" alt="Girl falling into the spiral of digital chaos" />
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="four wide right aligned column">
|
||||
<svg-displayer file="robot" alt="Murder Robot in office environment" />
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/robot.svg" alt="Shrunken man near giant tablet">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Secure Data Sharing</h2>
|
||||
@@ -440,11 +347,11 @@
|
||||
<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://addons.mozilla.org/en-US/firefox/addon/ublock-origin/" target="_blank">uBlock Origin</a> and a
|
||||
<a href="https://pi-hole.net/" target="_blank">Pi-hole</a> on the network.</h3>
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<svg-displayer file="icecream" alt="Emergence of a 4th dimensional being perceived as a large ice cream" />
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/icecream.svg" alt="Emergence of a 4th dimensional being perceived as a large ice cream ">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -493,12 +400,7 @@
|
||||
|
||||
<div v-if="true" class="middle aligned centered row">
|
||||
<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>Solid Scribe was created by one passionate developer</h2>
|
||||
<p>
|
||||
I was tired of all my data being owned by big companies, having it farmed out for marketing, and leaving the contents of my life exposed to corporations.
|
||||
</p>
|
||||
@@ -506,10 +408,9 @@
|
||||
If you want to give it a shot, feel free to make an account. There are no ads. None of this data is shared or public. I don't make any money.
|
||||
</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>If you want to help me out, 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">
|
||||
@@ -518,14 +419,10 @@
|
||||
<p>Awesomely 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>
|
||||
@@ -536,28 +433,11 @@ 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(){
|
||||
@@ -571,40 +451,6 @@ export default {
|
||||
|
||||
},
|
||||
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,10 +1,10 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="ui basic segment no-fluf-segment">
|
||||
|
||||
<div class="ui grid" ref="content">
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<!-- :class="{ 'sixteen wide column':showOneColumn 'sixteen wide column':!showOneColumn}" -->
|
||||
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
|
||||
|
||||
<div class="ui stackable grid">
|
||||
|
||||
@@ -12,12 +12,6 @@
|
||||
<search-input />
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="$store.getters.totals && $store.getters.totals['showTrackMetricsButton']">
|
||||
<router-link class="ui fluid green button" to="/metrictrack">
|
||||
<i class="calendar check outlin icon"></i>Metric Track
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
|
||||
|
||||
<div class="ui basic button shrinking"
|
||||
@@ -25,17 +19,18 @@
|
||||
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>
|
||||
+{{ $store.getters.totals['youGotMailCount'] }}
|
||||
</div>
|
||||
|
||||
<tag-display
|
||||
v-if="$store.getters.totals && Object.keys($store.getters.totals['tags'] || {}).length"
|
||||
:user-tags="$store.getters.totals['tags']"
|
||||
:active-tags="searchTags"
|
||||
v-on:tagClick="tagId => toggleTagFilter(tagId)"
|
||||
/>
|
||||
|
||||
<paste-button />
|
||||
<div class="ui basic shrinking icon button" v-on:click="toggleTitleView()" v-if="$store.getters.totals && $store.getters.totals['totalNotes'] > 0">
|
||||
<i v-if="titleView" class="th icon"></i>
|
||||
<i v-if="!titleView" class="bars icon"></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -50,7 +45,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !showLoading">
|
||||
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !loadingInProgress">
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}"
|
||||
@@ -62,15 +57,11 @@
|
||||
</div>
|
||||
|
||||
<div v-if="fastFilters['onlyArchived'] == 1" class="sixteen wide column">
|
||||
<h2>
|
||||
<i class="green archive icon"></i>
|
||||
Archived Notes</h2>
|
||||
<h2>Archived Notes</h2>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
|
||||
<h2>
|
||||
<i class="green trash alternate outline icon"></i>
|
||||
Trashed Notes
|
||||
<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>
|
||||
@@ -80,8 +71,7 @@
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="fastFilters['onlyShowSharedNotes'] == 1">
|
||||
<h2><i class="green paper plane outline icon"></i>
|
||||
Shared Notes</h2>
|
||||
<h2>Shared Notes</h2>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="tagSuggestions.length > 0">
|
||||
@@ -92,57 +82,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note title card display -->
|
||||
<div class="sixteen wide column">
|
||||
|
||||
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0 && fastFilters['notesHome'] == 1">
|
||||
No Notes Yet. <br>Thats ok.<br><br> <br>
|
||||
<img loading="lazy" width="25%" src="/api/static/assets/marketing/hamburger.svg" alt="Create a new note"><br>
|
||||
Create one when you feel ready.
|
||||
</h3>
|
||||
|
||||
<!-- Go to one wide column, do not do this on mobile interface -->
|
||||
<div :class="{'one-column':( showOneColumn), 'floating-list':( isFloatingList ), 'hidden-floating-list':(collapseFloatingList)}" v-on:scroll="onScroll">
|
||||
|
||||
|
||||
<div class="ui basic fitted right aligned segment" v-if="isFloatingList">
|
||||
<div class="ui small basic green left floated button" v-on:click="closeAllNotes()" v-if="openNotes.length >= 1">
|
||||
<i class="close icon"></i>
|
||||
Close Notes
|
||||
</div>
|
||||
<div class="ui small green button" v-on:click="collapseFloatingList = true">
|
||||
<i class="caret square left outline icon"></i>
|
||||
Hide List
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- render each section based on notes in set -->
|
||||
<div v-for="section,index in noteSections" v-if="section.length > 0" class="note-card-section">
|
||||
<h5 class="ui tiny dividing header"><i :class="`green ${sectionData[index][0]} icon`"></i>{{ sectionData[index][1] }}</h5>
|
||||
|
||||
<div class="note-card-display-area">
|
||||
<note-title-display-card
|
||||
v-on:tagClick="tagId => toggleTagFilter(tagId)"
|
||||
v-for="note in section"
|
||||
:ref="'note-'+note.id"
|
||||
:onClick="openNote"
|
||||
:data="note"
|
||||
:title-view="titleView || isFloatingList"
|
||||
:currently-open="openNotes.includes(note.id)"
|
||||
:key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated + note.archived + note.pinned + note.trashed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="loading-section" v-if="showLoading">
|
||||
<loading-icon message="Decrypting Notes" />
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- found attachments -->
|
||||
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
|
||||
<h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
|
||||
@@ -154,24 +93,51 @@
|
||||
/>
|
||||
</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>
|
||||
|
||||
<div class="show-hidden-note-list-button"
|
||||
v-if="collapseFloatingList && openNotes.length > 0" v-on:click="collapseFloatingList = false">
|
||||
<i class="caret square right outline icon"></i>
|
||||
</div>
|
||||
|
||||
<!-- flexbox note container evenly spaces open notes -->
|
||||
<div class="note-panel-container" :class="{ 'note-panel-fullwidth':collapseFloatingList}" v-if="openNotes.length">
|
||||
<note-input-panel
|
||||
v-for="noteId in openNotes"
|
||||
v-if="noteId != null"
|
||||
:key="noteId"
|
||||
:noteid="noteId"
|
||||
:url-data="$route.params"
|
||||
:open-notes="openNotes.length"
|
||||
/>
|
||||
</div>
|
||||
<note-input-panel
|
||||
v-if="activeNoteId1 != null"
|
||||
:key="activeNoteId1"
|
||||
:noteid="activeNoteId1"
|
||||
:url-data="$route.params"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -181,7 +147,7 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'NotesPage',
|
||||
name: 'SearchBar',
|
||||
components: {
|
||||
|
||||
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
|
||||
@@ -190,9 +156,9 @@
|
||||
// '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,
|
||||
'paste-button':require('@/components/PasteButton.vue').default,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -202,8 +168,6 @@
|
||||
searchResultsCount: 0,
|
||||
searchTags: [],
|
||||
notes: [],
|
||||
openNotes: [],
|
||||
collapseFloatingList: false,
|
||||
highlights: [],
|
||||
searchDebounce: null,
|
||||
fastFilters: {},
|
||||
@@ -211,10 +175,10 @@
|
||||
|
||||
//Load up notes in batches
|
||||
firstLoadBatchSize: 10, //First set of rapidly loaded notes
|
||||
batchSize: 20, //Size of batch loaded when user scrolls through current batch
|
||||
batchSize: 25, //Size of batch loaded when user scrolls through current batch
|
||||
batchOffset: 0, //Tracks the current batch that has been loaded
|
||||
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
|
||||
showLoading: false,
|
||||
loadingInProgress: false,
|
||||
scrollLoadEnabled: true,
|
||||
|
||||
//Clear button is not visible
|
||||
@@ -260,39 +224,37 @@
|
||||
|
||||
this.$parent.loginGateway()
|
||||
|
||||
//If user is on title view,
|
||||
this.titleView = this.$store.getters.getIsUserOnMobile
|
||||
|
||||
this.$io.on('new_note_created', noteId => {
|
||||
|
||||
// Push new note to top of list and animate
|
||||
this.updateSingleNote(noteId)
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
//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 => {
|
||||
|
||||
const drawFocus = !this.openNotes.includes(parseInt(noteId))
|
||||
this.updateSingleNote(noteId, drawFocus)
|
||||
|
||||
//Do not update note if its open
|
||||
if(this.openNotes.includes(parseInt(noteId))){
|
||||
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}) => {
|
||||
|
||||
const drawFocus = !this.openNotes.includes(parseInt(noteId))
|
||||
this.updateSingleNote(noteId, drawFocus)
|
||||
//Do not update note if its open
|
||||
if(this.activeNoteId1 != noteId){
|
||||
this.updateSingleNote(noteId, true)
|
||||
}
|
||||
})
|
||||
|
||||
this.$bus.$on('update_single_note', (noteId) => {
|
||||
|
||||
const drawFocus = !this.openNotes.includes(parseInt(noteId))
|
||||
this.updateSingleNote(noteId, drawFocus)
|
||||
|
||||
//Do not update note if its open
|
||||
if(this.activeNoteId1 != noteId){
|
||||
this.updateSingleNote(noteId)
|
||||
}
|
||||
})
|
||||
|
||||
//Update totals for app
|
||||
@@ -300,7 +262,19 @@
|
||||
|
||||
//Close note event
|
||||
this.$bus.$on('close_active_note', ({noteId, modified}) => {
|
||||
this.closeNote(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) => {
|
||||
@@ -347,13 +321,11 @@
|
||||
|
||||
//Reload page content - don't trigger if load is in progress
|
||||
this.$bus.$on('note_reload', () => {
|
||||
if(!this.showLoading){
|
||||
if(!this.loadingInProgress){
|
||||
this.reset()
|
||||
}
|
||||
})
|
||||
|
||||
// Window scroll needed when scrolling full page.
|
||||
// second scroll event added on note-list for floating view scroll detection
|
||||
window.addEventListener('scroll', this.onScroll)
|
||||
|
||||
//Close notes when back button is pressed
|
||||
@@ -380,9 +352,9 @@
|
||||
},
|
||||
mounted() {
|
||||
|
||||
//Open note on PAGE LOAD if ID is set
|
||||
//Open note on load if ID is set
|
||||
if(this.$route.params.id > 1){
|
||||
this.openNote(this.$route.params.id)
|
||||
this.activeNoteId1 = this.$route.params.id
|
||||
}
|
||||
|
||||
//Loads initial batch and tags
|
||||
@@ -391,123 +363,34 @@
|
||||
},
|
||||
watch: {
|
||||
'$route.params.id': function(id){
|
||||
this.openNote(id)
|
||||
},
|
||||
'$route' (to, from) {
|
||||
|
||||
|
||||
// Reload the notes if returning to this page
|
||||
if(to.fullPath == '/notes' && !from.fullPath.includes('/notes/open/')){
|
||||
this.reset()
|
||||
}
|
||||
|
||||
// Close all notes if returning to /notes page
|
||||
if(to.fullPath == '/notes' && from.fullPath.includes('/notes/open/')){
|
||||
this.closeAllNotes()
|
||||
}
|
||||
|
||||
//Lookup tags set in URL
|
||||
if(to.params.tag && this.$store.getters.totals && this.$store.getters.totals['tags'][to.params.tag]){
|
||||
|
||||
//Lookup tag in store by string
|
||||
const tagObject = this.$store.getters.totals['tags'][to.params.tag]
|
||||
|
||||
//Pull key out of string and load tags for that key
|
||||
this.toggleTagFilter(tagObject.id)
|
||||
return
|
||||
}
|
||||
//Open note on ID, null id will close note
|
||||
this.activeNoteId1 = id
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isFloatingList(){
|
||||
|
||||
//If note 1 or 2 is open, show floating column
|
||||
return (this.openNotes.length > 0)
|
||||
|
||||
methods: {
|
||||
toggleTitleView(){
|
||||
this.titleView = !this.titleView
|
||||
},
|
||||
showOneColumn(){
|
||||
|
||||
return this.$store.getters.getIsUserOnMobile
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
//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){
|
||||
|
||||
//
|
||||
|
||||
const intId = parseInt(id)
|
||||
if(this.openNotes.includes(intId)){
|
||||
|
||||
console.log('Open already open note?')
|
||||
|
||||
// const openIndex = this.openNotes.indexOf(intId)
|
||||
// if(openIndex != -1){
|
||||
// console.log('Open note and remove it ', intId + ' on index ' + openIndex)
|
||||
// this.openNotes.splice(openIndex, 1)
|
||||
// }
|
||||
// this.$bus.$emit('close_note_by_id', intId)
|
||||
return
|
||||
}
|
||||
|
||||
//Don't open note if a link is clicked in display card
|
||||
if(event && event.target && event.target.nodeName){
|
||||
const nodeClick = event.target.nodeName
|
||||
if(nodeClick == 'A'){ return }
|
||||
}
|
||||
|
||||
// Push note to stack if not open
|
||||
if(Number.isInteger(intId) && !this.openNotes.includes(intId)){
|
||||
this.openNotes.push(intId)
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
// change route if open ID is not the same as current ID
|
||||
if(this.$route.params.id != id){
|
||||
console.log('Open note, change route -> route id ' + this.$route.params.id + ' note id ->' + id + ', ' +(this.$route.params.id == id))
|
||||
this.$router.push('/notes/open/'+id)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
//Open note if a link was not clicked
|
||||
this.$router.push('/notes/open/'+id)
|
||||
return
|
||||
},
|
||||
closeNote(noteId, modified){
|
||||
|
||||
console.log('close note', this.$route.fullPath)
|
||||
|
||||
const openIndex = this.openNotes.indexOf(noteId)
|
||||
if(openIndex != -1){
|
||||
console.log('Removing note id ', noteId + ' on index ' + openIndex)
|
||||
this.openNotes.splice(openIndex, 1)
|
||||
}
|
||||
|
||||
// //A note has been closed
|
||||
// if(this.$route.fullPath != '/notes'){
|
||||
// this.$router.push('/notes')
|
||||
// }
|
||||
if(this.openNotes.length == 0 && this.$route.fullPath != '/notes'){
|
||||
this.$router.push('/notes')
|
||||
}
|
||||
|
||||
if(modified){
|
||||
console.log('Just closed Note -> ' + noteId + ', modified -> ', modified)
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
//Focus and animate if modified
|
||||
this.updateSingleNote(noteId, modified)
|
||||
}
|
||||
|
||||
console.log('closeNote(): Open notes length ', this.openNotes.length)
|
||||
},
|
||||
closeAllNotes(){
|
||||
console.log('Close all notes ------------')
|
||||
for (let i = this.openNotes.length - 1; i >= 0; i--) {
|
||||
console.log('Close all notes -> ' + this.openNotes[i])
|
||||
this.closeNote(this.openNotes[i])
|
||||
}
|
||||
console.log('----------------')
|
||||
},
|
||||
toggleTagFilter(tagId){
|
||||
|
||||
this.searchTags = [tagId]
|
||||
@@ -524,10 +407,6 @@
|
||||
},
|
||||
onScroll(e){
|
||||
|
||||
if(!this.scrollLoadEnabled){
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(this.loadingBatchTimeout)
|
||||
this.loadingBatchTimeout = setTimeout(() => {
|
||||
|
||||
@@ -537,12 +416,12 @@
|
||||
const height = document.getElementById('app').scrollHeight
|
||||
|
||||
//Load if less than 500px from the bottom
|
||||
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled){
|
||||
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled && !this.loadingInProgress){
|
||||
|
||||
this.search(true, this.batchSize, true)
|
||||
this.search(false, this.batchSize, true)
|
||||
}
|
||||
|
||||
}, 50)
|
||||
}, 30)
|
||||
|
||||
|
||||
return
|
||||
@@ -563,24 +442,21 @@
|
||||
}
|
||||
|
||||
this.lastVisibilityState = document.visibilityState
|
||||
|
||||
},
|
||||
// @TODO Don't even trigger this if the note wasn't changed
|
||||
updateSingleNote(noteId, focuseAndAnimate = true){
|
||||
|
||||
// console.log('updating single note', noteId)
|
||||
|
||||
noteId = parseInt(noteId)
|
||||
|
||||
//Find local note, if it exists; continue
|
||||
let note = null
|
||||
if(this.$refs['note-'+noteId]?.[0]?.note){
|
||||
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
|
||||
note = this.$refs['note-'+noteId][0].note
|
||||
//Show that note is working on updating
|
||||
this.$refs['note-'+noteId][0].showWorking = true
|
||||
}
|
||||
|
||||
this.rebuildNoteCategorise()
|
||||
// return
|
||||
|
||||
//Lookup one note using passed in ID
|
||||
const postData = {
|
||||
@@ -602,7 +478,6 @@
|
||||
return
|
||||
}
|
||||
|
||||
// if old note data and new note data exists
|
||||
if(note && newNote){
|
||||
|
||||
//go through each prop and update it with new values
|
||||
@@ -611,7 +486,7 @@
|
||||
})
|
||||
|
||||
//Push new note to front if its modified or we want it to
|
||||
if( note.updated != newNote.updated ){
|
||||
if( focuseAndAnimate || note.updated != newNote.updated ){
|
||||
|
||||
// Find note, in section, move to front
|
||||
Object.keys(this.noteSections).forEach( key => {
|
||||
@@ -625,9 +500,6 @@
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if( focuseAndAnimate ){
|
||||
this.$nextTick( () => {
|
||||
//Trigger close animation on note
|
||||
this.$refs['note-'+noteId][0].justClosed()
|
||||
@@ -670,14 +542,19 @@
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
//Don't double load note batches
|
||||
if(this.showLoading){
|
||||
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){
|
||||
this.batchOffset = 0 // Reset batch offset if we are not merging note batches or new set will be offset from current and overwrite current set with second batch
|
||||
Object.keys(this.noteSections).forEach( key => {
|
||||
this.noteSections[key] = []
|
||||
})
|
||||
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
|
||||
}
|
||||
this.searchResultsCount = 0
|
||||
|
||||
//Remove all filter limits from previous queries
|
||||
delete this.fastFilters.limitSize
|
||||
@@ -705,40 +582,25 @@
|
||||
}
|
||||
|
||||
//Perform search - or die
|
||||
this.showLoading = showLoading
|
||||
this.scrollLoadEnabled = false
|
||||
this.loadingInProgress = true
|
||||
axios.post('/api/note/search', postData)
|
||||
.then(response => {
|
||||
|
||||
//Reset a lot of stuff if we are not merging batches
|
||||
if(!mergeExisting){
|
||||
Object.keys(this.noteSections).forEach( key => {
|
||||
this.noteSections[key] = []
|
||||
})
|
||||
}
|
||||
this.searchResultsCount = 0
|
||||
|
||||
// console.timeEnd('Fetch TitleCard Batch '+notesInNextLoad)
|
||||
|
||||
//Save the number of notes just loaded
|
||||
this.batchOffset += response.data.notes.length
|
||||
|
||||
//Enable scroll loading if endpoint retured notes
|
||||
//Enable or disable scroll loading
|
||||
this.scrollLoadEnabled = response.data.notes.length > 0
|
||||
|
||||
if(response.data.total > 0){
|
||||
this.searchResultsCount = response.data.total
|
||||
}
|
||||
|
||||
this.showLoading = false
|
||||
this.loadingInProgress = false
|
||||
this.generateNoteCategories(response.data.notes, mergeExisting)
|
||||
|
||||
//cache initial notes for faster reloads
|
||||
if(!mergeExisting && this.showClear == false){
|
||||
const cachedNotesJson = JSON.stringify(response.data.notes)
|
||||
localStorage.setItem('snippetCache', cachedNotesJson)
|
||||
}
|
||||
|
||||
return resolve(true)
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
|
||||
@@ -853,7 +715,7 @@
|
||||
//clear out tags
|
||||
this.searchTags = []
|
||||
this.tagSuggestions = []
|
||||
this.showLoading = false
|
||||
this.loadingInProgress = false
|
||||
this.searchTerm = ''
|
||||
this.$bus.$emit('reset_fast_filters') //Clear out search
|
||||
|
||||
@@ -870,32 +732,15 @@
|
||||
filter[options[index]] = 1
|
||||
|
||||
this.fastFilters = filter
|
||||
|
||||
//If notes exist in cache, load them up
|
||||
let showLoading = true
|
||||
const cachedNotesJson = localStorage.getItem('snippetCache')
|
||||
const cachedNotes = JSON.parse(cachedNotesJson)
|
||||
if(cachedNotes && cachedNotes.length > 0 && !this.showClear){
|
||||
|
||||
//Load cache. do not merge existing
|
||||
this.generateNoteCategories(cachedNotes, false)
|
||||
showLoading = false
|
||||
}
|
||||
|
||||
//Fetch First batch of notes with new filter
|
||||
this.search(showLoading, this.batchSize, false)
|
||||
// .then( r => this.search(false, this.batchSize, true))
|
||||
this.search(true, this.firstLoadBatchSize, false)
|
||||
.then( r => this.search(false, this.batchSize, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.text-fix {
|
||||
padding: 8px 0 0 15px;
|
||||
display: inline-block;
|
||||
color: var(--menu-accent);
|
||||
}
|
||||
.detail {
|
||||
float: right;
|
||||
}
|
||||
@@ -913,150 +758,4 @@
|
||||
.note-card-section + .note-card-section {
|
||||
padding: 15px 0 0;
|
||||
}
|
||||
.loading-section {
|
||||
color: var(--main-accent);
|
||||
box-shadow: 0 1px 3px 0 var(--main-accent);
|
||||
border-radius: 6px;
|
||||
background-color: var(--small_element_bg_color);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.floating-list {
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 25%;
|
||||
height: 100vh;
|
||||
background-color: var(--small_element_bg_color);
|
||||
padding: 15px 5px 0px 10px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
background-color: var(--border_color);
|
||||
}
|
||||
.floating-list::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
.note-panel-container {
|
||||
position: fixed;
|
||||
width: 75%;
|
||||
height: 100vh;
|
||||
background: gray;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
align-content: stretch;
|
||||
|
||||
z-index: 1000;
|
||||
}
|
||||
.note-panel-fullwidth {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.note-panel-container > div {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
.hidden-floating-list {
|
||||
left: -1000px !important;
|
||||
}
|
||||
.show-hidden-note-list-button {
|
||||
position: fixed;
|
||||
top: 25px;
|
||||
left: 0;
|
||||
min-width: 45px;
|
||||
background-color: var(--main-accent);
|
||||
color: var(--text_color);
|
||||
display: block;
|
||||
z-index: 1100;
|
||||
cursor: pointer;
|
||||
border-bottom-right-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
padding: 8px 0px 8px 13px;
|
||||
text-align: left;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
@media (min-width:320px) { /* smartphones, iPhone, portrait 480x320 phones */
|
||||
.floating-list {
|
||||
left: -1000px;
|
||||
}
|
||||
.note-panel-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@media (min-width:481px) { /* portrait e-readers (Nook/Kindle), smaller tablets @ 600 or @ 640 wide. */
|
||||
.floating-list {
|
||||
left: 0px;
|
||||
}
|
||||
.note-panel-container {
|
||||
width: 75%;
|
||||
}
|
||||
}
|
||||
@media (min-width:641px) { /* portrait tablets, portrait iPad, landscape e-readers, landscape 800x480 or 854x480 phones */
|
||||
|
||||
}
|
||||
@media (min-width:961px) { /* tablet, landscape iPad, lo-res laptops ands desktops */
|
||||
|
||||
}
|
||||
@media (min-width:1025px) { /* big landscape tablets, laptops, and desktops */
|
||||
|
||||
}
|
||||
@media (min-width:1281px) { /* hi-res laptops and desktops */
|
||||
|
||||
}
|
||||
@media (min-width:2000px) { /* BIG hi-res laptops and desktops */
|
||||
.floating-list {
|
||||
left: 180px;
|
||||
width: calc(30% - 180px);
|
||||
}
|
||||
.note-panel-container {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
.master-note-edit {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background: var(--small_element_bg_color);
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.master-note-edit + .master-note-edit {
|
||||
border-left: 2px solid var(--main-accent);
|
||||
border-left: 5px solid var(--border_color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</style>
|
@@ -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>
|
@@ -1,163 +1,129 @@
|
||||
<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 class="row"></div>
|
||||
<!-- spacer column -->
|
||||
<div class="sixteen wide column">
|
||||
<h2 class="ui dividing header">
|
||||
<i class="cog icon"></i>
|
||||
Settings
|
||||
</h2>
|
||||
|
||||
<div class="ui segment">
|
||||
<h3>Change Password</h3>
|
||||
<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 -->
|
||||
<div class="ui segment">
|
||||
<div class="ui grid">
|
||||
<div class="sixteen wide column">
|
||||
<h3 class="ui header">
|
||||
Accent Color
|
||||
</h3>
|
||||
<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 -->
|
||||
<div class="ui segment">
|
||||
<h3>Two Factor Authentication</h3>
|
||||
<div class="ui stackable grid">
|
||||
<div class="four wide column">
|
||||
<p>1. Enter Password and get QR</p>
|
||||
<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="five wide column">
|
||||
<p>2. Scan QR Code</p>
|
||||
<p v-if="qrCode == ''">QR Code Will appear here.</p>
|
||||
<img v-if="qrCode != ''" :src="qrCode" alt="QR Code">
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<p>3. Verify with code</p>
|
||||
<div class="ui input" v-if="qrCode != ''">
|
||||
<input type="text" placeholder="Verification Code" v-model="verificationToken" v-on:keyup.enter="verifyQrCode()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- change password -->
|
||||
<div class="ui segment">
|
||||
<h3>Change Password</h3>
|
||||
<div class="ui grid">
|
||||
<div class="five wide column">
|
||||
<label>Current Password</label>
|
||||
<div class="ui fluid input">
|
||||
<input v-model="change1" type="password" placeholder="Current Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="five wide column">
|
||||
<label>New Password</label>
|
||||
<div class="ui fluid input">
|
||||
<input v-model="change2" type="password" placeholder="New Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<label>Rereat New Password</label>
|
||||
<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 -->
|
||||
<div class="ui segment">
|
||||
<div class="ui grid">
|
||||
<div class="sixteen wide column">
|
||||
<h3>Log Out</h3>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<div class="ui button" v-on:click="logout()">
|
||||
Log Out this browser
|
||||
</div>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<div class="ui button" v-on:click="revokeAllSessions()">
|
||||
Log Out all other browsers
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -209,6 +175,14 @@
|
||||
})
|
||||
|
||||
},
|
||||
logout() {
|
||||
this.$store.commit('destroyLoginToken')
|
||||
this.$router.push('/')
|
||||
axios.post('/api/user/logout')
|
||||
setTimeout(() => {
|
||||
this.$bus.$emit('notification', 'Logged Out')
|
||||
}, 200)
|
||||
},
|
||||
setAccentColor(color){
|
||||
|
||||
let root = document.documentElement
|
||||
|
@@ -6,14 +6,11 @@ import Router from 'vue-router'
|
||||
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')
|
||||
|
||||
Vue.use(Router)
|
||||
@@ -44,12 +41,6 @@ export default new Router({
|
||||
meta: {title: 'Open Note'},
|
||||
component: NotesPage,
|
||||
},
|
||||
{
|
||||
path: '/search/tags/:tag',
|
||||
name: 'Search Notes',
|
||||
meta: {title: 'Search Notes'},
|
||||
component: NotesPage,
|
||||
},
|
||||
{
|
||||
path: '/notes/open/:id/menu/:openMenu',
|
||||
name: 'Open Note Menu',
|
||||
@@ -62,19 +53,7 @@ export default new Router({
|
||||
meta: {title:'Help'},
|
||||
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'},
|
||||
@@ -110,24 +89,11 @@ 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: {
|
||||
}
|
||||
})
|
@@ -9,9 +9,7 @@ export default new Vuex.Store({
|
||||
username: null,
|
||||
nightMode: false,
|
||||
isUserOnMobile: false,
|
||||
fetchTotalsTimeout: null,
|
||||
userTotals: null, // {} // setting this to object breaks reactivity
|
||||
activeSessions: 0,
|
||||
userTotals: null,
|
||||
},
|
||||
mutations: {
|
||||
setUsername(state, username){
|
||||
@@ -26,33 +24,39 @@ export default new Vuex.Store({
|
||||
localStorage.removeItem('loginToken')
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('currentVersion')
|
||||
localStorage.removeItem('snippetCache')
|
||||
delete axios.defaults.headers.common['authorizationtoken']
|
||||
state.username = null
|
||||
state.userTotals = null
|
||||
},
|
||||
toggleNightMode(state, pastTheme){
|
||||
|
||||
const themes = {
|
||||
'white':{
|
||||
'body_bg_color': '#f1f1f1',//'#f5f6f7',
|
||||
'body_bg_color': '#f5f6f7',
|
||||
'small_element_bg_color': '#fff',
|
||||
'text_color': '#3d3d3d',
|
||||
'dark_border_color': '#d9d9d9',//'#DFE1E6',
|
||||
'dark_border_color': '#DFE1E6',
|
||||
'border_color': '#DFE1E6',
|
||||
'menu-accent': '#cecece',
|
||||
'menu-text': '#5e6268',
|
||||
},
|
||||
'black':{
|
||||
'body_bg_color': 'rgb(12 4 30)',
|
||||
//'#0f0f0f',//'#000',
|
||||
'body_bg_color': '#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',
|
||||
'dark_border_color': '#ACACAC', //Lighter color to accent elemnts user can interact with
|
||||
'border_color': '#555',
|
||||
'menu-accent': '#626262',
|
||||
'menu-text': '#d9d9d9',
|
||||
},
|
||||
'night':{
|
||||
'body_bg_color': '#000',
|
||||
'small_element_bg_color': '#000',
|
||||
'text_color': '#a98457',
|
||||
'dark_border_color': '#a98457',
|
||||
'border_color': '#555',
|
||||
'menu-accent': '#626262',
|
||||
'menu-text': '#a69682',
|
||||
},
|
||||
}
|
||||
|
||||
//Catch values not in set
|
||||
@@ -94,6 +98,10 @@ 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,23 +109,8 @@ export default new Vuex.Store({
|
||||
state.socket = socket
|
||||
},
|
||||
setUserTotals(state, 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
|
||||
//Save all the totals for the user
|
||||
state.userTotals = totalsObject
|
||||
|
||||
//Set computer version from server
|
||||
const currentVersion = localStorage.getItem('currentVersion')
|
||||
@@ -137,15 +130,6 @@ export default new Vuex.Store({
|
||||
// Object.keys(totalsObject).forEach( key => {
|
||||
// console.log(key + ' -- ' + totalsObject[key])
|
||||
// })
|
||||
},
|
||||
setActiveSessions(state, countData){
|
||||
//Count of the number of active socket.io sessions for this user
|
||||
state.activeSessions = countData
|
||||
},
|
||||
hideMetricTrackingReminder(state){
|
||||
if(state.userTotals){
|
||||
state.userTotals['showTrackMetricsButton'] = false
|
||||
}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
@@ -171,29 +155,19 @@ 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
|
||||
fetchAndUpdateUserTotals ({ commit }) {
|
||||
axios.post('/api/user/totals')
|
||||
.then( ({data}) => {
|
||||
commit('setUserTotals', data)
|
||||
})
|
||||
.catch( error => {
|
||||
if(error.response && error.response.status == 400){
|
||||
commit('destroyLoginToken')
|
||||
location.reload()
|
||||
}
|
||||
axios.post('/api/user/totals', postData)
|
||||
.then( ({data}) => {
|
||||
commit('setUserTotals', data)
|
||||
})
|
||||
.catch( error => {
|
||||
if(error.response && error.response.status == 400){
|
||||
commit('destroyLoginToken')
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
@@ -12,4 +12,3 @@ bundle.*
|
||||
client/dist*
|
||||
server/public/*
|
||||
client/dist*
|
||||
*_scrape*
|
@@ -1,11 +0,0 @@
|
||||
const path = '../../'
|
||||
const prefix = '/$1'
|
||||
module.exports = {
|
||||
moduleNameMapper: {
|
||||
"@root/(.*)": ".",
|
||||
"@models/(.*)": path+"server/models"+prefix,
|
||||
"@routes/(.*)": path+"server/routes"+prefix,
|
||||
"@helpers/(.*)": path+"server/helpers"+prefix,
|
||||
"@config/(.*)": path+"server/config"+prefix,
|
||||
}
|
||||
}
|
8993
package-lock.json
generated
19
package.json
@@ -1,29 +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": "^4.16.4",
|
||||
"express-rate-limit": "^5.1.3",
|
||||
"gm": "^1.23.1",
|
||||
"helmet": "^4.1.1",
|
||||
"helmet": "^3.23.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"module-alias": "^2.2.2",
|
||||
"multer": "^1.4.2",
|
||||
"mysql2": "^2.2.5",
|
||||
"node-tesseract-ocr": "^2.0.0",
|
||||
"mysql2": "^1.7.0",
|
||||
"node-tesseract-ocr": "^1.0.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"request": "^2.88.2",
|
||||
"request-promise": "^4.2.6",
|
||||
"request-promise": "^4.2.5",
|
||||
"socket.io": "^2.3.0",
|
||||
"speakeasy": "^2.0.0"
|
||||
},
|
||||
@@ -33,8 +33,5 @@
|
||||
"@routes": "server/routes",
|
||||
"@helpers": "server/helpers",
|
||||
"@config": "server/config"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0"
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
//Import mysql2 package
|
||||
const mysql = require('mysql2');
|
||||
const os = require('os') //Used to get path of home directory
|
||||
const result = require('dotenv').config({ path:(os.homedir()+'/.env') })
|
||||
|
||||
// Create the connection pool.
|
||||
const pool = mysql.createPool({
|
||||
|
@@ -6,7 +6,6 @@ const speakeasy = require('speakeasy')
|
||||
let Auth = {}
|
||||
|
||||
const tokenSecretKey = process.env.JSON_KEY
|
||||
const sessionTokenUses = 300 //Defines number of uses each session token has before being refreshed
|
||||
|
||||
//Creates session token
|
||||
Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => {
|
||||
@@ -27,7 +26,7 @@ Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) =>
|
||||
|
||||
return db.promise().query(
|
||||
'INSERT INTO user_active_session (salt, encrypted_master_password, created, uses, user_hash, session_id) VALUES (?,?,?,?,?,?)',
|
||||
[salt, encryptedMasterPass, created, sessionTokenUses, userHash, sessionId])
|
||||
[salt, encryptedMasterPass, created, 40, userHash, sessionId])
|
||||
|
||||
})
|
||||
.then((r,f) => {
|
||||
|
@@ -72,8 +72,6 @@ CryptoString.createSalt = () => {
|
||||
|
||||
return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64')
|
||||
}
|
||||
|
||||
// Creates a small random salt
|
||||
CryptoString.createSmallSalt = () => {
|
||||
|
||||
return crypto.randomBytes(20).toString('base64')
|
||||
|
@@ -6,7 +6,7 @@ let SiteScrape = module.exports = {}
|
||||
|
||||
const removeWhitespace = /\s+/g
|
||||
|
||||
const commonWords = ['just','start','what','these','how', 'was', 'being','can','way','share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
|
||||
const commonWords = ['share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
|
||||
'give','use','find','tell','ask','work','seem','feel','try','leave','call','good','new','first','last','long','great','little','own','other','old',
|
||||
'right','big','high','different','small','large','next','early','young','important','few','public','bad','same','able','to','of','in','for','on',
|
||||
'with','at','by','from','up','about','into','over','after','the','and','a','that','I','it','not','he','as','you','this','but','his','they','her',
|
||||
@@ -54,7 +54,7 @@ SiteScrape.getCleanUrls = (textBlock) => {
|
||||
SiteScrape.getHostName = (url) => {
|
||||
|
||||
var hostname = 'https://'+(new URL(url)).hostname;
|
||||
// console.log('hostname', hostname)
|
||||
console.log('hostname', hostname)
|
||||
return hostname
|
||||
}
|
||||
|
||||
@@ -63,95 +63,36 @@ SiteScrape.getDisplayImage = ($, url) => {
|
||||
|
||||
const hostname = SiteScrape.getHostName(url)
|
||||
|
||||
let metaImg = $('[property="og:image"]')
|
||||
let shortcutIcon = $('[rel="shortcut icon"]')
|
||||
let favicon = $('[rel="icon"]')
|
||||
let metaImg = $('meta[property="og:image"]')
|
||||
let shortcutIcon = $('link[rel="shortcut icon"]')
|
||||
let favicon = $('link[rel="icon"]')
|
||||
let randomImg = $('img')
|
||||
|
||||
//Set of images we may want gathered from various places in source
|
||||
let imagesWeWant = []
|
||||
let thumbnail = ''
|
||||
console.log('----')
|
||||
|
||||
//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 first random image we find
|
||||
if(randomImg && randomImg[0] && randomImg[0].attribs){
|
||||
thumbnail = hostname + randomImg[0].attribs.src
|
||||
console.log('random img '+thumbnail)
|
||||
}
|
||||
//Grab the shortcut icon
|
||||
//Grab the favicon of the site
|
||||
if(favicon && favicon[0] && favicon[0].attribs){
|
||||
imagesWeWant.push(favicon[0].attribs.href)
|
||||
thumbnail = hostname + favicon[0].attribs.href
|
||||
console.log('favicon '+thumbnail)
|
||||
}
|
||||
//Grab the shortcut icon
|
||||
if(shortcutIcon && shortcutIcon[0] && shortcutIcon[0].attribs){
|
||||
imagesWeWant.push(shortcutIcon[0].attribs.href)
|
||||
thumbnail = hostname + shortcutIcon[0].attribs.href
|
||||
console.log('shortcut '+thumbnail)
|
||||
}
|
||||
//Grab the presentation image for the site
|
||||
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
|
||||
|
||||
thumbnail = metaImg[0].attribs.content
|
||||
console.log('ogImg '+thumbnail)
|
||||
}
|
||||
|
||||
console.log('-----')
|
||||
return thumbnail
|
||||
}
|
||||
|
||||
@@ -162,28 +103,19 @@ SiteScrape.getKeywords = ($) => {
|
||||
|
||||
majorContent += $('[class*=content]').text()
|
||||
.replace(removeWhitespace, " ") //Remove all whitespace
|
||||
// .replace(/\W\s/g, '') //Remove all non alphanumeric characters
|
||||
.substring(0,6000) //Limit to 6000 characters
|
||||
.replace(/\W\s/g, '') //Remove all non alphanumeric characters
|
||||
.substring(0,3000) //Limit to 3000 characters
|
||||
.toLowerCase()
|
||||
.replace(/[^A-Za-z0-9- ]/g, '');
|
||||
|
||||
|
||||
console.log(majorContent)
|
||||
|
||||
//Count frequency of each word in scraped text
|
||||
let frequency = {}
|
||||
majorContent.split(' ').forEach(word => {
|
||||
// Exclude short or common words
|
||||
if(commonWords.includes(word) || word.length <= 2){
|
||||
return
|
||||
if(commonWords.includes(word)){
|
||||
return //Exclude certain words
|
||||
}
|
||||
if(!frequency[word]){
|
||||
frequency[word] = 0
|
||||
}
|
||||
// Skip some plurals
|
||||
if(frequency[word+'s'] || frequency[word+'es']){
|
||||
return
|
||||
}
|
||||
frequency[word]++
|
||||
})
|
||||
|
||||
@@ -201,7 +133,7 @@ SiteScrape.getKeywords = ($) => {
|
||||
});
|
||||
|
||||
let finalWords = []
|
||||
for(let i=0; i<6; i++){
|
||||
for(let i=0; i<5; i++){
|
||||
if(sortable[i] && sortable[i][0]){
|
||||
finalWords.push(sortable[i][0])
|
||||
}
|
||||
|
144
server/index.js
@@ -1,12 +1,7 @@
|
||||
//Set up environmental variables, pulled from ~/.env file used as process.env.DB_HOST
|
||||
//Set up environmental variables, pulled from .env file used as process.env.DB_HOST
|
||||
const os = require('os') //Used to get path of home directory
|
||||
const 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')
|
||||
|
||||
@@ -20,24 +15,23 @@ const helmet = require('helmet')
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
app.use( helmet() )
|
||||
// allow for the parsing of url encoded forms
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
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'
|
||||
@@ -57,31 +51,12 @@ io.on('connection', function(socket){
|
||||
Auth.decodeToken(token)
|
||||
.then(userData => {
|
||||
socket.join(userData.userId)
|
||||
|
||||
//Track active logged in user accounts
|
||||
const usersInRoom = io.sockets.adapter.rooms[userData.userId]
|
||||
io.to(userData.userId).emit('update_active_user_count', usersInRoom.length)
|
||||
|
||||
}).catch(error => {
|
||||
//Don't add user to room if they are not logged in
|
||||
// console.log(error)
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('get_active_user_count', token => {
|
||||
Auth.decodeToken(token)
|
||||
.then(userData => {
|
||||
socket.join(userData.userId)
|
||||
|
||||
//Track active logged in user accounts
|
||||
const usersInRoom = io.sockets.adapter.rooms[userData.userId]
|
||||
io.to(userData.userId).emit('update_active_user_count', usersInRoom.length)
|
||||
|
||||
}).catch(error => {
|
||||
// console.log(error)
|
||||
})
|
||||
})
|
||||
|
||||
//Renew Session tokens when users request a new one
|
||||
socket.on('renew_session_token', token => {
|
||||
|
||||
@@ -116,12 +91,29 @@ io.on('connection', function(socket){
|
||||
|
||||
//Emit all sorted diffs to user
|
||||
socket.emit('past_diffs', noteDiffs[rawTextId])
|
||||
} else {
|
||||
socket.emit('past_diffs', null)
|
||||
}
|
||||
|
||||
const usersInRoom = io.sockets.adapter.rooms[rawTextId]
|
||||
if(usersInRoom){
|
||||
//Update users in room count
|
||||
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
|
||||
|
||||
//Debugging text - prints out notes in limbo
|
||||
let noteDiffKeys = Object.keys(noteDiffs)
|
||||
let totalDiffs = 0
|
||||
noteDiffKeys.forEach(diffSetKey => {
|
||||
if(noteDiffs[diffSetKey]){
|
||||
totalDiffs += noteDiffs[diffSetKey].length
|
||||
}
|
||||
})
|
||||
//Debugging Text
|
||||
if(noteDiffKeys.length > 0){
|
||||
console.log('Total notes in limbo -> ', noteDiffKeys.length)
|
||||
console.log('Total Diffs for all notes -> ', totalDiffs)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
@@ -147,13 +139,31 @@ io.on('connection', function(socket){
|
||||
|
||||
noteDiffs[noteId].push(data)
|
||||
|
||||
// Go over each user in this note-room
|
||||
//Remove duplicate diffs if they exist
|
||||
for (var i = noteDiffs[noteId].length - 1; i >= 0; i--) {
|
||||
|
||||
let pastDiff = noteDiffs[noteId][i]
|
||||
|
||||
for (var j = noteDiffs[noteId].length - 1; j >= 0; j--) {
|
||||
let currentDiff = noteDiffs[noteId][j]
|
||||
|
||||
if(i == j){
|
||||
continue
|
||||
}
|
||||
|
||||
if(currentDiff.diff == pastDiff.diff || currentDiff.time == pastDiff.time){
|
||||
console.log('Removing Duplicate')
|
||||
noteDiffs[noteId].splice(i,1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Each user joins a room when they open the app.
|
||||
io.in(noteId).clients((error, clients) => {
|
||||
if (error) throw error;
|
||||
|
||||
//Go through each client in note-room and send them the diff
|
||||
//Go through each client in note room and send them the diff
|
||||
clients.forEach(socketId => {
|
||||
// only send off diff if user
|
||||
if(socketId != socket.id){
|
||||
io.to(socketId).emit('incoming_diff', data)
|
||||
}
|
||||
@@ -180,6 +190,7 @@ io.on('connection', function(socket){
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo)
|
||||
|
||||
if(noteDiffs[checkpoint.rawTextId].length == 0){
|
||||
@@ -194,14 +205,14 @@ io.on('connection', function(socket){
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('disconnect', function(socket){
|
||||
socket.on('disconnect', function(){
|
||||
// console.log('user disconnected');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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
|
||||
@@ -211,7 +222,6 @@ app.use(express.json({limit: '5mb'}))
|
||||
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
|
||||
|
||||
@@ -233,7 +243,10 @@ app.use(function(req, res, next){
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
next('Unauthorized')
|
||||
console.log(error)
|
||||
|
||||
res.statusMessage = error //Throw 400 error if token is bad
|
||||
res.status(400).end()
|
||||
})
|
||||
} else {
|
||||
next() //No token. Move along.
|
||||
@@ -242,24 +255,23 @@ app.use(function(req, res, next){
|
||||
|
||||
|
||||
// Test Area
|
||||
// const printResults = true
|
||||
// let UserTest = require('@models/User')
|
||||
// let NoteTest = require('@models/Note')
|
||||
// let AuthTest = require('@helpers/Auth')
|
||||
// Auth.test()
|
||||
// UserTest.keyPairTest('genMan30', '1', printResults)
|
||||
// .then( ({testUserId, masterKey}) =>
|
||||
// NoteTest.test(testUserId, masterKey, printResults))
|
||||
// .then( message => {
|
||||
// if(printResults) console.log(message)
|
||||
// Auth.testTwoFactor()
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.log(error)
|
||||
// })
|
||||
const printResults = true
|
||||
let UserTest = require('@models/User')
|
||||
let NoteTest = require('@models/Note')
|
||||
let AuthTest = require('@helpers/Auth')
|
||||
|
||||
Auth.testTwoFactor()
|
||||
|
||||
Auth.test()
|
||||
UserTest.keyPairTest('genMan23', '1', printResults)
|
||||
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
|
||||
.then( message => {
|
||||
if(printResults) console.log(message)
|
||||
})
|
||||
|
||||
|
||||
//Test
|
||||
app.get('/api', (req, res) => res.send('Solidscribe /API is up and running'))
|
||||
app.get('/api', (req, res) => res.send('Solidscribe API is up and running'))
|
||||
|
||||
//Serve up uploaded files
|
||||
app.use('/api/static', express.static( __dirname+'/../staticFiles' ))
|
||||
@@ -288,23 +300,7 @@ app.use('/api/attachment', attachment)
|
||||
var quickNote = require('@routes/quicknoteController')
|
||||
app.use('/api/quick-note', quickNote)
|
||||
|
||||
//cycle tracking endpoint
|
||||
var metricTracking = require('@routes/metrictrackingController')
|
||||
app.use('/api/metric-tracking', metricTracking)
|
||||
|
||||
//Output running status
|
||||
app.listen(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,7 +1,6 @@
|
||||
let db = require('@config/database')
|
||||
|
||||
let SiteScrape = require('@helpers/SiteScrape')
|
||||
const cs = require('@helpers/CryptoString')
|
||||
|
||||
let Attachment = module.exports = {}
|
||||
|
||||
@@ -33,7 +32,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 +45,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 +79,18 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeSha
|
||||
})
|
||||
}
|
||||
|
||||
//Returns all attachments
|
||||
Attachment.forNote = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND visible = 1 ORDER BY last_indexed DESC;`, [userId, noteId])
|
||||
.then((rows, fields) => {
|
||||
resolve(rows[0]) //Return all attachments found by query
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.urlForNote = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
@@ -185,7 +166,6 @@ Attachment.delete = (userId, attachmentId, urlDelete = false) => {
|
||||
.catch(console.log)
|
||||
}
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -302,13 +282,9 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
|
||||
//Once everything is done being scraped, emit new attachment events
|
||||
SocketIo.to(userId).emit('update_counts')
|
||||
|
||||
// Tell user to update attachments with scraped text
|
||||
SocketIo.to(userId).emit('update_note_attachments')
|
||||
|
||||
solrAttachmentText += freshlyScrapedText
|
||||
resolve(solrAttachmentText)
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -336,13 +312,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 +324,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 +346,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,24 +353,21 @@ 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) => {
|
||||
|
||||
@@ -427,7 +395,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)
|
||||
@@ -452,12 +420,9 @@ Attachment.processUrl = (userId, noteId, url) => {
|
||||
const keywords = SiteScrape.getKeywords($)
|
||||
|
||||
var desiredSearchText = ''
|
||||
desiredSearchText += pageTitle
|
||||
if(keywords){
|
||||
desiredSearchText += "\n " + keywords
|
||||
}
|
||||
desiredSearchText += pageTitle + "\n"
|
||||
desiredSearchText += keywords
|
||||
|
||||
console.log('Results from site scrape-------------')
|
||||
console.log({
|
||||
pageTitle,
|
||||
hostname,
|
||||
@@ -507,142 +472,40 @@ Attachment.processUrl = (userId, noteId, url) => {
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Scrape pooped out')
|
||||
console.log('Issue with scrape', error.statusCode)
|
||||
clearTimeout(requestTimeout)
|
||||
return resolve('No site text')
|
||||
// console.log('Scrape pooped out')
|
||||
// console.log('Issue with scrape')
|
||||
console.log(error)
|
||||
// resolve('')
|
||||
})
|
||||
|
||||
requestTimeout = setTimeout( () => {
|
||||
console.log('Cancel the request, its taking to long.')
|
||||
request.cancel()
|
||||
return resolve('Request Timeout')
|
||||
|
||||
desiredSearchText = 'No Description for -> '+url
|
||||
|
||||
created = Math.round((+new Date)/1000)
|
||||
db.promise()
|
||||
.query(`UPDATE attachment SET
|
||||
text = ?,
|
||||
last_indexed = ?,
|
||||
WHERE id = ?
|
||||
`, [desiredSearchText, created, insertedId])
|
||||
.then((rows, fields) => {
|
||||
resolve(desiredSearchText) //Return found text
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
//Create attachment in DB with scrape text and provided data
|
||||
// db.promise()
|
||||
// .query(`INSERT INTO attachment
|
||||
// (note_id, user_id, attachment_type, text, url, last_indexed)
|
||||
// VALUES (?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created])
|
||||
// .then((rows, fields) => {
|
||||
// resolve(desiredSearchText) //Return found text
|
||||
// })
|
||||
// .catch(console.log)
|
||||
|
||||
}, scrapeTime )
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.generatePushKey = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
db.promise()
|
||||
.query("SELECT pushkey FROM user WHERE id = ? LIMIT 1", [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
const pushKey = rows[0][0].pushkey
|
||||
|
||||
// push key exists
|
||||
if(pushKey && pushKey.length > 0){
|
||||
|
||||
return resolve(pushKey)
|
||||
|
||||
} else {
|
||||
|
||||
// generate and save a new key
|
||||
const newPushKey = cs.createSmallSalt()
|
||||
db.promise()
|
||||
.query('UPDATE user SET pushkey = ? WHERE id = ? LIMIT 1', [newPushKey,userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
return resolve(newPushKey)
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.deletePushKey = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
db.promise()
|
||||
.query('UPDATE user SET pushkey = null WHERE id = ? LIMIT 1', [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
return resolve(rows[0].affectedRows == 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.getPushkeyBookmarklet = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
Attachment.generatePushKey(userId)
|
||||
.then( pushKey => {
|
||||
|
||||
let bookmarklet = Attachment.generateBookmarkletText(pushKey)
|
||||
return resolve(bookmarklet)
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.pushUrl = (pushkey,url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let userId = null
|
||||
pushkey = pushkey.replace(/ /g, '+')
|
||||
|
||||
db.promise()
|
||||
.query("SELECT id FROM user WHERE pushkey = ? LIMIT 1", [pushkey])
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(rows[0].length == 0){
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
userId = rows[0][0].id
|
||||
return Attachment.scrapeUrlsCreateAttachments(userId, null, [url])
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
if(typeof SocketIo != 'undefined'){
|
||||
//Once everything is done being scraped, emit new attachment events
|
||||
SocketIo.to(userId).emit('update_counts')
|
||||
|
||||
// Tell user to update attachments with scraped text
|
||||
SocketIo.to(userId).emit('update_note_attachments')
|
||||
}
|
||||
|
||||
return resolve(true)
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.generateBookmarkletText = (pushKey) => {
|
||||
|
||||
const endpoint = '/api/public/pushmebaby'
|
||||
let url = 'https://www.solidscribe.com' + endpoint
|
||||
if(process.env.NODE_ENV === 'development'){
|
||||
// url = 'https://192.168.1.164' + endpoint
|
||||
}
|
||||
|
||||
// Terminate each line with a semi-colon, super important, since spaces are removed.
|
||||
|
||||
// document.getElementById(id).remove();
|
||||
url += '?pushkey='+encodeURIComponent(pushKey)
|
||||
const bookmarkletV3 = `
|
||||
javascript: (() => {
|
||||
var p = encodeURIComponent(window.location.href);
|
||||
var n = "`+url+`&url="+p;
|
||||
window.open(n, '_blank', 'noopener=noopener');
|
||||
window.focus();
|
||||
|
||||
var k = document.createElement("div");
|
||||
k.setAttribute("style", "position:fixed;right:10px;top:10px;z-index:222222;border-radius:4px;font-size:1.3em;padding:20px 15px;background: #8f51be;color:white;");
|
||||
k.innerHTML = "Posted URL to your Solid Scribe account";
|
||||
|
||||
document.body.appendChild(k);
|
||||
|
||||
setTimeout(()=>{
|
||||
k.remove();
|
||||
},5000);
|
||||
|
||||
})();
|
||||
`
|
||||
|
||||
return bookmarkletV3
|
||||
.replace(/\t|\r|\n/gm, "") // Remove tabs, new lines, returns
|
||||
.replace(/\s+/g, ' ') // remove double spaces
|
||||
.trim()
|
||||
}
|
@@ -1,71 +0,0 @@
|
||||
let db = require('@config/database')
|
||||
|
||||
let Note = require('@models/Note')
|
||||
|
||||
let MetricTracking = module.exports = {};
|
||||
|
||||
|
||||
MetricTracking.get = (userId, masterKey) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
db.promise()
|
||||
.query(`
|
||||
SELECT note.id FROM note WHERE quick_note = 2 AND user_id = ? LIMIT 1`, [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Quick Note is set, return note object
|
||||
if(rows[0][0] != undefined){
|
||||
|
||||
let noteId = rows[0][0].id
|
||||
const note = Note.get(userId, noteId, masterKey)
|
||||
.then(noteData => {
|
||||
return resolve(noteData)
|
||||
})
|
||||
|
||||
} else {
|
||||
return resolve('no data')
|
||||
}
|
||||
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
MetricTracking.create = (userId, masterKey) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let finalId = null
|
||||
return Note.create(userId, 'Metric Tracking', '', masterKey)
|
||||
.then(insertedId => {
|
||||
finalId = insertedId
|
||||
db.promise().query('UPDATE note SET quick_note = 2 WHERE id = ? AND user_id = ?',[insertedId, userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
const note = Note.get(userId, finalId, masterKey)
|
||||
.then(noteData => {
|
||||
return resolve(noteData)
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
MetricTracking.save = (userId, metricData, masterKey) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let finalId = null
|
||||
|
||||
MetricTracking.get(userId, masterKey)
|
||||
.then(noteObject => {
|
||||
|
||||
return Note.update(userId, noteObject.id, metricData, noteObject.title, noteObject.color, noteObject.pinned, noteObject.archived, null, masterKey)
|
||||
|
||||
})
|
||||
.then( saveResults => {
|
||||
return resolve(saveResults)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
@@ -17,7 +17,6 @@ const fs = require('fs')
|
||||
const gm = require('gm')
|
||||
|
||||
Note.test = (userId, masterKey, printResults) => {
|
||||
return false;
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
|
||||
@@ -43,7 +42,7 @@ Note.test = (userId, masterKey, printResults) => {
|
||||
testNoteId = newNoteId
|
||||
|
||||
return Note.update
|
||||
(userId, testNoteId, 'Note text', 'Test Note beans barns Title', 0, 0, 0, 'hash', masterKey)
|
||||
(userId, testNoteId, 'Note text', 'Test Note beans Title', 0, 0, 0, 'hash', masterKey)
|
||||
|
||||
})
|
||||
.then(() => {
|
||||
@@ -64,14 +63,14 @@ Note.test = (userId, masterKey, printResults) => {
|
||||
|
||||
if(printResults) console.log('Test: Reindex normal note - Pass')
|
||||
|
||||
return Note.encryptedIndexSearch(userId, 'beans barns', null, masterKey)
|
||||
return Note.encryptedIndexSearch(userId, 'beans', null, masterKey)
|
||||
|
||||
})
|
||||
.then(textSearchResults => {
|
||||
|
||||
if(textSearchResults['ids'] && textSearchResults['ids'].length >= 1){
|
||||
if(printResults) console.log('Test: Normal Note Search Index - Pass')
|
||||
} else { console.log('Test: Search Index - Fail-------------> 🥱') }
|
||||
} else { console.log('Test: Search Index - Fail') }
|
||||
|
||||
return ShareNote.addUserToSharedNote(userId, testNoteId, shareUserId, masterKey)
|
||||
})
|
||||
@@ -163,10 +162,6 @@ Note.test = (userId, masterKey, printResults) => {
|
||||
return resolve('Test: Complete ---')
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
return reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -198,7 +193,7 @@ Note.create = (userId, noteTitle = '', noteText = '', masterKey) => {
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(typeof SocketIo != 'undefined'){
|
||||
if(SocketIo){
|
||||
SocketIo.to(userId).emit('new_note_created', rows[0].insertId)
|
||||
}
|
||||
|
||||
@@ -346,7 +341,7 @@ Note.reindex = (userId, masterKey, removeId = null) => {
|
||||
setTimeout(() => {
|
||||
|
||||
if(masterKey == null || note.salt == null){
|
||||
console.log('Error indexing note - master key or salt missing', note.id)
|
||||
console.log('Error indexing note', note.id)
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
@@ -395,13 +390,13 @@ Note.reindex = (userId, masterKey, removeId = null) => {
|
||||
|
||||
return Promise.all(reindexQueue)
|
||||
})
|
||||
.then(updatePromiseResults => {
|
||||
.then(rawSearchIndex => {
|
||||
|
||||
const created = Math.round((+new Date)/1000)
|
||||
const jsonSearchIndex = JSON.stringify(searchIndex)
|
||||
const encryptedJsonIndex = cs.encrypt(masterKey, searchIndexSalt, jsonSearchIndex)
|
||||
|
||||
db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
|
||||
return db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
|
||||
[encryptedJsonIndex, created, userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
@@ -411,7 +406,6 @@ Note.reindex = (userId, masterKey, removeId = null) => {
|
||||
.then((rows, fields) => {
|
||||
|
||||
// console.log('Indexd Note Count: ' + rows[0]['affectedRows'])
|
||||
// @TODO - Return number of reindexed notes
|
||||
resolve(true)
|
||||
|
||||
})
|
||||
@@ -448,10 +442,6 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(!rows[0] || !rows[0][0] || !rows[0][0]['note_raw_text_id']){
|
||||
return reject(false)
|
||||
}
|
||||
|
||||
const textId = rows[0][0]['note_raw_text_id']
|
||||
let salt = rows[0][0]['salt']
|
||||
let snippetSalt = rows[0][0]['snippet_salt']
|
||||
@@ -459,7 +449,8 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
|
||||
//Shared notes use encrypted key - decrypt key then decrypt note
|
||||
const encryptedShareKey = rows[0][0].encrypted_share_password_key
|
||||
if(encryptedShareKey != null){
|
||||
masterKey = crypto.privateDecrypt(userPrivateKey, Buffer.from(encryptedShareKey, 'base64') )
|
||||
masterKey = crypto.privateDecrypt(userPrivateKey,
|
||||
Buffer.from(encryptedShareKey, 'base64') )
|
||||
}
|
||||
|
||||
let encryptedNoteText = ''
|
||||
@@ -484,14 +475,10 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
|
||||
for (var i = 0; i < rows[0].length; i++) {
|
||||
const otherNote = rows[0][i]
|
||||
//Re-encrypt for other user
|
||||
let updatedSnippet = '' //Default to no snippet
|
||||
if(noteText.length > 500){
|
||||
updatedSnippet = cs.encrypt(masterKey, otherNote.snippet_salt, snippet)
|
||||
}
|
||||
const updatedSnippet = cs.encrypt(masterKey, otherNote.snippet_salt, snippet)
|
||||
db.promise().query('UPDATE note SET snippet = ? WHERE id = ?', [updatedSnippet, otherNote.id])
|
||||
.then((rows, fields) => {
|
||||
SocketIo.to(otherNote['user_id']).emit('new_note_text_saved', {'noteId':otherNote.id, hash})
|
||||
})
|
||||
|
||||
SocketIo.to(otherNote['user_id']).emit('new_note_text_saved', {'noteId':otherNote.id, hash})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -502,24 +489,21 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
|
||||
})
|
||||
.then( (rows, fields) => {
|
||||
|
||||
//Set openend time to a minute ago
|
||||
const theFuture = Math.round((+new Date)/1000) + 10
|
||||
|
||||
//Update other note attributes
|
||||
return db.promise()
|
||||
.query('UPDATE note SET pinned = ?, archived = ?, color = ?, snippet = ?, indexed = 0, opened = ? WHERE id = ? AND user_id = ? LIMIT 1',
|
||||
[pinned, archived, color, noteSnippet, theFuture, noteId, userId])
|
||||
.query('UPDATE note SET pinned = ?, archived = ?, color = ?, snippet = ?, indexed = 0 WHERE id = ? AND user_id = ? LIMIT 1',
|
||||
[pinned, archived, color, noteSnippet, noteId, userId])
|
||||
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(typeof SocketIo != 'undefined'){
|
||||
if(SocketIo){
|
||||
SocketIo.to(userId).emit('new_note_text_saved', {noteId, hash})
|
||||
|
||||
//Async attachment reindex
|
||||
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
|
||||
}
|
||||
|
||||
//Async attachment reindex
|
||||
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
|
||||
|
||||
//Send back updated response
|
||||
resolve(rows[0])
|
||||
})
|
||||
@@ -668,9 +652,60 @@ Note.delete = (userId, noteId, masterKey = null) => {
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Returns noteData
|
||||
//
|
||||
//text is the current text for the note that will be compared to the text in the database
|
||||
Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Note.get(userId, noteId)
|
||||
.then(noteObject => {
|
||||
|
||||
if(!noteObject.text || !usersCurrentText || noteObject.encrypted == 1){
|
||||
return resolve(null)
|
||||
}
|
||||
|
||||
let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
|
||||
let newText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
|
||||
|
||||
if(noteObject.updated == lastUpdated){
|
||||
// console.log('No note diff')
|
||||
return resolve(null)
|
||||
}
|
||||
|
||||
if(noteObject.updated > lastUpdated){
|
||||
newText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
|
||||
oldText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
|
||||
}
|
||||
|
||||
const dmp = new DiffMatchPatch.diff_match_patch()
|
||||
const diff = dmp.diff_main(oldText, newText)
|
||||
|
||||
dmp.diff_cleanupSemantic(diff)
|
||||
const patch_list = dmp.patch_make(oldText, newText, diff);
|
||||
const patch_text = dmp.patch_toText(patch_list);
|
||||
|
||||
//Patch text - shows a list of changes
|
||||
var patches = dmp.patch_fromText(patch_text);
|
||||
// console.log(patch_text)
|
||||
|
||||
//results[1] - contains diagnostic data for patch apply, its possible it can fail
|
||||
var results = dmp.patch_apply(patches, oldText);
|
||||
|
||||
//Compile return data for front end
|
||||
const returnData = {
|
||||
updatedText: results[0],
|
||||
diffs: results[1].length, //Only use length for now
|
||||
updated: Math.max(noteObject.updated,lastUpdated) //Return most recently updated date
|
||||
|
||||
}
|
||||
|
||||
//Final change in notes
|
||||
// console.log(returnData)
|
||||
|
||||
resolve(returnData)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
Note.get = (userId, noteId, masterKey) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -694,7 +729,6 @@ Note.get = (userId, noteId, masterKey) => {
|
||||
note_raw_text.text,
|
||||
note_raw_text.salt,
|
||||
note_raw_text.updated as updated,
|
||||
GROUP_CONCAT(DISTINCT(tag.text) ORDER BY tag.text DESC) AS tags,
|
||||
note.id,
|
||||
note.user_id,
|
||||
note.created,
|
||||
@@ -711,13 +745,12 @@ Note.get = (userId, noteId, masterKey) => {
|
||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
LEFT JOIN attachment ON (note.id = attachment.note_id)
|
||||
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
|
||||
LEFT JOIN note_tag ON (note.id = note_tag.note_id AND note_tag.user_id = ?)
|
||||
LEFT JOIN tag ON (note_tag.tag_id = tag.id)
|
||||
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, userId, noteId])
|
||||
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, noteId])
|
||||
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
const nowTime = Math.round((+new Date)/1000)
|
||||
let noteLockedOut = false
|
||||
let noteData = rows[0][0]
|
||||
// const rawTextId = noteData['rawTextId']
|
||||
@@ -743,15 +776,13 @@ Note.get = (userId, noteId, masterKey) => {
|
||||
noteData.title = textObject[0]
|
||||
noteData.text = textObject[1]
|
||||
|
||||
const nowTime = Math.round((+new Date)/1000)
|
||||
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
|
||||
.then(results => {
|
||||
//Return note data
|
||||
// delete noteData.salt //remove salt from return data
|
||||
// delete noteData.encrypted_share_password_key
|
||||
noteData.lockedOut = noteLockedOut
|
||||
resolve(noteData)
|
||||
})
|
||||
|
||||
//Return note data
|
||||
// delete noteData.salt //remove salt from return data
|
||||
// delete noteData.encrypted_share_password_key
|
||||
noteData.lockedOut = noteLockedOut
|
||||
resolve(noteData)
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -831,98 +862,39 @@ Note.encryptedIndexSearch = (userId, searchQuery, searchTags, masterKey) => {
|
||||
const decipheredSearchIndex = cs.decrypt(masterKey, row.salt, row.index)
|
||||
const searchIndex = JSON.parse(decipheredSearchIndex)
|
||||
|
||||
//Clean up search word, leave in spaces, split to array
|
||||
const words = searchQuery.toLowerCase().replace(/[^a-z0-9 ]/g, '').split(' ')
|
||||
//Clean up search word
|
||||
const word = searchQuery.toLowerCase().replace(/[^a-z0-9]/g, '')
|
||||
|
||||
let wordSearchCount = 0;
|
||||
|
||||
|
||||
let partialWords = [] //For debugging
|
||||
let exactWords = [] //For debugging
|
||||
|
||||
|
||||
let exactWordIdSets = []
|
||||
let partialMatchNoteIds = []
|
||||
|
||||
words.forEach(word => {
|
||||
|
||||
//Skip short words
|
||||
if(word.length <= 2){
|
||||
return
|
||||
let noteIds = []
|
||||
let partials = []
|
||||
Object.keys(searchIndex).forEach(wordIndex => {
|
||||
if( wordIndex.indexOf(word) != -1 && wordIndex != word){
|
||||
partials.push(wordIndex)
|
||||
noteIds.push(...searchIndex[wordIndex])
|
||||
}
|
||||
|
||||
//count all words being searched
|
||||
wordSearchCount++
|
||||
|
||||
//Save all exact match sets if found
|
||||
if(searchIndex[word]){
|
||||
// exactWords.push(word) //Words for debugging
|
||||
exactWordIdSets.push(...searchIndex[word])
|
||||
}
|
||||
|
||||
//Find all partial word matches in index
|
||||
Object.keys(searchIndex).forEach(wordIndex => {
|
||||
if( wordIndex.indexOf(word) != -1 && wordIndex != word){
|
||||
// partialWords.push(wordIndex) //partialWords for debugging
|
||||
partialMatchNoteIds.push(...searchIndex[wordIndex])
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
//If more than one work was searched, remove notes that don't contain both
|
||||
if(words.length > 1 && exactWordIdSets.length > 0){
|
||||
const exactArray = searchIndex[word] ? searchIndex[word] : []
|
||||
|
||||
//Find ids that appear more than once, this means there was an exact match in more than one note
|
||||
let overlappingIds = exactWordIdSets.filter((e, i, a) => a.indexOf(e) !== i)
|
||||
overlappingIds = [...new Set(overlappingIds)]
|
||||
|
||||
//If there are notes that appear
|
||||
if(overlappingIds.length > 0){
|
||||
exactWordIdSets = overlappingIds
|
||||
}
|
||||
|
||||
//If note appears in partial and exact, show only that set
|
||||
const partialIntersect = exactWordIdSets.filter(x => partialMatchNoteIds.includes(x))
|
||||
if(partialIntersect.length > 0){
|
||||
exactWordIdSets = partialIntersect
|
||||
partialMatchNoteIds = []
|
||||
}
|
||||
let searchData = {
|
||||
'word':word,
|
||||
'exact': exactArray,
|
||||
'partials': partials,
|
||||
'partial': [...new Set(noteIds) ],
|
||||
}
|
||||
|
||||
//Remove duplicates from final id sets
|
||||
let finalExact = [ ...new Set(exactWordIdSets) ]
|
||||
let finalPartial = [ ...new Set(partialMatchNoteIds) ]
|
||||
|
||||
//Remove exact matches from partials set if there is overlap
|
||||
if(finalExact.length > 0 && finalPartial.length > 0){
|
||||
finalPartial = finalPartial
|
||||
.filter( ( el ) => !finalExact.includes( el ) )
|
||||
if(searchData['exact'].length > 0 && searchData['partial'].length > 0){
|
||||
searchData['partial'] = searchData['partial']
|
||||
.filter( ( el ) => !searchData['exact'].includes( el ) )
|
||||
}
|
||||
|
||||
//Combine the two filtered sets
|
||||
let finalIdSearchSet = finalExact.concat(finalPartial)
|
||||
searchData['ids'] = searchData['exact'].concat(searchData['partial'])
|
||||
searchData['total'] = searchData['ids'].length
|
||||
|
||||
// let searchData = {
|
||||
// 'query':searchQuery,
|
||||
// 'words_count': words.length,
|
||||
// 'exact_matches': exactWordIdSets.length,
|
||||
// 'word_search_count': wordSearchCount,
|
||||
// 'exactWords': exactWords,
|
||||
// 'exact': finalExact,
|
||||
// 'partialWords': partialWords,
|
||||
// 'partial': finalPartial,
|
||||
// }
|
||||
// console.log(searchData['total'])
|
||||
|
||||
// //Lump all found note ids into one array
|
||||
// searchData['ids'] = finalIdSearchSet
|
||||
// searchData['total'] = searchData['ids'].length
|
||||
|
||||
// console.log('-----------------')
|
||||
// console.log(searchData)
|
||||
// console.log('-----------------')
|
||||
|
||||
return resolve({ 'ids':finalIdSearchSet })
|
||||
return resolve({ 'ids':searchData['ids'] })
|
||||
|
||||
|
||||
} else {
|
||||
@@ -993,8 +965,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
||||
GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs,
|
||||
shareUser.username as shareUsername,
|
||||
note.shared,
|
||||
note.encrypted_share_password_key,
|
||||
note.indexed
|
||||
note.encrypted_share_password_key
|
||||
FROM note
|
||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
LEFT JOIN note_tag ON (note.id = note_tag.note_id)
|
||||
@@ -1002,7 +973,6 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
||||
LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.visible = 1)
|
||||
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
|
||||
WHERE note.user_id = ?
|
||||
AND note.quick_note <= 1
|
||||
`
|
||||
|
||||
//If text search returned results, limit search to those ids
|
||||
@@ -1152,7 +1122,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
console.log('Error opening note id -> '+note.id+' for userId -> '+userId)
|
||||
console.log('Error opening note id -> ', note.id)
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
|
@@ -13,14 +13,11 @@ QuickNote.get = (userId, masterKey) => {
|
||||
SELECT note.id FROM note WHERE quick_note = 1 AND user_id = ? LIMIT 1`, [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Quick Note is set, return note object
|
||||
//Quick Note is set, return note text
|
||||
if(rows[0][0] != undefined){
|
||||
|
||||
let noteId = rows[0][0].id
|
||||
const note = Note.get(userId, noteId, masterKey)
|
||||
.then(noteData => {
|
||||
return resolve(noteData)
|
||||
})
|
||||
return resolve({'noteId':noteId})
|
||||
|
||||
} else {
|
||||
//Or create a new note and get the id
|
||||
@@ -84,7 +81,7 @@ QuickNote.update = (userId, pushText, masterKey) => {
|
||||
.replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities
|
||||
.replace(/<[^>]+>/g, '') //Rip out all HTML tags
|
||||
|
||||
//Turn links into actual link
|
||||
//Turn links into actual linx
|
||||
clean = QuickNote.makeUrlLink(clean)
|
||||
|
||||
if(clean == ''){ clean = ' ' }
|
||||
@@ -117,7 +114,7 @@ QuickNote.update = (userId, pushText, masterKey) => {
|
||||
}
|
||||
})
|
||||
.then( saveResults => {
|
||||
return resolve(saveResults)
|
||||
return resolve(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
@@ -138,33 +138,6 @@ Tag.get = (userId, noteId) => {
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Get just tag string for note
|
||||
//
|
||||
Tag.fornote = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
|
||||
db.promise()
|
||||
.query(`SELECT GROUP_CONCAT(DISTINCT(tag.text) ORDER BY tag.text DESC) AS tags
|
||||
FROM note_tag
|
||||
LEFT JOIN tag ON (note_tag.tag_id = tag.id)
|
||||
WHERE note_tag.note_id = ?
|
||||
AND user_id = ?;
|
||||
`, [noteId,userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//pull IDs out of returned results
|
||||
// let ids = rows[0].map( item => {})
|
||||
|
||||
resolve( rows[0][0] ) //Return all tags found by query
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Get all tags for a note and concatinate into a string 'all, tags, like, this'
|
||||
//
|
||||
|
@@ -9,8 +9,7 @@ const speakeasy = require('speakeasy')
|
||||
|
||||
let User = module.exports = {}
|
||||
|
||||
const version = '3.8.0'
|
||||
// 3.7.3 - diff/patch update
|
||||
const version = '3.1.5'
|
||||
|
||||
//Login a user, if that user does not exist create them
|
||||
//Issues login token
|
||||
@@ -194,19 +193,17 @@ 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 = {}
|
||||
const userHash = cs.hash(String(userId)).toString('base64')
|
||||
|
||||
db.promise().query(
|
||||
`SELECT
|
||||
SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes,
|
||||
SUM(trashed = 1) AS trashedNotes,
|
||||
SUM(share_user_id IS NULL && trashed = 0 AND quick_note < 2) AS totalNotes,
|
||||
SUM(share_user_id IS NULL && trashed = 0) AS totalNotes,
|
||||
SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount,
|
||||
SUM(share_user_id != ? && trashed = 0) AS sharedToNotes
|
||||
FROM note
|
||||
@@ -247,71 +244,15 @@ User.getCounts = (userId, extendedOptions) => {
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
})
|
||||
@@ -553,17 +494,10 @@ User.revokeActiveSessions = (userId, sessionId) => {
|
||||
|
||||
User.deleteUser = (userId, password) => {
|
||||
|
||||
if(!userId || !password){
|
||||
return new Promise((resolve, reject) => {
|
||||
return resolve('Missing User ID or Password. No Action Taken.')
|
||||
})
|
||||
}
|
||||
|
||||
//Verify user is correct by decryptig master key with password
|
||||
|
||||
let deletePromises = []
|
||||
|
||||
//Delete all notes and raw text
|
||||
let noteDelete = db.promise().query(`
|
||||
DELETE note, note_raw_text
|
||||
FROM note
|
||||
@@ -572,14 +506,12 @@ User.deleteUser = (userId, password) => {
|
||||
`,[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']
|
||||
let tables = ['user_key', 'user_encrypted_search_index', 'attachment']
|
||||
tables.forEach(tableName => {
|
||||
|
||||
const query = `DELETE FROM ${tableName} WHERE user_id = ?`
|
||||
@@ -587,7 +519,79 @@ User.deleteUser = (userId, password) => {
|
||||
deletePromises.push(deleteQuery)
|
||||
})
|
||||
|
||||
//Remove all note attachments and files
|
||||
|
||||
return Promise.all(deletePromises)
|
||||
}
|
||||
|
||||
User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let masterKey = null
|
||||
let testUserId = null
|
||||
|
||||
|
||||
const randomUsername = Math.random().toString(36).substring(2, 15);
|
||||
const randomPassword = '1'
|
||||
const secondPassword = '2'
|
||||
|
||||
User.register(testUserName, password)
|
||||
.then( ({ token, userId }) => {
|
||||
testUserId = userId
|
||||
|
||||
if(printResults) console.log('Test: Register User '+testUserName+' - Pass')
|
||||
|
||||
return User.getMasterKey(testUserId, password)
|
||||
})
|
||||
.then(newMasterKey => {
|
||||
masterKey = newMasterKey
|
||||
|
||||
if(printResults) console.log('Test: Generate/Decrypt Master Key - Pass')
|
||||
|
||||
return User.generateKeypair(testUserId, masterKey)
|
||||
})
|
||||
.then(({publicKey, privateKey}) => {
|
||||
|
||||
const publicKeyMessage = 'Test: Public key decrypt - Pass'
|
||||
const privateKeyMessage = 'Test: Private key decrypt - Pass'
|
||||
|
||||
//Encrypt Message with private Key
|
||||
const privateKeyEncrypted = crypto.privateEncrypt(privateKey, Buffer.from(privateKeyMessage, 'utf8')).toString('base64')
|
||||
const decryptedPrivate = crypto.publicDecrypt(publicKey, Buffer.from(privateKeyEncrypted, 'base64'))
|
||||
//Conver back to a string
|
||||
if(printResults) console.log(decryptedPrivate.toString('utf8'))
|
||||
|
||||
//Encrypt with public key
|
||||
const pubEncrMsc = crypto.publicEncrypt(publicKey, Buffer.from(publicKeyMessage, 'utf8')).toString('base64')
|
||||
const publicDeccryptMessage = crypto.privateDecrypt(privateKey, Buffer.from(pubEncrMsc, 'base64') )
|
||||
//Convert it back to string
|
||||
if(printResults) console.log(publicDeccryptMessage.toString('utf8'))
|
||||
|
||||
return User.login(testUserName, password)
|
||||
})
|
||||
.then( ({token, userId}) => {
|
||||
|
||||
if(printResults) console.log('Test: Login New User - Pass')
|
||||
|
||||
return User.changePassword(testUserId, randomPassword, secondPassword)
|
||||
|
||||
})
|
||||
.then(passwordChangeResults => {
|
||||
|
||||
if(printResults) console.log('Test: Password Change - ', passwordChangeResults?'Pass':'Fail')
|
||||
|
||||
return User.login(testUserName, secondPassword)
|
||||
|
||||
})
|
||||
.then(reLogin => {
|
||||
|
||||
if(printResults) console.log('Test: Login With new Password - Pass')
|
||||
|
||||
return User.getMasterKey(testUserId, secondPassword)
|
||||
})
|
||||
.then(newMasterKey => {
|
||||
|
||||
masterKey = newMasterKey
|
||||
|
||||
resolve({testUserId, masterKey})
|
||||
})
|
||||
})
|
||||
}
|
@@ -6,27 +6,20 @@ let router = express.Router()
|
||||
|
||||
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 +28,11 @@ router.post('/textsearch', function (req, res) {
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
router.post('/get', function (req, res) {
|
||||
Attachment.forNote(userId, req.body.noteId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
router.post('/update', function (req, res) {
|
||||
Attachment.update(userId, req.body.attachmentId, req.body.updatedText, req.body.noteId)
|
||||
.then( result => {
|
||||
@@ -60,26 +58,5 @@ router.post('/upload', upload.single('file'), function (req, res, next) {
|
||||
|
||||
})
|
||||
|
||||
//
|
||||
// Push URL to attachments
|
||||
// push action on - public controller
|
||||
//
|
||||
|
||||
// get push key
|
||||
router.post('/getbookmarklet', function (req, res) {
|
||||
|
||||
Attachment.getPushkeyBookmarklet(userId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
// generate new push key
|
||||
router.post('/generatepushkey', function (req, res) {
|
||||
|
||||
})
|
||||
|
||||
// delete push key
|
||||
router.post('/deletepushkey', function (req, res) {
|
||||
|
||||
})
|
||||
|
||||
module.exports = router
|
@@ -1,45 +0,0 @@
|
||||
//
|
||||
// /api/metric-tracking
|
||||
//
|
||||
|
||||
var express = require('express')
|
||||
var router = express.Router()
|
||||
|
||||
let MetricTracking = require('@models/MetricTracking');
|
||||
|
||||
let userId = null
|
||||
let masterKey = null
|
||||
|
||||
// middleware that is specific to this router
|
||||
router.use(function setUserId (req, res, next) {
|
||||
|
||||
//Session key is required to continue
|
||||
if(!req.headers.sessionId){
|
||||
next('Unauthorized')
|
||||
}
|
||||
|
||||
if(req.headers.userId){
|
||||
userId = req.headers.userId
|
||||
masterKey = req.headers.masterKey
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/get', function (req, res) {
|
||||
MetricTracking.get(userId, masterKey)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
router.post('/create', function (req, res) {
|
||||
MetricTracking.create(userId, masterKey)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
//Push text to quick note
|
||||
router.post('/save', function (req, res) {
|
||||
MetricTracking.save(userId, req.body.cycleData, masterKey)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
|
||||
module.exports = router
|
@@ -10,17 +10,12 @@ let masterKey = null
|
||||
|
||||
// middleware that is specific to this router
|
||||
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()
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
//
|
||||
@@ -60,6 +55,14 @@ router.post('/search', function (req, res) {
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/difftext', function (req, res) {
|
||||
Note.getDiffText(userId, req.body.noteId, req.body.text, req.body.updated)
|
||||
.then( fullDiffText => {
|
||||
//Response should be full diff text
|
||||
res.send(fullDiffText)
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/reindex', function (req, res) {
|
||||
Note.reindex(userId, masterKey)
|
||||
.then( data => {
|
||||
|
@@ -1,85 +1,18 @@
|
||||
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 Note = require('@models/Note')
|
||||
|
||||
//
|
||||
// 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) {
|
||||
router.post('/opensharednote', function (req, res) {
|
||||
|
||||
Note.getShared(req.body.noteId, req.body.sharedKey)
|
||||
.then(results => res.send(results))
|
||||
})
|
||||
|
||||
//
|
||||
// Login User
|
||||
//
|
||||
const loginLimiter = rateLimit({
|
||||
windowMs: 30 * 60 * 1000, // 30 min window
|
||||
max: 25, // start blocking after 25 requests
|
||||
message:'Please try to login again later'
|
||||
})
|
||||
router.post('/login', loginLimiter, function (req, res) {
|
||||
|
||||
User.login(req.body.username, req.body.password, req.body.authToken)
|
||||
.then( returnData => {
|
||||
|
||||
res.send(returnData)
|
||||
})
|
||||
})
|
||||
|
||||
//
|
||||
// Register User
|
||||
//
|
||||
const registerLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour window
|
||||
max: 5, // start blocking after 5 requests
|
||||
message:'Please try again to create an acount in an hour'
|
||||
})
|
||||
router.post('/register', registerLimiter, function (req, res) {
|
||||
|
||||
User.register(req.body.username, req.body.password)
|
||||
.then( returnData => {
|
||||
|
||||
res.send(returnData)
|
||||
})
|
||||
})
|
||||
|
||||
//
|
||||
// Public Pushme Action
|
||||
//
|
||||
const pushMeLimiter = rateLimit({
|
||||
windowMs: 30 * 60 * 1000, //30 min window
|
||||
max: 50, // start blocking after x requests
|
||||
message:'Error'
|
||||
})
|
||||
router.get('/pushmebaby', pushMeLimiter, function (req, res) {
|
||||
|
||||
|
||||
Attachment.pushUrl(req.query.pushkey, req.query.url)
|
||||
.then((() => {
|
||||
const jsCode = `
|
||||
<script>
|
||||
window.close();
|
||||
</script>
|
||||
<h1>Posting URL</h1>
|
||||
`;
|
||||
res.header('Content-Security-Policy', "script-src 'unsafe-inline'");
|
||||
res.set('Content-Type', 'text/html');
|
||||
res.send(Buffer.from(jsCode));
|
||||
}))
|
||||
})
|
||||
|
||||
module.exports = router
|
@@ -6,19 +6,13 @@ 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){
|
||||
userId = req.headers.userId
|
||||
masterKey = req.headers.masterKey
|
||||
next()
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
//Get quick note text
|
||||
|
@@ -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)
|
||||
|
@@ -5,24 +5,20 @@ const User = require('@models/User')
|
||||
const Auth = require('@helpers/Auth')
|
||||
const cs = require('@helpers/CryptoString')
|
||||
|
||||
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.use(function timeLog (req, res, next) {
|
||||
// console.log('Time: ', Date.now())
|
||||
next()
|
||||
})
|
||||
|
||||
// Login User
|
||||
router.post('/login', function (req, res) {
|
||||
|
||||
User.login(req.body.username, req.body.password, req.body.authToken)
|
||||
.then( returnData => {
|
||||
res.send(returnData)
|
||||
})
|
||||
})
|
||||
// Logout User
|
||||
router.post('/logout', function (req, res) {
|
||||
|
||||
@@ -32,6 +28,19 @@ router.post('/logout', function (req, res) {
|
||||
})
|
||||
})
|
||||
|
||||
// Register User
|
||||
router.post('/register', function (req, res) {
|
||||
|
||||
User.register(req.body.username, req.body.password)
|
||||
.then( returnData => {
|
||||
|
||||
res.send(returnData)
|
||||
})
|
||||
.catch(e => {
|
||||
res.send(false)
|
||||
})
|
||||
})
|
||||
|
||||
// change password
|
||||
router.post('/changepassword', function (req, res) {
|
||||
|
||||
@@ -53,7 +62,7 @@ router.post('/revokesessions', function(req, res) {
|
||||
|
||||
// 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 ))
|
||||
})
|
||||
|
||||
|
@@ -1,100 +0,0 @@
|
||||
const Attachment = require('../../models/Attachment')
|
||||
const User = require('../../models/User')
|
||||
|
||||
const testUserName = 'jestTestUserAttachment'
|
||||
const password = 'Beans19934!!!'
|
||||
|
||||
let newUserId = null
|
||||
let masterKey = null
|
||||
let newPushKey = null
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
// Find and Delete Previous Test user, log in, get key
|
||||
return User.getByUserName(testUserName)
|
||||
.then((user) => {
|
||||
return User.deleteUser(user?.id, password)
|
||||
})
|
||||
.then((results) => {
|
||||
|
||||
return User.register(testUserName, password)
|
||||
})
|
||||
.then(({ token, userId }) => {
|
||||
newUserId = userId
|
||||
|
||||
return User.getMasterKey(userId, password)
|
||||
})
|
||||
.then((newMasterKey) => {
|
||||
masterKey = newMasterKey
|
||||
|
||||
return true
|
||||
})
|
||||
.catch(((error) => {
|
||||
console.log(error)
|
||||
}))
|
||||
|
||||
})
|
||||
|
||||
|
||||
test('Test Generate Push Key', () => {
|
||||
|
||||
return Attachment.generatePushKey(newUserId)
|
||||
.then( (pushKey) => {
|
||||
newPushKey = pushKey
|
||||
return Attachment.generatePushKey(newUserId)
|
||||
})
|
||||
.then( (pushKey) => {
|
||||
// expect a long, defined pushkey
|
||||
expect(pushKey).toBeDefined()
|
||||
expect(pushKey?.length).toBeGreaterThan(20)
|
||||
expect(pushKey).toMatch(newPushKey)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
test('Test get Push Key Bookmarklet', () => {
|
||||
|
||||
return Attachment.getPushkeyBookmarklet(newUserId)
|
||||
.then(( bookmarklet => {
|
||||
// Expect a bookmarklet containting URL encoded pushkey from above
|
||||
const keyCheck = bookmarklet.includes(encodeURIComponent(newPushKey))
|
||||
|
||||
expect(bookmarklet).toBeDefined()
|
||||
expect(keyCheck).toBe(true)
|
||||
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
test('Test Push URL', () => {
|
||||
|
||||
let url = 'https://www.solidscribe.com'
|
||||
|
||||
return Attachment.pushUrl(newPushKey, url)
|
||||
.then(( results => {
|
||||
|
||||
return Attachment.textSearch(newUserId, 'scribe')
|
||||
|
||||
}))
|
||||
.then((results) => {
|
||||
|
||||
expect(results.length == 1).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test('Test Delete Push Key', () => {
|
||||
|
||||
return Attachment.deletePushKey(newUserId)
|
||||
.then(( results => {
|
||||
// Expect a true bool
|
||||
expect(results).toBe(true)
|
||||
}))
|
||||
})
|
||||
|
||||
afterAll(done => {
|
||||
// Close Database
|
||||
const db = require('../../config/database')
|
||||
db.end()
|
||||
done()
|
||||
})
|
@@ -1,117 +0,0 @@
|
||||
const Note = require('../../models/Note')
|
||||
const User = require('../../models/User')
|
||||
|
||||
const testUserName = 'jestTestUserNote'
|
||||
const password = 'Beans1234!!!'
|
||||
const secondPassword = 'Rice1234!!!'
|
||||
|
||||
let newUserId = null
|
||||
let masterKey = null
|
||||
|
||||
let testNoteId = 0
|
||||
let testNoteId2 = 0
|
||||
|
||||
|
||||
const searchWord1 = 'beans'
|
||||
const searchWord2 = 'RICE'
|
||||
const updatedNoteText = 'Some Note Text for Testing more '+searchWord2+' is nice'
|
||||
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
// Find and Delete Previous Test user, log in, get key
|
||||
return User.getByUserName(testUserName)
|
||||
.then((user) => {
|
||||
return User.deleteUser(user?.id, password)
|
||||
})
|
||||
.then((results) => {
|
||||
|
||||
return User.register(testUserName, password)
|
||||
})
|
||||
.then(({ token, userId }) => {
|
||||
newUserId = userId
|
||||
|
||||
return User.getMasterKey(userId, password)
|
||||
})
|
||||
.then((newMasterKey) => {
|
||||
masterKey = newMasterKey
|
||||
|
||||
return true
|
||||
})
|
||||
.catch(((error) => {
|
||||
console.log(error)
|
||||
}))
|
||||
|
||||
})
|
||||
|
||||
test('Create Note', () => {
|
||||
const noteTitle = 'Test Note'
|
||||
const noteText = 'Some Note Text for Testing'
|
||||
|
||||
return Note.create(newUserId, noteTitle, noteText, masterKey)
|
||||
.then((noteId) => {
|
||||
testNoteId = noteId
|
||||
expect(noteId).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('Create Another Note', () => {
|
||||
const noteTitle = 'Test Note2'
|
||||
const noteText = 'Some Note Text for Testing more '+searchWord1
|
||||
|
||||
return Note.create(newUserId, noteTitle, noteText, masterKey)
|
||||
.then((noteId) => {
|
||||
testNoteId2 = noteId
|
||||
expect(noteId).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('Update a note', () => {
|
||||
|
||||
return Note.update(newUserId, testNoteId, updatedNoteText, 'title', 0, 0, 0, 'hash', masterKey)
|
||||
.then((results) => {
|
||||
expect(results.changedRows).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
test('Decrypt a note', () => {
|
||||
|
||||
return Note.get(newUserId, testNoteId, masterKey)
|
||||
.then((noteData) => {
|
||||
expect(noteData.text).toMatch(updatedNoteText)
|
||||
})
|
||||
})
|
||||
|
||||
test('Update note search index', () => {
|
||||
return Note.reindex(newUserId, masterKey)
|
||||
.then((results) => {
|
||||
expect(results).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test('Search Encrypted Index', () => {
|
||||
const searchString = `${searchWord1} ${searchWord2}`
|
||||
|
||||
return Note.encryptedIndexSearch(newUserId, searchString, null, masterKey)
|
||||
.then(({ids}) => {
|
||||
// Make sure beans is in one note and rice is in updated text
|
||||
expect(ids.length).toEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
test('Search Encrypted Index no results', () => {
|
||||
|
||||
return Note.encryptedIndexSearch(newUserId, 'zzz', null, masterKey)
|
||||
.then(({ids}) => {
|
||||
// Make sure beans is in one note and rice is in updated text
|
||||
expect(ids.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
afterAll(done => {
|
||||
// Close Database
|
||||
const db = require('../../config/database')
|
||||
db.end()
|
||||
done()
|
||||
})
|
@@ -1,67 +0,0 @@
|
||||
const Note = require('../../models/Note')
|
||||
const User = require('../../models/User')
|
||||
const ShareNote = require('../../models/ShareNote')
|
||||
|
||||
const testUserName = 'jestTestUserNote'
|
||||
const password = 'Beans1234!!!'
|
||||
let newUserId = null
|
||||
let masterKey = null
|
||||
|
||||
const testUserName2 = 'jestTestUserDude'
|
||||
const password2 = 'Rice1234!!!'
|
||||
let newUserId2 = null
|
||||
let masterKey2 = null
|
||||
|
||||
|
||||
let testNoteId = 0
|
||||
let testNoteId2 = 0
|
||||
// let sharedNoteId = 0 //ID of note shared with user
|
||||
const shareUserId = 61
|
||||
const searchWord1 = 'beans'
|
||||
const searchWord2 = 'RICE'
|
||||
const updatedNoteText = 'Some Note Text for Testing more '+searchWord2+' is nice'
|
||||
|
||||
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
// Find and Delete Previous Test user, log in, get key
|
||||
return
|
||||
User.getByUserName(testUserName)
|
||||
.then(user => {
|
||||
User.deleteUser(user?.id, password)
|
||||
})
|
||||
.then(user => {
|
||||
User.getByUserName(testUserName2)
|
||||
})
|
||||
.then(user => {
|
||||
User.deleteUser(user?.id, password)
|
||||
})
|
||||
.then((results) => {
|
||||
|
||||
return User.register(testUserName, password)
|
||||
})
|
||||
.then(({ token, userId }) => {
|
||||
newUserId = userId
|
||||
|
||||
return User.getMasterKey(userId, password)
|
||||
})
|
||||
.then((newMasterKey) => {
|
||||
masterKey = newMasterKey
|
||||
|
||||
return true
|
||||
})
|
||||
.catch(((error) => {
|
||||
console.log(error)
|
||||
}))
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
afterAll(done => {
|
||||
// Close Database
|
||||
const db = require('../../config/database')
|
||||
db.end()
|
||||
done()
|
||||
})
|
@@ -1,112 +0,0 @@
|
||||
const User = require('../../models/User')
|
||||
const crypto = require('crypto')
|
||||
|
||||
const testUserName = 'jestTestUser'
|
||||
const password = 'Beans1234!!!'
|
||||
const secondPassword = 'Rice1234!!!'
|
||||
|
||||
let testUserId = null
|
||||
let masterKey = null
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
// Find and Delete Previous Test user
|
||||
return User.getByUserName(testUserName)
|
||||
.then((user) => {
|
||||
return User.deleteUser(user?.id, password)
|
||||
})
|
||||
.then((results) => {
|
||||
|
||||
return results
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
test('Test User Registration', () => {
|
||||
|
||||
return User.register(testUserName, password)
|
||||
.then((({ token, userId }) => {
|
||||
|
||||
testUserId = userId
|
||||
|
||||
expect(token).toBeDefined()
|
||||
expect(userId).toBeGreaterThan(0)
|
||||
}))
|
||||
})
|
||||
|
||||
test('Test decrypting user masterKey', () => {
|
||||
|
||||
return User.getMasterKey(testUserId, password)
|
||||
.then((newMasterKey) => {
|
||||
masterKey = newMasterKey
|
||||
|
||||
expect(masterKey).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
test('Test generating public and private key pair', () => {
|
||||
|
||||
return User.generateKeypair(testUserId, masterKey)
|
||||
.then(({publicKey, privateKey}) => {
|
||||
|
||||
const publicKeyMessage = 'Test: Public key decrypt - Pass'
|
||||
const privateKeyMessage = 'Test: Private key decrypt - Pass'
|
||||
|
||||
//Encrypt Message with private Key
|
||||
const privateKeyEncrypted = crypto.privateEncrypt(privateKey, Buffer.from(privateKeyMessage, 'utf8')).toString('base64')
|
||||
const decryptedPrivate = crypto.publicDecrypt(publicKey, Buffer.from(privateKeyEncrypted, 'base64'))
|
||||
//Conver back to a string
|
||||
expect(decryptedPrivate.toString('utf8')).toMatch(privateKeyMessage)
|
||||
|
||||
//Encrypt with public key
|
||||
const pubEncrMsc = crypto.publicEncrypt(publicKey, Buffer.from(publicKeyMessage, 'utf8')).toString('base64')
|
||||
const publicDeccryptMessage = crypto.privateDecrypt(privateKey, Buffer.from(pubEncrMsc, 'base64') )
|
||||
//Convert it back to string
|
||||
expect(publicDeccryptMessage.toString('utf8')).toMatch(publicKeyMessage)
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
test('Test Logging in User', () => {
|
||||
|
||||
return User.login(testUserName, password)
|
||||
.then(({token, userId}) => {
|
||||
expect(token).toBeDefined()
|
||||
expect(userId).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('Test Changing Password', () => {
|
||||
return User.changePassword(testUserId, password, secondPassword)
|
||||
.then((passwordChangeResults) => {
|
||||
|
||||
expect(passwordChangeResults).toBe(true)
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
test('Test Login with wrong password', () => {
|
||||
|
||||
return User.login(testUserName, password)
|
||||
.then(({token, userId}) => {
|
||||
|
||||
expect(token).toBeNull()
|
||||
expect(userId).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
test('Test decrypting masterKey with new Password', () => {
|
||||
return User.getMasterKey(testUserId, secondPassword)
|
||||
.then((newMasterKey) => {
|
||||
|
||||
expect(newMasterKey).toBeDefined()
|
||||
expect(newMasterKey.length).toBe(28)
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(done => {
|
||||
// Close Database
|
||||
const db = require('../../config/database')
|
||||
db.end()
|
||||
done()
|
||||
})
|
@@ -1,11 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /home/mab/ss
|
||||
echo 'Make sure this is being run from root folder of project'
|
||||
|
||||
echo '::--:: Starting dev server. cd client; npm run serve -> 192.168.1.164:8081'
|
||||
screen -dmS "NoteClientScreen" bash -c "cd /home/mab/ss/client; npm run serve -- --port 8081 --https true"
|
||||
echo 'Starting Client webpack dev server (/app), in a screen, watching for file changes...'
|
||||
screen -dm bash -c "cd client/; npm run watch"
|
||||
|
||||
echo '::--:: Starting API server (/api), watching for file changes...'
|
||||
cd /home/mab/ss/server
|
||||
pm2 flush
|
||||
echo 'Starting API server (/api), watching for file changes...'
|
||||
cd server
|
||||
pm2 start ecosystem.config.js
|
||||
|
4
staticFiles/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*
|
||||
*/
|
||||
!.gitignore
|
||||
!assets
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" id="svg8" version="1.1" viewBox="0 0 132.29166 132.29167" height="500" width="500">
|
||||
<defs id="defs2"/>
|
||||
<metadata id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g style="display:inline" transform="translate(0,-164.70832)" id="layer1">
|
||||
<path id="path3813-4" d="m 56.22733,165.36641 -55.56249926,15.875 8e-7,63.5 47.62499846,11.90625 v 27.78125 l -47.76066333,-13.9757 0.13566407,10.00695 55.56249926,15.875 v -47.625 l -47.6249985,-11.90625 -8e-7,-47.625 47.7606633,-13.94121 c 0.135664,-2.30629 -0.135664,-9.87129 -0.135664,-9.87129 z" style="fill:#0f7425;fill-opacity:1;stroke:none;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;shape-rendering:crispedges"/>
|
||||
<path id="path4563" d="m 20.508581,220.92891 c 15.265814,-14.23899 27.809717,-7.68002 39.687499,3.96875 v -7.9375 C 51.75093,200.8366 37.512584,206.01499 20.508581,205.05391 Z" style="fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;shape-rendering:crispedges"/>
|
||||
<path id="path4563-6" d="m 111.78985,220.92891 c -15.265834,-14.23899 -27.809737,-7.68002 -39.68752,3.96875 v -7.9375 c 8.445151,-16.12356 22.683497,-10.94517 39.68752,-11.90625 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;shape-rendering:crispedges"/>
|
||||
<path id="path3813-4-2" d="m 76.07108,165.36641 55.5625,15.875 v 63.5 l -47.625,11.90625 v 27.78125 l 47.76067,-13.9757 -0.13567,10.00695 -55.5625,15.875 v -47.625 l 47.625,-11.90626 V 189.17891 L 75.93542,175.2377 c -0.13567,-2.30629 0.13566,-9.87129 0.13566,-9.87129 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;shape-rendering:crispedges"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"theme_color":"#000",
|
||||
"background_color": "#000",
|
||||
"description": "Take Notes",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/api/static/assets/logo.png",
|
||||
"sizes": "496x496",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/api/static/assets/maskable_icon.png",
|
||||
"sizes": "826x826",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"name": "Solid Scribe",
|
||||
"short_name": "Solid Scribe",
|
||||
"start_url": "/#/notes",
|
||||
"author":"Max"
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"background_color": "purple",
|
||||
"description": "Take Notes",
|
||||
"display": "fullscreen",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/api/static/assets/favicon.ico",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"name": "Notes",
|
||||
"short_name": "Notes",
|
||||
"start_url": "/#/notes"
|
||||
}
|
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" id="svg8" version="1.1" viewBox="0 0 132.29166 132.29167" height="500" width="500">
|
||||
<defs id="defs2"/>
|
||||
<metadata id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g style="display:inline" transform="translate(0,-164.70832)" id="layer1">
|
||||
<path id="path3813-4" d="m 56.22733,165.36641 -55.56249926,15.875 8e-7,63.5 47.62499846,11.90625 v 27.78125 l -47.76066333,-13.9757 0.13566407,10.00695 55.56249926,15.875 v -47.625 l -47.6249985,-11.90625 -8e-7,-47.625 47.7606633,-13.94121 c 0.135664,-2.30629 -0.135664,-9.87129 -0.135664,-9.87129 z" style="fill:#0f7425;fill-opacity:1;stroke:none;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
|
||||
<path id="path4563" d="m 20.508581,220.92891 c 15.265814,-14.23899 27.809717,-7.68002 39.687499,3.96875 v -7.9375 C 51.75093,200.8366 37.512584,206.01499 20.508581,205.05391 Z" style="fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
|
||||
<path id="path4563-6" d="m 111.78985,220.92891 c -15.265834,-14.23899 -27.809737,-7.68002 -39.68752,3.96875 v -7.9375 c 8.445151,-16.12356 22.683497,-10.94517 39.68752,-11.90625 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
|
||||
<path id="path3813-4-2" d="m 76.07108,165.36641 55.5625,15.875 v 63.5 l -47.625,11.90625 v 27.78125 l 47.76067,-13.9757 -0.13567,10.00695 -55.5625,15.875 v -47.625 l 47.625,-11.90626 V 189.17891 L 75.93542,175.2377 c -0.13567,-2.30629 0.13566,-9.87129 0.13566,-9.87129 z" style="display:inline;fill:#04cb03;fill-opacity:1;stroke:none;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"theme_color":"#000",
|
||||
"background_color": "#000",
|
||||
"description": "Take Notes",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/api/static/assets/logo.png",
|
||||
"sizes": "496x496",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/api/static/assets/maskable_icon.png",
|
||||
"sizes": "826x826",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"name": "Solid Scribe",
|
||||
"short_name": "Solid Scribe",
|
||||
"start_url": "/#/notes",
|
||||
"author":"Max"
|
||||
}
|
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 1.6 KiB |