🔀 Merge pull request #155 from Lissy93/FEATURE/minimal-view

[FEATURE] Minimal view
Yay, all checks are passing, and we've got a new feature 😊
This commit is contained in:
Alicia Sykes 2021-08-14 19:45:24 +01:00 committed by GitHub
commit 1fdb687955
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1017 additions and 117 deletions

View File

@ -1,5 +1,10 @@
# Changelog
## ✨ 1.5.9 - New Minimal/ Startpage View [PR #155](https://github.com/Lissy93/dashy/issues/155)
- Adds a new view, called minimal view, designed to be like a light-weight startpage
- Implemented all the required features (filtering, opening methods, icons, etc) into minimal view
- Adds `appConfig.startingView` into schema, for specifying the initial default view to be loaded
## ✨ 1.5.8 - Multi-Tasking Support in Workspace View [PR #146](https://github.com/Lissy93/dashy/pull/146)
- Adds option to keep launched apps open in the background, to reduce friction when switching between websites, Re: #144
- This can be enabled by setting `appConfig.enableMultiTasking: true`

1
.github/CODEOWNERS vendored
View File

@ -13,4 +13,3 @@ src/assets/locales/nl.json @evroon
# Bot PR Permissions
docs/assets/CONTRIBUTORS.svg @liss-bot
docs/*.md @liss-bot

113
README.md
View File

@ -8,7 +8,6 @@
[![Awesome Self-Hosted](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/awesome-selfhosted/awesome-selfhosted#personal-dashboards)
![Docker Pulls](https://img.shields.io/docker/pulls/lissy93/dashy?logo=docker&style=flat-square)
![Stars](https://flat.badgen.net/github/stars/lissy93/dashy?icon=github)
![GitHub Status](https://flat.badgen.net/github/status/lissy93/dashy?icon=github)
![License MIT](https://img.shields.io/badge/License-MIT-09be48?style=flat-square&logo=opensourceinitiative)
![Current Version](https://img.shields.io/github/package-json/v/lissy93/dashy?style=flat-square&logo=azurepipelines&color=00af87)
@ -29,12 +28,13 @@
<ul>
<li><a href="#theming-">🎨 Theming</a></li>
<li><a href="#icons-">🧸 Icons</a></li>
<li><a href="#cloud-backup--sync-">☁ Cloud Backup &amp; Sync</a></li>
<li><a href="#authentication-">💂 Authentication</a></li>
<li><a href="#status-indicators-">🚦 Status Indicators</a></li>
<li><a href="#authentication-">💂 Authentication</a></li>
<li><a href="#opening-methods-%EF%B8%8F">🖱️ Opening Methods</a></li>
<li><a href="#alternate-views-">👓 Alternate Views</a></li>
<li><a href="#searching-and-shortcuts-">🔎 Searching and Shortcuts</a></li>
<li><a href="#config-editor-%EF%B8%8F">⚙️ Config Editor</a></li>
<li><a href="#cloud-backup--sync-">☁ Cloud Backup &amp; Sync</a></li>
<li><a href="#language-switching-">🌎 Language Switching</a></li>
<li><a href="#setting-dashboard-info-">🌳 Dashboard Info</a></li>
</ul>
@ -216,18 +216,18 @@ Both sections and items can have an icon associated with them, and defined under
---
## Cloud Backup & Sync ☁
## Status Indicators 🚦
> For full backup documentation, see: [**Cloud Backup & Sync**](./docs/backup-restore.md)
> For full monitoring documentation, see: [**Status Indicators**](./docs/status-indicators.md)
Dashy has an **optional** built-in feature for securely backing up your config to a hosted cloud service, and then restoring it on another instance. This feature is totally optional, and if you do not enable it, then Dashy will not make any external network requests.
Dashy has an optional feature that can display a small icon next to each of your running services, indicating it's current status. This is useful if you are using Dashy as your homelab's start page, as it gives you an overview of the health of each of your running services. Hovering over the indicator will show additional information, including average response time and an error message for services which are down.
This is useful not only for backing up your configuration off-site, but it also enables Dashy to be used without having write a YAML config file, and makes it possible to use a public hosted instance, without the need to self-host.
By default, this feature is off, but you can enable it globally by setting `appConfig.statusCheck: true`, or enable/ disable it for an individual item, with `item[n].statusCheck`.
All data is encrypted before being sent to the backend. In Dashy, this is done in [`CloudBackup.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/CloudBackup.js), using [crypto.js](https://github.com/brix/crypto-js)'s AES method, using the users chosen password as the key. The data is then sent to a [Cloudflare worker](https://developers.cloudflare.com/workers/learning/how-workers-works) (a platform for running serverless functions), and stored in a [KV](https://developers.cloudflare.com/workers/learning/how-kv-works) data store.
You can also specify an time interval in seconds under `appConfig.statusCheckInterval`, between checks, if this value is `0`, then status is only checked on initial page load, which is the default behavior. Status checks use the `url` attribute, but to call a different endpoint instead, you can set `statusCheckUrl`. Custom headers can also be specified using `statusCheckHeaders`.
<p align="center">
<img width="400" src="https://i.ibb.co/yBrVN4N/dashy-cloud-sync.png" />
<img alt="Status Checks demo" src="https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/status-check-demo.gif" width="600" />
</p>
**[⬆️ Back to Top](#dashy)**
@ -258,24 +258,6 @@ At present, access control is handled on the frontend, and therefore in security
</p>
**[⬆️ Back to Top](#dashy)**
---
## Status Indicators 🚦
> For full monitoring documentation, see: [**Status Indicators**](./docs/status-indicators.md)
Dashy has an optional feature that can display a small icon next to each of your running services, indicating it's current status. This is useful if you are using Dashy as your homelab's start page, as it gives you an overview of the health of each of your running services. Hovering over the indicator will show additional information, including average response time and an error message for services which are down.
By default, this feature is off, but you can enable it globally by setting `appConfig.statusCheck: true`, or enable/ disable it for an individual item, with `item[n].statusCheck`.
You can also specify an time interval in seconds under `appConfig.statusCheckInterval`, between checks, if this value is `0`, then status is only checked on initial page load, which is the default behavior. Status checks use the `url` attribute, but to call a different endpoint instead, you can set `statusCheckUrl`. Custom headers can also be specified using `statusCheckHeaders`.
<p align="center">
<img alt="Status Checks demo" src="https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/status-check-demo.gif" width="600" />
</p>
**[⬆️ Back to Top](#dashy)**
---
@ -292,17 +274,34 @@ One of the primary purposes of Dashy is to make launching commonly used apps and
Even if the target is not set (or is set to `sametab`), you can still launch any given app in an alternative method: Alt + Click will open the modal, and Ctrl + Click will open in a new tab. You can also right-click on any item to see all options (as seen in the screenshot below). This custom context menu can be disabled by setting `appConfig.disableContextMenu: true`.
In the workspace view, you can keep previously opened websites/ apps open in the background, by setting `appConfig.enableMultiTasking: true`. This comes at the cost of performance, but does mean that your session with each app is preserved, enabling you to quickly switch between your apps.
<p align="center">
<img width="500" src="https://i.ibb.co/vmZdSRt/dashy-context-menu-2.png" />
</p>
The modal and workspace views work by rendering the target application in an iframe. For this to work, the HTTP response header [`X-Frame-Options`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) for a given application needs to be set to `ALLOW`. If you are getting a `Refused to Connect` error then this header is set to `DENY` (or `SAMEORIGIN` and it's on a different host).
The modal and workspace views work by rendering the target application in an iframe. For this to work, the HTTP response header [`X-Frame-Options`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) for a given application needs to be set to `ALLOW`. If you are getting a `Refused to Connect` error then this header is set to `DENY` (or `SAMEORIGIN` and it's on a different host). Here are [instructions on how to do this](./docs/troubleshooting.md#refused-to-connect-in-modal-or-workspace-view) with common web servers.
---
## Alternate Views 👓
As well as the default homepage, there is also:
- A minimal view, useful for use as a browser start page
- A workspace view, useful for visiting many apps simultaneously
You can change the view from the UI, using the switch icon in the top-right corner, or select a default view in the config, under `appConfig.startingView` attribute (can be either `default`, `minimal` or `workspace`). Clicking the page title on any view will take you back to your default starting view.
Here's a quick demo of the workspace view:
<p align="center">
<b>Example of Workspace View</b><br>
<img alt="Workspace view demo" src="https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/workspace-demo.gif" width="600" />
</p>
<p align="center">
<b>Example of Minimal View</b><br>
<img alt="Workspace view demo" src="https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/minimal-view-demo.gif" width="600" />
</p>
**[⬆️ Back to Top](#dashy)**
---
@ -345,6 +344,24 @@ A full list of available config options can be found [here](./docs/configuring.m
---
## Cloud Backup & Sync ☁
> For full backup documentation, see: [**Cloud Backup & Sync**](./docs/backup-restore.md)
Dashy has an **optional** built-in feature for securely backing up your config to a hosted cloud service, and then restoring it on another instance. This feature is totally optional, and if you do not enable it, then Dashy will not make any external network requests.
This is useful not only for backing up your configuration off-site, but it also enables Dashy to be used without having write a YAML config file, and makes it possible to use a public hosted instance, without the need to self-host.
All data is encrypted before being sent to the backend. In Dashy, this is done in [`CloudBackup.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/CloudBackup.js), using [crypto.js](https://github.com/brix/crypto-js)'s AES method, using the users chosen password as the key. The data is then sent to a [Cloudflare worker](https://developers.cloudflare.com/workers/learning/how-workers-works) (a platform for running serverless functions), and stored in a [KV](https://developers.cloudflare.com/workers/learning/how-kv-works) data store.
<p align="center">
<img width="400" src="https://i.ibb.co/yBrVN4N/dashy-cloud-sync.png" />
</p>
**[⬆️ Back to Top](#dashy)**
---
## Language Switching 🌎
> For full internationalization documentation, see: [**Multi-Language Support**](./docs/multi-language-support.md)
@ -358,8 +375,8 @@ Dashy has the ability to support multiple languages and locales. When available,
- 🇸🇮 **Slovenian**: `sl` - Contributed by **[@UrekD](https://github.com/UrekD)**
#### Add your Language
It would be awesome for open source projects to be available to everyone, without language being a barrier to entry for non-native English speakers. If you have a few minutes to sapir, you're help with translating it would be very much appreciated.
There's not too much text to cover, 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 [Adding a New Language Docs](./docs/multi-language-support.md#adding-a-new-language), and feel free to reach out if you need any support.
I would love for Dashy to be available to everyone, without language being a barrier to entry for non-native English speakers. If you have a few minutes to sapir, you're help with translating it would be very much appreciated.
It's quite a quick task, all text is in [a single JSON file](https://github.com/Lissy93/dashy/tree/master/src/assets/locales), and you don't have to translate it all. For more info, see the [Adding a New Language Docs](./docs/multi-language-support.md#adding-a-new-language), and feel free to reach out if you need any support.
**[⬆️ Back to Top](#dashy)**
@ -367,15 +384,7 @@ There's not too much text to cover, and it's all located in [a single JSON file]
## Setting Dashboard Info 🌳
Page settings are defined under [`pageInfo`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#pageinfo). Here you can set things like title, sub-title, navigation links, footer text, etc
- `title` - Your dashboard title, displayed in the header and browser tab
- `description` - Description of your dashboard, also displayed as a subtitle
- `logo` - The path to an image to display in the header (to the right of the title). This can be either local, where `/` is the root of `./public`, or any remote image, such as `https://i.ibb.co/yhbt6CY/dashy.png`
- `navLinks` - Optional list of a maximum of 6 links, which will be displayed in the navigation bar, see [`pageInfo.navLinks`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#pageinfonavlinks-optional) for structure
- `footerText` - Text to display in the footer (note that this will override the default footer content). This can also include HTML and inline CSS
For example, a `pageInfo` section might look something like this:
Page settings are defined under [`pageInfo`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#pageinfo). Here you can set things like title, sub-title, navigation links, footer text, etc. For example:
```yaml
pageInfo:
@ -423,11 +432,14 @@ Found a bug, or something that isn't working as you'd expect? Please raise it as
If you're using Dashy, and would like to help support it's development, then that would be awesome! Contributions of any type, however small are always very much appreciated, and you will be appropriately credited for your effort.
Several areas that we need a bit of help with at the moment are:
- Adding translations - Help make Dashy available to non-native English speakers by adding text for you're language
- Adding translations - Help make Dashy available to non-native English speakers by [adding text for you're language](./docs/multi-language-support.md#adding-a-new-language)
- Donate a small amount, by [Sponsoring @Lissy93 on GitHub](https://github.com/sponsors/Lissy93) (only if you can afford to), and you'll also receive some extra perks!
- Community Engagement: Join the [discussion](https://github.com/Lissy93/dashy/discussions), and help answer other users questions, or spread the word by sharing Dashy online
- Complete a [short survey](https://n9fy6xak9yd.typeform.com/to/gl0L68ou), to have your say about future features
- Share your dashboard in the [Showcase](https://github.com/Lissy93/dashy/blob/master/docs/showcase.md#dashy-showcase-), to help provide inspiration for others
- Community Engagement: Join the [discussion](https://github.com/Lissy93/dashy/discussions), and help answer other users questions, suggest features, share tips and ask questions
- Spread the word, by sharing Dashy online, to help new users discover it
- Submit a PR, to add a new feature, fix a bug, update the docs, add a theme or something else
- Star Dashy on GitHub/ DockerHub or leave an upvote / review on [these platforms](https://github.com/Lissy93/dashy/blob/master/docs/contributing.md#star-upvote-or-leave-a-review)
[![Sponsor Lissy93 on GitHub](./docs/assets/sponsor-button.svg)](https://github.com/sponsors/Lissy93)
@ -463,7 +475,7 @@ Huge thanks to the sponsors helping to support Dashy's development!
<!-- readme: sponsors -end -->
#### Contributors
![Auto-generated contributors](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/CONTRIBUTORS.svg)
[![Auto-generated contributors](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/CONTRIBUTORS.svg)](./docs/credits.md)
#### Packages
Dashy was made possible thanks to the following packages and components. For more details on each, see [Dependency Credits](./docs/credits.md#dependencies-). Full credit to their respective authors.
@ -482,6 +494,8 @@ Dashy was made possible thanks to the following packages and components. For mor
[![Open Project in VS Code](https://img.shields.io/badge/Open_in-VS_Code-863cfc?style=for-the-badge&logo=visualstudiocode)](https://open.vscode.dev/Lissy93/Dashy)
Before getting started, you'll need [Git](https://git-scm.com/downloads), [Node](https://nodejs.org/en/download/) and optionally [Yarn](https://yarnpkg.com/) (run `npm i -g yarn`) installed.
To set up the development environment:
1. Get Code: `git clone git@github.com:Lissy93/dashy.git` and `cd dashy`
2. Install dependencies: `yarn`
@ -498,7 +512,7 @@ Like most Git repos, we are following the [Github Flow](https://guides.github.co
Branch names are specified in the following format: `[TYPE]/[TICKET]_[TITLE]`. E.g. `FEATURE/420_Awesome-feature` or `FIX/690_login-server-error`.
Most commit messages use git [commit emojis](https://gist.github.com/parmentf/035de27d6ed1dce0b36a) - e.g. ✨ = New feature, 🐛 = Bug fix, 💄 = UI stuff, 🚧 = Work in progress, 🔖 = New release, and so on. Take a look at [gitmoji.dev](https://gitmoji.dev/) for a list of what each emoji indicates
Most commit messages use git [commit emojis](https://gist.github.com/parmentf/035de27d6ed1dce0b36a) - e.g. ✨ = New feature, 🐛 = Bug fix, 💄 = UI stuff, 🚧 = Work in progress, 🌐 = Language, 🔖 = New release, and so on. Take a look at [gitmoji.dev](https://gitmoji.dev/) for a list of what each emoji indicates
Before you submit your pull request, please ensure you've checked off all the boxes in the template. For your PR to be merged, it must:
- Must be backwards compatible
@ -509,11 +523,11 @@ Before you submit your pull request, please ensure you've checked off all the bo
If you're new to web development, I've put together a short [list of resources](https://github.com/Lissy93/dashy/blob/master/docs/developing.md#resources-for-beginners), to help beginners get started
**Repo Status**:
![Open PRs](https://flat.badgen.net/github/open-prs/lissy93/dashy?icon=github)
![Total PRs](https://flat.badgen.net/github/prs/lissy93/dashy?icon=github)
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/lissy93/dashy?style=flat-square)
![Last Commit](https://flat.badgen.net/github/last-commit/lissy93/dashy?icon=github)
![Contributors](https://flat.badgen.net/github/contributors/lissy93/dashy?icon=github)
[![Open PRs](https://flat.badgen.net/github/open-prs/lissy93/dashy?icon=github)](https://github.com/Lissy93/dashy/pulls)
[![Total PRs](https://flat.badgen.net/github/prs/lissy93/dashy?icon=github)](https://github.com/Lissy93/dashy/pulls?q=)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/lissy93/dashy?style=flat-square)](https://github.com/Lissy93/dashy/commits/master)
[![Last Commit](https://flat.badgen.net/github/last-commit/lissy93/dashy?icon=github)](https://github.com/Lissy93/dashy/commits/master)
[![Contributors](https://flat.badgen.net/github/contributors/lissy93/dashy?icon=github)](https://github.com/Lissy93/dashy/graphs/contributors)
**[⬆️ Back to Top](#dashy)**
@ -575,6 +589,7 @@ There are a few self-hosted web apps, that serve a similar purpose to Dashy. If
- [Organizr](https://organizr.app/) (`GPL-3.0 License`)
- [Heimdall](https://github.com/linuxserver/Heimdall) (`MIT`)
- [Smashing](https://github.com/Smashing/smashing) (`MIT`)
- See more 👉 [Awesome Self-Hosted](https://github.com/awesome-selfhosted/awesome-selfhosted#personal-dashboards)
**[⬆️ Back to Top](#dashy)**

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -57,15 +57,16 @@ To disallow any changes from being written to disk via the UI config editor, set
**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.
**`startingView`** | `enum` | _Optional_ | Which page to load by default, and on the base page or domain root. You can still switch to different views from within the UI. Can be either `default`, `minimal` or `workspace`. Defaults to `default`
**`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`
**`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
**`enableFontAwesome`** | `boolean` | _Optional_ | Where `true` is enabled, if left blank font-awesome will be enabled only if required by 1 or more icons
**`fontAwesomeKey`** | `string` | _Optional_ | If you have a font-awesome key, then you can use it here and make use of premium icons. It is a 10-digit alpha-numeric string from you're FA kit URL (e.g. `13014ae648`)
**`faviconApi`** | `string` | _Optional_ | Only applicable if you are using favicons for item icons. Specifies which service to use to resolve favicons. Set to `local` to do this locally, without using an API. Services running locally will use this option always. Available options are: `local`, `faviconkit`, `google`, `clearbit`, `webmasterapi` and `allesedv`. Defaults to `faviconkit`. See [Icons](/docs/icons.md#favicons) for more info
**`faviconApi`** | `enum` | _Optional_ | Only applicable if you are using favicons for item icons. Specifies which service to use to resolve favicons. Set to `local` to do this locally, without using an API. Services running locally will use this option always. Available options are: `local`, `faviconkit`, `google`, `clearbit`, `webmasterapi` and `allesedv`. Defaults to `faviconkit`. See [Icons](/docs/icons.md#favicons) for more info
**`auth`** | `array` | _Optional_ | An array of objects containing usernames and hashed passwords. If this is not provided, then authentication will be off by default, and you will not need any credentials to access the app. Note authentication is done on the client side, and so if your instance of Dashy is exposed to the internet, it is recommend to configure your web server to handle this. See [`auth`](#appconfigauth-optional)
**`layout`** | `string` | _Optional_ | App layout, either `horizontal`, `vertical`, `auto` or `sidebar`. Defaults to `auto`. This specifies the layout and direction of how sections are positioned on the home screen. This can also be modified from the UI.
**`iconSize`** | `string` | _Optional_ | The size of link items / icons. Can be either `small`, `medium,` or `large`. Defaults to `medium`. This can also be set directly from the UI.
**`layout`** | `enum` | _Optional_ | App layout, either `horizontal`, `vertical`, `auto` or `sidebar`. Defaults to `auto`. This specifies the layout and direction of how sections are positioned on the home screen. This can also be modified from the UI.
**`iconSize`** | `enum` | _Optional_ | The size of link items / icons. Can be either `small`, `medium,` or `large`. Defaults to `medium`. This can also be set directly from the UI.
**`theme`** | `string` | _Optional_ | The default theme for first load (you can change this later from the UI)
**`cssThemes`** | `string[]` | _Optional_ | An array of custom theme names which can be used in the theme switcher dropdown
**`customColors`** | `object`| _Optional_ | Enables you to apply a custom color palette to any given theme. Use the theme name (lowercase) as the key, for an object including key-value-pairs, with the color variable name as keys, and 6-digit hex code as value. See [Theming](/docs/theming.md#modifying-theme-colors) for more info

View File

@ -34,14 +34,14 @@ Note that if you're theme is just for yourself, and you're not submitting a PR,
## Writing Translations
For full docs about Dashy's multi-language support, see [Multi-Language Support](./multi-language-support.md)
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.
@ -84,7 +84,7 @@ export const languages = [
},
];
```
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!
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.
@ -137,4 +137,42 @@ Checklist:
## 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.
---
## Hiding Page Furniture on Certain Routes
For some pages (such as the login page, the minimal start page, etc) the basic page furniture, (like header, footer, nav, etc) is not needed. This section explains how you can hide furniture on a new view (step 1), or add a component that should be hidden on certain views (step 2).
##### 1. Add the route name to the should hide array
In [`./src/utils/defaults.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/defaults.js), there's an array called `hideFurnitureOn`. Append the name of the route (the same as it appears in [`router.js`](https://github.com/Lissy93/dashy/blob/master/src/router.js)) here.
##### 2. Add the conditional to the structural component to hide
First, import the helper function:
```javascript
import { shouldBeVisible } from '@/utils/MiscHelpers';
```
Then you can create a computed value, that calls this function, passing in the route name:
```javascript
export default {
...
computed: {
...
isVisible() {
return shouldBeVisible(this.$route.name);
},
},
};
```
Finally, in the markup of your component, just add a `v-if` statement, referencing your computed value
```vue
<header v-if="isVisible">
...
</header>
```
---

View File

@ -1,6 +1,6 @@
{
"name": "Dashy",
"version": "1.5.8",
"version": "1.5.9",
"license": "MIT",
"main": "server",
"scripts": {

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="swatchbook" class="svg-inline--fa fa-swatchbook fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M112 424c13.25 0 24-10.75 24-24 0-13.26-10.75-24-24-24s-24 10.74-24 24c0 13.25 10.75 24 24 24zm368-136h-97.61l69.02-69.02c12.5-12.5 12.5-32.76 0-45.25L338.27 60.59c-6.25-6.25-14.44-9.37-22.63-9.37s-16.38 3.12-22.63 9.37L224 129.61V32c0-17.67-14.33-32-32-32H32C14.33 0 0 14.33 0 32v368c0 61.86 50.14 112 112 112h368c17.67 0 32-14.33 32-32V320c0-17.67-14.33-32-32-32zM176 400c0 17.88-7.41 34.03-19.27 45.65-3.65 3.57-7.7 6.53-11.99 9.05-.86.51-1.76.96-2.64 1.43-4.47 2.34-9.12 4.31-14.02 5.57-5.16 1.35-10.48 2.29-16.06 2.29H112c-35.29 0-64-28.71-64-64v-96h128V400zm0-144H48v-80h128v80zm0-128H48V48h128v80zm48 69.49l91.65-91.65 90.51 90.51L224 378.51V197.49zM464 464H206.39l128-128H464v128z"></path></svg>

After

Width:  |  Height:  |  Size: 928 B

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="home" class="svg-inline--fa fa-home fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M570.24 247.41L512 199.52V104a8 8 0 0 0-8-8h-32a8 8 0 0 0-7.95 7.88v56.22L323.87 45a56.06 56.06 0 0 0-71.74 0L5.76 247.41a16 16 0 0 0-2 22.54L14 282.25a16 16 0 0 0 22.53 2L64 261.69V448a32.09 32.09 0 0 0 32 32h128a32.09 32.09 0 0 0 32-32V344h64v104a32.09 32.09 0 0 0 32 32h128a32.07 32.07 0 0 0 32-31.76V261.67l27.53 22.62a16 16 0 0 0 22.53-2L572.29 270a16 16 0 0 0-2.05-22.59zM463.85 432H368V328a32.09 32.09 0 0 0-32-32h-96a32.09 32.09 0 0 0-32 32v104h-96V222.27L288 77.65l176 144.56z"></path></svg>

After

Width:  |  Height:  |  Size: 713 B

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="splotch" class="svg-inline--fa fa-splotch fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M472.29 195.89l-67.06-22.95c-19.28-6.6-33.54-20.92-38.14-38.3L351.1 74.19c-11.58-43.77-76.57-57.13-109.98-22.62l-46.14 47.67c-13.26 13.71-33.54 20.93-54.2 19.31l-71.88-5.62c-52.05-4.07-86.93 44.88-59.03 82.83l38.54 52.42c11.08 15.07 12.82 33.86 4.64 50.24L24.62 355.4c-20.59 41.25 22.84 84.87 73.49 73.81l69.96-15.28c20.11-4.39 41.45 0 57.07 11.73l54.32 40.83c39.32 29.56 101.04 7.57 104.45-37.22l4.7-61.86c1.35-17.79 12.8-33.86 30.63-42.99l62-31.74c44.88-22.96 39.59-80.17-8.95-96.79z"></path></svg>

After

Width:  |  Height:  |  Size: 719 B

View File

@ -0,0 +1,50 @@
<template>
<div
@click="selectSection(index)"
:class="`minimal-section-heading ${selected ? 'selected' : ''}`">
<h3>{{ title }}</h3>
</div>
</template>
<script>
export default {
name: 'MinimalHeadings',
props: {
index: Number,
title: String,
selected: Boolean,
},
methods: {
selectSection(index) {
this.$emit('sectionSelected', index);
},
},
};
</script>
<style scoped lang="scss">
@import '@/styles/media-queries.scss';
@import '@/styles/style-helpers.scss';
div.minimal-section-heading {
cursor: pointer;
padding: 0.5rem 0.25rem;
margin-bottom: 0;
background: var(--minimal-view-section-heading-background);
border: 1px solid var(--minimal-view-section-heading-color);
border-bottom: none;
border-radius: var(--curve-factor) var(--curve-factor) 0 0;
h3 {
margin: 0;
color: var(--minimal-view-section-heading-color);
}
&.selected {
background: var(--minimal-view-section-heading-color);
h3 {
color: var(--minimal-view-section-heading-background);
}
}
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<form>
<input
id="filter-tiles"
v-model="input"
ref="filter"
class="minimal-search"
:placeholder="$t('search.search-placeholder')"
v-on:input="userIsTypingSomething"
@keydown.esc="clearFilterInput" />
<i v-if="input.length > 0"
class="clear-search"
:title="$t('search.clear-search-tooltip')"
@click="clearFilterInput">x</i>
</form>
</template>
<script>
import ArrowKeyNavigation from '@/utils/ArrowKeyNavigation';
import { getCustomKeyShortcuts } from '@/utils/ConfigHelpers';
export default {
name: 'MinimalSearch',
props: {
active: Boolean,
},
data() {
return {
input: '', // Users current search term
akn: new ArrowKeyNavigation(), // Class that manages arrow key naviagtion
getCustomKeyShortcuts,
};
},
methods: {
/* Emmits users's search term up to parent */
userIsTypingSomething() {
this.$emit('user-is-searchin', this.input);
},
/* Resets everything to initial state, when user is finished */
clearFilterInput() {
this.input = ''; // Clear input model
this.userIsTypingSomething(); // Emmit new empty value
document.activeElement.blur(); // Remove focus
this.akn.resetIndex(); // Reset current element index
},
handleHotKey(key) {
const usersHotKeys = this.getCustomKeyShortcuts();
usersHotKeys.forEach((hotkey) => {
if (hotkey.hotkey === parseInt(key, 10)) {
if (hotkey.url) window.open(hotkey.url, '_blank');
}
});
},
startFiltering(event) {
const currentElem = document.activeElement.id;
const { key, keyCode } = event;
/* If a modal is open, then do nothing */
if (!this.active) return;
if (/^[a-zA-Z]$/.test(key) && currentElem !== 'filter-tiles') {
/* Letter key pressed - start searching */
if (this.$refs.filter) this.$refs.filter.focus();
this.userIsTypingSomething();
} else if (/^[0-9]$/.test(key)) {
/* Number key pressed, check if user has a custom binding */
this.handleHotKey(key);
} else if (keyCode >= 37 && keyCode <= 40) {
/* Arrow key pressed - start navigation */
this.akn.arrowNavigation(keyCode);
} else if (keyCode === 27) {
/* Esc key pressed - reset form */
this.clearFilterInput();
}
},
},
mounted() {
window.addEventListener('keydown', this.startFiltering);
},
beforeDestroy() {
window.removeEventListener('keydown', this.startFiltering);
},
};
</script>
<style scoped lang="scss">
@import '@/styles/media-queries.scss';
form {
display: flex;
align-items: center;
input {
display: inline-block;
width: 80%;
max-width: 400px;
font-size: 1.2rem;
padding: 0.5rem 1rem;
margin: 1rem auto;
outline: none;
border: 1px solid var(--outline-color);
border-radius: var(--curve-factor);
background: var(--minimal-view-search-background);
color: var(--minimal-view-search-color);
&:focus {
border-color: var(--minimal-view-search-color);
opacity: var(--dimming-factor);
}
}
.clear-search {
//position: absolute;
color: var(--minimal-view-search-color);
padding: 0.15rem 0.5rem 0.2rem 0.5rem;
font-style: normal;
font-size: 1rem;
opacity: var(--dimming-factor);
border-radius: 50px;
cursor: pointer;
right: 0.5rem;
top: 1rem;
border: 1px solid var(--minimal-view-search-color);
font-size: 1rem;
margin: 0.5rem;
&:hover {
opacity: 1;
color: var(--minimal-view-search-background);
background: var(--minimal-view-search-color);
}
}
}
</style>

