mirror of https://github.com/bastienwirtz/homer
Merge branch 'main' into main
commit
92d5b8d424
14
README.md
14
README.md
|
@ -43,8 +43,8 @@
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Getting started](#getting-started)
|
- [Getting started](#getting-started)
|
||||||
- [Configuration](docs/configuration.md)
|
- [Configuration](docs/configuration.md)
|
||||||
|
- [Custom services](docs/customservices.md)
|
||||||
- [Tips & tricks](docs/tips-and-tricks.md)
|
- [Tips & tricks](docs/tips-and-tricks.md)
|
||||||
- [Roadmap](#roadmap)
|
|
||||||
- [Development](docs/development.md)
|
- [Development](docs/development.md)
|
||||||
|
|
||||||
|
|
||||||
|
@ -73,7 +73,11 @@ See [documentation](docs/configuration.md) for information about the configurati
|
||||||
To launch container:
|
To launch container:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run -p 8080:8080 -v /your/local/assets/:/www/assets b4bz/homer:latest
|
docker run -d \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-v </your/local/assets/>:/www/assets \
|
||||||
|
--restart=always \
|
||||||
|
b4bz/homer:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Default assets will be automatically installed in the `/www/assets` directory. Use `UID` and/or `GID` env var to change the assets owner (`docker run -e "UID=1000" -e "GID=1000" [...]`).
|
Default assets will be automatically installed in the `/www/assets` directory. Use `UID` and/or `GID` env var to change the assets owner (`docker run -e "UID=1000" -e "GID=1000" [...]`).
|
||||||
|
@ -130,9 +134,3 @@ npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
Then your dashboard is ready to use in the `/dist` directory.
|
Then your dashboard is ready to use in the `/dist` directory.
|
||||||
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
- [ ] Add new themes.
|
|
||||||
- [ ] Add support for custom service card (add custom feature to some service / app link)
|
|
||||||
|
|
|
@ -66,6 +66,17 @@ colors:
|
||||||
# Optional message
|
# Optional message
|
||||||
message:
|
message:
|
||||||
# url: "https://<my-api-endpoint>" # Can fetch information from an endpoint to override value below.
|
# url: "https://<my-api-endpoint>" # Can fetch information from an endpoint to override value below.
|
||||||
|
# mapping: # allows to map fields from the remote format to the one expected by Homer
|
||||||
|
# title: 'id' # use value from field 'id' as title
|
||||||
|
# content: 'value' # value from field 'value' as content
|
||||||
|
# refreshInterval: 10000 # Optional: time interval to refresh message
|
||||||
|
#
|
||||||
|
# Real example using chucknorris.io for showing Chuck Norris facts as messages:
|
||||||
|
# url: https://api.chucknorris.io/jokes/random
|
||||||
|
# mapping:
|
||||||
|
# title: 'id'
|
||||||
|
# content: 'value'
|
||||||
|
# refreshInterval: 10000
|
||||||
style: "is-warning"
|
style: "is-warning"
|
||||||
title: "Optional message!"
|
title: "Optional message!"
|
||||||
icon: "fa fa-exclamation-triangle"
|
icon: "fa fa-exclamation-triangle"
|
||||||
|
@ -81,6 +92,11 @@ links:
|
||||||
- name: "link 2"
|
- name: "link 2"
|
||||||
icon: "fas fa-book"
|
icon: "fas fa-book"
|
||||||
url: "https://github.com/bastienwirtz/homer"
|
url: "https://github.com/bastienwirtz/homer"
|
||||||
|
# this will link to a second homer page that will load config from page2.yml and keep default config values as in config.yml file
|
||||||
|
# see url field and assets/page.yml used in this example:
|
||||||
|
- name: "Second Page"
|
||||||
|
icon: "fas fa-file-alt"
|
||||||
|
url: "#page2"
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
# First level array represents a group.
|
# First level array represents a group.
|
||||||
|
@ -88,6 +104,8 @@ links:
|
||||||
services:
|
services:
|
||||||
- name: "Application"
|
- name: "Application"
|
||||||
icon: "fas fa-code-branch"
|
icon: "fas fa-code-branch"
|
||||||
|
# A path to an image can also be provided. Note that icon take precedence if both icon and logo are set.
|
||||||
|
# logo: "path/to/logo"
|
||||||
items:
|
items:
|
||||||
- name: "Awesome app"
|
- name: "Awesome app"
|
||||||
logo: "assets/tools/sample.png"
|
logo: "assets/tools/sample.png"
|
||||||
|
@ -118,9 +136,10 @@ services:
|
||||||
# background: red # optional color for card to set color directly without custom stylesheet
|
# background: red # optional color for card to set color directly without custom stylesheet
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
View [Custom Services](customservices.md) for details about all available custom services (like PiHole) and how to configure them.
|
View [Custom Services](customservices.md) for details about all available custom services (like PiHole) and how to configure them.
|
||||||
|
|
||||||
If you choose to fetch message information from an endpoint, the output format should be:
|
If you choose to fetch message information from an endpoint, the output format should be as follows (or you can [custom map fields as shown in tips-and-tricks](./tips-and-tricks.md#mapping-fields)):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# Custom Services
|
# Custom Services
|
||||||
|
|
||||||
Here is an overview of all custom services that are available within Homer.
|
Some service can use a specific a component that provides some extra features by adding a `type` key to the service yaml
|
||||||
|
configuration. Available services are in `src/components/`. Here is an overview of all custom services that are available
|
||||||
|
within Homer.
|
||||||
|
|
||||||
## PiHole
|
## PiHole
|
||||||
|
|
||||||
|
@ -17,6 +19,7 @@ The following configuration is available for the PiHole service.
|
||||||
type: "PiHole"
|
type: "PiHole"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## OpenWeatherMap
|
## OpenWeatherMap
|
||||||
|
|
||||||
Using the OpenWeatherMap service you can display weather information about a given location.
|
Using the OpenWeatherMap service you can display weather information about a given location.
|
||||||
|
@ -36,3 +39,37 @@ items:
|
||||||
|
|
||||||
**Remarks:**
|
**Remarks:**
|
||||||
If for some reason your city can't be found by entering the name in the `location` property, you could also try to configure the OWM city ID in the `locationId` property. To retrieve your specific City ID, go to the [OWM website](https://openweathermap.org), search for your city and retrieve the ID from the URL (for example, the City ID of Amsterdam is 2759794).
|
If for some reason your city can't be found by entering the name in the `location` property, you could also try to configure the OWM city ID in the `locationId` property. To retrieve your specific City ID, go to the [OWM website](https://openweathermap.org), search for your city and retrieve the ID from the URL (for example, the City ID of Amsterdam is 2759794).
|
||||||
|
|
||||||
|
|
||||||
|
## Medusa
|
||||||
|
|
||||||
|
This service displays News (grey), Warning (orange) or Error (red) notifications bubbles from the Medusa application.
|
||||||
|
Two lines are needed in the config.yml :
|
||||||
|
```
|
||||||
|
type: "Medusa"
|
||||||
|
apikey: "01234deb70424befb1f4ef6a23456789"
|
||||||
|
```
|
||||||
|
The url must be the root url of Medusa application.
|
||||||
|
The Medusa API key can be found in General configuration > Interface. It is needed to access Medusa API.
|
||||||
|
|
||||||
|
|
||||||
|
## Sonarr/Radarr
|
||||||
|
|
||||||
|
This service displays Activity (blue), Warning (orange) or Error (red) notifications bubbles from the Radarr/Sonarr application.
|
||||||
|
Two lines are needed in the config.yml :
|
||||||
|
```
|
||||||
|
type: "Radarr" or "Sonarr"
|
||||||
|
apikey: "01234deb70424befb1f4ef6a23456789"
|
||||||
|
```
|
||||||
|
The url must be the root url of Radarr/Sonarr application.
|
||||||
|
The Radarr/Sonarr API key can be found in Settings > General. It is needed to access the API.
|
||||||
|
|
||||||
|
|
||||||
|
## PaperlessNG
|
||||||
|
|
||||||
|
For Paperless you need an API-Key which you have to store at the item in the field `apikey`.
|
||||||
|
|
||||||
|
|
||||||
|
## Ping
|
||||||
|
|
||||||
|
For Ping you need an API-Key which you have to store at the item in the field `apikey`.
|
||||||
|
|
|
@ -113,6 +113,64 @@ docker create \
|
||||||
|
|
||||||
|
|
||||||
## Get the news headlines in Homer
|
## Get the news headlines in Homer
|
||||||
|
|
||||||
|
### Mapping Fields
|
||||||
|
Most times, the url you're getting headlines from follows a different schema than the one expected by Homer.
|
||||||
|
|
||||||
|
For example, if you would like to show jokes from ChuckNorris.io, you'll find that the url https://api.chucknorris.io/jokes/random is giving you info like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"categories": [],
|
||||||
|
"created_at": "2020-01-05 13:42:22.089095",
|
||||||
|
"icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
|
||||||
|
"id": "MR2-BnMBR667xSpQBIleUg",
|
||||||
|
"updated_at": "2020-01-05 13:42:22.089095",
|
||||||
|
"url": "https://api.chucknorris.io/jokes/MR2-BnMBR667xSpQBIleUg",
|
||||||
|
"value": "Chuck Norris can quitely sneak up on himself"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
but... you need that info to be transformed to something like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "MR2-BnMBR667xSpQBIleUg",
|
||||||
|
"content": "Chuck Norris can quitely sneak up on himself"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, you can do that using the `mapping` field in your `message` configuration. This example would be something like this:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
message:
|
||||||
|
url: https://api.chucknorris.io/jokes/random
|
||||||
|
mapping:
|
||||||
|
title: 'id'
|
||||||
|
content: 'value'
|
||||||
|
```
|
||||||
|
|
||||||
|
As you would see, using the ID as a title doesn't seem nice, that's why when a field is empty it would keep the default values, like this:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
message:
|
||||||
|
url: https://api.chucknorris.io/jokes/random
|
||||||
|
mapping:
|
||||||
|
content: 'value'
|
||||||
|
title: "Chuck Norris Facts!"
|
||||||
|
```
|
||||||
|
|
||||||
|
and even an error message in case the `url` didn't respond or threw an error:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
message:
|
||||||
|
url: https://api.chucknorris.io/jokes/random
|
||||||
|
mapping:
|
||||||
|
content: 'value'
|
||||||
|
title: "Chuck Norris Facts!"
|
||||||
|
content: "Message could not be loaded"
|
||||||
|
```
|
||||||
|
|
||||||
#### `by @JamiePhonic`
|
#### `by @JamiePhonic`
|
||||||
|
|
||||||
Homer allows you to set a "message" that will appear at the top of the page, however, you can also supply a `url:`.
|
Homer allows you to set a "message" that will appear at the top of the page, however, you can also supply a `url:`.
|
||||||
|
|
30
package.json
30
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "homer",
|
"name": "homer",
|
||||||
"version": "20.06.1",
|
"version": "21.09.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
|
@ -8,28 +8,28 @@
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||||
"bulma": "^0.9.1",
|
"bulma": "^0.9.3",
|
||||||
"core-js": "^3.8.1",
|
"core-js": "^3.17.3",
|
||||||
"js-yaml": "^3.14.1",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"vue": "^2.6.12"
|
"vue": "^2.6.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "~4.5.9",
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
"@vue/cli-plugin-eslint": "~4.5.9",
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
"@vue/cli-plugin-pwa": "~4.5.9",
|
"@vue/cli-plugin-pwa": "~4.5.0",
|
||||||
"@vue/cli-service": "~4.5.9",
|
"@vue/cli-service": "~4.5.0",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"eslint": "^7.16.0",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-prettier": "^3.3.0",
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
"eslint-plugin-vue": "^7.3.0",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"sass": "^1.30.0",
|
"sass": "^1.26.5",
|
||||||
"sass-loader": "^10.1.0",
|
"sass-loader": "^8.0.2",
|
||||||
"vue-template-compiler": "^2.6.12"
|
"vue-template-compiler": "^2.6.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
# Additionnal page configuration
|
||||||
|
|
||||||
|
# Additionnal configurations are loaded using its file name, minus the extension, as an anchor (https://<mydashboad>#<config>).
|
||||||
|
# `config.yml` is still used as a base configuration, and all values here will overwrite it, so you don't have to re-defined everything
|
||||||
|
|
||||||
|
|
||||||
|
subtitle: "this is another dashboard page"
|
||||||
|
|
||||||
|
# This overwrites message config. Setting it to empty to remove message from this page and keep it only in the main one:
|
||||||
|
message: ~
|
||||||
|
|
||||||
|
# as we want to include a differente link here (so we can get back to home page), we need to replicate all links or they will be revome when overwriting the links field:
|
||||||
|
links:
|
||||||
|
- name: "Home"
|
||||||
|
icon: "fas fa-home"
|
||||||
|
url: "#"
|
||||||
|
- name: "Contribute"
|
||||||
|
icon: "fab fa-github"
|
||||||
|
url: "https://github.com/bastienwirtz/homer"
|
||||||
|
target: "_blank" # optional html a tag target attribute
|
||||||
|
- name: "Wiki"
|
||||||
|
icon: "fas fa-book"
|
||||||
|
url: "https://www.wikipedia.org/"
|
||||||
|
|
||||||
|
services:
|
||||||
|
- name: "More applications on another page!"
|
||||||
|
icon: "fas fa-cloud"
|
||||||
|
items:
|
||||||
|
- name: "Awesome app on a second page!"
|
||||||
|
logo: "assets/tools/sample.png"
|
||||||
|
subtitle: "Bookmark example"
|
||||||
|
tag: "app"
|
||||||
|
url: "https://www.reddit.com/r/selfhosted/"
|
||||||
|
target: "_blank"
|
|
@ -56,6 +56,11 @@ links:
|
||||||
- name: "Wiki"
|
- name: "Wiki"
|
||||||
icon: "fas fa-book"
|
icon: "fas fa-book"
|
||||||
url: "https://www.wikipedia.org/"
|
url: "https://www.wikipedia.org/"
|
||||||
|
# this will link to a second homer page that will load config from additionnal-page.yml and keep default config values as in config.yml file
|
||||||
|
# see url field and assets/additionnal-page.yml.dist used in this example:
|
||||||
|
- name: "another page!"
|
||||||
|
icon: "fas fa-file-alt"
|
||||||
|
url: "#additionnal-page"
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
# First level array represent a group.
|
# First level array represent a group.
|
||||||
|
|
27
src/App.vue
27
src/App.vue
|
@ -13,7 +13,9 @@
|
||||||
<section v-if="config.header" class="first-line">
|
<section v-if="config.header" class="first-line">
|
||||||
<div v-cloak class="container">
|
<div v-cloak class="container">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
|
<a href="#">
|
||||||
<img v-if="config.logo" :src="config.logo" alt="dashboard logo" />
|
<img v-if="config.logo" :src="config.logo" alt="dashboard logo" />
|
||||||
|
</a>
|
||||||
<i v-if="config.icon" :class="config.icon"></i>
|
<i v-if="config.icon" :class="config.icon"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-title">
|
<div class="dashboard-title">
|
||||||
|
@ -62,6 +64,11 @@
|
||||||
<template v-for="group in services">
|
<template v-for="group in services">
|
||||||
<h2 v-if="group.name" class="column is-full group-title">
|
<h2 v-if="group.name" class="column is-full group-title">
|
||||||
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
||||||
|
<div v-else-if="group.logo" class="group-logo media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="group.logo" :alt="`${group.name} logo`" />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
</h2>
|
</h2>
|
||||||
<Service
|
<Service
|
||||||
|
@ -85,6 +92,11 @@
|
||||||
>
|
>
|
||||||
<h2 v-if="group.name" class="group-title">
|
<h2 v-if="group.name" class="group-title">
|
||||||
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
||||||
|
<div v-else-if="group.logo" class="group-logo media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="group.logo" :alt="`${group.name} logo`" />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
</h2>
|
</h2>
|
||||||
<Service
|
<Service
|
||||||
|
@ -149,10 +161,24 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created: async function () {
|
created: async function () {
|
||||||
|
this.buildDashboard();
|
||||||
|
window.onhashchange = this.buildDashboard;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
buildDashboard: async function () {
|
||||||
const defaults = jsyaml.load(defaultConfig);
|
const defaults = jsyaml.load(defaultConfig);
|
||||||
let config;
|
let config;
|
||||||
try {
|
try {
|
||||||
config = await this.getConfig();
|
config = await this.getConfig();
|
||||||
|
const path =
|
||||||
|
window.location.hash.substring(1) != ""
|
||||||
|
? window.location.hash.substring(1)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
let pathConfig = await this.getConfig(`assets/${path}.yml`); // the slash (/) is included in the pathname
|
||||||
|
config = Object.assign(config, pathConfig);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
config = this.handleErrors("⚠️ Error loading configuration", error);
|
config = this.handleErrors("⚠️ Error loading configuration", error);
|
||||||
|
@ -170,7 +196,6 @@ export default {
|
||||||
this.createStylesheet(stylesheet);
|
this.createStylesheet(stylesheet);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
getConfig: function (path = "assets/config.yml") {
|
getConfig: function (path = "assets/config.yml") {
|
||||||
return fetch(path).then((response) => {
|
return fetch(path).then((response) => {
|
||||||
if (response.redirected) {
|
if (response.redirected) {
|
||||||
|
|
|
@ -106,7 +106,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.first-line {
|
.first-line {
|
||||||
height: 100px;
|
min-height: 100px;
|
||||||
vertical-align: center;
|
vertical-align: center;
|
||||||
background-color: var(--highlight-primary);
|
background-color: var(--highlight-primary);
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
height: 80px;
|
min-height: 80px;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,8 +140,7 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navbar,
|
.navbar {
|
||||||
.navbar-menu {
|
|
||||||
background-color: var(--highlight-secondary);
|
background-color: var(--highlight-secondary);
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -153,6 +152,9 @@ body {
|
||||||
background-color: var(--highlight-hover);
|
background-color: var(--highlight-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navbar-menu {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navbar-end {
|
.navbar-end {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
@ -197,6 +199,11 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media.no-subtitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.media-content {
|
.media-content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: inherit;
|
text-overflow: inherit;
|
||||||
|
@ -206,7 +213,7 @@ body {
|
||||||
color: var(--highlight-secondary);
|
color: var(--highlight-secondary);
|
||||||
background-color: var(--highlight-secondary);
|
background-color: var(--highlight-secondary);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1rem;
|
bottom: 1rem;
|
||||||
right: -0.2rem;
|
right: -0.2rem;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -219,7 +226,6 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border-radius: 5px;
|
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
|
||||||
transition: cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
|
transition: cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
|
||||||
|
@ -255,11 +261,13 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.column div:first-of-type .card {
|
.column div:first-of-type .card {
|
||||||
border-radius: 5px 5px 0 0;
|
border-top-left-radius: 0.25rem;
|
||||||
|
border-top-right-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column div:last-child .card {
|
.column div:last-child .card {
|
||||||
border-radius: 0 0 5px 5px;
|
border-bottom-left-radius: 0.25rem;
|
||||||
|
border-bottom-right-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,3 +348,7 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group-logo {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
|
@ -37,8 +37,8 @@ export default {
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function (response) {
|
||||||
that.offline = false;
|
that.offline = !response.ok;
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
that.offline = true;
|
that.offline = true;
|
||||||
|
|
|
@ -4,7 +4,11 @@
|
||||||
aria-label="Toggle dark mode"
|
aria-label="Toggle dark mode"
|
||||||
class="navbar-item is-inline-block-mobile"
|
class="navbar-item is-inline-block-mobile"
|
||||||
>
|
>
|
||||||
<i class="fas fa-fw fa-adjust"></i>
|
<i
|
||||||
|
:class="`${faClasses[mode]}`"
|
||||||
|
class="fa-fw"
|
||||||
|
:title="`${titles[mode]}`"
|
||||||
|
></i>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -14,21 +18,55 @@ export default {
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
isDark: null,
|
isDark: null,
|
||||||
|
faClasses: null,
|
||||||
|
titles: null,
|
||||||
|
mode: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created: function () {
|
created: function () {
|
||||||
this.isDark =
|
this.faClasses = ["fas fa-adjust", "fas fa-circle", "far fa-circle"];
|
||||||
"overrideDark" in localStorage
|
this.titles = ["Auto-switch", "Light theme", "Dark theme"];
|
||||||
? JSON.parse(localStorage.overrideDark)
|
this.mode = 0;
|
||||||
: matchMedia("(prefers-color-scheme: dark)").matches;
|
if ("overrideDark" in localStorage) {
|
||||||
|
// Light theme is 1 and Dark theme is 2
|
||||||
|
this.mode = JSON.parse(localStorage.overrideDark) ? 2 : 1;
|
||||||
|
}
|
||||||
|
this.isDark = this.getIsDark();
|
||||||
this.$emit("updated", this.isDark);
|
this.$emit("updated", this.isDark);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleTheme: function () {
|
toggleTheme: function () {
|
||||||
this.isDark = !this.isDark;
|
this.mode = (this.mode + 1) % 3;
|
||||||
localStorage.overrideDark = this.isDark;
|
switch (this.mode) {
|
||||||
|
// Default behavior
|
||||||
|
case 0:
|
||||||
|
localStorage.removeItem("overrideDark");
|
||||||
|
break;
|
||||||
|
// Force light theme
|
||||||
|
case 1:
|
||||||
|
localStorage.overrideDark = false;
|
||||||
|
break;
|
||||||
|
// Force dark theme
|
||||||
|
case 2:
|
||||||
|
localStorage.overrideDark = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Should be unreachable
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDark = this.getIsDark();
|
||||||
this.$emit("updated", this.isDark);
|
this.$emit("updated", this.isDark);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getIsDark: function () {
|
||||||
|
const values = [
|
||||||
|
matchMedia("(prefers-color-scheme: dark)").matches,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
];
|
||||||
|
return values[this.mode];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -22,26 +22,52 @@ export default {
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
show: false,
|
|
||||||
message: {},
|
message: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created: async function () {
|
created: async function () {
|
||||||
// Look for a new message if an endpoint is provided.
|
// Look for a new message if an endpoint is provided.
|
||||||
this.message = Object.assign({}, this.item);
|
this.message = Object.assign({}, this.item);
|
||||||
if (this.item && this.item.url) {
|
await this.getMessage();
|
||||||
const fetchedMessage = await this.getMessage(this.item.url);
|
},
|
||||||
// keep the original config value if no value is provided by the endpoint
|
computed: {
|
||||||
for (const prop of ["title", "style", "content"]) {
|
show: function () {
|
||||||
if (prop in fetchedMessage && fetchedMessage[prop] !== null) {
|
return this.message.title || this.message.content;
|
||||||
this.message[prop] = fetchedMessage[prop];
|
},
|
||||||
}
|
},
|
||||||
}
|
watch: {
|
||||||
}
|
item: function (item) {
|
||||||
this.show = this.message.title || this.message.content;
|
this.message = Object.assign({}, item);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getMessage: function (url) {
|
getMessage: async function () {
|
||||||
|
if (!this.item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.item.url) {
|
||||||
|
let fetchedMessage = await this.downloadMessage(this.item.url);
|
||||||
|
console.log("done");
|
||||||
|
if (this.item.mapping) {
|
||||||
|
fetchedMessage = this.mapRemoteMessage(fetchedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep the original config value if no value is provided by the endpoint
|
||||||
|
const message = this.message;
|
||||||
|
for (const prop of ["title", "style", "content", "icon"]) {
|
||||||
|
if (prop in fetchedMessage && fetchedMessage[prop] !== null) {
|
||||||
|
message[prop] = fetchedMessage[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.message = { ...message }; // Force computed property to re-evaluate
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.item.refreshInterval) {
|
||||||
|
setTimeout(this.getMessage, this.item.refreshInterval);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadMessage: function (url) {
|
||||||
return fetch(url).then(function (response) {
|
return fetch(url).then(function (response) {
|
||||||
if (response.status != 200) {
|
if (response.status != 200) {
|
||||||
return;
|
return;
|
||||||
|
@ -49,6 +75,15 @@ export default {
|
||||||
return response.json();
|
return response.json();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mapRemoteMessage: function (message) {
|
||||||
|
let mapped = {};
|
||||||
|
// map property from message into mapped according to mapping config (only if field has a value):
|
||||||
|
for (const prop in this.item.mapping)
|
||||||
|
if (message[this.item.mapping[prop]])
|
||||||
|
mapped[prop] = message[this.item.mapping[prop]];
|
||||||
|
return mapped;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -51,9 +51,9 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchStatus: async function () {
|
fetchStatus: async function () {
|
||||||
this.status = await fetch(
|
this.status = await fetch(`${this.item.url}/control/status`, {
|
||||||
`${this.item.url}/control/status`
|
credentials: "include",
|
||||||
).then((response) => response.json());
|
}).then((response) => response.json());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,3 @@
|
||||||
<script>
|
|
||||||
export default {};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
||||||
*/
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
@ -20,7 +7,7 @@ export default {};
|
||||||
>
|
>
|
||||||
<a :href="item.url" :target="item.target" rel="noreferrer">
|
<a :href="item.url" :target="item.target" rel="noreferrer">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="media">
|
<div :class="mediaClass">
|
||||||
<div v-if="item.logo" class="media-left">
|
<div v-if="item.logo" class="media-left">
|
||||||
<figure class="image is-48x48">
|
<figure class="image is-48x48">
|
||||||
<img :src="item.logo" :alt="`${item.name} logo`" />
|
<img :src="item.logo" :alt="`${item.name} logo`" />
|
||||||
|
@ -33,7 +20,9 @@ export default {};
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">{{ item.name }}</p>
|
<p class="title is-4">{{ item.name }}</p>
|
||||||
<p class="subtitle is-6">{{ item.subtitle }}</p>
|
<p class="subtitle is-6" v-if="item.subtitle">
|
||||||
|
{{ item.subtitle }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tag" :class="item.tagstyle" v-if="item.tag">
|
<div class="tag" :class="item.tagstyle" v-if="item.tag">
|
||||||
|
@ -51,11 +40,23 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
item: Object,
|
item: Object,
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
mediaClass: function () {
|
||||||
|
return { media: true, "no-subtitle": !this.item.subtitle };
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.media-left img {
|
.media-left {
|
||||||
|
.image {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="card" :class="item.class">
|
||||||
|
<a :href="item.url" :target="item.target" rel="noreferrer">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media">
|
||||||
|
<div v-if="item.logo" class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="item.logo" :alt="`${item.name} logo`" />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.icon" class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-4">{{ item.name }}</p>
|
||||||
|
<p class="subtitle is-6">{{ item.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="notifs">
|
||||||
|
<strong
|
||||||
|
v-if="config !== null && config.system.news.unread > 0"
|
||||||
|
class="notif news"
|
||||||
|
title="News"
|
||||||
|
>{{ config.system.news.unread }}</strong
|
||||||
|
>
|
||||||
|
<strong
|
||||||
|
v-if="config !== null && config.main.logs.numWarnings > 0"
|
||||||
|
class="notif warnings"
|
||||||
|
title="Warning"
|
||||||
|
>{{ config.main.logs.numWarnings }}</strong
|
||||||
|
>
|
||||||
|
<strong
|
||||||
|
v-if="config !== null && config.main.logs.numErrors > 0"
|
||||||
|
class="notif errors"
|
||||||
|
title="Error"
|
||||||
|
>{{ config.main.logs.numErrors }}</strong
|
||||||
|
>
|
||||||
|
<strong
|
||||||
|
v-if="serverError"
|
||||||
|
class="notif errors"
|
||||||
|
title="Connection error to Medusa API, check url and apikey in config.yml"
|
||||||
|
>?</strong
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tag" :class="item.tagstyle" v-if="item.tag">
|
||||||
|
<strong class="tag-text">#{{ item.tag }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Medusa",
|
||||||
|
props: {
|
||||||
|
item: Object,
|
||||||
|
},
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
config: null,
|
||||||
|
serverError: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
this.fetchConfig();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchConfig: function () {
|
||||||
|
fetch(`${this.item.url}/api/v2/config`, {
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "X-Api-Key": `${this.item.apikey}` },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((conf) => {
|
||||||
|
this.config = conf;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
this.serverError = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.media-left img {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
.notifs {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
top: 0.3em;
|
||||||
|
right: 0.5em;
|
||||||
|
}
|
||||||
|
.notif {
|
||||||
|
padding-right: 0.35em;
|
||||||
|
padding-left: 0.35em;
|
||||||
|
padding-top: 0.2em;
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
position: relative;
|
||||||
|
margin-left: 0.3em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.news {
|
||||||
|
background-color: #777777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warnings {
|
||||||
|
background-color: #d08d2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errors {
|
||||||
|
background-color: #e51111;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="card" :class="item.class">
|
||||||
|
<a :href="item.url" :target="item.target" rel="noreferrer">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media">
|
||||||
|
<div v-if="item.logo" class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="item.logo" :alt="`${item.name} logo`" />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.icon" class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-4">{{ item.name }}</p>
|
||||||
|
<p class="subtitle is-6">
|
||||||
|
<template v-if="item.subtitle">
|
||||||
|
{{ item.subtitle }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="api">
|
||||||
|
happily storing {{ api.count }} documents
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tag" :class="item.tagstyle" v-if="item.tag">
|
||||||
|
<strong class="tag-text">#{{ item.tag }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Paperless",
|
||||||
|
props: {
|
||||||
|
item: Object,
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
api: null,
|
||||||
|
}),
|
||||||
|
created() {
|
||||||
|
this.fetchStatus();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchStatus: async function () {
|
||||||
|
if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing
|
||||||
|
var apikey = this.item.apikey;
|
||||||
|
if (!apikey) {
|
||||||
|
console.error(
|
||||||
|
"apikey is not present in config.yml for the paperless entry!"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `${this.item.url}/api/documents/`;
|
||||||
|
this.api = await fetch(url, {
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Token " + this.item.apikey,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Not 2xx response");
|
||||||
|
} else {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => console.log(e));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.media-left img {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -64,7 +64,9 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
fetchStatus: async function () {
|
fetchStatus: async function () {
|
||||||
const url = `${this.item.url}/api.php`;
|
const url = `${this.item.url}/api.php`;
|
||||||
this.api = await fetch(url)
|
this.api = await fetch(url, {
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.catch((e) => console.log(e));
|
.catch((e) => console.log(e));
|
||||||
},
|
},
|
||||||
|
@ -83,13 +85,13 @@ export default {
|
||||||
&.enabled:before {
|
&.enabled:before {
|
||||||
background-color: #94e185;
|
background-color: #94e185;
|
||||||
border-color: #78d965;
|
border-color: #78d965;
|
||||||
box-shadow: 0 0 4px 1px #94e185;
|
box-shadow: 0 0 5px 1px #94e185;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled:before {
|
&.disabled:before {
|
||||||
background-color: #c9404d;
|
background-color: #c9404d;
|
||||||
border-color: #c42c3b;
|
border-color: #c42c3b;
|
||||||
box-shadow: 0 0 4px 1px #c9404d;
|
box-shadow: 0 0 5px 1px #c9404d;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="card" :class="item.class">
|
||||||
|
<a :href="item.url" :target="item.target" rel="noreferrer">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media">
|
||||||
|
<div v-if="item.logo" class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="item.logo" :alt="`${item.name} logo`" />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.icon" class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-4">{{ item.name }}</p>
|
||||||
|
<p class="subtitle is-6">
|
||||||
|
<template v-if="item.subtitle">
|
||||||
|
{{ item.subtitle }}
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="status" class="status" :class="status">
|
||||||
|
{{ status }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tag" :class="item.tagstyle" v-if="item.tag">
|
||||||
|
<strong class="tag-text">#{{ item.tag }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Ping",
|
||||||
|
props: {
|
||||||
|
item: Object,
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
status: null,
|
||||||
|
}),
|
||||||
|
created() {
|
||||||
|
this.fetchStatus();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchStatus: async function () {
|
||||||
|
const url = `${this.item.url}`;
|
||||||
|
fetch(url, {
|
||||||
|
method: "HEAD",
|
||||||
|
cache: "no-cache",
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw Error(response.statusText);
|
||||||
|
}
|
||||||
|
this.status = "online";
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = "offline";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.media-left img {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-title);
|
||||||
|
|
||||||
|
&.online:before {
|
||||||
|
background-color: #94e185;
|
||||||
|
border-color: #78d965;
|
||||||
|
box-shadow: 0 0 5px 1px #94e185;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offline:before {
|
||||||
|
background-color: #c9404d;
|
||||||
|
border-color: #c42c3b;
|
||||||
|
box-shadow: 0 0 5px 1px #c9404d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: " ";
|
||||||
|
display: inline-block;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,151 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="card" :class="item.class">
|
||||||
|
<a :href="item.url" :target="item.target" rel="noreferrer">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media">
|
||||||
|
<div v-if="item.logo" class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="item.logo" :alt="`${item.name} logo`" />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.icon" class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-4">{{ item.name }}</p>
|
||||||
|
<p class="subtitle is-6">{{ item.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="notifs">
|
||||||
|
<strong
|
||||||
|
v-if="activity > 0"
|
||||||
|
class="notif activity"
|
||||||
|
title="Activity"
|
||||||
|
>{{ activity }}</strong
|
||||||
|
>
|
||||||
|
<strong
|
||||||
|
v-if="warnings > 0"
|
||||||
|
class="notif warnings"
|
||||||
|
title="Warning"
|
||||||
|
>{{ warnings }}</strong
|
||||||
|
>
|
||||||
|
<strong v-if="errors > 0" class="notif errors" title="Error">{{
|
||||||
|
errors
|
||||||
|
}}</strong>
|
||||||
|
<strong
|
||||||
|
v-if="serverError"
|
||||||
|
class="notif errors"
|
||||||
|
title="Connection error to Radarr API, check url and apikey in config.yml"
|
||||||
|
>?</strong
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tag" :class="item.tagstyle" v-if="item.tag">
|
||||||
|
<strong class="tag-text">#{{ item.tag }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Radarr",
|
||||||
|
props: {
|
||||||
|
item: Object,
|
||||||
|
},
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
activity: null,
|
||||||
|
warnings: null,
|
||||||
|
errors: null,
|
||||||
|
serverError: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
this.fetchConfig();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchConfig: function () {
|
||||||
|
fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((health) => {
|
||||||
|
this.warnings = 0;
|
||||||
|
this.errors = 0;
|
||||||
|
for (var i = 0; i < health.length; i++) {
|
||||||
|
if (health[i].type == "warning") {
|
||||||
|
this.warnings++;
|
||||||
|
} else if (health[i].type == "error") {
|
||||||
|
this.errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
this.serverError = true;
|
||||||
|
});
|
||||||
|
fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((queue) => {
|
||||||
|
this.activity = 0;
|
||||||
|
for (var i = 0; i < queue.length; i++) {
|
||||||
|
if (queue[i].movie) {
|
||||||
|
this.activity++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
this.serverError = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.media-left img {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
.notifs {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
top: 0.3em;
|
||||||
|
right: 0.5em;
|
||||||
|
}
|
||||||
|
.notif {
|
||||||
|
padding-right: 0.35em;
|
||||||
|
padding-left: 0.35em;
|
||||||
|
padding-top: 0.2em;
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
position: relative;
|
||||||
|
margin-left: 0.3em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.activity {
|
||||||
|
background-color: #4fb5d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warnings {
|
||||||
|
background-color: #d08d2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errors {
|
||||||
|
background-color: #e51111;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,151 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="card" :class="item.class">
|
||||||
|
<a :href="item.url" :target="item.target" rel="noreferrer">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media">
|
||||||
|
<div v-if="item.logo" class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="item.logo" :alt="`${item.name} logo`" />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.icon" class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-4">{{ item.name }}</p>
|
||||||
|
<p class="subtitle is-6">{{ item.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="notifs">
|
||||||
|
<strong
|
||||||
|
v-if="activity > 0"
|
||||||
|
class="notif activity"
|
||||||
|
title="Activity"
|
||||||
|
>{{ activity }}</strong
|
||||||
|
>
|
||||||
|
<strong
|
||||||
|
v-if="warnings > 0"
|
||||||
|
class="notif warnings"
|
||||||
|
title="Warning"
|
||||||
|
>{{ warnings }}</strong
|
||||||
|
>
|
||||||
|
<strong v-if="errors > 0" class="notif errors" title="Error">{{
|
||||||
|
errors
|
||||||
|
}}</strong>
|
||||||
|
<strong
|
||||||
|
v-if="serverError"
|
||||||
|
class="notif errors"
|
||||||
|
title="Connection error to Sonarr API, check url and apikey in config.yml"
|
||||||
|
>?</strong
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tag" :class="item.tagstyle" v-if="item.tag">
|
||||||
|
<strong class="tag-text">#{{ item.tag }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Sonarr",
|
||||||
|
props: {
|
||||||
|
item: Object,
|
||||||
|
},
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
activity: null,
|
||||||
|
warnings: null,
|
||||||
|
errors: null,
|
||||||
|
serverError: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
this.fetchConfig();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchConfig: function () {
|
||||||
|
fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((health) => {
|
||||||
|
this.warnings = 0;
|
||||||
|
this.errors = 0;
|
||||||
|
for (var i = 0; i < health.length; i++) {
|
||||||
|
if (health[i].type == "warning") {
|
||||||
|
this.warnings++;
|
||||||
|
} else if (health[i].type == "error") {
|
||||||
|
this.errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
this.serverError = true;
|
||||||
|
});
|
||||||
|
fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((queue) => {
|
||||||
|
this.activity = 0;
|
||||||
|
for (var i = 0; i < queue.length; i++) {
|
||||||
|
if (queue[i].series) {
|
||||||
|
this.activity++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
this.serverError = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.media-left img {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
.notifs {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
top: 0.3em;
|
||||||
|
right: 0.5em;
|
||||||
|
}
|
||||||
|
.notif {
|
||||||
|
padding-right: 0.35em;
|
||||||
|
padding-left: 0.35em;
|
||||||
|
padding-top: 0.2em;
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
position: relative;
|
||||||
|
margin-left: 0.3em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.activity {
|
||||||
|
background-color: #4fb5d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warnings {
|
||||||
|
background-color: #d08d2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errors {
|
||||||
|
background-color: #e51111;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,6 +12,7 @@ module.exports = {
|
||||||
publicPath: "",
|
publicPath: "",
|
||||||
pwa: {
|
pwa: {
|
||||||
manifestPath: "assets/manifest.json",
|
manifestPath: "assets/manifest.json",
|
||||||
|
manifestCrossorigin: "use-credentials",
|
||||||
appleMobileWebAppStatusBarStyle: "black",
|
appleMobileWebAppStatusBarStyle: "black",
|
||||||
appleMobileWebAppCapable: "yes",
|
appleMobileWebAppCapable: "yes",
|
||||||
name: manifestOptions.name,
|
name: manifestOptions.name,
|
||||||
|
|
Loading…
Reference in New Issue