Merge branch 'master' of github.com:Lissy93/dashy into DOCS/updates-to-docs

This commit is contained in:
Alicia Sykes 2021-07-25 14:21:14 +01:00
commit 4b312dfadb
33 changed files with 1687 additions and 227 deletions

18
.github/workflows/issue-translator.yml vendored Normal file
View File

@ -0,0 +1,18 @@
# Will translate any issues opened in foraign language, and add the English translation as a comment
name: 'Issue Translator'
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: tomsun28/issues-translate-action@v2.5
with:
BOT_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
BOT_LOGIN_NAME: liss-bot
IS_MODIFY_TITLE: true
CUSTOM_BOT_NOTE: It looks like this issue isn't in English - not a problem, here's the translation! 🇬🇧

19
.github/workflows/security-scanning.yml vendored Normal file
View File

@ -0,0 +1,19 @@
# Uses Snyk to check for potential vulnerabilities, then sends results to GH security tab
name: Check for vulnerabilities with Snyk
on: push
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@master
continue-on-error: true
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --sarif-file-output=snyk.sarif
- name: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: snyk.sarif

View File

@ -29,6 +29,7 @@
- Easy single-file YAML-based configuration, which can also be configured directly through the UI - Easy single-file YAML-based configuration, which can also be configured directly through the UI
- Small bundle size, fully responsive UI and PWA makes the app easy to use on any device - Small bundle size, fully responsive UI and PWA makes the app easy to use on any device
- Easy to setup with Docker, or on bare metal, or with 1-Click cloud deployment - Easy to setup with Docker, or on bare metal, or with 1-Click cloud deployment
- Multi-language support, with additional languages coming soon
- Plus lots more... - Plus lots more...
## Demo ⚡ ## Demo ⚡
@ -304,6 +305,21 @@ From the Settings Menu in Dashy, you can download, backup, edit and rest your co
--- ---
## Language Switching 🌎
Dashy has the ability to support multiple languages and locales. When available, you're language should be automatically detected and applied on load, based on your browser or systems settings. But you can also select a language through the UI, under Config --> Switch Language.
Alternatively, set you're language in the config file, under `appConfig.language`. The language must be specified as either a 2-digit [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (such as `en`), or where available, the 2-digit code followed by a region or dialect (e.g. `en-GB`).
#### Supported Languages
- 🇬🇧 **English**: `en`
#### Add your Language
I would love for Dashy to be available and comfortable to use for all, including non-native English speakers. If you speak another language, and have a few minutes to sapir, you're help with translating it would be very much appreciated.
There's not too much text to translate, and it's all located in [a single JSON file](https://github.com/Lissy93/dashy/tree/master/src/assets/locales), and you don't have to translate it all, as any missing items will just fallback to English. For more info, see the [Development Guides Docs](https://github.com/Lissy93/dashy/blob/master/docs/development-guides.md#adding-translations), and feel free to reach out if you need any support.
---
## Sections & Items 🗃️ ## Sections & Items 🗃️
Dashy is made up of a series of sections, each containing a series of items. Dashy is made up of a series of sections, each containing a series of items.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 818 KiB

After

Width:  |  Height:  |  Size: 838 KiB

View File

@ -54,6 +54,7 @@ To disallow any changes from being written to disk via the UI config editor, set
**Field** | **Type** | **Required**| **Description** **Field** | **Type** | **Required**| **Description**
--- | --- | --- | --- --- | --- | --- | ---
**`language`** | `string` | _Optional_ | The 2 (or 4-digit) [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language, e.g. `en` or `en-GB`. This must be a language that the app has already been [translated](https://github.com/Lissy93/dashy/tree/master/src/assets/locales) into. If your language is unavailable, Dashy will fallback to English. By default Dashy will attempt to auto-detect your language, although this may not work on some privacy browsers.
**`statusCheck`** | `boolean` | _Optional_ | When set to `true`, Dashy will ping each of your services and display their status as a dot next to each item. This can be overridden by setting `statusCheck` under each item. Defaults to `false` **`statusCheck`** | `boolean` | _Optional_ | When set to `true`, Dashy will ping each of your services and display their status as a dot next to each item. This can be overridden by setting `statusCheck` under each item. Defaults to `false`
**`statusCheckInterval`** | `boolean` | _Optional_ | The number of seconds between checks. If set to `0` then service will only be checked on initial page load, which is usually the desired functionality. If value is less than `10` you may experience a hit in performance. Defaults to `0` **`statusCheckInterval`** | `boolean` | _Optional_ | The number of seconds between checks. If set to `0` then service will only be checked on initial page load, which is usually the desired functionality. If value is less than `10` you may experience a hit in performance. Defaults to `0`
**`backgroundImg`** | `string` | _Optional_ | Path to an optional full-screen app background image. This can be either remote (http) or local (/). Note that this will slow down initial load **`backgroundImg`** | `string` | _Optional_ | Path to an optional full-screen app background image. This can be either remote (http) or local (/). Note that this will slow down initial load

View File

@ -1,22 +1,22 @@
## Developing # Developing
This article outlines how to get Dashy running in a development environment, and outlines the basics of the architecture. This article outlines how to get Dashy running in a development environment, and outlines the basics of the architecture.
If you're adding new features, you may want to check out the [Development Guides](./docs/development-guides.md) docs, for tutorials covering basic tasks.
- [Setting up the Development Environment](#setting-up-the-dev-environment) - [Setting up the Development Environment](#setting-up-the-dev-environment)
- [Resources for Beginners](#resources-for-beginners) - [Resources for Beginners](#resources-for-beginners)
- [Style Guide](#style-guide) - [Style Guide](#style-guide)
- [Frontend Components](#frontend-components) - [Frontend Components](#frontend-components)
- [Common Tasks](#common-tasks)
- [Project Structure](#directory-structure) - [Project Structure](#directory-structure)
- [Dependencies and Packages](#dependencies-and-packages) - [Dependencies and Packages](#dependencies-and-packages)
### Setting up the Dev Environment ## Setting up the Dev Environment
#### Prerequisites ### Prerequisites
You will need either the latest or LTS version of **[Node.js](https://nodejs.org/)** to build and serve the application and **[Git](https://git-scm.com/downloads)** to easily fetch the code, and push any changes. If you plan on running or deploying the container, you'll also need **[Docker](https://docs.docker.com/get-docker/)**. To avoid any unexpected issues, ensure you've got at least **[NPM](https://www.npmjs.com/get-npm)** V 7.5 or **[Yarn](https://classic.yarnpkg.com/en/docs/install/#windows-stable)** 1.22 (you may find [NVM](https://github.com/nvm-sh/nvm) helpful for switching/ managing versions). You will need either the latest or LTS version of **[Node.js](https://nodejs.org/)** to build and serve the application and **[Git](https://git-scm.com/downloads)** to easily fetch the code, and push any changes. If you plan on running or deploying the container, you'll also need **[Docker](https://docs.docker.com/get-docker/)**. To avoid any unexpected issues, ensure you've got at least **[NPM](https://www.npmjs.com/get-npm)** V 7.5 or **[Yarn](https://classic.yarnpkg.com/en/docs/install/#windows-stable)** 1.22 (you may find [NVM](https://github.com/nvm-sh/nvm) helpful for switching/ managing versions).
#### Running the Project ### Running the Project
1. Get Code: `git clone git@github.com:Lissy93/dashy.git` 1. Get Code: `git clone git@github.com:Lissy93/dashy.git`
2. Navigate into the directory: `cd dashy` 2. Navigate into the directory: `cd dashy`
@ -25,7 +25,7 @@ You will need either the latest or LTS version of **[Node.js](https://nodejs.org
Dashy should now be being served on http://localhost:8080/. Hot reload is enabled, so making changes to any of the files will trigger them to be rebuilt and the page refreshed. Dashy should now be being served on http://localhost:8080/. Hot reload is enabled, so making changes to any of the files will trigger them to be rebuilt and the page refreshed.
#### Project Commands ### Project Commands
- `yarn dev` - Starts the development server with hot reloading - `yarn dev` - Starts the development server with hot reloading
- `yarn build` - Builds the project for production, and outputs it into `./dist` - `yarn build` - Builds the project for production, and outputs it into `./dist`
@ -47,7 +47,7 @@ Note:
- If you are using NPM, replace `yarn` with `npm run` - If you are using NPM, replace `yarn` with `npm run`
- If you are using Docker, precede each command with `docker exec -it [container-id]`. Container ID can be found by running `docker ps` - If you are using Docker, precede each command with `docker exec -it [container-id]`. Container ID can be found by running `docker ps`
### Environmental Variables ## Environmental Variables
- `PORT` - The port in which the application will run (defaults to `4000` for the Node.js server, and `80` within the Docker container) - `PORT` - The port in which the application will run (defaults to `4000` for the Node.js server, and `80` within the Docker container)
- `NODE_ENV` - Which environment to use, either `production`, `development` or `test` - `NODE_ENV` - Which environment to use, either `production`, `development` or `test`
- `VUE_APP_DOMAIN` - The URL where Dashy is going to be accessible from. This should include the protocol, hostname and (if not 80 or 443), then the port too, e.g. `https://localhost:3000`, `http://192.168.1.2:4002` or `https://dashy.mydomain.com` - `VUE_APP_DOMAIN` - The URL where Dashy is going to be accessible from. This should include the protocol, hostname and (if not 80 or 443), then the port too, e.g. `https://localhost:3000`, `http://192.168.1.2:4002` or `https://dashy.mydomain.com`
@ -58,7 +58,7 @@ If you do add new variables, ensure that there is always a fallback (define it i
Any environmental variables used by the frontend are preceded with `VUE_APP_`. Vue will merge the contents of your `.env` file into the app in a similar way to the ['dotenv'](https://github.com/motdotla/dotenv) package, where any variables that you set on your system will always take preference over the contents of any `.env` file. Any environmental variables used by the frontend are preceded with `VUE_APP_`. Vue will merge the contents of your `.env` file into the app in a similar way to the ['dotenv'](https://github.com/motdotla/dotenv) package, where any variables that you set on your system will always take preference over the contents of any `.env` file.
### Environment Modes ## Environment Modes
Both the Node app and Vue app supports several environments: `production`, `development` and `test`. You can set the environment using the `NODE_ENV` variable (either with your OS, in the Docker script or in an `.env` file - see [Environmental Variables](#environmental-variables) above). Both the Node app and Vue app supports several environments: `production`, `development` and `test`. You can set the environment using the `NODE_ENV` variable (either with your OS, in the Docker script or in an `.env` file - see [Environmental Variables](#environmental-variables) above).
The production environment will build the app in full, minifying and streamlining all assets. This means that building takes longer, but the app will then run faster. Whereas the dev environment creates a webpack configuration which enables HMR, doesn't hash assets or create vendor bundles in order to allow for fast re-builds when running a dev server. It supports sourcemaps and other debugging tools, re-compiles and reloads quickly but is not optimized, and so the app will not be as snappy as it could be. The test environment is intended for test running servers, it ignores assets that aren't needed for testing, and focuses on running all the E2E, regression and unit tests. For more information, see [Vue CLI Environment Modes](https://cli.vuejs.org/guide/mode-and-env.html#modes). The production environment will build the app in full, minifying and streamlining all assets. This means that building takes longer, but the app will then run faster. Whereas the dev environment creates a webpack configuration which enables HMR, doesn't hash assets or create vendor bundles in order to allow for fast re-builds when running a dev server. It supports sourcemaps and other debugging tools, re-compiles and reloads quickly but is not optimized, and so the app will not be as snappy as it could be. The test environment is intended for test running servers, it ignores assets that aren't needed for testing, and focuses on running all the E2E, regression and unit tests. For more information, see [Vue CLI Environment Modes](https://cli.vuejs.org/guide/mode-and-env.html#modes).
@ -67,7 +67,7 @@ By default:
- `production` is used by `yarn build` (or `vue-cli-service build`) and `yarn build-and-start` and `yarn pm2-start` - `production` is used by `yarn build` (or `vue-cli-service build`) and `yarn build-and-start` and `yarn pm2-start`
- `development` is used by `yarn dev` (or `vue-cli-service serve`) - `development` is used by `yarn dev` (or `vue-cli-service serve`)
- `test` is used by `yarn test` (or `vue-cli-service test:unit`) - `test` is used by `yarn test` (or `vue-cli-service test:unit`)
### Resources for Beginners ## Resources for Beginners
New to Web Development? Glad you're here! Dashy is a pretty simple app, so it should make a good candidate for your first PR. Presuming that you already have a basic knowledge of JavaScript, the following articles should point you in the right direction for getting up to speed with the technologies used in this project: New to Web Development? Glad you're here! Dashy is a pretty simple app, so it should make a good candidate for your first PR. Presuming that you already have a basic knowledge of JavaScript, the following articles should point you in the right direction for getting up to speed with the technologies used in this project:
- [Introduction to Vue.js](https://v3.vuejs.org/guide/introduction.html) - [Introduction to Vue.js](https://v3.vuejs.org/guide/introduction.html)
- [Vue.js Walkthrough](https://www.taniarascia.com/getting-started-with-vue/) - [Vue.js Walkthrough](https://www.taniarascia.com/getting-started-with-vue/)
@ -82,7 +82,7 @@ New to Web Development? Glad you're here! Dashy is a pretty simple app, so it sh
As well as Node, Git and Docker- you'll also need an IDE (e.g. [VS Code](https://code.visualstudio.com/) or [Vim](https://www.vim.org/)) and a terminal (Windows users may find [WSL](https://docs.microsoft.com/en-us/windows/wsl/) more convenient). As well as Node, Git and Docker- you'll also need an IDE (e.g. [VS Code](https://code.visualstudio.com/) or [Vim](https://www.vim.org/)) and a terminal (Windows users may find [WSL](https://docs.microsoft.com/en-us/windows/wsl/) more convenient).
### Style Guide ## Style Guide
Linting is done using [ESLint](https://eslint.org/), and using the [Vue.js Styleguide](https://github.com/vuejs/eslint-config-standard), which is very similar to the [AirBnB Stylguide](https://github.com/airbnb/javascript). You can run `yarn lint` to report and fix issues. While the dev server is running, issues will be reported to the console automatically. Any lint errors will trigger the build to fail. Note that all lint checks must pass before any PR can be merged. Linting is also run as a git pre-commit hook Linting is done using [ESLint](https://eslint.org/), and using the [Vue.js Styleguide](https://github.com/vuejs/eslint-config-standard), which is very similar to the [AirBnB Stylguide](https://github.com/airbnb/javascript). You can run `yarn lint` to report and fix issues. While the dev server is running, issues will be reported to the console automatically. Any lint errors will trigger the build to fail. Note that all lint checks must pass before any PR can be merged. Linting is also run as a git pre-commit hook
@ -99,7 +99,7 @@ The most significant things to note are:
For the full styleguide, see: [github.com/airbnb/javascript](https://github.com/airbnb/javascript) For the full styleguide, see: [github.com/airbnb/javascript](https://github.com/airbnb/javascript)
### Frontend Components ## Frontend Components
All frontend code is located in the `./src` directory, which is split into 5 sub-folders: All frontend code is located in the `./src` directory, which is split into 5 sub-folders:
- Components - All frontend web components are located here. Each component should have a distinct, well defined and simple task, and ideally should not be too long. The components directory is organised into a series of sub-directories, representing a specific area of the application - Components - All frontend web components are located here. Each component should have a distinct, well defined and simple task, and ideally should not be too long. The components directory is organised into a series of sub-directories, representing a specific area of the application
@ -116,37 +116,21 @@ The structure of the components directory is similar to that of the frontend app
<p align="center"><img src="https://i.ibb.co/wJCt0Lq/dashy-page-structure.png" width="600"/></p> <p align="center"><img src="https://i.ibb.co/wJCt0Lq/dashy-page-structure.png" width="600"/></p>
### Common Tasks ### Updating Dependencies
#### Creating a new theme
See [Theming](./theming.md)
#### Adding a new option in the config file
All application config is specified in `./public/conf.yml` - see [Configuring Docs](./configuring.md) for info. Before adding a new option in the config file, first ensure that there is nothing similar available, that is is definitely necessary, it will not conflict with any other options and most importantly that it will not cause any breaking changes. Ensure that you choose an appropriate and relevant section to place it under.
Checklist:
- Update the [Schema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.js) with the parameters for your new option
- Set a default value (if required) within [`defaults.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/defaults.js)
- Document the new value in [`configuring.md`](./configuring.md)
- Test that the reading of the new attribute is properly handled, and will not cause any errors when it is missing or populated with an unexpected value
#### Updating Dependencies
Running `yarn upgrade` will updated all dependencies based on the ranges specified in the `package.json`. The `yarn.lock` file will be updated, as will the contents of `./node_modules`, for more info, see the [yarn upgrade documentation](https://classic.yarnpkg.com/en/docs/cli/upgrade/). It is important to thoroughly test after any big dependency updates. Running `yarn upgrade` will updated all dependencies based on the ranges specified in the `package.json`. The `yarn.lock` file will be updated, as will the contents of `./node_modules`, for more info, see the [yarn upgrade documentation](https://classic.yarnpkg.com/en/docs/cli/upgrade/). It is important to thoroughly test after any big dependency updates.
### Development Tools ## Development Tools
#### Performance - Lighthouse ### Performance - Lighthouse
The easiest method of checking performance is to use Chromium's build in auditing tool, Lighthouse. To run the test, open Developer Tools (usually F12) --> Lighthouse and click on the 'Generate Report' button at the bottom. The easiest method of checking performance is to use Chromium's build in auditing tool, Lighthouse. To run the test, open Developer Tools (usually F12) --> Lighthouse and click on the 'Generate Report' button at the bottom.
#### Dependencies - BundlePhobia ### Dependencies - BundlePhobia
[BundlePhobia](https://bundlephobia.com/) is a really useful app that lets you analyze the cost of adding any particular dependency to an application [BundlePhobia](https://bundlephobia.com/) is a really useful app that lets you analyze the cost of adding any particular dependency to an application
### Directory Structure ## Directory Structure
#### Files in the Root: `./` ### Files in the Root: `./`
``` ```
├── package.json # Project meta-data, dependencies and paths to scripts ├── package.json # Project meta-data, dependencies and paths to scripts
@ -163,7 +147,7 @@ The easiest method of checking performance is to use Chromium's build in auditin
``` ```
#### Frontend Source: `./src/` ### Frontend Source: `./src/`
``` ```
./src ./src
@ -219,17 +203,17 @@ The easiest method of checking performance is to use Chromium's build in auditin
``` ```
--- ---
### Dependencies and Packages ## Dependencies and Packages
During development I made the conscious decision to not reinvent the wheel if not necessary. It is often really tempting to try an build everything yourself, but sometimes it's just not practical. Often there's packages out there, developed by amazing individuals which are probably built better than I could have done. That being said, I have looked through the code of most these dependencies, to verify that they are both legitimate and efficient. During development I made the conscious decision to not reinvent the wheel if not necessary. It is often really tempting to try an build everything yourself, but sometimes it's just not practical. Often there's packages out there, developed by amazing individuals which are probably built better than I could have done. That being said, I have looked through the code of most these dependencies, to verify that they are both legitimate and efficient.
The following packages are used. Full credit, and massive kudos to each of their authors. The following packages are used. Full credit, and massive kudos to each of their authors.
#### Core ### Core
At it's core, the application uses [Vue.js](https://github.com/vuejs/vue), as well as it's services. Styling is done with [SCSS](https://github.com/sass/sass), JavaScript is currently [Babel](https://github.com/babel/babel), (but I am in the process of converting to [TypeScript](https://github.com/Microsoft/TypeScript)), linting is done with [ESLint](https://github.com/eslint/eslint), the config is defined in [YAML](https://github.com/yaml/yaml), and there is a simple [Node.js](https://github.com/nodejs/node) server to serve up the static app. At it's core, the application uses [Vue.js](https://github.com/vuejs/vue), as well as it's services. Styling is done with [SCSS](https://github.com/sass/sass), JavaScript is currently [Babel](https://github.com/babel/babel), (but I am in the process of converting to [TypeScript](https://github.com/Microsoft/TypeScript)), linting is done with [ESLint](https://github.com/eslint/eslint), the config is defined in [YAML](https://github.com/yaml/yaml), and there is a simple [Node.js](https://github.com/nodejs/node) server to serve up the static app.
#### Frontend Components ### Frontend Components
- [`vue-select`](https://github.com/sagalbot/vue-select) - Dropdown component by @sagalbot `MIT` - [`vue-select`](https://github.com/sagalbot/vue-select) - Dropdown component by @sagalbot `MIT`
- [`vue-js-modal`](https://github.com/euvl/vue-js-modal) - Modal component by @euvl `MIT` - [`vue-js-modal`](https://github.com/euvl/vue-js-modal) - Modal component by @euvl `MIT`
@ -241,23 +225,23 @@ At it's core, the application uses [Vue.js](https://github.com/vuejs/vue), as we
- [`vue-prism-editor`](https://github.com/koca/vue-prism-editor) - Lightweight code editor by @koca `MIT` - [`vue-prism-editor`](https://github.com/koca/vue-prism-editor) - Lightweight code editor by @koca `MIT`
- Forked from [`prism.js`](https://github.com/PrismJS/prism) `MIT` - Forked from [`prism.js`](https://github.com/PrismJS/prism) `MIT`
#### Utilities ### Utilities
- [`crypto-js`](https://github.com/brix/crypto-js) - Encryption implementations by @evanvosberg and community `MIT` - [`crypto-js`](https://github.com/brix/crypto-js) - Encryption implementations by @evanvosberg and community `MIT`
- [`axios`](https://github.com/axios/axios) - Promise based HTTP client by @mzabriskie and community `MIT` - [`axios`](https://github.com/axios/axios) - Promise based HTTP client by @mzabriskie and community `MIT`
- [`ajv`](https://github.com/ajv-validator/ajv) - JSON schema Validator by @epoberezkin and community `MIT` - [`ajv`](https://github.com/ajv-validator/ajv) - JSON schema Validator by @epoberezkin and community `MIT`
#### Server ### Server
- [`connect`](https://github.com/senchalabs/connect) - Minimilistic middleware layer for chaining together Node.js requests handled by the server file `MIT` - [`connect`](https://github.com/senchalabs/connect) - Minimilistic middleware layer for chaining together Node.js requests handled by the server file `MIT`
- [`serve-static`](https://github.com/expressjs/serve-static) - Lightweight static Node file server `MIT` - [`serve-static`](https://github.com/expressjs/serve-static) - Lightweight static Node file server `MIT`
##### External Services #### External Services
The 1-Click deploy demo uses [Play-with-Docker Labs](https://play-with-docker.com/). Code is hosted on [GitHub](https://github.com), Docker image is hosted on [DockerHub](https://hub.docker.com/), and the demos are hosted on [Netlify](https://www.netlify.com/). The 1-Click deploy demo uses [Play-with-Docker Labs](https://play-with-docker.com/). Code is hosted on [GitHub](https://github.com), Docker image is hosted on [DockerHub](https://hub.docker.com/), and the demos are hosted on [Netlify](https://www.netlify.com/).
### Notes ## Notes
#### Known Warnings ### Known Warnings
When running the build command, several warnings appear. These are not errors, and do not affect the security or performance of the application. They will be addressed in a future update When running the build command, several warnings appear. These are not errors, and do not affect the security or performance of the application. They will be addressed in a future update

113
docs/development-guides.md Normal file
View File

@ -0,0 +1,113 @@
# Development Guides
A series of short tutorials, to guide you through the most common development tasks.
Sections:
- [Creating a new theme](#creating-a-new-theme)
- [Adding Translations](#adding-translations)
- [Adding a new option in the config file](#adding-a-new-option-in-the-config-file)
## Creating a new theme
See [Theming](./theming.md)
## Adding Translations
Dashy is using [vue-i18n](https://vue-i18n.intlify.dev/guide/) to manage multi-language support.
Adding a new language is pretty straightforward, with just three steps:
##### 1. Create a new Language File
Create a new JSON file in `./src/assets/locales` name is a 2-digit [ISO-639 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language, E.g. for German `de.json`, French `fr.json` or Spanish `es.json` - You can find a list of all ISO codes at [iso.org](https://www.iso.org/obp/ui).
If your language is a specific dialect or regional language, then use the Posfix [CLDR](http://cldr.unicode.org/) format, where, e.g. `en-GB.json` (British), `es-MX.json` (Spanish, in Mexico) or `zh-CN.json` (Chinese, simplified) - A list of which can be found [here](https://github.com/unicode-org/cldr-json/blob/master/cldr-json/cldr-core/availableLocales.json)
##### 2. Translate!
Using [`en.json`](https://github.com/Lissy93/dashy/tree/master/src/assets/locales/en.json) as an example, translate the JSON values to your language, while leaving the keys as they are. It's fine to leave out certain items, as if they're missing they will fall-back to English. If you see any attribute which include curly braces (`{xxx}`), then leave the inner value of these braces as is, as this is for variables.
```json
{
"theme-maker": {
"export-button": "Benutzerdefinierte Variablen exportieren",
"reset-button": "Stile zurücksetzen für",
"show-all-button": "Alle Variablen anzeigen",
"save-button": "Speichern",
"cancel-button": "Abbrechen",
"saved-toast": "{theme} Erfolgreich aktualisiert",
"reset-toast": "Benutzerdefinierte Farben für {theme} entfernt"
},
}
```
##### 3. Add your file to the app
In [`./src/utils/languages.js`](https://github.com/Lissy93/dashy/tree/master/src/utils/languages.js), you need to do 2 small things:
First import your new translation file, do this at the top of the page.
E.g. `import de from '@/assets/locales/de.json';`
Second, add it to the array of languages, e.g:
```javascript
export const languages = [
{
name: 'English',
code: 'en',
locale: en,
flag: '🇬🇧',
},
{
name: 'German', // The name of your language
code: 'de', // The ISO code of your language
locale: de, // The name of the file you imported (no quotes)
flag: '🇩🇪', // An optional flag emoji
},
];
```
You can also add your new language to the readme, under the [Language Switching](https://github.com/Lissy93/dashy#language-switching-) section and optionally include your name/ username if you'd like to be credited for your work. Done!
If you are not comfortable with making pull requests, or do not want to modify the code, then feel free to instead send the translated file to me, and I can add it into the application. I will be sure to credit you appropriately.
# Adding a new option in the config file
This section is for, if you're adding a new component or setting, that requires an additional item to be added to the users config file.
All of the users config is specified in `./public/conf.yml` - see [Configuring Docs](./configuring.md) for info.
Before adding a new option in the config file, first ensure that there is nothing similar available, that is is definitely necessary, it will not conflict with any other options and most importantly that it will not cause any breaking changes. Ensure that you choose an appropriate and relevant section to place it under.
Next decide the most appropriate place for your attribute:
- Application settings should be located under `appConfig`
- Page info (such as text and metadata) should be under `pageInfo`
- Data relating to specific sections should be under `section[n].displayData`
- And for setting applied to specific items, it should be under `item[n]`
In order for the user to be able to add your new attribute using the Config Editor, and for the build validation to pass, your attribute must be included within the [ConfigSchema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.js). You can read about how to do this on the [ajv docs](https://ajv.js.org/json-schema.html). Give your property a type and a description, as well as any other optional fields that you feel are relevant. For example:
```json
"fontAwesomeKey": {
"type": "string",
"pattern": "^[a-z0-9]{10}$",
"description": "API key for font-awesome",
"example": "0821c65656"
}
```
or
```json
"iconSize": {
"enum": [ "small", "medium", "large" ],
"default": "medium",
"description": "The size of each link item / icon"
}
```
Next, if you're property should have a default value, then add it to [`defaults.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/defaults.js). This ensures that nothing will break if the user does not use your property, and having all defaults together keeps things organised and easy to manage.
If your property needs additional logic for fetching, setting or processing, then you can add a helper function within [`ConfigHelpers.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigHelpers.js).
Finally, add your new property to the [`configuring.md`](./configuring.md) API docs. Put it under the relevant section, and be sure to include field name, data type, a description and mention that it is optional. If your new feature needs more explaining, then you can also document it under the relevant section elsewhere in the documentation.
Checklist:
- [] Ensure the new attribute is actually necessary, and nothing similar already exists
- [] Update the [Schema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.js) with the parameters for your new option
- [] Set a default value (if required) within [`defaults.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/defaults.js)
- [] Document the new value in [`configuring.md`](./configuring.md)
- [] Test that the reading of the new attribute is properly handled, and will not cause any errors when it is missing or populated with an unexpected value

View File

@ -1,6 +1,6 @@
{ {
"name": "Dashy", "name": "Dashy",
"version": "1.4.1", "version": "1.4.2",
"license": "MIT", "license": "MIT",
"main": "server", "main": "server",
"scripts": { "scripts": {
@ -31,6 +31,7 @@
"v-tooltip": "^2.1.3", "v-tooltip": "^2.1.3",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-cli-plugin-yaml": "^1.0.2", "vue-cli-plugin-yaml": "^1.0.2",
"vue-i18n": "^8.25.0",
"vue-js-modal": "^2.0.0-rc.6", "vue-js-modal": "^2.0.0-rc.6",
"vue-material-tabs": "^0.1.2", "vue-material-tabs": "^0.1.2",
"vue-prism-editor": "^1.2.2", "vue-prism-editor": "^1.2.2",
@ -40,6 +41,7 @@
"vue-toasted": "^1.1.28" "vue-toasted": "^1.1.28"
}, },
"devDependencies": { "devDependencies": {
"@architect/sandbox": "^3.7.4",
"@vue/cli-plugin-babel": "^4.5.12", "@vue/cli-plugin-babel": "^4.5.12",
"@vue/cli-plugin-eslint": "^4.5.12", "@vue/cli-plugin-eslint": "^4.5.12",
"@vue/cli-plugin-pwa": "^4.5.12", "@vue/cli-plugin-pwa": "^4.5.12",

View File

@ -17,6 +17,7 @@ import {
localStorageKeys, localStorageKeys,
splashScreenTime, splashScreenTime,
visibleComponents as defaultVisibleComponents, visibleComponents as defaultVisibleComponents,
language as defaultLanguage,
} from '@/utils/defaults'; } from '@/utils/defaults';
const Accumulator = new ConfigAccumulator(); const Accumulator = new ConfigAccumulator();
@ -70,9 +71,33 @@ export default {
this.isLoading = false; this.isLoading = false;
} }
}, },
/* Checks local storage, then appConfig, and if a custom language is specified, its applied */
applyLanguage() {
let language = defaultLanguage; // Language to apply
const availibleLocales = this.$i18n.availableLocales; // All available locales
// If user has specified a language, locally or in config, then check and apply it
const usersLang = localStorage[localStorageKeys.LANGUAGE] || this.appConfig.language;
if (usersLang && availibleLocales.includes(usersLang)) {
language = usersLang;
} else {
// Otherwise, attempt to apply language automatically, based on their system language
const usersBorwserLang1 = window.navigator.language || ''; // e.g. en-GB or or ''
const usersBorwserLang2 = usersBorwserLang1.split('-')[0]; // e.g. en or undefined
if (availibleLocales.includes(usersBorwserLang1)) {
language = usersBorwserLang1;
} else if (availibleLocales.includes(usersBorwserLang2)) {
language = usersBorwserLang2;
}
}
// Apply Language
this.$i18n.locale = language;
document.getElementsByTagName('html')[0].setAttribute('lang', language);
},
}, },
/* When component mounted, hide splash and initiate the injection of custom styles */ /* When component mounted, hide splash and initiate the injection of custom styles */
mounted() { mounted() {
this.applyLanguage();
this.hideSplash(); this.hideSplash();
if (this.appConfig.customCss) { if (this.appConfig.customCss) {
const cleanedCss = this.appConfig.customCss.replace(/<\/?[^>]+(>|$)/g, ''); const cleanedCss = this.appConfig.customCss.replace(/<\/?[^>]+(>|$)/g, '');

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="language" class="svg-inline--fa fa-language fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M160.3 203.8h-.5s-4.3 20.9-7.8 33l-11 37.3h37.9l-10.7-37.3c-3.6-12.1-7.9-33-7.9-33zM616 96H24c-13.3 0-24 10.7-24 24v272c0 13.3 10.7 24 24 24h592c13.3 0 24-10.7 24-24V120c0-13.3-10.7-24-24-24zM233.2 352h-22.6a12 12 0 0 1-11.5-8.6l-9.3-31.7h-59.9l-9.1 31.6c-1.5 5.1-6.2 8.7-11.5 8.7H86.8c-8.2 0-14-8.1-11.4-15.9l57.1-168c1.7-4.9 6.2-8.1 11.4-8.1h32.2c5.1 0 9.7 3.3 11.4 8.1l57.1 168c2.6 7.8-3.2 15.9-11.4 15.9zM600 376H320V136h280zM372 228h110.8c-6.3 12.8-15.1 25.9-25.9 38.5-6.6-7.8-12.8-15.8-18.3-24-3.5-5.3-10.6-6.9-16.1-3.6l-13.7 8.2c-5.9 3.5-7.6 11.3-3.8 17 6.5 9.7 14.4 20.1 23.5 30.6-9 7.7-18.6 14.8-28.7 21.2-5.4 3.4-7.1 10.5-3.9 16l7.9 13.9c3.4 5.9 11 7.9 16.8 4.2 12.5-7.9 24.6-17 36-26.8 10.7 9.6 22.3 18.6 34.6 26.6 5.8 3.7 13.6 1.9 17-4.1l8-13.9c3.1-5.5 1.5-12.5-3.8-16-9.2-6-18.4-13.1-27.2-20.9 1.5-1.7 2.9-3.3 4.3-5 17.1-20.6 29.6-41.7 36.8-62H540c6.6 0 12-5.4 12-12v-16c0-6.6-5.4-12-12-12h-64v-16c0-6.6-5.4-12-12-12h-16c-6.6 0-12 5.4-12 12v16h-64c-6.6 0-12 5.4-12 12v16c0 6.7 5.4 12.1 12 12.1z"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

136
src/assets/locales/en.json Normal file
View File

@ -0,0 +1,136 @@
{
"home": {
"no-results": "No Search Results",
"no-data": "No Data Configured"
},
"search": {
"search-label": "Search",
"search-placeholder": "Start typing to filter",
"clear-search-tooltip": "Clear Search"
},
"login": {
"title": "Dashy",
"username-label": "Username",
"password-label": "Password",
"login-button": "Login",
"remember-me-label": "Remember me for",
"remember-me-never": "Never",
"remember-me-hour": "4 Hours",
"remember-me-day": "1 Day",
"remember-me-week": "1 Week"
},
"config": {
"main-tab": "Config",
"view-config-tab": "View Config",
"edit-config-tab": "Edit Config",
"custom-css-tab": "Custom Styles",
"heading": "Configuration Options",
"download-config-button": "Download Config",
"edit-config-button": "Edit Config",
"edit-css-button": "Edit Custom CSS",
"cloud-sync-button": "Enable Cloud Sync",
"edit-cloud-sync-button": "Edit Cloud Sync",
"rebuild-app-button": "Rebuild Application",
"change-language-button": "Change App Language",
"reset-settings-button": "Reset Local Settings",
"app-info-button": "App Info",
"app-version-note": "Dashy version",
"backup-note": "It is recommend to make a backup of your configuration before making changes.",
"reset-config-msg-l1": "This will remove all user settings from local storage, but won't effect your 'conf.yml' file.",
"reset-config-msg-l2": "You should first backup any changes you've made locally, if you want to use them in the future.",
"reset-config-msg-l3": "Are you sure you want to proceed?",
"data-cleared-msg": "Data cleared successfully",
"actions-label": "Actions",
"copy-config-label": "Copy Config",
"data-copied-msg": "Config has been copied to clipboard",
"reset-config-label": "Reset Config",
"css-save-btn": "Save Changes",
"css-note-label": "Note",
"css-note-l1": "You will need to refresh the page for your changes to take effect.",
"css-note-l2": "Styles overrides are only stored locally, so it is recommended to make a copy of your CSS.",
"css-note-l3": "To remove all custom styles, delete the contents and hit Save Changes"
},
"settings": {
"theme-label": "Theme",
"layout-label": "Layout",
"layout-auto": "Auto",
"layout-horizontal": "Horizontal",
"layout-vertical": "Vertical",
"item-size-label": "Item Size",
"item-size-small": "Small",
"item-size-medium": "Medium",
"item-size-large": "Large",
"config-launcher-label": "Config"
},
"language-switcher": {
"title": "Change Application Language",
"dropdown-label": "Select a Language",
"save-button": "Save",
"success-msg": "Language Updated to"
},
"theme-maker": {
"title": "Theme Configurator",
"export-button": "Export Custom Variables",
"reset-button": "Reset Styles for",
"show-all-button": "Show All Variables",
"save-button": "Save",
"cancel-button": "Cancel",
"saved-toast": "{theme} Updated Successfully",
"copied-toast": "Theme data for {theme} copied to clipboard",
"reset-toast": "Custom Colors for {theme} Removed"
},
"config-editor": {
"save-location-label": "Save Location",
"location-local-label": "Apply Locally",
"location-disk-label": "Write Changes to Config File",
"save-button": "Save Changes",
"valid-label": "Config is Valid",
"status-success-msg": "Task Complete",
"status-fail-msg": "Task Failed",
"success-msg-disk": "Config file written to disk successfully",
"success-msg-local": "Local changes saved successfully",
"success-note-l1": "The app should rebuild automatically.",
"success-note-l2": "This may take up to a minute.",
"success-note-l3": "You will need to refresh the page for changes to take effect.",
"error-msg-save-mode": "Please select a Save Mode: Local or File",
"error-msg-cannot-save": "An error occurred saving config",
"error-msg-bad-json": "Error in JSON, possibly malformed",
"warning-msg-validation": "Validation Warning"
},
"app-rebuild": {
"title": "Rebuild Application",
"rebuild-note-l1": "A rebuild is required for changes written to the conf.yml file to take effect.",
"rebuild-note-l2": "This should happen automatically, but if it hasn't, you can manually trigger it here.",
"rebuild-note-l3": "This is not required for modifications stored locally.",
"rebuild-button": "Start Build",
"rebuilding-status-1": "Building...",
"rebuilding-status-2": "This may take a few minutes",
"error-permission": "You do no have permission to trigger this action",
"success-msg": "Build completed succesfully",
"fail-msg": "Build operation failed",
"reload-note": "A page reload is now required for changes to take effect",
"reload-button": "Reload Page"
},
"cloud-sync": {
"title": "Cloud Backup & Restore",
"intro-l1": "Cloud backup and restore is an optional feature, that enables you to upload your config to the internet, and then restore it on any other device or instance of Dashy.",
"intro-l2": "All data is fully end-to-end encrypted with AES, using your password as the key.",
"intro-l3": "For more info, please see the",
"backup-title-setup": "Make a Backup",
"backup-title-update": "Update Backup",
"password-label-setup": "Choose a Password",
"password-label-update": "Enter your Password",
"backup-button-setup": "Backup",
"backup-button-update": "Update Backup",
"backup-id-label": "Your Backup ID",
"backup-id-note": "This is used to restore from backups later. So keep it, along with your password somewhere safe.",
"restore-title": "Restore a Backup",
"restore-id-label": "Restore ID",
"restore-password-label": "Password",
"restore-button": "Restore",
"backup-error-unknown": "Unable to process request",
"backup-error-password": "Incorrect password. Please enter your current password.",
"backup-success-msg": "Completed Successfully",
"restore-success-msg": "Config Restored Successfully"
}
}

View File

@ -1,55 +1,55 @@
<template> <template>
<div class="cloud-backup-restore-wrapper"> <div class="cloud-backup-restore-wrapper">
<div class="section intro"> <div class="section intro">
<h2>Cloud Backup & Restore</h2> <h2>{{ $t('cloud-sync.title') }}</h2>
<p class="intro"> <p class="intro">
Cloud backup and restore is an optional feature, that enables you to upload your {{ $t('cloud-sync.intro-l1') }}
config to the internet, and then restore it on any other device or instance of Dashy.
<br><br> <br><br>
All data is fully end-to-end encrypted with AES, using your password as the key. {{ $t('cloud-sync.intro-l2') }}
<br> <br>
For more info, please see the {{ $t('cloud-sync.intro-l3') }}
<a href="https://github.com/Lissy93/dashy/blob/master/docs/backup-restore.md">docs</a> <a href="https://github.com/Lissy93/dashy/blob/master/docs/backup-restore.md">docs</a>
</p> </p>
</div> </div>
<div class="section backup-section"> <div class="section backup-section">
<h3 v-if="backupId">Update Backup</h3> <h3 v-if="backupId">{{ $t('cloud-sync.backup-title-setup') }}</h3>
<h3 v-else>Make a Backup</h3> <h3 v-else>{{ $t('cloud-sync.backup-title-setup') }}</h3>
<Input <Input
v-model="backupPassword" v-model="backupPassword"
name="backup-password" name="backup-password"
:label="backupId ? 'Enter your Password' : 'Choose a Password'" :label="backupId
? $t('cloud-sync.password-label-update') : $t('cloud-sync.password-label-setup')"
layout="vertical" layout="vertical"
type="password" type="password"
/> />
<Button :click="checkPass"> <Button :click="checkPass">
<template v-slot:text>{{backupId ? 'Update Backup' : 'Backup'}}</template> <template v-slot:text>
{{backupId
? $t('cloud-sync.backup-button-update') : $t('cloud-sync.backup-button-setup')}}
</template>
<template v-slot:icon><IconBackup /></template> <template v-slot:icon><IconBackup /></template>
</Button> </Button>
<div class="results-view" v-if="backupId"> <div class="results-view" v-if="backupId">
<span class="backup-id-label">Your Backup ID: </span> <span class="backup-id-label">{{ $t('cloud-sync.backup-id-label') }}: </span>
<pre class="backup-id-value">{{ backupId }}</pre> <pre class="backup-id-value">{{ backupId }}</pre>
<span class="backup-id-note"> <span class="backup-id-note">{{ $t('cloud-sync.backup-id-note') }}</span>
This is used to restore from backups later.
So keep it, along with your password somewhere safe.
</span>
</div> </div>
</div> </div>
<div class="section restore-section"> <div class="section restore-section">
<h3>Restore a Backup</h3> <h3>{{ $t('cloud-sync.restore-title') }}</h3>
<Input <Input
v-model="restoreCode" v-model="restoreCode"
name="restore-code" name="restore-code"
label="Restore ID" :label="$t('cloud-sync.restore-id-label')"
/> />
<Input <Input
v-model="restorePassword" v-model="restorePassword"
name="restore-password" name="restore-password"
label="Password" :label="$t('cloud-sync.restore-password-label')"
type="password" type="password"
/> />
<Button :click="restoreBackup"> <Button :click="restoreBackup">
<template v-slot:text>Restore</template> <template v-slot:text>{{ $t('cloud-sync.restore-button') }}</template>
<template v-slot:icon><IconRestore /></template> <template v-slot:icon><IconRestore /></template>
</Button> </Button>
</div> </div>
@ -101,7 +101,7 @@ export default {
} else if (savedHash === this.makeHash(this.backupPassword)) { } else if (savedHash === this.makeHash(this.backupPassword)) {
this.makeUpdate(); this.makeUpdate();
} else { } else {
this.showErrorMsg('Incorrect password. Please enter your current password.'); this.showErrorMsg(this.$t('cloud-sync.backup-error-password'));
} }
}, },
makeBackup() { makeBackup() {
@ -113,7 +113,7 @@ export default {
this.updateUiAfterBackup(response.data.backupId, false); this.updateUiAfterBackup(response.data.backupId, false);
} }
}).catch(() => { }).catch(() => {
this.showErrorMsg('Unable to process request'); this.showErrorMsg(this.$t('cloud-sync.backup-error-unknown'));
}); });
}, },
makeUpdate() { makeUpdate() {
@ -125,7 +125,7 @@ export default {
this.updateUiAfterBackup(response.data.backupId, true); this.updateUiAfterBackup(response.data.backupId, true);
} }
}).catch(() => { }).catch(() => {
this.showErrorMsg('Unable to process request'); this.showErrorMsg(this.$t('cloud-sync.backup-error-unknown'));
}); });
}, },
restoreFromBackup(config, backupId) { restoreFromBackup(config, backupId) {
@ -136,12 +136,14 @@ export default {
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme); localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
} }
this.setBackupIdLocally(backupId, this.restorePassword); this.setBackupIdLocally(backupId, this.restorePassword);
this.showSuccessMsg('Config Restored Succesfully'); this.showSuccessMsg(this.$t('cloud-sync.restore-success-msg'));
setTimeout(() => { location.reload(); }, 1500); // eslint-disable-line no-restricted-globals setTimeout(() => { location.reload(); }, 1500); // eslint-disable-line no-restricted-globals
}, },
updateUiAfterBackup(backupId, isUpdate = false) { updateUiAfterBackup(backupId, isUpdate = false) {
this.setBackupIdLocally(backupId, this.backupPassword); this.setBackupIdLocally(backupId, this.backupPassword);
this.showSuccessMsg(`${isUpdate ? 'Update' : 'Backup'} Completed Succesfully`); this.showSuccessMsg(
`${isUpdate ? 'Update' : 'Backup'} ${this.$t('cloud-sync.backup-success-msg')}`,
);
this.backupPassword = ''; this.backupPassword = '';
}, },
showErrorMsg(errorMsg) { showErrorMsg(errorMsg) {

View File

@ -1,67 +1,74 @@
<template> <template>
<Tabs :navAuto="true" name="Add Item" ref="tabView"> <Tabs :navAuto="true" name="Add Item" ref="tabView">
<TabItem name="Config" class="main-tab"> <TabItem :name="$t('config.main-tab')" class="main-tab">
<div class="main-options-container"> <div class="main-options-container">
<h2>Configuration Options</h2> <h2>Configuration Options</h2>
<a class="hyperlink-wrapper" @click="downloadConfigFile('conf.yml', yaml)"> <a class="hyperlink-wrapper" @click="downloadConfigFile('conf.yml', yaml)">
<button class="config-button center"> <button class="config-button center">
<DownloadIcon class="button-icon"/> <DownloadIcon class="button-icon"/>
Download Config {{ $t('config.download-config-button') }}
</button> </button>
</a> </a>
<button class="config-button center" @click="() => navigateToTab(2)"> <button class="config-button center" @click="() => navigateToTab(2)">
<EditIcon class="button-icon"/> <EditIcon class="button-icon"/>
Edit Config {{ $t('config.edit-config-button') }}
</button> </button>
<button class="config-button center" @click="() => navigateToTab(3)"> <button class="config-button center" @click="() => navigateToTab(3)">
<CustomCssIcon class="button-icon"/> <CustomCssIcon class="button-icon"/>
Edit Custom CSS {{ $t('config.edit-css-button') }}
</button> </button>
<button class="config-button center" @click="openCloudSync()"> <button class="config-button center" @click="openCloudSync()">
<CloudIcon class="button-icon"/> <CloudIcon class="button-icon"/>
{{backupId ? 'Edit Cloud Sync' : 'Enable Cloud Sync'}} {{backupId ? $t('config.edit-cloud-sync-button') : $t('config.cloud-sync-button') }}
</button>
<button class="config-button center" @click="openLanguageSwitchModal()">
<LanguageIcon class="button-icon"/>
{{ $t('config.change-language-button') }}
</button> </button>
<button class="config-button center" @click="openRebuildAppModal()"> <button class="config-button center" @click="openRebuildAppModal()">
<RebuildIcon class="button-icon"/> <RebuildIcon class="button-icon"/>
Rebuild Application {{ $t('config.rebuild-app-button') }}
</button> </button>
<button class="config-button center" @click="resetLocalSettings()"> <button class="config-button center" @click="resetLocalSettings()">
<DeleteIcon class="button-icon"/> <DeleteIcon class="button-icon"/>
Reset Local Settings {{ $t('config.reset-settings-button') }}
</button> </button>
<button class="config-button center" @click="openAboutModal()"> <button class="config-button center" @click="openAboutModal()">
<IconAbout class="button-icon" /> <IconAbout class="button-icon" />
App Info {{ $t('config.app-info-button') }}
</button> </button>
<p class="small-screen-note" style="display: none;"> <p class="small-screen-note" style="display: none;">
You are using a very small screen, and some screens in this menu may not be optimal You are using a very small screen, and some screens in this menu may not be optimal
</p> </p>
<p class="app-version">Dashy version {{ appVersion }}</p> <p class="app-version">{{ $t('config.app-version-note') }} {{ appVersion }}</p>
<p class="language">{{ getLanguage() }}</p>
<div class="config-note"> <div class="config-note">
<span> <span>{{ $t('config.backup-note') }}</span>
It is recommend to make a backup of your conf.yml file before making changes.
</span>
</div> </div>
</div> </div>
<!-- Rebuild App Modal --> <!-- Rebuild App Modal -->
<RebuildApp /> <RebuildApp />
</TabItem> </TabItem>
<TabItem name="View Config" class="code-container"> <TabItem :name="$t('config.view-config-tab')" class="code-container">
<pre id="conf-yaml">{{yaml}}</pre> <pre id="conf-yaml">{{yaml}}</pre>
<div class="yaml-action-buttons"> <div class="yaml-action-buttons">
<h2>Actions</h2> <h2>{{ $t('config.actions-label') }}</h2>
<a class="yaml-button download" @click="downloadConfigFile('conf.yml', yaml)"> <a class="yaml-button download" @click="downloadConfigFile('conf.yml', yaml)">
Download Config {{ $t('config.download-config-button') }}
</a>
<a class="yaml-button copy" @click="copyConfigToClipboard()">
{{ $t('config.copy-config-label') }}
</a>
<a class="yaml-button reset" @click="resetLocalSettings()">
{{ $t('config.reset-config-label') }}
</a> </a>
<a class="yaml-button copy" @click="copyConfigToClipboard()">Copy Config</a>
<a class="yaml-button reset" @click="resetLocalSettings()">Reset Config</a>
</div> </div>
</TabItem> </TabItem>
<TabItem name="Edit Config"> <TabItem :name="$t('config.edit-config-tab')">
<JsonEditor :config="config" /> <JsonEditor :config="config" />
</TabItem> </TabItem>
<TabItem name="Custom Styles"> <TabItem :name="$t('config.custom-css-tab')">
<CustomCssEditor :config="config" initialCss="hello" /> <CustomCssEditor :config="config" />
</TabItem> </TabItem>
</Tabs> </Tabs>
</template> </template>
@ -74,6 +81,7 @@ import 'highlight.js/styles/mono-blue.css';
import JsonToYaml from '@/utils/JsonToYaml'; import JsonToYaml from '@/utils/JsonToYaml';
import { localStorageKeys, modalNames } from '@/utils/defaults'; import { localStorageKeys, modalNames } from '@/utils/defaults';
import { getUsersLanguage } from '@/utils/ConfigHelpers';
import JsonEditor from '@/components/Configuration/JsonEditor'; import JsonEditor from '@/components/Configuration/JsonEditor';
import CustomCssEditor from '@/components/Configuration/CustomCss'; import CustomCssEditor from '@/components/Configuration/CustomCss';
import RebuildApp from '@/components/Configuration/RebuildApp'; import RebuildApp from '@/components/Configuration/RebuildApp';
@ -84,6 +92,7 @@ import EditIcon from '@/assets/interface-icons/config-edit-json.svg';
import CustomCssIcon from '@/assets/interface-icons/config-custom-css.svg'; import CustomCssIcon from '@/assets/interface-icons/config-custom-css.svg';
import CloudIcon from '@/assets/interface-icons/cloud-backup-restore.svg'; import CloudIcon from '@/assets/interface-icons/cloud-backup-restore.svg';
import RebuildIcon from '@/assets/interface-icons/application-rebuild.svg'; import RebuildIcon from '@/assets/interface-icons/application-rebuild.svg';
import LanguageIcon from '@/assets/interface-icons/config-language.svg';
import IconAbout from '@/assets/interface-icons/application-about.svg'; import IconAbout from '@/assets/interface-icons/application-about.svg';
export default { export default {
@ -115,6 +124,7 @@ export default {
EditIcon, EditIcon,
CloudIcon, CloudIcon,
CustomCssIcon, CustomCssIcon,
LanguageIcon,
RebuildIcon, RebuildIcon,
IconAbout, IconAbout,
}, },
@ -133,20 +143,21 @@ export default {
openCloudSync() { openCloudSync() {
this.$modal.show(modalNames.CLOUD_BACKUP); this.$modal.show(modalNames.CLOUD_BACKUP);
}, },
openLanguageSwitchModal() {
this.$modal.show(modalNames.LANG_SWITCHER);
},
copyConfigToClipboard() { copyConfigToClipboard() {
navigator.clipboard.writeText(this.jsonParser(this.config)); navigator.clipboard.writeText(this.jsonParser(this.config));
// event.target.textContent = 'Copied to clipboard'; this.$toasted.show(this.$t('config.data-copied-msg'));
}, },
/* Checks that the user is sure, then resets site-wide local storage, and reloads page */ /* Checks that the user is sure, then resets site-wide local storage, and reloads page */
resetLocalSettings() { resetLocalSettings() {
const msg = 'This will remove all user settings from local storage, ' const msg = `${this.$t('config.reset-config-msg-l1')
+ 'but won\'t effect your \'conf.yml\' file. ' }${this.$t('config.reset-config-msg-l2')}\n\n${this.$t('config.reset-config-msg-l3')}`;
+ 'It is recommend to make a backup of your modified YAML settings first.\n\n'
+ 'Are you sure you want to proceed?';
const isTheUserSure = confirm(msg); // eslint-disable-line no-alert, no-restricted-globals const isTheUserSure = confirm(msg); // eslint-disable-line no-alert, no-restricted-globals
if (isTheUserSure) { if (isTheUserSure) {
localStorage.clear(); localStorage.clear();
this.$toasted.show('Data cleared succesfully'); this.$toasted.show(this.$t('config.data-cleared-msg'));
setTimeout(() => { setTimeout(() => {
location.reload(true); // eslint-disable-line no-restricted-globals location.reload(true); // eslint-disable-line no-restricted-globals
}, 1900); }, 1900);
@ -162,11 +173,19 @@ export default {
element.click(); element.click();
document.body.removeChild(element); document.body.removeChild(element);
}, },
/* Highlights the YAML config in View config tab */
initiateStntaxHighlighter() {
hljs.registerLanguage('yaml', yaml);
const highlighted = hljs.highlight(this.jsonParser(this.config), { language: 'yaml' }).value;
document.getElementById('conf-yaml').innerHTML = highlighted;
},
getLanguage() {
const lang = getUsersLanguage();
return lang ? `${lang.flag} ${lang.name}` : '';
},
}, },
mounted() { mounted() {
hljs.registerLanguage('yaml', yaml); this.initiateStntaxHighlighter();
const highlighted = hljs.highlight(this.jsonParser(this.config), { language: 'yaml' }).value;
document.getElementById('conf-yaml').innerHTML = highlighted;
}, },
}; };
</script> </script>
@ -213,10 +232,11 @@ a.config-button, button.config-button {
} }
} }
p.app-version { p.app-version, p.language {
margin: 0.5rem auto; margin: 0.5rem auto;
font-size: 1rem; font-size: 1rem;
color: var(--transparent-white-50); color: var(--transparent-white-50);
cursor: default;
} }
div.code-container { div.code-container {

View File

@ -1,11 +1,10 @@
<template> <template>
<div class="css-editor-outer"> <div class="css-editor-outer">
<prism-editor class="my-editor" v-model="customCss" :highlight="highlighter" line-numbers /> <prism-editor class="my-editor" v-model="customCss" :highlight="highlighter" line-numbers />
<button class="save-button" @click="save()">Save Changes</button> <button class="save-button" @click="save()">{{ $t('config.css-save-btn') }}</button>
<p class="quick-note"> <p class="quick-note">
<b>Note</b>: You will need to refresh the page for your changes to take effect. <b>{{ $t('config.css-note-label') }}:</b>
Styles overides are only stored locally, so it is reccomended to make a copy of your CSS. {{ $t('config.css-note-l1') }} {{ $t('config.css-note-l2') }} {{ $t('config.css-note-l3') }}
To remove all custom styles, delete the contents and hit Save Changes
</p> </p>
<CustomThemeMaker :themeToEdit="currentTheme" class="color-config" /> <CustomThemeMaker :themeToEdit="currentTheme" class="color-config" />
</div> </div>

View File

@ -8,19 +8,25 @@
/> />
<!-- Options raido, and save button --> <!-- Options raido, and save button -->
<div class="save-options"> <div class="save-options">
<span class="save-option-title">Save Location:</span> <span class="save-option-title">{{ $t('config-editor.save-location-label') }}:</span>
<div class="option"> <div class="option">
<input type="radio" id="local" value="local" <input type="radio" id="local" value="local"
v-model="saveMode" class="radio-option" :disabled="!allowWriteToDisk" /> v-model="saveMode" class="radio-option" :disabled="!allowWriteToDisk" />
<label for="local" class="save-option-label">Apply Locally</label> <label for="local" class="save-option-label">
{{ $t('config-editor.location-local-label') }}
</label>
</div> </div>
<div class="option"> <div class="option">
<input type="radio" id="file" value="file" v-model="saveMode" class="radio-option" <input type="radio" id="file" value="file" v-model="saveMode" class="radio-option"
:disabled="!allowWriteToDisk" /> :disabled="!allowWriteToDisk" />
<label for="file" class="save-option-label">Write Changes to Config File</label> <label for="file" class="save-option-label">
{{ $t('config-editor.location-disk-label') }}
</label>
</div> </div>
</div> </div>
<button :class="`save-button ${!isValid ? 'err' : ''}`" @click="save()">Save Changes</button> <button :class="`save-button ${!isValid ? 'err' : ''}`" @click="save()">
{{ $t('config-editor.save-button') }}
</button>
<!-- List validation warnings --> <!-- List validation warnings -->
<p class="errors"> <p class="errors">
<ul> <ul>
@ -28,24 +34,23 @@
{{error.msg}} {{error.msg}}
</li> </li>
<li v-if="errorMessages.length < 1" class="type-valid"> <li v-if="errorMessages.length < 1" class="type-valid">
Config is Valid {{ $t('config-editor.valid-label') }}
</li> </li>
</ul> </ul>
</p> </p>
<!-- Information notes --> <!-- Information notes -->
<p v-if="saveSuccess !== undefined" <p v-if="saveSuccess !== undefined"
:class="`response-output status-${saveSuccess ? 'success' : 'fail'}`"> :class="`response-output status-${saveSuccess ? 'success' : 'fail'}`">
{{saveSuccess ? 'Task Complete' : 'Task Failed'}} {{saveSuccess
? $t('config-editor.status-success-msg') : $t('config-editor.status-fail-msg') }}
</p> </p>
<p class="response-output">{{ responseText }}</p> <p class="response-output">{{ responseText }}</p>
<p v-if="saveSuccess" class="response-output"> <p v-if="saveSuccess" class="response-output">
The app should rebuild automatically. {{ $t('config-editor.success-note-l1') }}
This may take up to a minute. {{ $t('config-editor.success-note-l2') }}
You will need to refresh the page for changes to take effect. {{ $t('config-editor.success-note-l3') }}
</p>
<p class="note">
It is recommend to backup your existing confiruration before making any changes.
</p> </p>
<p class="note">{{ $t('config.backup-note') }}</p>
</div> </div>
</template> </template>
@ -103,7 +108,7 @@ export default {
} else if (this.saveMode === 'file') { } else if (this.saveMode === 'file') {
this.writeConfigToDisk(); this.writeConfigToDisk();
} else { } else {
this.$toasted.show('Please select a Save Mode: Local or File'); this.$toasted.show(this.$t('config-editor.error-msg-save-mode'));
} }
}, },
writeConfigToDisk() { writeConfigToDisk() {
@ -121,9 +126,9 @@ export default {
this.responseText = response.data.message; this.responseText = response.data.message;
if (this.saveSuccess) { if (this.saveSuccess) {
this.carefullyClearLocalStorage(); this.carefullyClearLocalStorage();
this.showToast('Config file written to disk succesfully', true); this.showToast(this.$t('config-editor.success-msg-disk'), true);
} else { } else {
this.showToast('An error occurred saving config', false); this.showToast(this.$t('config-editor.error-msg-cannot-save'), false);
} }
}) })
.catch((error) => { .catch((error) => {
@ -146,7 +151,7 @@ export default {
if (data.appConfig.theme) { if (data.appConfig.theme) {
localStorage.setItem(localStorageKeys.THEME, data.appConfig.theme); localStorage.setItem(localStorageKeys.THEME, data.appConfig.theme);
} }
this.showToast('Changes saved succesfully', true); this.showToast(this.$t('config-editor.success-msg-local'), true);
}, },
carefullyClearLocalStorage() { carefullyClearLocalStorage() {
localStorage.removeItem(localStorageKeys.PAGE_INFO); localStorage.removeItem(localStorageKeys.PAGE_INFO);
@ -160,7 +165,8 @@ export default {
case 'validation': case 'validation':
errorMessages.push({ errorMessages.push({
type: 'validation', type: 'validation',
msg: `Validatation Warning: ${error.error.keyword} ${error.error.message}`, msg: `${this.$t('config-editor.warning-msg-validation')}: `
+ `${error.error.keyword} ${error.error.message}`,
}); });
break; break;
case 'error': case 'error':
@ -172,7 +178,7 @@ export default {
default: default:
errorMessages.push({ errorMessages.push({
type: 'editor', type: 'editor',
msg: 'Error in JSON', msg: this.$t('config-editor.error-msg-bad-json'),
}); });
break; break;
} }

View File

@ -2,35 +2,41 @@
<modal :name="modalName" :resizable="true" width="50%" height="60%" classes="dashy-modal"> <modal :name="modalName" :resizable="true" width="50%" height="60%" classes="dashy-modal">
<div class="rebuild-app-container"> <div class="rebuild-app-container">
<!-- Title, intro and start button --> <!-- Title, intro and start button -->
<h3 class="rebuild-app-title">Rebuild Application</h3> <h3 class="rebuild-app-title">{{ $t('app-rebuild.title') }}</h3>
<p> <p>
A rebuild is required for changes written to the conf.yml file to take effect. {{ $t('app-rebuild.rebuild-note-l1') }}
This should happen automatically, but if it hasn't, you can manually trigger it here.<br> {{ $t('app-rebuild.rebuild-note-l2') }}<br>
This is not required for modifications stored locally. {{ $t('app-rebuild.rebuild-note-l3') }}
</p> </p>
<Button :click="startBuild" :disabled="loading || !allowRebuild" :disallow="!allowRebuild"> <Button :click="startBuild" :disabled="loading || !allowRebuild" :disallow="!allowRebuild">
<template v-slot:text>{{ loading ? 'Building...' : 'Start Build' }}</template> <template v-slot:text>
{{ loading ? $t('app-rebuild.rebuilding-status-1') : $t('app-rebuild.rebuild-button') }}
</template>
<template v-slot:icon><RebuildIcon /></template> <template v-slot:icon><RebuildIcon /></template>
</Button> </Button>
<div v-if="!allowRebuild"> <div v-if="!allowRebuild">
<p class="disallow-rebuild-msg">You do no have permission to trigger this action</p> <p class="disallow-rebuild-msg">{{ $t('app-rebuild.error-permission') }}</p>
</div> </div>
<!-- Loading animation and text (shown while build is happening) --> <!-- Loading animation and text (shown while build is happening) -->
<div v-if="loading" class="loader-info"> <div v-if="loading" class="loader-info">
<LoadingAnimation class="loader" /> <LoadingAnimation class="loader" />
<p class="loading-message">This may take a few minutes...</p> <p class="loading-message">{{ $t('app-rebuild.rebuilding-status-2') }}...</p>
</div> </div>
<!-- Build response, and next actions (shown after build is done) --> <!-- Build response, and next actions (shown after build is done) -->
<div class="rebuild-response" v-if="success !== undefined"> <div class="rebuild-response" v-if="success !== undefined">
<p v-if="success" class="response-status success"> Build completed succesfully</p> <p v-if="success" class="response-status success">
<p v-else class="response-status failure"> Build operation failed</p> {{ $t('app-rebuild.success-msg') }}
</p>
<p v-else class="response-status failure">
{{ $t('app-rebuild.fail-msg') }}
</p>
<pre class="output"><code>{{ output || error }}</code></pre> <pre class="output"><code>{{ output || error }}</code></pre>
<p class="rebuild-message">{{ message }}</p> <p class="rebuild-message">{{ message }}</p>
<p v-if="success" class="rebuild-message"> <p v-if="success" class="rebuild-message">
A page reload is now required for changes to take effect {{ $t('app-rebuild.reload-note') }}
</p> </p>
<Button :click="refreshPage" v-if="success"> <Button :click="refreshPage" v-if="success">
<template v-slot:text>Reload Page</template> <template v-slot:text>{{ $t('app-rebuild.reload-button') }}</template>
<template v-slot:icon><ReloadIcon /></template> <template v-slot:icon><ReloadIcon /></template>
</Button> </Button>
</div> </div>
@ -65,6 +71,7 @@ export default {
allowRebuild: true, allowRebuild: true,
}), }),
methods: { methods: {
/* Calls to the rebuild endpoint, to kickoff the app build */
startBuild() { startBuild() {
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin; const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
const endpoint = `${baseUrl}/config-manager/rebuild`; const endpoint = `${baseUrl}/config-manager/rebuild`;
@ -77,6 +84,7 @@ export default {
this.finished({ success: false, error }); this.finished({ success: false, error });
}); });
}, },
/* Called when rebuild is complete, updates UI with either success or fail message */
finished(responseData) { finished(responseData) {
this.loading = false; this.loading = false;
if (responseData) { if (responseData) {
@ -89,7 +97,8 @@ export default {
this.error = error; this.error = error;
} }
this.$toasted.show( this.$toasted.show(
(this.success ? '✅ Build Completed Succesfully' : '❌ Build Failed'), (this.success
? `${this.$t('app-rebuild.success-msg')}` : `${this.$t('app-rebuild.fail-msg')}`),
{ className: `toast-${this.success ? 'success' : 'error'}` }, { className: `toast-${this.success ? 'success' : 'error'}` },
); );
}, },

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="config-options"> <div class="config-options">
<!-- Button and label --> <!-- Button and label -->
<span>Config</span> <span>{{ $t('settings.config-launcher-label') }}</span>
<div class="config-buttons"> <div class="config-buttons">
<IconSpanner @click="showEditor()" tabindex="-2" <IconSpanner @click="showEditor()" tabindex="-2"
v-tooltip="tooltip('Update configuration locally')" /> v-tooltip="tooltip('Update configuration')" />
<IconCloud @click="showCloudModal()" tabindex="-2" <IconCloud @click="showCloudModal()" tabindex="-2"
v-tooltip="tooltip('Backup / restore cloud config')" /> v-tooltip="tooltip('Backup / restore cloud config')" />
</div> </div>
@ -20,6 +20,13 @@
@closed="$emit('modalChanged', false)" classes="dashy-modal"> @closed="$emit('modalChanged', false)" classes="dashy-modal">
<CloudBackupRestore :config="combineConfig()" /> <CloudBackupRestore :config="combineConfig()" />
</modal> </modal>
<!-- Modal for manually changing locale -->
<modal :name="modalNames.LANG_SWITCHER" classes="dashy-modal"
:resizable="true" width="30%" height="25%">
<LanguageSwitcher />
</modal>
</div> </div>
</template> </template>
@ -29,6 +36,7 @@ import IconSpanner from '@/assets/interface-icons/config-editor.svg';
import IconCloud from '@/assets/interface-icons/cloud-backup-restore.svg'; import IconCloud from '@/assets/interface-icons/cloud-backup-restore.svg';
import ConfigContainer from '@/components/Configuration/ConfigContainer'; import ConfigContainer from '@/components/Configuration/ConfigContainer';
import CloudBackupRestore from '@/components/Configuration/CloudBackupRestore'; import CloudBackupRestore from '@/components/Configuration/CloudBackupRestore';
import LanguageSwitcher from '@/components/Settings/LanguageSwitcher';
import { topLevelConfKeys, localStorageKeys, modalNames } from '@/utils/defaults'; import { topLevelConfKeys, localStorageKeys, modalNames } from '@/utils/defaults';
export default { export default {
@ -43,6 +51,7 @@ export default {
IconCloud, IconCloud,
ConfigContainer, ConfigContainer,
CloudBackupRestore, CloudBackupRestore,
LanguageSwitcher,
}, },
props: { props: {
sections: Array, sections: Array,

View File

@ -1,6 +1,6 @@
<template> <template>
<div :class="`theme-configurator-wrapper ${showingAllVars ? 'showing-all' : ''}`"> <div :class="`theme-configurator-wrapper ${showingAllVars ? 'showing-all' : ''}`">
<h3 class="configurator-title">Theme Configurator</h3> <h3 class="configurator-title">{{ $t('theme-maker.title') }}</h3>
<div class="color-row-container"> <div class="color-row-container">
<div class="color-row" v-for="colorName in Object.keys(customColors)" :key="colorName"> <div class="color-row" v-for="colorName in Object.keys(customColors)" :key="colorName">
<label :for="`color-input-${colorName}`" class="color-name"> <label :for="`color-input-${colorName}`" class="color-name">
@ -33,17 +33,21 @@
</div> <!-- End of color list --> </div> <!-- End of color list -->
</div> </div>
<p @click="exportToClipboard" class="action-text-btn"> <p @click="exportToClipboard" class="action-text-btn">
Export Custom Variables {{ $t('theme-maker.export-button') }}
</p> </p>
<p @click="resetAndSave" class="action-text-btn show-all-vars-btn"> <p @click="resetAndSave" class="action-text-btn show-all-vars-btn">
Reset Styles for '{{ themeToEdit }}' {{ $t('theme-maker.reset-button') }} '{{ themeToEdit }}'
</p> </p>
<p @click="findAllVariableNames" class="action-text-btn"> <p @click="findAllVariableNames" class="action-text-btn">
Show All Variables {{ $t('theme-maker.show-all-button') }}
</p> </p>
<div class="action-buttons"> <div class="action-buttons">
<Button :click="saveChanges"><SaveIcon />Save</Button> <Button :click="saveChanges">
<Button :click="resetUnsavedColors"><CancelIcon />Cancel</Button> <SaveIcon /> {{ $t('theme-maker.save-button') }}
</Button>
<Button :click="resetUnsavedColors">
<CancelIcon /> {{ $t('theme-maker.cancel-button') }}
</Button>
</div> </div>
</div> </div>
</template> </template>
@ -89,7 +93,7 @@ export default {
const priorSettings = JSON.parse(localStorage[localStorageKeys.CUSTOM_COLORS] || '{}'); const priorSettings = JSON.parse(localStorage[localStorageKeys.CUSTOM_COLORS] || '{}');
priorSettings[this.themeToEdit] = this.customColors; priorSettings[this.themeToEdit] = this.customColors;
localStorage.setItem(localStorageKeys.CUSTOM_COLORS, JSON.stringify(priorSettings)); localStorage.setItem(localStorageKeys.CUSTOM_COLORS, JSON.stringify(priorSettings));
this.$toasted.show('Theme Updates Succesfully'); this.$toasted.show(this.$t('theme-maker.saved-toast', { theme: this.themeToEdit }));
this.$emit('closeThemeConfigurator'); this.$emit('closeThemeConfigurator');
}, },
/* Itterates over available variables, removing them from the DOM */ /* Itterates over available variables, removing them from the DOM */
@ -107,7 +111,7 @@ export default {
delete priorSettings[this.themeToEdit]; delete priorSettings[this.themeToEdit];
localStorage.setItem(localStorageKeys.CUSTOM_COLORS, JSON.stringify(priorSettings)); localStorage.setItem(localStorageKeys.CUSTOM_COLORS, JSON.stringify(priorSettings));
this.resetUnsavedColors(); this.resetUnsavedColors();
this.$toasted.show(`Custom Colors for ${this.themeToEdit} Removed`); this.$toasted.show(this.$t('theme-maker.reset-toast', { theme: this.themeToEdit }));
}, },
/* Generates CSS for the currently applied variables, and copys to users clipboard */ /* Generates CSS for the currently applied variables, and copys to users clipboard */
exportToClipboard() { exportToClipboard() {
@ -117,7 +121,7 @@ export default {
clipboardText += (`--${customVar}: ${this.customColors[customVar]};\n`); clipboardText += (`--${customVar}: ${this.customColors[customVar]};\n`);
}); });
navigator.clipboard.writeText(clipboardText); navigator.clipboard.writeText(clipboardText);
this.$toasted.show(`Theme data for ${themeName} copied to clipboard`); this.$toasted.show(this.$t('theme-maker.copied-toast', { theme: themeName }));
}, },
/* Returns a JSON object, with the variable name as key, and color as value */ /* Returns a JSON object, with the variable name as key, and color as value */
makeInitialData(variableArray) { makeInitialData(variableArray) {

View File

@ -1,13 +1,25 @@
<template> <template>
<div> <div>
<span class="options-label">Icon Size</span> <span class="options-label">{{ $t('settings.item-size-label') }}</span>
<div class="display-options"> <div class="display-options">
<IconSmall @click="updateIconSize('small')" v-tooltip="tooltip('Small')" <IconSmall
:class="`layout-icon ${iconSize === 'small' ? 'selected' : ''}`" tabindex="-2" /> @click="updateIconSize('small')"
<IconMedium @click="updateIconSize('medium')" v-tooltip="tooltip('Medium')" v-tooltip="tooltip($t('settings.item-size-small'))"
:class="`layout-icon ${iconSize === 'medium' ? 'selected' : ''}`" tabindex="-2" /> :class="`layout-icon ${iconSize === 'small' ? 'selected' : ''}`"
<IconLarge @click="updateIconSize('large')" v-tooltip="tooltip('Large')" tabindex="-2"
:class="`layout-icon ${iconSize === 'large' ? 'selected' : ''}`" tabindex="-2" /> />
<IconMedium
@click="updateIconSize('medium')"
v-tooltip="tooltip($t('settings.item-size-medium'))"
:class="`layout-icon ${iconSize === 'medium' ? 'selected' : ''}`"
tabindex="-2"
/>
<IconLarge
@click="updateIconSize('large')"
v-tooltip="tooltip($t('settings.item-size-large'))"
:class="`layout-icon ${iconSize === 'large' ? 'selected' : ''}`"
tabindex="-2"
/>
</div> </div>
</div> </div>
</template> </template>

View File

@ -0,0 +1,110 @@
<template>
<div class="language-switcher">
<h3 class="title">{{ $t('language-switcher.title') }}</h3>
<p class="intro">{{ $t('language-switcher.dropdown-label') }}:</p>
<v-select
v-model="language"
:selectOnTab="true"
:options="availibleLanguages"
class="language-dropdown"
label="name"
:input="setLangLocally()"
/>
<Button class="save-button" :click="saveLanguage" :disallow="!language">
{{ $t('language-switcher.save-button') }}
<SaveConfigIcon />
</Button>
<p v-if="language">{{ language.flag }} {{ language.name }}</p>
<p v-if="$i18n.availableLocales.length <= 1" class="sad-times">
There are not currently any additional languages supported,
but stay tuned as more are on their way!
</p>
</div>
</template>
<script>
import Button from '@/components/FormElements/Button';
import { languages } from '@/utils/languages';
import SaveConfigIcon from '@/assets/interface-icons/save-config.svg';
import { localStorageKeys, modalNames } from '@/utils/defaults';
export default {
name: 'LanguageSwitcher',
inject: ['config'],
components: {
Button,
SaveConfigIcon,
},
data() {
return {
availibleLanguages: languages,
language: '',
modalName: modalNames.LANG_SWITCHER,
};
},
methods: {
/* Save language to local storage, show success msg and close modal */
saveLanguage() {
const selectedLanguage = this.language;
if (this.checkLocale(selectedLanguage)) {
localStorage.setItem(localStorageKeys.LANGUAGE, selectedLanguage.code);
this.setLangLocally();
const successMsg = `${selectedLanguage.flag} `
+ `${this.$t('language-switcher.success-msg')} ${selectedLanguage.name}`;
this.$toasted.show(successMsg, { className: 'toast-success' });
this.$modal.hide(this.modalName);
} else {
this.$toasted.show('Unable to update language', { className: 'toast-error' });
}
},
/* Check language is supported, before saving */
checkLocale(selectedLanguage) {
if (!selectedLanguage || !selectedLanguage.code) return false;
const i18nLocales = this.$i18n.availableLocales;
return i18nLocales.includes(selectedLanguage.code);
},
/* Apply language locally */
setLangLocally() {
if (this.language && this.language.code) {
this.$i18n.locale = this.language.code;
}
},
},
};
</script>
<style scoped lang="scss">
.language-switcher {
height: 100%;
margin: 0;
padding: 1rem;
background: var(--config-settings-background);
color: var(--config-settings-color);
h3.title {
text-align: center;
}
p.intro {
margin: 0;
}
button.save-button {
margin: 0 auto;
width: 100%;
}
p.sad-times {
color: var(--warning);
text-align: center;
}
.language-dropdown {
margin: 1rem auto;
div.vs__dropdown-toggle {
padding: 0.2rem 0;
}
}
}
</style>

View File

@ -1,13 +1,25 @@
<template> <template>
<div> <div>
<span class="options-label">Layout</span> <span class="options-label">{{ $t('settings.layout-label') }}</span>
<div class="display-options"> <div class="display-options">
<IconDeafault @click="updateDisplayLayout('auto')" v-tooltip="tooltip('Auto')" <IconDeafault
:class="`layout-icon ${displayLayout === 'auto' ? 'selected' : ''}`" tabindex="-2" /> @click="updateDisplayLayout('auto')"
<IconHorizontal @click="updateDisplayLayout('horizontal')" v-tooltip="tooltip('Horizontal')" v-tooltip="tooltip($t('settings.layout-auto'))"
:class="`layout-icon ${displayLayout === 'horizontal' ? 'selected' : ''}`" tabindex="-2" /> :class="`layout-icon ${displayLayout === 'auto' ? 'selected' : ''}`"
<IconVertical @click="updateDisplayLayout('vertical')" v-tooltip="tooltip('Vertical')" tabindex="-2"
:class="`layout-icon ${displayLayout === 'vertical' ? 'selected' : ''}`" tabindex="-2" /> />
<IconHorizontal
@click="updateDisplayLayout('horizontal')"
v-tooltip="tooltip($t('settings.layout-horizontal'))"
:class="`layout-icon ${displayLayout === 'horizontal' ? 'selected' : ''}`"
tabindex="-2"
/>
<IconVertical
@click="updateDisplayLayout('vertical')"
v-tooltip="tooltip($t('settings.layout-vertical'))"
:class="`layout-icon ${displayLayout === 'vertical' ? 'selected' : ''}`"
tabindex="-2"
/>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,16 +1,16 @@
<template> <template>
<form> <form>
<label for="filter-tiles">Search</label> <label for="filter-tiles">{{ $t('search.search-label') }}</label>
<input <input
id="filter-tiles" id="filter-tiles"
v-model="input" v-model="input"
ref="filter" ref="filter"
placeholder="Start typing to filter..." :placeholder="$t('search.search-placeholder')"
v-on:input="userIsTypingSomething" v-on:input="userIsTypingSomething"
@keydown.esc="clearFilterInput" /> @keydown.esc="clearFilterInput" />
<i v-if="input.length > 0" <i v-if="input.length > 0"
class="clear-search" class="clear-search"
title="Clear search" :title="$t('search.clear-search-tooltip')"
@click="clearFilterInput">x</i> @click="clearFilterInput">x</i>
</form> </form>
</template> </template>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="theme-selector-section" v-click-outside="closeThemeConfigurator"> <div class="theme-selector-section" v-click-outside="closeThemeConfigurator">
<div> <div>
<span class="theme-label">Theme</span> <span class="theme-label">{{ $t('settings.theme-label') }}</span>
<v-select <v-select
:options="themeNames" :options="themeNames"
v-model="selectedTheme" v-model="selectedTheme"

View File

@ -1,18 +1,23 @@
// Import core framework and essential utils
import Vue from 'vue'; import Vue from 'vue';
import VueI18n from 'vue-i18n'; // i18n for localization
/* Import component Vue plugins, used throughout the app */ // Import component Vue plugins, used throughout the app
import VTooltip from 'v-tooltip'; // A Vue directive for Popper.js, tooltip component import VTooltip from 'v-tooltip'; // A Vue directive for Popper.js, tooltip component
import VModal from 'vue-js-modal'; // Modal component import VModal from 'vue-js-modal'; // Modal component
import VSelect from 'vue-select'; // Select dropdown component import VSelect from 'vue-select'; // Select dropdown component
import VTabs from 'vue-material-tabs'; // Tab view component, used on the config page import VTabs from 'vue-material-tabs'; // Tab view component, used on the config page
import Toasted from 'vue-toasted'; // Toast component, used to show confirmation notifications import Toasted from 'vue-toasted'; // Toast component, used to show confirmation notifications
import { toastedOptions } from '@/utils/defaults'; // Import base Dashy components and utils
import Dashy from '@/App.vue'; import Dashy from '@/App.vue';
import router from '@/router'; import router from '@/router';
import registerServiceWorker from '@/registerServiceWorker'; import registerServiceWorker from '@/registerServiceWorker';
import clickOutside from '@/utils/ClickOutside'; import clickOutside from '@/utils/ClickOutside';
import { toastedOptions, language as defaultLanguage } from '@/utils/defaults';
import { messages } from '@/utils/languages';
Vue.use(VueI18n);
Vue.use(VTooltip); Vue.use(VTooltip);
Vue.use(VModal); Vue.use(VModal);
Vue.use(VTabs); Vue.use(VTabs);
@ -22,10 +27,18 @@ Vue.directive('clickOutside', clickOutside);
Vue.config.productionTip = false; Vue.config.productionTip = false;
// Setup i18n translations
const i18n = new VueI18n({
locale: defaultLanguage,
fallbackLocale: defaultLanguage,
messages,
});
// Register Service Worker // Register Service Worker
registerServiceWorker(); registerServiceWorker();
new Vue({ // Render function
router, const render = (awesome) => awesome(Dashy);
render: (awesome) => awesome(Dashy),
}).$mount('#app'); // All done, now just initialize main Vue app!
new Vue({ router, render, i18n }).$mount('#app');

View File

@ -24,8 +24,8 @@ html {
/* Overriding styles for the modal component */ /* Overriding styles for the modal component */
.vm--modal, .dashy-modal { .vm--modal, .dashy-modal {
box-shadow: 0 40px 70px -2px hsl(0deg 0% 0% / 60%), 1px 1px 6px var(--primary) !important; box-shadow: 0 40px 70px -2px hsl(0deg 0% 0% / 60%), 1px 1px 6px var(--primary) !important;
min-width: 300px; min-width: 350px;
min-height: 500px; min-height: 200px;
} }
.vm--overlay { .vm--overlay {
background: #00000080; background: #00000080;
@ -48,4 +48,32 @@ html {
background: var(--success) !important; background: var(--success) !important;
color: var(--white) !important; color: var(--white) !important;
font-size: 1.25rem !important; font-size: 1.25rem !important;
}
/* v-select, dropdown styles */
.v-select {
.vs__dropdown-toggle {
border-color: var(--primary);
background: var(--background);
cursor: pointer;
span.vs__selected {
color: var(--primary);
}
.vs__actions svg path { fill: var(--primary); }
}
ul.vs__dropdown-menu {
background: var(--background);
border-color: var(--primary);
li {
color: var(--primary);
&:hover {
color: var(--background);
background: var(--primary);
}
&.vs__dropdown-option--highlight {
color: var(--background);
background: var(--primary);
}
}
}
} }

View File

@ -11,6 +11,7 @@ import {
pageInfo as defaultPageInfo, pageInfo as defaultPageInfo,
iconSize as defaultIconSize, iconSize as defaultIconSize,
layout as defaultLayout, layout as defaultLayout,
language as defaultLanguage,
} from '@/utils/defaults'; } from '@/utils/defaults';
import conf from '../../public/conf.yml'; import conf from '../../public/conf.yml';
@ -33,6 +34,8 @@ export default class ConfigAccumulator {
|| appConfigFile.layout || defaultLayout; || appConfigFile.layout || defaultLayout;
usersAppConfig.iconSize = localStorage[localStorageKeys.ICON_SIZE] usersAppConfig.iconSize = localStorage[localStorageKeys.ICON_SIZE]
|| appConfigFile.iconSize || defaultIconSize; || appConfigFile.iconSize || defaultIconSize;
usersAppConfig.language = localStorage[localStorageKeys.LANGUAGE]
|| appConfigFile.language || defaultLanguage;
return usersAppConfig; return usersAppConfig;
} }

View File

@ -1,5 +1,11 @@
import ConfigAccumulator from '@/utils/ConfigAccumalator'; import ConfigAccumulator from '@/utils/ConfigAccumalator';
import { visibleComponents, localStorageKeys, theme as defaultTheme } from '@/utils/defaults'; import { languages } from '@/utils/languages';
import {
visibleComponents,
localStorageKeys,
theme as defaultTheme,
language as defaultLanguage,
} from '@/utils/defaults';
/** /**
* Initiates the Accumulator class and generates a complete config object * Initiates the Accumulator class and generates a complete config object
@ -74,3 +80,15 @@ export const getCustomKeyShortcuts = () => {
}); });
return results.flat(); return results.flat();
}; };
/**
* Gets the users chosen language. Defaults to English.
* @returns {object} Language, including code, name and flag
*/
export const getUsersLanguage = () => {
const langCode = localStorage[localStorageKeys.LANGUAGE]
|| config.appConfig.language
|| defaultLanguage;
const langObj = languages.find(lang => lang.code === langCode);
return langObj;
};

View File

@ -54,6 +54,10 @@
"type": "string", "type": "string",
"description": "A URL to an image asset to be displayed as background" "description": "A URL to an image asset to be displayed as background"
}, },
"language": {
"type": "string",
"description": "The ISO code of your desired language, must have translations present, check docs for more info"
},
"theme": { "theme": {
"type": "string", "type": "string",
"default": "callisto", "default": "callisto",

View File

@ -11,6 +11,8 @@ module.exports = {
}, },
/* Default appConfig to be used, if user does not specify their own */ /* Default appConfig to be used, if user does not specify their own */
appConfig: {}, appConfig: {},
/* Default language code */
language: 'en',
/* Default icon size to be applied on initial load */ /* Default icon size to be applied on initial load */
iconSize: 'medium', iconSize: 'medium',
/* Default layout to be applied on initial load */ /* Default layout to be applied on initial load */
@ -58,6 +60,7 @@ module.exports = {
}, },
/* Key names for local storage identifiers */ /* Key names for local storage identifiers */
localStorageKeys: { localStorageKeys: {
LANGUAGE: 'language',
HIDE_WELCOME_BANNER: 'hideWelcomeHelpers', HIDE_WELCOME_BANNER: 'hideWelcomeHelpers',
LAYOUT_ORIENTATION: 'layoutOrientation', LAYOUT_ORIENTATION: 'layoutOrientation',
COLLAPSE_STATE: 'collapseState', COLLAPSE_STATE: 'collapseState',
@ -87,6 +90,7 @@ module.exports = {
REBUILD_APP: 'REBUILD_APP', REBUILD_APP: 'REBUILD_APP',
THEME_MAKER: 'THEME_MAKER', THEME_MAKER: 'THEME_MAKER',
ABOUT_APP: 'ABOUT_APP', ABOUT_APP: 'ABOUT_APP',
LANG_SWITCHER: 'LANG_SWITCHER',
}, },
/* Key names for the top-level objects in conf.yml */ /* Key names for the top-level objects in conf.yml */
topLevelConfKeys: { topLevelConfKeys: {

23
src/utils/languages.js Normal file
View File

@ -0,0 +1,23 @@
// Locales - Import translation files here!
import en from '@/assets/locales/en.json';
// Language data - Add your country name, locale code and imported file here
export const languages = [
{
name: 'English',
code: 'en',
locale: en,
flag: '🇬🇧',
},
// Including:
// name - Human readable name for your language (e.g German)
// code - ISO language code (e.g. de)
// locale - The file that you imported above
// flag - A nice emoji flag (optional, e.g. 🇩🇪)
];
const i18nMessages = {};
languages.forEach((lang) => {
i18nMessages[lang.code] = lang.locale;
});
export const messages = i18nMessages;

View File

@ -13,7 +13,7 @@
:appConfig="appConfig" :appConfig="appConfig"
:pageInfo="pageInfo" :pageInfo="pageInfo"
:modalOpen="modalOpen" :modalOpen="modalOpen"
class="filter-container" class="settings-outer"
/> />
<!-- Main content, section for each group of items --> <!-- Main content, section for each group of items -->
<div v-if="checkTheresData(sections)" <div v-if="checkTheresData(sections)"
@ -27,14 +27,14 @@
:groupId="`section-${index}`" :groupId="`section-${index}`"
:items="filterTiles(section.items)" :items="filterTiles(section.items)"
:itemSize="itemSizeBound" :itemSize="itemSizeBound"
@itemClicked="finishedSearching()" @itemClicked="finishedSearching()"
@change-modal-visibility="updateModalVisibility" @change-modal-visibility="updateModalVisibility"
:class="(filterTiles(section.items).length === 0 && searchValue) ? 'no-results' : ''" :class="(filterTiles(section.items).length === 0 && searchValue) ? 'no-results' : ''"
/> />
</div> </div>
<!-- Show message when there's no data to show --> <!-- Show message when there's no data to show -->
<div v-if="checkIfResults()" class="no-data"> <div v-if="checkIfResults()" class="no-data">
{{searchValue ? 'No Search Results' : 'No Data Configured'}} {{searchValue ? $t('home.no-results') : $t('home.no-data')}}
</div> </div>
</div> </div>
</template> </template>
@ -63,6 +63,7 @@ export default {
modalOpen: false, // When true, keybindings are disabled modalOpen: false, // When true, keybindings are disabled
}), }),
computed: { computed: {
/* Updates layout (when button clicked), and saves in local storage */
layoutOrientation: { layoutOrientation: {
get() { return this.appConfig.layout || Defaults.layout; }, get() { return this.appConfig.layout || Defaults.layout; },
set: function setLayout(layout) { set: function setLayout(layout) {
@ -70,6 +71,7 @@ export default {
this.layout = layout; this.layout = layout;
}, },
}, },
/* Updates icon size (when button clicked), and saves in local storage */
iconSize: { iconSize: {
get() { return this.appConfig.iconSize || Defaults.iconSize; }, get() { return this.appConfig.iconSize || Defaults.iconSize; },
set: function setIconSize(iconSize) { set: function setIconSize(iconSize) {
@ -192,6 +194,7 @@ export default {
return itemsFound; return itemsFound;
} }
}, },
/* If user has a background image, then generate CSS attributes */
getBackgroundImage() { getBackgroundImage() {
if (this.appConfig && this.appConfig.backgroundImg) { if (this.appConfig && this.appConfig.backgroundImg) {
return `background: url('${this.appConfig.backgroundImg}');background-size:cover;`; return `background: url('${this.appConfig.backgroundImg}');background-size:cover;`;
@ -267,6 +270,7 @@ export default {
.no-results { display: none; } .no-results { display: none; }
} }
/* Custom styles only applied when there is no sections in config */
.no-data { .no-data {
font-size: 2rem; font-size: 2rem;
color: var(--background); color: var(--background);
@ -277,7 +281,8 @@ export default {
border-radius: var(--curve-factor); border-radius: var(--curve-factor);
} }
section.filter-container { /* Settings section, includes search, config and user settings */
section.settings-outer {
border-bottom: 1px solid var(--outline-color); border-bottom: 1px solid var(--outline-color);
@include phone { @include phone {
flex-direction: column; flex-direction: column;

View File

@ -1,20 +1,31 @@
<template> <template>
<div class="login-page"> <div class="login-page">
<form class="login-form"> <form class="login-form">
<h2 class="login-title">Dashy</h2> <h2 class="login-title">{{ $t('login.title') }}</h2>
<Input v-model="username" label="Username" class="login-field username" type="text" /> <Input
<Input v-model="password" label="Password" class="login-field password" type="password" /> v-model="username"
<label>Remember me for</label> type="text"
:label="$t('login.username-label')"
class="login-field username"
/>
<Input
v-model="password"
type="password"
:label="$t('login.password-label')"
class="login-field password"
/>
<label>{{ $t('login.remember-me-label') }}</label>
<v-select <v-select
v-model="timeout" v-model="timeout"
:options="dropDownMenu"
label="label"
:selectOnTab="true" :selectOnTab="true"
:options="dropDownMenu"
class="login-time-dropdown" class="login-time-dropdown"
/> />
<Button class="login-button" :click="submitLogin">Login</Button> <Button class="login-button" :click="submitLogin">
{{ $t('login.login-button') }}
</Button>
<transition name="bounce"> <transition name="bounce">
<p :class="`login-error-message ${status}`" v-show="message">{{ message }}</p> <p :class="`login-error-message ${status}`" v-show="message">{{ message }}</p>
</transition> </transition>
</form> </form>
</div> </div>
@ -38,12 +49,12 @@ export default {
password: '', password: '',
message: '', message: '',
status: 'waiting', // wating, error, success status: 'waiting', // wating, error, success
timeout: { label: 'Never', time: 0 }, timeout: { label: this.$t('login.remember-me-never'), time: 0 },
dropDownMenu: [ // Data for timeout dropdown menu, label + value dropDownMenu: [ // Data for timeout dropdown menu, translated label + value in ms
{ label: 'Never', time: 0 }, // Time is specified in ms { label: this.$t('login.remember-me-never'), time: 0 },
{ label: '4 Hours', time: 14400 * 1000 }, { label: this.$t('login.remember-me-hour'), time: 14400 * 1000 },
{ label: '1 Day', time: 86400 * 1000 }, { label: this.$t('login.remember-me-day'), time: 86400 * 1000 },
{ label: '1 Week', time: 604800 * 1000 }, { label: this.$t('login.remember-me-week'), time: 604800 * 1000 },
], ],
}; };
}, },
@ -52,8 +63,9 @@ export default {
Input, Input,
}, },
methods: { methods: {
/* Checks form is filled in, then initiates the login, and redirects to /home */
submitLogin() { submitLogin() {
const timeout = this.timeout.time || 0; const timeout = this.timeout ? this.timeout.time : 0;
const response = checkCredentials(this.username, this.password, this.appConfig.auth || []); const response = checkCredentials(this.username, this.password, this.appConfig.auth || []);
this.message = response.msg; // Show error or success message to the user this.message = response.msg; // Show error or success message to the user
this.status = response.correct ? 'success' : 'error'; this.status = response.correct ? 'success' : 'error';
@ -64,6 +76,7 @@ export default {
}, 250); }, 250);
} }
}, },
/* Since we don't have the Theme setter at this point, we must manually set users theme */
setTheme() { setTheme() {
const theme = localStorage[localStorageKeys.THEME] || Defaults.theme; const theme = localStorage[localStorageKeys.THEME] || Defaults.theme;
document.getElementsByTagName('html')[0].setAttribute('data-theme', theme); document.getElementsByTagName('html')[0].setAttribute('data-theme', theme);
@ -78,43 +91,48 @@ export default {
<style lang="scss"> <style lang="scss">
/* Login page base styles */
.login-page { .login-page {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 800px; min-height: calc(100vh - var(--footer-height));
.login-form { /* Login form container */
form.login-form {
background: var(--login-form-background); background: var(--login-form-background);
color: var(--login-form-color); color: var(--login-form-color);
border: 1px solid var(--login-form-color); border: 1px solid var(--login-form-color);
border-radius: var(--curve-factor); border-radius: var(--curve-factor);
font-size: 1.4rem;
padding: 2rem; padding: 2rem;
margin: 2rem auto; margin: 2rem auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/* Login form title */
h2.login-title { h2.login-title {
font-size: 3rem; font-size: 3rem;
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
text-align: center; text-align: center;
cursor: default;
} }
/* Set sizings for input fields and login button */
.login-field input, Button.login-button { .login-field input, Button.login-button {
width: 18rem; width: 20rem;
margin: 0.5rem auto; margin: 0.5rem auto;
font-size: 1.4rem; font-size: 1.4rem;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
} }
/* Custom colors for username/ password input fields */
.login-field input { .login-field input {
color: var(--login-form-color); color: var(--login-form-color);
border-color: var(--login-form-color); border-color: var(--login-form-color);
background: var(--login-form-background); background: var(--login-form-background);
&:focus {
}
} }
/* Custom colors for Login Button */
Button.login-button { Button.login-button {
background: var(--login-form-color); background: var(--login-form-color);
border-color: var(--login-form-background); border-color: var(--login-form-background);
@ -128,6 +146,7 @@ export default {
box-shadow: 1px 1px 6px var(--login-form-color); box-shadow: 1px 1px 6px var(--login-form-color);
} }
} }
/* Apply color to status message, depending on status */
p.login-error-message { p.login-error-message {
font-size: 1rem; font-size: 1rem;
text-align: center; text-align: center;
@ -138,6 +157,7 @@ export default {
} }
} }
/* Enter animations for error/ success message */
.bounce-enter-active { animation: bounce-in 0.25s; } .bounce-enter-active { animation: bounce-in 0.25s; }
.bounce-leave-active { animation: bounce-in 0.25s reverse; } .bounce-leave-active { animation: bounce-in 0.25s reverse; }
@keyframes bounce-in { @keyframes bounce-in {
@ -146,6 +166,7 @@ export default {
100% { transform: scale(1); } 100% { transform: scale(1); }
} }
/* Custom styles for dropdown component */
.v-select.login-time-dropdown { .v-select.login-time-dropdown {
margin: 0.5rem 0; margin: 0.5rem 0;
.vs__dropdown-toggle { .vs__dropdown-toggle {

896
yarn.lock

File diff suppressed because it is too large Load Diff