Compare commits
No commits in common. "v0.1.7-DEV-6" and "master" have entirely different histories.
v0.1.7-DEV
...
master
|
@ -1,8 +1,8 @@
|
||||||
/dist
|
/dist
|
||||||
/src-bex/www
|
|
||||||
/src-capacitor
|
/src-capacitor
|
||||||
/src-cordova
|
/src-cordova
|
||||||
/src-electron
|
|
||||||
/.quasar
|
/.quasar
|
||||||
/node_modules
|
/node_modules
|
||||||
|
.eslintrc.js
|
||||||
/src-ssr
|
/src-ssr
|
||||||
|
/quasar.config.*.temporary.compiled*
|
||||||
|
|
108
.eslintrc.cjs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
module.exports = {
|
||||||
|
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
|
||||||
|
// This option interrupts the configuration hierarchy at this file
|
||||||
|
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
|
||||||
|
root: true,
|
||||||
|
|
||||||
|
// https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser
|
||||||
|
// Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working
|
||||||
|
// `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted
|
||||||
|
parserOptions: {
|
||||||
|
parser: require.resolve("@typescript-eslint/parser"),
|
||||||
|
extraFileExtensions: [".vue"],
|
||||||
|
},
|
||||||
|
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
node: true,
|
||||||
|
"vue/setup-compiler-macros": true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Rules order is important, please avoid shuffling them
|
||||||
|
extends: [
|
||||||
|
// Base ESLint recommended rules
|
||||||
|
// 'eslint:recommended',
|
||||||
|
|
||||||
|
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage
|
||||||
|
// ESLint typescript rules
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
|
||||||
|
// Uncomment any of the lines below to choose desired strictness,
|
||||||
|
// but leave only one uncommented!
|
||||||
|
// See https://eslint.vuejs.org/rules/#available-rules
|
||||||
|
"plugin:vue/vue3-essential", // Priority A: Essential (Error Prevention)
|
||||||
|
"plugin:vue/vue3-strongly-recommended", // Priority B: Strongly Recommended (Improving Readability)
|
||||||
|
"plugin:vue/vue3-recommended", // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
|
||||||
|
|
||||||
|
"standard",
|
||||||
|
],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
// required to apply rules which need type information
|
||||||
|
"@typescript-eslint",
|
||||||
|
|
||||||
|
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
|
||||||
|
// required to lint *.vue files
|
||||||
|
"vue",
|
||||||
|
],
|
||||||
|
|
||||||
|
globals: {
|
||||||
|
ga: "readonly", // Google Analytics
|
||||||
|
cordova: "readonly",
|
||||||
|
__statics: "readonly",
|
||||||
|
__QUASAR_SSR__: "readonly",
|
||||||
|
__QUASAR_SSR_SERVER__: "readonly",
|
||||||
|
__QUASAR_SSR_CLIENT__: "readonly",
|
||||||
|
__QUASAR_SSR_PWA__: "readonly",
|
||||||
|
process: "readonly",
|
||||||
|
Capacitor: "readonly",
|
||||||
|
chrome: "readonly",
|
||||||
|
},
|
||||||
|
|
||||||
|
// add your custom rules here
|
||||||
|
rules: {
|
||||||
|
// allow async-await
|
||||||
|
"generator-star-spacing": "off",
|
||||||
|
// allow paren-less arrow functions
|
||||||
|
"arrow-parens": "off",
|
||||||
|
"one-var": "off",
|
||||||
|
"no-void": "off",
|
||||||
|
"multiline-ternary": "off",
|
||||||
|
|
||||||
|
"import/first": "off",
|
||||||
|
"import/namespace": "error",
|
||||||
|
"import/default": "error",
|
||||||
|
"import/export": "error",
|
||||||
|
"import/extensions": "off",
|
||||||
|
"import/no-unresolved": "off",
|
||||||
|
"import/no-extraneous-dependencies": "off",
|
||||||
|
|
||||||
|
// The core 'import/named' rules
|
||||||
|
// does not work with type definitions
|
||||||
|
"import/named": "off",
|
||||||
|
|
||||||
|
"prefer-promise-reject-errors": "off",
|
||||||
|
|
||||||
|
quotes: ["warn", "single", { avoidEscape: true }],
|
||||||
|
|
||||||
|
// this rule, if on, would require explicit return type on the `render` function
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
|
||||||
|
// in plain CommonJS modules, you can't use `import foo = require('foo')` to pass this rule, so it has to be disabled
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
|
||||||
|
// The core 'no-unused-vars' rules (in the eslint:recommended ruleset)
|
||||||
|
// does not work with type definitions
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
|
||||||
|
// allow debugger during development only
|
||||||
|
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
|
||||||
|
|
||||||
|
// CUSTOM RULES
|
||||||
|
"object-shorthand": "off",
|
||||||
|
"quote-props": "off",
|
||||||
|
camelcase: "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": ["error", { ignoreRestArgs: true }],
|
||||||
|
},
|
||||||
|
};
|
110
.eslintrc.js
|
@ -1,110 +0,0 @@
|
||||||
const { resolve } = require('path');
|
|
||||||
module.exports = {
|
|
||||||
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
|
|
||||||
// This option interrupts the configuration hierarchy at this file
|
|
||||||
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
|
|
||||||
root: true,
|
|
||||||
|
|
||||||
// https://eslint.vuejs.org/user-guide/#how-to-use-custom-parser
|
|
||||||
// Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working
|
|
||||||
// `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted
|
|
||||||
parserOptions: {
|
|
||||||
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#configuration
|
|
||||||
// https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#eslint
|
|
||||||
// Needed to make the parser take into account 'vue' files
|
|
||||||
extraFileExtensions: ['.vue'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
project: resolve(__dirname, './tsconfig.json'),
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
|
|
||||||
sourceType: 'module' // Allows for the use of imports
|
|
||||||
},
|
|
||||||
|
|
||||||
env: {
|
|
||||||
browser: true
|
|
||||||
},
|
|
||||||
|
|
||||||
// Rules order is important, please avoid shuffling them
|
|
||||||
extends: [
|
|
||||||
// Base ESLint recommended rules
|
|
||||||
// 'eslint:recommended',
|
|
||||||
|
|
||||||
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage
|
|
||||||
// ESLint typescript rules
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
// consider disabling this class of rules if linting takes too long
|
|
||||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
|
||||||
|
|
||||||
// Uncomment any of the lines below to choose desired strictness,
|
|
||||||
// but leave only one uncommented!
|
|
||||||
// See https://eslint.vuejs.org/rules/#available-rules
|
|
||||||
'plugin:vue/essential', // Priority A: Essential (Error Prevention)
|
|
||||||
// 'plugin:vue/strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
|
|
||||||
// 'plugin:vue/recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
|
|
||||||
|
|
||||||
'standard'
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
// required to apply rules which need type information
|
|
||||||
'@typescript-eslint',
|
|
||||||
|
|
||||||
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
|
|
||||||
// required to lint *.vue files
|
|
||||||
'vue',
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
globals: {
|
|
||||||
ga: false, // Google Analytics
|
|
||||||
cordova: true,
|
|
||||||
|
|
||||||
process: true,
|
|
||||||
Capacitor: true,
|
|
||||||
chrome: true
|
|
||||||
},
|
|
||||||
|
|
||||||
// add your custom rules here
|
|
||||||
rules: {
|
|
||||||
// allow async-await
|
|
||||||
'generator-star-spacing': 'off',
|
|
||||||
// allow paren-less arrow functions
|
|
||||||
'arrow-parens': 'off',
|
|
||||||
'one-var': 'off',
|
|
||||||
'brace-style': ["error", "stroustrup"],
|
|
||||||
'import/first': 'off',
|
|
||||||
'import/named': 'error',
|
|
||||||
'import/namespace': 'error',
|
|
||||||
'import/default': 'error',
|
|
||||||
'import/export': 'error',
|
|
||||||
'import/extensions': 'off',
|
|
||||||
'import/no-unresolved': 'off',
|
|
||||||
'import/no-extraneous-dependencies': 'off',
|
|
||||||
'prefer-promise-reject-errors': 'off',
|
|
||||||
|
|
||||||
// TypeScript
|
|
||||||
quotes: ['warn', 'single', { avoidEscape: true }],
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
||||||
|
|
||||||
// allow debugger during development only
|
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
|
||||||
|
|
||||||
"quotes": [1, "double"],
|
|
||||||
"semi": [1, "never"],
|
|
||||||
"curly": [1, "all"],
|
|
||||||
"no-trailing-spaces": [1],
|
|
||||||
//"indent": [2, 2],
|
|
||||||
"@typescript-eslint/indent": [1, 2],
|
|
||||||
"vue/no-v-html": [0],
|
|
||||||
"camelcase": 'off',
|
|
||||||
'@typescript-eslint/camelcase': 'off',
|
|
||||||
'@typescript-eslint/unbound-method': 'off',
|
|
||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
|
||||||
'@typescript-eslint/ban-types': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-member-access':'off'
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
22
.github/workflows/build.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
# This is a basic workflow that is manually triggered
|
# This is a basic workflow that is manually triggered
|
||||||
|
|
||||||
name: Build App
|
name: Build App - FA Rewrite
|
||||||
|
|
||||||
# Manually triggered
|
# Manually triggered
|
||||||
on:
|
on:
|
||||||
|
@ -15,22 +15,25 @@ jobs:
|
||||||
# Builds all major OS versions of FA
|
# Builds all major OS versions of FA
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
environment: production
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||||
|
node: [16]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the code
|
- name: Check out the code
|
||||||
uses: actions/checkout@v2.3.4
|
uses: actions/checkout@v2.3.4
|
||||||
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
- name: Setup Node.js environment
|
||||||
uses: actions/setup-node@v2.1.5
|
uses: actions/setup-node@v2.1.5
|
||||||
|
|
||||||
- name: npm install and build
|
- name: quasar install and build
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install --global yarn@1.22.19
|
||||||
npm run build
|
yarn global add @quasar/cli
|
||||||
|
yarn install
|
||||||
|
yarn build
|
||||||
|
|
||||||
- name: Upload Linux Artifact
|
- name: Upload Linux Artifact
|
||||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
|
@ -38,7 +41,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
# Artifact name
|
# Artifact name
|
||||||
name: Linux_FA ${{ github.event.inputs.version }}
|
name: Linux_FA ${{ github.event.inputs.version }}
|
||||||
path: dist/electron/Packaged/
|
path: |
|
||||||
|
dist/electron/Packaged/*.snap
|
||||||
|
dist/electron/Packaged/*.AppImage
|
||||||
|
|
||||||
- name: Upload MacOS Artifact
|
- name: Upload MacOS Artifact
|
||||||
if: ${{ matrix.os == 'macOS-latest' }}
|
if: ${{ matrix.os == 'macOS-latest' }}
|
||||||
|
@ -46,7 +51,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
# Artifact name
|
# Artifact name
|
||||||
name: Mac_FA ${{ github.event.inputs.version }}
|
name: Mac_FA ${{ github.event.inputs.version }}
|
||||||
path: dist/electron/Packaged/
|
path: dist/electron/Packaged/*.dmg
|
||||||
|
|
||||||
- name: Upload Windows Artifact
|
- name: Upload Windows Artifact
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
|
@ -54,5 +59,4 @@ jobs:
|
||||||
with:
|
with:
|
||||||
# Artifact name
|
# Artifact name
|
||||||
name: Windows_FA ${{ github.event.inputs.version }}
|
name: Windows_FA ${{ github.event.inputs.version }}
|
||||||
path: dist/electron/Packaged/
|
path: dist/electron/Packaged/*.exe
|
||||||
|
|
||||||
|
|
12
.gitignore
vendored
|
@ -1,11 +1,11 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.thumbs.db
|
.thumbs.db
|
||||||
.history
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# Quasar core related directories
|
# Quasar core related directories
|
||||||
.quasar
|
.quasar
|
||||||
/dist
|
/dist
|
||||||
|
/quasar.config.*.temporary.compiled*
|
||||||
|
|
||||||
# Cordova related directories and files
|
# Cordova related directories and files
|
||||||
/src-cordova/node_modules
|
/src-cordova/node_modules
|
||||||
|
@ -28,8 +28,16 @@ yarn-error.log*
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
|
|
||||||
|
# local .env files
|
||||||
|
.env.local*
|
||||||
|
|
||||||
|
# Local Playwright test results folder
|
||||||
|
/test-results
|
||||||
|
|
||||||
|
.nyc_output
|
||||||
|
coverage/
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
// https://github.com/michael-ciniawsky/postcss-load-config
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
plugins: [
|
|
||||||
// to edit target browsers: use "browserslist" field in package.json
|
|
||||||
require('autoprefixer')
|
|
||||||
]
|
|
||||||
}
|
|
14
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"vue.volar",
|
||||||
|
"wayou.vscode-todo-highlight"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"octref.vetur",
|
||||||
|
"hookyqr.beautify",
|
||||||
|
"dbaeumer.jshint",
|
||||||
|
"ms-vscode.vscode-typescript-tslint-plugin"
|
||||||
|
]
|
||||||
|
}
|
23
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"editor.bracketPairColorization.enabled": true,
|
||||||
|
"editor.guides.bracketPairs": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||||
|
"editor.codeActionsOnSave": [
|
||||||
|
"source.fixAll.eslint"
|
||||||
|
],
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"vue"
|
||||||
|
],
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"todo-tree.tree.scanMode": "workspace",
|
||||||
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +0,0 @@
|
||||||
patreon: elvanos
|
|
||||||
github: Elvanos
|
|
676
LICENSE.md
|
@ -1,676 +0,0 @@
|
||||||
|
|
||||||
### GNU GENERAL PUBLIC LICENSE
|
|
||||||
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc.
|
|
||||||
<https://fsf.org/>
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
### Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom
|
|
||||||
to share and change all versions of a program--to make sure it remains
|
|
||||||
free software for all its users. We, the Free Software Foundation, use
|
|
||||||
the GNU General Public License for most of our software; it applies
|
|
||||||
also to any other work released this way by its authors. You can apply
|
|
||||||
it to your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you
|
|
||||||
have certain responsibilities if you distribute copies of the
|
|
||||||
software, or if you modify it: responsibilities to respect the freedom
|
|
||||||
of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the
|
|
||||||
manufacturer can do so. This is fundamentally incompatible with the
|
|
||||||
aim of protecting users' freedom to change the software. The
|
|
||||||
systematic pattern of such abuse occurs in the area of products for
|
|
||||||
individuals to use, which is precisely where it is most unacceptable.
|
|
||||||
Therefore, we have designed this version of the GPL to prohibit the
|
|
||||||
practice for those products. If such problems arise substantially in
|
|
||||||
other domains, we stand ready to extend this provision to those
|
|
||||||
domains in future versions of the GPL, as needed to protect the
|
|
||||||
freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish
|
|
||||||
to avoid the special danger that patents applied to a free program
|
|
||||||
could make it effectively proprietary. To prevent this, the GPL
|
|
||||||
assures that patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
### TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
#### 0. Definitions
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds
|
|
||||||
of works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of
|
|
||||||
an exact copy. The resulting work is called a "modified version" of
|
|
||||||
the earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user
|
|
||||||
through a computer network, with no transfer of a copy, is not
|
|
||||||
conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices" to
|
|
||||||
the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
#### 1. Source Code
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work for
|
|
||||||
making modifications to it. "Object code" means any non-source form of
|
|
||||||
a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users can
|
|
||||||
regenerate automatically from other parts of the Corresponding Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that same
|
|
||||||
work.
|
|
||||||
|
|
||||||
#### 2. Basic Permissions
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not convey,
|
|
||||||
without conditions so long as your license otherwise remains in force.
|
|
||||||
You may convey covered works to others for the sole purpose of having
|
|
||||||
them make modifications exclusively for you, or provide you with
|
|
||||||
facilities for running those works, provided that you comply with the
|
|
||||||
terms of this License in conveying all material for which you do not
|
|
||||||
control copyright. Those thus making or running the covered works for
|
|
||||||
you must do so exclusively on your behalf, under your direction and
|
|
||||||
control, on terms that prohibit them from making any copies of your
|
|
||||||
copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under the
|
|
||||||
conditions stated below. Sublicensing is not allowed; section 10 makes
|
|
||||||
it unnecessary.
|
|
||||||
|
|
||||||
#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such
|
|
||||||
circumvention is effected by exercising rights under this License with
|
|
||||||
respect to the covered work, and you disclaim any intention to limit
|
|
||||||
operation or modification of the work as a means of enforcing, against
|
|
||||||
the work's users, your or third parties' legal rights to forbid
|
|
||||||
circumvention of technological measures.
|
|
||||||
|
|
||||||
#### 4. Conveying Verbatim Copies
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
#### 5. Conveying Modified Source Versions
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these
|
|
||||||
conditions:
|
|
||||||
|
|
||||||
- a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
- b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under
|
|
||||||
section 7. This requirement modifies the requirement in section 4
|
|
||||||
to "keep intact all notices".
|
|
||||||
- c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
- d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
#### 6. Conveying Non-Source Forms
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms of
|
|
||||||
sections 4 and 5, provided that you also convey the machine-readable
|
|
||||||
Corresponding Source under the terms of this License, in one of these
|
|
||||||
ways:
|
|
||||||
|
|
||||||
- a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
- b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the Corresponding
|
|
||||||
Source from a network server at no charge.
|
|
||||||
- c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
- d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
- e) Convey the object code using peer-to-peer transmission,
|
|
||||||
provided you inform other peers where the object code and
|
|
||||||
Corresponding Source of the work are being offered to the general
|
|
||||||
public at no charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal,
|
|
||||||
family, or household purposes, or (2) anything designed or sold for
|
|
||||||
incorporation into a dwelling. In determining whether a product is a
|
|
||||||
consumer product, doubtful cases shall be resolved in favor of
|
|
||||||
coverage. For a particular product received by a particular user,
|
|
||||||
"normally used" refers to a typical or common use of that class of
|
|
||||||
product, regardless of the status of the particular user or of the way
|
|
||||||
in which the particular user actually uses, or expects or is expected
|
|
||||||
to use, the product. A product is a consumer product regardless of
|
|
||||||
whether the product has substantial commercial, industrial or
|
|
||||||
non-consumer uses, unless such uses represent the only significant
|
|
||||||
mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to
|
|
||||||
install and execute modified versions of a covered work in that User
|
|
||||||
Product from a modified version of its Corresponding Source. The
|
|
||||||
information must suffice to ensure that the continued functioning of
|
|
||||||
the modified object code is in no case prevented or interfered with
|
|
||||||
solely because modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or
|
|
||||||
updates for a work that has been modified or installed by the
|
|
||||||
recipient, or for the User Product in which it has been modified or
|
|
||||||
installed. Access to a network may be denied when the modification
|
|
||||||
itself materially and adversely affects the operation of the network
|
|
||||||
or violates the rules and protocols for communication across the
|
|
||||||
network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
#### 7. Additional Terms
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders
|
|
||||||
of that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
- a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
- b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
- c) Prohibiting misrepresentation of the origin of that material,
|
|
||||||
or requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
- d) Limiting the use for publicity purposes of names of licensors
|
|
||||||
or authors of the material; or
|
|
||||||
- e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
- f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions
|
|
||||||
of it) with contractual assumptions of liability to the recipient,
|
|
||||||
for any liability that these contractual assumptions directly
|
|
||||||
impose on those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions; the
|
|
||||||
above requirements apply either way.
|
|
||||||
|
|
||||||
#### 8. Termination
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your license
|
|
||||||
from a particular copyright holder is reinstated (a) provisionally,
|
|
||||||
unless and until the copyright holder explicitly and finally
|
|
||||||
terminates your license, and (b) permanently, if the copyright holder
|
|
||||||
fails to notify you of the violation by some reasonable means prior to
|
|
||||||
60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
#### 9. Acceptance Not Required for Having Copies
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or run
|
|
||||||
a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
#### 10. Automatic Licensing of Downstream Recipients
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
#### 11. Patents
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims owned
|
|
||||||
or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within the
|
|
||||||
scope of its coverage, prohibits the exercise of, or is conditioned on
|
|
||||||
the non-exercise of one or more of the rights that are specifically
|
|
||||||
granted under this License. You may not convey a covered work if you
|
|
||||||
are a party to an arrangement with a third party that is in the
|
|
||||||
business of distributing software, under which you make payment to the
|
|
||||||
third party based on the extent of your activity of conveying the
|
|
||||||
work, and under which the third party grants, to any of the parties
|
|
||||||
who would receive the covered work from you, a discriminatory patent
|
|
||||||
license (a) in connection with copies of the covered work conveyed by
|
|
||||||
you (or copies made from those copies), or (b) primarily for and in
|
|
||||||
connection with specific products or compilations that contain the
|
|
||||||
covered work, unless you entered into that arrangement, or that patent
|
|
||||||
license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
#### 12. No Surrender of Others' Freedom
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under
|
|
||||||
this License and any other pertinent obligations, then as a
|
|
||||||
consequence you may not convey it at all. For example, if you agree to
|
|
||||||
terms that obligate you to collect a royalty for further conveying
|
|
||||||
from those to whom you convey the Program, the only way you could
|
|
||||||
satisfy both those terms and this License would be to refrain entirely
|
|
||||||
from conveying the Program.
|
|
||||||
|
|
||||||
#### 13. Use with the GNU Affero General Public License
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
#### 14. Revised Versions of this License
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions
|
|
||||||
of the GNU General Public License from time to time. Such new versions
|
|
||||||
will be similar in spirit to the present version, but may differ in
|
|
||||||
detail to address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Program
|
|
||||||
specifies that a certain numbered version of the GNU General Public
|
|
||||||
License "or any later version" applies to it, you have the option of
|
|
||||||
following the terms and conditions either of that numbered version or
|
|
||||||
of any later version published by the Free Software Foundation. If the
|
|
||||||
Program does not specify a version number of the GNU General Public
|
|
||||||
License, you may choose any version ever published by the Free
|
|
||||||
Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future versions
|
|
||||||
of the GNU General Public License can be used, that proxy's public
|
|
||||||
statement of acceptance of a version permanently authorizes you to
|
|
||||||
choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
#### 15. Disclaimer of Warranty
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
|
|
||||||
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
|
|
||||||
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
|
|
||||||
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
|
|
||||||
CORRECTION.
|
|
||||||
|
|
||||||
#### 16. Limitation of Liability
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
|
|
||||||
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
|
|
||||||
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
|
|
||||||
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
|
|
||||||
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
|
||||||
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
|
|
||||||
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
||||||
|
|
||||||
#### 17. Interpretation of Sections 15 and 16
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
### How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these
|
|
||||||
terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest to
|
|
||||||
attach them to the start of each source file to most effectively state
|
|
||||||
the exclusion of warranty; and each file should have at least the
|
|
||||||
"copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper
|
|
||||||
mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands \`show w' and \`show c' should show the
|
|
||||||
appropriate parts of the General Public License. Of course, your
|
|
||||||
program's commands might be different; for a GUI interface, you would
|
|
||||||
use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or
|
|
||||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
|
||||||
necessary. For more information on this, and how to apply and follow
|
|
||||||
the GNU GPL, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your
|
|
||||||
program into proprietary programs. If your program is a subroutine
|
|
||||||
library, you may consider it more useful to permit linking proprietary
|
|
||||||
applications with the library. If this is what you want to do, use the
|
|
||||||
GNU Lesser General Public License instead of this License. But first,
|
|
||||||
please read <https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
75
README.md
|
@ -1,56 +1,47 @@
|
||||||
# Fantasia Archive (fantasiaarchive)
|
# Fantasia Archive (fantasia-archive)
|
||||||
|
|
||||||
A database manager for world building
|
A worldbuilding database manager
|
||||||
|
|
||||||
# How to Install
|
Use Yarn 1.22.19 or stuff is gonna bug out.
|
||||||
|
|
||||||
## Install Node.JS
|
Make sure you are running this with Node v16.17.0 ("nvm" is great for these older versions)
|
||||||
|
|
||||||
Simply install Node.JS for your system. You can get it straight from their site, or on Linux, install it through your package manager.
|
> Playwright tests run from built, live version of FA. Therefore, to run them, you need to localy build the app on your machine first - Both on first time using them and every time something is changed in the source code.
|
||||||
|
|
||||||
## Download the program
|
## Install Quasar CLI for smoothest experience
|
||||||
|
##### Details found here: https://quasar.dev/start/quasar-cli
|
||||||
|
|
||||||
Click on the green code button above, and download the repository as a zip file. Extract the zip file and move the resulting fantasia-archive-master folder to your home directory.
|
##### Ensure that the Yarn global install location is in your PATH after install. (details in article linked above)
|
||||||
|
|
||||||
## Install the dependencies
|
|
||||||
|
|
||||||
In the terminal, do the following:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd fantasia-archive-master
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Optional: Start the app in development mode (hot-code reloading, error reporting, etc.)
|
|
||||||
|
|
||||||
If you don't know what's going on, skip this step.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build the app for production
|
|
||||||
|
|
||||||
Once again in the terminal, run the following:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
After it is complete, go to /dist/electron/Packaged and select the dmg or AppImage file there and run it. You have finished installing the program.
|
|
||||||
|
|
||||||
### New object files go into
|
|
||||||
|
|
||||||
```
|
```
|
||||||
src\databaseManager\blueprints
|
yarn global add @quasar/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
### Document configuration details can be found in the following file
|
## Install the dependencies and set up the project
|
||||||
|
|
||||||
```
|
```
|
||||||
I_Blueprint.ts
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start the app in Quasar development mode (hot-code reloading, error reporting, etc.)
|
||||||
|
```
|
||||||
|
quasar dev -m electron
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build the app for production
|
||||||
|
```
|
||||||
|
quasar build -m electron
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing:
|
||||||
|
|
||||||
|
#### Component test - via Playwright
|
||||||
|
```
|
||||||
|
test:component
|
||||||
|
```
|
||||||
|
#### E2E test - via Playwright
|
||||||
|
```
|
||||||
|
test:e2e
|
||||||
```
|
```
|
||||||
|
|
||||||
### Customize the configuration
|
### Customize the configuration
|
||||||
|
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).
|
||||||
See [Configuring quasar.conf.js](https://quasar.dev/quasar-cli/quasar-conf-js).
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
/* eslint-env node */
|
|
||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
"@quasar/babel-preset-app"
|
|
||||||
]
|
|
||||||
}
|
|
57
index.html
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= productName %></title>
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="description" content="<%= productDescription %>">
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
<meta name="msapplication-tap-highlight" content="no">
|
||||||
|
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
|
||||||
|
<link rel="icon" type="image/ico" href="favicon.ico">
|
||||||
|
<style>
|
||||||
|
.splashLoadingWrapper{
|
||||||
|
background-color: #18303a;
|
||||||
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
transition: 0.3s opacity ease-in;
|
||||||
|
transition-delay: 2s;
|
||||||
|
z-index: 9999999999999999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splashLoading{
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splashLoading path{
|
||||||
|
fill: #d7ac47;
|
||||||
|
}
|
||||||
|
|
||||||
|
#q-app[data-v-app] ~ .splashLoadingWrapper{
|
||||||
|
opacity: 0 !important;
|
||||||
|
user-select: none !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- quasar:entry-point -->
|
||||||
|
<div class="splashLoadingWrapper" data-test="appSplashScreen-wrapper">
|
||||||
|
<svg class="splashLoading" width="350px" height="350px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-20,-20)"><path d="M79.9,52.6C80,51.8,80,50.9,80,50s0-1.8-0.1-2.6l-5.1-0.4c-0.3-2.4-0.9-4.6-1.8-6.7l4.2-2.9c-0.7-1.6-1.6-3.1-2.6-4.5 L70,35c-1.4-1.9-3.1-3.5-4.9-4.9l2.2-4.6c-1.4-1-2.9-1.9-4.5-2.6L59.8,27c-2.1-0.9-4.4-1.5-6.7-1.8l-0.4-5.1C51.8,20,50.9,20,50,20 s-1.8,0-2.6,0.1l-0.4,5.1c-2.4,0.3-4.6,0.9-6.7,1.8l-2.9-4.1c-1.6,0.7-3.1,1.6-4.5,2.6l2.1,4.6c-1.9,1.4-3.5,3.1-5,4.9l-4.5-2.1 c-1,1.4-1.9,2.9-2.6,4.5l4.1,2.9c-0.9,2.1-1.5,4.4-1.8,6.8l-5,0.4C20,48.2,20,49.1,20,50s0,1.8,0.1,2.6l5,0.4 c0.3,2.4,0.9,4.7,1.8,6.8l-4.1,2.9c0.7,1.6,1.6,3.1,2.6,4.5l4.5-2.1c1.4,1.9,3.1,3.5,5,4.9l-2.1,4.6c1.4,1,2.9,1.9,4.5,2.6l2.9-4.1 c2.1,0.9,4.4,1.5,6.7,1.8l0.4,5.1C48.2,80,49.1,80,50,80s1.8,0,2.6-0.1l0.4-5.1c2.3-0.3,4.6-0.9,6.7-1.8l2.9,4.2 c1.6-0.7,3.1-1.6,4.5-2.6L65,69.9c1.9-1.4,3.5-3,4.9-4.9l4.6,2.2c1-1.4,1.9-2.9,2.6-4.5L73,59.8c0.9-2.1,1.5-4.4,1.8-6.7L79.9,52.6 z M50,65c-8.3,0-15-6.7-15-15c0-8.3,6.7-15,15-15s15,6.7,15,15C65,58.3,58.3,65,50,65z" fill="currentColor"><animateTransform attributeName="transform" type="rotate" from="90 50 50" to="0 50 50" dur="1s" repeatCount="indefinite"></animateTransform></path></g><g transform="translate(20,20) rotate(15 50 50)"><path d="M79.9,52.6C80,51.8,80,50.9,80,50s0-1.8-0.1-2.6l-5.1-0.4c-0.3-2.4-0.9-4.6-1.8-6.7l4.2-2.9c-0.7-1.6-1.6-3.1-2.6-4.5 L70,35c-1.4-1.9-3.1-3.5-4.9-4.9l2.2-4.6c-1.4-1-2.9-1.9-4.5-2.6L59.8,27c-2.1-0.9-4.4-1.5-6.7-1.8l-0.4-5.1C51.8,20,50.9,20,50,20 s-1.8,0-2.6,0.1l-0.4,5.1c-2.4,0.3-4.6,0.9-6.7,1.8l-2.9-4.1c-1.6,0.7-3.1,1.6-4.5,2.6l2.1,4.6c-1.9,1.4-3.5,3.1-5,4.9l-4.5-2.1 c-1,1.4-1.9,2.9-2.6,4.5l4.1,2.9c-0.9,2.1-1.5,4.4-1.8,6.8l-5,0.4C20,48.2,20,49.1,20,50s0,1.8,0.1,2.6l5,0.4 c0.3,2.4,0.9,4.7,1.8,6.8l-4.1,2.9c0.7,1.6,1.6,3.1,2.6,4.5l4.5-2.1c1.4,1.9,3.1,3.5,5,4.9l-2.1,4.6c1.4,1,2.9,1.9,4.5,2.6l2.9-4.1 c2.1,0.9,4.4,1.5,6.7,1.8l0.4,5.1C48.2,80,49.1,80,50,80s1.8,0,2.6-0.1l0.4-5.1c2.3-0.3,4.6-0.9,6.7-1.8l2.9,4.2 c1.6-0.7,3.1-1.6,4.5-2.6L65,69.9c1.9-1.4,3.5-3,4.9-4.9l4.6,2.2c1-1.4,1.9-2.9,2.6-4.5L73,59.8c0.9-2.1,1.5-4.4,1.8-6.7L79.9,52.6 z M50,65c-8.3,0-15-6.7-15-15c0-8.3,6.7-15,15-15s15,6.7,15,15C65,58.3,58.3,65,50,65z" fill="currentColor"><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="90 50 50" dur="1s" repeatCount="indefinite"></animateTransform></path></g></svg>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
61
interfaces/I_appMenusDataList.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
export interface I_appMenusDataList {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Title of the main menu button
|
||||||
|
*/
|
||||||
|
title: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data contents of the menu dropdown
|
||||||
|
*/
|
||||||
|
data:{
|
||||||
|
/**
|
||||||
|
* Determines if the item shows as a separator or full-fledged menu item
|
||||||
|
*/
|
||||||
|
mode: 'separator' | 'item'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Title of the menu item
|
||||||
|
*/
|
||||||
|
text?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon/Avatar of the menu item
|
||||||
|
*/
|
||||||
|
icon?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger functionality of the item on click
|
||||||
|
*/
|
||||||
|
trigger?: ((...args: any[]) => unknown|void)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra arguments for the trigger if need be
|
||||||
|
*/
|
||||||
|
triggerArguments?: unknown[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditions to show the menu item as active
|
||||||
|
*/
|
||||||
|
conditions?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Special color class for the item
|
||||||
|
*/
|
||||||
|
specialColor?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determined if the item contains a submenu and its inner data
|
||||||
|
*/
|
||||||
|
submenu?: {
|
||||||
|
mode: 'separator' | 'item'
|
||||||
|
text?: string
|
||||||
|
icon?: string
|
||||||
|
trigger?: ((...args: any[]) => unknown|void)
|
||||||
|
triggerArguments?: []
|
||||||
|
conditions?: boolean
|
||||||
|
specialColor?: string
|
||||||
|
}[]
|
||||||
|
|
||||||
|
}[]
|
||||||
|
}
|
34
interfaces/I_extraEnvVariablesAPI.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
export interface I_extraEnvVariablesAPI {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full path to "electron-main.js" file in the dist, unpackaged form
|
||||||
|
*/
|
||||||
|
ELECTRON_MAIN_FILEPATH: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra rended timer buffer for tests to start after loading the app.
|
||||||
|
* - Increase if your machine isn't keeping up with the render times and tests are randomly failing.
|
||||||
|
* - Lower if your machine is quick and the tests are waiting for no reason at all.
|
||||||
|
* - Can be set manually for each component/e2e test inside the test file.
|
||||||
|
*/
|
||||||
|
FA_FRONTEND_RENDER_TIMER: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of test environment to load.
|
||||||
|
* - 'components'
|
||||||
|
* - 'e2e'
|
||||||
|
*/
|
||||||
|
TEST_ENV?: string|false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the component being tested.
|
||||||
|
* - MUST match the file name of the vue file being tested (including the capital letter at the start).
|
||||||
|
*/
|
||||||
|
COMPONENT_NAME?: string|false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component props, assuming they have any.
|
||||||
|
*/
|
||||||
|
COMPONENT_PROPS?: string|false
|
||||||
|
|
||||||
|
}
|
25
interfaces/I_faDevToolsControl.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
export interface I_faDevToolsControl {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the current state of the DevTools in the opened FA instance
|
||||||
|
*/
|
||||||
|
checkDecToolsStatus: () => boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the dev tools
|
||||||
|
* - If they are opened, close them
|
||||||
|
* - If they are closed, open them
|
||||||
|
*/
|
||||||
|
toggleDevTools: () => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the dev tools
|
||||||
|
*/
|
||||||
|
openDevTools: () => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the dev tools
|
||||||
|
*/
|
||||||
|
closeDevTools: () => void
|
||||||
|
|
||||||
|
}
|
15
interfaces/I_faExternalLinksManagerAPI.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export interface I_faExternalLinksManagerAPI {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the type of link input
|
||||||
|
* true - Is external
|
||||||
|
* false - is not external
|
||||||
|
*/
|
||||||
|
checkIfExternal: (url: string) => boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open external link in the default browser of the user
|
||||||
|
*/
|
||||||
|
openExternal: (url: string) => void
|
||||||
|
|
||||||
|
}
|
30
interfaces/I_faWindowControlAPI.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
export interface I_faWindowControlAPI {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the current visual sizing of the current window
|
||||||
|
*/
|
||||||
|
checkWindowMaximized: () => boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimizes the current window
|
||||||
|
*/
|
||||||
|
minimizeWindow: () => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mazimizes the current window
|
||||||
|
*/
|
||||||
|
maximizeWindow: () => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resizes the current window.
|
||||||
|
* - If the window is maximized, smallifies it
|
||||||
|
* - If the window is smallified, maximizes it
|
||||||
|
*/
|
||||||
|
resizeWindow: () => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the current window
|
||||||
|
*/
|
||||||
|
closeWindow: () => void
|
||||||
|
|
||||||
|
}
|
1
interfaces/T_documentList.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export type T_documentList = 'advancedSearchCheatSheet'| 'advancedSearchGuide'| 'changeLog'| 'license' | 'test' | ''
|
18550
package-lock.json
generated
116
package.json
|
@ -1,83 +1,63 @@
|
||||||
{
|
{
|
||||||
"name": "fantasiaarchive",
|
"name": "fantasia-archive",
|
||||||
"version": "0.1.7",
|
"version": "0.0.1",
|
||||||
"description": "A database manager for world building",
|
"description": "A worldbuilding database manager",
|
||||||
"productName": "Fantasia Archive",
|
"productName": "Fantasia Archive",
|
||||||
"author": "Elvanos <elvanos66@gmail.com>",
|
"author": "Elvanos <elvanos66@gmail.com>",
|
||||||
"license": "GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint --ext .js,.ts,.vue ./",
|
"lint": "eslint --ext .js,.ts,.vue ./",
|
||||||
"test": "echo \"No test specified\" && exit 0",
|
"dev:electron": "quasar dev -m electron",
|
||||||
"dev": "quasar dev -m electron",
|
"build": "quasar build -m electron --publish never",
|
||||||
"build": "quasar build -m electron"
|
"test:component": "node \"node_modules/@playwright/test/cli.js\" test src/components",
|
||||||
|
"test:e2e": "node \"node_modules/@playwright/test/cli.js\" test test/playwright-e2e/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.0.0",
|
"@electron/remote": "^2.0.10",
|
||||||
"@types/fs-extra": "^8.1.0",
|
"@quasar/cli": "^2.3.0",
|
||||||
"apexcharts": "^3.26.0",
|
"@quasar/extras": "^1.16.4",
|
||||||
"axios": "^0.18.1",
|
"app-root-path": "^3.1.0",
|
||||||
"core-js": "^3.6.5",
|
"axios": "^1.2.1",
|
||||||
"katex": "^0.12.0",
|
"lodash-es": "^4.17.21",
|
||||||
"lodash": "^4.17.20",
|
"pinia": "^2.0.11",
|
||||||
"mermaid": "^8.8.4",
|
"quasar": "^2.6.0",
|
||||||
"pouchdb": "^7.2.2",
|
"sqlite3": "^5.1.6",
|
||||||
"pouchdb-adapter-idb": "^7.2.2",
|
"uuid": "^9.0.1",
|
||||||
"pouchdb-find": "^7.2.2",
|
"vue": "^3.0.0",
|
||||||
"pouchdb-load": "^1.4.6",
|
"vue-i18n": "^9.2.2",
|
||||||
"pouchdb-purge-on-delete": "^7.0.1",
|
"vue-router": "^4.0.0"
|
||||||
"pouchdb-replication-stream": "^1.2.9",
|
|
||||||
"quasar": "^1.15.0",
|
|
||||||
"quasar-tiptap": "^1.9.1",
|
|
||||||
"tiptap": "^1.31.0",
|
|
||||||
"tiptap-extensions": "^1.34.0",
|
|
||||||
"vue-apexcharts": "^1.6.0",
|
|
||||||
"vue-class-component": "^7.2.2",
|
|
||||||
"vue-codemirror": "^4.0.6",
|
|
||||||
"vue-i18n": "^8.0.0",
|
|
||||||
"vue-property-decorator": "^8.3.0",
|
|
||||||
"vuex-class": "^0.3.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app": "^2.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
|
||||||
"@quasar/quasar-app-extension-qmarkdown": "^1.4.1",
|
"@playwright/test": "^1.37.1",
|
||||||
"@quasar/quasar-app-extension-qwindow": "^1.0.0-beta.7",
|
"@quasar/app-vite": "^1.3.0",
|
||||||
"@types/lodash": "^4.14.166",
|
"@quasar/quasar-app-extension-qmarkdown": "^2.0.0-beta.10",
|
||||||
"@types/node": "^10.17.15",
|
"@types/lodash-es": "^4.17.10",
|
||||||
"@types/pouchdb": "^6.4.0",
|
"@types/node": "^16.18.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.3.0",
|
"@types/uuid": "^9.0.5",
|
||||||
"@typescript-eslint/parser": "^3.3.0",
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
"babel-eslint": "^10.0.1",
|
"@typescript-eslint/parser": "^5.10.0",
|
||||||
"devtron": "^1.4.0",
|
"autoprefixer": "^10.4.2",
|
||||||
"electron": "^9.4.0",
|
"electron": "^25.5.0",
|
||||||
"electron-builder": "^22.9.1",
|
"electron-builder": "^24.3.0",
|
||||||
"electron-debug": "^3.2.0",
|
"eslint": "^8.10.0",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"eslint-config-standard": "^17.0.0",
|
||||||
"electron-packager": "^14.2.1",
|
"eslint-plugin-import": "^2.19.1",
|
||||||
"eslint": "^6.8.0",
|
"eslint-plugin-n": "^15.0.0",
|
||||||
"eslint-config-standard": "^14.1.0",
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
"eslint-loader": "^3.0.3",
|
"eslint-plugin-vue": "^9.0.0",
|
||||||
"eslint-plugin-import": "^2.14.0",
|
"jsdom": "^22.1.0",
|
||||||
"eslint-plugin-node": "^11.0.0",
|
"playwright": "^1.37.1",
|
||||||
"eslint-plugin-promise": "^4.0.1",
|
"postcss-html": "^1.5.0",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"sass": "1.29.0",
|
||||||
"eslint-plugin-vue": "^6.1.2",
|
"stylelint": "^15.10.3",
|
||||||
"quasar-app-extension-qdraggabletree": "0.0.5",
|
"stylelint-config-standard": "^34.0.0",
|
||||||
"typescript": "^3.8.3"
|
"stylelint-config-standard-scss": "^11.0.0",
|
||||||
|
"stylelint-config-standard-vue": "^1.0.0",
|
||||||
|
"typescript": "^4.5.4"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
|
||||||
"last 10 Chrome versions",
|
|
||||||
"last 10 Firefox versions",
|
|
||||||
"last 4 Edge versions",
|
|
||||||
"last 7 Safari versions",
|
|
||||||
"last 8 Android versions",
|
|
||||||
"last 8 ChromeAndroid versions",
|
|
||||||
"last 8 FirefoxAndroid versions",
|
|
||||||
"last 10 iOS versions",
|
|
||||||
"last 5 Opera versions"
|
|
||||||
],
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.18.1",
|
"node": "^18 || ^16 || ^14.19",
|
||||||
"npm": ">= 6.13.4",
|
"npm": ">= 6.13.4",
|
||||||
"yarn": ">= 1.21.1"
|
"yarn": ">= 1.21.1"
|
||||||
}
|
}
|
||||||
|
|
11
playwright.config.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { defineConfig } from '@playwright/test'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
workers: 3,
|
||||||
|
fullyParallel: false,
|
||||||
|
testMatch: '**/*playwright.@(spec|test).?(c|m)[jt]s?(x)',
|
||||||
|
reporter: [
|
||||||
|
['list'],
|
||||||
|
['json', { outputFile: 'test-results/test-results.json' }]
|
||||||
|
]
|
||||||
|
})
|
13
postcss.config.cjs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
// https://github.com/elchininet/postcss-rtlcss
|
||||||
|
// If you want to support RTL css, then
|
||||||
|
// 1. yarn/npm install postcss-rtlcss
|
||||||
|
// 2. optionally set quasar.config.js > framework > lang to an RTL language
|
||||||
|
// 3. uncomment the following line:
|
||||||
|
// require('postcss-rtlcss')
|
||||||
|
],
|
||||||
|
};
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 859 B |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 9.4 KiB |
BIN
public/images/fantasiaMascot/fantasia_cooking.png
Normal file
After Width: | Height: | Size: 153 KiB |
BIN
public/images/fantasiaMascot/fantasia_didYouKnow.png
Normal file
After Width: | Height: | Size: 145 KiB |
BIN
public/images/fantasiaMascot/fantasia_error.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
BIN
public/images/fantasiaMascot/fantasia_reading.png
Normal file
After Width: | Height: | Size: 145 KiB |
234
quasar.conf.js
|
@ -1,234 +0,0 @@
|
||||||
/*
|
|
||||||
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
|
|
||||||
* the ES6 features that are supported by your Node version. https://node.green/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Configuration for your app
|
|
||||||
// https://quasar.dev/quasar-cli/quasar-conf-js
|
|
||||||
/* eslint-env node */
|
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
||||||
const { configure } = require("quasar/wrappers")
|
|
||||||
|
|
||||||
module.exports = configure(function (ctx) {
|
|
||||||
return {
|
|
||||||
// https://quasar.dev/quasar-cli/supporting-ts
|
|
||||||
supportTS: {
|
|
||||||
tsCheckerConfig: {
|
|
||||||
eslint: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/prefetch-feature
|
|
||||||
// preFetch: true,
|
|
||||||
|
|
||||||
// app boot file (/src/boot)
|
|
||||||
// --> boot files are part of "main.js"
|
|
||||||
// https://quasar.dev/quasar-cli/boot-files
|
|
||||||
boot: [
|
|
||||||
"i18n",
|
|
||||||
"axios",
|
|
||||||
'notify-defaults',
|
|
||||||
'apex'
|
|
||||||
],
|
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
|
|
||||||
css: [
|
|
||||||
"app.scss"
|
|
||||||
],
|
|
||||||
|
|
||||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
|
||||||
extras: [
|
|
||||||
// 'ionicons-v4',
|
|
||||||
"mdi-v5",
|
|
||||||
"fontawesome-v5",
|
|
||||||
// 'eva-icons',
|
|
||||||
// 'themify',
|
|
||||||
// 'line-awesome',
|
|
||||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
|
||||||
|
|
||||||
"roboto-font-latin-ext", // optional, you are not bound to it
|
|
||||||
"material-icons" // optional, you are not bound to it
|
|
||||||
],
|
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
|
|
||||||
build: {
|
|
||||||
vueRouterMode: "history", // available values: 'hash', 'history'
|
|
||||||
|
|
||||||
// transpile: false,
|
|
||||||
|
|
||||||
// Add dependencies for transpiling with Babel (Array of string/regex)
|
|
||||||
// (from node_modules, which are by default not transpiled).
|
|
||||||
// Applies only if "transpile" is set to true.
|
|
||||||
// transpileDependencies: [],
|
|
||||||
|
|
||||||
// rtl: false, // https://quasar.dev/options/rtl-support
|
|
||||||
// preloadChunks: true,
|
|
||||||
// showProgress: false,
|
|
||||||
// gzip: true,
|
|
||||||
// analyze: true,
|
|
||||||
|
|
||||||
// Options below are automatically set depending on the env, set them if you want to override
|
|
||||||
// extractCSS: false,
|
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/handling-webpack
|
|
||||||
extendWebpack (cfg) {
|
|
||||||
// linting is slow in TS projects, we execute it only for production builds
|
|
||||||
if (ctx.prod) {
|
|
||||||
cfg.module.rules.push({
|
|
||||||
enforce: "pre",
|
|
||||||
test: /\.(js|vue)$/,
|
|
||||||
loader: "eslint-loader",
|
|
||||||
exclude: /node_modules/,
|
|
||||||
options:{
|
|
||||||
emitWarning : true,
|
|
||||||
emitError : false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
|
|
||||||
devServer: {
|
|
||||||
https: false,
|
|
||||||
port: 8080,
|
|
||||||
open: true // opens browser window automatically
|
|
||||||
},
|
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
|
|
||||||
framework: {
|
|
||||||
iconSet: "material-icons", // Quasar icon set
|
|
||||||
lang: "en-us", // Quasar language pack
|
|
||||||
config: {},
|
|
||||||
|
|
||||||
// Possible values for "importStrategy":
|
|
||||||
// * 'auto' - (DEFAULT) Auto-import needed Quasar components & directives
|
|
||||||
// * 'all' - Manually specify what to import
|
|
||||||
importStrategy: "auto",
|
|
||||||
|
|
||||||
// For special cases outside of where "auto" importStrategy can have an impact
|
|
||||||
// (like functional components as one of the examples),
|
|
||||||
// you can manually specify Quasar components/directives to be available everywhere:
|
|
||||||
//
|
|
||||||
components: [
|
|
||||||
'QAvatar',
|
|
||||||
'QBtn',
|
|
||||||
'QBtnDropdown',
|
|
||||||
'QIcon',
|
|
||||||
'QInput',
|
|
||||||
'QItem',
|
|
||||||
'QItemLabel',
|
|
||||||
'QItemSection',
|
|
||||||
'QList',
|
|
||||||
'QMenu',
|
|
||||||
'QScrollArea',
|
|
||||||
'QScrollObserver',
|
|
||||||
'QSeparator',
|
|
||||||
'QSpinner',
|
|
||||||
'QTooltip',
|
|
||||||
],
|
|
||||||
directives: [
|
|
||||||
'ClosePopup'
|
|
||||||
],
|
|
||||||
|
|
||||||
// Quasar plugins
|
|
||||||
plugins: ['AppFullscreen', 'Notify']
|
|
||||||
},
|
|
||||||
|
|
||||||
// animations: 'all', // --- includes all animations
|
|
||||||
// https://quasar.dev/options/animations
|
|
||||||
animations: 'all',
|
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
|
|
||||||
ssr: {
|
|
||||||
pwa: false
|
|
||||||
},
|
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
|
|
||||||
pwa: {
|
|
||||||
workboxPluginMode: "GenerateSW", // 'GenerateSW' or 'InjectManifest'
|
|
||||||
workboxOptions: {}, // only for GenerateSW
|
|
||||||
manifest: {
|
|
||||||
name: "Fantasia Archive",
|
|
||||||
short_name: "Fantasia Archive",
|
|
||||||
description: "A database manager for world building",
|
|
||||||
display: "standalone",
|
|
||||||
orientation: "portrait",
|
|
||||||
background_color: "#ffffff",
|
|
||||||
theme_color: "#027be3",
|
|
||||||
icons: [
|
|
||||||
{
|
|
||||||
src: "icons/icon-128x128.png",
|
|
||||||
sizes: "128x128",
|
|
||||||
type: "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "icons/icon-192x192.png",
|
|
||||||
sizes: "192x192",
|
|
||||||
type: "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "icons/icon-256x256.png",
|
|
||||||
sizes: "256x256",
|
|
||||||
type: "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "icons/icon-384x384.png",
|
|
||||||
sizes: "384x384",
|
|
||||||
type: "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "icons/icon-512x512.png",
|
|
||||||
sizes: "512x512",
|
|
||||||
type: "image/png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
|
|
||||||
cordova: {
|
|
||||||
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
|
|
||||||
},
|
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
|
|
||||||
capacitor: {
|
|
||||||
hideSplashscreen: true
|
|
||||||
},
|
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
|
|
||||||
electron: {
|
|
||||||
bundler: "builder", // 'packager' or 'builder'
|
|
||||||
|
|
||||||
packager: {
|
|
||||||
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
|
||||||
|
|
||||||
// OS X / Mac App Store
|
|
||||||
// appBundleId: '',
|
|
||||||
// appCategoryType: '',
|
|
||||||
// osxSign: '',
|
|
||||||
// protocol: 'myapp://path',
|
|
||||||
|
|
||||||
// Windows only
|
|
||||||
// win32metadata: { ... }
|
|
||||||
},
|
|
||||||
|
|
||||||
builder: {
|
|
||||||
// https://www.electron.build/configuration/configuration
|
|
||||||
|
|
||||||
appId: "fantasiaarchive",
|
|
||||||
win: {
|
|
||||||
icon: 'src-electron/icons/icon.ico'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
|
|
||||||
nodeIntegration: true,
|
|
||||||
|
|
||||||
extendWebpack (/* cfg */) {
|
|
||||||
// do something with Electron main process Webpack cfg
|
|
||||||
// chainWebpack also available besides this extendWebpack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
220
quasar.config.js
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
|
||||||
|
* the ES6 features that are supported by your Node version. https://node.green/
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Configuration for your app
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
||||||
|
|
||||||
|
const { configure } = require('quasar/wrappers')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
module.exports = configure(function (/* ctx */) {
|
||||||
|
return {
|
||||||
|
eslint: {
|
||||||
|
fix: true,
|
||||||
|
// include: [],
|
||||||
|
// exclude: [],
|
||||||
|
// rawOptions: {},
|
||||||
|
warnings: true,
|
||||||
|
errors: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
|
||||||
|
// preFetch: true,
|
||||||
|
|
||||||
|
// app boot file (/src/boot)
|
||||||
|
// --> boot files are part of "main.js"
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||||
|
boot: ['i18n', 'axios', 'externalLinkManager', 'notify-defaults'],
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||||
|
css: ['app.scss'],
|
||||||
|
|
||||||
|
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||||
|
extras: [
|
||||||
|
// 'ionicons-v4',
|
||||||
|
'mdi-v5',
|
||||||
|
'fontawesome-v6',
|
||||||
|
// 'eva-icons',
|
||||||
|
// 'themify',
|
||||||
|
// 'line-awesome',
|
||||||
|
|
||||||
|
// 'roboto-font', // optional, you are not bound to it
|
||||||
|
'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||||
|
'material-icons' // optional, you are not bound to it
|
||||||
|
],
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||||
|
build: {
|
||||||
|
target: {
|
||||||
|
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||||
|
node: 'node16'
|
||||||
|
},
|
||||||
|
|
||||||
|
vueRouterMode: 'history', // available values: 'hash', 'history'
|
||||||
|
// vueRouterBase,
|
||||||
|
// vueDevtools,
|
||||||
|
// vueOptionsAPI: false,
|
||||||
|
|
||||||
|
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
|
||||||
|
|
||||||
|
// publicPath: '/',
|
||||||
|
// analyze: true,
|
||||||
|
// env: {},
|
||||||
|
// rawDefine: {}
|
||||||
|
// ignorePublicFolder: true,
|
||||||
|
// minify: false,
|
||||||
|
// polyfillModulePreload: true,
|
||||||
|
// distDir
|
||||||
|
|
||||||
|
// extendViteConf (viteConf) {},
|
||||||
|
// viteVuePluginOptions: {},
|
||||||
|
|
||||||
|
vitePlugins: [
|
||||||
|
[
|
||||||
|
'@intlify/vite-plugin-vue-i18n',
|
||||||
|
{
|
||||||
|
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
|
||||||
|
// compositionOnly: false,
|
||||||
|
|
||||||
|
// if you want to use named tokens in your Vue I18n messages, such as 'Hello {name}',
|
||||||
|
// you need to set `runtimeOnly: false`
|
||||||
|
// runtimeOnly: false,
|
||||||
|
|
||||||
|
// you need to set i18n resource including paths !
|
||||||
|
include: path.resolve(__dirname, './src/i18n/**')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||||
|
devServer: {
|
||||||
|
// https: true
|
||||||
|
open: true // opens browser window automatically
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
|
||||||
|
framework: {
|
||||||
|
config: {
|
||||||
|
ripple: false,
|
||||||
|
dark: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// iconSet: 'material-icons', // Quasar icon set
|
||||||
|
// lang: 'en-US', // Quasar language pack
|
||||||
|
|
||||||
|
// For special cases outside of where the auto-import strategy can have an impact
|
||||||
|
// (like functional components as one of the examples),
|
||||||
|
// you can manually specify Quasar components/directives to be available everywhere:
|
||||||
|
//
|
||||||
|
// components: [],
|
||||||
|
// directives: [],
|
||||||
|
|
||||||
|
// Quasar plugins
|
||||||
|
plugins: ['Dialog', 'Notify']
|
||||||
|
},
|
||||||
|
|
||||||
|
// animations: 'all', // --- includes all animations
|
||||||
|
// https://v2.quasar.dev/options/animations
|
||||||
|
animations: 'all',
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#sourcefiles
|
||||||
|
// sourceFiles: {
|
||||||
|
// rootComponent: 'src/App.vue',
|
||||||
|
// router: 'src/router/index',
|
||||||
|
// store: 'src/store/index',
|
||||||
|
// registerServiceWorker: 'src-pwa/register-service-worker',
|
||||||
|
// serviceWorker: 'src-pwa/custom-service-worker',
|
||||||
|
// pwaManifestFile: 'src-pwa/manifest.json',
|
||||||
|
// electronMain: 'src-electron/electron-main',
|
||||||
|
// electronPreload: 'src-electron/electron-preload'
|
||||||
|
// },
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
|
||||||
|
ssr: {
|
||||||
|
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
||||||
|
// will mess up SSR
|
||||||
|
|
||||||
|
// extendSSRWebserverConf (esbuildConf) {},
|
||||||
|
// extendPackageJson (json) {},
|
||||||
|
|
||||||
|
pwa: false,
|
||||||
|
|
||||||
|
// manualStoreHydration: true,
|
||||||
|
// manualPostHydrationTrigger: true,
|
||||||
|
|
||||||
|
prodPort: 3000, // The default port that the production server should use
|
||||||
|
// (gets superseded if process.env.PORT is specified at runtime)
|
||||||
|
|
||||||
|
middlewares: [
|
||||||
|
'render' // keep this as last one
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
|
||||||
|
pwa: {
|
||||||
|
workboxMode: 'generateSW', // or 'injectManifest'
|
||||||
|
injectPwaMetaTags: true,
|
||||||
|
swFilename: 'sw.js',
|
||||||
|
manifestFilename: 'manifest.json',
|
||||||
|
useCredentialsForManifestTag: false
|
||||||
|
// useFilenameHashes: true,
|
||||||
|
// extendGenerateSWOptions (cfg) {}
|
||||||
|
// extendInjectManifestOptions (cfg) {},
|
||||||
|
// extendManifestJson (json) {}
|
||||||
|
// extendPWACustomSWConf (esbuildConf) {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
|
||||||
|
cordova: {
|
||||||
|
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
|
||||||
|
capacitor: {
|
||||||
|
hideSplashscreen: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
|
||||||
|
electron: {
|
||||||
|
// extendElectronMainConf (esbuildConf)
|
||||||
|
// extendElectronPreloadConf (esbuildConf)
|
||||||
|
|
||||||
|
inspectPort: 5858,
|
||||||
|
|
||||||
|
bundler: 'builder', // 'packager' or 'builder'
|
||||||
|
|
||||||
|
packager: {
|
||||||
|
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
||||||
|
// OS X / Mac App Store
|
||||||
|
// appBundleId: '',
|
||||||
|
// appCategoryType: '',
|
||||||
|
// osxSign: '',
|
||||||
|
// protocol: 'myapp://path',
|
||||||
|
// Windows only
|
||||||
|
// win32metadata: { ... }
|
||||||
|
},
|
||||||
|
|
||||||
|
builder: {
|
||||||
|
// https://www.electron.build/configuration/configuration
|
||||||
|
|
||||||
|
appId: 'fantasia-archive',
|
||||||
|
win: {
|
||||||
|
icon: 'src-electron/icons/icon.ico'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
|
||||||
|
bex: {
|
||||||
|
contentScripts: ['my-content-script']
|
||||||
|
|
||||||
|
// extendBexScriptsConf (esbuildConf) {}
|
||||||
|
// extendBexManifestJson (json) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,8 +1,5 @@
|
||||||
{
|
{
|
||||||
"qdraggabletree": {},
|
|
||||||
"@quasar/qmarkdown": {
|
"@quasar/qmarkdown": {
|
||||||
"import_md": true,
|
"import_md": true
|
||||||
"import_vmd": true
|
}
|
||||||
},
|
|
||||||
"@quasar/qwindow": {}
|
|
||||||
}
|
}
|
10
src-electron/customContentBridgeAPIs/extraEnvVariablesAPI.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { I_extraEnvVariablesAPI } from 'app/interfaces/I_extraEnvVariablesAPI'
|
||||||
|
import appRoot from 'app-root-path'
|
||||||
|
|
||||||
|
export const extraEnvVariablesAPI: I_extraEnvVariablesAPI = {
|
||||||
|
ELECTRON_MAIN_FILEPATH: appRoot + '/dist/electron/UnPackaged/electron-main.js',
|
||||||
|
FA_FRONTEND_RENDER_TIMER: 3000,
|
||||||
|
TEST_ENV: (process.env.TEST_ENV) ? process.env.TEST_ENV : false,
|
||||||
|
COMPONENT_NAME: (process.env.COMPONENT_NAME) ? process.env.COMPONENT_NAME : false,
|
||||||
|
COMPONENT_PROPS: (process.env.COMPONENT_PROPS) ? JSON.parse(process.env.COMPONENT_PROPS) : false
|
||||||
|
}
|
42
src-electron/customContentBridgeAPIs/faDevToolsControlAPI.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { BrowserWindow } from '@electron/remote'
|
||||||
|
import { I_faDevToolsControl } from 'app/interfaces/I_faDevToolsControl'
|
||||||
|
|
||||||
|
export const faDevToolsControlAPI: I_faDevToolsControl = {
|
||||||
|
|
||||||
|
checkDecToolsStatus () {
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow()
|
||||||
|
if (currentWindow !== null) {
|
||||||
|
return currentWindow.webContents.isDevToolsOpened()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleDevTools () {
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow()
|
||||||
|
|
||||||
|
if (currentWindow !== null) {
|
||||||
|
const devToolsOpened = currentWindow.webContents.isDevToolsOpened()
|
||||||
|
if (devToolsOpened) {
|
||||||
|
currentWindow.webContents.closeDevTools()
|
||||||
|
} else {
|
||||||
|
currentWindow.webContents.openDevTools()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openDevTools () {
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow()
|
||||||
|
|
||||||
|
if (currentWindow !== null) {
|
||||||
|
currentWindow.webContents.openDevTools()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeDevTools () {
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow()
|
||||||
|
|
||||||
|
if (currentWindow !== null) {
|
||||||
|
currentWindow.webContents.closeDevTools()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { shell } from 'electron'
|
||||||
|
|
||||||
|
import { I_faExternalLinksManagerAPI } from 'app/interfaces/I_faExternalLinksManagerAPI'
|
||||||
|
|
||||||
|
export const faExternalLinksManagerAPI: I_faExternalLinksManagerAPI = {
|
||||||
|
|
||||||
|
checkIfExternal (url: string) {
|
||||||
|
return (
|
||||||
|
(url.includes('http://') || url.includes('https://')) &&
|
||||||
|
!url.includes('localhost')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
openExternal (url: string) {
|
||||||
|
shell.openExternal(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
48
src-electron/customContentBridgeAPIs/faWindowControlAPI.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { BrowserWindow } from '@electron/remote'
|
||||||
|
import { I_faWindowControlAPI } from 'app/interfaces/I_faWindowControlAPI'
|
||||||
|
|
||||||
|
export const faWindowControlAPI: I_faWindowControlAPI = {
|
||||||
|
|
||||||
|
checkWindowMaximized () {
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow()
|
||||||
|
if (currentWindow !== null) {
|
||||||
|
return currentWindow.isMaximized()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
minimizeWindow () {
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow()
|
||||||
|
|
||||||
|
if (currentWindow !== null) {
|
||||||
|
currentWindow.minimize()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
maximizeWindow () {
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow()
|
||||||
|
|
||||||
|
if (currentWindow !== null) {
|
||||||
|
currentWindow.maximize()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resizeWindow () {
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow()
|
||||||
|
|
||||||
|
if (currentWindow !== null) {
|
||||||
|
if (currentWindow.isMaximized()) {
|
||||||
|
currentWindow.unmaximize()
|
||||||
|
} else {
|
||||||
|
currentWindow.maximize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeWindow () {
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow()
|
||||||
|
if (currentWindow !== null) {
|
||||||
|
currentWindow.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
src-electron/electron-env.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
declare namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
QUASAR_ELECTRON_PRELOAD: string;
|
||||||
|
APP_URL: string;
|
||||||
|
}
|
||||||
|
}
|
1
src-electron/electron-flag.d.ts
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable */
|
||||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||||
import "quasar/dist/types/feature-flag";
|
import "quasar/dist/types/feature-flag";
|
||||||
|
|
69
src-electron/electron-main.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { fixAppName } from 'src-electron/mainScripts/fixAppName'
|
||||||
|
import { windowsDevToolsExtensionsFix } from 'src-electron/mainScripts/windowsDevToolsExtensionsFix'
|
||||||
|
import { startApp, openAppWindowManager, closeAppManager } from 'app/src-electron/mainScripts/appManagement'
|
||||||
|
import { tweakMenuRemover, tweakRetriveOS } from 'src-electron/mainScripts/tweaks'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines what platform the app is running on
|
||||||
|
* - Needed in case process is undefined under Linux
|
||||||
|
*/
|
||||||
|
const platform = tweakRetriveOS()
|
||||||
|
|
||||||
|
// Fix app name and connected pathing to it
|
||||||
|
fixAppName()
|
||||||
|
|
||||||
|
// Fix Windows-only DevTools-bug concerning dark mode
|
||||||
|
windowsDevToolsExtensionsFix(platform)
|
||||||
|
|
||||||
|
// Start a singular app instance
|
||||||
|
startApp()
|
||||||
|
|
||||||
|
// Performance improvement tweak
|
||||||
|
tweakMenuRemover()
|
||||||
|
|
||||||
|
// Set up manager for opening a singular app window
|
||||||
|
openAppWindowManager()
|
||||||
|
|
||||||
|
// Set up manager for closing app instance
|
||||||
|
closeAppManager(platform)
|
||||||
|
|
||||||
|
import * as sqlite3 from 'sqlite3'
|
||||||
|
import { app } from 'electron'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
if (!fs.existsSync(`${app.getPath('userData')}/_faProjectTemp/`)) {
|
||||||
|
fs.mkdirSync(`${app.getPath('userData')}/_faProjectTemp/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3.verbose()
|
||||||
|
const db = new sqlite3.Database(`${app.getPath('userData')}/_faProjectTemp/test.fae`)
|
||||||
|
/*
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run('CREATE TABLE IF NOT EXISTS lorem (info TEXT, yeet TEXT)')
|
||||||
|
|
||||||
|
const stmt = db.prepare('INSERT INTO lorem VALUES (?,?)')
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
stmt.run(['Ipsum ' + i, 'Yeet ' + (i + 10)])
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt.finalize()
|
||||||
|
|
||||||
|
db.each('SELECT rowid AS id, info, yeet FROM lorem', (_err, row: {id:string, info:string, yeet:string}) => {
|
||||||
|
console.log(row.id + ': ' + row.info)
|
||||||
|
console.log(row.id + ': ' + row.yeet)
|
||||||
|
})
|
||||||
|
}) */
|
||||||
|
|
||||||
|
/* db.serialize(() => {
|
||||||
|
db.run('CREATE TABLE IF NOT EXISTS sqlar (name TEXT, data BLOB)')
|
||||||
|
|
||||||
|
const buffer = fs.readFileSync(`${app.getPath('userData')}/_faProjectTemp/testImg.jpg`)
|
||||||
|
|
||||||
|
db.run('INSERT INTO sqlar VALUES (?, ?)', ['test', buffer])
|
||||||
|
|
||||||
|
db.each('SELECT rowid AS id, name, data FROM sqlar', (_err, row: {id:string, name:string, data:string}) => {
|
||||||
|
fs.writeFileSync(`${app.getPath('userData')}/_faProjectTemp/testImg2.jpg`, row.data)
|
||||||
|
})
|
||||||
|
}) */
|
||||||
|
|
||||||
|
db.close()
|
41
src-electron/electron-preload.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* This file is used specifically for security reasons.
|
||||||
|
* Here you can access Nodejs stuff and inject functionality into
|
||||||
|
* the renderer thread (accessible there through the "window" object)
|
||||||
|
*
|
||||||
|
* WARNING!
|
||||||
|
* If you import anything from node_modules, then make sure that the package is specified
|
||||||
|
* in package.json > dependencies and NOT in devDependencies
|
||||||
|
*
|
||||||
|
* Example (injects window.myAPI.doAThing() into renderer thread):
|
||||||
|
*
|
||||||
|
* import { contextBridge } from 'electron'
|
||||||
|
*
|
||||||
|
* contextBridge.exposeInMainWorld('myAPI', {
|
||||||
|
* doAThing: () => {}
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* WARNING!
|
||||||
|
* If accessing Node functionality (like importing @electron/remote) then in your
|
||||||
|
* electron-main.ts you will need to set the following when you instantiate BrowserWindow:
|
||||||
|
*
|
||||||
|
* mainWindow = new BrowserWindow({
|
||||||
|
* // ...
|
||||||
|
* webPreferences: {
|
||||||
|
* // ...
|
||||||
|
* sandbox: false // <-- to be able to import @electron/remote in preload script
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { contextBridge } from 'electron'
|
||||||
|
|
||||||
|
import { faWindowControlAPI } from 'src-electron/customContentBridgeAPIs/faWindowControlAPI'
|
||||||
|
import { extraEnvVariablesAPI } from 'src-electron/customContentBridgeAPIs/extraEnvVariablesAPI'
|
||||||
|
import { faDevToolsControlAPI } from 'src-electron/customContentBridgeAPIs/faDevToolsControlAPI'
|
||||||
|
import { faExternalLinksManagerAPI } from 'app/src-electron/customContentBridgeAPIs/faExternalLinksManagerAPI'
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('faWindowControlAPI', faWindowControlAPI)
|
||||||
|
contextBridge.exposeInMainWorld('faDevToolsControlAPI', faDevToolsControlAPI)
|
||||||
|
contextBridge.exposeInMainWorld('faExternalLinksManagerAPI', faExternalLinksManagerAPI)
|
||||||
|
contextBridge.exposeInMainWorld('extraEnvVariables', extraEnvVariablesAPI)
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 65 KiB |
BIN
src-electron/icons/icon.png
Normal file
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 44 KiB |
|
@ -1,41 +0,0 @@
|
||||||
/**
|
|
||||||
* This file is used specifically and only for development. It installs
|
|
||||||
* `electron-debug` & `vue-devtools`. There shouldn't be any need to
|
|
||||||
* modify this file, but it can be used to extend your development
|
|
||||||
* environment.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import electronDebug from 'electron-debug'
|
|
||||||
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
|
|
||||||
import { app, BrowserWindow } from 'electron'
|
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
|
||||||
// allow for a small delay for mainWindow to be created
|
|
||||||
setTimeout(() => {
|
|
||||||
// Install `electron-debug` with `devtron`
|
|
||||||
electronDebug({ showDevTools: false })
|
|
||||||
|
|
||||||
// Install vuejs devtools
|
|
||||||
installExtension(VUEJS_DEVTOOLS)
|
|
||||||
.then(name => {
|
|
||||||
console.log(`Added Extension: ${name}`)
|
|
||||||
// get main window
|
|
||||||
const win = BrowserWindow.getFocusedWindow()
|
|
||||||
if (win) {
|
|
||||||
win.webContents.on('did-frame-finish-load', () => {
|
|
||||||
win.webContents.once('devtools-opened', () => {
|
|
||||||
win.webContents.focus()
|
|
||||||
})
|
|
||||||
// open electron debug
|
|
||||||
console.log('Opening dev tools')
|
|
||||||
win.webContents.openDevTools()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.log('An error occurred: ', err)
|
|
||||||
})
|
|
||||||
}, 250)
|
|
||||||
})
|
|
||||||
|
|
||||||
import './electron-main'
|
|
|
@ -1,114 +0,0 @@
|
||||||
import { app, BrowserWindow, nativeTheme, Menu, MenuItem, shell } from 'electron'
|
|
||||||
app.commandLine.appendSwitch('disable-software-rasterizer', 'true')
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (process.platform === 'win32' && nativeTheme.shouldUseDarkColors === true) {
|
|
||||||
require('fs').unlinkSync(require('path').join(app.getPath('userData'), 'DevTools Extensions'))
|
|
||||||
}
|
|
||||||
} catch (_) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set `__statics` path to static files in production;
|
|
||||||
* The reason we are setting it here is that the path needs to be evaluated at runtime
|
|
||||||
*/
|
|
||||||
if (process.env.PROD) {
|
|
||||||
global.__statics = __dirname
|
|
||||||
}
|
|
||||||
|
|
||||||
let mainWindow
|
|
||||||
|
|
||||||
function createWindow () {
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initial window options
|
|
||||||
*/
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
useContentSize: true,
|
|
||||||
frame: false,
|
|
||||||
|
|
||||||
webPreferences: {
|
|
||||||
// Change from /quasar.conf.js > electron > nodeIntegration;
|
|
||||||
// More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
|
|
||||||
nodeIntegration: process.env.QUASAR_NODE_INTEGRATION,
|
|
||||||
nodeIntegrationInWorker: process.env.QUASAR_NODE_INTEGRATION,
|
|
||||||
enableRemoteModule: true,
|
|
||||||
// More info: /quasar-cli/developing-electron-apps/electron-preload-script
|
|
||||||
// preload: path.resolve(__dirname, 'electron-preload.js')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindow.setMenu(null)
|
|
||||||
mainWindow.maximize()
|
|
||||||
|
|
||||||
mainWindow.loadURL(process.env.APP_URL)
|
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
|
||||||
mainWindow = null
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindow.webContents.on('will-navigate', event => {
|
|
||||||
event.preventDefault()
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindow.webContents.on('new-window', (event, url) => {
|
|
||||||
event.preventDefault()
|
|
||||||
shell.openExternal(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindow.webContents.on('context-menu', (event, params) => {
|
|
||||||
const menu = new Menu()
|
|
||||||
|
|
||||||
// Add each spelling suggestion
|
|
||||||
for (const suggestion of params.dictionarySuggestions) {
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: suggestion,
|
|
||||||
click: () => mainWindow.webContents.replaceMisspelling(suggestion)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow users to add the misspelled word to the dictionary
|
|
||||||
if (params.misspelledWord) {
|
|
||||||
menu.append(
|
|
||||||
new MenuItem({
|
|
||||||
label: 'Add to dictionary',
|
|
||||||
click: () => mainWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if((params.dictionarySuggestions && params.dictionarySuggestions.length) || params.misspelledWord){
|
|
||||||
menu.popup()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
|
||||||
|
|
||||||
if (!gotTheLock) {
|
|
||||||
app.quit()
|
|
||||||
} else {
|
|
||||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
|
||||||
// Someone tried to run a second instance, we should focus our window.
|
|
||||||
if (mainWindow) {
|
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create myWindow, load the rest of the app, etc...
|
|
||||||
app.on('ready', createWindow)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (mainWindow === null) {
|
|
||||||
createWindow()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
35
src-electron/mainScripts/appManagement.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { initialize } from '@electron/remote/main'
|
||||||
|
import { mainWindowCreation } from 'app/src-electron/mainScripts/mainWindowCreation'
|
||||||
|
import { app } from 'electron'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the app's Electron instance
|
||||||
|
*/
|
||||||
|
export const startApp = () => {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the singular app's window and make sure it is the only one
|
||||||
|
*/
|
||||||
|
export const openAppWindowManager = () => {
|
||||||
|
// Create the app window in the normal way
|
||||||
|
app.whenReady().then(mainWindowCreation)
|
||||||
|
|
||||||
|
// Create the app window, if it still doesn't exist yet
|
||||||
|
app.on('activate', () => {
|
||||||
|
mainWindowCreation()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the app's Electron instance when all windows are closed
|
||||||
|
*/
|
||||||
|
export const closeAppManager = (platform: string) => {
|
||||||
|
// Close app if we are on anything that isn't Mac
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (platform !== 'darwin') {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
27
src-electron/mainScripts/fixAppName.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { app } from 'electron'
|
||||||
|
import path from 'path'
|
||||||
|
import packageJSON from '../../package.json' assert {type: 'json'}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the app name will have "-dev" affix at the end for the appData.
|
||||||
|
*/
|
||||||
|
const determineAppName = () => {
|
||||||
|
if (process.env.DEBUGGING) {
|
||||||
|
return `${packageJSON.name}-dev`
|
||||||
|
}
|
||||||
|
|
||||||
|
return packageJSON.name
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix the name and pathing of the app.
|
||||||
|
* - This function exists mostly due to dev-mode returning "Electron" instead of the app name.
|
||||||
|
*/
|
||||||
|
export const fixAppName = () => {
|
||||||
|
const appName = determineAppName()
|
||||||
|
if (appName) {
|
||||||
|
app.setName(appName)
|
||||||
|
const appData = app.getPath('appData')
|
||||||
|
app.setPath('userData', path.join(appData, appName))
|
||||||
|
}
|
||||||
|
}
|
101
src-electron/mainScripts/mainWindowCreation.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { BrowserWindow, app, screen } from 'electron'
|
||||||
|
import { enable } from '@electron/remote/main'
|
||||||
|
import path from 'path'
|
||||||
|
/**
|
||||||
|
* Prevent app to launch a secondary instance
|
||||||
|
*/
|
||||||
|
const preventSecondaryAppInstance = (appWindow: BrowserWindow | undefined) => {
|
||||||
|
// Do not limit the window amount if we are in auto-test mode
|
||||||
|
if (process.env.TEST_ENV && (process.env.TEST_ENV === 'components' || process.env.TEST_ENV === 'e2e')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Determines if the app is the primary instance
|
||||||
|
* - This exists as a variable due to the app bugging out if used directly from "app"
|
||||||
|
*/
|
||||||
|
const isPrimaryInstance = app.requestSingleInstanceLock()
|
||||||
|
|
||||||
|
// Check this is NOT the primary app instance
|
||||||
|
if (!isPrimaryInstance) {
|
||||||
|
app.quit()
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
Someone tried to start a second instance. That one is closed already.
|
||||||
|
Our instance here (the first instance) maximizes it's own window and refocuses it.
|
||||||
|
*/
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
if (appWindow) {
|
||||||
|
if (appWindow.isMinimized()) {
|
||||||
|
appWindow.restore()
|
||||||
|
}
|
||||||
|
appWindow.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the main app window
|
||||||
|
*/
|
||||||
|
export const mainWindowCreation = () => {
|
||||||
|
/**
|
||||||
|
* Retrieve actual display size to stop flicker/debounce that happens with "maximize" function at first
|
||||||
|
*/
|
||||||
|
const displaySizes = screen.getPrimaryDisplay().workAreaSize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial window options
|
||||||
|
*/
|
||||||
|
let appWindow: BrowserWindow | undefined = new BrowserWindow({
|
||||||
|
width: displaySizes.width,
|
||||||
|
height: displaySizes.height,
|
||||||
|
useContentSize: true,
|
||||||
|
frame: false,
|
||||||
|
show: false,
|
||||||
|
center: true,
|
||||||
|
icon: path.resolve(__dirname, '../icons/icon.png'),
|
||||||
|
webPreferences: {
|
||||||
|
sandbox: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Enable actual webContents inside the created window
|
||||||
|
enable(appWindow.webContents)
|
||||||
|
|
||||||
|
// Show the windows once electron is ready to show the actual HTML content
|
||||||
|
appWindow.once('ready-to-show', () => {
|
||||||
|
if (appWindow) {
|
||||||
|
appWindow.show()
|
||||||
|
appWindow.focus()
|
||||||
|
appWindow.maximize()
|
||||||
|
|
||||||
|
// In case the windows somehow didn't maximize at first, make sure it does; this is a fix for slower machines
|
||||||
|
setTimeout(() => {
|
||||||
|
if (appWindow) {
|
||||||
|
appWindow.maximize()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set the current window's menu as empty
|
||||||
|
appWindow.setMenu(null)
|
||||||
|
|
||||||
|
// Load the basic app URL
|
||||||
|
appWindow.loadURL(process.env.APP_URL)
|
||||||
|
|
||||||
|
// Open DevTools by default if the app is running in Dev mode or Production with debug enabled
|
||||||
|
if (process.env.DEBUGGING) {
|
||||||
|
appWindow.webContents.openDevTools()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the app window properly closes when it is closed in any way, shape or form
|
||||||
|
appWindow.on('closed', () => {
|
||||||
|
appWindow = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if we are on the primary or secondary instance of the app
|
||||||
|
preventSecondaryAppInstance(appWindow)
|
||||||
|
}
|
16
src-electron/mainScripts/tweaks.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Menu } from 'electron'
|
||||||
|
import os from 'os'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current OS string-indetifier
|
||||||
|
*/
|
||||||
|
export const tweakRetriveOS = () => {
|
||||||
|
return process.platform || os.platform()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the default menu from all current and future windows the current app
|
||||||
|
*/
|
||||||
|
export const tweakMenuRemover = () => {
|
||||||
|
Menu.setApplicationMenu(null)
|
||||||
|
}
|
15
src-electron/mainScripts/windowsDevToolsExtensionsFix.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { app, nativeTheme } from 'electron'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Fix Windows related-bug with DevTools extensions and dark mode
|
||||||
|
*/
|
||||||
|
export const windowsDevToolsExtensionsFix = (platform: string) => {
|
||||||
|
try {
|
||||||
|
if (platform === 'win32' && nativeTheme.shouldUseDarkColors === true) {
|
||||||
|
require('fs').unlinkSync(
|
||||||
|
path.join(app.getPath('userData'), 'DevTools Extensions')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
392
src/App.vue
|
@ -1,393 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="q-app">
|
|
||||||
<appWindowButtons />
|
|
||||||
<router-view />
|
<router-view />
|
||||||
<q-window
|
|
||||||
v-model="advSearchWindowVisible"
|
|
||||||
no-resize
|
|
||||||
dark
|
|
||||||
title="Advanced Search Cheatsheet"
|
|
||||||
:height="625"
|
|
||||||
:width="500"
|
|
||||||
:start-x="50"
|
|
||||||
:start-y="150"
|
|
||||||
:actions="['pin', 'close']"
|
|
||||||
content-class="bg-gunmetal-light text-accent advSearchWindow"
|
|
||||||
>
|
|
||||||
<div class="q-pa-md fit">
|
|
||||||
<q-markdown no-heading-anchor-links>
|
|
||||||
{{$t('documents.advancedSearchCheatSheet')}}
|
|
||||||
</q-markdown>
|
|
||||||
</div>
|
|
||||||
</q-window>
|
|
||||||
|
|
||||||
<q-window
|
|
||||||
v-model="corkboardWindowVisible"
|
|
||||||
dark
|
|
||||||
title="Note board"
|
|
||||||
gripper-border-color="primary"
|
|
||||||
gripper-background-color="primary"
|
|
||||||
:height="600"
|
|
||||||
:width="350"
|
|
||||||
:start-x="350"
|
|
||||||
:start-y="100"
|
|
||||||
:actions="['pin', 'close']"
|
|
||||||
content-class="bg-gunmetal-light text-accent noteBoardWindow"
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
class="corkboardInput"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
autocomplete="off"
|
|
||||||
spellcheck="false"
|
|
||||||
>
|
|
||||||
<q-input
|
|
||||||
|
|
||||||
v-model="corkboardContent"
|
|
||||||
filled
|
|
||||||
dark
|
|
||||||
@keyup="processCorkboardInput"
|
|
||||||
type="textarea"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</q-window>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import BaseClass from "src/BaseClass"
|
import { useRouter } from 'vue-router'
|
||||||
import { Component, Watch } from "vue-property-decorator"
|
import { tipsTricksTriviaNotification } from 'app/src/scripts/appInfo/tipsTricksTriviaNotification'
|
||||||
import { defaultKeybinds } from "src/scripts/appSettings/defaultKeybinds"
|
|
||||||
import appWindowButtons from "src/components/appHeader/AppWindowButtons.vue"
|
|
||||||
import PouchDB from "pouchdb"
|
|
||||||
import { OptionsStateInteface } from "./store/module-options/state"
|
|
||||||
import { colors } from "quasar"
|
|
||||||
import { tipsTricks } from "src/scripts/utilities/tipsTricks"
|
|
||||||
import { shell } from "electron"
|
|
||||||
import { summonAllPlusheForms } from "src/scripts/utilities/plusheMascot"
|
|
||||||
import { saveCorkboard, retrieveCorkboard } from "src/scripts/projectManagement/projectManagent"
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
appWindowButtons: appWindowButtons
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export default class App extends BaseClass {
|
|
||||||
/****************************************************************/
|
|
||||||
// APP START & END SETUP
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
async created () {
|
|
||||||
// Catch middle clicks
|
|
||||||
window.addEventListener("auxclick", this.reactToMiddleClick)
|
|
||||||
|
|
||||||
// Add a secondary blocker to prevent the middle-mouse button scrolling
|
|
||||||
document.body.onmousedown = function (e) {
|
|
||||||
if (e.button === 1) {
|
|
||||||
e.preventDefault()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load settings
|
|
||||||
await this.loadSettings()
|
|
||||||
|
|
||||||
await this.loadCorkboardCotent()
|
|
||||||
|
|
||||||
// Load the popup hint on
|
|
||||||
this.loadHintPopup()
|
|
||||||
|
|
||||||
// React to keybind presses
|
|
||||||
window.addEventListener("keydown", this.triggerKeyPush)
|
|
||||||
|
|
||||||
// Catch normal clicks inside wysiwyg
|
|
||||||
window.addEventListener("click", this.openWysiwygLink)
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyed () {
|
|
||||||
window.removeEventListener("auxclick", this.reactToMiddleClick)
|
|
||||||
|
|
||||||
this.deregisterCustomKeybinds()
|
|
||||||
this.deregisterDefaultKeybinds()
|
|
||||||
window.removeEventListener("keydown", this.triggerKeyPush)
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// START NOTIFICATION
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model for the startup notification
|
* Local router variable
|
||||||
*/
|
*/
|
||||||
starupNotif = null as any
|
const router = useRouter()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification checker
|
* Testing type currently possibly happening
|
||||||
* Can go up to 3
|
|
||||||
*/
|
*/
|
||||||
popupCheck = 0
|
const testingType = window.extraEnvVariables.TEST_ENV
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the actual popup
|
* Name of the component being possibly tested via component testing
|
||||||
*/
|
*/
|
||||||
loadHintPopup () {
|
const testingComponentName = window.extraEnvVariables.COMPONENT_NAME
|
||||||
const options = this.SGET_options
|
|
||||||
|
|
||||||
// Considering there is a bit of a delay between the initial load of the store DB content, we give the program 3 attempts to load the data over 3 seconds. If no is loaded in that time, we assume that the settings are not set at all and display the hint as normal.
|
|
||||||
if ((!options._id || !options._rev) && this.popupCheck < 3) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.popupCheck++
|
|
||||||
this.loadHintPopup()
|
|
||||||
}, 1000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.hideTooltipsStart) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageToShow = tipsTricks[Math.floor(Math.random() * tipsTricks.length)]
|
|
||||||
const plusheForm = summonAllPlusheForms[Math.floor(Math.random() * summonAllPlusheForms.length)]
|
|
||||||
this.starupNotif = this.$q.notify({
|
|
||||||
|
|
||||||
timeout: 15000,
|
|
||||||
icon: (this.hidePlushes) ? "mdi-help" : undefined,
|
|
||||||
color: "info",
|
|
||||||
message: "Did you know?",
|
|
||||||
avatar: (!this.hidePlushes) ? plusheForm : undefined,
|
|
||||||
caption: messageToShow,
|
|
||||||
actions: [{ icon: "mdi-close", color: "white" }]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide the startup notification if the user changed the route before it disappeared
|
* Determine if some testing is happening
|
||||||
*/
|
*/
|
||||||
@Watch("$route", { deep: true })
|
const isComponentTesting = (testingType && testingType === 'components' && testingComponentName)
|
||||||
onUrlChange () {
|
|
||||||
if (typeof this.starupNotif === "function") {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
this.starupNotif()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// KEYBIND HANDLING
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React to keybind combinations being pushed and submit them to the store
|
* In case of some testing happening:
|
||||||
|
* - Reroute to the proper component path route assuming all is properly set.
|
||||||
|
* - Otherwise, make sure we are on homepage on load.
|
||||||
*/
|
*/
|
||||||
triggerKeyPush (e:any) {
|
if (isComponentTesting) {
|
||||||
// console.log("")
|
router.push({ path: `/componentTesting/${testingComponentName}` })
|
||||||
// console.log(`Key: ${e.key}`)
|
} else {
|
||||||
// console.log(`Ctrl: ${e.ctrlKey}`)
|
router.push({ path: '/' })
|
||||||
// console.log(`Shift: ${e.shiftKey}`)
|
// TODO add checking if "Did you know" popup should show
|
||||||
// console.log(`Alt: ${e.altKey}`)
|
// TODO add checking if "Did you know" popup should be showing a mascot or an icon
|
||||||
// console.log(e)
|
tipsTricksTriviaNotification(false)
|
||||||
|
|
||||||
if (e?.altKey === true || e?.ctrlKey || e?.shiftKey) {
|
|
||||||
const ouputKeycombo = {
|
|
||||||
altKey: e.altKey,
|
|
||||||
ctrlKey: e.ctrlKey,
|
|
||||||
shiftKey: e.shiftKey,
|
|
||||||
which: e.which
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.SSET_updatePressedKey(ouputKeycombo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a default keybind into the store
|
|
||||||
*/
|
|
||||||
registerDefaultKeybinds () {
|
|
||||||
// @ts-ignore
|
|
||||||
defaultKeybinds.forEach(e => this.SSET_registerDefaultKeybind(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a default keybind from the store
|
|
||||||
*/
|
|
||||||
deregisterDefaultKeybinds () {
|
|
||||||
// @ts-ignore
|
|
||||||
defaultKeybinds.forEach(e => this.SSET_deregisterDefaultKeybind(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a custom keybind into the store
|
|
||||||
*/
|
|
||||||
registerCustomKeybinds () {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.SGET_options.userKeybindList.forEach(e => this.SSET_registerUserKeybind(e))
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a custom keybind from the store
|
|
||||||
*/
|
|
||||||
deregisterCustomKeybinds () {
|
|
||||||
// @ts-ignore
|
|
||||||
defaultKeybinds.forEach(e => this.SSET_deregisterUserKeybind(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// VARIOUS APP FUNCTIONALITY
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open wysiwyg links in default browser window
|
|
||||||
*/
|
|
||||||
openWysiwygLink (event: MouseEvent) {
|
|
||||||
event.preventDefault()
|
|
||||||
// @ts-ignore
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
if (event.target && event.target.tagName.toLowerCase() === "a" && event.target.closest(".fieldWysiwyg")) {
|
|
||||||
const isValidHttpUrl = (string:string) => {
|
|
||||||
let url
|
|
||||||
|
|
||||||
try {
|
|
||||||
url = new URL(string)
|
|
||||||
}
|
|
||||||
catch (_) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.protocol === "http:" || url.protocol === "https:"
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
if (isValidHttpUrl(event.target.href)) {
|
|
||||||
// @ts-ignore
|
|
||||||
shell.openExternal(event.target.href).catch(e => console.log(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* React to middle mouse button clicks
|
|
||||||
*/
|
|
||||||
reactToMiddleClick (e: {button: number, preventDefault: ()=> void}) {
|
|
||||||
if (e.button === 1) {
|
|
||||||
e.preventDefault()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load settings for the first time upon app load
|
|
||||||
*/
|
|
||||||
async loadSettings () {
|
|
||||||
const SettingsDB = new PouchDB("fa-settings")
|
|
||||||
const settingsData = await SettingsDB.allDocs({ include_docs: true })
|
|
||||||
const settings = settingsData?.rows[0]?.doc as unknown as OptionsStateInteface
|
|
||||||
|
|
||||||
if (settings) {
|
|
||||||
this.SSET_options(settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.registerDefaultKeybinds()
|
|
||||||
this.registerCustomKeybinds()
|
|
||||||
|
|
||||||
await SettingsDB.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update dark/light mode across the app based on what is currently in the store
|
|
||||||
*/
|
|
||||||
@Watch("SGET_options", { deep: true })
|
|
||||||
onSettingsChange () {
|
|
||||||
const options = this.SGET_options
|
|
||||||
|
|
||||||
this.hidePlushes = options.hidePlushes
|
|
||||||
this.$q.dark.set(options.darkMode)
|
|
||||||
if (options.darkMode) {
|
|
||||||
colors.setBrand("dark", "#1b333e")
|
|
||||||
colors.setBrand("primary", "#ffd673")
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
colors.setBrand("dark", "#18303a")
|
|
||||||
colors.setBrand("primary", "#e8bb50")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the mascot... nooo :(
|
|
||||||
*/
|
|
||||||
hidePlushes = false
|
|
||||||
|
|
||||||
@Watch("SGET_getAdvSearchWindowVisible")
|
|
||||||
onAdvSearchWindowOpen () {
|
|
||||||
this.advSearchWindowVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
advSearchWindowVisible = false
|
|
||||||
|
|
||||||
@Watch("SGET_getNoteCorkboardhWindowVisible")
|
|
||||||
onCorkboardWindowOpen () {
|
|
||||||
this.corkboardWindowVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
corkboardWindowVisible = false
|
|
||||||
|
|
||||||
corkboardContent = ""
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debounce timer to prevent buggy input sync
|
|
||||||
*/
|
|
||||||
corkboardTimer = null as any
|
|
||||||
|
|
||||||
processCorkboardInput () {
|
|
||||||
clearTimeout(this.corkboardTimer)
|
|
||||||
this.corkboardTimer = setTimeout(() => {
|
|
||||||
saveCorkboard(this.corkboardContent).catch(e => console.log(e))
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Corkboard checker
|
|
||||||
* Can go up to 3
|
|
||||||
*/
|
|
||||||
corkboardCheck = 0
|
|
||||||
|
|
||||||
async loadCorkboardCotent () {
|
|
||||||
const options = this.SGET_options
|
|
||||||
|
|
||||||
this.corkboardContent = await retrieveCorkboard()
|
|
||||||
|
|
||||||
// Considering there is a bit of a delay between the initial load of the store DB content, we give the program 3 attempts to load the data over 3 seconds. If no is loaded in that time, we assume that the settings are not set at all and display the hint as normal.
|
|
||||||
if ((!options._id || !options._rev) && this.corkboardCheck < 3) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.corkboardCheck++
|
|
||||||
this.loadCorkboardCotent().catch(e => console.log(e))
|
|
||||||
}, 1000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.preventFilledNoteBoardPopup) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.corkboardContent.length) {
|
|
||||||
this.corkboardWindowVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Local keybinds
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
@Watch("SGET_getCurrentKeyBindData", { deep: true })
|
|
||||||
processKeyPush () {
|
|
||||||
// Toggle the Advanced search cheatsheet
|
|
||||||
if (this.determineKeyBind("toggleAdvSearchCheatsheet")) {
|
|
||||||
this.advSearchWindowVisible = !this.advSearchWindowVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle Note Board - CTRL + ALT + SHIFT + P
|
|
||||||
if (this.determineKeyBind("toggleNoteCorkboard")) {
|
|
||||||
this.corkboardWindowVisible = !this.corkboardWindowVisible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
625
src/BaseClass.ts
|
@ -1,625 +0,0 @@
|
||||||
import { OptionsStateInteface } from "./store/module-options/state"
|
|
||||||
import { KeyManagementInterface } from "./store/module-keybinds/state"
|
|
||||||
import { I_OpenedDocument, I_ShortenedDocument } from "./interfaces/I_OpenedDocument"
|
|
||||||
import { Component, Vue } from "vue-property-decorator"
|
|
||||||
import { namespace } from "vuex-class"
|
|
||||||
import { I_Blueprint } from "src/interfaces/I_Blueprint"
|
|
||||||
import { I_NewObjectTrigger } from "src/interfaces/I_NewObjectTrigger"
|
|
||||||
import { uid, colors, extend } from "quasar"
|
|
||||||
import { I_FieldRelationship } from "src/interfaces/I_FieldRelationship"
|
|
||||||
import { I_KeyPressObject } from "src/interfaces/I_KeypressObject"
|
|
||||||
|
|
||||||
const Blueprints = namespace("blueprintsModule")
|
|
||||||
const AllDocuments = namespace("allDocumentsModule")
|
|
||||||
const OpenedDocuments = namespace("openedDocumentsModule")
|
|
||||||
const Keybinds = namespace("keybindsModule")
|
|
||||||
const Options = namespace("optionsModule")
|
|
||||||
const Dialogs = namespace("dialogsModule")
|
|
||||||
const FloatingWindows = namespace("floatingWindowsModule")
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class BaseClass extends Vue {
|
|
||||||
/****************************************************************/
|
|
||||||
// UTILITY FUNCTIONS
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates unique ID string
|
|
||||||
*/
|
|
||||||
generateUID () : string {
|
|
||||||
return uid()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves icon color for relationship searches
|
|
||||||
* If the current document has "activeTypeSearch" property, then return "primary" color instead
|
|
||||||
*/
|
|
||||||
retrieveIconColor (document: I_ShortenedDocument): string {
|
|
||||||
// @ts-ignore
|
|
||||||
return (document.activeTypeSearch) ? colors.getBrand("primary") : document.color
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Async wait for XY miliseconds
|
|
||||||
*/
|
|
||||||
sleep (ms:number) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strip all tags from a string
|
|
||||||
*/
|
|
||||||
stripTags (input: string) {
|
|
||||||
return (input) ? input.replace(/<[^>]+>/g, "") : input
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// FLOATING WINDOWS
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
@FloatingWindows.Getter("getAdvSearchWindowVisible") SGET_getAdvSearchWindowVisible!: string
|
|
||||||
@FloatingWindows.Mutation("setAdvSearchWindowVisible") SSET_setAdvSearchWindowVisible!: () => void
|
|
||||||
|
|
||||||
@FloatingWindows.Getter("getNoteCorkboardhWindowVisible") SGET_getNoteCorkboardhWindowVisible!: string
|
|
||||||
@FloatingWindows.Mutation("setNoteCorkboardWindowVisible") SSET_setNoteCorkboardWindowVisible!: () => void
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// KEYBINDS MANAGEMENT
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
@Keybinds.Getter("getCurrentKeyBindData") SGET_getCurrentKeyBindData!: KeyManagementInterface
|
|
||||||
|
|
||||||
@Keybinds.Mutation("registerDefaultKeybind") SSET_registerDefaultKeybind!: (input: I_KeyPressObject) => void
|
|
||||||
@Keybinds.Mutation("deregisterDefaultKeybind") SSET_deregisterDefaultKeybind!: (input: I_KeyPressObject) => void
|
|
||||||
@Keybinds.Mutation("registerUserKeybind") SSET_registerUserKeybind!: (input: I_KeyPressObject) => void
|
|
||||||
@Keybinds.Mutation("deregisterUserKeybind") SSET_deregisterUserKeybind!: (input: I_KeyPressObject) => void
|
|
||||||
@Keybinds.Mutation("updatePressedKey") SSET_updatePressedKey!: (input: I_KeyPressObject) => void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a humanly redable represetation of the keybind in string form
|
|
||||||
* @param keybindId - Keybind object to build the string out of
|
|
||||||
*/
|
|
||||||
retrieveKeybindString (keybind: I_KeyPressObject): string {
|
|
||||||
let keybindString = ""
|
|
||||||
|
|
||||||
if (!keybind) {
|
|
||||||
return keybindString
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keybind.ctrlKey) {
|
|
||||||
keybindString += "CTRL + "
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keybind.altKey) {
|
|
||||||
keybindString += "ALT + "
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keybind.shiftKey) {
|
|
||||||
keybindString += "SHIFT + "
|
|
||||||
}
|
|
||||||
|
|
||||||
const keybinds = [37, 38, 39, 40, 9, 32, 13, 192, 189, 187, 219, 221, 220, 186, 222, 188, 190, 191, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123]
|
|
||||||
|
|
||||||
if (keybinds.includes(keybind.which)) {
|
|
||||||
if (keybind.which === 13) {
|
|
||||||
keybindString += "ENTER"
|
|
||||||
}
|
|
||||||
if (keybind.which === 9) {
|
|
||||||
keybindString += "TAB"
|
|
||||||
}
|
|
||||||
if (keybind.which === 32) {
|
|
||||||
keybindString += "SPACE"
|
|
||||||
}
|
|
||||||
if (keybind.which === 37) {
|
|
||||||
keybindString += "LEFT ARROW"
|
|
||||||
}
|
|
||||||
if (keybind.which === 38) {
|
|
||||||
keybindString += "UP ARROW"
|
|
||||||
}
|
|
||||||
if (keybind.which === 39) {
|
|
||||||
keybindString += "RIGHT ARROW"
|
|
||||||
}
|
|
||||||
if (keybind.which === 40) {
|
|
||||||
keybindString += "DOWN ARROW"
|
|
||||||
}
|
|
||||||
if (keybind.which === 192) {
|
|
||||||
keybindString += "`"
|
|
||||||
}
|
|
||||||
if (keybind.which === 189) {
|
|
||||||
keybindString += "-"
|
|
||||||
}
|
|
||||||
if (keybind.which === 187) {
|
|
||||||
keybindString += "+"
|
|
||||||
}
|
|
||||||
if (keybind.which === 219) {
|
|
||||||
keybindString += "["
|
|
||||||
}
|
|
||||||
if (keybind.which === 221) {
|
|
||||||
keybindString += "]"
|
|
||||||
}
|
|
||||||
if (keybind.which === 220) {
|
|
||||||
keybindString += "\\"
|
|
||||||
}
|
|
||||||
if (keybind.which === 186) {
|
|
||||||
keybindString += ";"
|
|
||||||
}
|
|
||||||
if (keybind.which === 222) {
|
|
||||||
keybindString += "'"
|
|
||||||
}
|
|
||||||
if (keybind.which === 188) {
|
|
||||||
keybindString += ","
|
|
||||||
}
|
|
||||||
if (keybind.which === 190) {
|
|
||||||
keybindString += "."
|
|
||||||
}
|
|
||||||
if (keybind.which === 191) {
|
|
||||||
keybindString += "/"
|
|
||||||
}
|
|
||||||
if (keybind.which === 112) {
|
|
||||||
keybindString += "F1"
|
|
||||||
}
|
|
||||||
if (keybind.which === 113) {
|
|
||||||
keybindString += "F2"
|
|
||||||
}
|
|
||||||
if (keybind.which === 114) {
|
|
||||||
keybindString += "F3"
|
|
||||||
}
|
|
||||||
if (keybind.which === 115) {
|
|
||||||
keybindString += "F4"
|
|
||||||
}
|
|
||||||
if (keybind.which === 116) {
|
|
||||||
keybindString += "F5"
|
|
||||||
}
|
|
||||||
if (keybind.which === 117) {
|
|
||||||
keybindString += "F6"
|
|
||||||
}
|
|
||||||
if (keybind.which === 118) {
|
|
||||||
keybindString += "F7"
|
|
||||||
}
|
|
||||||
if (keybind.which === 119) {
|
|
||||||
keybindString += "F8"
|
|
||||||
}
|
|
||||||
if (keybind.which === 120) {
|
|
||||||
keybindString += "F9"
|
|
||||||
}
|
|
||||||
if (keybind.which === 121) {
|
|
||||||
keybindString += "F10"
|
|
||||||
}
|
|
||||||
if (keybind.which === 122) {
|
|
||||||
keybindString += "F11"
|
|
||||||
}
|
|
||||||
if (keybind.which === 123) {
|
|
||||||
keybindString += "F12"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
keybindString += String.fromCharCode(keybind.which)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keybind.note) {
|
|
||||||
keybindString += `<div class="text-italic keybindNote">${keybind.note}</div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
return keybindString
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the keybind triggered the proper condition
|
|
||||||
* @param keybindId - ID of the keybind to determine what to match
|
|
||||||
*/
|
|
||||||
determineKeyBind (keybindId: string): boolean {
|
|
||||||
const currentKeybindData = this.SGET_getCurrentKeyBindData
|
|
||||||
|
|
||||||
const currentKeyPress = currentKeybindData.currentKeyPress
|
|
||||||
const pairedDefaultKeybind = currentKeybindData.defaults.find(e => e.id === keybindId)
|
|
||||||
const pairedCustomKeybind = currentKeybindData.userKeybinds.find(e => e.id === keybindId)
|
|
||||||
|
|
||||||
// Kill the script if no keybind exists of this anywhere
|
|
||||||
if (!pairedDefaultKeybind) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldToCheck = (pairedCustomKeybind) || pairedDefaultKeybind
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentKeyPress.altKey === fieldToCheck.altKey &&
|
|
||||||
currentKeyPress.ctrlKey === fieldToCheck.ctrlKey &&
|
|
||||||
currentKeyPress.shiftKey === fieldToCheck.shiftKey &&
|
|
||||||
currentKeyPress.which === fieldToCheck.which
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// BLUEPRINT MANAGEMENT
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
@Blueprints.Getter("getAllBlueprints") SGET_allBlueprints !: I_Blueprint[]
|
|
||||||
@Blueprints.Getter("getBlueprint") SGET_blueprint!: (type: string) => I_Blueprint
|
|
||||||
|
|
||||||
@Blueprints.Mutation("setAllBlueprints") SSET_allBlueprints!: (input: I_Blueprint[]) => void
|
|
||||||
@Blueprints.Mutation("setBlueprint") SSET_blueprint!: (input: I_Blueprint) => void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a brand new route for the new object with individual ID
|
|
||||||
* @param newObject A new object to be creared
|
|
||||||
*/
|
|
||||||
addNewObjectRoute (newObject: I_NewObjectTrigger) {
|
|
||||||
const parentID = (newObject?.parent) || ""
|
|
||||||
|
|
||||||
this.$router.push({
|
|
||||||
path: `/project/display-content/${newObject._id}/${uid()}`,
|
|
||||||
query: { parent: parentID }
|
|
||||||
}).catch((e: {name: string}) => {
|
|
||||||
const errorName : string = e.name
|
|
||||||
if (errorName === "NavigationDuplicated") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open a new route for an already existing object
|
|
||||||
* @param existingObject An already existing object passed in
|
|
||||||
*/
|
|
||||||
openExistingDocumentRouteWithEdit (existingObject:I_OpenedDocument | I_FieldRelationship) {
|
|
||||||
const currentDoc = this.findRequestedOrActiveDocument()
|
|
||||||
|
|
||||||
if (currentDoc && existingObject._id === currentDoc._id && !currentDoc.editMode) {
|
|
||||||
const dataCopy: I_OpenedDocument = extend(true, {}, currentDoc)
|
|
||||||
dataCopy.editMode = true
|
|
||||||
const dataPass = { doc: dataCopy, treeAction: false }
|
|
||||||
this.SSET_updateOpenedDocument(dataPass)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$router.push({
|
|
||||||
path: existingObject.url,
|
|
||||||
query: { editMode: "editMode" }
|
|
||||||
}).catch((e: {name: string}) => {
|
|
||||||
const errorName : string = e.name
|
|
||||||
if (errorName === "NavigationDuplicated") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open a new route for an already existing object
|
|
||||||
* @param existingObject An already existing object passed in
|
|
||||||
*/
|
|
||||||
openExistingDocumentRoute (existingObject:I_OpenedDocument | I_FieldRelationship) {
|
|
||||||
this.$router.push({ path: existingObject.url }).catch((e: {name: string}) => {
|
|
||||||
const errorName : string = e.name
|
|
||||||
if (errorName === "NavigationDuplicated") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// OPTION MANAGEMENT
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
@Options.Getter("getOptions") SGET_options!: OptionsStateInteface
|
|
||||||
|
|
||||||
@Options.Action("setOptions") SSET_options!: (input: OptionsStateInteface) => void
|
|
||||||
|
|
||||||
toggleHierarchicalTree (): void {
|
|
||||||
const optionsSnapshot: OptionsStateInteface = extend(true, {}, this.SGET_options)
|
|
||||||
|
|
||||||
optionsSnapshot.hideHierarchyTree = !optionsSnapshot.hideHierarchyTree
|
|
||||||
|
|
||||||
this.SSET_options(optionsSnapshot)
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// ALL DOCUMENTS MANAGEMENT
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
@AllDocuments.Getter("getFirstRunState") SGET_allDocumentsFirstRunState!: boolean
|
|
||||||
|
|
||||||
@AllDocuments.Action("markAsNonFirstRun") SSET_allDocumentsMarkAsNonFirstRun!: () => void
|
|
||||||
|
|
||||||
@AllDocuments.Getter("getAllDocuments") SGET_allDocuments !: {
|
|
||||||
timestamp: string,
|
|
||||||
docs: I_ShortenedDocument[]
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllDocuments.Getter("getAllDocumentsWithoutCategories") SGET_allDocumentsWithoutCategories !: {
|
|
||||||
timestamp: string,
|
|
||||||
docs: I_ShortenedDocument[]
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllDocuments.Getter("getDocumentsByType") SGET_allDocumentsByType!: (documentTypeID: string) => {
|
|
||||||
timestamp: string,
|
|
||||||
id: string,
|
|
||||||
docs: I_ShortenedDocument[]
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllDocuments.Getter("getDocumentsByTypeWithoutCategories") SGET_allDocumentsByTypeWithoutCategories!: (documentTypeID: string) => {
|
|
||||||
timestamp: string,
|
|
||||||
id: string,
|
|
||||||
docs: I_ShortenedDocument[]
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllDocuments.Getter("getDocument") SGET_document!: (id: string) => I_ShortenedDocument
|
|
||||||
|
|
||||||
@AllDocuments.Action("addDocument") SSET_addDocument!: (input: {
|
|
||||||
doc: I_ShortenedDocument
|
|
||||||
}) => void
|
|
||||||
|
|
||||||
@AllDocuments.Action("updateDocument") SSET_updateDocument!: (input: {
|
|
||||||
doc: I_ShortenedDocument
|
|
||||||
}) => void
|
|
||||||
|
|
||||||
@AllDocuments.Action("removeDocument") SSET_removeDocument!: (input: {
|
|
||||||
doc: I_ShortenedDocument
|
|
||||||
}) => void
|
|
||||||
|
|
||||||
@AllDocuments.Action("mapNewDocumentType") SSET_mapNewDocumentType!: (input: {
|
|
||||||
id: string,
|
|
||||||
docs: I_ShortenedDocument[]
|
|
||||||
}) => void
|
|
||||||
|
|
||||||
@AllDocuments.Action("resetDocuments") SSET_resetAllDocuments!: () => void
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// OPEN DOCUMENTS MANAGEMENT
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
@OpenedDocuments.Getter("getAllDocuments") SGET_allOpenedDocuments !: {
|
|
||||||
treeAction: boolean,
|
|
||||||
lastRemovedIndex: number,
|
|
||||||
timestamp: string,
|
|
||||||
docs: I_OpenedDocument[]
|
|
||||||
}
|
|
||||||
|
|
||||||
@OpenedDocuments.Getter("getDocument") SGET_openedDocument!: (id: string) => I_OpenedDocument
|
|
||||||
|
|
||||||
@OpenedDocuments.Action("addDocument") SSET_addOpenedDocument!: (input: {
|
|
||||||
doc: I_OpenedDocument,
|
|
||||||
treeAction: boolean
|
|
||||||
}) => void
|
|
||||||
|
|
||||||
@OpenedDocuments.Action("updateDocument") SSET_updateOpenedDocument!: (input: {
|
|
||||||
doc: I_OpenedDocument,
|
|
||||||
treeAction: boolean
|
|
||||||
}) => void
|
|
||||||
|
|
||||||
@OpenedDocuments.Action("removeDocument") SSET_removeOpenedDocument!: (input: {
|
|
||||||
doc: I_OpenedDocument,
|
|
||||||
treeAction: boolean
|
|
||||||
}) => void
|
|
||||||
|
|
||||||
@OpenedDocuments.Action("closeAllDocuments") SSET_closeAllDocuments!: () => void
|
|
||||||
@OpenedDocuments.Action("forceCloseAllDocuments") SSET_forceCloseAllDocuments!: () => void
|
|
||||||
@OpenedDocuments.Action("closeAllButCurrentDocuments") SSET_closeAllButCurrentDocuments!: (input: I_OpenedDocument) => void
|
|
||||||
@OpenedDocuments.Action("forceCloseAllButCurrentDocuments") SSET_forceCloseAllButCurrentDocuments!: (input: I_OpenedDocument) => void
|
|
||||||
|
|
||||||
@OpenedDocuments.Action("triggerTreeAction") SSET_triggerTreeAction!: () => void
|
|
||||||
@OpenedDocuments.Action("resetDocuments") SSET_resetDocuments!: () => void
|
|
||||||
@OpenedDocuments.Action("resetRemoveIndex") SSET_resetRemoveIndex!: () => void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If provided with an document, finds it among the opened list
|
|
||||||
* Otherwise retireves the currently opened document based on the currently active route
|
|
||||||
*/
|
|
||||||
findRequestedOrActiveDocument (doc?: I_OpenedDocument) {
|
|
||||||
if (doc) {
|
|
||||||
return (this.SGET_allOpenedDocuments.docs.find(e => e.url === doc.url)) || false
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return (this.SGET_allOpenedDocuments.docs.find(e => e.url === this.$route.path)) || false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves value of requested field. If the field doesn't exist, returns false instead
|
|
||||||
* @param document - Document object that is expected to contain the field
|
|
||||||
* @param fieldID - ID of the field to check
|
|
||||||
*/
|
|
||||||
retrieveFieldValue (document: I_OpenedDocument| I_ShortenedDocument, fieldID: string) : string | number | [] | false | I_FieldRelationship {
|
|
||||||
const fieldData = document?.extraFields
|
|
||||||
|
|
||||||
// Fizzle if field doesnt exist
|
|
||||||
if (!fieldData) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const fieldValue = fieldData.find(f => f.id === fieldID)?.value as unknown as string
|
|
||||||
return fieldValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves value of requested field. If the field doesn't exist, returns false instead
|
|
||||||
* @param document - Document object that is expected to contain the field
|
|
||||||
* @param fieldID - ID of the field to check
|
|
||||||
*/
|
|
||||||
retrieveFieldType (document: I_OpenedDocument| I_ShortenedDocument, fieldID: string) : string | number | [] | false | I_FieldRelationship {
|
|
||||||
const fieldData = document?.extraFields
|
|
||||||
|
|
||||||
// Fizzle if field doesnt exist
|
|
||||||
if (!fieldData) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const documentBlueprint = this.SGET_blueprint(document.type)
|
|
||||||
|
|
||||||
const fieldType = documentBlueprint.extraFields.find(f => f.id === fieldID)?.type as unknown as string
|
|
||||||
return fieldType
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves value of requested field. If the field doesn't exist, returns false instead
|
|
||||||
* @param document - Document object that is expected to contain the field
|
|
||||||
* @param fieldID - ID of the field to check
|
|
||||||
*/
|
|
||||||
determineLegacyField (document: I_OpenedDocument| I_ShortenedDocument, fieldID: string) : string | number | [] | false | I_FieldRelationship {
|
|
||||||
const fieldData = document?.extraFields
|
|
||||||
|
|
||||||
// Fizzle if field doesnt exist
|
|
||||||
if (!fieldData) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const documentBlueprint = this.SGET_blueprint(document.type)
|
|
||||||
|
|
||||||
const legacyField = documentBlueprint.extraFields.find(f => f.id === fieldID)?.isLegacy as unknown as string
|
|
||||||
return (legacyField)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves array length of requested field. If the field doesn't exist or isn't array, returns false instead
|
|
||||||
* @param document - Document object that is expected to contain the field
|
|
||||||
* @param fieldID - ID of the field to check
|
|
||||||
*/
|
|
||||||
retrieveFieldLength (document: I_OpenedDocument, fieldID: string) : number | false {
|
|
||||||
const fieldData = document?.extraFields
|
|
||||||
|
|
||||||
// Fizzle if field doesnt exist
|
|
||||||
if (!fieldData) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const fieldValue = fieldData.find(f => f.id === fieldID)?.value
|
|
||||||
|
|
||||||
// Fizzle if the value isn't an array
|
|
||||||
if (!Array.isArray(fieldValue)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return fieldValue.length
|
|
||||||
}
|
|
||||||
|
|
||||||
@Dialogs.Getter("getDialogsState") SGET_getDialogsState!: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes the route
|
|
||||||
*/
|
|
||||||
refreshRoute () {
|
|
||||||
if (this.SGET_options.disableCloseAftertSelectQuickSearch && this.SGET_getDialogsState) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const remainingDocuments = this.SGET_allOpenedDocuments.docs
|
|
||||||
|
|
||||||
const lastIndex = this.SGET_allOpenedDocuments.lastRemovedIndex
|
|
||||||
|
|
||||||
const newIndex = (lastIndex > -1 && lastIndex < remainingDocuments.length) ? lastIndex : remainingDocuments.length - 1
|
|
||||||
|
|
||||||
if (lastIndex > -1) {
|
|
||||||
this.SSET_resetRemoveIndex()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assuming there are any documents in the current list
|
|
||||||
if (remainingDocuments.length > 0) {
|
|
||||||
const lastDocument = remainingDocuments[newIndex]
|
|
||||||
const currentRoute = this.$router.currentRoute.path
|
|
||||||
const existingDocument = this.SGET_allOpenedDocuments.docs.find(e => {
|
|
||||||
return e.url === currentRoute
|
|
||||||
})
|
|
||||||
|
|
||||||
// Prevent infite route cycling by checking if this actually exists in the open tabs
|
|
||||||
if (existingDocument) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load a new route if the new route isnt the one we are already on
|
|
||||||
const newRoute = `/project/display-content/${lastDocument.type}/${lastDocument._id}`
|
|
||||||
if (currentRoute !== newRoute) {
|
|
||||||
this.$router.push({ path: newRoute }).catch(e => console.log(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If there are no documents inthe list, just navigate to the front page of the project
|
|
||||||
else {
|
|
||||||
this.$router.push({ path: "/project" }).catch((e: {name: string}) => {
|
|
||||||
if (e && e.name !== "NavigationDuplicated") {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// DOCUMENT LIST MANAGEMENT
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively retrieves a full hieararchical path from a full list
|
|
||||||
*/
|
|
||||||
getDocumentHieararchicalPath (document: I_OpenedDocument, list: I_OpenedDocument[]) {
|
|
||||||
let hierarchicalString = ""
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const parentDoc = (this.retrieveFieldValue(document, "parentDoc"))?.value
|
|
||||||
|
|
||||||
const parentDocInDB = (list.find(p => p._id === parentDoc?._id))
|
|
||||||
|
|
||||||
if (!parentDoc || (parentDoc && !parentDocInDB)) {
|
|
||||||
const singleBlueprintName = this.SGET_allBlueprints.find(e => e._id === document.type)?.nameSingular
|
|
||||||
hierarchicalString += singleBlueprintName
|
|
||||||
return hierarchicalString
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchingDoc = list.find((doc:I_OpenedDocument) => {
|
|
||||||
return doc._id === parentDoc._id
|
|
||||||
}) as I_OpenedDocument
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
hierarchicalString += this.retrieveFieldValue(matchingDoc, "name")
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const connectedReturn = this.getDocumentHieararchicalPath(matchingDoc, list)
|
|
||||||
if (connectedReturn) {
|
|
||||||
hierarchicalString = `${connectedReturn} > ${hierarchicalString}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return hierarchicalString
|
|
||||||
}
|
|
||||||
|
|
||||||
mapShortDocument (doc: I_ShortenedDocument, dbDocuments: I_OpenedDocument[]) : I_ShortenedDocument {
|
|
||||||
return {
|
|
||||||
label: doc.extraFields.find(e => e.id === "name")?.value,
|
|
||||||
icon: doc.icon,
|
|
||||||
// @ts-ignore
|
|
||||||
id: doc._id,
|
|
||||||
_id: doc._id,
|
|
||||||
url: doc.url,
|
|
||||||
type: doc.type,
|
|
||||||
extraFields: doc.extraFields,
|
|
||||||
hasEdits: false,
|
|
||||||
// @ts-ignore
|
|
||||||
hierarchicalPath: this.getDocumentHieararchicalPath(doc, dbDocuments),
|
|
||||||
tags: doc.extraFields.find(e => e.id === "tags")?.value,
|
|
||||||
color: doc.extraFields.find(e => e.id === "documentColor")?.value,
|
|
||||||
bgColor: doc.extraFields.find(e => e.id === "documentBackgroundColor")?.value,
|
|
||||||
isCategory: doc.extraFields.find(e => e.id === "categorySwitch")?.value,
|
|
||||||
isMinor: doc.extraFields.find(e => e.id === "minorSwitch")?.value,
|
|
||||||
isDead: doc.extraFields.find(e => e.id === "deadSwitch")?.value
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deepFreeze (object: object) {
|
|
||||||
// Retrieve the property names defined on object
|
|
||||||
const propNames = Object.getOwnPropertyNames(object)
|
|
||||||
|
|
||||||
// Freeze properties before freezing self
|
|
||||||
|
|
||||||
for (const name of propNames) {
|
|
||||||
const value = object[name]
|
|
||||||
|
|
||||||
if (value && typeof value === "object") {
|
|
||||||
this.deepFreeze(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.freeze(object)
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 103 KiB |
|
@ -1 +0,0 @@
|
||||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#FFFFFF;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 8.8 KiB |
|
@ -1,4 +0,0 @@
|
||||||
import Vue from "vue"
|
|
||||||
import VueApexCharts from "vue-apexcharts"
|
|
||||||
|
|
||||||
Vue.component("apexchart", VueApexCharts)
|
|
|
@ -1,13 +1,31 @@
|
||||||
import axios, { AxiosInstance } from "axios"
|
import { boot } from 'quasar/wrappers'
|
||||||
import { boot } from "quasar/wrappers"
|
import axios, { AxiosInstance } from 'axios'
|
||||||
|
|
||||||
declare module "vue/types/vue" {
|
declare module '@vue/runtime-core' {
|
||||||
interface Vue {
|
interface ComponentCustomProperties {
|
||||||
$axios: AxiosInstance;
|
$axios: AxiosInstance;
|
||||||
|
$api: AxiosInstance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default boot(({ Vue }) => {
|
// Be careful when using SSR for cross-request state pollution
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
// due to creating a Singleton instance here;
|
||||||
Vue.prototype.$axios = axios
|
// If any client changes this (global) instance, it might be a
|
||||||
|
// good idea to move this instance creation inside of the
|
||||||
|
// "export default () => {}" function below (which runs individually
|
||||||
|
// for each client)
|
||||||
|
const api = axios.create({ baseURL: 'https://api.example.com' })
|
||||||
|
|
||||||
|
export default boot(({ app }) => {
|
||||||
|
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||||
|
|
||||||
|
app.config.globalProperties.$axios = axios
|
||||||
|
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||||
|
// so you won't necessarily have to import axios in each vue file
|
||||||
|
|
||||||
|
app.config.globalProperties.$api = api
|
||||||
|
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||||
|
// so you can easily perform requests against your app's API
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export { api }
|
||||||
|
|
41
src/boot/externalLinkManager.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { boot } from 'quasar/wrappers'
|
||||||
|
|
||||||
|
export default boot(() => {
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
/**
|
||||||
|
* Target of the click event
|
||||||
|
*/
|
||||||
|
const clickTarget = e.target as HTMLAnchorElement
|
||||||
|
|
||||||
|
if (clickTarget) {
|
||||||
|
/**
|
||||||
|
* Closest anchor element
|
||||||
|
* Selector might end up being empty if the anchor itself was clicked. If so, select it instead
|
||||||
|
*/
|
||||||
|
let originLink: HTMLAnchorElement|null|false = clickTarget.closest('a')
|
||||||
|
if (originLink === null) {
|
||||||
|
originLink = (clickTarget.tagName === 'a') ? clickTarget : false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originLink) {
|
||||||
|
/**
|
||||||
|
* HREF link of the url
|
||||||
|
*/
|
||||||
|
const linkURL = originLink.href
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the URL is extenal or not
|
||||||
|
*/
|
||||||
|
const isExternal = window.faExternalLinksManagerAPI.checkIfExternal(linkURL)
|
||||||
|
|
||||||
|
if (isExternal) {
|
||||||
|
/**
|
||||||
|
* If the URL is external, prevent default and open the URL via the electron API functionality
|
||||||
|
*/
|
||||||
|
e.preventDefault()
|
||||||
|
window.faExternalLinksManagerAPI.openExternal(linkURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,23 +1,35 @@
|
||||||
import { boot } from "quasar/wrappers"
|
import { boot } from 'quasar/wrappers'
|
||||||
import messages from "src/i18n"
|
import { createI18n } from 'vue-i18n'
|
||||||
import Vue from "vue"
|
|
||||||
import VueI18n from "vue-i18n"
|
|
||||||
|
|
||||||
declare module "vue/types/vue" {
|
import messages from 'src/i18n'
|
||||||
interface Vue {
|
|
||||||
i18n: VueI18n;
|
export type MessageLanguages = keyof typeof messages;
|
||||||
}
|
// Type-define 'en-US' as the master schema for the resource
|
||||||
|
export type MessageSchema = typeof messages['en-US'];
|
||||||
|
|
||||||
|
// See https://vue-i18n.intlify.dev/guide/advanced/typescript.html#global-resource-schema-type-definition
|
||||||
|
/* eslint-disable @typescript-eslint/no-empty-interface */
|
||||||
|
declare module 'vue-i18n' {
|
||||||
|
// define the locale messages schema
|
||||||
|
export interface DefineLocaleMessage extends MessageSchema {}
|
||||||
|
|
||||||
|
// define the datetime format schema
|
||||||
|
export interface DefineDateTimeFormat {}
|
||||||
|
|
||||||
|
// define the number format schema
|
||||||
|
export interface DefineNumberFormat {}
|
||||||
}
|
}
|
||||||
|
/* eslint-enable @typescript-eslint/no-empty-interface */
|
||||||
|
|
||||||
Vue.use(VueI18n)
|
export default boot(({ app }) => {
|
||||||
|
const i18n = createI18n({
|
||||||
export const i18n = new VueI18n({
|
locale: 'en-US',
|
||||||
locale: "en-us",
|
fallbackLocale: 'en-US',
|
||||||
fallbackLocale: "en-us",
|
legacy: false,
|
||||||
|
warnHtmlMessage: false,
|
||||||
messages
|
messages
|
||||||
})
|
})
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
|
||||||
// Set i18n instance on app
|
// Set i18n instance on app
|
||||||
app.i18n = i18n
|
app.use(i18n)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Notify } from "quasar"
|
|
||||||
|
|
||||||
Notify.setDefaults({
|
|
||||||
position: "bottom-right",
|
|
||||||
timeout: 4000,
|
|
||||||
progress: true,
|
|
||||||
textColor: "cultured"
|
|
||||||
})
|
|
8
src/boot/notify-defaults.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { Notify } from 'quasar'
|
||||||
|
|
||||||
|
Notify.setDefaults({
|
||||||
|
position: 'bottom-right',
|
||||||
|
timeout: 4000,
|
||||||
|
progress: true,
|
||||||
|
textColor: 'cultured'
|
||||||
|
})
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { _electron as electron } from 'playwright'
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { extraEnvVariablesAPI } from 'app/src-electron/customContentBridgeAPIs/extraEnvVariablesAPI'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra env settings to trigger component testing via Playwright
|
||||||
|
*/
|
||||||
|
const extraEnvSettings = {
|
||||||
|
TEST_ENV: 'components',
|
||||||
|
COMPONENT_NAME: 'AppControlMenus',
|
||||||
|
COMPONENT_PROPS: JSON.stringify({})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron main filepath
|
||||||
|
*/
|
||||||
|
const electronMainFilePath:string = extraEnvVariablesAPI.ELECTRON_MAIN_FILEPATH
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra rended timer buffer for tests to start after loading the app
|
||||||
|
* - Change here in order manually adjust this component's wait times
|
||||||
|
*/
|
||||||
|
const faFrontendRenderTimer = extraEnvVariablesAPI.FA_FRONTEND_RENDER_TIMER
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object of string data selectors for the component
|
||||||
|
*/
|
||||||
|
const selectorList = {
|
||||||
|
testMenu: 'appControlMenus-testMenu',
|
||||||
|
anyMenu: 'appControlMenus-anyMenu'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a custom "Test Title" menu button in the menu and check if it loaded
|
||||||
|
*/
|
||||||
|
test('Load "Test Title" menu button sub-component', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const testMenu = await appWindow.$(`[data-test-test-menu="${selectorList.testMenu}"]`)
|
||||||
|
|
||||||
|
// Check if the tested element exists
|
||||||
|
if (testMenu !== null) {
|
||||||
|
await expect(true).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we have exactly one testing menu loaded
|
||||||
|
*/
|
||||||
|
test('Check if we have exactly one testing menu loaded', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const anyMenus = await appWindow.$$(`[data-test-any-menu="${selectorList.anyMenu}"]`)
|
||||||
|
|
||||||
|
// Check for example one testing menu
|
||||||
|
if (anyMenus.length === 1) {
|
||||||
|
await expect(true).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// No menus/too many menus
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
76
src/components/AppControlMenus/AppControlMenus.vue
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="appControlMenus bg-dark"
|
||||||
|
>
|
||||||
|
<q-btn-group
|
||||||
|
flat
|
||||||
|
class="appControlMenus__inner"
|
||||||
|
>
|
||||||
|
<!-- Test data menu - FOR COMPOENT TEST PURPOSES ONLY -->
|
||||||
|
<AppControlSingleMenu
|
||||||
|
v-if="testingType === 'components'"
|
||||||
|
:data-input="testData"
|
||||||
|
data-test-test-menu="appControlMenus-testMenu"
|
||||||
|
data-test-any-menu="appControlMenus-anyMenu"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Project menu -->
|
||||||
|
<AppControlSingleMenu
|
||||||
|
v-if="testingType !== 'components'"
|
||||||
|
:data-input="project"
|
||||||
|
data-test-any-menu="appControlMenus-anyMenu"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Documents menu -->
|
||||||
|
<AppControlSingleMenu
|
||||||
|
v-if="testingType !== 'components'"
|
||||||
|
:data-input="documents"
|
||||||
|
data-test-any-menu="appControlMenus-anyMenu"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Tools menu -->
|
||||||
|
<AppControlSingleMenu
|
||||||
|
v-if="testingType !== 'components'"
|
||||||
|
:data-input="tools"
|
||||||
|
data-test-any-menu="appControlMenus-anyMenu"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Help & Info Menu -->
|
||||||
|
<AppControlSingleMenu
|
||||||
|
v-if="testingType !== 'components'"
|
||||||
|
:data-input="helpInfo"
|
||||||
|
data-test-any-menu="appControlMenus-anyMenu"
|
||||||
|
/>
|
||||||
|
</q-btn-group>
|
||||||
|
|
||||||
|
<!-- Dialog Popups -->
|
||||||
|
<DialogMarkdownDocument />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { testData } from 'app/src/components/AppControlMenus/_testData/test.raw.component'
|
||||||
|
|
||||||
|
import { project } from 'app/src/components/AppControlMenus/_data/project'
|
||||||
|
import { documents } from 'app/src/components/AppControlMenus/_data/documents'
|
||||||
|
import { tools } from 'app/src/components/AppControlMenus/_data/tools'
|
||||||
|
import { helpInfo } from 'app/src/components/AppControlMenus/_data/helpInfo'
|
||||||
|
|
||||||
|
import AppControlSingleMenu from 'app/src/components/AppControlMenus/AppControlSingleMenu/AppControlSingleMenu.vue'
|
||||||
|
import DialogMarkdownDocument from 'app/src/components/DialogMarkdownDocument/DialogMarkdownDocument.vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testing type currently possibly happening
|
||||||
|
*/
|
||||||
|
const testingType = window.extraEnvVariables.TEST_ENV
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.appControlMenus {
|
||||||
|
&__inner {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,409 @@
|
||||||
|
import { _electron as electron } from 'playwright'
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { extraEnvVariablesAPI } from 'app/src-electron/customContentBridgeAPIs/extraEnvVariablesAPI'
|
||||||
|
import { testData } from '../_testData/test.fixed.component'
|
||||||
|
import { rgbToHex } from 'src/scripts/_utilities/colorFormatConvertors'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra env settings to trigger component testing via Playwright
|
||||||
|
*/
|
||||||
|
const extraEnvSettings = {
|
||||||
|
TEST_ENV: 'components',
|
||||||
|
COMPONENT_NAME: 'AppControlSingleMenu',
|
||||||
|
COMPONENT_PROPS: JSON.stringify({})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron main filepath
|
||||||
|
*/
|
||||||
|
const electronMainFilePath:string = extraEnvVariablesAPI.ELECTRON_MAIN_FILEPATH
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra rended timer buffer for tests to start after loading the app
|
||||||
|
* - Change here in order manually adjust this component's wait times
|
||||||
|
*/
|
||||||
|
const faFrontendRenderTimer = extraEnvVariablesAPI.FA_FRONTEND_RENDER_TIMER
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object of string data selectors for the component
|
||||||
|
*/
|
||||||
|
const selectorList = {
|
||||||
|
menuWrapper: 'AppControlSingleMenu-wrapper',
|
||||||
|
menuTitle: 'AppControlSingleMenu-title',
|
||||||
|
menuItem: 'AppControlSingleMenu-menuItem',
|
||||||
|
menuItemText: 'AppControlSingleMenu-menuItem-text',
|
||||||
|
menuItemIcon: 'AppControlSingleMenu-menuItem-icon',
|
||||||
|
menuItemSubMenu: 'AppControlSingleMenu-menuItem-subMenu',
|
||||||
|
menuItemSubMenuItem: 'AppControlSingleMenu-menuItem-subMenu-item',
|
||||||
|
menuItemSubMenuItemText: 'AppControlSingleMenu-menuItem-subMenu-item-text',
|
||||||
|
menuItemSubMenuItemIcon: 'AppControlSingleMenu-menuItem-subMenu-item-icon'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if the component managed to load the test data
|
||||||
|
*/
|
||||||
|
test('Test if the component managed to load the test data', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const menuWrapperElement = await appWindow.$(`[data-test="${selectorList.menuWrapper}"]`)
|
||||||
|
|
||||||
|
// Check if the tested element exists
|
||||||
|
if (menuWrapperElement !== null) {
|
||||||
|
const hasProperInput = await menuWrapperElement.evaluate(el => !!el.dataset.testHasProperDataInput)
|
||||||
|
|
||||||
|
await expect(hasProperInput).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a custom "Test Title" menu button in the menu and check if it loaded
|
||||||
|
*/
|
||||||
|
test('Check if the "Menu title" element is properly loaded and has proper content in it', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const menuTitleElement = await appWindow.$(`[data-test="${selectorList.menuTitle}"]`)
|
||||||
|
const menuTitleElementText = (menuTitleElement !== null) ? await menuTitleElement.textContent() : ''
|
||||||
|
|
||||||
|
// Check if the tested element exists and has proper title content
|
||||||
|
if (menuTitleElement !== null && menuTitleElementText === testData.title) {
|
||||||
|
await expect(true).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist OR lacks proper title in it
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the main menu has a wrapper, click and check if all menu elements loaded properly
|
||||||
|
*/
|
||||||
|
test('Check if the main menu has a wrapper, click and check if all menu elements loaded properly', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const menuWrapper = await appWindow.$(`[data-test="${selectorList.menuWrapper}"]`)
|
||||||
|
|
||||||
|
// Check if wrapper exists for clicking and if so, click it
|
||||||
|
if (menuWrapper !== null) {
|
||||||
|
await menuWrapper.click()
|
||||||
|
|
||||||
|
const menuItems = await appWindow.$$(`[data-test="${selectorList.menuItem}"]`)
|
||||||
|
const dataItems = testData.data.filter(item => item.mode === 'item')
|
||||||
|
|
||||||
|
// Check if element feed matched the data feed
|
||||||
|
await expect(menuItems.length === dataItems.length).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Wrapper for opening the menu doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the first main menu item has proper text and icon
|
||||||
|
*/
|
||||||
|
test('Check if the first main menu item has proper text and icon', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const menuWrapper = await appWindow.$(`[data-test="${selectorList.menuWrapper}"]`)
|
||||||
|
|
||||||
|
// Check if wrapper exists for clicking and if so, click it
|
||||||
|
if (menuWrapper !== null) {
|
||||||
|
await menuWrapper.click()
|
||||||
|
|
||||||
|
const firstMenuItem = await appWindow.$(`[data-test="${selectorList.menuItem}"]`)
|
||||||
|
const firstDataItem = testData.data.filter(item => item.mode === 'item')[0]
|
||||||
|
|
||||||
|
// Check if the item wrapper exists
|
||||||
|
if (firstMenuItem !== null) {
|
||||||
|
const firstMenuItemTextElement = await firstMenuItem.$(`[data-test="${selectorList.menuItemText}"]`)
|
||||||
|
const firstMenuItemIconElement = await firstMenuItem.$(`[data-test="${selectorList.menuItemIcon}"]`)
|
||||||
|
|
||||||
|
// Check if the icon and text wrappers exist
|
||||||
|
if (firstMenuItemTextElement !== null && firstMenuItemIconElement !== null) {
|
||||||
|
const firstMenuItemText = await firstMenuItemTextElement.textContent()
|
||||||
|
const firstDataItemText = firstDataItem.text
|
||||||
|
|
||||||
|
const firstMenuItemIconClassObject = await firstMenuItemIconElement.evaluate(el => el.classList)
|
||||||
|
const firstMenuItemIconClassList = Object.values(firstMenuItemIconClassObject).concat()
|
||||||
|
const firstDataItemIcon = firstDataItem.icon as string
|
||||||
|
|
||||||
|
await expect(firstMenuItemText === firstDataItemText && firstMenuItemIconClassList.includes(firstDataItemIcon)).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Item text or icon wrappers don't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Item wrapper doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Wrapper for opening the menu doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if text color class applied properly to any main menu item: Secondary
|
||||||
|
*/
|
||||||
|
test('Check if text color class applied properly to any main menu item: Secondary', async () => {
|
||||||
|
const testColorString = 'secondary'
|
||||||
|
const testColorHexString = '#f75746'
|
||||||
|
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const menuWrapper = await appWindow.$(`[data-test="${selectorList.menuWrapper}"]`)
|
||||||
|
|
||||||
|
// Check if wrapper exists for clicking and if so, click it
|
||||||
|
if (menuWrapper !== null) {
|
||||||
|
await menuWrapper.click()
|
||||||
|
|
||||||
|
const colorMenuItem = await appWindow.$(`.text-${testColorString}[data-test="${selectorList.menuItem}"]`)
|
||||||
|
|
||||||
|
// Check if the item wrapper exists
|
||||||
|
if (colorMenuItem !== null) {
|
||||||
|
const colorMenuString = await colorMenuItem.evaluate(el => getComputedStyle(el).getPropertyValue('color'))
|
||||||
|
const colorHexString = rgbToHex(colorMenuString)
|
||||||
|
|
||||||
|
// Compare color of the string with secondary color
|
||||||
|
await expect(colorHexString).toBe(testColorHexString)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Item wrapper doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Wrapper for opening the menu doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the sub-menu opens properly on click of the main menu item and all parts are loaded properly
|
||||||
|
*/
|
||||||
|
test('Check if the sub-menu opens properly on click of the main menu item and all parts are loaded properly', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const menuWrapper = await appWindow.$(`[data-test="${selectorList.menuWrapper}"]`)
|
||||||
|
|
||||||
|
// Check if main menu wrapper exists for clicking and if so, click it
|
||||||
|
if (menuWrapper !== null) {
|
||||||
|
await menuWrapper.click()
|
||||||
|
await appWindow.waitForTimeout(600)
|
||||||
|
|
||||||
|
const menuItems = await appWindow.$$(`[data-test="${selectorList.menuItem}"]`)
|
||||||
|
const dataElement = testData.data.filter(item => item.mode === 'item').find(el => el.submenu !== undefined)
|
||||||
|
const dataIndex = testData.data.filter(item => item.mode === 'item').findIndex(el => el.submenu !== undefined)
|
||||||
|
|
||||||
|
const submenuTrigger = menuItems[dataIndex]
|
||||||
|
|
||||||
|
// Check if the submenu trigger exists and click it if it does
|
||||||
|
if (submenuTrigger !== null) {
|
||||||
|
await submenuTrigger.click()
|
||||||
|
await appWindow.waitForTimeout(600)
|
||||||
|
} else {
|
||||||
|
// Submenu trigger doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
const subMenuWrapper = await appWindow.$(`[data-test="${selectorList.menuItemSubMenu}"]`)
|
||||||
|
|
||||||
|
// Check if submenu wrapper doesn't exist
|
||||||
|
if (subMenuWrapper === null) {
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
const subMenuItems = await appWindow.$$(`[data-test="${selectorList.menuItemSubMenuItem}"]`)
|
||||||
|
const dataSubmenuItems = (dataElement?.submenu !== undefined) ? dataElement.submenu.filter(item => item.mode === 'item') : false
|
||||||
|
|
||||||
|
const subMenuItemsCount = subMenuItems.length
|
||||||
|
const dataSubmenuItemsCount = (dataSubmenuItems) ? dataSubmenuItems.length : false
|
||||||
|
|
||||||
|
await expect(subMenuItemsCount === dataSubmenuItemsCount).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Wrapper for opening the main menu doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the first sub-menu item has proper text and icon
|
||||||
|
*/
|
||||||
|
test('Check if the first sub-menu item has proper text and icon', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const menuWrapper = await appWindow.$(`[data-test="${selectorList.menuWrapper}"]`)
|
||||||
|
|
||||||
|
// Check if main menu wrapper exists for clicking and if so, click it
|
||||||
|
if (menuWrapper !== null) {
|
||||||
|
await menuWrapper.click()
|
||||||
|
await appWindow.waitForTimeout(600)
|
||||||
|
|
||||||
|
const menuItems = await appWindow.$$(`[data-test="${selectorList.menuItem}"]`)
|
||||||
|
const dataElement = testData.data.filter(item => item.mode === 'item').find(el => el.submenu !== undefined)
|
||||||
|
const dataIndex = testData.data.filter(item => item.mode === 'item').findIndex(el => el.submenu !== undefined)
|
||||||
|
|
||||||
|
const submenuTrigger = menuItems[dataIndex]
|
||||||
|
|
||||||
|
// Check if the submenu trigger exists and click it if it does
|
||||||
|
if (submenuTrigger !== null) {
|
||||||
|
await submenuTrigger.click()
|
||||||
|
await appWindow.waitForTimeout(600)
|
||||||
|
} else {
|
||||||
|
// Submenu trigger doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
const subMenuWrapper = await appWindow.$(`[data-test="${selectorList.menuItemSubMenu}"]`)
|
||||||
|
|
||||||
|
// Check if submenu wrapper doesn't exist
|
||||||
|
if (subMenuWrapper === null) {
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstSubMenuItem = await appWindow.$(`[data-test="${selectorList.menuItemSubMenuItem}"]`)
|
||||||
|
const firstDataSubmenuItem = (dataElement?.submenu !== undefined) ? dataElement.submenu.filter(item => item.mode === 'item')[0] : false
|
||||||
|
|
||||||
|
// Check if the sub-menu item wrapper exists and if the first data-item isn't false
|
||||||
|
if (firstSubMenuItem !== null && firstDataSubmenuItem) {
|
||||||
|
const firstSubmenuItemTextElement = await firstSubMenuItem.$(`[data-test="${selectorList.menuItemSubMenuItemText}"]`)
|
||||||
|
const firstSubmenuItemIconElement = await firstSubMenuItem.$(`[data-test="${selectorList.menuItemSubMenuItemIcon}"]`)
|
||||||
|
|
||||||
|
// Check if the icon and text wrappers exist
|
||||||
|
if (firstSubmenuItemTextElement !== null && firstSubmenuItemIconElement !== null) {
|
||||||
|
const firstSubmenuItemText = await firstSubmenuItemTextElement.textContent()
|
||||||
|
const firstDataItemText = firstDataSubmenuItem.text
|
||||||
|
|
||||||
|
const firstSubmenuItemIconClassObject = await firstSubmenuItemIconElement.evaluate(el => el.classList)
|
||||||
|
const firstSubmenuItemIconClassList = Object.values(firstSubmenuItemIconClassObject).concat()
|
||||||
|
const firstDataItemIcon = firstDataSubmenuItem.icon as string
|
||||||
|
|
||||||
|
await expect(firstSubmenuItemText === firstDataItemText && firstSubmenuItemIconClassList.includes(firstDataItemIcon)).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Item text or icon wrappers don't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Item wrapper doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Wrapper for opening the main menu doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if text color class applied properly to any sub-main menu item: Secondary
|
||||||
|
*/
|
||||||
|
test('Check if text color class applied properly to any sub-main menu item: Secondary', async () => {
|
||||||
|
const testColorString = 'secondary'
|
||||||
|
const testColorHexString = '#f75746'
|
||||||
|
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const menuWrapper = await appWindow.$(`[data-test="${selectorList.menuWrapper}"]`)
|
||||||
|
|
||||||
|
// Check if main menu wrapper exists for clicking and if so, click it
|
||||||
|
if (menuWrapper !== null) {
|
||||||
|
await menuWrapper.click()
|
||||||
|
await appWindow.waitForTimeout(600)
|
||||||
|
|
||||||
|
const menuItems = await appWindow.$$(`[data-test="${selectorList.menuItem}"]`)
|
||||||
|
const dataIndex = testData.data.filter(item => item.mode === 'item').findIndex(el => el.submenu !== undefined)
|
||||||
|
|
||||||
|
const submenuTrigger = menuItems[dataIndex]
|
||||||
|
|
||||||
|
// Check if the submenu trigger exists and click it if it does
|
||||||
|
if (submenuTrigger !== null) {
|
||||||
|
await submenuTrigger.click()
|
||||||
|
await appWindow.waitForTimeout(600)
|
||||||
|
} else {
|
||||||
|
// Submenu trigger doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
const subMenuWrapper = await appWindow.$(`[data-test="${selectorList.menuItemSubMenu}"]`)
|
||||||
|
|
||||||
|
// Check if submenu wrapper doesn't exist
|
||||||
|
if (subMenuWrapper === null) {
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorSubMenuItem = await appWindow.$(`.text-${testColorString}[data-test="${selectorList.menuItemSubMenuItem}"]`)
|
||||||
|
|
||||||
|
// Check if the colored sub-menu item wrapper exists
|
||||||
|
if (colorSubMenuItem !== null) {
|
||||||
|
const colorMenuString = await colorSubMenuItem.evaluate(el => getComputedStyle(el).getPropertyValue('color'))
|
||||||
|
const colorHexString = rgbToHex(colorMenuString)
|
||||||
|
|
||||||
|
// Compare color of the string with secondary color
|
||||||
|
await expect(colorHexString).toBe(testColorHexString)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Item wrapper doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Wrapper for opening the main menu doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,212 @@
|
||||||
|
<template>
|
||||||
|
<!-- Main menu - Wrapper -->
|
||||||
|
<q-btn
|
||||||
|
v-if="hasProperDataInput"
|
||||||
|
flat
|
||||||
|
class="appControlSingleMenu non-selectable"
|
||||||
|
dark
|
||||||
|
size="md"
|
||||||
|
no-caps
|
||||||
|
data-test="AppControlSingleMenu-wrapper"
|
||||||
|
:data-test-has-proper-data-input="hasProperDataInput"
|
||||||
|
>
|
||||||
|
<!-- Main menu - Title -->
|
||||||
|
<span
|
||||||
|
v-if="menuTitle"
|
||||||
|
data-test="AppControlSingleMenu-title"
|
||||||
|
>
|
||||||
|
{{ menuTitle }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Main menu - Content -->
|
||||||
|
<q-menu
|
||||||
|
anchor="bottom left"
|
||||||
|
square
|
||||||
|
dark
|
||||||
|
transition-show="jump-down"
|
||||||
|
transition-hide="jump-up"
|
||||||
|
>
|
||||||
|
<q-list
|
||||||
|
class="appControlSingleMenu__list"
|
||||||
|
dark
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(menuItem,index) in menuData"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<q-separator
|
||||||
|
v-if="menuItem.mode === 'separator'"
|
||||||
|
class="appControlSingleMenu__separator"
|
||||||
|
dark
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
v-if="menuItem.mode === 'item'"
|
||||||
|
v-close-popup="menuItem.submenu === undefined ? true : false"
|
||||||
|
clickable
|
||||||
|
data-test="AppControlSingleMenu-menuItem"
|
||||||
|
:class="['appControlSingleMenu__item', `text-${menuItem.specialColor}`, 'non-selectable']"
|
||||||
|
:disable="(!menuItem.conditions)"
|
||||||
|
@click="(menuItem.trigger)
|
||||||
|
? menuItem.triggerArguments
|
||||||
|
? menuItem.trigger(...menuItem.triggerArguments)
|
||||||
|
: menuItem.trigger()
|
||||||
|
: false"
|
||||||
|
>
|
||||||
|
<q-item-section data-test="AppControlSingleMenu-menuItem-text">
|
||||||
|
{{ menuItem.text }}
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon
|
||||||
|
class="appControlSingleMenu__icon"
|
||||||
|
:name="menuItem.icon"
|
||||||
|
data-test="AppControlSingleMenu-menuItem-icon"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- Sub-menu -->
|
||||||
|
<q-menu
|
||||||
|
v-if="menuItem.submenu !== undefined"
|
||||||
|
anchor="top end"
|
||||||
|
self="top start"
|
||||||
|
square
|
||||||
|
dark
|
||||||
|
transition-show="jump-right"
|
||||||
|
transition-hide="jump-left"
|
||||||
|
class="-subMenu"
|
||||||
|
data-test="AppControlSingleMenu-menuItem-subMenu"
|
||||||
|
>
|
||||||
|
<q-list
|
||||||
|
class="appControlSingleMenu__list"
|
||||||
|
dark
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(submenuItem,subIndex) in menuItem.submenu"
|
||||||
|
:key="subIndex"
|
||||||
|
>
|
||||||
|
<q-separator
|
||||||
|
v-if="submenuItem.mode === 'separator'"
|
||||||
|
class="appControlSingleMenu__separator"
|
||||||
|
dark
|
||||||
|
/>
|
||||||
|
<q-item
|
||||||
|
v-if="submenuItem.mode === 'item'"
|
||||||
|
v-close-popup
|
||||||
|
clickable
|
||||||
|
:class="['appControlSingleMenu__item', `text-${submenuItem.specialColor}`, 'non-selectable']"
|
||||||
|
:disable="(!submenuItem.conditions)"
|
||||||
|
data-test="AppControlSingleMenu-menuItem-subMenu-item"
|
||||||
|
@click="(submenuItem.trigger) ? submenuItem.trigger() : false"
|
||||||
|
>
|
||||||
|
<q-item-section
|
||||||
|
data-test="AppControlSingleMenu-menuItem-subMenu-item-text"
|
||||||
|
>
|
||||||
|
{{ submenuItem.text }}
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon
|
||||||
|
class="appControlSingleMenu__icon"
|
||||||
|
data-test="AppControlSingleMenu-menuItem-subMenu-item-icon"
|
||||||
|
:name="submenuItem.icon"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
<!-- Sub-menu end -->
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
<!-- Main menu end - Content -->
|
||||||
|
</q-btn>
|
||||||
|
<!-- Main menu end - Wrappper -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// TODO - ADD TESTS
|
||||||
|
|
||||||
|
import { I_appMenusDataList } from 'app/interfaces/I_appMenusDataList'
|
||||||
|
import { testData } from '../_testData/test.raw.component'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All component props
|
||||||
|
*/
|
||||||
|
const props = defineProps<{
|
||||||
|
/**
|
||||||
|
* Data input for the component
|
||||||
|
*/
|
||||||
|
dataInput: I_appMenusDataList
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testing type currently possibly happening
|
||||||
|
*/
|
||||||
|
const testingType = window.extraEnvVariables.TEST_ENV
|
||||||
|
|
||||||
|
const componentData = computed(() => {
|
||||||
|
if (testingType === 'components') {
|
||||||
|
return testData
|
||||||
|
} else {
|
||||||
|
return props.dataInput
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the input has "proper" data in it
|
||||||
|
* Checks for:
|
||||||
|
* - Title
|
||||||
|
* - Overall data feed
|
||||||
|
*/
|
||||||
|
const hasProperDataInput = !!(componentData.value.title && componentData.value.data)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu title from the prop
|
||||||
|
*/
|
||||||
|
const menuTitle = componentData.value.title
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu data content from the prop
|
||||||
|
*/
|
||||||
|
const menuData = componentData.value.data
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.appControlSingleMenu {
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: $appControlMenus_singleHover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: $iconSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
background-color: $appControlMenus_bgColor;
|
||||||
|
color: $appControlMenus_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
min-height: 42px;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: $appControlMenus_singleHover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text legibitility fix: Red and green blend too much into each other without a little shadow */
|
||||||
|
&.text-secondary {
|
||||||
|
text-shadow: 0 0 3px black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__separator {
|
||||||
|
background-color: $appControlMenus_separatorColor;
|
||||||
|
height: 0.5px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
20
src/components/AppControlMenus/_data/documents.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { i18n } from 'app/src/i18n/externalFileLoader'
|
||||||
|
|
||||||
|
import { I_appMenusDataList } from 'app/interfaces/I_appMenusDataList'
|
||||||
|
|
||||||
|
// TODO - add functionality for all buttons and conditions
|
||||||
|
|
||||||
|
export const documents: I_appMenusDataList = {
|
||||||
|
title: i18n.global.t('AppControlMenus.documents.title'),
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.documents.items.quickAddNewDocument'),
|
||||||
|
icon: 'mdi-text-box-plus-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
85
src/components/AppControlMenus/_data/helpInfo.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { i18n } from 'app/src/i18n/externalFileLoader'
|
||||||
|
|
||||||
|
import { I_appMenusDataList } from 'app/interfaces/I_appMenusDataList'
|
||||||
|
|
||||||
|
// TODO - add functionality for all buttons and conditions
|
||||||
|
import { toggleDevTools } from 'app/src/scripts/appInfo/toggleDevTools'
|
||||||
|
import { openDialogMarkdownDocument } from 'app/src/scripts/appInfo/openDialogMarkdownDocument'
|
||||||
|
|
||||||
|
export const helpInfo: I_appMenusDataList = {
|
||||||
|
title: i18n.global.t('AppControlMenus.helpInfo.title'),
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.helpInfo.items.showKeybindsCheatsheet'),
|
||||||
|
icon: 'mdi-keyboard-settings',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.helpInfo.items.advancedSearchGuide'),
|
||||||
|
icon: 'mdi-file-question',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: openDialogMarkdownDocument,
|
||||||
|
triggerArguments: ['advancedSearchGuide'],
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.helpInfo.items.tipsTricksTrivia'),
|
||||||
|
icon: 'mdi-fire-alert',
|
||||||
|
trigger: openDialogMarkdownDocument,
|
||||||
|
triggerArguments: ['tipsTricksTrivia'],
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.helpInfo.items.changelog'),
|
||||||
|
icon: 'mdi-clipboard-text',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: openDialogMarkdownDocument,
|
||||||
|
triggerArguments: ['changeLog'],
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.helpInfo.items.aboutFantsiaArchive'),
|
||||||
|
icon: 'mdi-information-variant',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.helpInfo.items.license'),
|
||||||
|
icon: 'mdi-script-text-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: openDialogMarkdownDocument,
|
||||||
|
triggerArguments: ['license'],
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.helpInfo.items.toggleDeveloperTools'),
|
||||||
|
icon: 'mdi-code-tags',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: toggleDevTools,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
115
src/components/AppControlMenus/_data/project.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import { i18n } from 'app/src/i18n/externalFileLoader'
|
||||||
|
|
||||||
|
import { I_appMenusDataList } from 'app/interfaces/I_appMenusDataList'
|
||||||
|
|
||||||
|
// TODO - add functionality for all buttons and conditions
|
||||||
|
|
||||||
|
export const project: I_appMenusDataList = {
|
||||||
|
title: i18n.global.t('AppControlMenus.project.title'),
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.project.items.newProject'),
|
||||||
|
icon: 'mdi-plus',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.project.items.saveProject'),
|
||||||
|
icon: 'mdi-package-variant-closed',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.project.items.loadProject'),
|
||||||
|
icon: 'mdi-package-variant',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.project.items.exportProjectDocuments'),
|
||||||
|
icon: 'mdi-database-export-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.project.items.showProjectDashboard'),
|
||||||
|
icon: 'mdi-chart-bar',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.project.items.projectSettings'),
|
||||||
|
icon: 'mdi-book-cog-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.project.items.closeProject'),
|
||||||
|
icon: 'mdi-exit-to-app',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.project.items.advancedProjectTools'),
|
||||||
|
icon: 'keyboard_arrow_right',
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: 'grey',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.project.items.aptMerge'),
|
||||||
|
icon: 'mdi-folder-plus-outline',
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.project.items.aptConvertOld'),
|
||||||
|
icon: 'mdi-wrench',
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
75
src/components/AppControlMenus/_data/tools.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { i18n } from 'app/src/i18n/externalFileLoader'
|
||||||
|
|
||||||
|
import { I_appMenusDataList } from 'app/interfaces/I_appMenusDataList'
|
||||||
|
|
||||||
|
// TODO - add functionality for all buttons and conditions
|
||||||
|
|
||||||
|
export const tools: I_appMenusDataList = {
|
||||||
|
title: i18n.global.t('AppControlMenus.tools.title'),
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.tools.items.quickAddNewDocument'),
|
||||||
|
icon: 'mdi-text-box-plus-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.tools.items.quickSearchDocument'),
|
||||||
|
icon: 'mdi-database-search',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.tools.items.massDeleteDocument'),
|
||||||
|
icon: 'mdi-text-box-remove-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: 'secondary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.tools.items.toggleTree'),
|
||||||
|
icon: 'mdi-page-layout-sidebar-left',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.tools.items.showNoteBoard'),
|
||||||
|
icon: 'mdi-clipboard-text-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: i18n.global.t('AppControlMenus.tools.items.programSettings'),
|
||||||
|
icon: 'mdi-tune',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { I_appMenusDataList } from 'app/interfaces/I_appMenusDataList'
|
||||||
|
|
||||||
|
export const testData:I_appMenusDataList = {
|
||||||
|
title: 'Test Title',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 1 - Open Dialog with Markdown document',
|
||||||
|
icon: 'mdi-text-box-plus-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
triggerArguments: ['changeLog'],
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 2',
|
||||||
|
icon: 'mdi-database-search',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 3 - Secondary',
|
||||||
|
icon: 'mdi-text-box-remove-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: 'secondary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 4',
|
||||||
|
icon: 'mdi-page-layout-sidebar-left',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 5',
|
||||||
|
icon: 'mdi-clipboard-text-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 6 - Grey, Submenu',
|
||||||
|
icon: 'keyboard_arrow_right',
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: 'grey',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Submenu-Test Button 1',
|
||||||
|
icon: 'mdi-folder-plus-outline',
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Submenu-Test Button 2 - Secondary',
|
||||||
|
icon: 'mdi-wrench',
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: 'secondary'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { I_appMenusDataList } from 'app/interfaces/I_appMenusDataList'
|
||||||
|
import { openDialogMarkdownDocument } from 'src/scripts/appInfo/openDialogMarkdownDocument'
|
||||||
|
|
||||||
|
export const testData:I_appMenusDataList = {
|
||||||
|
title: 'Test Title',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 1 - Open Dialog with Markdown document',
|
||||||
|
icon: 'mdi-text-box-plus-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: openDialogMarkdownDocument,
|
||||||
|
triggerArguments: ['changeLog'],
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 2',
|
||||||
|
icon: 'mdi-database-search',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 3 - Secondary',
|
||||||
|
icon: 'mdi-text-box-remove-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: 'secondary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 4',
|
||||||
|
icon: 'mdi-page-layout-sidebar-left',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 5',
|
||||||
|
icon: 'mdi-clipboard-text-outline',
|
||||||
|
submenu: undefined,
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Test Button 6 - Grey, Submenu',
|
||||||
|
icon: 'keyboard_arrow_right',
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: 'grey',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Submenu-Test Button 1',
|
||||||
|
icon: 'mdi-folder-plus-outline',
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'item',
|
||||||
|
text: 'Submenu-Test Button 2 - Secondary',
|
||||||
|
icon: 'mdi-wrench',
|
||||||
|
trigger: undefined,
|
||||||
|
conditions: true,
|
||||||
|
specialColor: 'secondary'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,65 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<q-header
|
|
||||||
elevated
|
|
||||||
class="bg-dark text-cultured appHeader"
|
|
||||||
>
|
|
||||||
|
|
||||||
<div class="appHeaderInner">
|
|
||||||
<appControl
|
|
||||||
class="appControl"
|
|
||||||
/>
|
|
||||||
<topTabs
|
|
||||||
class="topTabs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</q-header>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component } from "vue-property-decorator"
|
|
||||||
|
|
||||||
import BaseClass from "src/BaseClass"
|
|
||||||
import topTabs from "src/components/appHeader/TopTabs.vue"
|
|
||||||
import appControl from "src/components/appHeader/AppControl.vue"
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
topTabs: topTabs,
|
|
||||||
appControl: appControl
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export default class AppHeader extends BaseClass {
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.appHeaderInner {
|
|
||||||
z-index: 999999;
|
|
||||||
display: flex;
|
|
||||||
min-height: 40px;
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
width: calc(100% - 147px);
|
|
||||||
|
|
||||||
.appControl {
|
|
||||||
width: 375px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topTabs {
|
|
||||||
max-width: calc(100% - 415px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.appWindowButtons {
|
|
||||||
//width: 200px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { _electron as electron } from 'playwright'
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { extraEnvVariablesAPI } from 'app/src-electron/customContentBridgeAPIs/extraEnvVariablesAPI'
|
||||||
|
import { T_documentList } from 'app/interfaces/T_documentList'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra env settings to trigger component testing via Playwright
|
||||||
|
*/
|
||||||
|
const extraEnvSettings = {
|
||||||
|
TEST_ENV: 'components',
|
||||||
|
COMPONENT_NAME: 'DialogMarkdownDocument',
|
||||||
|
COMPONENT_PROPS: JSON.stringify({})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron main filepath
|
||||||
|
*/
|
||||||
|
const electronMainFilePath:string = extraEnvVariablesAPI.ELECTRON_MAIN_FILEPATH
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra rended timer buffer for tests to start after loading the app
|
||||||
|
* - Change here in order manually adjust this component's wait times
|
||||||
|
*/
|
||||||
|
const faFrontendRenderTimer = extraEnvVariablesAPI.FA_FRONTEND_RENDER_TIMER
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object of string data selectors for the component
|
||||||
|
*/
|
||||||
|
const selectorList = {
|
||||||
|
markdownWrapper: 'dialogMarkdownDocument-markdown-wrapper',
|
||||||
|
markdownContent: 'dialogMarkdownDocument-markdown-content',
|
||||||
|
closeButton: 'dialogMarkdownDocument-button-close'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feed 'license' input as the file to open and check if the opened dialog afterwars has all the needed elements in it
|
||||||
|
*/
|
||||||
|
test('Open test "license" dialog with all elements in it', async () => {
|
||||||
|
const testString: T_documentList = 'license'
|
||||||
|
|
||||||
|
extraEnvSettings.COMPONENT_PROPS = JSON.stringify({ directInput: testString })
|
||||||
|
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const closeButton = await appWindow.$(`[data-test="${selectorList.closeButton}"]`)
|
||||||
|
const markdownWrapper = await appWindow.$(`[data-test="${selectorList.markdownWrapper}"]`)
|
||||||
|
const markdownContent = await appWindow.$(`[data-test="${selectorList.markdownContent}"]`)
|
||||||
|
|
||||||
|
// Check if the tested elements exists
|
||||||
|
if (closeButton !== null && markdownWrapper !== null && markdownContent !== null) {
|
||||||
|
expect(true).toBe(true)
|
||||||
|
} else {
|
||||||
|
// Elements don't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feed 'license' input as the file to open and check if the opened dialog afterwars has all the needed elements in it
|
||||||
|
*/
|
||||||
|
test('Open test "license" dialog and try closing it', async () => {
|
||||||
|
const testString: T_documentList = 'license'
|
||||||
|
|
||||||
|
extraEnvSettings.COMPONENT_PROPS = JSON.stringify({ directInput: testString })
|
||||||
|
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const closeButton = await appWindow.$(`[data-test="${selectorList.closeButton}"]`)
|
||||||
|
const markdownContent = await appWindow.$(`[data-test="${selectorList.markdownContent}"]`)
|
||||||
|
|
||||||
|
// Check if the close button exists
|
||||||
|
if (closeButton !== null && markdownContent !== null) {
|
||||||
|
await closeButton.click()
|
||||||
|
await appWindow.waitForTimeout(1500)
|
||||||
|
|
||||||
|
// Check if the content is properly hidden after closing the popup
|
||||||
|
await expect(await markdownContent.isHidden()).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Close button doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
147
src/components/DialogMarkdownDocument/DialogMarkdownDocument.vue
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
<template>
|
||||||
|
<!-- Dialog wrapper -->
|
||||||
|
<q-dialog
|
||||||
|
v-model="dialogModel"
|
||||||
|
:class="['dialogMarkdownDocument', `${documentName}`]"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<!-- Dialog contents wrapper -->
|
||||||
|
<q-card-section :class="['dialogMarkdownDocument__content', `${documentName}`, 'q-mt-xl', 'q-mb-lg', 'q-mr-lg', 'q-ml-xl', 'q-pt-none']">
|
||||||
|
<div
|
||||||
|
class="flex justify-center"
|
||||||
|
data-test="dialogMarkdownDocument-markdown-wrapper"
|
||||||
|
>
|
||||||
|
<!-- Dialog markdown -->
|
||||||
|
<q-markdown
|
||||||
|
no-heading-anchor-links
|
||||||
|
data-test="dialogMarkdownDocument-markdown-content"
|
||||||
|
:class="[`${documentName}`, 'dialogMarkdownDocument']"
|
||||||
|
:src="$t(`documents.${documentName}`)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- Card actions wrapper -->
|
||||||
|
<q-card-actions
|
||||||
|
align="around"
|
||||||
|
class="q-mb-lg"
|
||||||
|
>
|
||||||
|
<!-- Close button -->
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
label="Close"
|
||||||
|
color="accent"
|
||||||
|
data-test="dialogMarkdownDocument-button-close"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { QMarkdown } from '@quasar/quasar-ui-qmarkdown'
|
||||||
|
import '@quasar/quasar-ui-qmarkdown/dist/index.css'
|
||||||
|
import { T_documentList } from 'app/interfaces/T_documentList'
|
||||||
|
import { S_DialogMarkdown } from 'src/stores/S_DialogMarkdown'
|
||||||
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All component props
|
||||||
|
*/
|
||||||
|
const props = defineProps<{
|
||||||
|
/**
|
||||||
|
* Custom input directly fed to the component in case it doesn't get triggered from the global store
|
||||||
|
*/
|
||||||
|
directInput?: T_documentList
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model for the current popup dialog
|
||||||
|
*/
|
||||||
|
const dialogModel = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the document shown inside the dialog
|
||||||
|
*/
|
||||||
|
const documentName = ref('')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the popup dialog via direct input-feed
|
||||||
|
*/
|
||||||
|
const openDialog = (input: T_documentList) => {
|
||||||
|
documentName.value = input
|
||||||
|
dialogModel.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger dialog popup via reaction to store update
|
||||||
|
*/
|
||||||
|
watch(() => S_DialogMarkdown.dialogUUID, () => {
|
||||||
|
openDialog(S_DialogMarkdown.documentToOpen)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger dialog popup via reaction to direct prop feed
|
||||||
|
*/
|
||||||
|
watch(() => props.directInput, () => {
|
||||||
|
if (props.directInput !== undefined && props.directInput !== '') {
|
||||||
|
openDialog(props.directInput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the prop feed-status on the first mount and open the dialog if the prop is properly fed in
|
||||||
|
* This exist mostly due to component tests being flaky otherwise
|
||||||
|
*/
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.directInput !== undefined && props.directInput !== '') {
|
||||||
|
openDialog(props.directInput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
.dialogMarkdownDocument {
|
||||||
|
.q-card {
|
||||||
|
max-width: calc(100vw - 100px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.license .q-card {
|
||||||
|
width: 680px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.changeLog .q-card {
|
||||||
|
width: 1100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.advancedSearchGuide .q-card {
|
||||||
|
width: 1100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tipsTricksTrivia .q-card {
|
||||||
|
width: 1100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
overflow: auto;
|
||||||
|
max-height: calc(100vh - 235px);
|
||||||
|
min-height: 650px;
|
||||||
|
|
||||||
|
&.tipsTricksTrivia {
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.changeLog {
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.advancedSearchGuide {
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -1,747 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<!-- New document dialog -->
|
|
||||||
<newDocumentDialog
|
|
||||||
:dialog-trigger="newObjectDialogTrigger"
|
|
||||||
@trigger-dialog-close="newObjectDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Existing document dialog -->
|
|
||||||
<existingDocumentDialog
|
|
||||||
:dialog-trigger="existingObjectDialogTrigger"
|
|
||||||
@trigger-dialog-close="existingObjectDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Delele document dialog -->
|
|
||||||
<deleteDocumentCheckDialog
|
|
||||||
:dialog-trigger="deleteObjectDialogTrigger"
|
|
||||||
@trigger-dialog-close="deleteObjectDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Advanced search guide dialog -->
|
|
||||||
<advancedSearchGuideDialog
|
|
||||||
:dialog-trigger="advancedSearchGuideDialogTrigger"
|
|
||||||
@trigger-dialog-close="advancedSearchGuideDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Keybind dialog -->
|
|
||||||
<keybindCheatsheetDialog
|
|
||||||
:dialog-trigger="keybindsDialogTrigger"
|
|
||||||
@trigger-dialog-close="keybindsDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Tips, Tricks & Trivia dialog -->
|
|
||||||
<tipsTricksTriviaDialog
|
|
||||||
:dialog-trigger="tipsTricksDialogTrigger"
|
|
||||||
@trigger-dialog-close="tipsTricksDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-page-sticky position="top-right"
|
|
||||||
class="documentControl bg-dark"
|
|
||||||
:class="{'fullScreen': hideHierarchyTree}"
|
|
||||||
v-if="!disableDocumentControlBar"
|
|
||||||
>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="documentControl__blocker"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="documentControl__wrapper"
|
|
||||||
:class="{'fullScreen': hideHierarchyTree}"
|
|
||||||
>
|
|
||||||
|
|
||||||
<div class="documentControl__left">
|
|
||||||
|
|
||||||
<template v-if="!disableDocumentControlBarGuides">
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-keyboard-settings"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
@click="keybindsDialogAssignUID"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom middle"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Open keybinds cheatsheet
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-file-question"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
@click="advancedSearchGuideAssignUID"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom middle"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Open advanced search guide
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-fire-alert"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
@click="tipsTricksAssignUID"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom middle"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Open Tips, Tricks & Trivia
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-separator vertical inset color="accent" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-package-variant-closed"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
:disable="!projectExists"
|
|
||||||
@click="commenceExport"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom middle"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Export current project
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-separator vertical inset color="accent" />
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-page-layout-sidebar-left"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
@click="toggleHierarchicalTree"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom middle"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Toggle hierarchical tree
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-clipboard-text-outline"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
@click="SSET_setNoteCorkboardWindowVisible"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom middle"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Show note board
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-separator vertical inset color="accent" />
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-database-search"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
@click="existingObjectAssignUID"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom middle"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Quick-search an existing document
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-text-box-plus-outline"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
@click="newObjectAssignUID"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom middle"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Quick-add a new document
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="documentControl__right">
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-file-document-edit"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
@click="toggleEditMode"
|
|
||||||
v-if="currentyEditable && SGET_allOpenedDocuments.docs.length > 0 && this.$route.path !== '/project'"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom middle"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Edit current document
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-content-save-edit"
|
|
||||||
:color="(!hasEdits) ? 'teal-14' : 'primary'"
|
|
||||||
outline
|
|
||||||
@click="saveCurrentDocument(true)"
|
|
||||||
v-if="!currentyEditable && SGET_allOpenedDocuments.docs.length > 0 && this.$route.path !== '/project'"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom left"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Save document without exiting edit mode
|
|
||||||
</q-tooltip>
|
|
||||||
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-content-save"
|
|
||||||
:color="(!hasEdits) ? 'teal-14' : 'primary'"
|
|
||||||
outline
|
|
||||||
@click="saveCurrentDocument(false)"
|
|
||||||
v-if="!currentyEditable && SGET_allOpenedDocuments.docs.length > 0 && this.$route.path !== '/project'"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom left"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Save current document
|
|
||||||
</q-tooltip>
|
|
||||||
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-file-tree"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
@click="addNewUnderParent"
|
|
||||||
v-if="!currentlyNew && SGET_allOpenedDocuments.docs.length > 0 && this.$route.path !== '/project'"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
max-width="500px"
|
|
||||||
anchor="bottom left"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Add a new document with the currently opened one as the parent
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-content-copy"
|
|
||||||
color="primary"
|
|
||||||
outline
|
|
||||||
@click="copyTargetDocument"
|
|
||||||
v-if="!currentlyNew && SGET_allOpenedDocuments.docs.length > 0 && this.$route.path !== '/project'"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
max-width="500px"
|
|
||||||
anchor="bottom left"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Copy current document
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-separator vertical inset color="accent"
|
|
||||||
v-if="!currentlyNew && SGET_allOpenedDocuments.docs.length > 0 && this.$route.path !== '/project'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-text-box-remove-outline"
|
|
||||||
color="secondary"
|
|
||||||
outline
|
|
||||||
@click="deleteObjectAssignUID"
|
|
||||||
v-if="!currentlyNew && SGET_allOpenedDocuments.docs.length > 0 && this.$route.path !== '/project'"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
:delay="500"
|
|
||||||
anchor="bottom left"
|
|
||||||
self="top middle"
|
|
||||||
>
|
|
||||||
Delete current document
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</q-page-sticky>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component, Watch } from "vue-property-decorator"
|
|
||||||
|
|
||||||
import BaseClass from "src/BaseClass"
|
|
||||||
import newDocumentDialog from "src/components/dialogs/NewDocument.vue"
|
|
||||||
import existingDocumentDialog from "src/components/dialogs/ExistingDocument.vue"
|
|
||||||
import deleteDocumentCheckDialog from "src/components/dialogs/DeleteDocumentCheck.vue"
|
|
||||||
import advancedSearchGuideDialog from "src/components/dialogs/AdvancedSearchGuide.vue"
|
|
||||||
import keybindCheatsheetDialog from "src/components/dialogs/KeybindCheatsheet.vue"
|
|
||||||
import tipsTricksTriviaDialog from "src/components/dialogs/TipsTricksTrivia.vue"
|
|
||||||
|
|
||||||
import { I_OpenedDocument } from "src/interfaces/I_OpenedDocument"
|
|
||||||
import { extend, Loading, QSpinnerGears } from "quasar"
|
|
||||||
import { saveDocument } from "src/scripts/databaseManager/documentManager"
|
|
||||||
import { createNewWithParent } from "src/scripts/documentActions/createNewWithParent"
|
|
||||||
import { copyDocument } from "src/scripts/documentActions/copyDocument"
|
|
||||||
|
|
||||||
import { retrieveCurrentProjectName, exportProject } from "src/scripts/projectManagement/projectManagent"
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
newDocumentDialog,
|
|
||||||
existingDocumentDialog,
|
|
||||||
deleteDocumentCheckDialog,
|
|
||||||
advancedSearchGuideDialog,
|
|
||||||
keybindCheatsheetDialog,
|
|
||||||
tipsTricksTriviaDialog
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export default class DocumentControl extends BaseClass {
|
|
||||||
projectExists: undefined | string | boolean = false
|
|
||||||
projectName = ""
|
|
||||||
|
|
||||||
disableDocumentControlBar = false
|
|
||||||
disableDocumentControlBarGuides = false
|
|
||||||
|
|
||||||
@Watch("SGET_options", { immediate: true, deep: true })
|
|
||||||
onSettingsChange () {
|
|
||||||
const options = this.SGET_options
|
|
||||||
this.disableDocumentControlBar = options.disableDocumentControlBar
|
|
||||||
this.disableDocumentControlBarGuides = options.disableDocumentControlBarGuides
|
|
||||||
this.hideHierarchyTree = options.hideHierarchyTree
|
|
||||||
}
|
|
||||||
|
|
||||||
hideHierarchyTree = false
|
|
||||||
|
|
||||||
async created () {
|
|
||||||
this.projectName = await retrieveCurrentProjectName()
|
|
||||||
this.projectExists = !!(await retrieveCurrentProjectName())
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Keybinds management
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Local keybinds
|
|
||||||
*/
|
|
||||||
@Watch("SGET_getCurrentKeyBindData", { deep: true })
|
|
||||||
async processKeyPush () {
|
|
||||||
// Quick new document
|
|
||||||
if (this.determineKeyBind("quickNewDocument") && !this.SGET_getDialogsState) {
|
|
||||||
this.newObjectAssignUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quick open existing document
|
|
||||||
if (this.determineKeyBind("quickExistingDocument") && !this.SGET_getDialogsState) {
|
|
||||||
this.existingObjectAssignUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete dialog - CTRL + D
|
|
||||||
if (this.determineKeyBind("deleteDocument") && !this.currentlyNew && this.SGET_allOpenedDocuments.docs.length > 0 && !this.SGET_getDialogsState && this.$route.path !== "/project") {
|
|
||||||
this.deleteObjectAssignUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit document - CTRL + E
|
|
||||||
if (this.determineKeyBind("editDocument") && this.currentyEditable && this.SGET_allOpenedDocuments.docs.length > 0 && !this.SGET_getDialogsState && this.$route.path !== "/project") {
|
|
||||||
this.toggleEditMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save document - CTRL + S
|
|
||||||
if (this.determineKeyBind("saveDocument") && !this.currentyEditable && this.SGET_allOpenedDocuments.docs.length > 0 && !this.SGET_getDialogsState && this.$route.path !== "/project") {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.saveCurrentDocument(false).catch(e => console.log(e))
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save document without exiting edit mode - CTRL + ALT + S
|
|
||||||
if (this.determineKeyBind("saveDocumentNoExit") && !this.currentyEditable && this.SGET_allOpenedDocuments.docs.length > 0 && !this.SGET_getDialogsState && this.$route.path !== "/project") {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.saveCurrentDocument(true).catch(e => console.log(e))
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new under parent - CTRL + SHIFT + N
|
|
||||||
if (this.determineKeyBind("addUnderParent") && !this.currentlyNew && this.SGET_allOpenedDocuments.docs.length > 0 && !this.SGET_getDialogsState && this.$route.path !== "/project") {
|
|
||||||
await this.sleep(100)
|
|
||||||
this.addNewUnderParent()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy document - CTRL + ALT + C
|
|
||||||
if (this.determineKeyBind("copyDocument") && !this.currentlyNew && this.SGET_allOpenedDocuments.docs.length > 0 && !this.SGET_getDialogsState && this.$route.path !== "/project") {
|
|
||||||
await this.sleep(100)
|
|
||||||
this.copyTargetDocument()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle hierarchical tree - CTRL + ALT + SHIFT + T
|
|
||||||
if (this.determineKeyBind("toggleHierarchicalTree")) {
|
|
||||||
// @ts-ignore
|
|
||||||
this.toggleHierarchicalTree()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Advanced search guide dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
advancedSearchGuideDialogTrigger: string | false = false
|
|
||||||
advancedSearchGuideDialogClose () {
|
|
||||||
this.advancedSearchGuideDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
advancedSearchGuideAssignUID () {
|
|
||||||
this.advancedSearchGuideDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Keybings cheatsheet dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
keybindsDialogTrigger: string | false = false
|
|
||||||
keybindsDialogClose () {
|
|
||||||
this.keybindsDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
keybindsDialogAssignUID () {
|
|
||||||
this.keybindsDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Delete dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
deleteObjectDialogTrigger: string | false = false
|
|
||||||
deleteObjectDialogClose () {
|
|
||||||
this.deleteObjectDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteObjectAssignUID () {
|
|
||||||
this.deleteObjectDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// New document dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
newObjectDialogTrigger: string | false = false
|
|
||||||
newObjectDialogClose () {
|
|
||||||
this.newObjectDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
newObjectAssignUID () {
|
|
||||||
this.newObjectDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Existing document dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
existingObjectDialogTrigger: string | false = false
|
|
||||||
existingObjectDialogClose () {
|
|
||||||
this.existingObjectDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
existingObjectAssignUID () {
|
|
||||||
this.existingObjectDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Tips, Tricka & Trivia dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
tipsTricksDialogTrigger: string | false = false
|
|
||||||
tipsTricksDialogClose () {
|
|
||||||
this.tipsTricksDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
tipsTricksAssignUID () {
|
|
||||||
this.tipsTricksDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Export project
|
|
||||||
/****************************************************************/
|
|
||||||
retrieveCurrentProjectName = retrieveCurrentProjectName
|
|
||||||
|
|
||||||
async commenceExport () {
|
|
||||||
const projectName = await retrieveCurrentProjectName()
|
|
||||||
const setup = {
|
|
||||||
message: "<h4>Exporting current project...</h4>",
|
|
||||||
spinnerColor: "primary",
|
|
||||||
messageColor: "cultured",
|
|
||||||
spinnerSize: 120,
|
|
||||||
backgroundColor: "dark",
|
|
||||||
// @ts-ignore
|
|
||||||
spinner: QSpinnerGears
|
|
||||||
}
|
|
||||||
exportProject(projectName, Loading, setup, this.$q)
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Add new document under parent
|
|
||||||
/****************************************************************/
|
|
||||||
addNewUnderParent () {
|
|
||||||
const currentDoc = this.findRequestedOrActiveDocument() as I_OpenedDocument
|
|
||||||
createNewWithParent(currentDoc, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Document copy
|
|
||||||
/****************************************************************/
|
|
||||||
documentPass = null as unknown as I_OpenedDocument
|
|
||||||
|
|
||||||
copyTargetDocument () {
|
|
||||||
this.documentPass = extend(true, {}, this.findRequestedOrActiveDocument())
|
|
||||||
|
|
||||||
const blueprint = this.SGET_blueprint(this.documentPass.type)
|
|
||||||
const newDocument = copyDocument(this.documentPass, this.generateUID(), blueprint)
|
|
||||||
|
|
||||||
const dataPass = {
|
|
||||||
doc: newDocument,
|
|
||||||
treeAction: false
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
this.SSET_addOpenedDocument(dataPass)
|
|
||||||
this.$router.push({
|
|
||||||
path: newDocument.url
|
|
||||||
}).catch((e: {name: string}) => {
|
|
||||||
const errorName : string = e.name
|
|
||||||
if (errorName === "NavigationDuplicated") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Toggle edit mode & Save document
|
|
||||||
/****************************************************************/
|
|
||||||
toggleEditMode () {
|
|
||||||
const currentDoc = this.findRequestedOrActiveDocument()
|
|
||||||
if (currentDoc && !currentDoc.editMode) {
|
|
||||||
const dataCopy: I_OpenedDocument = extend(true, {}, currentDoc)
|
|
||||||
dataCopy.editMode = true
|
|
||||||
const dataPass = { doc: dataCopy, treeAction: false }
|
|
||||||
this.SSET_updateOpenedDocument(dataPass)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
documentsCopy = null as unknown as I_OpenedDocument[]
|
|
||||||
|
|
||||||
async saveCurrentDocument (editMode: boolean) {
|
|
||||||
if (document.activeElement && editMode === false) {
|
|
||||||
(document.activeElement as HTMLElement).blur()
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentDoc = this.findRequestedOrActiveDocument()
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const isNew = currentDoc.isNew
|
|
||||||
|
|
||||||
const allDocuments = this.SGET_allOpenedDocuments
|
|
||||||
|
|
||||||
this.documentsCopy = extend(true, [], allDocuments.docs)
|
|
||||||
|
|
||||||
if (currentDoc) {
|
|
||||||
// @ts-ignore
|
|
||||||
const savedDocument: {
|
|
||||||
documentCopy: I_OpenedDocument,
|
|
||||||
allOpenedDocuments: I_OpenedDocument[]
|
|
||||||
} = await saveDocument(currentDoc, this.documentsCopy, this.SGET_allDocuments.docs, editMode, this).catch(err => console.log(err))
|
|
||||||
|
|
||||||
// Update the opened document
|
|
||||||
const dataPass = { doc: savedDocument.documentCopy, treeAction: true }
|
|
||||||
this.SSET_updateOpenedDocument(dataPass)
|
|
||||||
|
|
||||||
// Update document
|
|
||||||
if (!isNew) {
|
|
||||||
// @ts-ignore
|
|
||||||
this.SSET_updateDocument({ doc: this.mapShortDocument(savedDocument.documentCopy, this.SGET_allDocumentsByType(savedDocument.documentCopy.type).docs) })
|
|
||||||
}
|
|
||||||
// Add new document
|
|
||||||
else {
|
|
||||||
// @ts-ignore
|
|
||||||
this.SSET_addDocument({ doc: this.mapShortDocument(savedDocument.documentCopy, this.SGET_allDocumentsByType(savedDocument.documentCopy.type).docs) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update all others
|
|
||||||
for (const doc of savedDocument.allOpenedDocuments) {
|
|
||||||
// Update the opened document
|
|
||||||
const dataPass = { doc: doc, treeAction: true }
|
|
||||||
this.SSET_updateOpenedDocument(dataPass)
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
this.SSET_updateDocument({ doc: this.mapShortDocument(doc, this.SGET_allDocumentsByType(doc.type).docs) })
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$q.notify({
|
|
||||||
group: false,
|
|
||||||
type: "positive",
|
|
||||||
message: "Document successfully saved"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("$route", { immediate: true, deep: true })
|
|
||||||
onUrlChange () {
|
|
||||||
this.checkEditability()
|
|
||||||
this.checkNew()
|
|
||||||
this.checkHasEdits()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("SGET_allOpenedDocuments", { deep: true })
|
|
||||||
onDocChange () {
|
|
||||||
this.checkEditability()
|
|
||||||
this.checkNew()
|
|
||||||
this.checkHasEdits()
|
|
||||||
}
|
|
||||||
|
|
||||||
hasEdits = false
|
|
||||||
|
|
||||||
checkHasEdits () {
|
|
||||||
const currentDocument = this.findRequestedOrActiveDocument()
|
|
||||||
|
|
||||||
if (currentDocument && !currentDocument.hasEdits) {
|
|
||||||
this.hasEdits = true
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.hasEdits = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEditability () {
|
|
||||||
const currentDocument = this.findRequestedOrActiveDocument()
|
|
||||||
|
|
||||||
if (currentDocument && !currentDocument.editMode) {
|
|
||||||
this.currentyEditable = true
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.currentyEditable = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkNew () {
|
|
||||||
const currentDocument = this.findRequestedOrActiveDocument()
|
|
||||||
|
|
||||||
if (currentDocument && currentDocument.isNew) {
|
|
||||||
this.currentlyNew = true
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.currentlyNew = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentyEditable = false
|
|
||||||
currentlyNew = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.documentControl {
|
|
||||||
z-index: 999;
|
|
||||||
width: calc(100vw - 380px);
|
|
||||||
margin-top: 2.5px;
|
|
||||||
|
|
||||||
&.fullScreen {
|
|
||||||
width: calc(100vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__blocker {
|
|
||||||
position: absolute;
|
|
||||||
top: -7.5px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: darken($dark, 0.5);
|
|
||||||
z-index: 999;
|
|
||||||
height: 7.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
width: calc(100vw - 385px);
|
|
||||||
padding: 8.5px 15px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&.fullScreen {
|
|
||||||
width: calc(100vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: " ";
|
|
||||||
bottom: 1px;
|
|
||||||
right: -5px;
|
|
||||||
left: -5px;
|
|
||||||
position: absolute;
|
|
||||||
height: 1px;
|
|
||||||
background-color: rgba($accent, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__left,
|
|
||||||
&__right {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__left {
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
.q-btn,
|
|
||||||
.q-separator {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__right {
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
.q-btn,
|
|
||||||
.q-separator {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html body {
|
|
||||||
&.q-body--prevent-scroll {
|
|
||||||
.documentControl {
|
|
||||||
min-width: calc(100vw - 375px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,466 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-tooltip
|
|
||||||
content-class="documentPreviewWrapper"
|
|
||||||
:content-style="`z-index: ${specialZIndex} !important;`"
|
|
||||||
:delay="customDelay"
|
|
||||||
max-width="700px"
|
|
||||||
max-height="600px"
|
|
||||||
:target="customTarget"
|
|
||||||
:offset="[0, 0]"
|
|
||||||
:anchor="customAnchor"
|
|
||||||
:self="customSelf"
|
|
||||||
@before-show="openDocumentPreview"
|
|
||||||
@before-hide="consitentDocumentPreviewSwitch"
|
|
||||||
v-model="documentPreviewSwitch"
|
|
||||||
transition-show="scale"
|
|
||||||
transition-hide="scale"
|
|
||||||
ref="documentPreview"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="localBlueprint"
|
|
||||||
class="documentPreviewContent"
|
|
||||||
@mouseenter="clearCloseTimer"
|
|
||||||
@mouseleave="setCloseTimer"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="field in localBlueprint.extraFields"
|
|
||||||
:key="`${field.id}`"
|
|
||||||
class="col-12 q-mb-md"
|
|
||||||
v-show="hasValueFieldFilter(field) && !determineLegacyField(localDocument, field.id)"
|
|
||||||
>
|
|
||||||
|
|
||||||
<Field_Break
|
|
||||||
class="inputWrapper break"
|
|
||||||
v-if="field.type === 'break' && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="retrieveFieldValue(localDocument, field.id)"
|
|
||||||
:recursive="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field_Text
|
|
||||||
class="inputWrapper"
|
|
||||||
v-if="field.type === 'text' && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="retrieveFieldValue(localDocument, field.id)"
|
|
||||||
:isNew="false"
|
|
||||||
:editMode="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field_Number
|
|
||||||
class="inputWrapper"
|
|
||||||
v-if="field.type === 'number' && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="retrieveFieldValue(localDocument, field.id)"
|
|
||||||
:isNew="false"
|
|
||||||
:editMode="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field_Switch
|
|
||||||
class="inputWrapper"
|
|
||||||
v-if="field.type === 'switch' && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="retrieveFieldValue(localDocument, field.id)"
|
|
||||||
:isNew="false"
|
|
||||||
:editMode="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field_ColorPicker
|
|
||||||
class="inputWrapper"
|
|
||||||
v-if="field.type === 'colorPicker' && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="retrieveFieldValue(localDocument, field.id)"
|
|
||||||
:isNew="false"
|
|
||||||
:editMode="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field_List
|
|
||||||
class="inputWrapper"
|
|
||||||
v-if="field.type === 'list' && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="retrieveFieldValue(localDocument, field.id)"
|
|
||||||
:isNew="false"
|
|
||||||
:editMode="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field_SingleSelect
|
|
||||||
class="inputWrapper"
|
|
||||||
v-if="field.type === 'singleSelect' && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="retrieveFieldValue(localDocument, field.id)"
|
|
||||||
:isNew="false"
|
|
||||||
:editMode="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field_MultiSelect
|
|
||||||
class="inputWrapper"
|
|
||||||
v-if="field.type === 'multiSelect' && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="retrieveFieldValue(localDocument, field.id)"
|
|
||||||
:isNew="false"
|
|
||||||
:editMode="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field_SingleRelationship
|
|
||||||
class="inputWrapper"
|
|
||||||
v-if="(field.type === 'singleToNoneRelationship' || field.type === 'singleToSingleRelationship' || field.type === 'singleToManyRelationship') && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="retrieveFieldValue(localDocument, field.id)"
|
|
||||||
:isNew="false"
|
|
||||||
:editMode="false"
|
|
||||||
:current-id="localDocument._id"
|
|
||||||
:recursive="true"
|
|
||||||
:special-z-index="(specialZIndex)"
|
|
||||||
@menu-mode="reactToMenuMode"
|
|
||||||
@menu-enter="reactToMenuEnter"
|
|
||||||
@menu-leave="reactToMenuLeave"
|
|
||||||
@set-new-parent-id="setOtherContent"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field_MultiRelationship
|
|
||||||
class="inputWrapper"
|
|
||||||
v-if="(field.type === 'manyToNoneRelationship' || field.type ===
|
|
||||||
'manyToSingleRelationship' || field.type === 'manyToManyRelationship') && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="retrieveFieldValue(localDocument, field.id)"
|
|
||||||
:isNew="false"
|
|
||||||
:editMode="false"
|
|
||||||
:current-id="localDocument._id"
|
|
||||||
:recursive="true"
|
|
||||||
:special-z-index="specialZIndex"
|
|
||||||
@menu-mode="reactToMenuMode"
|
|
||||||
@menu-enter="reactToMenuEnter"
|
|
||||||
@menu-leave="reactToMenuLeave"
|
|
||||||
@set-new-parent-id="setOtherContent"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field_Wysiwyg
|
|
||||||
class="inputWrapper"
|
|
||||||
v-if="field.type === 'wysiwyg' && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="(retrieveFieldValue(localDocument, field.id)) ? retrieveFieldValue(localDocument, field.id) : ''"
|
|
||||||
:isNew="false"
|
|
||||||
:editMode="false"
|
|
||||||
:current-id="localDocument._id"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field_Tags
|
|
||||||
class="inputWrapper"
|
|
||||||
v-if="field.type === 'tags' && categoryFieldFilter(field.id)"
|
|
||||||
:inputDataBluePrint="field"
|
|
||||||
:inputDataValue="retrieveFieldValue(localDocument, field.id)"
|
|
||||||
:isNew="false"
|
|
||||||
:editMode="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</q-tooltip>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component, Prop, Watch } from "vue-property-decorator"
|
|
||||||
|
|
||||||
import BaseClass from "src/BaseClass"
|
|
||||||
import { I_ShortenedDocument } from "src/interfaces/I_OpenedDocument"
|
|
||||||
import { I_Blueprint } from "src/interfaces/I_Blueprint"
|
|
||||||
|
|
||||||
import Field_Break from "src/components/fields/Field_Break.vue"
|
|
||||||
import Field_Text from "src/components/fields/Field_Text.vue"
|
|
||||||
import Field_Number from "src/components/fields/Field_Number.vue"
|
|
||||||
import Field_Switch from "src/components/fields/Field_Switch.vue"
|
|
||||||
import Field_ColorPicker from "src/components/fields/Field_ColorPicker.vue"
|
|
||||||
import Field_List from "src/components/fields/Field_List.vue"
|
|
||||||
import Field_SingleSelect from "src/components/fields/Field_SingleSelect.vue"
|
|
||||||
import Field_MultiSelect from "src/components/fields/Field_MultiSelect.vue"
|
|
||||||
import Field_Wysiwyg from "src/components/fields/Field_Wysiwyg.vue"
|
|
||||||
import Field_Tags from "src/components/fields/Field_Tags.vue"
|
|
||||||
import { extend } from "quasar"
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
Field_Break,
|
|
||||||
Field_Text,
|
|
||||||
Field_Number,
|
|
||||||
Field_Switch,
|
|
||||||
Field_ColorPicker,
|
|
||||||
Field_List,
|
|
||||||
Field_SingleSelect,
|
|
||||||
Field_MultiSelect,
|
|
||||||
Field_SingleRelationship: () => import("src/components/fields/Field_SingleRelationship.vue"),
|
|
||||||
Field_MultiRelationship: () => import("src/components/fields/Field_MultiRelationship.vue"),
|
|
||||||
Field_Wysiwyg,
|
|
||||||
Field_Tags
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export default class DocumentPreview extends BaseClass {
|
|
||||||
/****************************************************************/
|
|
||||||
// LOCAL CONTENT
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Variable string for closing of the popup due to external influences
|
|
||||||
*/
|
|
||||||
@Prop({ default: "" }) readonly externalCloseTrigger!: string
|
|
||||||
|
|
||||||
@Watch("externalCloseTrigger")
|
|
||||||
reactToExternalClose () {
|
|
||||||
this.setCloseTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieved document ID
|
|
||||||
*/
|
|
||||||
@Prop() readonly documentId!: string
|
|
||||||
|
|
||||||
setOtherContent (id:string) {
|
|
||||||
this.hasOtherContent = true
|
|
||||||
this.setNewDocumentID(id).catch(e => console.log(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
async setNewDocumentID (id: string) {
|
|
||||||
this.localDocument = extend(true, {}, this.SGET_document(id))
|
|
||||||
if (!this.localDocument) {
|
|
||||||
// @ts-ignore
|
|
||||||
this.localDocument = extend(true, {}, this.SGET_openedDocument(id))
|
|
||||||
}
|
|
||||||
if (this.localDocument) {
|
|
||||||
this.localBlueprint = this.SGET_blueprint(this.localDocument.type)
|
|
||||||
|
|
||||||
await this.$nextTick()
|
|
||||||
|
|
||||||
document.querySelectorAll(".documentPreviewWrapper").forEach(e => {
|
|
||||||
e.scrollTop = 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("documentId", { immediate: true })
|
|
||||||
reactToDocumentIDChange (val: string) {
|
|
||||||
if (this.documentId) {
|
|
||||||
this.setNewDocumentID(val).catch(e => console.log(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localDocument = false as unknown as I_ShortenedDocument
|
|
||||||
localBlueprint = false as unknown as I_Blueprint
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if field should be showing if the category setting is turned on
|
|
||||||
*/
|
|
||||||
categoryFieldFilter (currentFieldID: string) {
|
|
||||||
const isCategory = this.retrieveFieldValue(this.localDocument, "categorySwitch")
|
|
||||||
|
|
||||||
const ignoredList = ["breakDocumentSettings", "name", "documentColor", "documentBackgroundColor", "parentDoc", "order", "categorySwitch", "minorSwitch", "deadSwitch", "finishedSwitch", "tags", "otherNames"]
|
|
||||||
return (
|
|
||||||
(
|
|
||||||
(!isCategory && currentFieldID !== "categoryDescription") ||
|
|
||||||
ignoredList.includes(currentFieldID)
|
|
||||||
) || (isCategory && currentFieldID === "categoryDescription")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the field in question
|
|
||||||
*/
|
|
||||||
hasValueFieldFilter (field: any) {
|
|
||||||
if (this.retrieveFieldType(this.localDocument, field.id) === "break") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = this.retrieveFieldValue(this.localDocument, field.id)
|
|
||||||
|
|
||||||
if (!value ||
|
|
||||||
(Array.isArray(value) && value.length === 0) ||
|
|
||||||
// @ts-ignore
|
|
||||||
(value?.value && value.value.length === 0) ||
|
|
||||||
// @ts-ignore
|
|
||||||
(value.value === null)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// GLOBAL OPTIONS
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* React to changes on the options store
|
|
||||||
*/
|
|
||||||
@Watch("SGET_options", { immediate: true, deep: true })
|
|
||||||
onSettingsChange () {
|
|
||||||
this.isDarkMode = this.SGET_options.darkMode
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if this is in dark-mode or not
|
|
||||||
*/
|
|
||||||
isDarkMode = false
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// VISIBILITY MANAGEMENT
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Variable string for closing of the popup due to external influences
|
|
||||||
*/
|
|
||||||
@Prop({ default: 999 }) readonly specialZIndex!: number
|
|
||||||
@Prop({ default: 750 }) readonly customDelay!: number
|
|
||||||
@Prop({ default: true }) readonly customTarget!: string | boolean
|
|
||||||
@Prop({ default: "bottom middle" }) readonly customAnchor!: string
|
|
||||||
@Prop({ default: "top middle" }) readonly customSelf!: string
|
|
||||||
@Prop({ default: 500 }) readonly customCloseDelay!: number
|
|
||||||
|
|
||||||
consitentDocumentPreviewSwitch () {
|
|
||||||
if (this.documentPreviewLock) {
|
|
||||||
this.documentPreviewSwitch = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
documentPreviewClose () {
|
|
||||||
this.documentPreviewLock = false
|
|
||||||
this.documentPreviewSwitch = false
|
|
||||||
this.hasOtherContent = false
|
|
||||||
}
|
|
||||||
|
|
||||||
documentPreviewLock = false
|
|
||||||
documentPreviewSwitch = false
|
|
||||||
|
|
||||||
hasOtherContent = false
|
|
||||||
|
|
||||||
openDocumentPreview () {
|
|
||||||
if (!this.hasOtherContent) {
|
|
||||||
this.documentPreviewLock = true
|
|
||||||
if (this.documentId) {
|
|
||||||
this.setNewDocumentID(this.documentId).catch(e => console.log(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCloseTimer () {
|
|
||||||
this.disableScroll()
|
|
||||||
clearTimeout(this.closeTimer)
|
|
||||||
}
|
|
||||||
|
|
||||||
setCloseTimer () {
|
|
||||||
this.enableScroll()
|
|
||||||
this.closeTimer = setTimeout(() => {
|
|
||||||
this.documentPreviewClose()
|
|
||||||
}, this.customCloseDelay)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debounce timer for nice user experience
|
|
||||||
*/
|
|
||||||
closeTimer = null as any
|
|
||||||
|
|
||||||
menuMode = false
|
|
||||||
|
|
||||||
reactToMenuMode (menuMode: boolean) {
|
|
||||||
this.menuMode = menuMode
|
|
||||||
}
|
|
||||||
|
|
||||||
reactToMenuEnter () {
|
|
||||||
this.clearCloseTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
reactToMenuLeave () {
|
|
||||||
this.setCloseTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
wheelOpt = { passive: false }
|
|
||||||
wheelEvent = "onwheel" in document.createElement("div") ? "wheel" : "mousewheel"
|
|
||||||
|
|
||||||
preventDefault (e: WheelEvent) {
|
|
||||||
// @ts-ignore
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
const previewWrapper = e.target.closest(".documentPreviewWrapper")
|
|
||||||
|
|
||||||
if (previewWrapper) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
const previewContent = previewWrapper.querySelector(".documentPreviewContent")
|
|
||||||
|
|
||||||
const wheelDirection = (e.deltaY > 0) ? "down" : "up"
|
|
||||||
|
|
||||||
if (wheelDirection === "up" && previewWrapper.scrollTop === 0) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
const combinedHeight = previewContent.getBoundingClientRect().height - previewWrapper.getBoundingClientRect().height
|
|
||||||
|
|
||||||
if (wheelDirection === "down" && previewWrapper.scrollTop >= combinedHeight) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disableScroll () {
|
|
||||||
// @ts-ignore
|
|
||||||
window.addEventListener("DOMMouseScroll", this.preventDefault, false)
|
|
||||||
// @ts-ignore
|
|
||||||
window.addEventListener(this.wheelEvent, this.preventDefault, this.wheelOpt)
|
|
||||||
// @ts-ignore
|
|
||||||
window.addEventListener("touchmove", this.preventDefault, this.wheelOpt)
|
|
||||||
}
|
|
||||||
|
|
||||||
enableScroll () {
|
|
||||||
// @ts-ignore
|
|
||||||
window.removeEventListener("DOMMouseScroll", this.preventDefault, false)
|
|
||||||
// @ts-ignore
|
|
||||||
window.removeEventListener(this.wheelEvent, this.preventDefault, this.wheelOpt)
|
|
||||||
// @ts-ignore
|
|
||||||
window.removeEventListener("touchmove", this.preventDefault, this.wheelOpt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.documentPreviewWrapper.no-pointer-events {
|
|
||||||
pointer-events: all !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
box-shadow: 0 3px 0 rgba(0, 0, 0, 0.25);
|
|
||||||
height: 600px;
|
|
||||||
background-color: map-get($customColors, 'gunmetal-lighter') !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.documentPreviewContent {
|
|
||||||
padding: 20px;
|
|
||||||
width: 700px;
|
|
||||||
max-width: 100%;
|
|
||||||
min-height: 600px;
|
|
||||||
background-color: map-get($customColors, 'gunmetal-lighter') !important;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
.inputWrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
font-size: 19px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-primary {
|
|
||||||
color: #ffd673 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colorIndicator {
|
|
||||||
border: 1px solid #c5c5c5 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connectionNote,
|
|
||||||
.listNote {
|
|
||||||
color: #fff !important;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldWysiwyg {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,204 @@
|
||||||
|
import { _electron as electron } from 'playwright'
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { extraEnvVariablesAPI } from 'app/src-electron/customContentBridgeAPIs/extraEnvVariablesAPI'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra env settings to trigger component testing via Playwright
|
||||||
|
*/
|
||||||
|
const extraEnvSettings = {
|
||||||
|
TEST_ENV: 'components',
|
||||||
|
COMPONENT_NAME: 'FantasiaMascotImage',
|
||||||
|
COMPONENT_PROPS: JSON.stringify({})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron main filepath
|
||||||
|
*/
|
||||||
|
const electronMainFilePath:string = extraEnvVariablesAPI.ELECTRON_MAIN_FILEPATH
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra rended timer buffer for tests to start after loading the app
|
||||||
|
* - Change here in order manually adjust this component's wait times
|
||||||
|
*/
|
||||||
|
const faFrontendRenderTimer:number = extraEnvVariablesAPI.FA_FRONTEND_RENDER_TIMER
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object of string data selectors for the component
|
||||||
|
*/
|
||||||
|
const selectorList = {
|
||||||
|
image: 'fantasiaMascotImage-image'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the wrapper contains 'IMG' element
|
||||||
|
*/
|
||||||
|
test('Check if the wrapper contains "IMG" element', async () => {
|
||||||
|
const testString = 'IMG'
|
||||||
|
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const imageElement = await appWindow.$(`[data-test="${selectorList.image}"]`)
|
||||||
|
|
||||||
|
// Check if the tested element exists
|
||||||
|
if (imageElement !== null) {
|
||||||
|
const elementType = await imageElement.evaluate(el => el.tagName)
|
||||||
|
|
||||||
|
await expect(elementType).toBe(testString)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to pass "width" prop to the component and check the result
|
||||||
|
*/
|
||||||
|
test('Visually check "width" prop', async () => {
|
||||||
|
const testString = '300px'
|
||||||
|
|
||||||
|
extraEnvSettings.COMPONENT_PROPS = JSON.stringify({ width: testString })
|
||||||
|
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const imageElement = await appWindow.$(`[data-test="${selectorList.image}"]`)
|
||||||
|
|
||||||
|
// Check if the tested element exists
|
||||||
|
if (imageElement !== null) {
|
||||||
|
const imageBoxData = await imageElement.boundingBox()
|
||||||
|
|
||||||
|
// Test if the tested element isn't invisisble for some reason
|
||||||
|
if (imageBoxData !== null) {
|
||||||
|
const roundedFirstValue = Math.round(imageBoxData.width)
|
||||||
|
const roundedSecondValue = Math.round(parseInt(testString))
|
||||||
|
|
||||||
|
await expect(roundedFirstValue).toBe(roundedSecondValue)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element is invisible
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to pass "height" prop to the component and check the result
|
||||||
|
*/
|
||||||
|
test('Visually check "height" prop', async () => {
|
||||||
|
const testString = '300px'
|
||||||
|
|
||||||
|
extraEnvSettings.COMPONENT_PROPS = JSON.stringify({ height: testString })
|
||||||
|
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const imageElement = await appWindow.$(`[data-test="${selectorList.image}"]`)
|
||||||
|
|
||||||
|
// Check if the tested element exists
|
||||||
|
if (imageElement !== null) {
|
||||||
|
const imageBoxData = await imageElement.boundingBox()
|
||||||
|
|
||||||
|
// Test if the tested element isn't invisisble for some reason
|
||||||
|
if (imageBoxData !== null) {
|
||||||
|
const roundedFirstValue = Math.round(imageBoxData.height)
|
||||||
|
const roundedSecondValue = Math.round(parseInt(testString))
|
||||||
|
|
||||||
|
await expect(roundedFirstValue).toBe(roundedSecondValue)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element is invisible
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if the component properly determines if the image will be random - YES
|
||||||
|
*/
|
||||||
|
test('Check if the image is random: YES', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const imageElement = await appWindow.$(`[data-test="${selectorList.image}"]`)
|
||||||
|
|
||||||
|
// Check if the tested element exists
|
||||||
|
if (imageElement !== null) {
|
||||||
|
const isRandom = await imageElement.evaluate(el => el.dataset.testIsRandom)
|
||||||
|
|
||||||
|
await expect(isRandom).toBe('true')
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if the component properly determines if the image will be random - NO
|
||||||
|
*/
|
||||||
|
test('Check if the image is random: NO', async () => {
|
||||||
|
const testString = 'flop'
|
||||||
|
|
||||||
|
extraEnvSettings.COMPONENT_PROPS = JSON.stringify({ fantasiaImage: testString })
|
||||||
|
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const imageElement = await appWindow.$(`[data-test="${selectorList.image}"]`)
|
||||||
|
|
||||||
|
// Check if the tested element exists
|
||||||
|
if (imageElement !== null) {
|
||||||
|
const isRandom = await imageElement.evaluate(el => el.dataset.testIsRandom)
|
||||||
|
const imageString = await imageElement.evaluate(el => el.dataset.testImage)
|
||||||
|
|
||||||
|
await expect.soft(imageString).toBe(testString)
|
||||||
|
await expect(isRandom).toBe('false')
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
})
|
79
src/components/FantasiaMascotImage/FantasiaMascotImage.vue
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<!-- Outer wrapper of the image -->
|
||||||
|
<div
|
||||||
|
class="fantasiaMascotImage"
|
||||||
|
>
|
||||||
|
<!-- Inner image of the chosen Fantasia Mascot -->
|
||||||
|
<img
|
||||||
|
:src="currentMascotImage"
|
||||||
|
class="fantasiaMascotImage__inner"
|
||||||
|
data-test="fantasiaMascotImage-image"
|
||||||
|
:data-test-image="fantasiaImage"
|
||||||
|
:data-test-is-random="isRandom"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { fantasiaImageList, determineCurrentImage } from 'app/src/scripts/appInfo/fantasiaMascotImageManager'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All component props
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
/**
|
||||||
|
* Name of the object key from the list to render. Leave empty in order to generate random image.
|
||||||
|
* Available ones:
|
||||||
|
* - didYouKnow
|
||||||
|
* - flop
|
||||||
|
* - hug
|
||||||
|
* - reading
|
||||||
|
* - cooking
|
||||||
|
* - error
|
||||||
|
*/
|
||||||
|
fantasiaImage: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom CSS string for the "height" attribute.
|
||||||
|
*/
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: 'initial'
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom CSS string for the "width" attribute.
|
||||||
|
*/
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: 'initial'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the image URL will be generating randomly or if it is set via the prop.
|
||||||
|
*/
|
||||||
|
const isRandom = ((imageUrl: string) => (imageUrl === ''))(props.fantasiaImage)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently selected image URL for rendering
|
||||||
|
*/
|
||||||
|
const currentMascotImage = determineCurrentImage(fantasiaImageList, isRandom, props.fantasiaImage)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.fantasiaMascotImage {
|
||||||
|
&__inner {
|
||||||
|
height: v-bind(height);
|
||||||
|
width: v-bind(width);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,200 @@
|
||||||
|
import { _electron as electron } from 'playwright'
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { extraEnvVariablesAPI } from 'app/src-electron/customContentBridgeAPIs/extraEnvVariablesAPI'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra env settings to trigger component testing via Playwright
|
||||||
|
*/
|
||||||
|
const extraEnvSettings = {
|
||||||
|
TEST_ENV: 'components',
|
||||||
|
COMPONENT_NAME: 'GlobalWindowButtons',
|
||||||
|
COMPONENT_PROPS: JSON.stringify({})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron main filepath
|
||||||
|
*/
|
||||||
|
const electronMainFilePath:string = extraEnvVariablesAPI.ELECTRON_MAIN_FILEPATH
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra rended timer buffer for tests to start after loading the app
|
||||||
|
* - Change here in order manually adjust this component's wait times
|
||||||
|
*/
|
||||||
|
const faFrontendRenderTimer:number = extraEnvVariablesAPI.FA_FRONTEND_RENDER_TIMER
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object of string data selectors for the component
|
||||||
|
*/
|
||||||
|
const selectorList = {
|
||||||
|
buttonMinimize: 'globalWindowButtons-button-minimize',
|
||||||
|
buttonResize: 'globalWindowButtons-button-resize',
|
||||||
|
buttonClose: 'globalWindowButtons-button-close'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if the component has three specific HTML element buttons properly mounted in it:
|
||||||
|
* - Minimize button
|
||||||
|
* - Resize button
|
||||||
|
* - Close button
|
||||||
|
*/
|
||||||
|
test('Wrapper should contain three specific buttons', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const resizeButton = await appWindow.$(`[data-test="${selectorList.buttonResize}"]`)
|
||||||
|
const minimizeButton = await appWindow.$(`[data-test="${selectorList.buttonMinimize}"]`)
|
||||||
|
const closeButton = await appWindow.$(`[data-test="${selectorList.buttonClose}"]`)
|
||||||
|
|
||||||
|
// Check if the tested elements exists
|
||||||
|
if (resizeButton !== null && minimizeButton !== null && closeButton !== null) {
|
||||||
|
await expect(true).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// At least one of the tested elements doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to click the resize button
|
||||||
|
*/
|
||||||
|
test('Click resize button - "smallify"', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const resizeButton = await appWindow.$(`[data-test="${selectorList.buttonResize}"]`)
|
||||||
|
|
||||||
|
// Check if the tested element exists
|
||||||
|
if (resizeButton !== null) {
|
||||||
|
await resizeButton.click()
|
||||||
|
|
||||||
|
const isMaximized = await appWindow.evaluate(() => window.faWindowControlAPI.checkWindowMaximized())
|
||||||
|
|
||||||
|
await expect(isMaximized).toBe(false)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to click the resize button, twice
|
||||||
|
*/
|
||||||
|
test('Click resize button - "maximize"', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const resizeButton = await appWindow.$(`[data-test="${selectorList.buttonResize}"]`)
|
||||||
|
|
||||||
|
// Check if the tested element exists
|
||||||
|
if (resizeButton !== null) {
|
||||||
|
let isMaximized = await appWindow.evaluate(() => window.faWindowControlAPI.checkWindowMaximized())
|
||||||
|
|
||||||
|
// Check if the window if maximized of not, react accordingly
|
||||||
|
if (isMaximized) {
|
||||||
|
// Click twice
|
||||||
|
await resizeButton.click()
|
||||||
|
|
||||||
|
await appWindow.waitForTimeout(1500)
|
||||||
|
|
||||||
|
await resizeButton.click()
|
||||||
|
} else {
|
||||||
|
await resizeButton.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
await appWindow.waitForTimeout(1500)
|
||||||
|
isMaximized = await appWindow.evaluate(() => window.faWindowControlAPI.checkWindowMaximized())
|
||||||
|
|
||||||
|
await expect(isMaximized).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to click the minimize button
|
||||||
|
*/
|
||||||
|
test('Click minimize button', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const minimizeButton = await appWindow.$(`[data-test="${selectorList.buttonMinimize}"]`)
|
||||||
|
|
||||||
|
// Check if the tested element exists
|
||||||
|
if (minimizeButton !== null) {
|
||||||
|
await minimizeButton.click()
|
||||||
|
|
||||||
|
const isMaximized = await appWindow.evaluate(() => window.faWindowControlAPI.checkWindowMaximized())
|
||||||
|
|
||||||
|
await expect(isMaximized).toBe(false)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to click the close button
|
||||||
|
* - This test can VERY occasionally fail when the window takes too long to close on weaker PCs. Simply rerunning the tests generally fixes this.
|
||||||
|
*/
|
||||||
|
test('Click close button', async () => {
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
env: extraEnvSettings,
|
||||||
|
args: [electronMainFilePath]
|
||||||
|
})
|
||||||
|
|
||||||
|
const appWindow = await electronApp.firstWindow()
|
||||||
|
await appWindow.waitForTimeout(faFrontendRenderTimer)
|
||||||
|
|
||||||
|
const closeButton = await appWindow.$(`[data-test="${selectorList.buttonClose}"]`)
|
||||||
|
|
||||||
|
// Check if the tested element exists
|
||||||
|
if (closeButton !== null) {
|
||||||
|
let windowIsClosed = false
|
||||||
|
|
||||||
|
// Listen to window close event
|
||||||
|
appWindow.on('close', () => {
|
||||||
|
windowIsClosed = true
|
||||||
|
})
|
||||||
|
|
||||||
|
await closeButton.click()
|
||||||
|
|
||||||
|
await expect(windowIsClosed).toBe(true)
|
||||||
|
await electronApp.close()
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist
|
||||||
|
test.fail()
|
||||||
|
}
|
||||||
|
})
|
183
src/components/GlobalWindowButtons/GlobalWindowButtons.vue
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
<template>
|
||||||
|
<!-- App base-control button-group -->
|
||||||
|
<q-btn-group
|
||||||
|
flat
|
||||||
|
class="globalWindowButtons bg-dark"
|
||||||
|
>
|
||||||
|
<!-- Minimize button -->
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dark
|
||||||
|
size="xs"
|
||||||
|
class="globalWindowButtons__button globalWindowButtons__minimize"
|
||||||
|
data-test="globalWindowButtons-button-minimize"
|
||||||
|
@click="minimizeWindow()"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
:delay="1000"
|
||||||
|
:offset="[0, 5]"
|
||||||
|
>
|
||||||
|
{{ $t('GlobalWindowButtons.minimizeButton') }}
|
||||||
|
</q-tooltip>
|
||||||
|
<q-icon
|
||||||
|
size="16px"
|
||||||
|
name="mdi-window-minimize"
|
||||||
|
/>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<!-- MinMax button -->
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dark
|
||||||
|
size="xs"
|
||||||
|
class="globalWindowButtons__button globalWindowButtons__resize"
|
||||||
|
data-test="globalWindowButtons-button-resize"
|
||||||
|
@click="[resizeWindow(),checkIfWindowMaximized()]"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
:delay="1000"
|
||||||
|
:offset="[0, 5]"
|
||||||
|
>
|
||||||
|
{{ isMaximized ? $t('GlobalWindowButtons.resizeButton') : $t('GlobalWindowButtons.maximizeButton') }}
|
||||||
|
</q-tooltip>
|
||||||
|
<q-icon
|
||||||
|
size="16px"
|
||||||
|
:name="(isMaximized)
|
||||||
|
? 'mdi-window-restore'
|
||||||
|
: 'mdi-window-maximize'"
|
||||||
|
/>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<!-- Close button -->
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dark
|
||||||
|
size="xs"
|
||||||
|
class="globalWindowButtons__button globalWindowButtons__close"
|
||||||
|
data-test="globalWindowButtons-button-close"
|
||||||
|
@click="tryCloseWindow()"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
:delay="1000"
|
||||||
|
:offset="[0, 5]"
|
||||||
|
>
|
||||||
|
{{ $t('GlobalWindowButtons.close') }}
|
||||||
|
</q-tooltip>
|
||||||
|
<q-icon
|
||||||
|
size="16px"
|
||||||
|
name="mdi-window-close"
|
||||||
|
/>
|
||||||
|
</q-btn>
|
||||||
|
</q-btn-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers minimize of the window by the minimize button click
|
||||||
|
*/
|
||||||
|
const minimizeWindow = () => {
|
||||||
|
if (process.env.MODE === 'electron') {
|
||||||
|
window.faWindowControlAPI.minimizeWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers resize of the window by the min/max button click
|
||||||
|
*/
|
||||||
|
const resizeWindow = () => {
|
||||||
|
if (process.env.MODE === 'electron') {
|
||||||
|
window.faWindowControlAPI.resizeWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers checking of the current app state by the close button click.
|
||||||
|
* This functionality checks the following:
|
||||||
|
|
||||||
|
* 1. If the app has any projects opened to begin with at the moment
|
||||||
|
* 2. If the project has any pending chnages to it
|
||||||
|
|
||||||
|
* If both is found to be true, then an appropriate dialog is opened.
|
||||||
|
* Otherwise, the app simply closes.
|
||||||
|
*/
|
||||||
|
const tryCloseWindow = () => {
|
||||||
|
// TODO add project close checking
|
||||||
|
if (process.env.MODE === 'electron') {
|
||||||
|
window.faWindowControlAPI.closeWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Checks if the window is maximized and sets local variable accordingly
|
||||||
|
*/
|
||||||
|
const checkIfWindowMaximized = () => {
|
||||||
|
if (process.env.MODE === 'electron') {
|
||||||
|
isMaximized.value = window.faWindowControlAPI.checkWindowMaximized()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the window is currently maximized or not
|
||||||
|
*/
|
||||||
|
const isMaximized: Ref<boolean> = ref(true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Window interval checker variable
|
||||||
|
*/
|
||||||
|
let checkerInterval: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook up a interval timer on mount for continuous checking
|
||||||
|
* This is done due to the fact that dragging via the top header bar doesn't properly fire "drag" event
|
||||||
|
* Async due to UI render blocking
|
||||||
|
*/
|
||||||
|
onMounted(async () => {
|
||||||
|
window.clearInterval(checkerInterval)
|
||||||
|
|
||||||
|
checkIfWindowMaximized()
|
||||||
|
|
||||||
|
checkerInterval = window.setInterval(() => {
|
||||||
|
checkIfWindowMaximized()
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Unhook the interval timer on unmounting in order to prevent left-over intervals ticking in the app
|
||||||
|
* Async due to UI render blocking
|
||||||
|
*/
|
||||||
|
onUnmounted(async () => {
|
||||||
|
window.clearInterval(checkerInterval)
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.globalWindowButtons {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 99999999;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
height: $globalWindowButtons_height;
|
||||||
|
color: $globalWindowButtons_color;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: $globalWindowButtons_hoverColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close {
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: $globalWindowButtons_close_hoverColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,811 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<div
|
|
||||||
:class="{'AppControl': !isFrontpage}"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- New document dialog -->
|
|
||||||
<newDocumentDialog
|
|
||||||
:dialog-trigger="newObjectDialogTrigger"
|
|
||||||
@trigger-dialog-close="newObjectDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Existing document dialog -->
|
|
||||||
<existingDocumentDialog
|
|
||||||
:dialog-trigger="existingObjectDialogTrigger"
|
|
||||||
@trigger-dialog-close="existingObjectDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Project close dialog -->
|
|
||||||
<projectCloseCheckDialog
|
|
||||||
:dialog-trigger="projectCloseCheckDialogTrigger"
|
|
||||||
:dialog-mode="'projectClose'"
|
|
||||||
@trigger-dialog-close="projectCloseCheckDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Keybind dialog -->
|
|
||||||
<keybindCheatsheetDialog
|
|
||||||
:dialog-trigger="keybindsDialogTrigger"
|
|
||||||
@trigger-dialog-close="keybindsDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Import project dialog -->
|
|
||||||
<importProjectCheckDialog
|
|
||||||
:dialog-trigger="importProjectDialogTrigger"
|
|
||||||
@trigger-dialog-close="importProjectDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Merge project dialog -->
|
|
||||||
<mergeProjectCheckDialog
|
|
||||||
:dialog-trigger="mergeProjectDialogTrigger"
|
|
||||||
@trigger-dialog-close="mergeProjectDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- New project dialog -->
|
|
||||||
<newProjectCheckDialog
|
|
||||||
:dialog-trigger="newProjectDialogTrigger"
|
|
||||||
@trigger-dialog-close="newProjectDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- About app dialog -->
|
|
||||||
<aboutAppDialog
|
|
||||||
:dialog-trigger="aboutAppDialogTrigger"
|
|
||||||
@trigger-dialog-close="aboutAppDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Changelog dialog -->
|
|
||||||
<changeLogDialog
|
|
||||||
:dialog-trigger="changeLogDialogTrigger"
|
|
||||||
@trigger-dialog-close="changeLogDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Program settings dialog -->
|
|
||||||
<programSettingsDialog
|
|
||||||
:dialog-trigger="programSettingsDialogTrigger"
|
|
||||||
@trigger-dialog-close="programSettingsDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Advanced search guide dialog -->
|
|
||||||
<advancedSearchGuideDialog
|
|
||||||
:dialog-trigger="advancedSearchGuideDialogTrigger"
|
|
||||||
@trigger-dialog-close="advancedSearchGuideDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Tips, Tricks & Trivia dialog -->
|
|
||||||
<tipsTricksTriviaDialog
|
|
||||||
:dialog-trigger="tipsTricksDialogTrigger"
|
|
||||||
@trigger-dialog-close="tipsTricksDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- License dialog -->
|
|
||||||
<licenseDialog
|
|
||||||
:dialog-trigger="licenseDialogTrigger"
|
|
||||||
@trigger-dialog-close="licenseDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Repair project dialog -->
|
|
||||||
<repairProjectDialog
|
|
||||||
:dialog-trigger="repairProjectDialogTrigger"
|
|
||||||
@trigger-dialog-close="repairProjectDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-btn-group
|
|
||||||
flat
|
|
||||||
class="AppControl__buttons"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- Options button -->
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
:ripple="false"
|
|
||||||
dark
|
|
||||||
size='md'
|
|
||||||
no-caps
|
|
||||||
@click="programSettingsDialogAssignUID"
|
|
||||||
>
|
|
||||||
<q-img
|
|
||||||
:src="appLogo"
|
|
||||||
style="height: 26px; width: 26px; margin: 0 -9px;"
|
|
||||||
/>
|
|
||||||
<q-tooltip anchor="center right" self="center left" :delay="500">
|
|
||||||
Program settings
|
|
||||||
</q-tooltip>
|
|
||||||
|
|
||||||
</q-btn>
|
|
||||||
<q-separator color="primary" vertical dark style="opacity: 0.1;" />
|
|
||||||
<!-- Project button-->
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
:ripple="false"
|
|
||||||
dark
|
|
||||||
size='md'
|
|
||||||
no-caps
|
|
||||||
>
|
|
||||||
Project
|
|
||||||
|
|
||||||
<q-menu
|
|
||||||
@show="checkProjectStatus"
|
|
||||||
anchor="bottom left"
|
|
||||||
class="bg-gunmetal-light"
|
|
||||||
dark
|
|
||||||
square
|
|
||||||
>
|
|
||||||
<q-list class="bg-gunmetal-light" dark>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
@click="newObjectAssignUID"
|
|
||||||
:disable="!projectExists || isFrontpage"
|
|
||||||
>
|
|
||||||
<q-item-section>Quick-add new document</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-text-box-plus-outline" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
@click="existingObjectAssignUID"
|
|
||||||
:disable="!projectExists || isFrontpage"
|
|
||||||
>
|
|
||||||
<q-item-section>Quick-search existing document</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-database-search" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-separator dark />
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
@click="toggleHierarchicalTree"
|
|
||||||
:disable="!projectExists || isFrontpage"
|
|
||||||
>
|
|
||||||
<q-item-section>Toggle hierarchical tree</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-page-layout-sidebar-left" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
@click="SSET_setNoteCorkboardWindowVisible"
|
|
||||||
:disable="!projectExists"
|
|
||||||
>
|
|
||||||
<q-item-section>Show note board</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-clipboard-text-outline" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-separator dark />
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
@click="newProjectAssignUID"
|
|
||||||
>
|
|
||||||
<q-item-section>New project</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-plus" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-separator dark />
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
@click="commenceExport"
|
|
||||||
:disable="!projectExists"
|
|
||||||
>
|
|
||||||
<q-item-section>Export current project</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-package-variant-closed" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
@click="importProjectAssignUID"
|
|
||||||
>
|
|
||||||
<q-item-section>Import existing project</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-package-variant" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
@click="mergeProjectAssignUID"
|
|
||||||
:disable="!projectExists"
|
|
||||||
>
|
|
||||||
<q-item-section>Merge another project into the current one</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-folder-plus-outline" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-separator dark />
|
|
||||||
|
|
||||||
<q-item clickable>
|
|
||||||
<q-item-section>Advanced project tools</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="keyboard_arrow_right" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-menu anchor="top end" self="top start">
|
|
||||||
<q-list class="bg-gunmetal text-accent">
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
@click="repairProjectAssignUID"
|
|
||||||
:disable="!projectExists || isFrontpage"
|
|
||||||
>
|
|
||||||
<q-item-section>Repair legacy project</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-wrench" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
</q-list>
|
|
||||||
</q-menu>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-separator dark />
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
@click="navigateToProjectPage"
|
|
||||||
:disable="!projectExists || isProjectPage"
|
|
||||||
>
|
|
||||||
<q-item-section>Show project overview</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-chart-bar" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
@click="projectCloseCheckDialogAssignUID"
|
|
||||||
:disable="!projectExists || isFrontpage"
|
|
||||||
>
|
|
||||||
<q-item-section>Close project</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-exit-to-app" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-menu>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<!-- Help button-->
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
:ripple="false"
|
|
||||||
dark
|
|
||||||
size='md'
|
|
||||||
no-caps
|
|
||||||
>
|
|
||||||
Help, Settings & Info
|
|
||||||
<q-menu
|
|
||||||
anchor="bottom left"
|
|
||||||
class="bg-gunmetal-light"
|
|
||||||
dark
|
|
||||||
square
|
|
||||||
>
|
|
||||||
<q-list class="bg-gunmetal-light" dark>
|
|
||||||
<q-item
|
|
||||||
@click="programSettingsDialogAssignUID"
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
>
|
|
||||||
<q-item-section>Program settings</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-tune" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-separator dark />
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
@click="keybindsDialogAssignUID"
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
>
|
|
||||||
<q-item-section>Show keybind cheatsheet</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-keyboard-settings" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
@click="advancedSearchGuideAssignUID"
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
>
|
|
||||||
<q-item-section>Advanced search guide</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-file-question" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
@click="tipsTricksAssignUID"
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
>
|
|
||||||
<q-item-section>Tips, Tricks & Trivia</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-fire-alert" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-separator dark />
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
@click="changeLogDialogAssignUID"
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
>
|
|
||||||
<q-item-section>Changelog</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-clipboard-text" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
@click="aboutAppDialogAssignUID"
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
>
|
|
||||||
<q-item-section>About Fantasia Archive</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-information-variant" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
@click="licenseAssignUID"
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
>
|
|
||||||
<q-item-section>License</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-script-text-outline" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-separator dark />
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
@click="toggleDevTools"
|
|
||||||
v-close-popup
|
|
||||||
clickable
|
|
||||||
active
|
|
||||||
active-class="bg-gunmetal-light text-cultured"
|
|
||||||
class="noHigh"
|
|
||||||
>
|
|
||||||
<q-item-section>Toggle developer tools</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-code-tags" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
</q-list>
|
|
||||||
</q-menu>
|
|
||||||
|
|
||||||
</q-btn>
|
|
||||||
</q-btn-group>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component, Watch } from "vue-property-decorator"
|
|
||||||
|
|
||||||
import BaseClass from "src/BaseClass"
|
|
||||||
import projectCloseCheckDialog from "src/components/dialogs/ProjectCloseCheck.vue"
|
|
||||||
import keybindCheatsheetDialog from "src/components/dialogs/KeybindCheatsheet.vue"
|
|
||||||
import importProjectCheckDialog from "src/components/dialogs/ImportProjectCheck.vue"
|
|
||||||
import mergeProjectCheckDialog from "src/components/dialogs/MergeProjectCheck.vue"
|
|
||||||
import newProjectCheckDialog from "src/components/dialogs/NewProjectCheck.vue"
|
|
||||||
import repairProjectDialog from "src/components/dialogs/RepairProject.vue"
|
|
||||||
import aboutAppDialog from "src/components/dialogs/AboutApp.vue"
|
|
||||||
import changeLogDialog from "src/components/dialogs/ChangeLog.vue"
|
|
||||||
import programSettingsDialog from "src/components/dialogs/ProgramSettings.vue"
|
|
||||||
import advancedSearchGuideDialog from "src/components/dialogs/AdvancedSearchGuide.vue"
|
|
||||||
import newDocumentDialog from "src/components/dialogs/NewDocument.vue"
|
|
||||||
import existingDocumentDialog from "src/components/dialogs/ExistingDocument.vue"
|
|
||||||
import tipsTricksTriviaDialog from "src/components/dialogs/TipsTricksTrivia.vue"
|
|
||||||
import licenseDialog from "src/components/dialogs/License.vue"
|
|
||||||
|
|
||||||
import { Loading, QSpinnerGears } from "quasar"
|
|
||||||
import { retrieveCurrentProjectName, exportProject } from "src/scripts/projectManagement/projectManagent"
|
|
||||||
import { toggleDevTools } from "src/scripts/utilities/devTools"
|
|
||||||
|
|
||||||
import appLogo from "src/assets/appLogo.png"
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
projectCloseCheckDialog,
|
|
||||||
keybindCheatsheetDialog,
|
|
||||||
importProjectCheckDialog,
|
|
||||||
mergeProjectCheckDialog,
|
|
||||||
newProjectCheckDialog,
|
|
||||||
aboutAppDialog,
|
|
||||||
changeLogDialog,
|
|
||||||
advancedSearchGuideDialog,
|
|
||||||
programSettingsDialog,
|
|
||||||
newDocumentDialog,
|
|
||||||
existingDocumentDialog,
|
|
||||||
tipsTricksTriviaDialog,
|
|
||||||
licenseDialog,
|
|
||||||
repairProjectDialog
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export default class AppControl extends BaseClass {
|
|
||||||
/****************************************************************/
|
|
||||||
// Import handling
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the developer tools on and off
|
|
||||||
*/
|
|
||||||
toggleDevTools = toggleDevTools
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the current project name
|
|
||||||
*/
|
|
||||||
retrieveCurrentProjectName = retrieveCurrentProjectName
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Just an image
|
|
||||||
*/
|
|
||||||
appLogo = appLogo
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Basic component functionality
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the project exists or not
|
|
||||||
*/
|
|
||||||
projectExists: undefined | string | boolean = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if we are on frontpage or not
|
|
||||||
*/
|
|
||||||
isFrontpage = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if we are on project page or not
|
|
||||||
*/
|
|
||||||
isProjectPage = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current project name
|
|
||||||
*/
|
|
||||||
projectName = ""
|
|
||||||
|
|
||||||
created () {
|
|
||||||
this.checkProjectStatus().catch(e => console.log(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkProjectStatus () {
|
|
||||||
this.projectName = await retrieveCurrentProjectName()
|
|
||||||
this.projectExists = !!(await retrieveCurrentProjectName())
|
|
||||||
this.isFrontpage = (this.$route.path === "/")
|
|
||||||
this.isProjectPage = (this.$route.path === "/project")
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Local keybinds
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
@Watch("SGET_getCurrentKeyBindData", { deep: true })
|
|
||||||
processKeyPush () {
|
|
||||||
// Keybind cheatsheet
|
|
||||||
if (this.determineKeyBind("openKeybindsCheatsheet") && !this.SGET_getDialogsState) {
|
|
||||||
this.keybindsDialogAssignUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// App options
|
|
||||||
if (this.determineKeyBind("openAppOptions") && !this.SGET_getDialogsState) {
|
|
||||||
this.programSettingsDialogAssignUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// App options
|
|
||||||
if (this.determineKeyBind("navigateToProjectOverview") && this.projectExists && !this.isProjectPage) {
|
|
||||||
this.navigateToProjectPage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Navigate to project page action
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
navigateToProjectPage () {
|
|
||||||
this.$router.push({ path: "/project" }).catch((e: {name: string}) => {
|
|
||||||
if (e && e.name !== "NavigationDuplicated") {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Export project action
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
async commenceExport () {
|
|
||||||
const projectName = await retrieveCurrentProjectName()
|
|
||||||
const setup = {
|
|
||||||
message: "<h4>Exporting current project...</h4>",
|
|
||||||
spinnerColor: "primary",
|
|
||||||
messageColor: "cultured",
|
|
||||||
spinnerSize: 120,
|
|
||||||
backgroundColor: "dark",
|
|
||||||
// @ts-ignore
|
|
||||||
spinner: QSpinnerGears
|
|
||||||
}
|
|
||||||
exportProject(projectName, Loading, setup, this.$q)
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Close project dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
projectCloseCheckDialogTrigger: string | false = false
|
|
||||||
projectCloseCheckDialogClose () {
|
|
||||||
this.projectCloseCheckDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
projectCloseCheckDialogAssignUID () {
|
|
||||||
this.projectCloseCheckDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Import project dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
importProjectDialogTrigger: string | false = false
|
|
||||||
importProjectDialogClose () {
|
|
||||||
this.importProjectDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
importProjectAssignUID () {
|
|
||||||
this.importProjectDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Merge project dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
mergeProjectDialogTrigger: string | false = false
|
|
||||||
mergeProjectDialogClose () {
|
|
||||||
this.mergeProjectDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeProjectAssignUID () {
|
|
||||||
this.mergeProjectDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// New project dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
newProjectDialogTrigger: string | false = false
|
|
||||||
newProjectDialogClose () {
|
|
||||||
this.newProjectDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
newProjectAssignUID () {
|
|
||||||
this.newProjectDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Keybings cheatsheet dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
keybindsDialogTrigger: string | false = false
|
|
||||||
keybindsDialogClose () {
|
|
||||||
this.keybindsDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
keybindsDialogAssignUID () {
|
|
||||||
this.keybindsDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// About app dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
aboutAppDialogTrigger: string | false = false
|
|
||||||
aboutAppDialogClose () {
|
|
||||||
this.aboutAppDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
aboutAppDialogAssignUID () {
|
|
||||||
this.aboutAppDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Changelog dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
changeLogDialogTrigger: string | false = false
|
|
||||||
changeLogDialogClose () {
|
|
||||||
this.changeLogDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
changeLogDialogAssignUID () {
|
|
||||||
this.changeLogDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Program settings dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
programSettingsDialogTrigger: string | false = false
|
|
||||||
programSettingsDialogClose () {
|
|
||||||
this.programSettingsDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
programSettingsDialogAssignUID () {
|
|
||||||
this.programSettingsDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Advanced search guide dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
advancedSearchGuideDialogTrigger: string | false = false
|
|
||||||
advancedSearchGuideDialogClose () {
|
|
||||||
this.advancedSearchGuideDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
advancedSearchGuideAssignUID () {
|
|
||||||
this.advancedSearchGuideDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// New document dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
newObjectDialogTrigger: string | false = false
|
|
||||||
newObjectDialogClose () {
|
|
||||||
this.newObjectDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
newObjectAssignUID () {
|
|
||||||
this.newObjectDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Existing document dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
existingObjectDialogTrigger: string | false = false
|
|
||||||
existingObjectDialogClose () {
|
|
||||||
this.existingObjectDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
existingObjectAssignUID () {
|
|
||||||
this.existingObjectDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Tips, Tricka & Trivia dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
tipsTricksDialogTrigger: string | false = false
|
|
||||||
tipsTricksDialogClose () {
|
|
||||||
this.tipsTricksDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
tipsTricksAssignUID () {
|
|
||||||
this.tipsTricksDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// License dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
licenseDialogTrigger: string | false = false
|
|
||||||
licenseDialogClose () {
|
|
||||||
this.licenseDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
licenseAssignUID () {
|
|
||||||
this.licenseDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Repair project dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
repairProjectDialogTrigger: string | false = false
|
|
||||||
repairProjectDialogClose () {
|
|
||||||
this.repairProjectDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
repairProjectAssignUID () {
|
|
||||||
this.repairProjectDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.AppControl {
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
&__buttons {
|
|
||||||
height: 40px;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,148 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<q-btn-group
|
|
||||||
flat
|
|
||||||
class="appWindowButtons bg-dark"
|
|
||||||
>
|
|
||||||
|
|
||||||
<projectCloseCheckDialog
|
|
||||||
:dialog-trigger="projectCloseCheckDialogTrigger"
|
|
||||||
:dialog-mode="'appClose'"
|
|
||||||
@trigger-dialog-close="projectCloseCheckDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Minimize button-->
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
:ripple="false"
|
|
||||||
:class="{'minimize': osSystem === 'darwin'}"
|
|
||||||
dark
|
|
||||||
size='sm'
|
|
||||||
@click="minimizeWindow">
|
|
||||||
<q-icon name="mdi-window-minimize"></q-icon>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<!-- MinMax button-->
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
:ripple="false"
|
|
||||||
:class="{'minMax': osSystem === 'darwin'}"
|
|
||||||
dark
|
|
||||||
size='sm'
|
|
||||||
@click="resizeWindow">
|
|
||||||
<q-icon :name="(isMaximized)? 'mdi-window-restore' : 'mdi-window-maximize'"></q-icon>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<!-- Close button-->
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
:ripple="false"
|
|
||||||
dark
|
|
||||||
size='sm'
|
|
||||||
@click="projectCloseCheckDialogAssignUID"
|
|
||||||
:class="[{'close': osSystem === 'darwin'}]"
|
|
||||||
>
|
|
||||||
<q-icon name="mdi-window-close"></q-icon>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
</q-btn-group>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component } from "vue-property-decorator"
|
|
||||||
import { remote } from "electron"
|
|
||||||
|
|
||||||
import BaseClass from "src/BaseClass"
|
|
||||||
import projectCloseCheckDialog from "src/components/dialogs/ProjectCloseCheck.vue"
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: { projectCloseCheckDialog }
|
|
||||||
})
|
|
||||||
export default class AppWindowButtons extends BaseClass {
|
|
||||||
/****************************************************************/
|
|
||||||
// Basic component functionality
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the window is maximed or not
|
|
||||||
*/
|
|
||||||
isMaximized = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the currently used OS
|
|
||||||
*/
|
|
||||||
osSystem = remote.process.platform
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Currently opened window
|
|
||||||
*/
|
|
||||||
currentWindow = remote.getCurrentWindow()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the window is currently maximized or not
|
|
||||||
*/
|
|
||||||
checkIfMaximized () {
|
|
||||||
this.isMaximized = this.currentWindow.isMaximized()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimizes the current window
|
|
||||||
*/
|
|
||||||
minimizeWindow () {
|
|
||||||
this.currentWindow.minimize()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resizes the window to either smaller or maximized
|
|
||||||
*/
|
|
||||||
resizeWindow () {
|
|
||||||
if (this.currentWindow.isMaximized()) {
|
|
||||||
this.currentWindow.unmaximize()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.currentWindow.maximize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
created () {
|
|
||||||
window.addEventListener("resize", this.checkIfMaximized)
|
|
||||||
this.checkIfMaximized()
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyed () {
|
|
||||||
window.addEventListener("resize", this.checkIfMaximized)
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Close project dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
projectCloseCheckDialogTrigger: string | false = false
|
|
||||||
projectCloseCheckDialogClose () {
|
|
||||||
this.projectCloseCheckDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
projectCloseCheckDialogAssignUID () {
|
|
||||||
this.projectCloseCheckDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.appWindowButtons {
|
|
||||||
border-radius: 0;
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 40px;
|
|
||||||
z-index: 99999999;
|
|
||||||
color: #fff;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss" >
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,586 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
|
|
||||||
<closeDocumentCheckDialog
|
|
||||||
:dialog-trigger="closeDocumentCheckDialogTrigger"
|
|
||||||
:dialog-document="dialogDoc"
|
|
||||||
@trigger-dialog-close="closeDocumentCheckDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Delele document dialog -->
|
|
||||||
<deleteDocumentCheckDialog
|
|
||||||
:dialog-trigger="deleteObjectDialogTrigger"
|
|
||||||
:document-id="toDeleteID"
|
|
||||||
:document-type="toDeleteType"
|
|
||||||
@trigger-dialog-close="deleteObjectDialogClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-tabs
|
|
||||||
v-if="localDocuments.length > 0"
|
|
||||||
:class="{'hasTextShadow': textShadow}"
|
|
||||||
align="left"
|
|
||||||
inline-label
|
|
||||||
outside-arrows
|
|
||||||
mobile-arrows
|
|
||||||
class="tabsWrapper"
|
|
||||||
dense
|
|
||||||
no-caps>
|
|
||||||
<transition-group
|
|
||||||
name="list"
|
|
||||||
tag="div"
|
|
||||||
class="headerTransitionWrapper"
|
|
||||||
enter-active-class="animated fadeIn"
|
|
||||||
leave-active-class="animated fadeOut"
|
|
||||||
appear
|
|
||||||
:duration="50">
|
|
||||||
|
|
||||||
<q-route-tab
|
|
||||||
:ripple="false"
|
|
||||||
v-for="document in localDocuments"
|
|
||||||
:to="`/project/display-content/${document.type}/${document._id}`"
|
|
||||||
:key="document.type+document._id"
|
|
||||||
:icon="(retrieveFieldValue(document,'categorySwitch') ? 'fas fa-folder-open' : document.icon)"
|
|
||||||
:style="`
|
|
||||||
color: ${retrieveFieldValue(document,'documentColor')};
|
|
||||||
background-color: ${retrieveFieldValue(document,'documentBackgroundColor')};
|
|
||||||
filter: ${(retrieveFieldValue(document,'minorSwitch') ? 'grayscale(100) brightness(0.7)' : '')}`"
|
|
||||||
:class="[
|
|
||||||
{'isBold':
|
|
||||||
(
|
|
||||||
retrieveFieldValue(document,'documentColor') !== '#ffffff' &&
|
|
||||||
retrieveFieldValue(document,'documentColor') !== '#fff'
|
|
||||||
) &&
|
|
||||||
retrieveFieldValue(document,'documentColor') !== ''
|
|
||||||
}]"
|
|
||||||
:alert="document.hasEdits"
|
|
||||||
alert-icon="mdi-feather"
|
|
||||||
@click.prevent.middle="tryCloseTab(document)"
|
|
||||||
@mouseleave="setDocumentPreviewClose"
|
|
||||||
>
|
|
||||||
<span class="isDeadIndicator" v-if="retrieveFieldValue(document,'deadSwitch')">
|
|
||||||
†
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
class="q-tab__label"
|
|
||||||
:class="{'isDead': (retrieveFieldValue(document,'deadSwitch') && !hideDeadCrossThrough)}">
|
|
||||||
{{retrieveFieldValue(document,'name')}}
|
|
||||||
</div>
|
|
||||||
<documentPreview
|
|
||||||
v-if="!preventPreviewsTabs"
|
|
||||||
:document-id="document._id"
|
|
||||||
:external-close-trigger="documentPreviewClose"
|
|
||||||
:custom-delay="1500"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
class="z-max q-ml-auto"
|
|
||||||
:class="{'q-mr-sm': document.hasEdits}"
|
|
||||||
size="xs"
|
|
||||||
icon="close"
|
|
||||||
style="color: #fff;"
|
|
||||||
@click.stop.prevent="tryCloseTab(document)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-menu
|
|
||||||
touch-position
|
|
||||||
context-menu
|
|
||||||
>
|
|
||||||
<q-list class="bg-gunmetal-light text-accent">
|
|
||||||
<q-item clickable>
|
|
||||||
<q-item-section>All opened tabs</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="keyboard_arrow_right" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-menu anchor="top end" self="top start">
|
|
||||||
<q-list class="bg-gunmetal text-accent">
|
|
||||||
<q-item
|
|
||||||
:to="`/project/display-content/${menuDoc.type}/${menuDoc._id}`"
|
|
||||||
v-for="menuDoc in localDocuments"
|
|
||||||
:key="menuDoc._id"
|
|
||||||
clickable
|
|
||||||
:style="`
|
|
||||||
color: ${retrieveFieldValue(menuDoc,'documentColor')};
|
|
||||||
background-color: ${retrieveFieldValue(menuDoc,'documentBackgroundColor')};
|
|
||||||
filter: ${(retrieveFieldValue(menuDoc,'minorSwitch') ? 'grayscale(100) brightness(0.7)' : '')}`"
|
|
||||||
>
|
|
||||||
<q-item-section class="isDeadIndicator grow-0" v-if="retrieveFieldValue(menuDoc,'deadSwitch')">
|
|
||||||
†
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section
|
|
||||||
:class="{'isDead': (retrieveFieldValue(menuDoc,'deadSwitch') && !hideDeadCrossThrough)}"
|
|
||||||
>{{retrieveFieldValue(menuDoc,'name')}}</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon :name="(retrieveFieldValue(menuDoc,'categorySwitch') ? 'fas fa-folder-open' : menuDoc.icon)" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-menu>
|
|
||||||
</q-item>
|
|
||||||
<q-separator dark />
|
|
||||||
<q-item clickable v-close-popup @click="copyName(document)">
|
|
||||||
<q-item-section>Copy name</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-text-recognition" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable v-close-popup @click="copyTextColor(document)">
|
|
||||||
<q-item-section>Copy text color</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-eyedropper" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable v-close-popup @click="copyBackgroundColor(document)">
|
|
||||||
<q-item-section>Copy background color</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-format-color-fill" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-separator dark />
|
|
||||||
<q-item v-if="!document.isNew" clickable v-close-popup @click="addNewUnderParent(document)">
|
|
||||||
<q-item-section>Create new document with this document as parent</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-file-tree" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable v-if="!document.isNew" v-close-popup @click="copyTargetDocument(document)">
|
|
||||||
<q-item-section>Copy this document</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-content-copy" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-separator v-if="!document.isNew" />
|
|
||||||
<q-item clickable v-close-popup @click="tryCloseTab(document)">
|
|
||||||
<q-item-section>Close this tab</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-close" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable v-close-popup @click="SSET_closeAllButCurrentDocuments(document)">
|
|
||||||
<q-item-section>Close all tabs without changes except for this</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-close-box-outline" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable v-close-popup @click="SSET_closeAllDocuments">
|
|
||||||
<q-item-section>Close all tabs without changes</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-close-box-multiple-outline" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-separator dark />
|
|
||||||
<q-item clickable v-close-popup @click="SSET_forceCloseAllButCurrentDocuments(document)">
|
|
||||||
<q-item-section class="text-secondary"><b>Force close all tabs except for this</b></q-item-section>
|
|
||||||
<q-item-section avatar class="text-secondary">
|
|
||||||
<q-icon name="mdi-close-box" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable v-close-popup @click="SSET_forceCloseAllDocuments">
|
|
||||||
<q-item-section class="text-secondary"><b>Force close all tabs</b></q-item-section>
|
|
||||||
<q-item-section avatar class="text-secondary">
|
|
||||||
<q-icon name="mdi-close-box-multiple" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-separator dark />
|
|
||||||
<q-item clickable v-close-popup @click="deleteTabDocument(document)">
|
|
||||||
<q-item-section class="text-secondary"><b>Delete this document</b></q-item-section>
|
|
||||||
<q-item-section avatar class="text-secondary">
|
|
||||||
<q-icon name="mdi-text-box-remove-outline" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
|
|
||||||
</q-menu>
|
|
||||||
</q-route-tab>
|
|
||||||
|
|
||||||
</transition-group>
|
|
||||||
</q-tabs>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import BaseClass from "src/BaseClass"
|
|
||||||
|
|
||||||
import { Component, Watch } from "vue-property-decorator"
|
|
||||||
import deleteDocumentCheckDialog from "src/components/dialogs/DeleteDocumentCheck.vue"
|
|
||||||
|
|
||||||
import { I_OpenedDocument } from "src/interfaces/I_OpenedDocument"
|
|
||||||
import closeDocumentCheckDialog from "src/components/dialogs/CloseDocumentCheck.vue"
|
|
||||||
import { createNewWithParent } from "src/scripts/documentActions/createNewWithParent"
|
|
||||||
import { copyDocumentName, copyDocumentTextColor, copyDocumentBackgroundColor } from "src/scripts/documentActions/uniqueFieldCopy"
|
|
||||||
import { copyDocument } from "src/scripts/documentActions/copyDocument"
|
|
||||||
import { extend, uid } from "quasar"
|
|
||||||
import documentPreview from "src/components/DocumentPreview.vue"
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
closeDocumentCheckDialog,
|
|
||||||
deleteDocumentCheckDialog,
|
|
||||||
documentPreview
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export default class TopTabs extends BaseClass {
|
|
||||||
/****************************************************************/
|
|
||||||
// App options
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the tabs have text shadow or not
|
|
||||||
*/
|
|
||||||
textShadow = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the "dead" document type should have a cross-text decoration or not
|
|
||||||
*/
|
|
||||||
hideDeadCrossThrough = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines document previews should be shown or not
|
|
||||||
*/
|
|
||||||
preventPreviewsTabs = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Watch changes on options
|
|
||||||
*/
|
|
||||||
@Watch("SGET_options", { immediate: true, deep: true })
|
|
||||||
onSettingsChange () {
|
|
||||||
const options = this.SGET_options
|
|
||||||
this.textShadow = options.textShadow
|
|
||||||
this.hideDeadCrossThrough = options.hideDeadCrossThrough
|
|
||||||
this.preventPreviewsTabs = options.preventPreviewsTabs
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Keybind handling
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* React to keypresses passed from the parent document
|
|
||||||
*/
|
|
||||||
@Watch("SGET_getCurrentKeyBindData", { deep: true })
|
|
||||||
processKeyPush () {
|
|
||||||
// Close tab dialog
|
|
||||||
if (this.determineKeyBind("closeTab") && this.localDocuments.length > 0 && !this.SGET_getDialogsState) {
|
|
||||||
this.tryCloseTab()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next tab
|
|
||||||
if (this.determineKeyBind("nextTab") && this.localDocuments.length > 0 && !this.SGET_getDialogsState) {
|
|
||||||
this.goToNextTab()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Previous tab
|
|
||||||
if (this.determineKeyBind("previousTab") && this.localDocuments.length > 0 && !this.SGET_getDialogsState) {
|
|
||||||
this.goToPreviousTab()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close all tabs without changes except for this - CTRL + ALT + SHIFT + W
|
|
||||||
if (this.determineKeyBind("closeAllTabsWithoutChangesButThis") && this.localDocuments.length > 0 && !this.SGET_getDialogsState) {
|
|
||||||
const currentDoc = this.findRequestedOrActiveDocument() as I_OpenedDocument
|
|
||||||
this.SSET_closeAllButCurrentDocuments(currentDoc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close all tabs without changes - CTRL + SHIFT + W
|
|
||||||
if (this.determineKeyBind("closeAllTabsWithoutChanges") && this.localDocuments.length > 0 && !this.SGET_getDialogsState) {
|
|
||||||
this.SSET_closeAllDocuments()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force close all tabs except for this - NONE
|
|
||||||
if (this.determineKeyBind("forceCloseAllTabsButThis") && this.localDocuments.length > 0 && !this.SGET_getDialogsState) {
|
|
||||||
const currentDoc = this.findRequestedOrActiveDocument() as I_OpenedDocument
|
|
||||||
this.SSET_forceCloseAllButCurrentDocuments(currentDoc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force close all tabs - NONE
|
|
||||||
if (this.determineKeyBind("forceCloseAllTabs") && this.localDocuments.length > 0 && !this.SGET_getDialogsState) {
|
|
||||||
this.SSET_forceCloseAllDocuments()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Tab management
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the local reference whenever something gets changed
|
|
||||||
*/
|
|
||||||
@Watch("SGET_allOpenedDocuments", { deep: true })
|
|
||||||
reactToDocumentListChange (val: {docs: I_OpenedDocument[]}, oldVal: {docs: I_OpenedDocument[]}) {
|
|
||||||
this.localDocuments = []
|
|
||||||
this.localDocuments = val.docs
|
|
||||||
|
|
||||||
// Re-check the route after a change
|
|
||||||
this.refreshRoute()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Local reference to the opened document list
|
|
||||||
*/
|
|
||||||
localDocuments: I_OpenedDocument[] = []
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current document to be closed
|
|
||||||
*/
|
|
||||||
dialogDoc = null as unknown as I_OpenedDocument
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to close the document being closed
|
|
||||||
*/
|
|
||||||
tryCloseTab (doc?: I_OpenedDocument) {
|
|
||||||
const matchingDocument = this.findRequestedOrActiveDocument(doc)
|
|
||||||
|
|
||||||
if (matchingDocument) {
|
|
||||||
this.dialogDoc = matchingDocument
|
|
||||||
this.closeDocumentCheckDialogAssignUID()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to navigate to next tab
|
|
||||||
*/
|
|
||||||
goToNextTab () {
|
|
||||||
let index = -1
|
|
||||||
const matchingDocument = this.localDocuments.find((e, i) => {
|
|
||||||
index = i
|
|
||||||
return e.url === this.$route.path
|
|
||||||
})
|
|
||||||
|
|
||||||
if (matchingDocument && index !== this.localDocuments.length - 1) {
|
|
||||||
this.$router.push({ path: this.localDocuments[index + 1].url }).catch((e: {name: string}) => {
|
|
||||||
if (e && e.name !== "NavigationDuplicated") {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (matchingDocument && index === this.localDocuments.length - 1) {
|
|
||||||
this.$router.push({ path: this.localDocuments[0].url }).catch((e: {name: string}) => {
|
|
||||||
if (e && e.name !== "NavigationDuplicated") {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to navigate to previous tab
|
|
||||||
*/
|
|
||||||
goToPreviousTab () {
|
|
||||||
let index = -1
|
|
||||||
const matchingDocument = this.localDocuments.find((e, i) => {
|
|
||||||
index = i
|
|
||||||
return e.url === this.$route.path
|
|
||||||
})
|
|
||||||
|
|
||||||
if (matchingDocument && index !== 0) {
|
|
||||||
this.$router.push({ path: this.localDocuments[index - 1].url }).catch((e: {name: string}) => {
|
|
||||||
if (e && e.name !== "NavigationDuplicated") {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchingDocument && index === 0) {
|
|
||||||
this.$router.push({ path: this.localDocuments[this.localDocuments.length - 1].url }).catch((e: {name: string}) => {
|
|
||||||
if (e && e.name !== "NavigationDuplicated") {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Document field copying
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
copyName (currentDoc: I_OpenedDocument) {
|
|
||||||
copyDocumentName(currentDoc)
|
|
||||||
}
|
|
||||||
|
|
||||||
copyTextColor (currentDoc: I_OpenedDocument) {
|
|
||||||
copyDocumentTextColor(currentDoc)
|
|
||||||
}
|
|
||||||
|
|
||||||
copyBackgroundColor (currentDoc: I_OpenedDocument) {
|
|
||||||
copyDocumentBackgroundColor(currentDoc)
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Document copy
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
documentPass = null as unknown as I_OpenedDocument
|
|
||||||
|
|
||||||
copyTargetDocument (currentDoc: I_OpenedDocument) {
|
|
||||||
this.documentPass = extend(true, {}, currentDoc)
|
|
||||||
|
|
||||||
const blueprint = this.SGET_blueprint(this.documentPass.type)
|
|
||||||
const newDocument = copyDocument(this.documentPass, this.generateUID(), blueprint)
|
|
||||||
|
|
||||||
const dataPass = {
|
|
||||||
doc: newDocument,
|
|
||||||
treeAction: false
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
this.SSET_addOpenedDocument(dataPass)
|
|
||||||
this.$router.push({
|
|
||||||
path: newDocument.url
|
|
||||||
}).catch((e: {name: string}) => {
|
|
||||||
const errorName : string = e.name
|
|
||||||
if (errorName === "NavigationDuplicated") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Add new document under parent
|
|
||||||
/****************************************************************/
|
|
||||||
addNewUnderParent (currentDoc: I_OpenedDocument) {
|
|
||||||
createNewWithParent(currentDoc, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Close document dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
closeDocumentCheckDialogTrigger: string | false = false
|
|
||||||
closeDocumentCheckDialogClose () {
|
|
||||||
this.closeDocumentCheckDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
closeDocumentCheckDialogAssignUID () {
|
|
||||||
this.closeDocumentCheckDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************/
|
|
||||||
// Delete dialog
|
|
||||||
/****************************************************************/
|
|
||||||
|
|
||||||
deleteObjectDialogTrigger: string | false = false
|
|
||||||
deleteObjectDialogClose () {
|
|
||||||
this.deleteObjectDialogTrigger = false
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteObjectAssignUID () {
|
|
||||||
this.deleteObjectDialogTrigger = this.generateUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
toDeleteID = ""
|
|
||||||
toDeleteType = ""
|
|
||||||
|
|
||||||
deleteTabDocument (targetDocument: I_OpenedDocument) {
|
|
||||||
this.toDeleteID = targetDocument._id
|
|
||||||
this.toDeleteType = targetDocument.type
|
|
||||||
this.deleteObjectAssignUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
setDocumentPreviewClose () {
|
|
||||||
this.documentPreviewClose = uid()
|
|
||||||
}
|
|
||||||
|
|
||||||
documentPreviewClose = ""
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.headerTransitionWrapper {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabsWrapper .fas,
|
|
||||||
.tabsWrapper .fab {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabsWrapper .mdi {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.tabsWrapper {
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
|
|
||||||
&.hasTextShadow {
|
|
||||||
.q-tab__label,
|
|
||||||
.q-tab__icon {
|
|
||||||
$shadowColorOutline: #000;
|
|
||||||
$shadowColorSurround: #000;
|
|
||||||
|
|
||||||
filter: drop-shadow(0 0 1px #000);
|
|
||||||
text-shadow:
|
|
||||||
//-2px -2px 0 $shadowColorSurround,
|
|
||||||
//2px -2px 0 $shadowColorSurround,
|
|
||||||
//-2px 2px 0 $shadowColorSurround,
|
|
||||||
//2px 2px 0 $shadowColorSurround,
|
|
||||||
-1px -1px 0 $shadowColorOutline,
|
|
||||||
1px -1px 0 $shadowColorOutline,
|
|
||||||
-1px 1px 0 $shadowColorOutline,
|
|
||||||
1px 1px 0 $shadowColorOutline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-tabs__arrow {
|
|
||||||
text-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.isBold .q-tab__label {
|
|
||||||
font-weight: 500 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-tab {
|
|
||||||
padding: 0 10px;
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
min-width: 170px;
|
|
||||||
width: 170px;
|
|
||||||
justify-content: flex-start;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
padding-top: 2px;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fas,
|
|
||||||
.fab {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mdi {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.q-tabs--dense .q-tab {
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-tab__alert-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
top: 4px;
|
|
||||||
right: -10px;
|
|
||||||
color: $primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body.body--dark {
|
|
||||||
.topTabs {
|
|
||||||
.q-tab {
|
|
||||||
color: #dcdcdc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,145 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<q-dialog
|
|
||||||
v-model="dialogModel"
|
|
||||||
@before-hide="triggerDialogClose"
|
|
||||||
>
|
|
||||||
<q-card
|
|
||||||
class="aboutDialog"
|
|
||||||
dark
|
|
||||||
>
|
|
||||||
<q-card-section>
|
|
||||||
<h6 class="text-center q-mt-lg q-mb-sm">About Fantasia Archive</h6>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section>
|
|
||||||
<div>
|
|
||||||
Currently running Fantasia Archive version: <span class="text-bold text-primary">{{appVersion}}</span>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-separator color="primary" horizonatal dark class="q-my-lg q-mx-auto" style="opacity: 0.5; width: 400px;" />
|
|
||||||
|
|
||||||
<q-card-section>
|
|
||||||
<div class="col-12 q-mx-sm q-my-md">
|
|
||||||
<div class="patreonButton shadow-1" @click="openPatreonLink">
|
|
||||||
Support Fantasia Archive on Patreon!
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 q-mb-lg">
|
|
||||||
<div class="row justify-center">
|
|
||||||
|
|
||||||
<div class="q-mx-sm q-my-md">
|
|
||||||
<div class="discordButton shadow-1" @click="openDiscordInviteLink">
|
|
||||||
Discord
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="q-mx-sm q-my-md">
|
|
||||||
<div class="redditButton shadow-1" @click="openRedditLink"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="q-mx-sm q-my-md">
|
|
||||||
<div class="websiteButton shadow-1" @click="openWebsiteLink">
|
|
||||||
Website
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="q-mx-sm q-my-md">
|
|
||||||
<div class="githubButton shadow-1" @click="openGithubLink">
|
|
||||||
GitHub
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="around" class="q-mb-lg q-mt-md">
|
|
||||||
<q-btn flat label="Close" color="accent" v-close-popup />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component, Watch } from "vue-property-decorator"
|
|
||||||
import { shell, remote } from "electron"
|
|
||||||
|
|
||||||
import DialogBase from "src/components/dialogs/_DialogBase"
|
|
||||||
@Component({
|
|
||||||
components: { }
|
|
||||||
})
|
|
||||||
export default class AboutApp extends DialogBase {
|
|
||||||
/**
|
|
||||||
* React to dialog opening request
|
|
||||||
*/
|
|
||||||
@Watch("dialogTrigger")
|
|
||||||
openDialog (val: string|false) {
|
|
||||||
if (val) {
|
|
||||||
if (this.SGET_getDialogsState) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.SSET_setDialogState(true)
|
|
||||||
this.dialogModel = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current app version
|
|
||||||
* NOTE: Show Electon version in DEV mode instead of NPM package version
|
|
||||||
*/
|
|
||||||
appVersion = remote.app.getVersion()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open Discord invite link in thw default browser window
|
|
||||||
*/
|
|
||||||
openDiscordInviteLink () {
|
|
||||||
shell.openExternal("https://discord.gg/JQDBvsN9Te").catch(e => console.log(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open Patreon link in thw default browser window
|
|
||||||
*/
|
|
||||||
openPatreonLink () {
|
|
||||||
shell.openExternal("https://www.patreon.com/elvanos").catch(e => console.log(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open Reddit link in thw default browser window
|
|
||||||
*/
|
|
||||||
openRedditLink () {
|
|
||||||
shell.openExternal("https://www.reddit.com/r/FantasiaArchive/").catch(e => console.log(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open Website link in thw default browser window
|
|
||||||
*/
|
|
||||||
openWebsiteLink () {
|
|
||||||
shell.openExternal("http://fantasiaarchive.com/").catch(e => console.log(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open GitHub link in thw default browser window
|
|
||||||
*/
|
|
||||||
openGithubLink () {
|
|
||||||
shell.openExternal("https://github.com/Elvanos/fantasia-archive").catch(e => console.log(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.aboutDialog {
|
|
||||||
text-align: center;
|
|
||||||
width: 650px;
|
|
||||||
max-width: 650px !important;
|
|
||||||
|
|
||||||
h6 {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,68 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<q-dialog
|
|
||||||
v-model="dialogModel"
|
|
||||||
@before-hide="triggerDialogClose"
|
|
||||||
>
|
|
||||||
<q-card
|
|
||||||
class="advancedSearchDialog"
|
|
||||||
dark
|
|
||||||
>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<q-scroll-area
|
|
||||||
class="q-mx-xl q-my-lg"
|
|
||||||
visible
|
|
||||||
dark
|
|
||||||
:thumb-style="thumbStyle"
|
|
||||||
style="max-height: calc(100vh - 235px); height: 800px; width: 100%;">
|
|
||||||
<q-markdown no-heading-anchor-links>
|
|
||||||
{{$t('documents.advancedSearchGuide')}}
|
|
||||||
</q-markdown>
|
|
||||||
</q-scroll-area>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="around" class="q-mb-lg">
|
|
||||||
<q-btn flat label="Close" color="accent" v-close-popup />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component, Watch } from "vue-property-decorator"
|
|
||||||
|
|
||||||
import DialogBase from "src/components/dialogs/_DialogBase"
|
|
||||||
@Component({
|
|
||||||
components: { }
|
|
||||||
})
|
|
||||||
export default class AdvancedSearchGuide extends DialogBase {
|
|
||||||
/**
|
|
||||||
* React to dialog opening request
|
|
||||||
*/
|
|
||||||
@Watch("dialogTrigger")
|
|
||||||
openDialog (val: string|false) {
|
|
||||||
if (val) {
|
|
||||||
if (this.SGET_getDialogsState) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.SSET_setDialogState(true)
|
|
||||||
this.dialogModel = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.advancedSearchDialog {
|
|
||||||
width: 1000px;
|
|
||||||
max-width: calc(100vw - 100px) !important;
|
|
||||||
|
|
||||||
h6 {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,68 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<q-dialog
|
|
||||||
v-model="dialogModel"
|
|
||||||
@before-hide="triggerDialogClose"
|
|
||||||
>
|
|
||||||
<q-card
|
|
||||||
class="changeLogDialog"
|
|
||||||
dark
|
|
||||||
>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<q-scroll-area
|
|
||||||
class="q-mx-xl q-my-lg"
|
|
||||||
visible
|
|
||||||
dark
|
|
||||||
:thumb-style="thumbStyle"
|
|
||||||
style="max-height: calc(100vh - 235px); height: 800px; width: 100%;">
|
|
||||||
<q-markdown no-heading-anchor-links>
|
|
||||||
{{$t('documents.changeLog')}}
|
|
||||||
</q-markdown>
|
|
||||||
</q-scroll-area>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="around" class="q-mb-lg">
|
|
||||||
<q-btn flat label="Close" color="accent" v-close-popup />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component, Watch } from "vue-property-decorator"
|
|
||||||
|
|
||||||
import DialogBase from "src/components/dialogs/_DialogBase"
|
|
||||||
@Component({
|
|
||||||
components: { }
|
|
||||||
})
|
|
||||||
export default class ChangeLog extends DialogBase {
|
|
||||||
/**
|
|
||||||
* React to dialog opening request
|
|
||||||
*/
|
|
||||||
@Watch("dialogTrigger")
|
|
||||||
openDialog (val: string|false) {
|
|
||||||
if (val) {
|
|
||||||
if (this.SGET_getDialogsState) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.SSET_setDialogState(true)
|
|
||||||
this.dialogModel = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.changeLogDialog {
|
|
||||||
width: 1000px;
|
|
||||||
max-width: calc(100vw - 100px) !important;
|
|
||||||
|
|
||||||
h6 {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,88 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-dialog
|
|
||||||
v-model="dialogModel"
|
|
||||||
@before-hide="triggerDialogClose"
|
|
||||||
>
|
|
||||||
<q-card dark class="documentCloseDialog">
|
|
||||||
<q-card-section class="row justify-center">
|
|
||||||
<h6 class="text-center q-my-sm">Discard changes to <span class="text-primary">{{retrieveFieldValue(dialogDocument,'name')}}</span>?</h6>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="around" class="q-mx-xl q-mt-lg q-mb-md">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
label="Cancel"
|
|
||||||
color="accent"
|
|
||||||
v-close-popup />
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
label="Discard changes"
|
|
||||||
color="secondary"
|
|
||||||
@click="closeDocument(dialogDocument)" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component, Watch, Prop } from "vue-property-decorator"
|
|
||||||
|
|
||||||
import DialogBase from "src/components/dialogs/_DialogBase"
|
|
||||||
import { I_OpenedDocument } from "src/interfaces/I_OpenedDocument"
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: { }
|
|
||||||
})
|
|
||||||
export default class CloseDocumentCheckDialog extends DialogBase {
|
|
||||||
/**
|
|
||||||
* React to dialog opening request
|
|
||||||
*/
|
|
||||||
@Watch("dialogTrigger")
|
|
||||||
openDialog (val: string|false) {
|
|
||||||
if (val) {
|
|
||||||
this.checkForCloseOpenedDocument()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current document being processed by the dialog
|
|
||||||
*/
|
|
||||||
@Prop() readonly dialogDocument!: I_OpenedDocument
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the document has edits or not. Based on this either skip this dialog altogether or show it.
|
|
||||||
*/
|
|
||||||
checkForCloseOpenedDocument () {
|
|
||||||
const input = this.dialogDocument
|
|
||||||
if (input?.hasEdits) {
|
|
||||||
if (this.SGET_getDialogsState) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.SSET_setDialogState(true)
|
|
||||||
this.dialogModel = true
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.closeDocument(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the document and removes it from the list
|
|
||||||
*/
|
|
||||||
closeDocument (input: I_OpenedDocument) {
|
|
||||||
const dataPass = { doc: input, treeAction: false }
|
|
||||||
this.SSET_removeOpenedDocument(dataPass)
|
|
||||||
|
|
||||||
this.dialogModel = false
|
|
||||||
this.SSET_setDialogState(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.documentCloseDialog {
|
|
||||||
min-width: 600px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,121 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-dialog
|
|
||||||
v-model="dialogModel"
|
|
||||||
@before-hide="triggerDialogClose"
|
|
||||||
>
|
|
||||||
<q-card dark class="documentCloseDialog">
|
|
||||||
<q-card-section class="row justify-center">
|
|
||||||
<h6 class="text-center q-my-sm">Delete <span class="text-primary">{{retrieveFieldValue(currentDocument, 'name')}}</span>?</h6>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section class="row justify-center q-mx-xl">
|
|
||||||
<div>
|
|
||||||
The document will be deleted <span class="text-bold text-secondary">FOREVER</span> with no way to retrieve it.
|
|
||||||
<br>
|
|
||||||
<span class="text-caption">(unless a previous export of the project exists from earlier time that cointains it)</span>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
Proceed?
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="around" class="q-mx-xl q-mt-lg q-mb-md">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
label="Cancel"
|
|
||||||
color="accent"
|
|
||||||
v-close-popup />
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
:disable="!retrieveFieldValue(currentDocument, 'name')"
|
|
||||||
label="Delete document"
|
|
||||||
color="secondary"
|
|
||||||
@click="deleteDocument()" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component, Watch, Prop } from "vue-property-decorator"
|
|
||||||
|
|
||||||
import DialogBase from "src/components/dialogs/_DialogBase"
|
|
||||||
import { I_ShortenedDocument } from "src/interfaces/I_OpenedDocument"
|
|
||||||
import PouchDB from "pouchdb"
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: { }
|
|
||||||
})
|
|
||||||
export default class DeleteDocumentCheckDialog extends DialogBase {
|
|
||||||
/**
|
|
||||||
* React to dialog opening request
|
|
||||||
*/
|
|
||||||
@Watch("dialogTrigger")
|
|
||||||
openDialog (val: string|false) {
|
|
||||||
if (val && (this.SGET_allOpenedDocuments.docs.length > 0 || (this.documentType.length > 0 && this.documentId.length > 0))) {
|
|
||||||
if (this.SGET_getDialogsState) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.SSET_setDialogState(true)
|
|
||||||
this.dialogModel = true
|
|
||||||
|
|
||||||
const documentID = (this.documentId.length > 0) ? this.documentId : this.$route.params.id
|
|
||||||
this.currentDocument = this.SGET_document(documentID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OPTIONAL
|
|
||||||
* Type of the document to delete
|
|
||||||
*/
|
|
||||||
@Prop({ default: "" }) readonly documentType!: ""
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OPTIONAL
|
|
||||||
* ID of the document to delete
|
|
||||||
*/
|
|
||||||
@Prop({ default: "" }) readonly documentId!: ""
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current document for deletion
|
|
||||||
*/
|
|
||||||
currentDocument = false as unknown as I_ShortenedDocument
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the document
|
|
||||||
*/
|
|
||||||
async deleteDocument () {
|
|
||||||
const documentID = (this.documentId.length > 0) ? this.documentId : this.$route.params.id
|
|
||||||
|
|
||||||
const documentType = (this.documentType.length > 0) ? this.documentType : this.$route.params.type
|
|
||||||
window.FA_dbs[documentType] = new PouchDB(documentType)
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
this.currentDocument = await window.FA_dbs[documentType].get(documentID)
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
await window.FA_dbs[documentType].remove(this.currentDocument)
|
|
||||||
|
|
||||||
const dataPass = { doc: this.currentDocument, treeAction: true }
|
|
||||||
|
|
||||||
this.dialogModel = false
|
|
||||||
this.SSET_setDialogState(false)
|
|
||||||
|
|
||||||
this.currentDocument = this.SGET_document(documentID)
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
this.SSET_removeOpenedDocument(dataPass)
|
|
||||||
// @ts-ignore
|
|
||||||
this.SSET_removeDocument({ doc: this.currentDocument })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.documentCloseDialog {
|
|
||||||
min-width: 600px;
|
|
||||||
}
|
|
||||||
</style>
|
|