View File

@ -0,0 +1,116 @@
<template>
<div :class="`minimal-section-inner ${selected ? 'selected' : ''} ${showAll ? 'show-all': ''}`">
<div class="section-items" v-if="selected || showAll">
<Item
v-for="(item, index) in items"
:id="`${index}_${makeId(item.title)}`"
:key="`${index}_${makeId(item.title)}`"
:url="item.url"
:title="item.title"
:description="item.description"
:icon="item.icon"
:target="item.target"
:color="item.color"
:backgroundColor="item.backgroundColor"
:statusCheckUrl="item.statusCheckUrl"
:statusCheckHeaders="item.statusCheckHeaders"
:itemSize="itemSize"
:hotkey="item.hotkey"
:enableStatusCheck="shouldEnableStatusCheck(item.statusCheck)"
:statusCheckInterval="getStatusCheckInterval()"
@itemClicked="$emit('itemClicked')"
@triggerModal="triggerModal"
/>
</div>
<IframeModal
:ref="`iframeModal-${groupId}`"
:name="`iframeModal-${groupId}`"
@closed="$emit('itemClicked')"
@modalChanged="modalChanged"
/>
</div>
</template>
<script>
import Item from '@/components/LinkItems/Item.vue';
import IframeModal from '@/components/LinkItems/IframeModal.vue';
export default {
name: 'ItemGroup',
inject: ['config'],
props: {
groupId: String,
title: String,
icon: String,
displayData: Object,
items: Array,
itemSize: String,
modalOpen: Boolean,
index: Number,
selected: Boolean,
showAll: Boolean,
},
components: {
Item,
IframeModal,
},
methods: {
selectSection(index) {
this.$emit('sectionSelected', index);
},
/* Returns a unique lowercase string, based on name, for section ID */
makeId(str) {
return str.replace(/\s+/g, '-').replace(/[^a-zA-Z ]/g, '').toLowerCase();
},
/* Opens the iframe modal */
triggerModal(url) {
this.$refs[`iframeModal-${this.groupId}`].show(url);
},
modalChanged(changedTo) {
this.$emit('change-modal-visibility', changedTo);
},
shouldEnableStatusCheck(itemPreference) {
const globalPreference = this.config.appConfig.statusCheck || false;
return itemPreference !== undefined ? itemPreference : globalPreference;
},
getStatusCheckInterval() {
let interval = this.config.appConfig.statusCheckInterval;
if (!interval) return 0;
if (interval > 60) interval = 60;
if (interval < 1) interval = 0;
return interval;
},
},
};
</script>
<style scoped lang="scss">
@import '@/styles/media-queries.scss';
@import '@/styles/style-helpers.scss';
.minimal-section-inner {
height: 100%;
display: flex;
flex-wrap: wrap;
flex-direction: column;
background: var(--minimal-view-group-background);
border-radius: 0 0 var(--curve-factor) var(--curve-factor);
.section-items {
display: grid;
@include phone { grid-template-columns: repeat(1, 1fr); }
@include tablet { grid-template-columns: repeat(2, 1fr); }
@include laptop { grid-template-columns: repeat(3, 1fr); }
@include monitor { grid-template-columns: repeat(4, 1fr); }
@include big-screen { grid-template-columns: repeat(5, 1fr); }
@include big-screen-up { grid-template-columns: repeat(6, 1fr); }
}
&.selected {
border: 1px solid var(--minimal-view-group-color);
grid-column-start: span var(--col-count, 3);
}
&.show-all {
border: none;
}
}
</style>

