Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d891ea734 | ||
|
|
3447b2e0e6 | ||
|
|
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 |
No files matched your search
@@ -7,9 +7,3 @@ pids
|
||||
*.seed
|
||||
*.pid.lock
|
||||
.env
|
||||
|
||||
# exclude everything
|
||||
staticFiles/*
|
||||
|
||||
# exception to the rule
|
||||
!staticFiles/assets/
|
||||
@@ -1,62 +0,0 @@
|
||||
echo '-------'
|
||||
echo 'Starting Database Restore'
|
||||
echo '-------'
|
||||
|
||||
#get Latest database backup
|
||||
|
||||
# Unzip File
|
||||
# gzip -dk file.gz
|
||||
|
||||
source ~/.env
|
||||
|
||||
echo "$PROD_DB_PASS"
|
||||
|
||||
BACKUPDIR="/home/mab/databaseBackupSolidScribe"
|
||||
|
||||
mkdir -p $BACKUPDIR
|
||||
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 [email protected]:/home/mab/pi/staticFiles /home/mab/ss/
|
||||
|
||||
echo "Updating Database"
|
||||
mysql -u root --password="$PROD_DB_PASS" < $BACKUPFILE
|
||||
|
||||
## Optimize Database Tables
|
||||
# mysqlcheck --all-databases
|
||||
mysqlcheck --all-databases -o -u root --password="$PROD_DB_PASS" #--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="$PROD_DB_PASS"
|
||||
|
||||
#clean up extracted and modified SQL dumps
|
||||
rm *.sql
|
||||
|
||||
|
||||
|
||||
echo '-------'
|
||||
echo "Applied Prod database to Dev. LastFile: $BACKUPFILE"
|
||||
echo '-------'
|
||||
@@ -1,61 +0,0 @@
|
||||
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="LetsKeepOut69FatHorses!"
|
||||
# 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 [email protected]:/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 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
|
||||
# Take all variables in .env and turn them into local variables for this script
|
||||
source /home/mab/.env
|
||||
|
||||
# BACKUPDIR defined in .env
|
||||
BACKUPDIR="/home/mab/databaseBackupPi"
|
||||
|
||||
mkdir -p $BACKUPDIR
|
||||
cd $BACKUPDIR
|
||||
|
||||
NOW=$(date +"%Y-%m-%d_%H-%M")
|
||||
mysqldump --all-databases --single-transaction --user root --password="$PROD_DB_PASS" > "backup-$NOW.sql"
|
||||
gzip "backup-$NOW.sql"
|
||||
ssh [email protected] -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 -f --
|
||||
|
||||
##
|
||||
# Restore DB
|
||||
##
|
||||
|
||||
#Restore DB
|
||||
# copy file over, run restore
|
||||
# scp -P 13328 backup-2019-12-04_03-00.sql [email protected]:/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
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
#
|
||||
# Push built release files to production server
|
||||
#
|
||||
|
||||
echo -e "\e[32m\nStarting Build. \n\e[0m"
|
||||
|
||||
# Build out new release
|
||||
cd client
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
# only compress client/dist and server with node_modules
|
||||
echo -e "\e[32m\nCompressing client and server code... \n\e[0m"
|
||||
tar -czf release.tar.gz server node_modules client/dist staticFiles/assets
|
||||
|
||||
#send compressed release to remote machine
|
||||
echo -e "\e[32m\nMoving compressed release to production... \n\e[0m"
|
||||
rsync -e 'ssh -p 13328' -havzC --update release.tar.gz [email protected]:/home/mab/pi/
|
||||
|
||||
# Remove Release from local after its been uploaded
|
||||
rm release.tar.gz
|
||||
|
||||
#uncompress release on server
|
||||
echo -e "\e[32m\nExtracting release on production... \n\e[0m"
|
||||
ssh [email protected] -p 13328 "cd /home/mab/pi/; rm -r server node_modules client; tar -xzf *.tar.gz --overwrite; rm *.tar.gz; pm2 reload all"
|
||||
|
||||
#Congratulate how awesome you are
|
||||
echo -e "\e[32m\nRelease Complete! Nice Work! \n\e[0m"
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,28 +1,14 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
.DS_Store
|
||||
node_modules/
|
||||
/dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Local dev TLS certs (mkcert)
|
||||
*.pem
|
||||
certs/
|
||||
@@ -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,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -1,5 +1,21 @@
|
||||
# Vue 3 + Vite
|
||||
# client2
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
> client2
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
## Build Setup
|
||||
|
||||
``` bash
|
||||
# install dependencies
|
||||
npm install
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
npm run dev
|
||||
|
||||
# build for production with minification
|
||||
npm run build
|
||||
|
||||
# build for production and view the bundle analyzer report
|
||||
npm run build --report
|
||||
```
|
||||
|
||||
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).
|
||||
@@ -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'
|
||||
))
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
'use strict'
|
||||
const merge = require('webpack-merge')
|
||||
const prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"'
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
'use strict'
|
||||
module.exports = {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
@@ -1,31 +1,62 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="description" content="Encrypted notes and scratch pad" />
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
|
||||
<!-- PWA. The manifest is a real static file: it used to be injected at
|
||||
runtime as a base64 data: URI from App.vue, with the production origin
|
||||
hardcoded, which broke icons and scope outside production. -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="icon" href="/api/static/assets/favicon.ico" type="image/ico"/>
|
||||
<link rel="shortcut icon" href="/api/static/assets/favicon.ico" type="image/x-icon"/>
|
||||
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="icon" type="image/svg+xml" href="/img/icons/logo.svg" />
|
||||
<meta name="theme-color" content="#000" />
|
||||
<link rel="manifest" href="/api/static/assets/manifest.json">
|
||||
|
||||
<!-- iOS has no share-target support, but still installs to the home screen.
|
||||
The apple-touch icon must be the opaque variant -- iOS composites
|
||||
transparent PNGs onto black and the dark half of the mark vanishes. -->
|
||||
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-180.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="Scribe" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
|
||||
<title>Solid Scribe</title>
|
||||
<title>Solid Scribe - An easy, encrypted Note App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<div id="app">
|
||||
<!-- placeholder data for scrapers with no JS -->
|
||||
<style>
|
||||
body {
|
||||
background-color: #212221;
|
||||
color: #aeaeae;
|
||||
}
|
||||
.centered {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
.logo {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
}
|
||||
.scrape-info {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="centered">
|
||||
<img class="logo" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo - if you can read this your connection is really slow">
|
||||
<h1>Solid Scribe</h1>
|
||||
<h3>An easy, encrypted Note App</h3>
|
||||
<h4>Loading...</h4>
|
||||
</div>
|
||||
|
||||
<div class="scrape-info">
|
||||
<h1>Solid Scribe</h1>
|
||||
<h2>A note application that respects your privacy.</h2>
|
||||
<p>Take notes with a clean editor that works on desktop or mobile.</p>
|
||||
<p>Search notes, links and files to find what you need.</p>
|
||||
<p>Accessable everywhere.</p>
|
||||
<p>Categorize notes with tags.</p>
|
||||
<p>Share data with fellow users.</p>
|
||||
<p>Encrypt notes for additional security.</p>
|
||||
<b>This site requires Javascipt to run.</b>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,32 +1,66 @@
|
||||
{
|
||||
"name": "solidscribe-client",
|
||||
"name": "client2",
|
||||
"version": "1.0.0",
|
||||
"description": "client2",
|
||||
"author": "max",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
|
||||
"start": "npm run dev",
|
||||
"build": "node build/build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"axios": "^1.1.3",
|
||||
"chart.js": "^4.6.1",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"fomantic-ui-css": "^2.9.0",
|
||||
"mitt": "^3.0.1",
|
||||
"socket.io-client": "^2.5.0",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"vue": "^3.5.18",
|
||||
"vue-chartjs": "^5.3.4",
|
||||
"vue-router": "^4.0.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuex": "^4.0.0"
|
||||
"axios": "^0.19.2",
|
||||
"es6-promise": "^4.2.8",
|
||||
"fomantic-ui-css": "^2.8.6",
|
||||
"vue": "^2.5.2",
|
||||
"vue-router": "^3.3.4",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.1.2",
|
||||
"vite-plugin-pwa": "^2.0.0",
|
||||
"workbox-window": "^7.4.1"
|
||||
}
|
||||
"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 ie <= 8"
|
||||
]
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 132.29166 132.29167" width="512" height="512">
|
||||
<rect x="0" y="0" width="132.29166" height="132.29167" fill="#ffffff"/>
|
||||
<g transform="translate(13.229166,13.229167) scale(0.8)">
|
||||
<g transform="translate(0,-164.70832)">
|
||||
<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>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 132.29166 132.29167" width="512" height="512">
|
||||
<g transform="translate(0,-164.70832)">
|
||||
<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: 1.8 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,78 +0,0 @@
|
||||
{
|
||||
"id": "/",
|
||||
"name": "Solid Scribe",
|
||||
"short_name": "Scribe",
|
||||
"description": "Encrypted notes and scratch pad",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"display_override": [
|
||||
"standalone",
|
||||
"minimal-ui"
|
||||
],
|
||||
"orientation": "any",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#000000",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/img/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Scratch Pad",
|
||||
"short_name": "Scratch",
|
||||
"url": "/quick",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/img/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Files & Links",
|
||||
"short_name": "Files",
|
||||
"url": "/attachments",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/img/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/share-target",
|
||||
"method": "GET",
|
||||
"enctype": "application/x-www-form-urlencoded",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text",
|
||||
"url": "url"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode == 2) }">
|
||||
|
||||
<!-- <div class="ui container" v-if="showFakeSite">
|
||||
<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>
|
||||
@@ -11,7 +11,7 @@
|
||||
<h2>You will be redirected to the correct domain in {{redirectSeconds}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="auth-block" v-if="requireAuth">
|
||||
<div class="ui raised inverted segment">
|
||||
@@ -51,24 +51,18 @@
|
||||
|
||||
<global-notification />
|
||||
|
||||
<update-prompt />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
// import io from 'socket.io-client'
|
||||
import axios from 'axios'
|
||||
import GlobalSiteMenu from '@/components/GlobalSiteMenu.vue'
|
||||
import GlobalNotification from '@/components/GlobalNotificationComponent.vue'
|
||||
import UpdatePrompt from '@/components/UpdatePrompt.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
'global-site-menu': GlobalSiteMenu,
|
||||
'global-notification': GlobalNotification,
|
||||
'update-prompt': UpdatePrompt,
|
||||
'global-site-menu': require('@/components/GlobalSiteMenu.vue').default,
|
||||
'global-notification':require('@/components/GlobalNotificationComponent.vue').default,
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
@@ -177,18 +171,16 @@ export default {
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
// Vite does not shim node's `process` into the browser bundle the way
|
||||
// webpack/vue-cli did; import.meta.env.DEV is the equivalent.
|
||||
const isDev = import.meta.env.DEV
|
||||
// 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)
|
||||
// }
|
||||
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', () => {
|
||||
@@ -207,11 +199,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 () {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import mitt from 'mitt'
|
||||
import Vue from 'vue'
|
||||
const EventBus = new Vue()
|
||||
|
||||
const emitter = mitt()
|
||||
Object.defineProperties(Vue.prototype, {
|
||||
$bus: {
|
||||
get: () => EventBus
|
||||
}
|
||||
})
|
||||
|
||||
// Preserves the Vue 2 $on/$emit/$off API used throughout the codebase
|
||||
const EventBus = {
|
||||
$on: (...args) => emitter.on(...args),
|
||||
$off: (...args) => emitter.off(...args),
|
||||
$emit: (...args) => emitter.emit(...args),
|
||||
}
|
||||
|
||||
export default EventBus
|
||||
export default EventBus;
|
||||
@@ -1,3 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
const helpers = {}
|
||||
|
||||
helpers.timeAgo = (time) => {
|
||||
@@ -51,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,9 +62,10 @@ helpers.timeAgo = (time) => {
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
install(app) {
|
||||
app.config.globalProperties.$helpers = helpers
|
||||
install () {
|
||||
Vue.helpers = helpers
|
||||
Vue.prototype.$helpers = helpers
|
||||
}
|
||||
}
|
||||
|
||||
export default plugin
|
||||
Vue.use(plugin)
|
||||
@@ -1,81 +0,0 @@
|
||||
//
|
||||
// Launch page preference.
|
||||
//
|
||||
// The app used to inject its manifest at runtime with
|
||||
// `start_url: window.location.href`, so installing from /metrictrack made the
|
||||
// installed app always open there. That mattered: not everyone wants the notes
|
||||
// page as their home.
|
||||
//
|
||||
// A static manifest can't carry a per-install start_url, and we need a static
|
||||
// same-origin manifest for share_target to register at all (a data: URI
|
||||
// manifest has an opaque origin, so scope resolution fails). So the preference
|
||||
// moves in-app: the manifest's start_url is "/", and we redirect from there.
|
||||
//
|
||||
// This is also strictly better than the old behaviour -- the old one froze the
|
||||
// home page at install time and could only be changed by reinstalling. This one
|
||||
// is editable in Settings.
|
||||
//
|
||||
|
||||
const LAUNCH_KEY = 'launchPage'
|
||||
const DEFAULT_LAUNCH = '/notes'
|
||||
|
||||
// Pages that make sense as a home. Anything else (a specific note, the share
|
||||
// target, login) would be a bad place to land every time.
|
||||
export const LAUNCH_OPTIONS = [
|
||||
{ path: '/notes', label: 'Notes' },
|
||||
{ path: '/overview', label: 'Overview' },
|
||||
{ path: '/quick', label: 'Scratch Pad' },
|
||||
{ path: '/metrictrack', label: 'Metric Tracking' },
|
||||
{ path: '/attachments', label: 'Files & Links' },
|
||||
]
|
||||
|
||||
export function getLaunchPage(){
|
||||
const stored = localStorage.getItem(LAUNCH_KEY)
|
||||
const valid = LAUNCH_OPTIONS.some(option => option.path === stored)
|
||||
return valid ? stored : DEFAULT_LAUNCH
|
||||
}
|
||||
|
||||
export function setLaunchPage(path){
|
||||
if(!LAUNCH_OPTIONS.some(option => option.path === path)){ return }
|
||||
localStorage.setItem(LAUNCH_KEY, path)
|
||||
}
|
||||
|
||||
export function isStandalone(){
|
||||
return window.matchMedia('(display-mode: standalone)').matches
|
||||
|| window.navigator.standalone === true
|
||||
}
|
||||
|
||||
//
|
||||
// Capture the page the user installed from, matching the old behaviour: if they
|
||||
// install while on /metrictrack, that becomes their launch page. Only set as a
|
||||
// default -- an explicit choice in Settings is never overwritten.
|
||||
//
|
||||
export function watchForInstall(){
|
||||
window.addEventListener('appinstalled', () => {
|
||||
if(localStorage.getItem(LAUNCH_KEY)){ return }
|
||||
|
||||
const path = window.location.pathname
|
||||
if(LAUNCH_OPTIONS.some(option => option.path === path)){
|
||||
localStorage.setItem(LAUNCH_KEY, path)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// "/" is a shim for installed launches. Redirect only when running standalone,
|
||||
// so the marketing home page still works normally in a browser tab.
|
||||
//
|
||||
export function installLaunchRedirect(router){
|
||||
|
||||
watchForInstall()
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
|
||||
// Only the very first navigation of a standalone launch
|
||||
if(to.path === '/' && from.matched.length === 0 && isStandalone()){
|
||||
return next(getLaunchPage())
|
||||
}
|
||||
|
||||
return next()
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -54,7 +43,6 @@ html {
|
||||
height:100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: var(--body_bg_color);
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
@@ -92,12 +80,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);
|
||||
@@ -147,9 +132,6 @@ body {
|
||||
.ui.dividing.header {
|
||||
border-bottom-color: var(--dark_border_color);
|
||||
}
|
||||
.ui.dividing.header > .sub.header {
|
||||
color: var(--dark_border_color);
|
||||
}
|
||||
.ui.icon.input > i.icon {
|
||||
color: var(--text_color);
|
||||
}
|
||||
@@ -178,21 +160,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 {
|
||||
color: var(--main-accent) !important;
|
||||
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 +176,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*/
|
||||
|
||||
@@ -313,7 +280,7 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
border: none;
|
||||
/*height: calc(100% - 69px);*/
|
||||
|
||||
min-height: 300px;
|
||||
min-height: 500px;
|
||||
background-color: var(--small_element_bg_color);
|
||||
/*margin-bottom: 15px;*/
|
||||
|
||||
@@ -332,9 +299,6 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 1100px;
|
||||
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
||||
|
||||
}
|
||||
.squire-box::selection,
|
||||
.squire-box::-moz-selection {
|
||||
@@ -356,14 +320,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;
|
||||
}
|
||||
.note-card-text p,
|
||||
.squire-box p {
|
||||
@@ -398,37 +357,8 @@ i.green.icon.icon.icon.icon, i.green.icon.icon.icon.icon.icon {
|
||||
.squire-box ol,
|
||||
.note-card-text ul,
|
||||
.squire-box ul {
|
||||
margin: 3px 0;
|
||||
display: block;
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
/* Add border 1 indent level */
|
||||
.note-card-text > ol > ol,
|
||||
.squire-box > ol > ol,
|
||||
.note-card-text > ul > ul,
|
||||
.squire-box > ul > ul
|
||||
{
|
||||
border-left: 1px solid var(--border_color);
|
||||
}
|
||||
.note-card-text ol > ol,
|
||||
.squire-box ol > ol,
|
||||
.note-card-text ul > ul,
|
||||
.squire-box ul > ul {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
|
||||
|
||||
ol {
|
||||
counter-reset: item;
|
||||
}
|
||||
ol li {
|
||||
display: block;
|
||||
}
|
||||
ol li:before {
|
||||
content: counters(item, ".") ".";
|
||||
counter-increment: item;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.note-card-text ul > li,
|
||||
.squire-box ul > li {
|
||||
position: relative;
|
||||
@@ -555,6 +485,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 - 122px);
|
||||
}
|
||||
|
||||
.ui.button.shrinking {
|
||||
font-size: 0.85714286rem;
|
||||
margin: 0 3px;
|
||||
@@ -592,15 +526,6 @@ padding-right: 10px;
|
||||
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 +555,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;
|
||||
@@ -945,64 +866,3 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
i.outline {
|
||||
outline-style: 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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
@@ -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 colors"
|
||||
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>
|
||||
@@ -152,24 +147,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>
|
||||
@@ -18,13 +18,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import axios from 'axios'
|
||||
export default {
|
||||
name: 'FileUploadButton',
|
||||
props: [ 'noteId' ],
|
||||
components: {
|
||||
'nm-button':defineAsyncComponent(() => import('@/components/NoteMenuButtonComponent.vue'))
|
||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -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,28 @@
|
||||
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: 40px;
|
||||
}
|
||||
.logo-display {
|
||||
width: 27px;
|
||||
@@ -108,49 +103,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 +128,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 +141,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 +150,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 +188,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 +206,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,22 +266,12 @@
|
||||
<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
|
||||
@@ -336,27 +279,14 @@
|
||||
</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>
|
||||
<router-link class="menu-item menu-button" exact-active-class="active" to="/settings">
|
||||
<i class="cog icon"></i>Settings
|
||||
</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 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>
|
||||
|
||||
@@ -369,15 +299,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'search-input': defineAsyncComponent(() => import('@/components/SearchInput.vue')),
|
||||
'counter':defineAsyncComponent(() => import('@/components/AnimatedCounterComponent.vue')),
|
||||
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||
'search-input': require('@/components/SearchInput.vue').default,
|
||||
'counter':require('@/components/AnimatedCounterComponent.vue').default,
|
||||
'logo':require('@/components/LogoComponent.vue').default,
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
@@ -394,7 +323,7 @@ import { defineAsyncComponent } from 'vue'
|
||||
beforeMount(){
|
||||
window.addEventListener('resize', this.resizeEventHandler)
|
||||
},
|
||||
beforeUnmount(){
|
||||
beforeDestroy(){
|
||||
window.removeEventListener('resize', this.resizeEventHandler)
|
||||
},
|
||||
mounted: function(){
|
||||
@@ -420,16 +349,6 @@ import { defineAsyncComponent } from 'vue'
|
||||
},
|
||||
},
|
||||
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('/')
|
||||
@@ -528,11 +447,8 @@ import { defineAsyncComponent } from 'vue'
|
||||
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,13 +22,15 @@
|
||||
</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">
|
||||
@@ -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>
|
||||
@@ -126,7 +110,6 @@
|
||||
enabled: false,
|
||||
username: '',
|
||||
password: '',
|
||||
password2: '',
|
||||
authToken: '',
|
||||
require2FA: false,
|
||||
}
|
||||
@@ -153,31 +136,14 @@
|
||||
this.$io.emit('user_connect', token)
|
||||
localStorage.setItem('loginToken', token)
|
||||
|
||||
//Redirect user to notes section after login, or back to
|
||||
//wherever they were headed (e.g. an Android share waiting to
|
||||
//be saved -- see ShareTargetPage.vue)
|
||||
this.$router.push(this.$route.query.redirect || '/notes')
|
||||
//Redirect user to notes section after login
|
||||
this.$router.push('/notes')
|
||||
}
|
||||
},
|
||||
register(){
|
||||
|
||||
let error = false
|
||||
|
||||
if( this.username.length == 0 || this.password.length == 0 || this.password2.length == 0 ){
|
||||
|
||||
this.$bus.$emit('notification', 'All fields are required.')
|
||||
error = true
|
||||
}
|
||||
|
||||
if( this.password !== this.password2 ){
|
||||
|
||||
this.$bus.$emit('notification', 'Passwords must be identical.')
|
||||
error = true
|
||||
}
|
||||
|
||||
if(error){
|
||||
//Login section
|
||||
this.$router.push('/login')
|
||||
if( this.username.length == 0 || this.password.length == 0 ){
|
||||
this.$bus.$emit('notification', 'Unable to Sign Up - Username and Password Required')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -237,6 +203,7 @@
|
||||
<style type="text/css" scoped="true">
|
||||
.small-terms {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,18 @@
|
||||
width="500">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
style="display:inline"
|
||||
transform="translate(0,-164.70832)"
|
||||
@@ -20,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>
|
||||
@@ -44,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
|
||||
@@ -82,7 +79,4 @@
|
||||
filter: saturate(145%);
|
||||
-webkit-filter: saturate(145%);
|
||||
}
|
||||
g > path {
|
||||
filter: drop-shadow(1px 1px 1px black);
|
||||
}
|
||||
</style>
|
||||
@@ -1,212 +0,0 @@
|
||||
<template>
|
||||
<div class="line-graph-holder">
|
||||
<Line v-if="hasData" :data="chartData" :options="chartOptions" />
|
||||
<div v-else class="no-data">No data for the selected range.</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Line } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from 'chart.js'
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Tooltip, Legend)
|
||||
|
||||
// Categorical slots, validated for both of this app's surfaces (#fff / #000)
|
||||
// with scripts/validate_palette.js. Assigned by a field's stable position,
|
||||
// never by its position in a particular graph, so toggling fields on and off
|
||||
// never repaints the ones that stay.
|
||||
const SERIES = {
|
||||
light: ['#2a78d6','#eb6834','#1baf7a','#eda100','#e87ba4','#008300','#4a3aa7','#e34948'],
|
||||
dark: ['#3987e5','#d95926','#199e70','#c98500','#d55181','#008300','#9085e9','#e66767'],
|
||||
}
|
||||
|
||||
// A field can carry an explicit Fomantic colour name chosen by the user; that
|
||||
// is their intent and it matches the field's icon elsewhere on the page, so it
|
||||
// wins over the generated slot.
|
||||
const FOMANTIC = {
|
||||
red:'#DB2828', orange:'#F2711C', yellow:'#FBBD08', olive:'#B5CC18',
|
||||
green:'#21BA45', teal:'#00B5AD', blue:'#2185D0', violet:'#6435C9',
|
||||
purple:'#A333C8', pink:'#E03997', brown:'#A5673F', grey:'#767676',
|
||||
black:'#1B1C1D',
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'MetricLineGraph',
|
||||
inject: ['getFieldColor'],
|
||||
components: { Line },
|
||||
props: {
|
||||
graph: { type: Object, required: true }, // { fieldIds: [...] }
|
||||
userFields: { type: Object, default: () => ({}) },
|
||||
cycleData: { type: Object, default: () => ({}) },
|
||||
tempChartDays: { type: [Number, String], default: 30 },
|
||||
},
|
||||
computed: {
|
||||
isDark(){
|
||||
return Number(this.$store.getters.getIsNightMode) === 1
|
||||
},
|
||||
surface(){
|
||||
return this.isDark ? '#000000' : '#ffffff'
|
||||
},
|
||||
textColor(){
|
||||
// Text wears text tokens, never the series colour.
|
||||
return this.isDark ? '#d9d9d9' : '#3d3d3d'
|
||||
},
|
||||
gridColor(){
|
||||
// One step off the surface, hairline and recessive.
|
||||
return this.isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.10)'
|
||||
},
|
||||
fieldIds(){
|
||||
return this.graph?.fieldIds || []
|
||||
},
|
||||
// Every date present in the data, oldest first, trimmed to the
|
||||
// requested window. Sorting by real date rather than object key
|
||||
// order means out-of-order writes cannot scramble the line.
|
||||
dateCodes(){
|
||||
const codes = Object.keys(this.cycleData)
|
||||
.map(code => ({ code, time: this.codeToDate(code) }))
|
||||
.filter(entry => !isNaN(entry.time))
|
||||
.sort((a, b) => a.time - b.time)
|
||||
.map(entry => entry.code)
|
||||
|
||||
const span = parseInt(this.tempChartDays)
|
||||
if(!span || span < 1 || span >= codes.length){ return codes }
|
||||
return codes.slice(-span)
|
||||
},
|
||||
hasData(){
|
||||
return this.fieldIds.length > 0 && this.dateCodes.length > 0
|
||||
},
|
||||
chartData(){
|
||||
return {
|
||||
labels: this.dateCodes.map(code => this.formatLabel(code)),
|
||||
datasets: this.fieldIds.map(fieldId => {
|
||||
const colour = this.colourFor(fieldId)
|
||||
return {
|
||||
label: this.userFields[fieldId]?.label || fieldId,
|
||||
data: this.dateCodes.map(code => this.valueAt(code, fieldId)),
|
||||
borderColor: colour,
|
||||
backgroundColor: colour,
|
||||
borderWidth: 2,
|
||||
tension: 0.25,
|
||||
// A marker that is drawn is >= 8px with a 2px ring in
|
||||
// the surface colour; dense series only show them on
|
||||
// hover so the line stays readable.
|
||||
pointRadius: this.denseData ? 0 : 4,
|
||||
pointHoverRadius: 5,
|
||||
pointBorderColor: this.surface,
|
||||
pointBorderWidth: 2,
|
||||
pointHoverBorderColor: this.surface,
|
||||
pointHoverBorderWidth: 2,
|
||||
pointHitRadius: 12,
|
||||
spanGaps: false, // a missing day is a gap, not a zero
|
||||
}
|
||||
}),
|
||||
}
|
||||
},
|
||||
denseData(){
|
||||
return this.dateCodes.length > 40
|
||||
},
|
||||
chartOptions(){
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 200 },
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: this.textColor, maxRotation: 0, autoSkipPadding: 16 },
|
||||
border: { color: this.gridColor },
|
||||
},
|
||||
y: {
|
||||
grid: { color: this.gridColor, lineWidth: 1, drawTicks: false },
|
||||
ticks: { color: this.textColor, padding: 8 },
|
||||
border: { display: false },
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
// Always present: identity is never carried by colour alone,
|
||||
// and it is the relief for the low-contrast light slots.
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
color: this.textColor,
|
||||
usePointStyle: true,
|
||||
pointStyle: 'circle',
|
||||
boxWidth: 8,
|
||||
boxHeight: 8,
|
||||
padding: 14,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: this.isDark ? '#1b1b1b' : '#ffffff',
|
||||
titleColor: this.textColor,
|
||||
bodyColor: this.textColor,
|
||||
borderColor: this.gridColor,
|
||||
borderWidth: 1,
|
||||
padding: 10,
|
||||
usePointStyle: true,
|
||||
callbacks: {
|
||||
label: ctx => {
|
||||
const v = ctx.parsed.y
|
||||
return `${ctx.dataset.label}: ${v === null ? '—' : v}`
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Date codes are stored as D.M.YYYY
|
||||
codeToDate(code){
|
||||
const [d, m, y] = String(code).split('.').map(Number)
|
||||
return new Date(y, (m || 1) - 1, d || 1).getTime()
|
||||
},
|
||||
formatLabel(code){
|
||||
const date = new Date(this.codeToDate(code))
|
||||
if(isNaN(date.getTime())){ return code }
|
||||
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
||||
},
|
||||
valueAt(code, fieldId){
|
||||
const raw = this.cycleData[code]?.[fieldId]
|
||||
if(raw === undefined || raw === null || raw === ''){ return null }
|
||||
const num = Number(raw)
|
||||
// Non-numeric field types cannot be plotted on a value axis; a gap
|
||||
// is honest, where the old code substituted -1 and bent the scale.
|
||||
return isNaN(num) ? null : num
|
||||
},
|
||||
colourFor(fieldId){
|
||||
const named = this.getFieldColor ? this.getFieldColor(fieldId) : null
|
||||
if(named && FOMANTIC[named]){ return FOMANTIC[named] }
|
||||
|
||||
// Stable slot: keyed off the field's position in userFields, so the
|
||||
// colour follows the field rather than its rank in this graph.
|
||||
const all = Object.keys(this.userFields)
|
||||
const index = all.indexOf(fieldId)
|
||||
const slots = this.isDark ? SERIES.dark : SERIES.light
|
||||
return slots[(index < 0 ? 0 : index) % slots.length]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line-graph-holder {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
}
|
||||
.no-data {
|
||||
padding: 2em 0;
|
||||
text-align: center;
|
||||
color: var(--menu-text, #767676);
|
||||
}
|
||||
</style>
|
||||
@@ -1,348 +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">
|
||||
<div 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>
|
||||
</div>
|
||||
|
||||
|
||||
<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 class="ui basic center aligned segment" v-if="!graphs || graphs.length == 0">
|
||||
<h4 class="ui header">No graphs yet</h4>
|
||||
<p>Choose <b>Add/Edit Graphs</b>, then <b>New Graph</b>, and pick which metrics to plot.</p>
|
||||
</div>
|
||||
|
||||
<div v-for="(graph, index) in graphs" :class="`ui not-padded ${editGraphs?'active ':'inactive '}segment`">
|
||||
|
||||
<!-- 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="isLineGraph(graph) && graph.fieldIds && graph.fieldIds.length > 0">
|
||||
<LineGraph
|
||||
:graph="graph"
|
||||
:userFields="userFields"
|
||||
:cycleData="cycleData"
|
||||
:tempChartDays="tempChartDays"
|
||||
/>
|
||||
</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':isGraphTypeActive(graphId, graph)}">
|
||||
{{ 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="`${getFieldColor(fieldId)} ${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">
|
||||
<div 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>
|
||||
</div>
|
||||
|
||||
|
||||
<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>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
const PILL_CALENDAR = 'pillCalendar'
|
||||
const LAST_DONE = 'lastDone'
|
||||
|
||||
export default {
|
||||
inject: ['getFieldColor', 'getFieldIcon'],
|
||||
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':defineAsyncComponent(() => import('@/components/Metrictracking/PillCalendarGraph.vue')),
|
||||
'LineGraph':defineAsyncComponent(() => import('@/components/Metrictracking/LineGraph.vue')),
|
||||
},
|
||||
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
|
||||
},
|
||||
mounted(){
|
||||
this.localGraphData = this.graphs
|
||||
},
|
||||
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){
|
||||
|
||||
// graphTypesDef is an object, so its keys are strings and the
|
||||
// default line graph arrives here as the STRING 'undefined'.
|
||||
// Saved graphs represent the line graph as an absent type, so
|
||||
// remove the key rather than storing a string that no render
|
||||
// test can match.
|
||||
if(this.isLineGraphType(newType)){
|
||||
delete this.localGraphData[index].type
|
||||
}
|
||||
else {
|
||||
this.localGraphData[index].type = newType
|
||||
}
|
||||
|
||||
this.saveGraphs()
|
||||
},
|
||||
// A line graph is the default: an absent type. Accept the string
|
||||
// form too so graphs saved by the older code still render.
|
||||
isLineGraphType(type){
|
||||
return type === undefined || type === null || type === '' || type === 'undefined'
|
||||
},
|
||||
isLineGraph(graph){
|
||||
return this.isLineGraphType(graph?.type)
|
||||
},
|
||||
isGraphTypeActive(graphId, graph){
|
||||
if(this.isLineGraphType(graphId)){
|
||||
return this.isLineGraph(graph)
|
||||
}
|
||||
return String(graphId) === String(graph?.type)
|
||||
},
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,636 +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 {
|
||||
|
||||
}
|
||||
|
||||
.days-since {
|
||||
padding: 0.75em 0.5em 0.25em 0.5em;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.days-since-row {
|
||||
padding: 0.15em 0;
|
||||
}
|
||||
.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}` }} -->
|
||||
|
||||
|
||||
|
||||
<template v-for="(entry, dateCode) in getChartData" :key="dateCode">
|
||||
<span class="pill-container" v-if="dateCode == `${day}.${calendar.month}.${calendar.year}`">
|
||||
<template v-for="(dayData, fieldId) in entry" :key="fieldId">
|
||||
<span
|
||||
v-if="showZeroValuesCheck(dayData.value, fieldId)"
|
||||
class="pill"
|
||||
:class="[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 ${getFieldColor(fieldId)} ${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 ${getFieldIcon(fieldId)} icon`"></i>
|
||||
<span v-if="!hideValues">
|
||||
{{ getDayValue(fieldId, dayData.value) }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<!-- <span v-for="fieldId in graph.fieldIds"></span> -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- time since each metric was last recorded -->
|
||||
<div class="days-since" v-if="daysSince.length > 0">
|
||||
<div class="days-since-row" v-for="entry in daysSince" :key="entry.fieldId">
|
||||
<i :class="`${entry.color} ${entry.icon} icon`"></i>
|
||||
<span v-if="entry.days === null">Days Since {{ entry.label }} - no entries yet</span>
|
||||
<span v-else>Days Since {{ entry.label }} - {{ entry.days }} {{ entry.days === 1 ? 'day' : 'days' }}.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
// let chartData = {}
|
||||
|
||||
export default {
|
||||
inject: ['getFieldColor', 'getFieldIcon', 'getFieldLabel'],
|
||||
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: {
|
||||
// "Days Since <metric> - <n> days" for every field on this graph.
|
||||
// Counts a day as recorded only if it passes the same
|
||||
// showZeroValuesCheck the pills use, so the "Show/Hide Lowest Value"
|
||||
// toggle moves these numbers too.
|
||||
daysSince(){
|
||||
|
||||
// showZeroValuesCheck reads lowestGraphValue, which getChartData
|
||||
// assigns as a side effect -- touch it so the threshold is current
|
||||
// and so this recomputes whenever the chart data does.
|
||||
void this.getChartData
|
||||
|
||||
const dayMs = 86400000
|
||||
const midnight = (time) => {
|
||||
const date = new Date(time)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
return date.getTime()
|
||||
}
|
||||
const today = midnight(Date.now())
|
||||
|
||||
return (this.graph?.fieldIds || []).map(fieldId => {
|
||||
|
||||
let latest = null
|
||||
|
||||
Object.keys(this.cycleData || {}).forEach(dateCode => {
|
||||
|
||||
const value = this.cycleData[dateCode]?.[fieldId]
|
||||
if(value === undefined || value === null || value === ''){ return }
|
||||
|
||||
// honour the Lowest Value toggle
|
||||
if(!this.showZeroValuesCheck(value, fieldId)){ return }
|
||||
|
||||
const time = this.dateCodeToTime(dateCode)
|
||||
// ignore unparseable codes and anything dated ahead of today
|
||||
if(time === null || time > today){ return }
|
||||
|
||||
if(latest === null || time > latest){ latest = time }
|
||||
})
|
||||
|
||||
return {
|
||||
fieldId,
|
||||
label: this.getFieldLabel(fieldId),
|
||||
icon: this.getFieldIcon(fieldId),
|
||||
color: this.getFieldColor(fieldId),
|
||||
days: latest === null ? null : Math.round((today - latest) / dayMs),
|
||||
}
|
||||
})
|
||||
},
|
||||
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: {
|
||||
// Date codes are stored unpadded as D.M.YYYY
|
||||
dateCodeToTime(dateCode){
|
||||
|
||||
const parts = String(dateCode).split('.').map(Number)
|
||||
if(parts.length !== 3 || parts.some(isNaN)){ return null }
|
||||
|
||||
const [day, month, year] = parts
|
||||
const date = new Date(year, month - 1, day)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
|
||||
return isNaN(date.getTime()) ? null : date.getTime()
|
||||
},
|
||||
showZeroValuesCheck(dayValue, fieldId){
|
||||
|
||||
// Optional chaining: a graph can reference a default field (BT,
|
||||
// CM, ...) that was never customised, so it has no userFields
|
||||
// entry and the bare lookup threw.
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// lowestGraphValue comes from Math.min of the charted values, so
|
||||
// it is Infinity when the window holds no data -- without the
|
||||
// finite guard nothing would ever clear the threshold.
|
||||
if(!isFinite(this.lowestGraphValue)){
|
||||
return true
|
||||
}
|
||||
|
||||
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,12 @@
|
||||
<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 v-if="note.title == '' && note.subtext == ''">
|
||||
Empty Note
|
||||
@@ -23,10 +20,23 @@
|
||||
<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>
|
||||
<!-- Sub text display -->
|
||||
<span v-if="note.subtext.length > 0"
|
||||
class="small-text"
|
||||
v-html="note.subtext"></span>
|
||||
|
||||
|
||||
<!-- Not indexed warning -->
|
||||
<!-- <span v-if="note.indexed != 1">
|
||||
<span class="green label">Not Indexed</span>
|
||||
</span> -->
|
||||
|
||||
|
||||
<div class="ui fluid basic button" v-if="note.encrypted == 1">
|
||||
<i class="green lock icon"></i>
|
||||
Locked
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Shared Details -->
|
||||
<span class="subtext" v-if="note.shared == 2">
|
||||
@@ -47,85 +57,28 @@
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- Sub text display -->
|
||||
<span v-if="note.subtext.length > 0"
|
||||
class="small-text"
|
||||
v-html="note.subtext"></span>
|
||||
|
||||
|
||||
<!-- Not indexed warning -->
|
||||
<!-- <span v-if="note.indexed != 1">
|
||||
<span class="green label">Not Indexed</span>
|
||||
</span> -->
|
||||
|
||||
|
||||
<!-- <div class="ui fluid basic button" v-if="note.encrypted == 1">
|
||||
<i class="green lock icon"></i>
|
||||
Locked
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- slim card view -->
|
||||
<div v-if="titleView" class="thin-container" @click="cardClicked">
|
||||
|
||||
<!-- icon -->
|
||||
<span v-if="noteIcon" class="thin-icon">
|
||||
<i :class="`${noteIcon} icon`" :style="{ 'color':iconColor }"></i>
|
||||
</span>
|
||||
|
||||
<!-- title -->
|
||||
<span class="thin-title" v-if="note.title.length > 0">{{ note.title }}</span>
|
||||
|
||||
<!-- snippet -->
|
||||
<span class="thick-sub" v-if="note.subtext.length > 0 && note.title.length == 0">
|
||||
{{ removeHtml(note.subtext) }}
|
||||
</span>
|
||||
<span class="thin-sub" v-else-if="note.subtext.length > 0">
|
||||
{{ removeHtml(note.subtext) }}
|
||||
</span>
|
||||
<span v-else-if="note.title.length == 0 && removeHtml(note.subtext).length == 0">
|
||||
Empty Note
|
||||
</span>
|
||||
|
||||
<!-- tags -->
|
||||
<span v-if="note.tags" class="thin-tags" >
|
||||
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- edited -->
|
||||
<span class="thin-right">
|
||||
{{$helpers.timeAgo( note.updated )}}
|
||||
|
||||
<i class="green link ellipsis vertical icon"></i>
|
||||
</span>
|
||||
|
||||
<div 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 +115,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"/>
|
||||
@@ -184,7 +143,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
@@ -192,9 +150,9 @@ import { defineAsyncComponent } from 'vue'
|
||||
name: 'NoteTitleDisplayCard',
|
||||
props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults', 'titleView' ],
|
||||
components: {
|
||||
'delete-button': defineAsyncComponent(() => import('@/components/NoteDeleteButtonComponent.vue')),
|
||||
'note-tag-edit': defineAsyncComponent(() => import('@/components/NoteTagEdit.vue')),
|
||||
'side-slide-menu': defineAsyncComponent(() => import('@/components/SideSlideMenuComponent.vue')),
|
||||
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
|
||||
'note-tag-edit': require('@/components/NoteTagEdit.vue').default,
|
||||
'side-slide-menu': require('@/components/SideSlideMenuComponent.vue').default,
|
||||
},
|
||||
methods:{
|
||||
cardClicked(){
|
||||
@@ -228,12 +186,10 @@ import { defineAsyncComponent } from 'vue'
|
||||
},
|
||||
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') })
|
||||
@@ -249,10 +205,11 @@ import { defineAsyncComponent } from 'vue'
|
||||
//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') })
|
||||
},
|
||||
@@ -267,10 +224,9 @@ import { defineAsyncComponent } from 'vue'
|
||||
//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') })
|
||||
@@ -286,9 +242,6 @@ import { defineAsyncComponent } from 'vue'
|
||||
},
|
||||
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',
|
||||
@@ -296,18 +249,16 @@ import { defineAsyncComponent } from 'vue'
|
||||
// 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)
|
||||
|
||||
},
|
||||
},
|
||||
@@ -382,11 +333,13 @@ import { defineAsyncComponent } from 'vue'
|
||||
|
||||
.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%;
|
||||
@@ -411,7 +364,9 @@ import { defineAsyncComponent } from 'vue'
|
||||
|
||||
/*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 {
|
||||
@@ -459,10 +414,10 @@ import { defineAsyncComponent } from 'vue'
|
||||
.note-title-display-card {
|
||||
position: relative;
|
||||
background-color: var(--small_element_bg_color);
|
||||
|
||||
/*The subtle shadow*/
|
||||
/*box-shadow: 0px 1px 2px 1px rgba(210, 211, 211, 0.46);*/
|
||||
box-shadow: 2px 2px 6px 0 rgba(0,0,0,.15);
|
||||
transition: box-shadow, border-color ease 0.5s, transform linear 0.5s;
|
||||
transition: box-shadow ease 0.5s, transform linear 0.1s;
|
||||
margin: 5px;
|
||||
/*padding: 0.7em 1em;*/
|
||||
border-radius: .28571429rem;
|
||||
@@ -471,7 +426,7 @@ import { defineAsyncComponent } from 'vue'
|
||||
/*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;
|
||||
@@ -480,72 +435,32 @@ import { defineAsyncComponent } from 'vue'
|
||||
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 {
|
||||
@@ -553,7 +468,6 @@ import { defineAsyncComponent } from 'vue'
|
||||
padding: 5px 10px 0;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
background-color: rgba(200, 200, 200, 0.2);
|
||||
}
|
||||
.hover-hide {
|
||||
opacity: 0.0;
|
||||
@@ -562,6 +476,7 @@ import { defineAsyncComponent } from 'vue'
|
||||
.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;
|
||||
@@ -571,8 +486,6 @@ import { defineAsyncComponent } from 'vue'
|
||||
line-height: 0.8em;
|
||||
text-overflow: ellipsis;
|
||||
float: left;
|
||||
color: var(--main-accent);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.tiny-thumb-box {
|
||||
max-height: 70px;
|
||||
@@ -733,36 +646,4 @@ import { defineAsyncComponent } from 'vue'
|
||||
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>
|
||||
@@ -73,6 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</span>
|
||||
</template>
|
||||
@@ -103,7 +105,7 @@
|
||||
this.tagSuggestions = []
|
||||
})
|
||||
},
|
||||
beforeUnmount(){
|
||||
beforeDestroy(){
|
||||
this.$bus.$off('reset_fast_filters')
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
@@ -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,31 +88,30 @@
|
||||
|
||||
<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> -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
export default {
|
||||
name: 'SideSlideMenu',
|
||||
props: [ 'name', 'styleObject', 'fullShadow', 'skipHistory' ],
|
||||
components: {
|
||||
'nm-button':defineAsyncComponent(() => import('@/components/NoteMenuButtonComponent.vue'))
|
||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -130,7 +129,7 @@ import { defineAsyncComponent } from 'vue'
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeUnmount(){
|
||||
beforeDestroy(){
|
||||
},
|
||||
mounted(){
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div class="svg-holder" v-if="svg[file]" v-html="svg[file]" :alt="alt"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props:[ 'file','alt' ],
|
||||
data() {
|
||||
return {
|
||||
//Remove title, remove id from each SVG
|
||||
svg: {
|
||||
'idea':'<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="1013.02965" height="844.99456" viewBox="0 0 1013.02965 844.99456"><path d="M1050.48389,297.81272c0,35.17-8.33,64.12-22.7799,88.92a177.67758,177.67758,0,0,1-16.19,23.25c-53.12,64.97-151.88,100.53-241.14,158.13-263.48,170.02-292.02-102.42-280.12-270.3,4.8-67.74,31.18-129.79,73.09-177.3a278.22062,278.22062,0,0,1,28.45-27.97c47.58-40.52,109.05-65.04,178.58-65.04,119.29,0,221.16,71.95,261.55,173.34a261.84957,261.84957,0,0,1,18.55993,96.97Z" transform="translate(-93.48517 -27.50272)" fill="var(--main-accent)"/><path d="M830.054,88.4227l-266.71,32.09a278.22062,278.22062,0,0,1,28.45-27.97Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M1027.704,386.7327a177.67758,177.67758,0,0,1-16.19,23.25l-228.46-27.48Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M1041.2439,228.72269l-387.18994-50.22,377.87,22.34A260.439,260.439,0,0,1,1041.2439,228.72269Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M516.51853,99.50486c0,.69-.02,1.37-.08,2.04a27.99706,27.99706,0,1,1-52.68-15.11,25.73956,25.73956,0,0,1,2.06-3.31,27.9889,27.9889,0,0,1,50.6,14.1C516.4885,97.97483,516.51853,98.73484,516.51853,99.50486Z" transform="translate(-93.48517 -27.50272)" fill="var(--main-accent)"/><path d="M516.51853,99.50486c0,.69-.02,1.37-.08,2.04l-41.92-5.04,41.9.72C516.4885,97.97483,516.51853,98.73484,516.51853,99.50486Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M506.51853,113.61484l-38.78,4.67a28.05694,28.05694,0,0,1-2.96-3.95Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M495.51853,82.61484l-31.76,3.82a25.73956,25.73956,0,0,1,2.06-3.31Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M900.25143,603.63055c-.32346.60949-.65991,1.20075-1.027,1.76443a27.99706,27.99706,0,1,1-39.45-38.042,25.73938,25.73938,0,0,1,3.37127-1.9581,27.98891,27.98891,0,0,1,38.08621,36.17477C900.94214,602.265,900.61239,602.95037,900.25143,603.63055Z" transform="translate(-93.48517 -27.50272)" fill="var(--main-accent)"/><path d="M900.25143,603.63055c-.32346.60949-.65991,1.20075-1.027,1.76443L864.5584,581.29206l36.67354,20.27763C900.94214,602.265,900.61239,602.95037,900.25143,603.63055Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M884.80385,611.4064l-36.44426-14.054a28.05745,28.05745,0,0,1-.76292-4.87668Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M889.61936,578.867l-29.8449-11.514a25.73938,25.73938,0,0,1,3.37127-1.9581Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M1098.56381,120.95526c.48176.494.9422.99477,1.367,1.5163a27.99707,27.99707,0,1,1-48.26355,25.96387,25.73851,25.73851,0,0,1-.83628-3.80793,27.9889,27.9889,0,0,1,46.06931-25.23471C1097.47405,119.88088,1098.02619,120.404,1098.56381,120.95526Z" transform="translate(-93.48517 -27.50272)" fill="var(--main-accent)"/><path d="M1098.56381,120.95526c.48176.494.9422.99477,1.367,1.5163l-33.52955,25.66038,30.499-28.73915C1097.47405,119.88088,1098.02619,120.404,1098.56381,120.95526Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M1101.25637,138.03864l-24.50216,30.41946a28.0572,28.0572,0,0,1-4.87694-.76118Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M1071.73725,123.52584l-20.07,24.90959a25.73851,25.73851,0,0,1-.83628-3.80793Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M573.26553,636.95591s61.44557,2.56024,61.44557,17.92163-69.12627,0-69.12627,0Z" transform="translate(-93.48517 -27.50272)" fill="#a0616a"/><path d="M481.09717,872.49728l-115.21045-7.6807L360.76625,803.371l-25.60232,53.76488-112.65022-15.3614c2.56023-97.28882,43.524-192.01741,46.08418-204.81858s8.67923-65.69564,8.67923-65.69564c11.1882-23.73325,69.81727-25.96064,115.77362-23.81013,19.04829.87055,35.94576,2.509,46.36577,3.68674,6.88688.768,10.95787,1.33137,10.95787,1.33137,18.35667,18.92015,30.18524,44.70155,37.45621,73.50417C514.04734,729.73871,481.09717,872.49728,481.09717,872.49728Z" transform="translate(-93.48517 -27.50272)" fill="#2f2e41"/><circle cx="259.60038" cy="187.01487" r="64.00581" fill="#a0616a"/><path d="M309.56161,229.879S291.64,309.24619,263.47743,332.28828s110.09,0,110.09,0-30.72278-51.20465-7.68069-74.24674S309.56161,229.879,309.56161,229.879Z" transform="translate(-93.48517 -27.50272)" fill="#a0616a"/><path d="M440.13345,557.58871,397.659,562.29945,278.83882,575.51034s-.56318-1.51061-1.5617-4.25007c-8.628-23.52854-50.18049-138.022-54.76341-172.406-5.12046-38.40349,48.64441-84.48767,48.64441-84.48767s51.20465-10.24093,66.566-10.24093,76.807,23.04209,76.807,23.04209l22.914,206.12433,1.97143,17.84474Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M487.83059,625.97242,401.73,575.51034s-1.51076-4.91576-4.071-13.21089c-1.28011-4.14756-2.8415-9.16551-4.60823-14.84931-9.93371-31.90055-26.60085-84.53892-39.96518-122.9935-20.48186-58.88534,79.36719,104.96952,79.36719,104.96952lLine truncated
|
||||
'onboarding':`<svg id="70bb2b30-be60-4de4-9e4b-86e23973444e" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1143.11" height="875.78" viewBox="0 0 1143.11 875.78"><defs><linearGradient id="e5da6213-051c-4da8-b1b3-926fff123e76" x1="539.33" y1="763.29" x2="539.33" y2="57.83" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.54" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient><linearGradient id="052f7d93-657d-41ac-9ec0-ae0276bba283" x1="1034.54" y1="812.84" x2="1034.54" y2="197.81" gradientTransform="matrix(-1, 0, 0, 1, 1347.17, 0)" xlink:href="#e5da6213-051c-4da8-b1b3-926fff123e76"/></defs><title>onboarding</title><ellipse cx="849.67" cy="829.19" rx="115.8" ry="28.95" fill="var(--main-accent)" opacity="0.1"/><path d="M63.7,616.77c-13.1,4.49-29,13.65-26.32,28.21,1.34,7.38,7.32,12.85,13,17.47a368.3,368.3,0,0,0,79.36,49.15c13.57,6.16,28,11.81,38.25,23.17s15,27.24,18.33,42.55a345.86,345.86,0,0,1,5.72,110.41c40.91.78,86.21-.14,115.16-29.61,17.33-17.65,26.62-43.83,47-57.49,13.59-9.11,30.34-11.37,46.52-13.27q114.87-13.43,230.93-16.26c32.42-.79,65.55-1.07,95.82-12.37s57.75-36,63.9-69.49c.95-5.16,1.57-10.85,5.1-14.45,3.15-3.23,7.87-4,12.28-4.57l265.89-36c14.56-2,29.63-4.13,41.89-12.36,11.86-8,19.86-20.78,27.51-33.24,17.53-28.52,35.72-63,23.58-95.61-9.71-26.06-35.81-41.07-60.75-50.39s-51.79-15.87-72.06-34.14c-30.84-27.8-37.68-73.95-46.37-115.36a675.43,675.43,0,0,0-33-109.89C943,151.89,927.85,121,905.27,96.61S851.62,54.93,820,56.17C778.7,57.81,742.37,90,701,86c-34.94-3.35-63.37-31.35-94.83-48.77C550.32,6.28,484.4,9,423.29,18.65c-64.8,10.21-130,28.08-182.5,68.27-12,9.2-23.48,19.82-30.53,33.66-10.8,21.23-9.87,47.1-15.28,70.69-21.8,95-138.68,134.63-162.71,228.94C23.73,453.69,30.05,490,45.72,520.94c16.85,33.31,50.7,47.51,68.05,76.1C102.17,607.63,79.06,611.5,63.7,616.77Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)" opacity="0.1"/><path d="M898.6,849s-10.48-72.2,21-119.72a110.5,110.5,0,0,0,18.68-67.38,185.42,185.42,0,0,0-5.1-33.61" transform="translate(-28.45 -12.11)" fill="none" stroke="#535461" stroke-miterlimit="10" stroke-width="2"/><path d="M952.92,598.2c-1.21,8.49-20.11,30.91-20.11,30.91S921,602.28,922.18,593.79a15.53,15.53,0,1,1,30.74,4.41Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M971.6,648.87c-5.64,6.46-33.69,15-33.69,15s4.64-29,10.27-35.42a15.53,15.53,0,1,1,23.42,20.41Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M959,729.07c-8.13,2.74-36.66-4-36.66-4s18.62-22.66,26.75-25.39A15.53,15.53,0,0,1,959,729.07Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M935.76,787.61c-7.55,4.07-36.81,2.2-36.81,2.2s14.52-25.47,22.07-29.54a15.53,15.53,0,0,1,14.74,27.34Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M905.4,660.9c5.24,6.79,32.74,17,32.74,17s-2.92-29.18-8.16-36a15.53,15.53,0,0,0-24.58,19Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M879.12,729.7c7,4.91,36.31,6.41,36.31,6.41s-11.5-27-18.54-31.88a15.53,15.53,0,1,0-17.77,25.47Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M862.75,800.08c6.1,6,34.71,12.45,34.71,12.45S890.68,784,884.57,778a15.53,15.53,0,1,0-21.82,22.1Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M952.92,598.2c-1.21,8.49-20.11,30.91-20.11,30.91S921,602.28,922.18,593.79a15.53,15.53,0,1,1,30.74,4.41Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M971.6,648.87c-5.64,6.46-33.69,15-33.69,15s4.64-29,10.27-35.42a15.53,15.53,0,1,1,23.42,20.41Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M959,729.07c-8.13,2.74-36.66-4-36.66-4s18.62-22.66,26.75-25.39A15.53,15.53,0,0,1,959,729.07Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M935.76,787.61c-7.55,4.07-36.81,2.2-36.81,2.2s14.52-25.47,22.07-29.54a15.53,15.53,0,0,1,14.74,27.34Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M905.4,660.9c5.24,6.79,32.74,17,32.74,17s-2.92-29.18-8.16-36a15.53,15.53,0,0,0-24.58,19Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M879.12,729.7c7,4.91,36.31,6.41,36.31,6.41s-11.5-27-18.54-31.88a15.53,15.53,0,1,0-17.77,25.47Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M862.75,800.08c6.1,6,34.71,12.45,34.71,12.45S890.68,784,884.57,778a15.53,15.53,0,1,0-21.82,22.1Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M899.46,846.71s-38.84-61.76-29.27-118A110.5,110.5,0,0,0,860,659.56a185.49,185.49,0,0,0-18.29-28.66" transform="translate(-28.45 -12.11)" fill="none" stroke="#535461" stroke-miterlimit="10" stroke-width="2"/><path d="M847.51,595.43c2.32,8.26-5.87,36.41-5.87,36.41s-21.7-19.72-24-28a15.53,15.53,0,1,1,29.9-8.44Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M885.11,634.19c-2.53,8.19-24.71,27.37-24.71,27.37sLine truncated
|
||||
'cloud':`<svg id="b75ffde3-65ff-4761-a8e9-ebaeba35e9db" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="863.67584" height="787.45556" viewBox="0 0 863.67584 787.45556"><title>cloud_docs</title><rect x="674.64346" y="434.39412" width="2" height="140" fill="#f2f2f2"/><rect x="674.64346" y="281.39412" width="2" height="140" fill="#f2f2f2"/><rect x="674.64346" y="87.39412" width="2" height="140" fill="#f2f2f2"/><circle cx="675.64346" cy="434.39412" r="25" fill="#f2f2f2"/><circle cx="675.64346" cy="267.39412" r="47" fill="#f2f2f2"/><circle cx="675.64346" cy="87.39412" r="63" fill="#f2f2f2"/><circle cx="650.64346" cy="59.39412" r="10" fill="#d0cde1"/><circle cx="690.64346" cy="81.39412" r="6" fill="#d0cde1"/><circle cx="675.64346" cy="113.39412" r="6" fill="#d0cde1"/><path d="M558.5736,732.72005a443.16545,443.16545,0,0,1-168.1455-33.12793A356.65064,356.65064,0,0,1,194.67956,234.2889,285.75854,285.75854,0,0,1,567.183,77.58041,228.807,228.807,0,0,1,692.65905,375.84359,183.245,183.245,0,0,1,453.788,476.33431a146.79491,146.79491,0,0,1-80.502-191.35742,117.77062,117.77062,0,0,1,153.34668-64.51123,94.41622,94.41622,0,0,1,51.71875,122.938A75.64668,75.64668,0,0,1,479.7406,384.888a60.71786,60.71786,0,0,1-33.29786-79.14941A48.831,48.831,0,0,1,510.02331,278.991a39.264,39.264,0,0,1,21.50781,51.125,31.612,31.612,0,0,1-41.16064,17.31641,25.4898,25.4898,0,0,1-13.9624-33.19,20.59,20.59,0,0,1,26.812-11.27929,16.67345,16.67345,0,0,1,9.1333,21.71,13.5396,13.5396,0,0,1-17.62891,7.4165,11.03155,11.03155,0,0,1-6.043-14.36377,9.02471,9.02471,0,0,1,11.752-4.94384,7.42021,7.42021,0,0,1,4.06445,9.66211,6.13434,6.13434,0,0,1-7.99023,3.36132,5.10967,5.10967,0,0,1-2.79883-6.65283,4.283,4.283,0,0,1,5.583-2.34863,3.62962,3.62962,0,0,1,1.98877,4.72705,3.10075,3.10075,0,0,1-4.043,1.70019,2.67971,2.67971,0,0,1-1.46924-3.49462,2.34588,2.34588,0,0,1,.52344-.78467,2.2689,2.2689,0,0,0-.73145.95556,3.10864,3.10864,0,0,0,1.70215,4.04639,4.13478,4.13478,0,0,0,5.38379-2.26465,5.41865,5.41865,0,0,0-2.96826-7.05566,7.02632,7.02632,0,0,0-9.145,3.84717,9.0309,9.0309,0,0,0,4.94629,11.75732,11.54031,11.54031,0,0,0,15.02246-6.31982,14.67283,14.67283,0,0,0-8.03662-19.10352,18.58956,18.58956,0,0,0-24.20557,10.18262,23.4616,23.4616,0,0,0,12.86572,30.583,29.61209,29.61209,0,0,0,38.5542-16.21924,37.26339,37.26339,0,0,0-20.41113-48.51856,46.83052,46.83052,0,0,0-60.97412,25.65088A58.718,58.718,0,0,0,480.49548,383.036a73.64788,73.64788,0,0,0,96.00439-40.38721A92.41557,92.41557,0,0,0,525.8778,222.31722a115.77022,115.77022,0,0,0-150.74023,63.41455,144.79736,144.79736,0,0,0,79.40527,188.751,181.24485,181.24485,0,0,0,236.26465-99.394A226.80605,226.80605,0,0,0,566.4281,79.43246C421.5448,20.35384,255.60974,90.161,196.53112,235.04378,122.63122,416.27523,209.952,623.84017,391.183,697.74056c226.666,92.42676,486.269-16.78515,578.69678-243.45215l1.85156.75586A446.43912,446.43912,0,0,1,564.39,732.68294C562.45153,732.70736,560.50915,732.72005,558.5736,732.72005ZM497.93982,320.27718a.3519.3519,0,0,0-.13428.02735.34584.34584,0,0,0-.18555.1875.6803.6803,0,0,0,.37305.8872,1.10164,1.10164,0,0,0,1.436-.603,1.62634,1.62634,0,0,0-.7207-2.03858l-.63916,1.56543A.33875.33875,0,0,0,497.93982,320.27718Z" transform="translate(-168.16208 -56.27222)" fill="#3f3d56"/><circle cx="489.56553" cy="232.87527" r="49.53794" fill="#2f2e41"/><polygon points="544.143 380.894 550.143 399.894 472.143 411.894 473.143 391.894 544.143 380.894" fill="#a0616a"/><path d="M621.30554,585.16634l-3,61s-17,31-15,75,1,45,1,45l-1,16,16,2v-21s23-58,25-87l23-87Z" transform="translate(-168.16208 -56.27222)" fill="#a0616a"/><path d="M734.30554,585.16634l3,61s17,31,15,75-1,45-1,45l1,16-16,2v-21s-23-58-25-87l-23-87Z" transform="translate(-168.16208 -56.27222)" fill="#a0616a"/><path d="M718.30554,450.16634s-72,11-83,8l-36,125s33,13,73,9a34.86506,34.86506,0,0,1,24,4c12,7,48-12,48-12l25-5Z" transform="translate(-168.16208 -56.27222)" fill="#2f2e41"/><rect x="496.64953" y="407.40309" width="80" height="91" fill="var(--main-accent)"/><rect x="484.63738" y="417.38516" width="80" height="91" fill="#f2f2f2"/><path d="M605.30554,389.16634l-1,14,18,62s7,44,21,70c0,0,8,40,23,35s-7-44-7-44l-16-60-19-79Z" transform="translate(-168.16208 -56.27222)" fill="#a0616a"/><circle cx="493.64346" cy="250.39412" r="30" fill="#a0616a"/><path d="M644.30554,324.16634s9,27-9,32-36,16-32,34l2,4,125-30a27.43931,27.43931,0,0,0-27-13s-27.807-.73647-25.9035-18.86823l-3.0965-7.13177Z" transform="translate(-168.16208 -56.27222)" fill="#a0616a"/><path d="M726.30554,362.16634s5,0,18,17,22,26,22,26,12,6,5,20-32,28-32,28-25,30-28,15,21-23,21-23,18-17,17-27l-41-34Z" transform="translate(-168.16208 -56.27222)" fill="#a0616a"/><path d="M601.30554,387.16634s124-29,127-29l6.29076,9.74735s-1.29076,7.25265-16.29076,9.25265c0,0,3,15-3,30v37s-70,8-76,14l-10.77042-53.41383s-14.22958-8.58617-24.22958-1.58617C604.30554,403.16634,600.30554,390.16634,601.30554,387.16634Z" transform="translate(-168.16208 -56.27222)" fill="#d0cde1"/><path d="M621.30554,780.16634sLine truncated
|
||||
'secure':'<svg id="4687a0c2-bd42-4093-8bbc-92d10a889ded" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="866" height="720.37" viewBox="0 0 866 720.37"><defs><linearGradient id="01871234-c547-4dad-9a9e-117fcc2bb7d8" x1="660.5" y1="458.72" x2="660.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.54" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient><linearGradient id="705039c9-bbaf-489f-8754-0d88ff1d230d" x1="802.94" y1="120.14" x2="802.94" y2="40.02" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/><linearGradient id="f1b18cc7-88bb-4220-8574-cb67e65e6847" x1="802.94" y1="269.14" x2="802.94" y2="189.02" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/><linearGradient id="729136a4-2a29-4999-b297-e6b2d53009e3" x1="802.94" y1="417.14" x2="802.94" y2="337.02" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/><linearGradient id="7a22862c-2f66-4029-a70d-d91858435973" x1="475.1" y1="785.18" x2="475.1" y2="418.21" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/><linearGradient id="668716b8-0ce2-4b29-be6e-2467fe221975" x1="475.5" y1="810.19" x2="475.5" y2="750.5" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/><linearGradient id="032f3a0d-ef41-4ad6-8a52-90e2f3059f59" x1="459.55" y1="723.31" x2="459.55" y2="473.18" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/></defs><title>secure server</title><g opacity="0.8"><rect x="455" width="411" height="458.72" fill="url(#01871234-c547-4dad-9a9e-117fcc2bb7d8)"/></g><rect x="461.79" y="306.27" width="397.41" height="142.46" fill="#fff"/><rect x="494.41" y="340.06" width="15.4" height="73" fill="#64ffda"/><g opacity="0.7"><rect x="524.81" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.6"><rect x="555.22" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="585.62" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="616.02" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.4"><rect x="646.42" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.3"><rect x="676.83" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><circle cx="802.94" cy="376.09" r="37.16" fill="var(--main-accent)"/><rect x="461.79" y="158.26" width="397.41" height="142.46" fill="#fff"/><rect x="494.41" y="192.05" width="15.4" height="73" fill="#64ffda"/><g opacity="0.7"><rect x="524.81" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.6"><rect x="555.22" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="585.62" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="616.02" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.4"><rect x="646.42" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.3"><rect x="676.83" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><circle cx="802.94" cy="228.09" r="37.16" fill="var(--main-accent)"/><rect x="461.79" y="10.25" width="397.41" height="142.46" fill="#fff"/><rect x="494.41" y="44.05" width="15.4" height="73" fill="#64ffda"/><g opacity="0.7"><rect x="524.81" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.6"><rect x="555.22" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="585.62" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="616.02" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.4"><rect x="646.42" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.3"><rect x="676.83" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><circle cx="802.94" cy="80.08" r="40.06" fill="url(#705039c9-bbaf-489f-8754-0d88ff1d230d)"/><circle cx="802.94" cy="229.08" r="40.06" fill="url(#f1b18cc7-88bb-4220-8574-cb67e65e6847)"/><circle cx="802.94" cy="377.08" r="40.06" fill="url(#729136a4-2a29-4999-b297-e6b2d53009e3)"/><circle cx="802.94" cy="80.08" r="37.16" fill="var(--main-accent)"/><g opacity="0.8"><path d="M750.13,785.18H200.07V434a15.8,15.8,0,0,1,15.8-15.8H734.32a15.8,15.8,0,0,1,15.8,15.8Z" transform="translate(-167 -89.81)" fill="url(#7a22862c-2f66-4029-a70d-d91858435973)"/></g><path d="M56.45,334.85h503.3a15.8,15.8,0,0,1,15.8,15.8V695.37a0,0,0,0,1,0,0H40.65a0,0,0,0,1,0,0V350.65A15.8,15.8,0,0,1,56.45,334.85Z" fill="#fff"/><rect x="57.87" y="355.82" width="500.46" height="328.25" fill="#f5f5f5"/><rect x="59.79" y="371.76" width="498.34" height="295.32" fill="#e0e0e0" opacity="0.8"/><rect x="59.79" y="356.07" width="498.34" height="15.69" fill="#f5f5f5"/><circle cx="70.47" cy="363.67" r="3.8" fill="#ff5252"/><circle cx="80.93" cy="363.67" r="3.8" fill="#ff0"/><circle cx="91.4" cy="363.67" r="3.8" fill="#69f0ae"/><text x="-167" y="-89.81"></text><g opacity="0.8"><path d="M765.52,808.77c-30.62,2.32-556.46,1.39-574.84,0C166.59,Line truncated
|
||||
'robot':`<svg id="fd25c83a-acc9-441d-8dce-2bbb37970907" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1076.4" height="733.47" viewBox="0 0 1076.4 733.47"><defs><linearGradient id="bc224463-365b-4973-a542-782a8bb75c72" x1="487.42" y1="814.54" x2="487.42" y2="138.95" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.54" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient><linearGradient id="f10c32a2-f99a-4cbc-971d-c4e860c06767" x1="875.73" y1="813.03" x2="875.73" y2="187.56" gradientTransform="matrix(-1, 0, 0, 1, 1751.04, 0)" xlink:href="#bc224463-365b-4973-a542-782a8bb75c72"/></defs><title>robotics</title><path d="M1130.38,364.35h-91.94V475.79h85.22c.22,1.15.44,2.3.68,3.45h-85.9v89.85l.43-.14c2.43-.85,4.84-1.75,7.23-2.72l1.55-.62q3.66-1.53,7.23-3.23c.84-.39,1.68-.8,2.51-1.22a150,150,0,0,0,36.17-25c.31-.26.6-.55.89-.82,1.46-1.41,2.9-2.85,4.3-4.32.52-.54,1-1.08,1.54-1.64,1.65-1.79,3.24-3.64,4.79-5.5.23-.28.45-.56.68-.82A115.92,115.92,0,0,0,1121.34,498c.13-.26.24-.55.36-.82,1.26-2.82,2.39-5.68,3.37-8.59.41-1.22.81-2.45,1.16-3.68.13-.37.23-.74.34-1.1,5.09-17.84,5.68-36.4,5.68-54.85q0-3.23,0-6.45c0-7.45-.11-14.95-.24-22.44,0-1.94-.1-3.88-.14-5.83-.13-4.94-.3-9.9-.54-14.84-.06-1.37-.13-2.75-.21-4.11,0-.79-.08-1.58-.14-2.37Q1130.71,368.61,1130.38,364.35Z" transform="translate(-61.56 -83.02)" fill="var(--main-accent)" opacity="0.1"/><path d="M1038.44,240.27h50.44a343.41,343.41,0,0,0-26.83-32.84c-7.49-8.18-15.37-16.16-23.61-23.84Z" transform="translate(-61.56 -83.02)" fill="var(--main-accent)" opacity="0.1"/><path d="M1038.44,243.72V360.9h91.65c0-.34,0-.69-.09-1-.08-.93-.16-1.85-.25-2.77-.09-1.08-.21-2.17-.32-3.25s-.25-2.35-.39-3.52-.26-2.35-.41-3.52-.29-2.34-.45-3.5-.31-2.33-.48-3.49c-.14-1-.28-1.93-.43-2.89-.21-1.37-.43-2.73-.66-4.09,0-.2-.07-.41-.1-.6a253.15,253.15,0,0,0-6.75-29.42A201.46,201.46,0,0,0,1108.39,273a215,215,0,0,0-17.1-29.29Z" transform="translate(-61.56 -83.02)" fill="var(--main-accent)" opacity="0.1"/><path d="M820.1,364.35V475.79h196.38V364.35Zm0,114.88V554.7c22.86-7.31,49-4.35,72.7,1.65,18.35,4.65,36.23,11,54.55,15.55a198.26,198.26,0,0,0,24.87,4.7l2.06.23,3,.27,3,.22a1.67,1.67,0,0,1,.31,0c.44,0,.89,0,1.34.06l.46,0c.54,0,1.08.06,1.62.06h0c.77,0,1.55,0,2.33.07h1c.77,0,1.55,0,2.33,0l1.71,0,1.38,0c.59,0,1.17,0,1.75-.06.94,0,1.88-.08,2.83-.14h0l1.75-.12c.72,0,1.45-.1,2.16-.17,1.53-.12,3-.27,4.55-.46.6-.07,1.21-.14,1.81-.23l.89-.11c1.51-.2,3-.43,4.51-.69,1.11-.17,2.22-.37,3.34-.58V479.23Zm146.06-239v3.44H820.1V360.9h196.38V243.71h-9.85v-3.44h9.85V164.54c-38.2-30.78-82.85-54.88-132.58-66a282.59,282.59,0,0,0-63.8-6.66V240.27Zm-449-.66.69.66H798.13V92.91c-49.9,4.15-98.56,20.91-137.79,48.68-18.85,13.33-35.78,29.14-56.93,39.36a89.15,89.15,0,0,1-35.6,9.2Zm-1.93-60.44v33.35l27.08-26.43C532.24,182.57,524.3,180.2,515.19,179.17Zm-.9,419.56.9,0V671.8q15.33-1.15,30.54-3.14A547.36,547.36,0,0,0,678,634.4h0q6.84-2.76,13.58-5.67,4.49-2,8.93-4,6.66-3.06,13.22-6.29A463.75,463.75,0,0,0,769.62,586c1.11-.76,2.23-1.52,3.33-2.29q3.31-2.3,6.59-4.64c1.09-.79,2.18-1.57,3.26-2.37l2.25-1.66c2.24-1.67,4.47-3.34,6.73-5h0c2.09-1.5,4.2-3,6.34-4.4v-86.4H572.94c.16-.5.31-1,.48-1.53-.18.51-.34,1-.49,1.53H515.19V598.67Zm.9-355V360.9H798.13V243.71Zm0,120.64V475.79H798.13V364.35ZM458.67,182.56l34.56,33.73V179Q475.86,180.08,458.67,182.56ZM218,364.35V475.79H493.23V364.35Zm66.52,275.24a511.15,511.15,0,0,0,129.73,30.78l.19,0q12.58,1.33,25.25,2.06l.31,0h0c17.69,1,35.45,1.23,53.18.57V479.23H218l1,128.32M141.79,475.79h54.28V364.35H174.6c-2.23,3.91-4.39,7.86-6.44,11.85-1.1,2.13-2.17,4.29-3.21,6.45C150.82,412,141.84,444.22,141.79,475.79Zm0,3.44c.44,22.64,5.54,44.87,16.73,65.4,9.3,17,22.22,32,37.51,45V479.23ZM176.61,360.9h19.46v-29A310.66,310.66,0,0,0,176.61,360.9ZM298.5,240.27H486.74l.68-.66-1-1h0L433.49,187A427.49,427.49,0,0,0,387,199.43c-1.44.47-2.88,1-4.31,1.45A422,422,0,0,0,298.5,240.27ZM218,305.64V360.9H459.38l0,0,0,0h33.83v-114l-3.86-3.14H293A378.08,378.08,0,0,0,218,305.64Z" transform="translate(-61.56 -83.02)" fill="var(--main-accent)" opacity="0.1"/><path d="M1038.44,302.83V360.9h91.65c0-.34,0-.69-.09-1-.08-.93-.16-1.85-.25-2.77-.09-1.08-.21-2.17-.32-3.25s-.25-2.35-.39-3.52-.26-2.35-.41-3.52-.29-2.34-.45-3.5-.31-2.33-.48-3.49c-.14-1-.28-1.93-.43-2.89-.21-1.37-.43-2.73-.66-4.09,0-.2-.07-.41-.1-.6a253.15,253.15,0,0,0-6.75-29.42Z" transform="translate(-61.56 -83.02)" fill="var(--main-accent)" opacity="0.1"/><path d="M1130.38,364.35h-91.94V475.79h85.22c.22,1.15.44,2.3.68,3.45h-85.9v89.85l.43-.14c2.43-.85,4.84-1.75,7.23-2.72l1.55-.62q3.66-1.53,7.23-3.23c.84-.39,1.68-.8,2.51-1.22a150,150,0,0,0,36.17-25c.31-.26.6-.55.89-.82,1.46-1.41,2.9-2.85,4.3-4.32.52-.54,1-1.08,1.54-1.64,1.65-1.79,3.24-3.64,4.79-5.5.23-.28.45-.56.68-.82A115.92,115.92,0,0,0,1121.34,498c.13-.26.24-.55.36-.82,1.26-2.82,2.39-5.68,3.37-8.59.41-1.22.81-2.45,1.16-3.68.13-.37.23-.74.34-1.1,5.09-17.84,5.68-36.4,5Line truncated
|
||||
'icecream':`<svg id="acfec7e7-ff55-4f51-a7da-3052482bb043" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="814.10215" height="785" viewBox="0 0 814.10215 785"><title>ice_cream</title><path d="M663.893,822.88894l-229.21475.7592c-60.81041-18.2721-110.6323-81.736-110.94745-176.88721a269.465,269.465,0,0,1,3.23812-42.81223L511.59074,562.471l142.42432-31.995a283.262,283.262,0,0,1,29.2563,30.4193L547.08072,632.80761,482.83355,666.7338,555.028,641.82691l149.84346-51.69428a363.37031,363.37031,0,0,1,24.91622,44.67247l-45.211,46.72484L735.721,648.04757q.32788.76935.653,1.54117C768.78735,726.53874,728.57068,822.67471,663.893,822.88894Z" transform="translate(-192.94892 -57.5)" fill="#f2f2f2"/><path d="M626.22411,508.93981c-34.66294,9.9552-77.27875,22.53336-121.09467,35.60691C441.778,563.45453,375.92052,583.40809,327.9742,597.99381l.0313-.171a65.89939,65.89939,0,0,1,12.15994-28.22451c35.51716-46.17886,80.74259-78.4385,131.07659-90.4187h.00442a188.90614,188.90614,0,0,1,43.20151-5.1543C554.49386,473.89268,592.42573,486.45281,626.22411,508.93981Z" transform="translate(-192.94892 -57.5)" fill="#f2f2f2"/><ellipse cx="284.72861" cy="762.5" rx="156" ry="22.5" fill="#3f3d56"/><polygon points="229.229 552.5 229.229 587.5 254.229 590.5 269.229 571.5 257.229 547.5 229.229 552.5" fill="#fbbebe"/><polygon points="324.229 696.5 337.229 731.5 361.229 723.5 343.229 694.5 324.229 696.5" fill="#fbbebe"/><path d="M487.17753,456s-130-13-126,23,61,139,61,139l38-7s-27-42-25-56-6-33-6-33l19,3,11,17,23,118,35,106,26-7s-14-44-10-58-19-68-19-68l16-94s31-9,22-40S487.17753,456,487.17753,456Z" transform="translate(-192.94892 -57.5)" fill="#2f2e41"/><path d="M433.17753,634s-15-12-14-7a35.88236,35.88236,0,0,1,0,11l-14,31s-34,31-3,32,47-31,47-31,43-32,38-37-31.24173-16.48346-31.24173-16.48346S447.17753,641,433.17753,634Z" transform="translate(-192.94892 -57.5)" fill="#2f2e41"/><path d="M529.17753,773s-14-13-15-7-2,14-2,14l-21,20s-30,29-4,30a50.27149,50.27149,0,0,0,40-17s38-8,36-14-12.02812-29.212-16.51406-30.106S541.17753,781,529.17753,773Z" transform="translate(-192.94892 -57.5)" fill="#2f2e41"/><circle cx="288.22861" cy="223.5" r="31" fill="#fbbebe"/><path d="M467.17753,302s13,35,12,38,41-25,41-25-18-4-18-20Z" transform="translate(-192.94892 -57.5)" fill="#fbbebe"/><path d="M581.17753,353s18,18,21,24,71,82,71,82,45,53,27,54-41-42-41-42-64-47-67-58l-37-38Z" transform="translate(-192.94892 -57.5)" fill="#fbbebe"/><path d="M487.17753,332h-11l-15,24s-15,13-4,40,20,61,20,61,32,4,38,14,40,42,40,42,6-21,4-52,0-78,0-78l-2-62s-38.62342-8.95238-41.31171-7.47619S487.17753,332,487.17753,332Z" transform="translate(-192.94892 -57.5)" fill="#575a89"/><path d="M542.17753,324l15-3s16,10,31,39c0,0-32,21-35,20S542.17753,324,542.17753,324Z" transform="translate(-192.94892 -57.5)" fill="#575a89"/><path d="M450.17753,392s-23,37-16,51,16,13,16,13,17-3,17-17,7-15,7-15,44-36,27-36-40,25-40,25l-1-23Z" transform="translate(-192.94892 -57.5)" fill="#fbbebe"/><path d="M465.17753,355s-15,4-18,20-2,24-2,24l20-2Z" transform="translate(-192.94892 -57.5)" fill="#575a89"/><path d="M437.17753,435s16-21,18-18Z" transform="translate(-192.94892 -57.5)" opacity="0.1"/><path d="M496.92557,243.47071s-16.4399-16.42593-32.26693-5.39712-23.19733,21.533-21.43077,28.66652,6.70064,16.9912,6.70064,16.9912-3.29287-14.78823,10.01317-16.23678,31.49853-10.75491,31.49853-10.75491l13.15752,26.28707s1.60057-6.21312,6.53114-4.57193S511.35516,245.06778,496.92557,243.47071Z" transform="translate(-192.94892 -57.5)" fill="#2f2e41"/><path d="M384.43046,793.39447c0,14.58087-8.6676,19.672-19.36056,19.672q-.37157,0-.742-.00826c-.49543-.01062-.98615-.0342-1.47095-.06724-9.6502-.683-17.14766-6.03594-17.14766-19.59647,0-14.03354,17.93326-31.7415,19.28034-33.052l.00236-.00236c.05191-.05072.07786-.07549.07786-.07549S384.43046,778.8136,384.43046,793.39447Z" transform="translate(-192.94892 -57.5)" fill="var(--main-accent)"/><path d="M364.36451,810.83584l7.08105-9.89437-7.09875,10.98077-.01886,1.13594c-.49543-.01062-.98615-.0342-1.47095-.06724l.7632-14.58794-.0059-.11325.013-.02123.072-1.37775-7.11645-11.00791,7.13886,9.97459.01653.29253.5768-11.02205-6.09255-11.37476,6.16687,9.44024.6004-22.851.00236-.07785v.07549l-.10026,18.01937,6.06544-7.14358-6.0902,8.69591-.16043,9.86842,5.6632-9.4709-5.68678,10.923-.08965,5.48625,8.22171-13.18188L364.56269,798.633Z" transform="translate(-192.94892 -57.5)" fill="#3f3d56"/><path d="M824.25561,277.1656s-6.15258,19.13-15.75329,46.61588c-3.10475,8.92836-6.58171,18.7294-10.33044,29.04561-5.22251,14.36534-10.97441,29.73228-16.99808,45.09922-3.70576,9.45775-7.51178,18.90109-11.36076,28.12986-20.68952,49.46327-42.72393,92.3161-57.161,92.91711-32.43645,1.345-101.91689-215.681-109.50028-239.66146-.44364-1.40225-.67244-2.14622-.67244-2.14622Z" transform="translate(-192.94892 -57.5)" fill="#3f3d56"/><path d="M808.50232,323.78148c-3.10475,8.92836-6.58171,18.7294-10.33044,29.04561L712.25139,277.1656h43.31079Z" transform="translate(-192.94892 -57.5)" opacity="0.1"/><path d="M78Line truncated
|
||||
'watching':`<svg id="bea18b66-86ba-42fe-9190-76147bd64da7" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1057" height="784.44" viewBox="0 0 1057 784.44"><defs><linearGradient id="e9b7614d-2d40-4533-bad7-5d0811b316bd" x1="890.27" y1="819.43" x2="890.27" y2="453.22" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.54" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient></defs><title>meditating</title><g opacity="0.2"><path d="M719.61,602a23.27,23.27,0,0,1-2.11,3.25Q707,597.9,696.12,590.78l.47-.93a20.53,20.53,0,0,1,7.27-7.92c4-2.46,8.51-3,12.22-1s5.78,6,6,10.67A20.6,20.6,0,0,1,719.61,602Z" transform="translate(-71.5 -57.78)" fill="#3f3d56"/><ellipse cx="717.85" cy="577.53" rx="17.05" ry="13.02" transform="translate(-200 883.82) rotate(-62.09)" fill="#3f3d56"/><ellipse cx="727.6" cy="559.12" rx="17.05" ry="13.02" transform="translate(-178.54 882.64) rotate(-62.09)" fill="#3f3d56"/><ellipse cx="737.35" cy="540.71" rx="17.05" ry="13.02" transform="translate(-157.08 881.47) rotate(-62.09)" fill="#3f3d56"/><ellipse cx="747.1" cy="522.3" rx="17.05" ry="13.02" transform="translate(-135.63 880.29) rotate(-62.09)" fill="#3f3d56"/><path d="M770.75,373.5a61.7,61.7,0,0,1-1-8.58L802.67,376l-32.83-17.08a62.45,62.45,0,0,1,22.08-44.23l31.81,46L799,309.49a62.37,62.37,0,1,1,58.36,109.89,62.4,62.4,0,0,1,1,13.39l-50-.4,49.28,8.12a62.37,62.37,0,0,1-36.29,47.05,62.37,62.37,0,1,1-86.65-45.89,62.39,62.39,0,0,1,36.1-68.15Z" transform="translate(-71.5 -57.78)" fill="var(--main-accent)"/><path d="M851.14,459.71a62.14,62.14,0,0,1-29.84,27.83,62.37,62.37,0,1,1-86.65-45.89C732.23,428.36,854.42,453.52,851.14,459.71Z" transform="translate(-71.5 -57.78)" opacity="0.1"/></g><g opacity="0.2"><path d="M288.48,716.67a43.27,43.27,0,0,0,2.39,7.05q23.11-8.85,46.75-17c-.14-.64-.28-1.3-.44-2a39.53,39.53,0,0,0-9.83-18.22c-6.29-6.48-14.45-9.64-22.3-7.72s-13.63,8.47-16.24,17.09A39.55,39.55,0,0,0,288.48,716.67Z" transform="translate(-71.5 -57.78)" fill="#3f3d56"/><ellipse cx="303.33" cy="671.75" rx="25.07" ry="32.81" transform="translate(-222.12 33.29) rotate(-13.72)" fill="#3f3d56"/><ellipse cx="293.82" cy="632.78" rx="25.07" ry="32.81" transform="translate(-213.16 29.93) rotate(-13.72)" fill="#3f3d56"/><ellipse cx="284.31" cy="593.82" rx="25.07" ry="32.81" transform="translate(-204.19 26.56) rotate(-13.72)" fill="#3f3d56"/><ellipse cx="274.8" cy="554.86" rx="25.07" ry="32.81" transform="translate(-195.22 23.2) rotate(-13.72)" fill="#3f3d56"/><path d="M300.88,266a118.65,118.65,0,0,0,5.81-15.56l-66.55,5.11,69.33-16.38a120.32,120.32,0,0,0-20.33-93L208.05,217l70.26-83.83A120.07,120.07,0,1,0,117.52,310.72a119.33,119.33,0,0,0-8.11,24.52L203,358.12,107.18,350A120.14,120.14,0,0,0,152.72,455a120.07,120.07,0,1,0,183.37-44.75A120.08,120.08,0,0,0,300.88,266Z" transform="translate(-71.5 -57.78)" fill="var(--main-accent)"/><path d="M110.16,388.94a119.51,119.51,0,0,0,42.56,66,120.07,120.07,0,1,0,183.37-44.75C346.88,386.55,107,375.85,110.16,388.94Z" transform="translate(-71.5 -57.78)" opacity="0.1"/></g><ellipse cx="532" cy="711.44" rx="525" ry="73" fill="var(--main-accent)" opacity="0.1"/><rect x="138" width="568" height="684" rx="19.8" fill="#464353"/><rect x="187" y="69" width="470" height="546" fill="var(--main-accent)" opacity="0.1"/><circle cx="422" cy="651" r="20" fill="#fff"/><circle cx="422" cy="40" r="9.5" fill="#fff"/><path d="M493.5,88.78a9,9,0,1,1-9,9,9,9,0,0,1,9-9m0-1a10,10,0,1,0,10,10,10,10,0,0,0-10-10Z" transform="translate(-71.5 -57.78)" fill="#fff"/><path d="M393.5,517.67V499.6h2.06v8.67l9.16-8.48a.79.79,0,0,1,.52-.19.86.86,0,0,1,.87.87V516.8a.86.86,0,0,1-.87.87.8.8,0,0,1-.54-.21l-9.14-8.41v8.62Z" transform="translate(-71.5 -57.78)" fill="#fff"/><path d="M405.24,500.09a.36.36,0,0,1,.37.37V516.8a.37.37,0,0,1-.58.29l-8.3-7.63-.87-.81.88-.8,8.29-7.68a.37.37,0,0,1,.21-.08m-10.18,0v17.07H394V500.09h1.07m10.18-1a1.33,1.33,0,0,0-.86.32l-8.32,7.7V499.6a.51.51,0,0,0-.51-.51H393.5a.51.51,0,0,0-.51.51v18a.51.51,0,0,0,.51.51h2.05a.51.51,0,0,0,.51-.51v-7.46l8.32,7.66a1.32,1.32,0,0,0,.86.31,1.35,1.35,0,0,0,1.37-1.36V500.46a1.36,1.36,0,0,0-1.37-1.37Z" transform="translate(-71.5 -57.78)" fill="#fff"/><path d="M591.46,517.67V509l-9.16,8.48a.82.82,0,0,1-.53.19.86.86,0,0,1-.6-.25.82.82,0,0,1-.27-.62V500.46a.87.87,0,0,1,1.42-.67l9.14,8.42V499.6h2l0,18.06,0,0Z" transform="translate(-71.5 -57.78)" fill="#fff"/><path d="M581.76,500.09a.32.32,0,0,1,.21.08l8.3,7.63.87.8-.87.81-8.3,7.68a.31.31,0,0,1-.21.07.36.36,0,0,1-.37-.36V500.46a.37.37,0,0,1,.37-.37m11.26,0v17.07H592V500.09H593m-11.26-1a1.36,1.36,0,0,0-1.37,1.37V516.8a1.36,1.36,0,0,0,1.37,1.36,1.33,1.33,0,0,0,.87-.31l8.32-7.71v7.51a.51.51,0,0,0,.51.51h2a.51.51,0,0,0,.52-.51v-18a.51.51,0,0,0-.52-.51h-2a.51.51,0,0,0-.51.51v7.47l-8.32-7.66a1.34,1.34,0,0,0-.87-.32Z" transform="translate(-71.5 -57.78)" fill="#fff"/><path d="M496.9,518a1.54,1.54,0,0,1-1.Line truncated
|
||||
'':``,
|
||||
'':``,
|
||||
'':``,
|
||||
'':``,
|
||||
'':``,
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
|
||||
},
|
||||
methods: {
|
||||
onMouseUp(){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style type="text/css" scoped>
|
||||
.svg-holder :deep(svg) {
|
||||
width: 100%;
|
||||
max-height: 250px;
|
||||
}
|
||||
</style>
|
||||
@@ -113,7 +113,6 @@
|
||||
<style type="text/css">
|
||||
.button-fix {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
.hover-row:hover {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -2,43 +2,22 @@
|
||||
.colors {
|
||||
position: fixed;
|
||||
z-index: 1023;
|
||||
top: 35px;
|
||||
top: 5px;
|
||||
/*height: 100px;*/
|
||||
width: 400px;
|
||||
left: 20%;
|
||||
}
|
||||
.colors-container {
|
||||
/*max-width: 360px;*/
|
||||
display: flex;
|
||||
/*flex-direction: column;*/
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
align-content: stretch;
|
||||
|
||||
height: 250px;
|
||||
width: 100%;
|
||||
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>
|
||||
@@ -86,14 +59,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
|
||||
export default {
|
||||
components:{
|
||||
'nm-button':defineAsyncComponent(() => import('@/components/NoteMenuButtonComponent.vue'))
|
||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||
},
|
||||
props: [ 'lastUsedColor' ],
|
||||
data: function(){
|
||||
return {
|
||||
hover: false,
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div class="update-prompt" v-if="visible">
|
||||
<span class="update-text">A new version is ready.</span>
|
||||
<div class="ui tiny green button" v-on:click="apply()">Update</div>
|
||||
<div class="ui tiny basic button" v-on:click="dismiss()">Later</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'UpdatePrompt',
|
||||
data(){
|
||||
return { visible: false }
|
||||
},
|
||||
// The service worker is configured with skipWaiting:false, so a new build
|
||||
// sits waiting until the user accepts it here rather than swapping assets
|
||||
// out from under an open note mid-edit.
|
||||
beforeMount(){
|
||||
this.$bus.$on('sw-update-available', this.onUpdateAvailable)
|
||||
},
|
||||
beforeUnmount(){
|
||||
this.$bus.$off('sw-update-available', this.onUpdateAvailable)
|
||||
},
|
||||
methods: {
|
||||
onUpdateAvailable(){
|
||||
this.visible = true
|
||||
},
|
||||
apply(){
|
||||
this.visible = false
|
||||
// Imported lazily: this component mounts with the app, but the
|
||||
// worker registers after mount.
|
||||
import('@/registerServiceWorker').then(({ updateSW }) => updateSW(true))
|
||||
},
|
||||
dismiss(){
|
||||
// Stays waiting; the hourly update check will offer it again.
|
||||
this.visible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.update-prompt {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 1em;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75em;
|
||||
padding: 0.75em 1em;
|
||||
border: 1px solid var(--border_color);
|
||||
border-radius: 6px;
|
||||
background-color: var(--small_element_bg_color);
|
||||
color: var(--text_color);
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||
}
|
||||
.update-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,51 +1,74 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
// The Vue build version to load with the `import` command
|
||||
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
|
||||
|
||||
import Vue from 'vue'
|
||||
|
||||
import Vuex from 'vuex'
|
||||
import 'es6-promise/auto' //Vuex likes promises
|
||||
import store from './stores/mainStore';
|
||||
|
||||
import App from './App'
|
||||
import router from './router'
|
||||
import store from './stores/mainStore'
|
||||
|
||||
import 'fomantic-ui-css/components/reset.min.css'
|
||||
import 'fomantic-ui-css/components/site.css'
|
||||
import 'fomantic-ui-css/components/button.min.css'
|
||||
import 'fomantic-ui-css/components/container.min.css'
|
||||
import 'fomantic-ui-css/components/form.min.css'
|
||||
import 'fomantic-ui-css/components/grid.min.css'
|
||||
import 'fomantic-ui-css/components/header.min.css'
|
||||
import 'fomantic-ui-css/components/icon.css'
|
||||
import 'fomantic-ui-css/components/input.min.css'
|
||||
import 'fomantic-ui-css/components/segment.min.css'
|
||||
import 'fomantic-ui-css/components/label.min.css'
|
||||
//Include entire fomantic ui library
|
||||
// import 'fomantic-ui-css/semantic.css';
|
||||
|
||||
import './assets/semantic-helper.css'
|
||||
//Required site and reset CSS
|
||||
import 'fomantic-ui-css/components/reset.css'
|
||||
import 'fomantic-ui-css/components/site.css' //modified to remove included LATO fonts
|
||||
|
||||
// Last, so Tailwind utilities win specificity ties against Fomantic
|
||||
import './tailwind.css'
|
||||
//Only include parts that are used
|
||||
import 'fomantic-ui-css/components/button.css'
|
||||
import 'fomantic-ui-css/components/container.css'
|
||||
import 'fomantic-ui-css/components/form.css'
|
||||
import 'fomantic-ui-css/components/grid.css'
|
||||
import 'fomantic-ui-css/components/header.css'
|
||||
import 'fomantic-ui-css/components/icon.css' //Modified to remove brand icons
|
||||
import 'fomantic-ui-css/components/input.css'
|
||||
import 'fomantic-ui-css/components/segment.css'
|
||||
import 'fomantic-ui-css/components/label.css'
|
||||
|
||||
import io from 'socket.io-client'
|
||||
import EventBus from './EventBus'
|
||||
import Helpers from './Helpers'
|
||||
import { installLaunchRedirect } from './LaunchPage'
|
||||
|
||||
const socket = io({ path: '/socket' })
|
||||
//Overwrite and site styles and themes and good stuff
|
||||
require('./assets/semantic-helper.css')
|
||||
// Fonts
|
||||
require('./assets/roboto-latin.woff2')
|
||||
require('./assets/roboto-latin-bold.woff2')
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
document.title = to.meta.title + ' - Solid Scribe'
|
||||
next()
|
||||
|
||||
require('./assets/squire.js')
|
||||
|
||||
//Import socket io, init using nginx configured socket path
|
||||
import io from 'socket.io-client';
|
||||
const socket = io({ path:'/socket' });
|
||||
|
||||
//integrate connected socket into vue instance
|
||||
Object.defineProperties(Vue.prototype, {
|
||||
$io: {
|
||||
get: () => socket
|
||||
}
|
||||
})
|
||||
|
||||
// Installed launches land on "/" and are redirected to the user's chosen
|
||||
// launch page -- see LaunchPage.js
|
||||
installLaunchRedirect(router)
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.config.globalProperties.$io = socket
|
||||
app.config.globalProperties.$bus = EventBus
|
||||
// This callback runs before every route change, including on page load.
|
||||
// Sets the title of the page using vue router
|
||||
router.beforeEach((to, from, next) => {
|
||||
document.title = to.meta.title + ' - Solid Scribe';
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
app.use(Helpers)
|
||||
//Attach event bus to main vue object, all components will inherit event bus
|
||||
import EventBus from './EventBus'
|
||||
import Helpers from './Helpers'
|
||||
|
||||
app.mount('#app')
|
||||
Vue.use(Vuex)
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// After mount, so a failed registration can never block the app rendering
|
||||
import('./registerServiceWorker')
|
||||
new Vue({
|
||||
el: '#app',
|
||||
router,
|
||||
store,
|
||||
components: { App },
|
||||
template: '<App/>'
|
||||
})
|
||||
@@ -9,8 +9,6 @@ const SquireButtonFunctions = {
|
||||
activeList: false,
|
||||
activeToDo: false,
|
||||
activeColor: null,
|
||||
activeCode: false,
|
||||
activeSubTitle: false,
|
||||
//
|
||||
lastUsedColor: null,
|
||||
}
|
||||
@@ -30,8 +28,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,21 +38,15 @@ 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
|
||||
@@ -153,12 +143,6 @@ const SquireButtonFunctions = {
|
||||
this.editor.italic()
|
||||
}
|
||||
},
|
||||
modifyCode(){
|
||||
|
||||
this.selectLineIfNoSelect()
|
||||
|
||||
this.editor.toggleCode()
|
||||
},
|
||||
undoCustom(){
|
||||
//The same as pressing CTRL + Z
|
||||
// this.editor.focus()
|
||||
@@ -171,17 +155,16 @@ const SquireButtonFunctions = {
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = this.$refs.squirebox
|
||||
this.$router.go(-1)
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
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(){
|
||||
//
|
||||
@@ -189,59 +172,55 @@ const SquireButtonFunctions = {
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = this.$refs.squirebox
|
||||
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(){
|
||||
//
|
||||
@@ -249,67 +228,63 @@ const SquireButtonFunctions = {
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = this.$refs.squirebox
|
||||
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(){
|
||||
//
|
||||
@@ -317,10 +292,7 @@ const SquireButtonFunctions = {
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = this.$refs.squirebox
|
||||
|
||||
//Close menu if user is on mobile, then sort list
|
||||
this.$router.go(-1)
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
// simple function that trys to evaluate javascript
|
||||
const shittyMath = (string) => {
|
||||
@@ -334,57 +306,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 = this.$refs.squirebox
|
||||
// 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 +376,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">
|
||||
@@ -102,14 +69,13 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'attachment-display': defineAsyncComponent(() => import('@/components/AttachmentDisplayCard.vue')),
|
||||
'counter':defineAsyncComponent(() => import('@/components/AnimatedCounterComponent.vue')),
|
||||
'attachment-display': require('@/components/AttachmentDisplayCard').default,
|
||||
'counter':require('@/components/AnimatedCounterComponent.vue').default,
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
@@ -125,7 +91,7 @@ import { defineAsyncComponent } from 'vue'
|
||||
//
|
||||
// Perform Login check
|
||||
//
|
||||
this.$root.loginGateway()
|
||||
this.$parent.loginGateway()
|
||||
|
||||
},
|
||||
mounted: function(){
|
||||
@@ -133,20 +99,13 @@ import { defineAsyncComponent } from 'vue'
|
||||
//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()
|
||||
},
|
||||
beforeUnmount(){
|
||||
beforeDestroy(){
|
||||
|
||||
//Remove scroll event on destroy
|
||||
window.removeEventListener('scroll', this.onScroll)
|
||||
|
||||
this.$io.removeListener('update_note_attachments')
|
||||
},
|
||||
watch:{
|
||||
$route (to, from){
|
||||
@@ -206,12 +165,6 @@ import { defineAsyncComponent } from 'vue'
|
||||
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.$root.loginGateway()
|
||||
|
||||
},
|
||||
mounted: function(){
|
||||
this.getBookmarklet()
|
||||
},
|
||||
beforeUnmount(){
|
||||
|
||||
},
|
||||
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>
|
||||
@@ -1,298 +0,0 @@
|
||||
<style>
|
||||
.calendar {
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
display: inline-block;
|
||||
}
|
||||
.day {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--border_color);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.today {
|
||||
font-weight: bold;
|
||||
}
|
||||
.day ~ .active {
|
||||
|
||||
background-color: var(--main-accent);
|
||||
color: var(--body_bg_color);
|
||||
}
|
||||
.day ~ .has-data {
|
||||
outline: #07f4f4;
|
||||
outline-style: none;
|
||||
outline-width: medium;
|
||||
outline-style: none;
|
||||
outline-offset: -4px;
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="squire-box">
|
||||
|
||||
<h3>Cycle Tracking Beta
|
||||
<span v-on:click="deleteData()">
|
||||
<i class="clickable trash icon"></i>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<div class="calendar">
|
||||
<p>{{calendar.monthName}}, {{calendar.year}}</p>
|
||||
<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':dateCode == `${day}.${calendar.month}.${calendar.year}`,
|
||||
'has-data':cycleData[`${day}.${calendar.month}.${calendar.year}`],
|
||||
}"
|
||||
v-on:click="openDayData(`${day}.${calendar.month}.${calendar.year}`)">
|
||||
{{ day }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui form">
|
||||
<h3><input type="text" v-model="dateCode" v-on:keypress.enter="openDayData(dateCode)"></h3>
|
||||
<div class="field" v-for="field in fields">
|
||||
<label>{{ field.label }}</label>
|
||||
|
||||
<div v-for="(value, key) in openDay">
|
||||
|
||||
<!-- float -->
|
||||
<div v-if="field.type == 'float' && field.id == key" class="ui left icon input">
|
||||
<input type="text" :placeholder="field.label" v-on:keyup="e => saveField(field.id, e.target.value)" :value="value">
|
||||
<i class="thermometer half icon"></i>
|
||||
</div>
|
||||
|
||||
<!-- range -->
|
||||
<div v-if="field.type == 'range' && field.id == key">
|
||||
<div :class="{green:(value == 1)}" v-on:click="saveField(field.id, 1)" class="ui button">1</div>
|
||||
<div :class="{green:(value == 2)}" v-on:click="saveField(field.id, 2)" class="ui button">2</div>
|
||||
<div :class="{green:(value == 3)}" v-on:click="saveField(field.id, 3)" class="ui button">3</div>
|
||||
<div :class="{green:(value == 4)}" v-on:click="saveField(field.id, 4)" class="ui button">4</div>
|
||||
<div :class="{green:(value == 5)}" v-on:click="saveField(field.id, 5)" class="ui button">5</div>
|
||||
</div>
|
||||
|
||||
<!-- text area -->
|
||||
<div v-if="field.type == 'text' && field.id == key">
|
||||
<textarea rows="3" v-on:keyup="e => saveField(field.id, e.target.value)">
|
||||
{{ value }}
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="ui green save button" v-on:click="saveDayData">
|
||||
Save
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'CycleTracking',
|
||||
components: {
|
||||
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
appWorkingDate: null,
|
||||
fields:[],
|
||||
defaultFields:[
|
||||
{'type':'float','label':'Basil Temp', 'id':'BT'},
|
||||
{'type':'range','label':'Cervical Mucus', 'id':'CM'},
|
||||
{'type':'text','label':'Notes', 'id':'NO'},
|
||||
],
|
||||
cycleData: {},
|
||||
dateCode: null,
|
||||
openDay: [],
|
||||
calendar: {
|
||||
monthName: '',
|
||||
month: '',
|
||||
year: '',
|
||||
days: [],
|
||||
weekdays: ['S','M','T','W','T','F','S'],
|
||||
today: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
beforeCreate() {
|
||||
|
||||
// Perform Login check
|
||||
this.$root.loginGateway()
|
||||
|
||||
},
|
||||
mounted(){
|
||||
if(!this.$store.getters.getLoggedIn){
|
||||
return
|
||||
}
|
||||
|
||||
// setup date code
|
||||
// day - month - year
|
||||
const now = new Date()
|
||||
this.appWorkingDate = now
|
||||
const dateSetup = [
|
||||
now.getDate(), // 1-31 (Day)
|
||||
now.getMonth()+1, // 0-11 (Month)
|
||||
now.getFullYear(), // 1888-2022 (Year)
|
||||
]
|
||||
|
||||
this.dateCode = dateSetup.join('.')
|
||||
|
||||
this.setupCalendar(this.appWorkingDate)
|
||||
|
||||
this.fetchCycleData()
|
||||
},
|
||||
methods: {
|
||||
saveField(fieldId, value){
|
||||
this.openDay[fieldId] = value
|
||||
},
|
||||
openDayData(dateCode){
|
||||
|
||||
if(String(dateCode).split('.').length != 3){
|
||||
console.log('Invalid Date Code')
|
||||
return
|
||||
}
|
||||
|
||||
this.dateCode = dateCode || this.dateCode
|
||||
|
||||
let currentDay = this.cycleData[this.dateCode] || {}
|
||||
|
||||
//Set up each entry empty or with current value
|
||||
this.fields.forEach(field => {
|
||||
currentDay[field.id] = currentDay[field.id] || ''
|
||||
})
|
||||
|
||||
this.openDay = currentDay
|
||||
|
||||
},
|
||||
saveDayData(){
|
||||
|
||||
// remove empty keys
|
||||
let cleanDayData = {}
|
||||
Object.keys(this.openDay).forEach(key => {
|
||||
if(this.openDay[key] != ''){
|
||||
cleanDayData[key] = this.openDay[key]
|
||||
}
|
||||
})
|
||||
|
||||
this.cycleData[this.dateCode] = cleanDayData
|
||||
|
||||
// Update calendar
|
||||
this.setupCalendar(this.appWorkingDate)
|
||||
|
||||
this.saveCycleData()
|
||||
|
||||
},
|
||||
fetchCycleData(){
|
||||
|
||||
axios.post('/api/cycle-tracking/get')
|
||||
.then(({ data }) => {
|
||||
if(data.hasOwnProperty("text")){
|
||||
|
||||
let appData = null
|
||||
try {
|
||||
appData = JSON.parse(data.text)
|
||||
} catch(e) {
|
||||
console.log('Didnt parse json')
|
||||
}
|
||||
|
||||
console.log(appData)
|
||||
|
||||
this.cycleData = appData?.cycleData || {}
|
||||
this.fields = appData?.fields || []
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.setupFields()
|
||||
this.openDayData(this.dateCode)
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', error) })
|
||||
|
||||
},
|
||||
saveCycleData(){
|
||||
|
||||
const appData = JSON.stringify({
|
||||
fields: this.fields,
|
||||
cycleData: this.cycleData,
|
||||
})
|
||||
axios.post('/api/cycle-tracking/save', { cycleData:appData })
|
||||
.then(response => {
|
||||
{ this.$bus.$emit('notification', 'Data Saved') }
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', error) })
|
||||
},
|
||||
deleteData(){
|
||||
axios.post('/api/cycle-tracking/save', { cycleData:'' })
|
||||
.then(response => {
|
||||
{ this.$bus.$emit('notification', 'Data Deleted') }
|
||||
})
|
||||
},
|
||||
setupFields(){
|
||||
|
||||
// push the first 3 default fields to users set
|
||||
if(this.fields.length == 0){
|
||||
for (let i = 0; i < 3; i++) {
|
||||
this.fields.push(this.defaultFields[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
setupCalendar(date){
|
||||
// ------------
|
||||
// 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.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
|
||||
|
||||
|
||||
/*
|
||||
October 2022
|
||||
S M T W T F S
|
||||
1 2 3 4 5 6
|
||||
7 8 9
|
||||
*/
|
||||
|
||||
// -------
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -6,11 +6,11 @@
|
||||
Help
|
||||
</h3>
|
||||
|
||||
<p><u>Encrypted Data</u><br></p><blockquote><p>Note text, note title and the note search index is fully encrypted. <br></p><p><br></p><p>Encryption is the transformation of data into a form unreadable by anyone without the password. Its purpose is to ensure privacy by keeping the information hidden from anyone for whom it is not intended, even those who can see the encrypted data. If every note in the database was leaked, nothing would be readable. If the government asked for your notes, it would all be gibberish. <br></p></blockquote><p><br></p><p><u>Some Data is not encrypted</u><br></p><blockquote><p>To keep up ease of use, your Username, Files, Tags, Attachments and attachment search text is not encrypted.<br></p></blockquote><p><br></p><p><u>Searching</u><br></p><blockquote><p>Note text searching is limited to key words.<br></p><p>To allow for higher security, the complexity of the search index has been reduced. To make sure that every word of every note is fully encrypted, the search index is encrypted as well. <br></p></blockquote><p><br></p><p><u>Files</u><br></p><blockquote><p>The files section will show files that have been uploaded to notes or web pages that have been scraped from notes.<br></p><p><br></p><p>The text next to each file can be modified and found later with the search bar. <br></p><p>Files are limited to 5MB in size. <br></p><p><br></p><p>Files can be uploaded to notes. If its an image, the picture will be put into the note.<br></p><p>Images added to notes will have text scanned, so it can be searched (This isn't super accurate so don't rely to heavily on it.) The text can be updated at any time and searched later.<br></p></blockquote><p><br></p><p><u>Scratch Pad</u><br></p><blockquote><p>The Scratch Pad was designed to allow rapid input to a single note. Rather than junking up all your notes with random links, numbers or haikus, you can put them all in one place. <br></p></blockquote><p><br></p><p><u>Dark Theme</u><br></p><blockquote><p>Dark theme limits the amount of blue emitted by your screen. Less blue light at night is supposed to be helpful for falling asleep.<br></p><p>Here is some good research on the topic: <a href="https://justgetflux.com/research.html">https://justgetflux.com/research.html</a><br></p></blockquote><p><br></p><p><u>Text Editor Keyboard Shortcuts</u><br></p><blockquote><ul><li><b>Number List</b> - CTRL + SHIFT + 9<br></li><li><b>Todo List</b> - CTRL + SHIFT + 8<br></li><li><b>Underline</b> - CTRL + u<br></li><li><b>Bold</b> - CTRL + b<br></li><li><b>Quote</b> - CRTL + i<br></li><li><b>Undo</b> - CTRL + z<br></li><li><b>Redo</b> - CTRL + y<br></li><li><b>Indent</b> - TAB<br></li><li><b>Un-Indent</b> - SHIFT + TAB<br></li><li><b>Manual Save</b> - CTRL + S<br></li><li><b>Close Note</b> - ESC<br></li></ul></blockquote><p><br></p><p><u>Shared Notes</u><br></p><blockquote><p>Shared notes still respect privacy but use a different security scheme. Instead of encrypting the note with your password, a shared password is created. This note then uses <a href="https://en.wikipedia.org/wiki/Public-key_cryptography">public-key cryptography</a> to share the note with other users, while still making it unreadable to anyone else. <br></p><ul><li>You can revoke any users access to a shared note at any time.<br></li><li>Notes you share can not be shared by other people.<br></li><li>If you turn off sharing, all users lose access and the note is reverted to the default security scheme.<br></li></ul><p><br></p><p>It is possible to generate a public URL for shared notes. This exposes the Encryption Key for the note. Anyone with the key to the note can read that note. The encryption key to a shared note can easily be changed by turning off sharing, then turning it back on. <br></p><p><u><br></u></p></blockquote><p><u>Links in notes</u><br></p><blockquote><p>Links put into notes are automatically scraped. This means the data from the link will be scanned to get an image and some text from the website to help make that link more accessible in the future. <br></p><p>You can edit the text of scarped links and any time and search for it later. <br></p></blockquote><p><br></p><p><u>Deleting Notes</u><br></p><blockquote><p>Deleting a note from the trash erases all note text and the note entry. It can not be recovered.<br></p><p>None of the files related to the note are deleted. They can be manually removed at any time.<br></p></blockquote><p><br></p><p><u>Daily Backups</u><br></p><blockquote><p>All notes are backed up, every night, at midnight. If there is data loss, it can be restored from a backup. If you experience some sort of cataclysmic data loss please contact the system administrator for a copy of your data or a restoration procedure. <br></p><p>All data in the backups remains encrypted and unreadable.<br></p></blockquote><p><br></p><p><u>Two Factor Authentication (2FA)</u><br></p><blockquote><p>Two factor authentication is an additional layer of security on top of your regLine truncated
|
||||
<p><u>Encrypted Data</u><br></p><blockquote><p>Note text, note title and the note search index is fully encrypted. <br></p><p><br></p><p>Encryption is the transformation of data into a form unreadable by anyone without the password. Its purpose is to ensure privacy by keeping the information hidden from anyone for whom it is not intended, even those who can see the encrypted data. If every note in the database was leaked, nothing would be readable. If the government asked for your notes, it would all be gibberish. <br></p></blockquote><p><br></p><p><u>Some Data is not encrypted</u><br></p><blockquote><p>To keep up ease of use, your Username, Files, Tags, Attachments and attachment search text is not encrypted.<br></p></blockquote><p><br></p><p><u>Searching</u><br></p><blockquote><p>Note text searching is limited to key words.<br></p><p>To allow for higher security, the complexity of the search index has been reduced. To make sure that every word of every note is fully encrypted, the search index is encrypted as well. <br></p></blockquote><p><br></p><p><u>Files</u><br></p><blockquote><p>The files section will show files that have been uploaded to notes or web pages that have been scraped from notes.<br></p><p><br></p><p>The text next to each file can be modified and found later with the search bar. <br></p><p>Files are limited to 5MB in size. <br></p><p><br></p><p>Files can be uploaded to notes. If its an image, the picture will be put into the note.<br></p><p>Images added to notes will have text scanned, so it can be searched (This isn't super accurate so don't rely to heavily on it.) The text can be updated at any time and searched later.<br></p></blockquote><p><br></p><p><u>Scratch Pad</u><br></p><blockquote><p>The Scratch Pad was designed to allow rapid input to a single note. Rather than junking up all your notes with random links, numbers or haikus, you can put them all in one place. <br></p></blockquote><p><br></p><p><u>Flux Theme</u><br></p><blockquote><p>Flux theme limits the amount of blue emitted by your screen. Most things turn sepia and a filter is applied to images to make them more sepia. Less blue light at night is supposed to be helpful for falling asleep.<br></p><p>Here is some good research on the topic: <a href="https://justgetflux.com/research.html">https://justgetflux.com/research.html</a><br></p></blockquote><p><br></p><p><u>Text Editor Keyboard Shortcuts</u><br></p><blockquote><ul><li><b>Number List</b> - CTRL + SHIFT + 9<br></li><li><b>Todo List</b> - CTRL + SHIFT + 8<br></li><li><b>Underline</b> - CTRL + u<br></li><li><b>Bold</b> - CTRL + b<br></li><li><b>Quote</b> - CRTL + i<br></li><li><b>Undo</b> - CTRL + z<br></li><li><b>Redo</b> - CTRL + y<br></li><li><b>Indent</b> - TAB<br></li><li><b>Un-Indent</b> - SHIFT + TAB<br></li><li><b>Manual Save</b> - CTRL + S<br></li><li><b>Close Note</b> - ESC<br></li></ul></blockquote><p><br></p><p><u>Shared Notes</u><br></p><blockquote><p>Shared notes still respect privacy but use a different security scheme. Instead of encrypting the note with your password, a shared password is created. This note then uses <a href="https://en.wikipedia.org/wiki/Public-key_cryptography">public-key cryptography</a> to share the note with other users, while still making it unreadable to anyone else. <br></p><ul><li>You can revoke any users access to a shared note at any time.<br></li><li>Notes you share can not be shared by other people.<br></li><li>If you turn off sharing, all users lose access and the note is reverted to the default security scheme.<br></li></ul><p><br></p><p>It is possible to generate a public URL for shared notes. This exposes the Encryption Key for the note. Anyone with the key to the note can read that note. The encryption key to a shared note can easily be changed by turning off sharing, then turning it back on. <br></p><p><u><br></u></p></blockquote><p><u>Links in notes</u><br></p><blockquote><p>Links put into notes are automatically scraped. This means the data from the link will be scanned to get an image and some text from the website to help make that link more accessible in the future. <br></p><p>You can edit the text of scarped links and any time and search for it later. <br></p></blockquote><p><br></p><p><u>Deleting Notes</u><br></p><blockquote><p>Deleting a note from the trash erases all note text and the note entry. It can not be recovered.<br></p><p>None of the files related to the note are deleted. They can be manually removed at any time.<br></p></blockquote><p><br></p><p><u>Daily Backups</u><br></p><blockquote><p>All notes are backed up, every night, at midnight. If there is data loss, it can be restored from a backup. If you experience some sort of cataclysmic data loss please contact the system administrator for a copy of your data or a restoration procedure. <br></p><p>All data in the backups remains encrypted and unreadable.<br></p></blockquote><p><br></p><p><u>Two Factor Authentication (2FA)</u><br></p><blockquote>Line truncated
|
||||
|
||||
<div class="ui grid">
|
||||
<div class="center aligned sixteen wide column">
|
||||
<router-link to="/terms">Solid Scribe Terms of Use</router-link>
|
||||
<router-link to="/terms"></i>Solid Scribe Terms of Use</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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="$root.loggedIn">
|
||||
<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>
|
||||
@@ -184,7 +152,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Small login form -->
|
||||
<div class="row" v-if="!$root.loggedIn">
|
||||
<div class="row" v-if="!$parent.loggedIn">
|
||||
<div class="sixteen wide middle algined column">
|
||||
<div class="ui text container">
|
||||
<h2>
|
||||
@@ -199,33 +167,13 @@
|
||||
<!-- Overview -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2 class="ui dividing header">Powerful text editing and privacy</h2>
|
||||
<h2>Powerful text editing and privacy</h2>
|
||||
<h3>Easily edit, share and organize thousands of notes.</h3>
|
||||
<h3>Feel safe knowing no one can read your notes but you.</h3>
|
||||
<!-- <h3>Tools to organize and collaborate on thousands of notes while maintaining security and respecting your privacy.</h3> -->
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<svg-displayer file="idea" alt="Explosion of New Ideas" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- theme selector -->
|
||||
<div class="ui white row">
|
||||
<div class="sixteen wide middle aligned column">
|
||||
<div class="ui container">
|
||||
<h2 style="color: var(--main-accent);">
|
||||
Pick your theme
|
||||
</h2>
|
||||
<h3 v-if="$root.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>
|
||||
|
||||
@@ -235,42 +183,42 @@
|
||||
<!-- note features -->
|
||||
<div class="six wide column">
|
||||
|
||||
<h1 class="ui center aligned dividing header"><i class="small green sliders horizontal icon"></i>App Features</h1>
|
||||
<h1 class="ui center aligned header"><i class="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>
|
||||
Create a million notes!
|
||||
Create as many notes as you want
|
||||
<div class="sub header">Create unlimited notes up to 5,000,000 characters long.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui header">
|
||||
<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 search or sort by tag.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui header">
|
||||
<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>
|
||||
<div class="sub header">Easily search all notes, files, links and tags.</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,7 +229,7 @@
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui header">
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey cloud moon icon"></i>
|
||||
@@ -295,8 +243,8 @@
|
||||
|
||||
<!-- 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">
|
||||
<h1 class="ui center aligned header"><i class="sliders horizontal icon"></i>Editing Features</h1>
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey list icon"></i>
|
||||
@@ -306,7 +254,7 @@
|
||||
<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">
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey file icon"></i>
|
||||
@@ -316,7 +264,7 @@
|
||||
<div class="sub header">Bold, Underline, Title, Add Links, Add Tables, Color Text, Color Background and more.</div>
|
||||
</div>
|
||||
</h2>
|
||||
<h2 class="ui header">
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey file icon"></i>
|
||||
@@ -326,7 +274,7 @@
|
||||
<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">
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey images icon"></i>
|
||||
@@ -336,7 +284,7 @@
|
||||
<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">
|
||||
<h2 class="ui dividing header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey users icon"></i>
|
||||
@@ -353,38 +301,38 @@
|
||||
<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">
|
||||
<h1 class="ui center aligned header"><i class="sliders horizontal icon"></i>Privacy Features</h1>
|
||||
<h2 class="ui dividing 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
|
||||
All Note Text is Encrypted
|
||||
<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 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>
|
||||
Note Search is Encrypted
|
||||
<div class="sub header">Easily search the contents of all your notes without compromising security.</div>
|
||||
</div>
|
||||
</h2>
|
||||
<h2 class="ui header">
|
||||
<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>
|
||||
Encrypted Sharing
|
||||
Encrypted Note 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>
|
||||
@@ -397,7 +345,7 @@
|
||||
</div>
|
||||
|
||||
<div class="six wide column">
|
||||
<svg-displayer file="onboarding" alt="Observe this chart" />
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -405,7 +353,8 @@
|
||||
|
||||
<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 +368,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>
|
||||
@@ -444,7 +393,7 @@
|
||||
<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 +442,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 +450,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:[email protected]">[email protected]</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,12 +461,12 @@
|
||||
<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>
|
||||
<router-link to="/terms"></i>Solid Scribe Terms of Use</router-link>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -532,80 +475,28 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
export default {
|
||||
name: 'WelcomePage',
|
||||
components: {
|
||||
'login-form':defineAsyncComponent(() => import('@/components/LoginFormComponent.vue')),
|
||||
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||
'svg-displayer':defineAsyncComponent(() => import('@/components/SvgDisplayer.vue')),
|
||||
'login-form':require('@/components/LoginFormComponent.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(){
|
||||
//Force HTTPS on prod, always. Dev doesn't have certs
|
||||
// const isDev = process.env['NODE_ENV'] == 'development'
|
||||
// if (!isDev && location.protocol != 'https:'){
|
||||
// window.location.replace('https://www.solidscribe.com')
|
||||
// }
|
||||
const isDev = process.env['NODE_ENV'] == 'development'
|
||||
if (!isDev && location.protocol != 'https:'){
|
||||
window.location.replace('https://www.solidscribe.com')
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
},
|
||||
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(){
|
||||
|
||||
|
||||
|
||||
@@ -33,14 +33,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
components: {
|
||||
'login-form':defineAsyncComponent(() => import('@/components/LoginFormComponent.vue')),
|
||||
'login-form':require('@/components/LoginFormComponent.vue').default,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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"
|
||||
@@ -29,13 +23,14 @@
|
||||
</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,59 +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 -->
|
||||
<template v-for="section,index in noteSections" :key="index">
|
||||
<div 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>
|
||||
</template>
|
||||
|
||||
|
||||
<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>
|
||||
@@ -156,46 +93,72 @@
|
||||
/>
|
||||
</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">
|
||||
<template v-for="noteId in openNotes" :key="noteId">
|
||||
<note-input-panel
|
||||
v-if="noteId != null"
|
||||
:noteid="noteId"
|
||||
:url-data="$route.params"
|
||||
:open-notes="openNotes.length"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<note-input-panel
|
||||
v-if="activeNoteId1 != null"
|
||||
:key="activeNoteId1"
|
||||
:noteid="activeNoteId1"
|
||||
:url-data="$route.params"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'NotesPage',
|
||||
name: 'SearchBar',
|
||||
components: {
|
||||
|
||||
'note-input-panel': defineAsyncComponent(() => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue')),
|
||||
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
|
||||
|
||||
'note-title-display-card': defineAsyncComponent(() => import('@/components/NoteTitleDisplayCard.vue')),
|
||||
// 'fast-filters': defineAsyncComponent(() => import('@/components/FastFilters.vue')),
|
||||
'search-input': defineAsyncComponent(() => import('@/components/SearchInput.vue')),
|
||||
'attachment-display': defineAsyncComponent(() => import('@/components/AttachmentDisplayCard.vue')),
|
||||
'tag-display':defineAsyncComponent(() => import('@/components/TagDisplayComponent.vue')),
|
||||
'loading-icon':defineAsyncComponent(() => import('@/components/LoadingIconComponent.vue')),
|
||||
'paste-button':defineAsyncComponent(() => import('@/components/PasteButton.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 {
|
||||
@@ -205,11 +168,6 @@ import { defineAsyncComponent } from 'vue'
|
||||
searchResultsCount: 0,
|
||||
searchTags: [],
|
||||
notes: [],
|
||||
openNotes: [],
|
||||
// fullPath of our own last URL write, so the $route watcher can
|
||||
// recognise it and not treat it as an inbound navigation
|
||||
lastSyncedFullPath: null,
|
||||
collapseFloatingList: false,
|
||||
highlights: [],
|
||||
searchDebounce: null,
|
||||
fastFilters: {},
|
||||
@@ -217,10 +175,10 @@ import { defineAsyncComponent } from 'vue'
|
||||
|
||||
//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
|
||||
@@ -264,41 +222,39 @@ import { defineAsyncComponent } from 'vue'
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
this.$root.loginGateway()
|
||||
|
||||
//If user is on title view,
|
||||
this.titleView = this.$store.getters.getIsUserOnMobile
|
||||
this.$parent.loginGateway()
|
||||
|
||||
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
|
||||
@@ -306,7 +262,19 @@ import { defineAsyncComponent } from 'vue'
|
||||
|
||||
//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) => {
|
||||
@@ -353,13 +321,11 @@ import { defineAsyncComponent } from 'vue'
|
||||
|
||||
//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
|
||||
@@ -369,7 +335,7 @@ import { defineAsyncComponent } from 'vue'
|
||||
// document.addEventListener('visibilitychange', this.visibiltyChangeAction);
|
||||
|
||||
},
|
||||
beforeUnmount(){
|
||||
beforeDestroy(){
|
||||
window.removeEventListener('scroll', this.onScroll)
|
||||
// document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
|
||||
|
||||
@@ -386,202 +352,44 @@ import { defineAsyncComponent } from 'vue'
|
||||
},
|
||||
mounted() {
|
||||
|
||||
// Restore every note named in ?open=
|
||||
const ids = this.parseOpenQuery(this.$route.query.open)
|
||||
|
||||
// Fold the legacy /notes/open/:id deep link in, first position
|
||||
const paramId = parseInt(this.$route.params.id, 10)
|
||||
if(Number.isInteger(paramId) && paramId > 1 && !ids.includes(paramId)){
|
||||
ids.unshift(paramId)
|
||||
//Open note on load if ID is set
|
||||
if(this.$route.params.id > 1){
|
||||
this.activeNoteId1 = this.$route.params.id
|
||||
}
|
||||
|
||||
// Push directly rather than via openNote(), which would write the URL
|
||||
// once per note; one normalising write follows instead.
|
||||
ids.forEach(id => {
|
||||
if(!this.openNotes.includes(id)){
|
||||
this.openNotes.push(id)
|
||||
}
|
||||
})
|
||||
|
||||
// Unconditional, so a junk ?open=abc gets scrubbed too
|
||||
this.syncOpenNotesToUrl()
|
||||
|
||||
//Loads initial batch and tags
|
||||
this.reset()
|
||||
|
||||
},
|
||||
watch: {
|
||||
'$route.params.id': function(id){
|
||||
this.openNote(id)
|
||||
},
|
||||
'$route' (to, from) {
|
||||
|
||||
// Our own URL write, not an inbound navigation
|
||||
if(to.fullPath === this.lastSyncedFullPath){ return }
|
||||
|
||||
// The discriminator is the absence of ?open=, NOT the path. A menu's
|
||||
// $router.go(-1) lands on /notes?open=5 with `from` containing
|
||||
// '/notes/open/', so a path comparison here would close every note
|
||||
// every time a side menu is dismissed.
|
||||
const landingOnBareNotes = (to.path === '/notes' && !to.query.open)
|
||||
|
||||
// Reload the notes if arriving at an empty /notes from elsewhere
|
||||
if(landingOnBareNotes && !from.path.startsWith('/notes/open/') && from.path !== '/notes'){
|
||||
this.reset()
|
||||
}
|
||||
|
||||
// Explicit navigation to a note-less /notes closes everything
|
||||
if(landingOnBareNotes && this.openNotes.length > 0){
|
||||
this.closeAllNotes()
|
||||
}
|
||||
|
||||
// Stale history entry (e.g. go(-1) after another note was closed):
|
||||
// rewrite the URL from the array. Never read it.
|
||||
if(to.path === '/notes' && to.query.open && to.query.open !== this.openNotes.join(',')){
|
||||
this.syncOpenNotesToUrl()
|
||||
}
|
||||
|
||||
//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: {
|
||||
// Where the open-note URL should point. A menu or a tag search owns the
|
||||
// path, so only a plain notes view gets normalised back to /notes.
|
||||
openNotesTargetPath(){
|
||||
|
||||
// A side menu is open; its close button is a $router.go(-1), so the
|
||||
// path has to stay put or there is nowhere sane to land.
|
||||
if(this.$route.params.openMenu){ return this.$route.path }
|
||||
|
||||
// Preserve a tag search
|
||||
if(this.$route.params.tag){ return this.$route.path }
|
||||
|
||||
return '/notes'
|
||||
},
|
||||
// Mirror openNotes into ?open=. This is the ONLY place the open-note
|
||||
// URL is written.
|
||||
syncOpenNotesToUrl(){
|
||||
|
||||
const query = Object.assign({}, this.$route.query)
|
||||
|
||||
if(this.openNotes.length > 0){
|
||||
query.open = this.openNotes.join(',')
|
||||
}
|
||||
else {
|
||||
delete query.open
|
||||
}
|
||||
|
||||
const target = { path: this.openNotesTargetPath(), query }
|
||||
const targetFullPath = this.$router.resolve(target).fullPath
|
||||
|
||||
// Already correct -- don't navigate at all
|
||||
if(targetFullPath === this.$route.fullPath){
|
||||
this.lastSyncedFullPath = targetFullPath
|
||||
return
|
||||
}
|
||||
|
||||
// Set before navigating so the watcher sees it
|
||||
this.lastSyncedFullPath = targetFullPath
|
||||
|
||||
this.$router.replace(target).catch(() => {})
|
||||
},
|
||||
// Tolerant parse of ?open=. Handles 'abc', '1,,2,2', '-4' and the
|
||||
// repeated form ?open=1&open=2 (which arrives as an array).
|
||||
parseOpenQuery(raw){
|
||||
|
||||
if(raw === undefined || raw === null){ return [] }
|
||||
|
||||
// Cap: a hand-crafted URL shouldn't be able to fire 50 note fetches
|
||||
const MAX_RESTORE = 8
|
||||
|
||||
return [].concat(raw)
|
||||
.join(',')
|
||||
.split(',')
|
||||
.map(value => parseInt(value, 10))
|
||||
.filter(value => Number.isInteger(value) && value > 0)
|
||||
.filter((value, index, all) => all.indexOf(value) === index)
|
||||
.slice(0, MAX_RESTORE)
|
||||
//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, 10)
|
||||
|
||||
// Also absorbs the undefined the '$route.params.id' watcher now gets
|
||||
// when we normalise /notes/open/5 into /notes?open=5
|
||||
if(!Number.isInteger(intId) || intId < 1){ 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 }
|
||||
}
|
||||
|
||||
if(this.openNotes.includes(intId)){
|
||||
// Already open, but an external push('/notes/open/5') still needs
|
||||
// the URL normalised back to the full set
|
||||
this.syncOpenNotesToUrl()
|
||||
return
|
||||
}
|
||||
|
||||
this.openNotes.push(intId)
|
||||
this.syncOpenNotesToUrl()
|
||||
},
|
||||
closeNote(noteId, modified, syncUrl = true){
|
||||
|
||||
// parseInt: indexOf is strict, and ids arriving from the URL are
|
||||
// strings. Without this the splice silently misses.
|
||||
const intId = parseInt(noteId, 10)
|
||||
const openIndex = this.openNotes.indexOf(intId)
|
||||
const didClose = (openIndex !== -1)
|
||||
|
||||
if(didClose){
|
||||
this.openNotes.splice(openIndex, 1)
|
||||
}
|
||||
|
||||
// Only write the URL when the array actually changed. NoteInputPanel
|
||||
// emits close_active_note twice per note (closeButtonAction -> close(),
|
||||
// then beforeUnmount -> close()); the second arrives after the splice.
|
||||
if(didClose && syncUrl){
|
||||
this.syncOpenNotesToUrl()
|
||||
}
|
||||
|
||||
if(modified){
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
//Focus and animate if modified
|
||||
this.updateSingleNote(noteId, modified)
|
||||
}
|
||||
},
|
||||
closeAllNotes(){
|
||||
|
||||
// Backwards, so the in-loop splice can't skip an entry. URL writes are
|
||||
// deferred so this is one navigation rather than N.
|
||||
for (let i = this.openNotes.length - 1; i >= 0; i--) {
|
||||
this.closeNote(this.openNotes[i], false, false)
|
||||
}
|
||||
|
||||
this.syncOpenNotesToUrl()
|
||||
//Open note if a link was not clicked
|
||||
this.$router.push('/notes/open/'+id)
|
||||
return
|
||||
},
|
||||
toggleTagFilter(tagId){
|
||||
|
||||
@@ -599,10 +407,6 @@ import { defineAsyncComponent } from 'vue'
|
||||
},
|
||||
onScroll(e){
|
||||
|
||||
if(!this.scrollLoadEnabled){
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(this.loadingBatchTimeout)
|
||||
this.loadingBatchTimeout = setTimeout(() => {
|
||||
|
||||
@@ -612,12 +416,12 @@ import { defineAsyncComponent } from 'vue'
|
||||
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
|
||||
@@ -638,24 +442,21 @@ import { defineAsyncComponent } from 'vue'
|
||||
}
|
||||
|
||||
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 = {
|
||||
@@ -677,7 +478,6 @@ import { defineAsyncComponent } from 'vue'
|
||||
return
|
||||
}
|
||||
|
||||
// if old note data and new note data exists
|
||||
if(note && newNote){
|
||||
|
||||
//go through each prop and update it with new values
|
||||
@@ -686,7 +486,7 @@ import { defineAsyncComponent } from 'vue'
|
||||
})
|
||||
|
||||
//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 => {
|
||||
@@ -700,9 +500,6 @@ import { defineAsyncComponent } from 'vue'
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if( focuseAndAnimate ){
|
||||
this.$nextTick( () => {
|
||||
//Trigger close animation on note
|
||||
this.$refs['note-'+noteId][0].justClosed()
|
||||
@@ -745,14 +542,19 @@ import { defineAsyncComponent } from 'vue'
|
||||
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
|
||||
@@ -780,46 +582,28 @@ import { defineAsyncComponent } from 'vue'
|
||||
}
|
||||
|
||||
//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 => {
|
||||
console.error('[NotesPage] search failed:', error)
|
||||
this.$bus.$emit('notification', 'Failed to Search Notes')
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
|
||||
})
|
||||
},
|
||||
rebuildNoteCategorise(){
|
||||
@@ -931,7 +715,7 @@ import { defineAsyncComponent } from 'vue'
|
||||
//clear out tags
|
||||
this.searchTags = []
|
||||
this.tagSuggestions = []
|
||||
this.showLoading = false
|
||||
this.loadingInProgress = false
|
||||
this.searchTerm = ''
|
||||
this.$bus.$emit('reset_fast_filters') //Clear out search
|
||||
|
||||
@@ -948,32 +732,15 @@ import { defineAsyncComponent } from 'vue'
|
||||
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;
|
||||
}
|
||||
@@ -991,150 +758,4 @@ import { defineAsyncComponent } from 'vue'
|
||||
.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,764 +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 -->
|
||||
<template v-for="section,index in noteSections" :key="index">
|
||||
<div 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>
|
||||
</template>
|
||||
|
||||
|
||||
<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 { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'SearchBar',
|
||||
components: {
|
||||
|
||||
'note-input-panel': defineAsyncComponent(() => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue')),
|
||||
|
||||
'note-title-display-card': defineAsyncComponent(() => import('@/components/NoteTitleDisplayCard.vue')),
|
||||
// 'fast-filters': defineAsyncComponent(() => import('@/components/FastFilters.vue')),
|
||||
'search-input': defineAsyncComponent(() => import('@/components/SearchInput.vue')),
|
||||
'attachment-display': defineAsyncComponent(() => import('@/components/AttachmentDisplayCard.vue')),
|
||||
'counter':defineAsyncComponent(() => import('@/components/AnimatedCounterComponent.vue')),
|
||||
'tag-display':defineAsyncComponent(() => import('@/components/TagDisplayComponent.vue')),
|
||||
'loading-icon':defineAsyncComponent(() => import('@/components/LoadingIconComponent.vue')),
|
||||
},
|
||||
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.$root.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);
|
||||
|
||||
},
|
||||
beforeUnmount(){
|
||||
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>
|
||||
@@ -86,7 +86,7 @@
|
||||
//
|
||||
// Perform Login check
|
||||
//
|
||||
this.$root.loginGateway()
|
||||
this.$parent.loginGateway()
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
|
||||
@@ -44,48 +44,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App / PWA -->
|
||||
<h4 class="ui header">
|
||||
App
|
||||
</h4>
|
||||
<div class="ui segment">
|
||||
<div class="ui doubling grid">
|
||||
<div class="sixteen wide column">
|
||||
|
||||
<p>
|
||||
Which page the installed app opens to. Set automatically to
|
||||
whichever page you installed from, and changeable here.
|
||||
</p>
|
||||
<div
|
||||
v-for="option in launchOptions"
|
||||
:key="option.path"
|
||||
class="ui compact basic button"
|
||||
:class="{'green':(launchPage == option.path)}"
|
||||
v-on:click="chooseLaunchPage(option.path)">
|
||||
{{ option.label }}
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<p>
|
||||
If the app gets stuck on an old version, this clears the stored
|
||||
copies of its files and reloads. Your notes are not affected.
|
||||
</p>
|
||||
<div class="ui compact basic button" v-on:click="clearAppCache()">
|
||||
<i class="redo icon"></i>
|
||||
Reset App Cache
|
||||
</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>
|
||||
<p>1. Enter Password and get QR</p>
|
||||
<div class="ui fluid action input">
|
||||
<input type="password" placeholder="Current Password" v-model="password">
|
||||
|
||||
@@ -98,12 +62,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<div class="ui tiny dividing header">2. Scan QR Code</div>
|
||||
<p>2. Scan QR Code</p>
|
||||
<p v-if="qrCode == ''">(QR Code will appear here)</p>
|
||||
<img v-if="qrCode != ''" :src="qrCode" class="ui image" alt="QR Code">
|
||||
<img v-if="qrCode != ''" :src="qrCode" alt="QR Code">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<div class="ui tiny dividing header">3. Verify with code</div>
|
||||
<p>3. Verify with code</p>
|
||||
<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">
|
||||
@@ -188,7 +152,7 @@
|
||||
|
||||
<div class="ui grid">
|
||||
<div class="center aligned sixteen wide column">
|
||||
<router-link to="/terms">Solid Scribe Terms of Use</router-link>
|
||||
<router-link to="/terms"></i>Solid Scribe Terms of Use</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -197,17 +161,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import { LAUNCH_OPTIONS, getLaunchPage, setLaunchPage } from '@/LaunchPage'
|
||||
import { resetAppCache } from '@/registerServiceWorker'
|
||||
|
||||
export default {
|
||||
name: 'SettingsPage',
|
||||
components: {
|
||||
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||
'logo':require('@/components/LogoComponent.vue').default,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -216,8 +176,6 @@ import { defineAsyncComponent } from 'vue'
|
||||
verificationToken: '',
|
||||
showNewNoteConfirm: false,
|
||||
|
||||
launchOptions: LAUNCH_OPTIONS,
|
||||
launchPage: getLaunchPage(),
|
||||
themeColors: [
|
||||
'#21BA45', //Green
|
||||
'#b5cc18', //Lime
|
||||
@@ -239,14 +197,6 @@ import { defineAsyncComponent } from 'vue'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
chooseLaunchPage(path){
|
||||
setLaunchPage(path)
|
||||
this.launchPage = getLaunchPage()
|
||||
this.$bus.$emit('notification', 'App will open to ' + path)
|
||||
},
|
||||
clearAppCache(){
|
||||
resetAppCache()
|
||||
},
|
||||
newQuickNote(){
|
||||
|
||||
this.showNewNoteConfirm = true
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<div class="ui container share-target">
|
||||
|
||||
<div class="ui basic center aligned segment">
|
||||
|
||||
<div v-if="status === 'working'">
|
||||
<loading-icon message="Saving to Scratch Pad" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="status === 'error'">
|
||||
<h3 class="ui header">
|
||||
<i class="exclamation triangle icon"></i>
|
||||
Couldn't save that
|
||||
</h3>
|
||||
<p>You may be offline. Your text is safe below — retry when you have a connection.</p>
|
||||
|
||||
<textarea class="shared-text" rows="8" readonly :value="pushText"></textarea>
|
||||
|
||||
<div class="ui buttons">
|
||||
<div class="ui green button" v-on:click="save()">
|
||||
<i class="redo icon"></i> Retry
|
||||
</div>
|
||||
<div class="ui basic button" v-on:click="discard()">
|
||||
Discard
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<loading-icon message="Working" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
// Shared text is parked here when the share arrives while logged out. The
|
||||
// share's query string does not survive the bounce through /login, so a
|
||||
// redirect param alone would lose it.
|
||||
const STASH_KEY = 'pendingShareText'
|
||||
|
||||
// Guard against a pathological share wedging localStorage
|
||||
const MAX_STASH = 100 * 1024
|
||||
|
||||
export default {
|
||||
name: 'ShareTargetPage',
|
||||
components: {
|
||||
'loading-icon': defineAsyncComponent(() => import('@/components/LoadingIconComponent.vue')),
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
status: 'working',
|
||||
pushText: '',
|
||||
}
|
||||
},
|
||||
// Deliberately NOT calling $root.loginGateway() the way other pages do --
|
||||
// it would bounce away and discard the shared payload before we read it.
|
||||
mounted(){
|
||||
|
||||
const text = this.composeSharedText()
|
||||
|
||||
if(!text){
|
||||
this.$router.replace('/quick')
|
||||
return
|
||||
}
|
||||
|
||||
this.pushText = text
|
||||
|
||||
// getLoggedIn is reliable here: App.vue's beforeCreate populates the
|
||||
// username from localStorage before any route component mounts.
|
||||
if(!this.$store.getters.getLoggedIn){
|
||||
this.stash(text)
|
||||
this.$router.replace({ path: '/login', query: { redirect: '/share-target' } })
|
||||
return
|
||||
}
|
||||
|
||||
this.save()
|
||||
},
|
||||
methods: {
|
||||
// Android routinely duplicates content across these fields -- the URL
|
||||
// lands in both `text` and `url`, the page title in both `title` and
|
||||
// `text`. Without the dedupe every shared link is saved twice.
|
||||
composeSharedText(){
|
||||
|
||||
const query = this.$route.query
|
||||
|
||||
const parts = ['title', 'text', 'url']
|
||||
.map(key => String(query[key] ?? '').trim())
|
||||
.filter(value => value.length > 0)
|
||||
.filter((value, index, all) => all.indexOf(value) === index)
|
||||
|
||||
if(parts.length > 0){ return parts.join('\n') }
|
||||
|
||||
// Nothing in the URL: this is the replay after logging in
|
||||
return localStorage.getItem(STASH_KEY) || ''
|
||||
},
|
||||
stash(text){
|
||||
if(text.length > MAX_STASH){ return }
|
||||
localStorage.setItem(STASH_KEY, text)
|
||||
},
|
||||
save(){
|
||||
|
||||
this.status = 'working'
|
||||
|
||||
// Same call the paste button makes. The server prepends to the
|
||||
// scratch pad, strips HTML and auto-links URLs, so nothing needs
|
||||
// formatting here.
|
||||
axios.post('/api/quick-note/update', { 'pushText': this.pushText })
|
||||
.then(() => {
|
||||
localStorage.removeItem(STASH_KEY)
|
||||
this.$bus.$emit('notification', 'Saved To Scratch Pad')
|
||||
this.$router.replace('/quick')
|
||||
})
|
||||
.catch(error => {
|
||||
// Keep the stash: offline is the likely cause and the service
|
||||
// worker means the app boots without a connection.
|
||||
console.error('[ShareTarget] save failed:', error)
|
||||
this.stash(this.pushText)
|
||||
this.status = 'error'
|
||||
})
|
||||
},
|
||||
discard(){
|
||||
localStorage.removeItem(STASH_KEY)
|
||||
this.$router.replace('/quick')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.share-target {
|
||||
padding-top: 3em;
|
||||
}
|
||||
.shared-text {
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
padding: 0.75em;
|
||||
border: 1px solid var(--border_color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--small_element_bg_color);
|
||||
color: var(--text_color);
|
||||
font-family: inherit;
|
||||
}
|
||||
</style>
|
||||
@@ -1,48 +0,0 @@
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import EventBus from './EventBus'
|
||||
|
||||
// How often to ask the server whether a new build exists
|
||||
const UPDATE_INTERVAL = 60 * 60 * 1000
|
||||
|
||||
//
|
||||
// The worker precaches static build output only -- see the SECURITY INVARIANT
|
||||
// comment in vite.config.js. Nothing user-derived ever enters Cache Storage,
|
||||
// which is why caches are deliberately NOT cleared on logout: doing so would
|
||||
// only destroy the offline shell.
|
||||
//
|
||||
export const updateSW = registerSW({
|
||||
onNeedRefresh(){
|
||||
// A new build is waiting. Let the user apply it rather than swapping
|
||||
// assets underneath them mid-edit.
|
||||
EventBus.$emit('sw-update-available')
|
||||
},
|
||||
onOfflineReady(){
|
||||
EventBus.$emit('notification', 'Ready to work offline')
|
||||
},
|
||||
onRegisteredSW(swUrl, registration){
|
||||
if(!registration){ return }
|
||||
setInterval(() => registration.update(), UPDATE_INTERVAL)
|
||||
},
|
||||
onRegisterError(error){
|
||||
console.error('[SW] registration failed:', error)
|
||||
},
|
||||
})
|
||||
|
||||
//
|
||||
// Escape hatch. A wedged service worker is very hard to talk someone through
|
||||
// remotely, so this is surfaced as a button in Settings.
|
||||
//
|
||||
export async function resetAppCache(){
|
||||
|
||||
if('serviceWorker' in navigator){
|
||||
const registrations = await navigator.serviceWorker.getRegistrations()
|
||||
await Promise.all(registrations.map(registration => registration.unregister()))
|
||||
}
|
||||
|
||||
if('caches' in window){
|
||||
const keys = await caches.keys()
|
||||
await Promise.all(keys.map(key => caches.delete(key)))
|
||||
}
|
||||
|
||||
window.location.reload()
|
||||
}
|
||||
@@ -1,41 +1,106 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
|
||||
const HomePage = () => import('@/pages/HomePage.vue')
|
||||
const LoginPage = () => import('@/pages/LoginPage.vue')
|
||||
const HelpPage = () => import('@/pages/HelpPage.vue')
|
||||
const TermsPage = () => import('@/pages/TermsPage.vue')
|
||||
const SettingsPage = () => import('@/pages/SettingsPage.vue')
|
||||
const SharePage = () => import('@/pages/SharePage.vue')
|
||||
const NotesPage = () => import('@/pages/NotesPage.vue')
|
||||
const QuickPage = () => import('@/pages/QuickPage.vue')
|
||||
const AttachmentsPage = () => import('@/pages/AttachmentsPage.vue')
|
||||
const OverviewPage = () => import('@/pages/OverviewPage.vue')
|
||||
const BookmarkletPage = () => import('@/pages/BookmarkletPage.vue')
|
||||
const NotFoundPage = () => import('@/pages/NotFoundPage.vue')
|
||||
const MetrictrackingPage = () => import('@/pages/MetrictrackingPage.vue')
|
||||
const ShareTargetPage = () => import('@/pages/ShareTargetPage.vue')
|
||||
//Breaking components into function sections allows webpack to load them dynamically
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'HomePage', meta: { title: 'Home' }, component: HomePage },
|
||||
{ path: '/login', name: 'LoginPage', meta: { title: 'Login' }, component: LoginPage },
|
||||
{ path: '/notes', name: 'Note Page', meta: { title: 'Notes' }, component: NotesPage },
|
||||
{ path: '/notes/open/:id', name: 'Open Note', meta: { title: 'Open Note' }, component: NotesPage },
|
||||
{ path: '/search/tags/:tag', name: 'Search Notes', meta: { title: 'Search Notes' }, component: NotesPage },
|
||||
{ path: '/notes/open/:id/menu/:openMenu', name: 'Open Note Menu', meta: { title: 'Open Note Menu' }, component: NotesPage },
|
||||
{ path: '/help', name: 'Help', 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' }, component: SettingsPage },
|
||||
{ path: '/public/note/:id/:token', name: 'Share', meta: { title: 'Shared' }, component: SharePage },
|
||||
{ path: '/share-target', name: 'Share Target', meta: { title: 'Saving Share' }, component: ShareTargetPage },
|
||||
{ path: '/quick', name: 'Quick', meta: { title: 'Scratch Pad' }, component: QuickPage },
|
||||
{ path: '/attachments', name: 'Attachments', meta: { title: 'Attachments' }, component: AttachmentsPage },
|
||||
{ path: '/attachments/note/:id', name: 'Attachments for Note', meta: { title: 'Attachments for Note' }, component: AttachmentsPage },
|
||||
{ path: '/attachments/type/:type', name: 'Attachments by Type', meta: { title: 'Attachments by Type' }, component: AttachmentsPage },
|
||||
{ path: '/overview', name: 'Overview of Notes', meta: { title: 'Overview of Notes' }, component: OverviewPage },
|
||||
{ path: '/metrictrack', name: 'Metric Tracking', meta: { title: 'Metric Tracking' }, component: MetrictrackingPage },
|
||||
{ path: '/:pathMatch(.*)*', name: 'Page Not Found', meta: { title: '404 Page Not Found' }, component: NotFoundPage },
|
||||
],
|
||||
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 NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
export default new Router({
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'HomePage',
|
||||
meta: {title:'Home'},
|
||||
component: HomePage
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'LoginPage',
|
||||
meta: {title:'Login'},
|
||||
component: LoginPage
|
||||
},
|
||||
{
|
||||
path: '/notes',
|
||||
name: 'Note Page', //don't change this
|
||||
meta: {title:'Notes'},
|
||||
component: NotesPage
|
||||
},
|
||||
{
|
||||
path: '/notes/open/:id',
|
||||
name: 'Open Note',
|
||||
meta: {title: 'Open Note'},
|
||||
component: NotesPage,
|
||||
},
|
||||
{
|
||||
path: '/notes/open/:id/menu/:openMenu',
|
||||
name: 'Open Note Menu',
|
||||
meta: {title: 'Open Note Menu'},
|
||||
component: NotesPage,
|
||||
},
|
||||
{
|
||||
path: '/help',
|
||||
name: 'Help',
|
||||
meta: {title:'Help'},
|
||||
component: HelpPage
|
||||
},
|
||||
{
|
||||
path: '/terms',
|
||||
name: 'Terms',
|
||||
meta: {title:'Terms'},
|
||||
component: TermsPage
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
meta: {title:'Settings'},
|
||||
component: SettingsPage
|
||||
},
|
||||
{
|
||||
path: '/public/note/:id/:token',
|
||||
name: 'Share',
|
||||
meta: {title:'Shared'},
|
||||
component: SharePage
|
||||
},
|
||||
{
|
||||
path: '/quick',
|
||||
name: 'Quick',
|
||||
meta: {title:'Scratch Pad'},
|
||||
component: QuickPage
|
||||
},
|
||||
{
|
||||
path: '/attachments',
|
||||
name: 'Attachments',
|
||||
meta: {title:'Attachments'},
|
||||
component: AttachmentsPage
|
||||
},
|
||||
{
|
||||
path: '/attachments/note/:id',
|
||||
name: 'Attachments for Note',
|
||||
meta: {title:'Attachments for Note'},
|
||||
component: AttachmentsPage
|
||||
},
|
||||
{
|
||||
path: '/attachments/type/:type',
|
||||
name: 'Attachments by Type',
|
||||
meta: {title:'Attachments by Type'},
|
||||
component: AttachmentsPage
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
name: 'Page Not Found',
|
||||
meta: {title:'404 Page Not Found'},
|
||||
component: NotFoundPage
|
||||
},
|
||||
]
|
||||
})
|
||||
@@ -1,14 +1,15 @@
|
||||
import { createStore } from 'vuex'
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import axios from 'axios'
|
||||
|
||||
export default createStore({
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
username: null,
|
||||
nightMode: false,
|
||||
isUserOnMobile: false,
|
||||
fetchTotalsTimeout: null,
|
||||
userTotals: null, // {} // setting this to object breaks reactivity
|
||||
activeSessions: 0,
|
||||
userTotals: null,
|
||||
},
|
||||
mutations: {
|
||||
setUsername(state, username){
|
||||
@@ -23,7 +24,6 @@ export default createStore({
|
||||
localStorage.removeItem('loginToken')
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('currentVersion')
|
||||
localStorage.removeItem('snippetCache')
|
||||
delete axios.defaults.headers.common['authorizationtoken']
|
||||
state.username = null
|
||||
state.userTotals = null
|
||||
@@ -41,15 +41,23 @@ export default createStore({
|
||||
'menu-text': '#5e6268',
|
||||
},
|
||||
'black':{
|
||||
'body_bg_color': 'rgb(12 4 30)',
|
||||
//'#0f0f0f',//'#000',
|
||||
'body_bg_color': '#0f0f0f',//'#000',
|
||||
'small_element_bg_color': '#000',
|
||||
'text_color': '#FFF',
|
||||
'dark_border_color': '#555',//'#ACACAC', //Lighter color to accent elemnts user can interact with
|
||||
'border_color': '#505050',
|
||||
'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': '#555',
|
||||
'border_color': '#555',
|
||||
'menu-accent': '#626262',
|
||||
'menu-text': '#a69682',
|
||||
},
|
||||
}
|
||||
|
||||
//Catch values not in set
|
||||
@@ -98,23 +106,8 @@ export default createStore({
|
||||
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')
|
||||
@@ -134,15 +127,6 @@ export default createStore({
|
||||
// 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: {
|
||||
@@ -168,29 +152,19 @@ export default createStore({
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Tailwind 4, wired to coexist with Fomantic UI.
|
||||
*
|
||||
* Deliberately NOT `@import "tailwindcss"` (the whole thing). That pulls in
|
||||
* Preflight as `@layer base`, which zeroes margin/padding/border on every
|
||||
* element, strips heading sizes and list markers, forces img/svg to
|
||||
* display:block and makes buttons and inputs transparent. Dropped on top of
|
||||
* Fomantic it visibly breaks the existing UI.
|
||||
*
|
||||
* Utilities are imported UNLAYERED on purpose. Tailwind's own index.css wraps
|
||||
* them in `@layer utilities`, and an unlayered stylesheet beats any layered
|
||||
* rule regardless of specificity -- since all of Fomantic is unlayered, the
|
||||
* utilities would quietly lose every conflict. Unlayered they compete on normal
|
||||
* specificity instead, and win ties by source order because this file is
|
||||
* imported last in main.js.
|
||||
*
|
||||
* Fomantic mostly uses two-class selectors (.ui.button = specificity 0,2,0), so
|
||||
* it still out-specifies a single utility (0,1,0). To beat Fomantic on a
|
||||
* property it already sets, use the important modifier -- `p-4!`, `bg-red-500!`.
|
||||
*
|
||||
* To try Preflight anyway, add: @import "tailwindcss/preflight.css" layer(base);
|
||||
*/
|
||||
|
||||
@import "tailwindcss/theme.css" layer(theme);
|
||||
@import "tailwindcss/utilities.css";
|
||||
@@ -1,114 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
// Opt-in HTTPS for the dev server, off by default so the usual
|
||||
// http://localhost:5173 workflow is unchanged. Only needed if you want to test
|
||||
// the installed PWA from a phone over the LAN *without* using
|
||||
// chrome://flags/#unsafely-treat-insecure-origin-as-secure -- and note Chrome
|
||||
// needs the mkcert root CA installed on the device for the origin to be
|
||||
// service-worker eligible; clicking through a cert warning is not enough.
|
||||
// DEV_HTTPS=1 pnpm dev
|
||||
const certDir = fileURLToPath(new URL('./certs', import.meta.url))
|
||||
const keyPath = `${certDir}/192.168.1.164+4-key.pem`
|
||||
const certPath = `${certDir}/192.168.1.164+4.pem`
|
||||
const useHttps =
|
||||
process.env.DEV_HTTPS === '1' && fs.existsSync(keyPath) && fs.existsSync(certPath)
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
transformAssetUrls: { includeAbsolute: false },
|
||||
},
|
||||
}),
|
||||
tailwindcss(),
|
||||
VitePWA({
|
||||
// Updates are user-driven via the prompt in src/registerServiceWorker.js
|
||||
registerType: 'prompt',
|
||||
injectRegister: false,
|
||||
// Lands at dist/sw.js -- root of the served directory, so the worker gets
|
||||
// root scope without needing a Service-Worker-Allowed header.
|
||||
filename: 'sw.js',
|
||||
// We ship public/manifest.json by hand rather than generating one.
|
||||
manifest: false,
|
||||
workbox: {
|
||||
//
|
||||
// SECURITY INVARIANT -- do not relax without thinking it through.
|
||||
//
|
||||
// Solid Scribe is end-to-end encrypted. The service worker precaches
|
||||
// ONLY static build output and is network-only for /api/*. Caching an
|
||||
// API response would write decrypted note content into Cache Storage on
|
||||
// disk, where it survives logout and sits outside the encryption model.
|
||||
//
|
||||
// Structurally this holds because globPatterns only ever reads dist/,
|
||||
// and user content is served by Express from staticFiles/ at
|
||||
// /api/static -- which never enters dist. The NetworkOnly rule below is
|
||||
// belt-and-braces: it documents the invariant and stops a future
|
||||
// runtimeCaching entry from silently widening the net.
|
||||
//
|
||||
// woff2 is NOT in Workbox's default globPatterns. Without it the Roboto
|
||||
// files are skipped and the offline shell renders in a fallback face.
|
||||
globPatterns: ['**/*.{js,css,html,woff2,png,svg,ico,json}'],
|
||||
// Vendor chunks (fomantic, chart.js) exceed the 2MiB default. An
|
||||
// oversized file is silently OMITTED from the precache, which half
|
||||
// breaks the offline shell -- watch the build output for a warning.
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
navigateFallback: '/index.html',
|
||||
// Matters for top-level navigations to /api/static/<file> (clicking an
|
||||
// attachment link) -- without this the worker answers with index.html.
|
||||
navigateFallbackDenylist: [/^\/api\//, /^\/socket/],
|
||||
cleanupOutdatedCaches: true,
|
||||
// Let the user decide when to swap assets, rather than doing it
|
||||
// mid-session underneath them.
|
||||
clientsClaim: false,
|
||||
skipWaiting: false,
|
||||
//
|
||||
// runtimeCaching is deliberately EMPTY. Do not add an entry for /api/*
|
||||
// or /socket -- not even NetworkOnly.
|
||||
//
|
||||
// There used to be a NetworkOnly route for both, as belt-and-braces for
|
||||
// the invariant above. It was worse than useless. registerRoute() makes
|
||||
// the worker call event.respondWith() for every matching request, and
|
||||
// Socket.IO's long-poll requests are aborted as a matter of course --
|
||||
// on reconnect, on transport upgrade, and whenever Android backgrounds
|
||||
// the page. An aborted fetch inside NetworkOnly throws Workbox's
|
||||
// `no-response`, the respondWith promise rejects, and Socket.IO sees a
|
||||
// dead transport and reconnects. The installed PWA was dropping its
|
||||
// live note sync on a loop and filling the logs with
|
||||
// "FetchEvent ... resulted in a network error response".
|
||||
//
|
||||
// With no route registered the worker never intercepts these at all and
|
||||
// the browser handles them natively, exactly as in a normal tab.
|
||||
//
|
||||
// Nothing is lost on the security side: the invariant is enforced by
|
||||
// globPatterns only ever reading dist/, and by there being no *caching*
|
||||
// strategy here. NetworkOnly enforced nothing -- it only inserted the
|
||||
// worker into the hot path of every poll.
|
||||
//
|
||||
},
|
||||
// Flip to true temporarily to exercise the worker on the dev server.
|
||||
// Remember to unregister it afterwards -- a lingering dev service worker
|
||||
// is a classic "why is my phone showing an ancient build".
|
||||
devOptions: { enabled: false, type: 'module' },
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
...(useHttps
|
||||
? { https: { key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) } }
|
||||
: {}),
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3000',
|
||||
'/socket': { target: 'http://localhost:3001', ws: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,97 +0,0 @@
|
||||
#
|
||||
# Working dev server config
|
||||
#
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name 192.168.1.164;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
|
||||
server {
|
||||
|
||||
listen 443 ssl;
|
||||
|
||||
ssl_certificate /home/mab/ss/client/certs/nginx-selfsigned.crt;
|
||||
ssl_certificate_key /home/mab/ss/client/certs/nginx-selfsigned.key;
|
||||
ssl_dhparam /home/mab/ss/client/certs/dhparam.pem;
|
||||
|
||||
ssl_session_cache shared:SSL:1m;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_protocols TLSV1.1 TLSV1.2 TLSV1.3;
|
||||
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
access_log /var/log/nginx/httpslocalhost.access.log;
|
||||
error_log /var/log/nginx/httpslocalhost.error.log;
|
||||
|
||||
client_max_body_size 20M;
|
||||
|
||||
location / {
|
||||
proxy_pass https://127.0.0.1:8081;
|
||||
proxy_set_header Host localhost;
|
||||
proxy_set_header X-Forwarded-Host localhost;
|
||||
proxy_set_header X-Forwarded-Server localhost;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_redirect off;
|
||||
proxy_connect_timeout 90s;
|
||||
proxy_read_timeout 90s;
|
||||
proxy_send_timeout 90s;
|
||||
proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
}
|
||||
|
||||
location /sockjs-node {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
|
||||
proxy_pass https://127.0.0.1:8081;
|
||||
proxy_redirect off;
|
||||
proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_redirect off;
|
||||
proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
}
|
||||
|
||||
location /socket {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Prod settings to serve static index
|
||||
# location / {
|
||||
# autoindex on;
|
||||
# #try_files $uri $uri/ /index.html;
|
||||
# }
|
||||
|
||||
# location / {
|
||||
# #autoindex on
|
||||
#
|
||||
# proxy_pass http://127.0.0.1:8444;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection 'upgrade';
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_cache_bypass $http_upgrade;
|
||||
# }
|
||||
@@ -1,103 +0,0 @@
|
||||
#
|
||||
# nginx for Solid Scribe on NixOS.
|
||||
#
|
||||
# Merge this into your configuration.nix and `nixos-rebuild switch`.
|
||||
# Replace REPO_PATH with the absolute path of the repo clone on the server.
|
||||
#
|
||||
# Four things differ from the previous config, and the first two are required
|
||||
# for the Vue 3 client to work at all:
|
||||
#
|
||||
# 1. SPA fallback. The old Vue 2 client used vue-router's default HASH mode
|
||||
# (no `mode:` was set), so every request was just "/" and routing happened
|
||||
# after the "#". That is why share links looked like /#/public/note/...
|
||||
# The Vue 3 client uses createWebHistory, so /notes, /settings and
|
||||
# /share-target are real HTTP requests. Without tryFiles they all 404.
|
||||
#
|
||||
# 2. root moves up to the vhost. It was inside locations."/", which left
|
||||
# /assets/, /sw.js and /manifest.json unresolvable once they needed their
|
||||
# own location blocks.
|
||||
#
|
||||
# It also no longer points into the repo. deploy.sh publishes the built
|
||||
# client to /var/www/ss/releases/<timestamp> and atomically repoints the
|
||||
# /var/www/ss/current symlink at it. Pointing nginx at that stable path
|
||||
# means a deploy is one rename -- nginx resolves the symlink per request,
|
||||
# so no reload is needed and no request sees a half-copied directory.
|
||||
# Rollback is the same rename pointing at an older release.
|
||||
#
|
||||
# 3. proxyWebsockets on /socket. recommendedProxySettings does NOT add the
|
||||
# Upgrade/Connection headers, so Socket.IO has been silently falling back
|
||||
# to long-polling -- that's the live note-sync path running degraded.
|
||||
#
|
||||
# 4. Cache headers. See the warning below.
|
||||
#
|
||||
# !! CLOUDFLARE !!
|
||||
# The tunnel sits in front of this and Cloudflare caches .js by DEFAULT, which
|
||||
# includes /sw.js. If the edge caches the service worker, the update check can
|
||||
# never see a new one and the app is pinned to an old build with no remote way
|
||||
# to recover. The nginx headers below are necessary but NOT sufficient --
|
||||
# add a Cloudflare Cache Rule that BYPASSES cache for /sw.js (and /index.html)
|
||||
# before cutting over.
|
||||
#
|
||||
# Also confirm which Host header cloudflared sends. This vhost is keyed on
|
||||
# "192.168.1.7"; if the tunnel forwards the public hostname instead, nginx will
|
||||
# not match this vhost and will fall through to the default server. Either add
|
||||
# the public hostname to serverAliases or set the tunnel's httpHostHeader.
|
||||
#
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
recommendedGzipSettings = true;
|
||||
|
||||
virtualHosts."192.168.1.7" = {
|
||||
# serverAliases = [ "notes.example.com" ]; # if the tunnel forwards the public host
|
||||
|
||||
# Published by deploy.sh; a symlink to the live release. Not in the repo.
|
||||
root = "/var/www/ss/current";
|
||||
|
||||
locations."/" = {
|
||||
tryFiles = "$uri $uri/ /index.html";
|
||||
};
|
||||
|
||||
# Never let the HTTP layer cache the service worker or the app shell.
|
||||
locations."= /sw.js".extraConfig = ''
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
expires -1;
|
||||
'';
|
||||
|
||||
locations."= /index.html".extraConfig = ''
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
expires -1;
|
||||
'';
|
||||
|
||||
# The manifest changes rarely but should not be pinned for a year.
|
||||
locations."= /manifest.json".extraConfig = ''
|
||||
add_header Cache-Control "no-cache";
|
||||
expires -1;
|
||||
'';
|
||||
|
||||
# Vite content-hashes everything under /assets, so these are immutable.
|
||||
locations."/assets/".extraConfig = ''
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
'';
|
||||
|
||||
locations."/api" = {
|
||||
proxyPass = "http://localhost:3000";
|
||||
};
|
||||
|
||||
locations."/socket" = {
|
||||
proxyPass = "http://localhost:3001";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Note on file access: nginx runs as its own user and needs read access to the
|
||||
# web root, with +x on every parent directory. Serving from /var/www/ss is what
|
||||
# makes that a non-issue -- the one-time `chmod 755 /var/www /var/www/ss` in
|
||||
# deploy.sh is the whole of it. This is the reason the root is not the repo
|
||||
# clone: a clone under /home/mab would require loosening the permissions on the
|
||||
# home directory itself.
|
||||
#
|
||||
# Uploads are NOT here. Express serves them from REPO_PATH/staticFiles at
|
||||
# /api/static via the proxy below, so they are untouched by deploys.
|
||||
@@ -1,225 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Deploy Solid Scribe. Run this ON the server, from inside the repo.
|
||||
#
|
||||
# cd /path/to/SolidScribe && ./deploy.sh
|
||||
#
|
||||
# Replaces buildAndUpdateProd.sh, which built the old Vue 2 client and rsynced a
|
||||
# tarball to /home/mab/pi -- a directory that no longer exists.
|
||||
#
|
||||
# Assumes:
|
||||
# - this repo is a git clone with a tracking branch
|
||||
# - node + pnpm are on PATH for the user running this (on NixOS that means
|
||||
# environment.systemPackages or the user's profile -- NOT only inside a
|
||||
# nix-shell, or this will fail at the install step)
|
||||
# - pm2 is already managing NoteServer (pm2 resurrect on boot)
|
||||
# - nginx serves $WEBROOT/current with an SPA fallback to index.html
|
||||
#
|
||||
# nginx is NOT touched here. On NixOS it is declarative -- see configs/.
|
||||
#
|
||||
# --- the web root --------------------------------------------------------
|
||||
#
|
||||
# The built client is published to /var/www/ss, NOT served out of the repo.
|
||||
# nginx runs as its own user, so serving from a clone under /home/mab meant
|
||||
# loosening the home directory's permissions for every parent dir. /var/www is
|
||||
# the conventional place for this and sidesteps the whole problem.
|
||||
#
|
||||
# Layout, which is the standard symlink-swap pattern:
|
||||
#
|
||||
# /var/www/ss/
|
||||
# releases/2026-10-28_1147/ <- one directory per deploy
|
||||
# releases/2026-10-28_0930/
|
||||
# current -> releases/2026-10-28_1147
|
||||
#
|
||||
# nginx points at the stable path $WEBROOT/current. Publishing is a rename of
|
||||
# that symlink, which is a single atomic syscall: no request ever sees a
|
||||
# half-copied directory, and no window exists where the root is missing.
|
||||
# Rolling back is the same rename pointing at the previous release.
|
||||
#
|
||||
# Only the *client* moves. The API server still runs from the repo under pm2,
|
||||
# and user uploads are still served by Express from $REPO/staticFiles at
|
||||
# /api/static -- deliberately, so deploys never touch user data.
|
||||
#
|
||||
# ONE-TIME SETUP on a new server (needs root):
|
||||
#
|
||||
# sudo mkdir -p /var/www/ss/releases
|
||||
# sudo chown -R "$USER" /var/www/ss
|
||||
# sudo chmod 755 /var/www /var/www/ss
|
||||
#
|
||||
# Override the location for a staging box with: WEBROOT=/var/www/ss-staging ./deploy.sh
|
||||
#
|
||||
# --- the database account ------------------------------------------------
|
||||
#
|
||||
# The API connects as a least-privilege account with DML rights only. This is
|
||||
# deliberate: the server fetches untrusted URLs for link previews, and a
|
||||
# service able to reshape or drop its own tables is a far larger blast radius
|
||||
# than that feature warrants.
|
||||
#
|
||||
# CREATE USER 'app'@'localhost' IDENTIFIED BY '<password>';
|
||||
# GRANT SELECT, INSERT, UPDATE, DELETE ON application.* TO 'app'@'localhost';
|
||||
# FLUSH PRIVILEGES;
|
||||
#
|
||||
# -- deliberately NOT granted: CREATE, ALTER, DROP, INDEX
|
||||
#
|
||||
# Then set DB_USER=app and DB_PASS=<password> in ~/.env. Note the asymmetry:
|
||||
# the admin scripts here (backupDatabase.sh, applyLastBackup.sh,
|
||||
# migrateDatabase.sh) still connect as root with $PROD_DB_PASS.
|
||||
#
|
||||
# DELETE is genuinely required -- the scraper drops shell attachment rows for
|
||||
# dead links and purges expired cache entries.
|
||||
#
|
||||
# --- schema changes ------------------------------------------------------
|
||||
#
|
||||
# Schema is managed by Prisma, for migrations and scaffolding ONLY -- the
|
||||
# running server never imports it and still talks to MariaDB through raw SQL.
|
||||
# The schema lives in prisma/schema.prisma; migrations in prisma/migrations/.
|
||||
#
|
||||
# Because the app holds no DDL, a deploy that introduces schema changes needs
|
||||
# one manual step, run once per environment as a database admin:
|
||||
#
|
||||
# nix-shell shell.nix --run ./migrateDatabase.sh
|
||||
#
|
||||
# nix-shell is required: the prisma CLI comes from nixpkgs (prisma_7, 7.8.0),
|
||||
# not from node_modules. Alternatively add prisma_7 to the server's
|
||||
# environment.systemPackages and call the script directly.
|
||||
#
|
||||
# It is idempotent, so re-running is safe. On a database that predates Prisma
|
||||
# the script detects the missing migration history and baselines it
|
||||
# automatically (marking 0_init applied rather than executing it) -- it does
|
||||
# NOT need to be done by hand.
|
||||
#
|
||||
# The API verifies the schema and the grants at boot; if either is missing it
|
||||
# logs the remedy and serves normally with link previews disabled.
|
||||
#
|
||||
# NEVER run `prisma migrate dev`, `prisma migrate reset` or `prisma db push`.
|
||||
# The first wants a shadow database; the latter two can drop data.
|
||||
#
|
||||
# DATABASE_URL is composed by the script from the admin credentials in ~/.env
|
||||
# and is never read by the application.
|
||||
#
|
||||
# Note: _prisma_migrations lives inside the `application` database, so the
|
||||
# least-privilege `app` user can write migration history (its grant is
|
||||
# database-wide). It holds no DDL, so it cannot reshape tables -- narrowing
|
||||
# this further would mean per-table grants, which is a separate project.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$REPO"
|
||||
|
||||
WEBROOT="${WEBROOT:-/var/www/ss}"
|
||||
RELEASE="$WEBROOT/releases/$(date +%Y-%m-%d_%H%M%S)"
|
||||
# Number of old releases to keep around for rollback.
|
||||
KEEP_RELEASES=5
|
||||
|
||||
step(){ printf '\n\033[32m::--:: %s\033[0m\n' "$1"; }
|
||||
|
||||
# --- sanity checks before changing anything ------------------------------
|
||||
|
||||
for tool in git pnpm pm2 rsync; do
|
||||
command -v "$tool" >/dev/null || { echo "$tool is not on PATH" >&2; exit 1; }
|
||||
done
|
||||
|
||||
# Fail here rather than after a three-minute build. A missing web root is
|
||||
# almost always a server that never had the one-time setup above run on it.
|
||||
if [ ! -d "$WEBROOT/releases" ]; then
|
||||
echo "$WEBROOT/releases does not exist -- run the one-time setup at the top of this script." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -w "$WEBROOT/releases" ]; then
|
||||
echo "$WEBROOT/releases is not writable by $(whoami) -- check the chown in the one-time setup." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
|
||||
echo "Working tree has local modifications. Commit or stash them first:" >&2
|
||||
git status --short >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- pull ----------------------------------------------------------------
|
||||
|
||||
step "Pulling $(git rev-parse --abbrev-ref HEAD)..."
|
||||
# --ff-only: refuse to create a merge commit on the server. If this fails the
|
||||
# server has diverged and wants a human.
|
||||
git pull --ff-only
|
||||
|
||||
# --- dependencies --------------------------------------------------------
|
||||
|
||||
step "Installing server dependencies..."
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
step "Installing client dependencies..."
|
||||
pnpm install --frozen-lockfile --dir client
|
||||
|
||||
# --- build ---------------------------------------------------------------
|
||||
|
||||
# dist is gitignored, so the server builds it. The build stays inside the repo
|
||||
# and is only copied to $WEBROOT once it has been checked, so a failed build
|
||||
# cannot touch what is currently being served.
|
||||
step "Building client..."
|
||||
rm -rf client/dist
|
||||
pnpm --dir client exec vite build
|
||||
|
||||
# manifest.json is checked in under client/public and copied verbatim by Vite;
|
||||
# if it is missing, the PWA installs without icons, scope or a share target.
|
||||
for required in index.html sw.js manifest.json; do
|
||||
if [ ! -f "client/dist/$required" ]; then
|
||||
echo "Build output looks wrong (missing $required) -- $WEBROOT left untouched." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# --- publish -------------------------------------------------------------
|
||||
|
||||
step "Publishing to $RELEASE..."
|
||||
mkdir -p "$RELEASE"
|
||||
# Trailing slash on the source: copy the *contents* of dist into the release
|
||||
# dir. --delete is harmless on a fresh dir but keeps a re-run of the same
|
||||
# timestamp honest.
|
||||
rsync -a --delete client/dist/ "$RELEASE/"
|
||||
|
||||
# nginx reads these as its own user, so they must be world-readable.
|
||||
chmod -R a+rX "$RELEASE"
|
||||
|
||||
step "Pointing $WEBROOT/current at the new release..."
|
||||
# Two steps because `ln -sfn` over an existing symlink unlinks before relinking,
|
||||
# which leaves a brief window with no web root at all. Creating a scratch
|
||||
# symlink and renaming it over the old one is a single atomic rename(2).
|
||||
ln -sfn "$RELEASE" "$WEBROOT/current.new"
|
||||
mv -T "$WEBROOT/current.new" "$WEBROOT/current"
|
||||
|
||||
# --- prune ---------------------------------------------------------------
|
||||
|
||||
# Keep the most recent $KEEP_RELEASES for rollback, drop the rest. Never remove
|
||||
# whatever `current` actually resolves to, however old it is.
|
||||
live="$(readlink -f "$WEBROOT/current")"
|
||||
ls -1d "$WEBROOT"/releases/*/ 2>/dev/null \
|
||||
| sort -r \
|
||||
| tail -n +$((KEEP_RELEASES + 1)) \
|
||||
| while read -r stale; do
|
||||
[ "$(readlink -f "$stale")" = "$live" ] && continue
|
||||
echo " pruning $(basename "$stale")"
|
||||
rm -rf "$stale"
|
||||
done
|
||||
|
||||
# --- restart -------------------------------------------------------------
|
||||
|
||||
step "Reloading the API server..."
|
||||
# reload, not restart: graceful in cluster mode. --update-env so changes to
|
||||
# ecosystem.config.js are picked up.
|
||||
pm2 reload "$REPO/server/ecosystem.config.js" --env production --update-env
|
||||
|
||||
# Persist the process list so `pm2 resurrect` on boot gets the current state.
|
||||
pm2 save
|
||||
|
||||
step "Done."
|
||||
echo " serving $(readlink -f "$WEBROOT/current")"
|
||||
echo
|
||||
echo " To roll back, repoint the symlink at the previous release:"
|
||||
echo " ls $WEBROOT/releases"
|
||||
echo " ln -sfn $WEBROOT/releases/<older> $WEBROOT/current.new && mv -T $WEBROOT/current.new $WEBROOT/current"
|
||||
echo
|
||||
pm2 list
|
||||
@@ -12,4 +12,3 @@ bundle.*
|
||||
client/dist*
|
||||
server/public/*
|
||||
client/dist*
|
||||
*_scrape*
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// The @alias map must mirror _moduleAliases in package.json, which is what
|
||||
// module-alias/register sets up at runtime.
|
||||
//
|
||||
// These were previously written as "../../server/..." relative paths, which
|
||||
// jest resolves relative to the requiring file - so they never actually
|
||||
// resolved, and every existing test sidestepped them with relative requires.
|
||||
// <rootDir> is depth-independent and works from any test directory.
|
||||
//
|
||||
module.exports = {
|
||||
moduleNameMapper: {
|
||||
"^@root/(.*)$": "<rootDir>/$1",
|
||||
"^@models/(.*)$": "<rootDir>/server/models/$1",
|
||||
"^@routes/(.*)$": "<rootDir>/server/routes/$1",
|
||||
"^@helpers/(.*)$": "<rootDir>/server/helpers/$1",
|
||||
"^@config/(.*)$": "<rootDir>/server/config/$1",
|
||||
"^@workers/(.*)$": "<rootDir>/server/workers/$1",
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Apply database migrations. Run from the repo root, INSIDE nix-shell (that is
|
||||
# where the prisma CLI lives).
|
||||
#
|
||||
# nix-shell shell.nix --run ./migrateDatabase.sh
|
||||
#
|
||||
# Works both ways round: it applies pending migrations to an existing database,
|
||||
# and builds a complete one from empty. Safe to re-run -- a second run is a
|
||||
# no-op. Replaces migrateDatabase.sh.
|
||||
#
|
||||
# --- why this is a separate step -----------------------------------------
|
||||
#
|
||||
# The application connects as a least-privilege account (production uses `app`)
|
||||
# holding DML rights only -- no CREATE, ALTER or DROP. That is deliberate: the
|
||||
# server fetches untrusted URLs for link previews, and a service able to reshape
|
||||
# its own tables is a far larger blast radius than that feature warrants.
|
||||
#
|
||||
# So the app never applies DDL. It verifies the schema at boot
|
||||
# (server/helpers/Schema.js) and disables link previews if anything is missing.
|
||||
#
|
||||
# --- authoring a new migration -------------------------------------------
|
||||
#
|
||||
# NEVER run `prisma migrate dev`. It can decide the database has drifted and
|
||||
# offer to RESET it, which on a box holding a production data restore would
|
||||
# destroy real data. It also wants a shadow database. Instead:
|
||||
#
|
||||
# 1. Edit prisma/schema.prisma
|
||||
# 2. NAME=add_whatever
|
||||
# DIR="prisma/migrations/$(date +%Y%m%d%H%M%S)_$NAME"
|
||||
# mkdir -p "$DIR"
|
||||
# prisma migrate diff --from-config-datasource \
|
||||
# --to-schema prisma/schema.prisma --script > "$DIR/migration.sql"
|
||||
# 3. READ the generated SQL -- this review is what makes the approach safe
|
||||
# 4. ./migrateDatabase.sh
|
||||
#
|
||||
# Note: Prisma emits `COLLATE utf8mb4_unicode_ci`, but this database is
|
||||
# `utf8mb4_general_ci` throughout, and `prisma migrate diff` does NOT report the
|
||||
# difference. Fix the collation in any generated migration by hand. This script
|
||||
# checks for drift at the end.
|
||||
#
|
||||
# --- credentials ---------------------------------------------------------
|
||||
#
|
||||
# Same convention as applyLastBackup.sh / backupDatabase.sh: source ~/.env and
|
||||
# connect as root with $PROD_DB_PASS. Falls back to passwordless root (the dev
|
||||
# case, where shell.nix also exports DATABASE_URL). Override with:
|
||||
#
|
||||
# DB_ADMIN_USER=admin DB_ADMIN_PASS=secret ./migrateDatabase.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$REPO"
|
||||
|
||||
DB_NAME="application" # hardcoded in server/config/database.js too
|
||||
|
||||
# --- preconditions -------------------------------------------------------
|
||||
|
||||
if ! command -v prisma >/dev/null 2>&1; then
|
||||
echo "prisma is not on PATH -- run this inside nix-shell:" >&2
|
||||
echo " nix-shell shell.nix --run ./migrateDatabase.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v mariadb >/dev/null 2>&1 && ! command -v mysql >/dev/null 2>&1; then
|
||||
echo "no mariadb/mysql client on PATH -- run this inside nix-shell." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v mariadb >/dev/null 2>&1; then MYSQL_BIN="mariadb"; else MYSQL_BIN="mysql"; fi
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "node is not on PATH -- needed to percent-encode the database password." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.env" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$HOME/.env"
|
||||
fi
|
||||
|
||||
ADMIN_USER="${DB_ADMIN_USER:-root}"
|
||||
ADMIN_PASS="${DB_ADMIN_PASS:-${PROD_DB_PASS:-}}"
|
||||
|
||||
mysql_args=(--user "$ADMIN_USER")
|
||||
[ -n "$ADMIN_PASS" ] && mysql_args+=(--password="$ADMIN_PASS")
|
||||
|
||||
LOCAL_SOCKET="$REPO/.mysql/mysql.sock"
|
||||
[ -S "$LOCAL_SOCKET" ] && mysql_args+=(--socket="$LOCAL_SOCKET")
|
||||
|
||||
# Prisma needs a URL, and it must carry the ADMIN credentials -- never the
|
||||
# app account, which holds no DDL.
|
||||
DB_ADMIN_HOST="${DB_ADMIN_HOST:-127.0.0.1}"
|
||||
DB_ADMIN_PORT="${DB_ADMIN_PORT:-3306}"
|
||||
|
||||
#
|
||||
# Percent-encode the password. Production passwords routinely contain @ : / ? #
|
||||
# or %, every one of which silently corrupts a URL -- an unencoded `@` would
|
||||
# truncate the host and produce a baffling connection error rather than an
|
||||
# obvious one.
|
||||
#
|
||||
encode_component() {
|
||||
node -e 'process.stdout.write(encodeURIComponent(process.argv[1] || ""))' "$1"
|
||||
}
|
||||
|
||||
ADMIN_USER_ENC="$(encode_component "$ADMIN_USER")"
|
||||
|
||||
if [ -n "$ADMIN_PASS" ]; then
|
||||
ADMIN_PASS_ENC="$(encode_component "$ADMIN_PASS")"
|
||||
export DATABASE_URL="mysql://${ADMIN_USER_ENC}:${ADMIN_PASS_ENC}@${DB_ADMIN_HOST}:${DB_ADMIN_PORT}/${DB_NAME}"
|
||||
else
|
||||
export DATABASE_URL="mysql://${ADMIN_USER_ENC}@${DB_ADMIN_HOST}:${DB_ADMIN_PORT}/${DB_NAME}"
|
||||
fi
|
||||
|
||||
# Keep the CLI from phoning checkpoint.prisma.io for update checks.
|
||||
export CHECKPOINT_DISABLE=1
|
||||
|
||||
if ! "$MYSQL_BIN" "${mysql_args[@]}" -e "SELECT 1" >/dev/null 2>&1; then
|
||||
echo "Cannot connect to the database as '$ADMIN_USER'." >&2
|
||||
echo "Set PROD_DB_PASS in ~/.env, or pass DB_ADMIN_USER / DB_ADMIN_PASS." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# Collation is explicit because the server default is utf8mb4_unicode_ci while
|
||||
# every application table is utf8mb4_general_ci. A table created later without
|
||||
# an explicit COLLATE would otherwise inherit the wrong one, changing string
|
||||
# comparison and FULLTEXT ranking -- and Prisma neither models collation nor
|
||||
# reports the difference in `migrate diff`.
|
||||
#
|
||||
# For a database that already exists with the wrong default, the one-time
|
||||
# non-destructive fix is:
|
||||
# ALTER DATABASE application CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
#
|
||||
"$MYSQL_BIN" "${mysql_args[@]}" -e "CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` \
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"
|
||||
|
||||
sql() { "$MYSQL_BIN" "${mysql_args[@]}" -N -B "$DB_NAME" -e "$1" 2>/dev/null; }
|
||||
|
||||
echo '-------'
|
||||
echo "Migrating '$DB_NAME' as '$ADMIN_USER'"
|
||||
echo '-------'
|
||||
|
||||
# --- baseline an existing, pre-Prisma database ---------------------------
|
||||
#
|
||||
# This is the single most dangerous moment in adopting Prisma. A database that
|
||||
# already has the schema but no _prisma_migrations table must have the baseline
|
||||
# marked applied, NOT executed -- running 0_init against it would fail on
|
||||
# existing tables. Detect and handle it here rather than leaving it as a manual
|
||||
# step someone forgets.
|
||||
#
|
||||
HAS_MIGRATION_TABLE=$(sql "SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA='$DB_NAME' AND TABLE_NAME='_prisma_migrations'" || echo 0)
|
||||
HAS_EXISTING_SCHEMA=$(sql "SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA='$DB_NAME' AND TABLE_NAME='attachment'" || echo 0)
|
||||
|
||||
if [ "${HAS_MIGRATION_TABLE:-0}" -eq 0 ] && [ "${HAS_EXISTING_SCHEMA:-0}" -gt 0 ]; then
|
||||
echo "Existing schema found with no migration history -- baselining."
|
||||
|
||||
#
|
||||
# Report drift BEFORE recording the baseline.
|
||||
#
|
||||
# `migrate resolve` records a migration without executing it, so baselining
|
||||
# a database that does not actually match the schema leaves it permanently
|
||||
# short of those objects while Prisma reports everything as applied. That
|
||||
# is exactly how production ended up missing scrape_cache: this script
|
||||
# previously inferred "the attachment table exists" => "the whole schema
|
||||
# exists", which is not the same claim.
|
||||
#
|
||||
# Later migrations are additive and idempotent, so drift is recoverable --
|
||||
# but the operator should see it rather than discover it from a log line
|
||||
# days later.
|
||||
#
|
||||
if ! prisma migrate diff \
|
||||
--from-config-datasource \
|
||||
--to-schema prisma/schema.prisma \
|
||||
--exit-code >/dev/null 2>&1
|
||||
then
|
||||
echo ""
|
||||
echo "NOTE: this database does not yet match prisma/schema.prisma."
|
||||
echo " Baselining 0_init anyway, then applying later migrations to"
|
||||
echo " converge it. Differences:"
|
||||
prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma \
|
||||
| sed 's/^/ /'
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Marking 0_init as already applied (NOT executing it)."
|
||||
prisma migrate resolve --applied 0_init
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# --- apply ---------------------------------------------------------------
|
||||
|
||||
prisma migrate deploy
|
||||
|
||||
# --- seed ----------------------------------------------------------------
|
||||
|
||||
if [ -f prisma/seed.sql ]; then
|
||||
echo ""
|
||||
echo "Applying reference data (prisma/seed.sql)..."
|
||||
"$MYSQL_BIN" "${mysql_args[@]}" "$DB_NAME" < prisma/seed.sql
|
||||
fi
|
||||
|
||||
# --- verify --------------------------------------------------------------
|
||||
#
|
||||
# Print proof rather than leaving the operator to trust us.
|
||||
#
|
||||
echo ""
|
||||
echo '-------'
|
||||
echo 'Verification'
|
||||
echo '-------'
|
||||
|
||||
prisma migrate status
|
||||
|
||||
echo ""
|
||||
"$MYSQL_BIN" "${mysql_args[@]}" "$DB_NAME" --table -e "
|
||||
SELECT 'attachment FULLTEXT index' AS check_name,
|
||||
IFNULL(MAX(INDEX_TYPE), 'MISSING') AS result
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND INDEX_NAME = 'attachment_text_index'
|
||||
UNION ALL
|
||||
SELECT 'tables',
|
||||
CAST(COUNT(*) AS CHAR) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME <> '_prisma_migrations'
|
||||
UNION ALL
|
||||
SELECT 'attachment_type seed rows',
|
||||
CAST(COUNT(*) AS CHAR) FROM attachment_type
|
||||
UNION ALL
|
||||
-- _prisma_migrations is Prisma's own bookkeeping table and is created with
|
||||
-- its default collation; only application tables are checked here.
|
||||
SELECT 'non-general_ci app tables (expect 0)',
|
||||
CAST(COUNT(*) AS CHAR) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME <> '_prisma_migrations'
|
||||
AND TABLE_COLLATION <> 'utf8mb4_general_ci';
|
||||
"
|
||||
|
||||
#
|
||||
# Assert that everything the application actually needs now exists.
|
||||
#
|
||||
# This is the check that was missing. `prisma migrate deploy` reporting success
|
||||
# only means the recorded migrations ran -- it says nothing about whether the
|
||||
# database ended up in the intended shape, which is precisely how production
|
||||
# could pass this script and still log
|
||||
# "Scraper: DISABLED - missing table scrape_cache ...".
|
||||
#
|
||||
# Mirrors server/helpers/Schema.js's boot verification, so a green run here
|
||||
# means a green boot there.
|
||||
#
|
||||
FAILURES=0
|
||||
require_object() {
|
||||
local label="$1" count="$2"
|
||||
if [ "${count:-0}" -eq 0 ]; then
|
||||
echo "FAILED: missing $label" >&2
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
require_object "table scrape_cache" "$(sql "SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA='$DB_NAME' AND TABLE_NAME='scrape_cache'")"
|
||||
|
||||
for column in scrape_state scrape_attempts enqueued_at; do
|
||||
require_object "attachment.$column" "$(sql "SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA='$DB_NAME' AND TABLE_NAME='attachment' AND COLUMN_NAME='$column'")"
|
||||
done
|
||||
|
||||
require_object "attachment index attachment_scrape_state" "$(sql "SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA='$DB_NAME' AND TABLE_NAME='attachment' AND INDEX_NAME='attachment_scrape_state'")"
|
||||
|
||||
# Checked by TYPE, not just by name: a BTREE index called attachment_text_index
|
||||
# would make MATCH..AGAINST return nothing at all, with no error anywhere.
|
||||
FT=$(sql "SELECT IFNULL(MAX(INDEX_TYPE),'MISSING') FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA='$DB_NAME' AND INDEX_NAME='attachment_text_index'")
|
||||
if [ "$FT" != "FULLTEXT" ]; then
|
||||
echo "FAILED: attachment_text_index is '$FT', expected FULLTEXT." >&2
|
||||
echo "Attachment search depends on MATCH..AGAINST and would silently return nothing." >&2
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
if [ "$FAILURES" -gt 0 ]; then
|
||||
echo "" >&2
|
||||
echo "$FAILURES required object(s) missing after migrating -- the scraper will stay disabled." >&2
|
||||
echo "This should not happen; check 'prisma migrate status' and the migration SQL." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo '-------'
|
||||
echo 'Done. Restart the API so it re-verifies the schema:'
|
||||
echo ' pm2 restart NoteServer'
|
||||
echo '-------'
|
||||
@@ -1,27 +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",
|
||||
"cheerio": "^1.1.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.5",
|
||||
"socket.io": "^2.3.0",
|
||||
"speakeasy": "^2.0.0"
|
||||
},
|
||||
@@ -30,10 +32,6 @@
|
||||
"@models": "server/models",
|
||||
"@routes": "server/routes",
|
||||
"@helpers": "server/helpers",
|
||||
"@config": "server/config",
|
||||
"@workers": "server/workers"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0"
|
||||
"@config": "server/config"
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// Prisma CLI configuration.
|
||||
//
|
||||
// Prisma 7 removed `url` from the datasource block in schema.prisma; the
|
||||
// connection string for migrations lives here instead.
|
||||
//
|
||||
// This file configures the CLI only. The running server does not use Prisma --
|
||||
// it talks to MariaDB through server/config/database.js (mysql2) with raw SQL.
|
||||
// Prisma is a development and admin tool here, for schema migrations and
|
||||
// scaffolding.
|
||||
//
|
||||
// DATABASE_URL must point at an account with DDL rights. In dev that is the
|
||||
// root user exported by shell.nix; in production migrateDatabase.sh composes it
|
||||
// from the admin credentials in ~/.env. It must NEVER be the least-privilege
|
||||
// `app` account the server runs as, which deliberately holds no CREATE/ALTER.
|
||||
//
|
||||
// .mjs rather than .js because this project is CommonJS and Prisma's example
|
||||
// config is ESM; the explicit extension avoids any ambiguity.
|
||||
//
|
||||
export default {
|
||||
schema: 'prisma/schema.prisma',
|
||||
|
||||
migrations: {
|
||||
path: 'prisma/migrations',
|
||||
},
|
||||
|
||||
datasource: {
|
||||
// process.env directly rather than env() -- the helper throws when the
|
||||
// variable is unset, which makes the failure harder to read.
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
--
|
||||
-- Repair: converge environments that were baselined before the scrape objects
|
||||
-- existed.
|
||||
--
|
||||
-- WHY THIS EXISTS
|
||||
--
|
||||
-- The 0_init baseline was generated from a database that already had
|
||||
-- scrape_cache and the attachment.scrape_* columns, on the understanding that
|
||||
-- every environment was in that state. Production was not. Because
|
||||
-- `prisma migrate resolve --applied 0_init` records a migration WITHOUT
|
||||
-- executing it, production ended up with 0_init marked applied while those
|
||||
-- objects were never created -- so the scraper logged:
|
||||
--
|
||||
-- Scraper: DISABLED - missing table scrape_cache, attachment.scrape_state, ...
|
||||
--
|
||||
-- Re-running 0_init is not an option (it would try to CREATE TABLE over ~2200
|
||||
-- rows of live data), so this additive, idempotent migration brings any
|
||||
-- environment up to the intended schema instead.
|
||||
--
|
||||
-- Every statement uses IF NOT EXISTS, so this is a clean no-op on environments
|
||||
-- that already had these objects. MariaDB 11.4 supports that syntax for columns
|
||||
-- and indexes; both dev and production are MariaDB, so this is safe here. It
|
||||
-- would NOT be valid on MySQL 8.
|
||||
--
|
||||
-- The definitions below are identical to those in 0_init -- deliberately so,
|
||||
-- because the end state must be the same whichever path an environment took.
|
||||
--
|
||||
|
||||
-- One row per scraped URL, shared across users and notes.
|
||||
--
|
||||
-- url_hash carries the UNIQUE index rather than `url` because a UNIQUE on
|
||||
-- varchar(1000) utf8mb4 is 4000 bytes and exceeds InnoDB's 3072-byte key limit.
|
||||
--
|
||||
-- Collation is explicit: Prisma renders utf8mb4_unicode_ci, but every table in
|
||||
-- this database is utf8mb4_general_ci, and `prisma migrate diff` does not
|
||||
-- report the difference.
|
||||
CREATE TABLE IF NOT EXISTS `scrape_cache` (
|
||||
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`url_hash` CHAR(64) NOT NULL,
|
||||
`url` VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
`canonical_url` VARCHAR(1000) NULL,
|
||||
`status` SMALLINT NOT NULL DEFAULT 0,
|
||||
`title` VARCHAR(512) NULL,
|
||||
`site_name` VARCHAR(255) NULL,
|
||||
`description` MEDIUMTEXT NULL,
|
||||
`keywords` VARCHAR(512) NULL,
|
||||
`search_text` MEDIUMTEXT NULL,
|
||||
`image_url` VARCHAR(1000) NULL,
|
||||
`file_location` VARCHAR(500) NULL,
|
||||
`etag` VARCHAR(255) NULL,
|
||||
`last_modified` VARCHAR(128) NULL,
|
||||
`content_type` VARCHAR(128) NULL,
|
||||
`fetch_state` VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
`failure_count` INTEGER NOT NULL DEFAULT 0,
|
||||
`last_error` VARCHAR(255) NULL,
|
||||
`fetched_at` INTEGER NULL,
|
||||
`expires_at` INTEGER NULL,
|
||||
|
||||
UNIQUE INDEX `scrape_cache_url_hash`(`url_hash`),
|
||||
INDEX `scrape_cache_expires_at`(`expires_at`),
|
||||
INDEX `scrape_cache_fetch_state`(`fetch_state`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- Per-attachment scrape lifecycle, so the in-memory queue survives a restart.
|
||||
--
|
||||
-- scrape_state: 0 = pending, 1 = fetching, 2 = done, 3 = failed.
|
||||
-- DEFAULT 2 is load-bearing -- it keeps every pre-existing attachment row
|
||||
-- (~2200 in production) out of the scraper's boot recovery sweep.
|
||||
ALTER TABLE `attachment` ADD COLUMN IF NOT EXISTS `scrape_state` TINYINT NOT NULL DEFAULT 2;
|
||||
ALTER TABLE `attachment` ADD COLUMN IF NOT EXISTS `scrape_attempts` TINYINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE `attachment` ADD COLUMN IF NOT EXISTS `enqueued_at` INTEGER NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS `attachment_scrape_state` ON `attachment` (`scrape_state`, `enqueued_at`);
|
||||
@@ -1,211 +0,0 @@
|
||||
--
|
||||
-- Baseline migration: the schema as it existed when Prisma was adopted.
|
||||
--
|
||||
-- Generated with:
|
||||
-- prisma migrate diff --from-empty --to-schema prisma/schema.prisma --script
|
||||
--
|
||||
-- HAND-CORRECTED, and it matters: Prisma renders `COLLATE utf8mb4_unicode_ci`
|
||||
-- for MySQL, but every live table is `utf8mb4_general_ci`. Prisma does not
|
||||
-- model collation and its own `migrate diff` does NOT report the difference, so
|
||||
-- without this correction a database rebuilt from empty would silently differ
|
||||
-- from production in string comparison, sorting and FULLTEXT ranking.
|
||||
-- If you regenerate this file, re-apply the collation fix. There is a test for
|
||||
-- it in server/tests/schema/migrations.test.js.
|
||||
--
|
||||
-- Things Prisma cannot express, which therefore need handling elsewhere:
|
||||
-- * the DATABASE-level default collation -- set by migrateDatabase.sh when it
|
||||
-- creates the database, since tables created later without an explicit
|
||||
-- COLLATE would otherwise inherit the server default
|
||||
-- * ENGINE=InnoDB (relies on the server default, which is InnoDB)
|
||||
-- * AUTO_INCREMENT start values (cosmetic; a fresh database starting at 1 is
|
||||
-- correct)
|
||||
-- * the COMMENT on note_raw_text.text
|
||||
-- * row data -- see prisma/seed.sql for the attachment_type lookup rows
|
||||
--
|
||||
-- Context carried over from the migration this replaces: scrape_cache.url_hash
|
||||
-- carries the UNIQUE index rather than `url` because a UNIQUE on
|
||||
-- varchar(1000) utf8mb4 is 4000 bytes and exceeds InnoDB's 3072-byte key limit;
|
||||
-- and attachment.scrape_state DEFAULT 2 ("done") is load-bearing -- it keeps
|
||||
-- pre-existing attachment rows out of the scraper's boot recovery sweep.
|
||||
--
|
||||
-- On environments that already had this schema (dev and production), this file
|
||||
-- is NOT executed -- it is marked applied with `prisma migrate resolve`.
|
||||
--
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `attachment` (
|
||||
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`note_id` INTEGER UNSIGNED NULL,
|
||||
`user_id` INTEGER UNSIGNED NOT NULL,
|
||||
`attachment_type` INTEGER UNSIGNED NULL,
|
||||
`text` MEDIUMTEXT NULL,
|
||||
`url` VARCHAR(1000) NULL DEFAULT '',
|
||||
`last_indexed` INTEGER NULL,
|
||||
`file_location` VARCHAR(500) NULL,
|
||||
`visible` TINYINT NULL DEFAULT 1,
|
||||
`scrape_state` TINYINT NOT NULL DEFAULT 2,
|
||||
`scrape_attempts` TINYINT NOT NULL DEFAULT 0,
|
||||
`enqueued_at` INTEGER NULL,
|
||||
|
||||
INDEX `attachment_scrape_state`(`scrape_state`, `enqueued_at`),
|
||||
INDEX `attachment_type`(`attachment_type`),
|
||||
INDEX `note_id`(`note_id`),
|
||||
INDEX `user_id`(`user_id`),
|
||||
FULLTEXT INDEX `attachment_text_index`(`text`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `attachment_type` (
|
||||
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`mime_type` VARCHAR(500) NULL,
|
||||
`extension` VARCHAR(10) NULL,
|
||||
`name` VARCHAR(50) NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `note` (
|
||||
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INTEGER UNSIGNED NULL,
|
||||
`note_raw_text_id` INTEGER NULL,
|
||||
`opened` INTEGER NULL,
|
||||
`created` INTEGER NULL,
|
||||
`pinned` INTEGER NULL DEFAULT 0,
|
||||
`archived` INTEGER NULL DEFAULT 0,
|
||||
`trashed` TINYINT NULL DEFAULT 0,
|
||||
`color` VARCHAR(500) NULL,
|
||||
`shared` INTEGER NULL DEFAULT 0,
|
||||
`share_user_id` INTEGER NULL,
|
||||
`quick_note` INTEGER NULL DEFAULT 0,
|
||||
`indexed` TINYINT NULL DEFAULT 0,
|
||||
`snippet` VARCHAR(1300) NULL,
|
||||
`snippet_salt` VARCHAR(1300) NULL,
|
||||
`encrypted_share_password_key` VARCHAR(1300) NULL,
|
||||
|
||||
INDEX `user_note`(`user_id`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `note_raw_text` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`text` LONGTEXT NULL,
|
||||
`updated` BIGINT NULL,
|
||||
`salt` VARCHAR(1300) NULL,
|
||||
`last_decrypted_date` INTEGER NULL,
|
||||
`decrypt_attempts_count` INTEGER NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `note_tag` (
|
||||
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`note_id` INTEGER UNSIGNED NULL,
|
||||
`tag_id` INTEGER UNSIGNED NULL,
|
||||
`user_id` INTEGER UNSIGNED NULL,
|
||||
|
||||
INDEX `note_id`(`note_id`),
|
||||
INDEX `tag_id`(`tag_id`),
|
||||
INDEX `user_id`(`user_id`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `scrape_cache` (
|
||||
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`url_hash` CHAR(64) NOT NULL,
|
||||
`url` VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
`canonical_url` VARCHAR(1000) NULL,
|
||||
`status` SMALLINT NOT NULL DEFAULT 0,
|
||||
`title` VARCHAR(512) NULL,
|
||||
`site_name` VARCHAR(255) NULL,
|
||||
`description` MEDIUMTEXT NULL,
|
||||
`keywords` VARCHAR(512) NULL,
|
||||
`search_text` MEDIUMTEXT NULL,
|
||||
`image_url` VARCHAR(1000) NULL,
|
||||
`file_location` VARCHAR(500) NULL,
|
||||
`etag` VARCHAR(255) NULL,
|
||||
`last_modified` VARCHAR(128) NULL,
|
||||
`content_type` VARCHAR(128) NULL,
|
||||
`fetch_state` VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
`failure_count` INTEGER NOT NULL DEFAULT 0,
|
||||
`last_error` VARCHAR(255) NULL,
|
||||
`fetched_at` INTEGER NULL,
|
||||
`expires_at` INTEGER NULL,
|
||||
|
||||
UNIQUE INDEX `scrape_cache_url_hash`(`url_hash`),
|
||||
INDEX `scrape_cache_expires_at`(`expires_at`),
|
||||
INDEX `scrape_cache_fetch_state`(`fetch_state`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `tag` (
|
||||
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`text` VARCHAR(400) NULL,
|
||||
`hash` INTEGER NULL,
|
||||
|
||||
INDEX `hash`(`hash`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `user` (
|
||||
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`username` VARCHAR(150) NOT NULL DEFAULT '',
|
||||
`password` TEXT NOT NULL,
|
||||
`salt` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`iterations` INTEGER NOT NULL,
|
||||
`last_login` DATETIME(0) NULL,
|
||||
`created` DATETIME(0) NULL,
|
||||
`two_fa_secret` VARCHAR(50) NULL,
|
||||
`two_fa_enabled` TINYINT NULL DEFAULT 0,
|
||||
`two_fa_every_login` TINYINT NULL DEFAULT 0,
|
||||
`pushkey` VARCHAR(100) NULL,
|
||||
|
||||
UNIQUE INDEX `id_UNIQUE`(`id`),
|
||||
UNIQUE INDEX `username_UNIQUE`(`username`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `user_active_session` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`salt` VARCHAR(100) NULL,
|
||||
`encrypted_master_password` VARCHAR(500) NULL,
|
||||
`created` INTEGER NULL,
|
||||
`uses` INTEGER NULL,
|
||||
`user_hash` VARCHAR(500) NULL,
|
||||
`active` TINYINT NULL DEFAULT 1,
|
||||
`session_id` VARCHAR(100) NULL,
|
||||
`last_used` INTEGER NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `user_encrypted_search_index` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INTEGER NULL,
|
||||
`salt` VARCHAR(100) NULL,
|
||||
`index` LONGTEXT NULL,
|
||||
`last_update` INTEGER NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `user_key` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INTEGER NULL,
|
||||
`salt` VARCHAR(1300) NULL,
|
||||
`key` VARCHAR(5000) NULL,
|
||||
`created` INTEGER NULL,
|
||||
`public_key` VARCHAR(5000) NULL,
|
||||
`private_key_encrypted` VARCHAR(5000) NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
@@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "mysql"
|
||||
@@ -1,151 +0,0 @@
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
}
|
||||
|
||||
model attachment {
|
||||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
note_id Int? @db.UnsignedInt
|
||||
user_id Int @db.UnsignedInt
|
||||
attachment_type Int? @db.UnsignedInt
|
||||
text String? @db.MediumText
|
||||
url String? @default("") @db.VarChar(1000)
|
||||
last_indexed Int?
|
||||
file_location String? @db.VarChar(500)
|
||||
visible Int? @default(1) @db.TinyInt
|
||||
scrape_state Int @default(2) @db.TinyInt
|
||||
scrape_attempts Int @default(0) @db.TinyInt
|
||||
enqueued_at Int?
|
||||
|
||||
@@index([scrape_state, enqueued_at], map: "attachment_scrape_state")
|
||||
@@index([attachment_type], map: "attachment_type")
|
||||
@@index([note_id], map: "note_id")
|
||||
@@index([user_id], map: "user_id")
|
||||
@@fulltext([text], map: "attachment_text_index")
|
||||
}
|
||||
|
||||
model attachment_type {
|
||||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
mime_type String? @db.VarChar(500)
|
||||
extension String? @db.VarChar(10)
|
||||
name String? @db.VarChar(50)
|
||||
}
|
||||
|
||||
model note {
|
||||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
user_id Int? @db.UnsignedInt
|
||||
note_raw_text_id Int?
|
||||
opened Int?
|
||||
created Int?
|
||||
pinned Int? @default(0)
|
||||
archived Int? @default(0)
|
||||
trashed Int? @default(0) @db.TinyInt
|
||||
color String? @db.VarChar(500)
|
||||
shared Int? @default(0)
|
||||
share_user_id Int?
|
||||
quick_note Int? @default(0)
|
||||
indexed Int? @default(0) @db.TinyInt
|
||||
snippet String? @db.VarChar(1300)
|
||||
snippet_salt String? @db.VarChar(1300)
|
||||
encrypted_share_password_key String? @db.VarChar(1300)
|
||||
|
||||
@@index([user_id], map: "user_note")
|
||||
}
|
||||
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model note_raw_text {
|
||||
id Int @id @default(autoincrement())
|
||||
text String? @db.LongText
|
||||
updated BigInt?
|
||||
salt String? @db.VarChar(1300)
|
||||
last_decrypted_date Int?
|
||||
decrypt_attempts_count Int? @default(0)
|
||||
}
|
||||
|
||||
model note_tag {
|
||||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
note_id Int? @db.UnsignedInt
|
||||
tag_id Int? @db.UnsignedInt
|
||||
user_id Int? @db.UnsignedInt
|
||||
|
||||
@@index([note_id], map: "note_id")
|
||||
@@index([tag_id], map: "tag_id")
|
||||
@@index([user_id], map: "user_id")
|
||||
}
|
||||
|
||||
model scrape_cache {
|
||||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
url_hash String @unique(map: "scrape_cache_url_hash") @db.Char(64)
|
||||
url String @default("") @db.VarChar(1000)
|
||||
canonical_url String? @db.VarChar(1000)
|
||||
status Int @default(0) @db.SmallInt
|
||||
title String? @db.VarChar(512)
|
||||
site_name String? @db.VarChar(255)
|
||||
description String? @db.MediumText
|
||||
keywords String? @db.VarChar(512)
|
||||
search_text String? @db.MediumText
|
||||
image_url String? @db.VarChar(1000)
|
||||
file_location String? @db.VarChar(500)
|
||||
etag String? @db.VarChar(255)
|
||||
last_modified String? @db.VarChar(128)
|
||||
content_type String? @db.VarChar(128)
|
||||
fetch_state String @default("pending") @db.VarChar(20)
|
||||
failure_count Int @default(0)
|
||||
last_error String? @db.VarChar(255)
|
||||
fetched_at Int?
|
||||
expires_at Int?
|
||||
|
||||
@@index([expires_at], map: "scrape_cache_expires_at")
|
||||
@@index([fetch_state], map: "scrape_cache_fetch_state")
|
||||
}
|
||||
|
||||
model tag {
|
||||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
text String? @db.VarChar(400)
|
||||
hash Int?
|
||||
|
||||
@@index([hash], map: "hash")
|
||||
}
|
||||
|
||||
model user {
|
||||
id Int @id @unique(map: "id_UNIQUE") @default(autoincrement()) @db.UnsignedInt
|
||||
username String @unique(map: "username_UNIQUE") @default("") @db.VarChar(150)
|
||||
password String @db.Text
|
||||
salt String @default("") @db.VarChar(500)
|
||||
iterations Int
|
||||
last_login DateTime? @db.DateTime(0)
|
||||
created DateTime? @db.DateTime(0)
|
||||
two_fa_secret String? @db.VarChar(50)
|
||||
two_fa_enabled Int? @default(0) @db.TinyInt
|
||||
two_fa_every_login Int? @default(0) @db.TinyInt
|
||||
pushkey String? @db.VarChar(100)
|
||||
}
|
||||
|
||||
model user_active_session {
|
||||
id Int @id @default(autoincrement())
|
||||
salt String? @db.VarChar(100)
|
||||
encrypted_master_password String? @db.VarChar(500)
|
||||
created Int?
|
||||
uses Int?
|
||||
user_hash String? @db.VarChar(500)
|
||||
active Int? @default(1) @db.TinyInt
|
||||
session_id String? @db.VarChar(100)
|
||||
last_used Int?
|
||||
}
|
||||
|
||||
model user_encrypted_search_index {
|
||||
id Int @id @default(autoincrement())
|
||||
user_id Int?
|
||||
salt String? @db.VarChar(100)
|
||||
index String? @db.LongText
|
||||
last_update Int?
|
||||
}
|
||||
|
||||
model user_key {
|
||||
id Int @id @default(autoincrement())
|
||||
user_id Int?
|
||||
salt String? @db.VarChar(1300)
|
||||
key String? @db.VarChar(5000)
|
||||
created Int?
|
||||
public_key String? @db.VarChar(5000)
|
||||
private_key_encrypted String? @db.VarChar(5000)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
--
|
||||
-- Reference data required for a working database.
|
||||
--
|
||||
-- Applied by ./migrateDatabase.sh after migrations. Kept out of the migration
|
||||
-- history on purpose: environments that were baselined with
|
||||
-- `prisma migrate resolve` never execute 0_init, so seed rows placed inside it
|
||||
-- would silently never exist there.
|
||||
--
|
||||
-- Idempotent, so re-running is harmless.
|
||||
--
|
||||
|
||||
-- attachment_type is a lookup table the application joins against.
|
||||
-- attachment_type = 1 is hardcoded for scraped URLs, 2 for uploaded images,
|
||||
-- 3 for PDFs (see server/models/Attachment.js). Without these rows a freshly
|
||||
-- scaffolded database creates attachments that reference nothing.
|
||||
INSERT INTO `attachment_type` (`id`, `mime_type`, `extension`, `name`) VALUES
|
||||
(1, NULL, NULL, 'URL'),
|
||||
(2, NULL, '.jpg', 'Image - Jpg'),
|
||||
(3, NULL, '.pdf', 'PDF'),
|
||||
(4, NULL, '.doc', 'DOC')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`mime_type` = VALUES(`mime_type`),
|
||||
`extension` = VALUES(`extension`),
|
||||
`name` = VALUES(`name`);
|
||||
@@ -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({
|
||||
|
||||