View File

@ -1,8 +1,8 @@
<template>
<!-- User Footer -->
<footer v-if="text && text !== ''" v-html="text"></footer>
<footer v-if="text && text !== '' && visible" v-html="text"></footer>
<!-- Default Footer -->
<footer v-else>
<footer v-else-if="visible">
Developed by <a :href="authorUrl">{{authorName}}</a>.
Licensed under <a :href="licenseUrl">{{license}}</a>
{{ showCopyright? '©': '' }} {{date}}.
@ -11,6 +11,9 @@
</template>
<script>
import { shouldBeVisible } from '@/utils/MiscHelpers';
export default {
name: 'Footer',
props: {
@ -23,6 +26,11 @@ export default {
showCopyright: { type: Boolean, default: true },
repoUrl: { type: String, default: 'https://github.com/lissy93/dashy' },
},
computed: {
visible() {
return shouldBeVisible(this.$route.name);
},
},
};
</script>

View File

@ -1,5 +1,5 @@
<template>
<header>
<header v-if="visible">
<PageTitle
v-if="titleVisible"
:title="pageInfo.title"
@ -14,6 +14,7 @@
import PageTitle from '@/components/PageStrcture/PageTitle.vue';
import Nav from '@/components/PageStrcture/Nav.vue';
import { visibleComponents as defaultVisibleComponents } from '@/utils/defaults';
import { shouldBeVisible } from '@/utils/MiscHelpers';
export default {
name: 'Header',
@ -31,6 +32,11 @@ export default {
navVisible: (this.visibleComponents || defaultVisibleComponents).navigation,
};
},
computed: {
visible() {
return shouldBeVisible(this.$route.name);
},
},
};
</script>

View File

@ -1,10 +1,12 @@
<template>
<div class="config-options">
<div class="config-options" v-click-outside="closeViewSwitcher">
<!-- Button and label -->
<span>{{ $t('settings.config-launcher-label') }}</span>
<span class="config-label">{{ $t('settings.config-launcher-label') }}</span>
<div class="config-buttons">
<IconSpanner @click="showEditor()" tabindex="-2"
v-tooltip="tooltip($t('settings.config-launcher-tooltip'))" />
<IconViewMode @click="openChangeViewMenu()" tabindex="-2"
v-tooltip="tooltip($t('settings.config-launcher-tooltip'))" />
</div>
<!-- Modal containing all the configuration options -->
@ -19,27 +21,55 @@
<LanguageSwitcher />
</modal>
<!-- Menu for switching view -->
<div v-if="viewSwitcherOpen" class="view-switcher">
<ul>
<li>
<router-link to="/home">
<IconHome /><span>Default</span>
</router-link>
</li>
<li>
<router-link to="/minimal">
<IconMinimalView /><span>Minimal</span>
</router-link>
<li>
<router-link to="/workspace">
<IconWorkspaceView /><span>Workspace</span>
</router-link>
</li>
</ul>
</div>
</div>
</template>
<script>
import IconSpanner from '@/assets/interface-icons/config-editor.svg';
import ConfigContainer from '@/components/Configuration/ConfigContainer';
import LanguageSwitcher from '@/components/Settings/LanguageSwitcher';
import { topLevelConfKeys, localStorageKeys, modalNames } from '@/utils/defaults';
import IconSpanner from '@/assets/interface-icons/config-editor.svg';
import IconViewMode from '@/assets/interface-icons/application-change-view.svg';
import IconHome from '@/assets/interface-icons/application-home.svg';
import IconWorkspaceView from '@/assets/interface-icons/open-workspace.svg';
import IconMinimalView from '@/assets/interface-icons/application-minimal.svg';
export default {
name: 'ConfigLauncher',
data() {
return {
modalNames,
viewSwitcherOpen: false,
};
},
components: {
IconSpanner,
ConfigContainer,
LanguageSwitcher,
IconSpanner,
IconViewMode,
IconHome,
IconWorkspaceView,
IconMinimalView,
},
props: {
sections: Array,
@ -48,7 +78,6 @@ export default {
},
methods: {
showEditor: function show() {
// TODO: If users first time, then show note explaining that config is only stored locally
this.$modal.show(modalNames.CONF_EDITOR);
this.$emit('modalChanged', true);
},
@ -64,6 +93,12 @@ export default {
tooltip(content) {
return { content, trigger: 'hover focus', delay: 250 };
},
openChangeViewMenu() {
this.viewSwitcherOpen = !this.viewSwitcherOpen;
},
closeViewSwitcher() {
this.viewSwitcherOpen = false;
},
},
};
</script>
@ -93,4 +128,38 @@ export default {
}
}
}
.view-switcher {
position: absolute;
right: 1rem;
margin-top: 3rem;
z-index: 5;
background: var(--background);
border: 1px solid var(--settings-text-color);
border-radius: var(--curve-factor);
box-shadow: var(--settings-container-shadow);
ul {
list-style: none;
margin: 0;
padding: 0;
li {
cursor: pointer;
padding: 0.25rem 0.75rem;
a {
color: var(--settings-text-color);
text-decoration: none;
display: flex;
align-items: center;
}
&:hover {
background: var(--settings-text-color);
a { color: var(--background); }
}
svg {
margin: 0 0.25rem 0 0;
border: none;
}
}
}
}
</style>

View File

@ -1,13 +1,24 @@
/**
* This is the router config, which defined the location for
* each page within the app, and how they should be loaded
* Note that the page paths are defined in @/utils/defaults.js
*/
// Import Vue.js and vue router
import Vue from 'vue';
import Router from 'vue-router';
// Import views
import Home from '@/views/Home.vue';
import Login from '@/views/Login.vue';
import Workspace from '@/views/Workspace.vue';
import Minimal from '@/views/Minimal.vue';
import DownloadConfig from '@/views/DownloadConfig.vue';
// Import helper functions, config data and defaults
import { isLoggedIn } from '@/utils/Auth';
import { config } from '@/utils/ConfigHelpers';
import { metaTagData } from '@/utils/defaults';
import { metaTagData, startingView, routePaths } from '@/utils/defaults';
Vue.use(Router);
@ -21,30 +32,62 @@ const isAuthenticated = () => {
return (!users || users.length === 0 || isLoggedIn(users));
};
/* Get the users chosen starting view from app config, or return default */
const getStartingView = () => config.appConfig.startingView || startingView;
/**
* Returns the component that should be rendered at the base path,
* Defaults to Home, but the user can change this to Workspace of Minimal
*/
const getStartingComponent = () => {
const usersPreference = getStartingView();
switch (usersPreference) {
case 'default': return Home;
case 'minimal': return Minimal;
case 'workspace': return Workspace;
default: return Home;
}
};
/* Returns the meta tags for each route */
const makeMetaTags = (defaultTitle) => ({
title: config.pageInfo.title || defaultTitle,
metaTags: metaTagData,
});
/* List of all routes, props, components and metadata */
const router = new Router({
routes: [
{
{ // The default view can be customized by the user
path: '/',
name: `landing-page-${getStartingView()}`,
component: getStartingComponent(),
props: config,
meta: makeMetaTags('Home Page'),
},
{ // Default home page
path: routePaths.home,
name: 'home',
component: Home,
props: config,
meta: {
title: config.pageInfo.title || 'Home Page',
metaTags: metaTagData,
},
meta: makeMetaTags('Home Page'),
},
{
path: '/workspace',
{ // Workspace view page
path: routePaths.workspace,
name: 'workspace',
component: Workspace,
props: config,
meta: {
title: config.pageInfo.title || 'Dashy Workspace',
metaTags: metaTagData,
},
meta: makeMetaTags('Workspace'),
},
{
path: '/login',
{ // Minimal view page
path: routePaths.minimal,
name: 'minimal',
component: Minimal,
props: config,
meta: makeMetaTags('Start Page'),
},
{ // The login page
path: routePaths.login,
name: 'login',
component: Login,
props: {
@ -55,34 +98,38 @@ const router = new Router({
next();
},
},
{
path: '/about',
{ // The about app page
path: routePaths.about,
name: 'about',
component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
meta: makeMetaTags('About Dashy'),
},
{
path: '/download',
{ // The export config page
path: routePaths.download,
name: 'download',
component: DownloadConfig,
props: config,
meta: {
title: config.pageInfo.title || 'Download Dashy Config',
metaTags: metaTagData,
},
meta: makeMetaTags('Download Config'),
},
],
});
/**
* Before loading a route, check if the user has authentication enabled *
* if so, then ensure that they are correctly logged in as a valid user *
* If not logged in, prevent access and redirect them to the login page *
* */
router.beforeEach((to, from, next) => {
if (to.name !== 'login' && !isAuthenticated()) next({ name: 'login' });
else next();
});
const defaultTitle = 'Dashy';
/* If title is missing, then apply default page title */
router.afterEach((to) => {
Vue.nextTick(() => {
document.title = to.meta.title || defaultTitle;
document.title = to.meta.title || 'Dashy';
});
});
// Export the now configured router
export default router;

View File

@ -35,54 +35,74 @@
* all variables are optional, since they inherit initial values from above*
* Using specific variables makes overriding for custom themes really easy */
--heading-text-color: var(--primary);
// Nav-bar links
--nav-link-text-color: var(--primary);
--nav-link-background-color: #607d8b33;
--nav-link-text-color-hover: var(--primary);
--nav-link-background-color-hover: #607d8b33;
--nav-link-border-color: transparent;
--nav-link-border-color-hover: var(--primary);
// Link items and sections
--item-text-color: var(--primary);
--item-text-color-hover: var(--item-text-color);
--item-group-outer-background: var(--primary);
--item-group-heading-text-color: var(--item-group-background);
--item-group-heading-text-color-hover: var(--background);
// Settings and config
--settings-background: var(--background);
--settings-text-color: var(--primary);
--search-container-background: var(--background-darker);
--search-field-background: var(--background);
--search-label-color: var(--settings-text-color);
--footer-text-color: var(--medium-grey);
--footer-text-color-link: var(--primary);
--footer-background: var(--background-darker);
--welcome-popup-background: var(--background-darker);
--welcome-popup-text-color: var(--primary);
--config-code-background: #fff;
--config-code-color: var(--background);
--config-settings-color: var(--primary);
--config-settings-background: var(--background-darker);
--toast-background: var(--primary);
--toast-color: var(--background);
--code-editor-color: var(--black);
--code-editor-background: var(--white);
// Search bar (on homepage)
--search-container-background: var(--background-darker);
--search-field-background: var(--background);
--search-label-color: var(--settings-text-color);
// Page footer
--footer-text-color: var(--medium-grey);
--footer-text-color-link: var(--primary);
--footer-background: var(--background-darker);
// Right-click context menu
--context-menu-background: var(--background);
--context-menu-color: var(--primary);
--context-menu-secondary-color: var(--background-darker);
// Workspace view
--side-bar-background: var(--background-darker);
--side-bar-background-lighter: var(--background);
--side-bar-color: var(--primary);
// Minimal view
--minimal-view-background-color: var(--background);
--minimal-view-title-color: var(--primary);
--minimal-view-settings-color: var(--primary);
--minimal-view-section-heading-color: var(--primary);
--minimal-view-section-heading-background: var(--background-darker);
--minimal-view-search-background: var(--background-darker);
--minimal-view-search-color: var(--primary);
--minimal-view-group-color: var(--primary);
--minimal-view-group-background: var(--background-darker);
// Login page
--login-form-color: var(--primary);
--login-form-background: var(--background);
--login-form-background-secondary: var(--background-darker);
// About page
--about-page-color: var(--white);
--about-page-background: var(--background);
--about-page-accent: var(--primary);
// Webpage colors, highlight, scrollbar
--scroll-bar-color: var(--primary);
--scroll-bar-background: var(--background-darker);
--highlight-color: var(--background);
--highlight-background: var(--primary);
// Misc components
--loading-screen-color: var(--primary);
--loading-screen-background: var(--background);
--login-form-color: var(--primary);
--login-form-background: var(--background);
--login-form-background-secondary: var(--background-darker);
--about-page-color: var(--white);
--about-page-background: var(--background);
--about-page-accent: var(--primary);
--side-bar-background: var(--background-darker);
--side-bar-background-lighter: var(--background);
--side-bar-color: var(--primary);
--status-check-tooltip-background: var(--background-darker);
--status-check-tooltip-color: var(--primary);
--code-editor-color: var(--black);
--code-editor-background: var(--white);
--context-menu-background: var(--background);
--context-menu-color: var(--primary);
--context-menu-secondary-color: var(--background-darker);
--welcome-popup-background: var(--background-darker);
--welcome-popup-text-color: var(--primary);
--toast-background: var(--primary);
--toast-color: var(--background);
}

View File

@ -481,7 +481,7 @@ html[data-theme='material'], html[data-theme='material-dark'] {
html[data-theme='material'] {
--primary: #363636;
--background: #f5f5f5;
--background: #eee;
--background-darker: #5c90eb;
--item-group-outer-background: none;
--item-group-shadow: none;
@ -501,7 +501,7 @@ html[data-theme='material'] {
--config-settings-background: #f5f5f5;
--config-settings-color: #473f3f;
--heading-text-color: #fff;
--curve-factor: 4px;
--curve-factor: 3px;
--curve-factor-navbar: 8px;
--search-container-background: #4285f4;
--welcome-popup-text-color: #f5f5f5;
@ -510,6 +510,30 @@ html[data-theme='material'] {
--context-menu-secondary-color: #f5f5f5;
--transparent-white-50: #00000080;
--minimal-view-background-color: var(--background);
--minimal-view-title-color: var(--background-darker);
--minimal-view-settings-color: var(--primary);
--minimal-view-section-heading-color: var(--primary);
--minimal-view-section-heading-background: #f6f6f6;
--minimal-view-search-background: #fff;
--minimal-view-search-color: var(--primary);
--minimal-view-group-color: var(--primary);
--minimal-view-group-background: #fff;
.minimal-section-inner.selected, div.minimal-section-heading {
border: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
}
.title-and-search form input {
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
}
div.minimal-section-heading.selected {
background: #5c90eb;
}
div.minimal-section-inner {
background: #ffffff80;
}
div.jsoneditor div.jsoneditor-menu {
background: #5c90eb !important;
}
@ -585,6 +609,25 @@ html[data-theme='material-dark'] {
// --login-form-color: #131a1f;
--login-form-background-secondary: #131a1f;
// --minimal-view-background-color: var(--background);
// --minimal-view-title-color: var(--primary);
// --minimal-view-settings-color: var(--primary);
--minimal-view-section-heading-color: #131a1f;
--minimal-view-section-heading-background: var(--background);
--minimal-view-search-background: #131a1f;
// --minimal-view-search-color: var(--primary);
// --minimal-view-group-color: var(--primary);
--minimal-view-group-background: #131a1f;
div.minimal-section-heading h3, div.minimal-section-heading.selected h3 {
color: #d5d5d5;
}
div.minimal-section-heading, .minimal-section-inner.selected, input.minimal-search {
box-shadow: 2px 2px 4px #000000, 0 1px 3px #000000cc;
border-color: #131a1f80;
}
&::-webkit-scrollbar-thumb {
border-left: 1px solid #131a1f;
}
@ -812,6 +855,10 @@ html[data-theme="oblivion-scotch"] {
border: 1px solid #313d4f;
box-shadow: 0 1px 5px #0c0d0e;
}
.minimal-home div.item-group-container, input.minimal-search {
box-shadow: 0 1px 6px #00000099, 0 1px 1px #000000cc;
}
}
html[data-theme="oblivion-blue"] {

View File

@ -15,6 +15,30 @@
}
}
.svg-button {
color: var(--primary);
width: 1.5rem;
height: 1.5rem;
svg {
path {
fill: var(--settings-text-color);
}
width: 1rem;
height: 1rem;
margin: 0.2rem;
padding: 0.2rem;
text-align: center;
background: var(--background);
border: 1px solid currentColor;
border-radius: var(--curve-factor);
cursor: pointer;
&:hover, &.selected {
background: var(--settings-text-color);
path { fill: var(--background); }
}
}
}
/* Custom highlight color */
.highlight {
::selection {

View File

@ -67,6 +67,15 @@
"type": "string",
"description": "The ISO code of your desired language, must have translations present, check docs for more info"
},
"startingView": {
"enum": [
"default",
"minimal",
"workspace"
],
"default": "default",
"description": "Which page to load by default, and on the base page or domain root. You can still switch to different views from within the UI"
},
"theme": {
"type": "string",
"default": "callisto",

6
src/utils/MiscHelpers.js Normal file
View File

@ -0,0 +1,6 @@
import { hideFurnitureOn } from '@/utils/defaults';
/* Returns false if page furniture should be hidden on said route */
export const shouldBeVisible = (routeName) => !hideFurnitureOn.includes(routeName);
export const x = () => null;

View File

@ -13,6 +13,8 @@ module.exports = {
appConfig: {},
/* Default language code */
language: 'en',
/* The page to use as the starting homepage */
startingView: 'default',
/* Default icon size to be applied on initial load */
iconSize: 'medium',
/* Default layout to be applied on initial load */
@ -23,6 +25,15 @@ module.exports = {
fontAwesomeKey: '0821c65656',
/* Default API to use for fetching of user service favicon icons (if enabled) */
faviconApi: 'faviconkit',
/* The page paths for each route within the app for the router */
routePaths: {
home: '/home',
minimal: '/minimal',
workspace: '/workspace',
about: '/about',
login: '/login',
download: '/download',
},
/* List of built-in themes, to be displayed within the theme-switcher dropdown */
builtInThemes: [
'callisto',
@ -58,6 +69,13 @@ module.exports = {
settings: true,
footer: true,
},
/* A list of route names that page furniture (header, footer, etc) should be hidden on */
hideFurnitureOn: [
'minimal',
'login',
'download',
'landing-page-minimal',
],
/* Key names for local storage identifiers */
localStorageKeys: {
LANGUAGE: 'language',

288
src/views/Minimal.vue Normal file
View File

@ -0,0 +1,288 @@
<template>
<div class="minimal-home" :style="getBackgroundImage() + setColumnCount()">
<!-- Buttons for config and home page -->
<div class="minimal-buttons">
<ConfigLauncher :sections="sections" :pageInfo="pageInfo" :appConfig="appConfig"
@modalChanged="modalChanged" class="config-launcher" />
</div>
<!-- Page title and search bar -->
<div class="title-and-search">
<router-link to="/">
<h1>{{ pageInfo.title }}</h1>
</router-link>
<MinimalSearch @user-is-searchin="(s) => { this.searchValue = s; }" :active="!modalOpen" />
</div>
<div v-if="checkTheresData(sections)"
:class="`item-group-container ${!tabbedView ? 'showing-all' : ''}`">
<!-- Section heading buttons -->
<MinimalHeading
v-for="(section, index) in getSections(sections)"
:key="`heading-${index}`"
:index="index"
:title="section.name"
:selected="selectedSection === index"
@sectionSelected="sectionSelected"
class="headings"
/>
<!-- Section item groups -->
<MinimalSection
v-for="(section, index) in getSections(sections)"
:key="`body-${index}`"
:index="index"
:title="section.name"
:icon="section.icon || undefined"
:groupId="`section-${index}`"
:items="filterTiles(section.items)"
:selected="selectedSection === index"
:showAll="!tabbedView"
itemSize="small"
@sectionSelected="sectionSelected"
@itemClicked="finishedSearching()"
@change-modal-visibility="updateModalVisibility"
/>
<div v-if="checkIfResults()" class="no-data">
{{searchValue ? $t('home.no-results') : $t('home.no-data')}}
</div>
</div>
<div v-else class="no-data"> {{ $t('home.no-data') }} </div>
</div>
</template>
<script>
import MinimalSection from '@/components/MinimalView/MinimalSection.vue';
import MinimalHeading from '@/components/MinimalView/MinimalHeading.vue';
import MinimalSearch from '@/components/MinimalView/MinimalSearch.vue';
import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHelper';
import Defaults, { localStorageKeys } from '@/utils/defaults';
import ConfigLauncher from '@/components/Settings/ConfigLauncher';
export default {
name: 'home',
props: {
sections: Array, // Main site content
appConfig: Object, // Main site configuation (optional)
pageInfo: Object,
},
components: {
MinimalSection,
MinimalHeading,
MinimalSearch,
ConfigLauncher,
},
data: () => ({
searchValue: '',
layout: '',
modalOpen: false, // When true, keybindings are disabled
selectedSection: 0, // The index of currently selected section
tabbedView: true, // By default use tabs, when searching then show all instead
theme: GetTheme(),
}),
watch: {
/* When the theme changes, then call the update method */
searchValue() {
this.tabbedView = !(this.searchValue.length > 0);
},
},
methods: {
sectionSelected(index) {
this.selectedSection = index;
},
/* Returns true if there is one or more sections in the config */
checkTheresData(sections) {
const localSections = localStorage[localStorageKeys.CONF_SECTIONS];
return (sections && sections.length >= 1) || (localSections && localSections.length >= 1);
},
/* Returns sections from local storage if available, otherwise uses the conf.yml */
getSections(sections) {
// If the user has stored sections in local storage, return those
const localSections = localStorage[localStorageKeys.CONF_SECTIONS];
if (localSections) {
const json = JSON.parse(localSections);
if (json.length >= 1) return json;
}
// Otherwise, return the usuall data from conf.yml
return sections;
},
/* Updates local data with search value, triggered from filter comp */
searching(searchValue) {
this.searchValue = searchValue || '';
},
/* Clears input field, once a searched item is opened */
finishedSearching() {
this.$refs.filterComp.clearFilterInput();
},
/* Extracts the site name from domain, used for the searching functionality */
getDomainFromUrl(url) {
if (!url) return '';
const urlPattern = /^(?:https?:\/\/)?(?:w{3}\.)?([a-z\d.-]+)\.(?:[a-z.]{2,10})(?:[/\w.-]*)*/;
const domainPattern = url.match(urlPattern);
return domainPattern ? domainPattern[1] : '';
},
/* Returns only the tiles that match the users search query */
filterTiles(allTiles) {
if (!allTiles) return [];
return allTiles.filter((tile) => {
const {
title, description, provider, url,
} = tile;
const searchTerm = this.searchValue.toLowerCase();
return (title && title.toLowerCase().includes(searchTerm))
|| (provider && provider.toLowerCase().includes(searchTerm))
|| (description && description.toLowerCase().includes(searchTerm))
|| this.getDomainFromUrl(url).includes(searchTerm);
});
},
/* Update data when modal is open (so that key bindings can be disabled) */
updateModalVisibility(modalState) {
this.modalOpen = modalState;
},
/* Checks if any of the icons are Font Awesome glyphs */
checkIfFontAwesomeNeeded() {
let isNeeded = false;
if (!this.sections) return false;
this.sections.forEach((section) => {
if (section.icon && section.icon.includes('fa-')) isNeeded = true;
section.items.forEach((item) => {
if (item.icon && item.icon.includes('fa-')) isNeeded = true;
});
});
return isNeeded;
},
/* Injects font-awesome's script tag, only if needed */
initiateFontAwesome() {
if (this.appConfig.enableFontAwesome || this.checkIfFontAwesomeNeeded()) {
const fontAwesomeScript = document.createElement('script');
const faKey = this.appConfig.fontAwesomeKey || Defaults.fontAwesomeKey;
fontAwesomeScript.setAttribute('src', `https://kit.fontawesome.com/${faKey}.js`);
document.head.appendChild(fontAwesomeScript);
}
},
/* Returns true if there is more than 1 sub-result visible during searching */
checkIfResults() {
if (!this.sections) return false;
else {
let itemsFound = true;
this.sections.forEach((section) => {
if (this.filterTiles(section.items).length > 0) itemsFound = false;
});
return itemsFound;
}
},
/* Make CSS to set the number of columns based on the number of sections */
setColumnCount() {
return `--col-count: ${this.sections.length};`;
},
/* Make CSS styles to apply the users custom background image */
getBackgroundImage() {
if (this.appConfig && this.appConfig.backgroundImg) {
return `background: url('${this.appConfig.backgroundImg}');background-size:cover;`;
}
return '';
},
/* If theme present, then call helper to apply it, and any custom colors */
applyTheme() {
if (this.theme) {
ApplyLocalTheme(this.theme);
ApplyCustomVariables(this.theme);
}
},
modalChanged(modalState) {
this.modalOpen = modalState;
},
},
mounted() {
this.initiateFontAwesome();
this.applyTheme();
},
};
</script>
<style lang="scss" scoped>
@import '@/styles/media-queries.scss';
@import '@/styles/style-helpers.scss';
.minimal-home {
display: flex;
flex-direction: column;
margin: 1rem auto;
padding-bottom: 1px;
padding-top: 10vh;
min-height: calc(99vh - var(--footer-height));
width: 90%;
max-width: 1000px;
background: var(--minimal-view-background-color);
}
.title-and-search {
text-align: center;
h1 {
color: var(--minimal-view-title-color);
margin: 0;
font-size: 3rem;
}
a {
text-decoration: none;
}
}
/* Outside container wrapping the item groups*/
.item-group-container {
display: grid;
gap: 0 0.5rem;
margin: 3rem auto;
width: 90%;
grid-template-columns: repeat(var(--col-count), 1fr);
@extend .scroll-bar;
&.showing-all {
flex-direction: column;
display: flex;
.headings {
display: none;
}
}
}
@include phone {
.item-group-container {
display: flex;
flex-direction: column;
}
}
.no-data {
font-size: 2rem;
color: var(--minimal-view-background-color);
background: #ffffffeb;
width: fit-content;
margin: 2rem auto;
padding: 0.5rem 1rem;
border-radius: var(--curve-factor);
}
.minimal-buttons {
position: absolute;
top: 0.5rem;
right: 1rem;
display: flex;
.home-page-icon {
color: var(--minimal-view-settings-color);
width: 1.5rem;
height: 1.5rem;
@extend .svg-button;
}
}
</style>
<style lang="scss">
.minimal-home .minimal-buttons {
.config-launcher span.config-label { display: none; }
svg { opacity: var(--dimming-factor); border: none; }
&:hover svg { opacity: 1; }
.view-switcher {
margin-top: 2rem;
right: 0;
}
}
</style>