🔀 Merge branch 'master' of github.com:Lissy93/dashy into REFACTOR/widget-and-docs-improvments

This commit is contained in:
Alicia Sykes 2022-06-30 15:00:51 +01:00
commit 9235123f10
30 changed files with 2174 additions and 626 deletions

21
.github/AUTHORS.txt vendored
View File

@ -13,6 +13,7 @@ Jeremy <hauvi> - 1 commits
Kieren <onnel> - 1 commits
Leonardo <olma> - 1 commits
M <seno> - 1 commits
Markus <raus> - 1 commits
PlusaN <61884717+PlusaN@users.noreply.github.com> - 1 commits
Rune <jørnerå> - 1 commits
Ryan <urne> - 1 commits
@ -24,7 +25,9 @@ deepsource-io[bot] <deepsource-io[bot]@users.noreply.github.com> - 1 commits
dr460nf1r3 <njcrypted@protonmail.com> - 1 commits
icy-comet <50461557+icy-comet@users.noreply.github.com> - 1 commits
jnach <33467747+jnach@users.noreply.github.com> - 1 commits
pablomalo <paul.gouin50@gmail.com> - 1 commits
tazboyz16 <tazboyz_16@yahoo.com> - 1 commits
zcq100 <m@zcq100.com> - 1 commits
Alejandro <ina> - 2 commits
Alessandro <e> - 2 commits
BOZG <sr@bozg.se> - 2 commits
@ -32,10 +35,13 @@ Brendan <&#39;Lear> - 2 commits
CHAIYEON <H> - 2 commits
Dan <ilber> - 2 commits
Ruben <ilv> - 2 commits
k073l <21180271+k073l@users.noreply.github.com> - 2 commits
liss-bot <87835202+liss-bot@users.noreply.github.com> - 2 commits
patrickheeney <patrickheeney@gmail.com> - 2 commits
ᗪєνιη <υн> - 2 commits
Walkx <71191962+walkxcode@users.noreply.github.com> - 3 commits
aterox <kenneth@kenneth.church> - 3 commits
bogyeong <boggyhint@gmail.com> - 3 commits
stanly0726 <37040069+stanly0726@users.noreply.github.com> - 3 commits
Niklas <abe> - 4 commits
Rémy <RANDI> - 4 commits
@ -53,13 +59,14 @@ github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> - 16
snyk-bot <snyk-bot@users.noreply.github.com> - 18 commits
aterox <church.kennetha@gmail.com> - 19 commits
EVOTk <45015615+EVOTk@users.noreply.github.com> - 22 commits
Marcell <ülö> - 23 commits
Alicia <yke> - 28 commits
repo-visualizer <repo-visualizer@users.noreply.github.com> - 39 commits
snyk-bot <snyk-bot@snyk.io> - 44 commits
Alicia <o> - 78 commits
repo-visualizer <repo-visualizer@users.noreply.github.com> - 43 commits
snyk-bot <snyk-bot@snyk.io> - 50 commits
Lissy93 <gh@d0h.co> - 78 commits
liss-bot <liss-bot@d0h.co> - 89 commits
Alicia <yke> - 123 commits
Lissy93 <Lissy93@users.noreply.github.com> - 207 commits
Alicia <yke> - 439 commits
Alicia <o> - 84 commits
liss-bot <liss-bot@d0h.co> - 95 commits
Alicia <yke> - 148 commits
Lissy93 <Lissy93@users.noreply.github.com> - 208 commits
Alicia <yke> - 440 commits
Alicia <yke> - 1488 commits

View File

@ -1,37 +0,0 @@
# Will add a comment and close any new issues opened by
# users who have not yet committed to, or starred the repo
name: 🎯 Issue Spam Control
on:
issues:
types: [opened, reopened]
jobs:
check-user:
if: >
${{
! contains( github.event.issue.labels.*.name, '📌 Keep Open') &&
! contains( github.event.issue.labels.*.name, '🌈 Feedback') &&
! contains( github.event.issue.labels.*.name, '💯 Showcase') &&
github.event.comment.author_association != 'CONTRIBUTOR'
}}
runs-on: ubuntu-latest
name: Close issue opened by non-stargazer
steps:
- name: close
uses: uhyo/please-star-first@v1.0.1
with:
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
message: |
Welcome to Dashy 👋
It's great to have you here, but unfortunately your ticket has been closed to prevent spam. Before reopening this issue, please ensure the following criteria are met.
Issues are sometimes closed when users:
- Have only recently joined GitHub
- Have not yet stared this repository
- Have not previously interacted with the repo
Before you reopen this issue, please also ensure that:
- You have checked that a similar issue does not already exist
- You have checked the documentation for an existing solution
- You have completed the relevant sections in the Issue template
Once you have verified the above standards are met, you may reopen this issue. Sorry for any inconvenience caused, I'm just a bot, and sometimes make mistakes 🤖

22
.github/workflows/new-issues-check.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: ⭐ Hello non-Stargazers
on:
issues:
types: [opened, reopened]
jobs:
check-user:
if: >
${{
! contains( github.event.issue.labels.*.name, '📌 Keep Open') &&
! contains( github.event.issue.labels.*.name, '🌈 Feedback') &&
! contains( github.event.issue.labels.*.name, '💯 Showcase') &&
github.event.comment.author_association != 'CONTRIBUTOR'
}}
runs-on: ubuntu-latest
name: Add comment to issues opened by non-stargazers
steps:
- name: comment
uses: qxip/please-star-light@v4
with:
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
autoclose: false
message: "If you're enjoying Dashy, consider dropping us a ⭐<br>_<sub>🤖 I'm a bot, and this message was automated</sub>_"

View File

@ -1,4 +1 @@
# Heroku config - Specifies the commands to execute when the app starts
# See docs for more info: https://devcenter.heroku.com/articles/procfile
web: node server.js
web: npm run build-and-start

View File

@ -535,6 +535,13 @@ Huge thanks to the sponsors helping to support Dashy's development!
<sub><b>Eddy Lazzarin</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/UlisesGascon">
<img src="https://avatars.githubusercontent.com/u/5110813?u=3c41facd8aa26154b9451de237c34b0f78d672a5&v=4" width="80;" alt="UlisesGascon"/>
<br />
<sub><b>Ulises Gascón</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/BOZG">
<img src="https://avatars.githubusercontent.com/u/6022344?u=a52f42b946a1e1156f7bb9d7f65e9e28bb2da89f&v=4" width="80;" alt="BOZG"/>
@ -542,13 +549,21 @@ Huge thanks to the sponsors helping to support Dashy's development!
<sub><b>Stephen Rigney</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/bmcgonag">
<img src="https://avatars.githubusercontent.com/u/7346620?u=2a0f9284f3e12ac1cc15288c254d1ec68a5081e8&v=4" width="80;" alt="bmcgonag"/>
<br />
<sub><b>Brian McGonagill</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Robert-Ernst">
<img src="https://avatars.githubusercontent.com/u/9050259?u=7253b4063f1ffe3b5a894263c8b2056151802508&v=4" width="80;" alt="Robert-Ernst"/>
<br />
<sub><b>Robert Ernst</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/vlad-timofeev">
<img src="https://avatars.githubusercontent.com/u/11474041?v=4" width="80;" alt="vlad-timofeev"/>
@ -562,8 +577,14 @@ Huge thanks to the sponsors helping to support Dashy's development!
<br />
<sub><b>Kit L.</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/mDafox">
<img src="https://avatars.githubusercontent.com/u/21359974?v=4" width="80;" alt="mDafox"/>
<br />
<sub><b>Manu Devos</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Byolock">
<img src="https://avatars.githubusercontent.com/u/25748003?v=4" width="80;" alt="Byolock"/>
@ -584,7 +605,8 @@ Huge thanks to the sponsors helping to support Dashy's development!
<br />
<sub><b>Hugalafutro</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/shadowking001">
<img src="https://avatars.githubusercontent.com/u/43928955?u=a00b44f22e5a82234d9b406ac048def1fbc16e31&v=4" width="80;" alt="shadowking001"/>
@ -605,8 +627,7 @@ Huge thanks to the sponsors helping to support Dashy's development!
<br />
<sub><b>Robin Candau</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/ced4568">
<img src="https://avatars.githubusercontent.com/u/60725859?v=4" width="80;" alt="ced4568"/>
@ -627,7 +648,8 @@ Huge thanks to the sponsors helping to support Dashy's development!
<br />
<sub><b>Undefined</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/jtfinley72">
<img src="https://avatars.githubusercontent.com/u/96497997?v=4" width="80;" alt="jtfinley72"/>

View File

@ -1,5 +1,6 @@
{
"name": "Dashy",
"website": "https://dashy.to/",
"description": "A Dashboard for your Homelab 🚀",
"repository": "https://github.com/lissy93/dashy",
"logo": "https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/logo.png",
@ -13,4 +14,4 @@
"lissy93"
],
"stack": "heroku-20"
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 MiB

After

Width:  |  Height:  |  Size: 9.9 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -18,6 +18,13 @@
<sub><b>Eddy Lazzarin</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/UlisesGascon">
<img src="https://avatars.githubusercontent.com/u/5110813?u=3c41facd8aa26154b9451de237c34b0f78d672a5&v=4" width="80;" alt="UlisesGascon"/>
<br />
<sub><b>Ulises Gascón</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/BOZG">
<img src="https://avatars.githubusercontent.com/u/6022344?u=a52f42b946a1e1156f7bb9d7f65e9e28bb2da89f&v=4" width="80;" alt="BOZG"/>
@ -25,13 +32,21 @@
<sub><b>Stephen Rigney</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/bmcgonag">
<img src="https://avatars.githubusercontent.com/u/7346620?u=2a0f9284f3e12ac1cc15288c254d1ec68a5081e8&v=4" width="80;" alt="bmcgonag"/>
<br />
<sub><b>Brian McGonagill</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Robert-Ernst">
<img src="https://avatars.githubusercontent.com/u/9050259?u=7253b4063f1ffe3b5a894263c8b2056151802508&v=4" width="80;" alt="Robert-Ernst"/>
<br />
<sub><b>Robert Ernst</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/vlad-timofeev">
<img src="https://avatars.githubusercontent.com/u/11474041?v=4" width="80;" alt="vlad-timofeev"/>
@ -45,8 +60,14 @@
<br />
<sub><b>Kit L.</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/mDafox">
<img src="https://avatars.githubusercontent.com/u/21359974?v=4" width="80;" alt="mDafox"/>
<br />
<sub><b>Manu Devos</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Byolock">
<img src="https://avatars.githubusercontent.com/u/25748003?v=4" width="80;" alt="Byolock"/>
@ -67,7 +88,8 @@
<br />
<sub><b>Hugalafutro</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/shadowking001">
<img src="https://avatars.githubusercontent.com/u/43928955?u=a00b44f22e5a82234d9b406ac048def1fbc16e31&v=4" width="80;" alt="shadowking001"/>
@ -88,8 +110,7 @@
<br />
<sub><b>Robin Candau</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/ced4568">
<img src="https://avatars.githubusercontent.com/u/60725859?v=4" width="80;" alt="ced4568"/>
@ -110,7 +131,8 @@
<br />
<sub><b>Undefined</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/jtfinley72">
<img src="https://avatars.githubusercontent.com/u/96497997?v=4" width="80;" alt="jtfinley72"/>
@ -203,14 +225,21 @@
<sub><b>Remygrandin</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/boggy-cs">
<img src="https://avatars.githubusercontent.com/u/82003678?v=4" width="80;" alt="boggy-cs"/>
<br />
<sub><b>Bogyeong Kim</b></sub>
</a>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/stanly0726">
<img src="https://avatars.githubusercontent.com/u/37040069?v=4" width="80;" alt="stanly0726"/>
<br />
<sub><b>Stanly0726</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/onedr0p">
<img src="https://avatars.githubusercontent.com/u/213795?v=4" width="80;" alt="onedr0p"/>
@ -245,15 +274,15 @@
<br />
<sub><b>Dan Gilbert</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/rubenandre">
<img src="https://avatars.githubusercontent.com/u/9402773?v=4" width="80;" alt="rubenandre"/>
<br />
<sub><b>Rúben Silva</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Singebob">
<img src="https://avatars.githubusercontent.com/u/24290044?v=4" width="80;" alt="Singebob"/>
@ -288,15 +317,15 @@
<br />
<sub><b>DeepSource Bot</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/emiran-orange">
<img src="https://avatars.githubusercontent.com/u/71817149?v=4" width="80;" alt="emiran-orange"/>
<br />
<sub><b>Emiran-orange</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/FormatToday">
<img src="https://avatars.githubusercontent.com/u/20515769?v=4" width="80;" alt="FormatToday"/>
@ -324,13 +353,6 @@
<br />
<sub><b>Jemy SCHNEPP</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/KierenConnell">
<img src="https://avatars.githubusercontent.com/u/46445781?v=4" width="80;" alt="KierenConnell"/>
<br />
<sub><b>Kieren Connell</b></sub>
</a>
</td></tr>
</table>
<!-- readme: contributors -end -->

View File

@ -1,7 +1,7 @@
![Dashy Docs](https://i.ibb.co/4mdNf7M/heading-docs.png)
### Running Dashy
- [Quick Start](/docs/quick-start.md) - TDLR guide on getting Dashy up and running
- [Quick Start](/docs/quick-start.md) - TLDR guide on getting Dashy up and running
- [Deployment](/docs/deployment.md) - Full guide on deploying Dashy either locally or online
- [Configuring](/docs/configuring.md) - Complete list of all available options in the config file
- [App Management](/docs/management.md) - Managing your app, updating, security, web server configuration, etc

View File

@ -1,8 +1,9 @@
# Troubleshooting
> _**This document contains common problems and their solutions.**_
> _**This document contains common problems and their solutions.**_<br>
> Please ensure your issue isn't listed here, before opening a new ticket.
>
> _If you came across an issue where the solution was not immediately obvious, consider adding it to this list to help other users._
> _If you come across an issue not listed below, consider adding it, to help other users._
### Contents
- [Refused to Connect in Web Content View](#refused-to-connect-in-modal-or-workspace-view)
@ -32,6 +33,7 @@
- [Weather Forecast Widget 401](#weather-forecast-widget-401)
- [Font Awesome Icons not Displaying](#font-awesome-icons-not-displaying)
- [Copy to Clipboard not Working](#copy-to-clipboard-not-working)
- [How to Reset Local Settings](#how-to-reset-local-settings)
- [How-To Open Browser Console](#how-to-open-browser-console)
- [Git Contributions not Displaying](#git-contributions-not-displaying)
@ -94,10 +96,18 @@ If this works, but you wish to continue using HTML5 history mode, then a bit of
## 404 after Launch from Mobile Home Screen
Similar to the above issue, if you get a 404 after using iOS's “add to Home Screen” feature, then this is caused by Vue router.
Similar to the above issue, if you get a 404 after using iOS and Android's “Add to Home Screen” feature, then this is caused by Vue router.
It can be fixed by setting `appConfig.routingMode` to `hash`
See also: [#628](https://github.com/Lissy93/dashy/issues/628)
See also: [#628](https://github.com/Lissy93/dashy/issues/628), [#762](https://github.com/Lissy93/dashy/issues/762)
---
## 404 On Multi-Page Apps
Similar to above, if you get a 404 error when visiting a page directly on multi-page apps, then this can be fixed under `appConfig`, by setting `routingMode` to `hash`. Then rebuilding, and refreshing the page.
See also: [#670](https://github.com/Lissy93/dashy/issues/670), [#763](https://github.com/Lissy93/dashy/issues/763)
---
@ -448,6 +458,21 @@ As a workaround, you could either:
---
## How to Reset Local Settings
Some settings are stored locally, in the browser's storage.
In some instances cached assets can prevent your settings from being updated, in which case you may wish to reset local data.
To clear all local data from the UI, head to the Config Menu, then click "Reset Local Settings", and Confirm when prompted.
This will not affect your config file. But be sure that you keep a backup of your config, if you've not written changes it to disk.
You can also view any and all data that Dashy is storing, using the developer tools. Open your browser's dev tools (usually <kbd>F12</kbd>), in Chromium head to the Application tab, or in Firefox go to the Storage tab. Select Local Storage, then scroll down the the URL Dashy is running on. You should now see all data being stored, and you can select and delete any fields you wish.
For a full list of all data that may be cached, see the [Privacy Docs](/docs/privacy.md#browser-storage).
---
## How-To Open Browser Console
When raising a bug, one crucial piece of info needed is the browser's console output. This will help the developer diagnose and fix the issue.

View File

@ -48,6 +48,12 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
- [AdGuard Home Filters](#adguard-home-filters)
- [AdGuard Home DNS Info](#adguard-home-dns-info)
- [AdGuard Home Top Domains](#adguard-home-top-domains)
- [Nextcloud User](#nextcloud-user)
- [Nextcloud User Statuses](#nextcloud-user-statuses)
- [Nextcloud Notifications](#nextcloud-notifications)
- [Nextcloud System](#nextcloud-system)
- [Nextcloud Stats](#nextcloud-stats)
- [Nextcloud PHP Opcache](#nextcloud-php-opcache-stats)
- **[System Resource Monitoring](#system-resource-monitoring)**
- [CPU Usage Current](#current-cpu-usage)
- [CPU Usage Per Core](#cpu-usage-per-core)
@ -1565,6 +1571,224 @@ Fetches data from your [AdGuard Home](https://adguard.com/en/adguard-home/overvi
---
### Nextcloud User
Nextcloud is a [self hosted](https://nextcloud.com/install/#instructions-server) productivity platform, it can also be used free of charge with [hundreds of existing hosting providers](https://nextcloud.com/sign-up/) that offer a free Nextcloud account.
Displays branding information of a Nextcloud server (logo, url, slogan) and some user details (name, login name, last login, disk space or quota). Use with regular or admin user.
Shows quota usage when quota is enabled for the user or disk usage when not enabled.
Known issues: the User API incorrectly reports available disk space as total for admin users when quota is not enabled (which usually is the case for admins).
<p align="center"><img width="450" src="https://i.ibb.co/F8Fdm3t/nextcloud-user.png" alt="nextcloud-user" /></p>
##### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`hostname`** | `string` | Required | The URL of the Nextcloud server
**`username`** | `string` | Required | Nextcloud username
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
##### Example
```yaml
- type: nextcloud-user
useProxy: true
options:
hostname: https://nextcloud.example.com
username: alice
password: xxxxx-xxxxx-xxxxx-xxxxx
```
##### Info
- **CORS**: 🟠 Proxied
- **Auth**: 🟢 Required
- **Price**: 🟢 Free
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
---
### Nextcloud User Statuses
Show user statuses for selected users.
<p align="center"><img width="450" src="https://i.ibb.co/Lk4DFT5/nextcloud-userstatus.png" alt="nextcloud-userstatus" /></p>
##### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`hostname`** | `string` | Required | The URL of the Nextcloud server
**`username`** | `string` | Required | Nextcloud username
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
**`users`** | `array` | Required | Nextcloud User IDs to show statuses for, list size between `1` and `100`
**`showEmpty`** | `boolean` | _Optional_ | Show statuses without a message, defaults to `true`
##### Example
```yaml
- type: nextcloud-userstatus
useProxy: true
options:
hostname: https://nextcloud.example.com
username: alice
password: xxxxx-xxxxx-xxxxx-xxxxx
users: ['bob', 'alice']
```
##### Info
- **CORS**: 🟠 Proxied
- **Auth**: 🟢 Required
- **Price**: 🟢 Free
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
---
### Nextcloud Notifications
Displays your notifications and allows deleting them.
<p align="center"><img width="450" src="https://i.ibb.co/yQCS51k/nextcloud-notifications.png" alt="nextcloud-notifications" /></p>
##### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`hostname`** | `string` | Required | The URL of the Nextcloud server
**`username`** | `string` | Required | Nextcloud username
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
**`limit`** | `number\|string` | _Optional_ | Limit displayed notifications either by count, e.g. `5` to show the 5 most recent, or by age, e.g. `1d` to only show notifications not older than a day. Accepted suffixes for age limit are `m`, `h` and `d`.
##### Example
```yaml
- type: nextcloud-userstatus
useProxy: true
options:
hostname: https://nextcloud.example.com
username: alice
password: xxxxx-xxxxx-xxxxx-xxxxx
limit: 6h
```
##### Info
- **CORS**: 🟠 Proxied
- **Auth**: 🟢 Required
- **Price**: 🟢 Free
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
---
### Nextcloud System
Visualises overall memory utilisation and CPU load averages, shows server versions.
<p align="center"><img width="450" src="https://i.ibb.co/KW4t6nG/nextcloud-system.png" alt="nextcloud-system" /></p>
##### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`hostname`** | `string` | Required | The URL of the Nextcloud server
**`username`** | `string` | Required | Must be a Nextcloud admin user
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
##### Example
```yaml
- type: nextcloud-system
useProxy: true
options:
hostname: https://nextcloud.example.com
username: alice
password: xxxxx-xxxxx-xxxxx-xxxxx
```
##### Info
- **CORS**: 🟠 Proxied
- **Auth**: 🟢 Required
- **Price**: 🟢 Free
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
---
### Nextcloud Stats
Shows key usage statistics about your Nextcloud server.
<p align="center"><img width="450" src="https://i.ibb.co/pPXPQFB/nextcloud-stats.png" alt="nextcloud-stats" /></p>
##### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`hostname`** | `string` | Required | The URL of the Nextcloud server
**`username`** | `string` | Required | Must be a Nextcloud admin user
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
##### Example
```yaml
- type: nextcloud-stats
useProxy: true
options:
hostname: https://nextcloud.example.com
username: alice
password: xxxxx-xxxxx-xxxxx-xxxxx
```
##### Info
- **CORS**: 🟠 Proxied
- **Auth**: 🟢 Required
- **Price**: 🟢 Free
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
---
### Nextcloud PHP Opcache Stats
Shows statistics about PHP Opcache perforamnce on your Nextcloud server.
<p align="center"><img width="450" src="https://i.ibb.co/xf6M4J2/nextcloud-phpopcache.png" alt="nextcloud-phpopcache" /></p>
##### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`hostname`** | `string` | Required | The URL of the Nextcloud server
**`username`** | `string` | Required | Must be a Nextcloud admin user
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
##### Example
```yaml
- type: nextcloud-stats
useProxy: true
options:
hostname: https://nextcloud.example.com
username: alice
password: xxxxx-xxxxx-xxxxx-xxxxx
```
##### Info
- **CORS**: 🟠 Proxied
- **Auth**: 🟢 Required
- **Price**: 🟢 Free
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
---
## System Resource Monitoring
The easiest method for displaying system info and resource usage in Dashy is with [Glances](https://nicolargo.github.io/glances/).

View File

@ -24,21 +24,23 @@ const printSuccess = () => {
// Check if the SSL certs are present and SSL should be enabled
let enableSSL = false;
stat(httpsCerts.public).then(() => {
stat(httpsCerts.private).then(() => {
const checkCertificateFiles = stat(httpsCerts.public).then(() => {
return stat(httpsCerts.private).then(() => {
enableSSL = true;
}).catch(() => { printNotSoGood('Private key not present'); });
}).catch(() => { printNotSoGood('Public key not present'); });
const startSSLServer = (app) => {
// If SSL should be enabled, create a secured server and start it
if (enableSSL) {
const httpsServer = https.createServer({
key: fs.readFileSync(httpsCerts.private),
cert: fs.readFileSync(httpsCerts.public),
}, app);
httpsServer.listen(SSLPort, () => { printSuccess(); });
}
checkCertificateFiles.then(() => {
// If SSL should be enabled, create a secured server and start it
if (enableSSL) {
const httpsServer = https.createServer({
key: fs.readFileSync(httpsCerts.private),
cert: fs.readFileSync(httpsCerts.public),
}, app);
httpsServer.listen(SSLPort, () => { printSuccess(); });
}
});
};
const middleware = (req, res, next) => {

View File

@ -303,6 +303,77 @@
"remaining": "Remaining",
"up": "Up",
"down": "Down"
},
"nextcloud": {
"active": "active",
"and": "and",
"applications": "applications",
"available": "available",
"away": "Away",
"cache-full": "CACHE FULL",
"chat-room": "chat room",
"delete-all": "Deleta all",
"delete-notification": "Delete notification",
"disabled": "disabled",
"disk-quota": "Disk Quota",
"disk-space": "Disk Space",
"dnd": "Do Not Distrub",
"email": "email",
"enabled": "enabled",
"federated-shares-ucfirst": "Federated shares",
"federated-shares": "federated shares",
"files": "file{plural}",
"free": "free",
"groups": "groups",
"hit-rate": "hit rate",
"hits": "hits",
"home": "home",
"in": "in",
"keys": "keys",
"last-24-hours": "last 24 hours",
"last-5-minutes": "in the last 5 minutes",
"last-hour": "in the last hour",
"last-login": "Last login",
"last-restart": "Last restart",
"load-averages": "Load Averages over all CPU cores",
"local-shares": "Local shares",
"local": "local",
"max-keys": "max keys",
"memory-used": "memory used",
"memory-utilisation": "memory utilisation",
"memory": "memory",
"misses": "misses",
"no-notifications": "No notifications",
"no-pending-updates": "no pending updates",
"nothing-to-show": "Nothing to show here at this time",
"of-which": "of which",
"of": "of",
"offline": "Offline",
"online": "Online",
"other": "other",
"overall": "Ovarall",
"private-link": "private link",
"public-link": "public link",
"quota-enabled": "Disk Quota is {not}enabled for this user",
"received": "received",
"scripts": "scripts",
"sent": "sent",
"started": "Started",
"storages-by-type": "Storages by type",
"storages": "storage{plural}",
"strings-use": "strings use",
"tasks": "Tasks",
"total-files": "total files",
"total-users": "total users",
"total": "total",
"until": "Until",
"updates-available-for": "Updates are available for",
"updates-available": "update{plural} available",
"used": "used",
"user": "user",
"using": "using",
"version": "version",
"wasted": "wasted"
}
}
}

View File

@ -175,9 +175,13 @@ export default {
if (this.enableStatusCheck) this.checkWebsiteStatus();
// If continious status checking is enabled, then start ever-lasting loop
if (this.statusCheckInterval > 0) {
setInterval(this.checkWebsiteStatus, this.statusCheckInterval * 1000);
this.intervalId = setInterval(this.checkWebsiteStatus, this.statusCheckInterval * 1000);
}
},
beforeDestroy() {
// Stop periodic status-check when item is destroyed (e.g. navigating in multi-page setup)
if (this.intervalId) clearInterval(this.intervalId);
},
};
</script>

View File

@ -1,15 +1,15 @@
<template>
<div class="apod-wrapper" v-if="image">
<div class="apod-wrapper" v-if="url">
<a :href="link" class="title" target="__blank" title="View Article">
{{ title }}
</a>
<a :href="hdImage" title="View HD Image" class="picture" target="__blank">
<img :src="image" :alt="title" />
<a :href="hdurl" title="View HD Image" class="picture" target="__blank">
<img :src="url" :alt="title" />
</a>
<p class="copyright">{{ copyright }}</p>
<p class="description">{{ truncatedDescription }}</p>
<p class="explanation">{{ truncatedExplanation }}</p>
<p @click="toggleShowFull" class="expend-details-btn">
{{ showFullDesc ? $t('widgets.general.show-less') : $t('widgets.general.show-more') }}
{{ showFullExp ? $t('widgets.general.show-less') : $t('widgets.general.show-more') }}
</p>
</div>
</template>
@ -24,17 +24,17 @@ export default {
data() {
return {
title: null,
image: null,
hdImage: null,
link: null,
description: null,
url: null,
hdurl: null,
link: 'https://apod.nasa.gov/apod/astropix.html',
explanation: null,
copyright: null,
showFullDesc: false,
showFullExp: false,
};
},
computed: {
truncatedDescription() {
return this.showFullDesc ? this.description : `${this.description.substring(0, 100)}...`;
truncatedExplanation() {
return this.showFullExp ? this.explanation : `${this.explanation.substring(0, 100)}...`;
},
},
methods: {
@ -52,14 +52,14 @@ export default {
},
processData(data) {
this.title = data.title;
this.image = data.url;
this.hdImage = data.hdurl;
this.link = data.apod_site;
this.description = data.description;
this.url = data.url;
this.hdurl = data.hdurl;
this.link = data.link;
this.explanation = data.explanation;
this.copyright = data.copyright;
},
toggleShowFull() {
this.showFullDesc = !this.showFullDesc;
this.showFullExp = !this.showFullExp;
},
},
};
@ -85,7 +85,7 @@ export default {
opacity: var(--dimming-factor);
color: var(--widget-text-color);
}
p.description {
p.explanation {
color: var(--widget-text-color);
font-size: 1rem;
margin: 0.5rem 0;

View File

@ -0,0 +1,208 @@
<template>
<div class="nextcloud-widget nextcloud-status-wrapper">
<div v-if="notifications.length">
<!-- group actions: delete all -->
<p v-if="canDeleteNotification('delete-all')" class="group-action">
<span class="action secondary" @click="deleteNotifications">{{ tt('delete-all') }}</span>
</p>
<hr/>
<!-- notifications list -->
<div v-for="(notification, idx) in notifications" :key="idx" class="notification">
<div><img :src="notificationIcon(notification.icon)" /></div>
<div>
<p>
<small class="date" v-tooltip="dateTooltip(notification)">
{{ getTimeAgo(Date.parse(notification.datetime)) }}
</small> <span v-tooltip="subjectTooltip(notification)">{{ notification.subject }} </span>
<!-- notifications item: action links -->
<span v-if="notification.actions.length">
<span v-for="(action, idx) in notification.actions" :key="idx">
<a :href="action.link" class="action" target="_blank">{{ action.label }}</a>
</span>
</span>
<span v-if="canDeleteNotification('delete')">
<a @click="deleteNotification(notification.notification_id)"
class="action secondary">{{ tt('delete-notification') }}</a>
</span>
</p>
</div>
<hr/>
</div>
</div>
<!-- empty notifications list -->
<div v-else class="sep">
<p>{{ tt('no-notifications') }}</p>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
/**
* NextcloudNotifications widget - Displays the user's notifications
* Used endpoints
* - capabilities: to determine if the User Notification API is enabled
* - notifications: to fetch list of notifications, delete all or a single notification
*/
export default {
mixins: [WidgetMixin, NextcloudMixin],
components: {},
data() {
return {
notifications: [],
};
},
computed: {
/* Parse the limit user option to either an integer or to an integer + 'm', 'h' or 'd' */
limit() {
const lim = this.options.limit;
const defaultLimit = [0, false];
if (typeof lim === 'string') {
const k = { m: 60, h: 60 * 60, d: 60 * 60 * 24 };
const m = lim.match(/(\d+)([hmd])/);
if (m.length !== 3) return defaultLimit;
return [false, m[1] * k[m[2]] * 1000];
}
if (typeof lim === 'number') {
return [parseInt(this.options.limit, 10) || 0, false];
}
return defaultLimit;
},
},
methods: {
allowedStatuscodes() {
return [100, 200];
},
async fetchData() {
if (!this.hasValidCredentials()) return;
await this.loadCapabilities();
if (!this.capabilities?.notifications?.enabled) {
this.error('This Nextcloud server doesn\'t support the Notifications API');
return;
}
this.makeRequest(this.endpoint('notifications'), this.headers)
.then(this.processNotifications)
.finally(this.finishLoading);
},
processNotifications(response) {
const notifications = this.validateResponse(response);
const [limitCount, limitTime] = this.limit;
this.notifications = [];
notifications.forEach((notification) => {
if (limitCount && this.notifications.length === limitCount) return; // count limit
const notiTime = Date.parse(notification.datetime);
const nowTime = new Date().getTime();
if (limitTime && notiTime && nowTime - notiTime > limitTime) return; // time limit
this.notifications.push(notification);
});
},
/* Transform icon URL to SVG Color API request URL
* @see https://docs.nextcloud.com/server/latest/developer_manual/html_css_design/icons.html */
notificationIcon(url) {
const color = this.getValueFromCss('widget-text-color').replace('#', '');
return url.replace('core/img', 'svg/core')
.replace(/extra-apps\/([^/]+)\/img/, 'svg/$1')
.replace(/apps\/([^/]+)\/img/, 'svg/$1')
.replace('.svg', `?color=${color}`);
},
/* Notification actions */
canDeleteNotification(deleteTarget) {
const capNotif = this.capabilities?.notifications?.features;
return Array.isArray(capNotif) && capNotif.includes(deleteTarget);
},
deleteNotifications() {
this.makeRequest(this.endpoint('notifications'), this.headers, 'DELETE')
.then(() => {
this.notifications = [];
});
},
deleteNotification(id) {
this.makeRequest(`${this.endpoint('notifications')}/${id}`, this.headers, 'DELETE')
.then(this.fetchData);
},
/* Tooltip generators */
subjectTooltip(notification) {
const content = notification.message;
return {
content, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
dateTooltip(notification) {
const content = new Date(Date.parse(notification.datetime)).toLocaleString();
return {
content, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
},
created() {
this.overrideUpdateInterval = 60;
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-status-wrapper {
div p small i {
position: relative;
top: .25em;
}
small.date {
background: var(--widget-text-color);
color: var(--widget-accent-color);
border-radius: .25em;
padding: .15em .3em;
margin: .25em .25em .25em 0;
display: inline-block;
font-weight: bold;
}
p.group-action {
margin-top: 0;
}
span.action, span a.action {
cursor: pointer;
margin: .1em .5em .1em 0;
padding: .15em;
border-radius: .25em;
white-space: nowrap;
}
span.action:hover, span a.action:hover {
background: var(--widget-text-color);
color: var(--widget-accent-color);
text-decoration: underline;
}
.secondary {
opacity: .5;
font-size: 75%;
margin-left: .2rem;
}
div.notification {
display: table;
width: 100%;
> div:first-child {
float: right;
}
> div:nth-child(2) {
float: left;
width: 93%;
}
> div {
display: table-cell;
text-align: left;
> img {
float: right;
width: 1em;
position: relative;
top: 1em;
opacity: .75;
}
}
}
div hr {
margin-top: .3em;
margin-bottom: 0;
}
}
</style>

View File

@ -0,0 +1,214 @@
<template>
<div v-if="didLoadData" class="nextcloud-widget nextcloud-phpopcache-wrapper">
<div class="sep">
<!-- PHP opcache enabled and cache full -->
<p v-tooltip="opcacheStartTimeTooltip()">
<i class="fal fa-microchip"></i>
<strong>PHP opcache</strong>&nbsp;
<em v-if="opcache.opcache_enabled" class="oc-enabled">
{{ tt('enabled') }}
</em>
<em v-else class="oc-disabled">{{ tt('disabled') }}</em>&nbsp;
<strong v-if="opcache.cache_full" class="oc-full">
<i class="far fa-siren-on"></i>{{ tt('cache-full') }}
</strong>
</p>
<hr/>
<!-- PHP opcache stats -->
<div v-if="opcache.opcache_enabled">
<!-- PHP opcache stats: hit/miss -->
<p v-tooltip="opcacheStatsTooltip()">
<i class="fal fa-bullseye-arrow"></i>
<em v-html="formatNumber(opcache_stats.hits)"></em>&nbsp;
<small>{{ tt('hits') }}</small>&nbsp;
<em v-html="formatNumber(opcache_stats.misses)"></em>&nbsp;
<small>{{ tt('misses') }}</small>&nbsp;
<em v-html="formatPercent(opcache_stats.opcache_hit_rate, 3)"></em>&nbsp;
<small>{{ tt('hit-rate') }}</small>
</p>
<hr/>
<!-- PHP opcache stats: memory -->
<p v-tooltip="opcacheMemoryUsageTooltip()">
<i class="fal fa-memory"></i>
<em v-html="formatPercent(opcache.memory_usage.used_memory_percentage, 1)"></em>&nbsp;
<small>of</small>
<em v-html="convertBytes(opcache.memory_usage.total_memory)"></em>&nbsp;
<small>{{ tt('memory-used') }}</small>
</p>
<hr/>
<!-- PHP opcache stats: interned strings -->
<p v-tooltip="opcacheInternedStringsTooltip()">
<i class="fal fa-puzzle-piece"></i>
<em v-html="formatNumber(opcache.interned_strings_usage.number_of_strings, 1, true)"></em>
&nbsp;<small>{{ tt('strings-use') }}</small>
<em v-html="formatPercent(opcache.interned_strings_usage.used_memory_percentage)"></em>
&nbsp;<small>{{ tt('of') }}</small>
<em v-html="convertBytes(opcache.interned_strings_usage.total_memory)"></em>
</p>
<hr/>
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
/**
* NextcloudPhpOpcache widget - Shows statistics about PHP opcache performance
* Used endpoints
* - serverinfo: requires Nextcloud admin user
*/
export default {
mixins: [WidgetMixin, NextcloudMixin],
components: {},
data() {
return {
opcache: {
opcache_enabled: null,
full: null,
opcache_statistics: {
num_cached_scripts: null,
num_cached_keys: null,
max_cached_keys: null,
hits: null,
start_time: null,
last_restart_time: null,
misses: null,
opcache_hit_rate: null,
},
memory_usage: {
used_memory: null,
free_memory: null,
total_memory: null,
wasted_memory: null,
used_memory_percentage: null,
current_wasted_percentage: null,
},
interned_strings_usage: {
buffer_size: null,
used_memory: null,
total_memory: null,
free_memory: null,
number_of_strings: null,
used_memory_percentage: null,
},
},
};
},
computed: {
didLoadData() {
return typeof (this?.opcache?.opcache_enabled) === 'boolean';
},
// shortcuts to data members
opcache_stats() {
return this.opcache.opcache_statistics;
},
opcache_interned() {
return this.opcache.interned_strings_usage;
},
},
methods: {
allowedStatuscodes() {
return [200];
},
fetchData() {
if (!this.hasValidCredentials()) return;
this.makeRequest(this.endpoint('serverinfo'), this.headers)
.then(this.processServerInfo)
.finally(() => this.finishLoading());
},
processServerInfo(serverData) {
const data = this.validateResponse(serverData);
this.opcache = data.server?.php?.opcache;
if (!this.opcache) return;
this.updateOpcacheMemory();
this.updateOpcacheInterned();
},
updateOpcacheMemory() {
this.opcache_stats.opcache_hit_rate = parseFloat(
this.opcache_stats.opcache_hit_rate,
).toFixed(3);
this.opcache.memory_usage.total_memory = (
this.opcache.memory_usage.used_memory + this.opcache.memory_usage.free_memory
);
this.opcache.memory_usage.used_memory_percentage = parseFloat(
(this.opcache.memory_usage.used_memory / this.opcache.memory_usage.total_memory) * 100,
).toFixed(1);
},
updateOpcacheInterned() {
this.opcache_interned.total_memory = (
this.opcache_interned.used_memory + this.opcache_interned.free_memory
);
this.opcache_interned.used_memory_percentage = parseFloat(
(this.opcache_interned.used_memory / this.opcache_interned.total_memory) * 100,
).toFixed(5);
},
/* Tooltip generators */
opcacheStartTimeTooltip() {
let content = `${this.tt('started')} `
+ `${new Date(this.opcache_stats.start_time * 1000).toLocaleString()}`;
if (this.opcache_stats.last_restart_time) {
content = content.concat(
`<br><br>${this.tt('last-restart')} `
+ `${new Date(this.opcache_stats.last_restart_time * 1000).toLocaleString()}`,
);
}
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
opcacheStatsTooltip() {
const content = `${parseFloat(this.opcache_stats.hits).toLocaleString()} ${this.tt('hits')}<br>`
+ `${parseFloat(this.opcache_stats.misses).toLocaleString()} ${this.tt('misses')}<br><br>`
+ `${parseFloat(this.opcache_stats.num_cached_scripts).toLocaleString()} ${this.tt('scripts')}<br>`
+ `${parseFloat(this.opcache_stats.num_cached_keys).toLocaleString()} ${this.tt('keys')}<br>`
+ `${parseFloat(this.opcache_stats.max_cached_keys).toLocaleString()} ${this.tt('max-keys')}<br>`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
opcacheMemoryUsageTooltip() {
const content = `PHP opcache ${this.tt('memory-utilisation')}<br><br>`
+ `${this.convertBytes(this.opcache.memory_usage.total_memory)} ${this.tt('total')}<br>`
+ `${this.convertBytes(this.opcache.memory_usage.used_memory)} ${this.tt('used')}<br>`
+ `${this.convertBytes(this.opcache.memory_usage.free_memory)} ${this.tt('free')}<br><br>`
+ `${this.convertBytes(this.opcache.memory_usage.wasted_memory)} (`
+ `${parseFloat(this.opcache.memory_usage.current_wasted_percentage).toFixed(1)}%) ${this.tt('wasted')}`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
opcacheInternedStringsTooltip() {
const content = 'PHP opcache interned strings<br><br>'
+ `${this.convertBytes(this.opcache_interned.buffer_size)} ${this.tt('total')} ${this.tt('memory')}<br>`
+ `${this.convertBytes(this.opcache_interned.used_memory)} ${this.tt('used')} ${this.tt('memory')}<br>`
+ `${this.convertBytes(this.opcache_interned.free_memory)} ${this.tt('free')} ${this.tt('memory')}<br><br>`
+ `${parseFloat(this.opcache_interned.number_of_strings).toLocaleString()}`
+ ' strings';
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
},
created() {
this.overrideUpdateInterval = 60;
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-phpopcache-wrapper {
.oc-enabled {
color: var(--success);
}
.oc-disabled {
color: var(--neutral);
}
.oc-full {
color: var(--danger);
}
}
</style>

View File

@ -0,0 +1,199 @@
<template>
<div v-if="didLoadData" class="nextcloud-widget nextcloud-stats-wrapper">
<div class="server-info sep">
<!-- server info: users -->
<div v-if="activeUsers">
<p v-tooltip="activeUsersTooltip()">
<i class="fal fa-user"></i>
<em v-html="formatNumber(storage.num_users)"></em>
<strong>{{ tt('total-users') }}</strong> <small>{{ tt('of-which') }}</small>
<em v-html="formatNumber(activeUsers.last24hours)"></em>
<strong>{{ tt('active') }}</strong> <small>({{ tt('last-24-hours') }})</small>
</p>
</div>
<hr />
<div v-if="nextcloud">
<!-- server info: apps -->
<p v-tooltip="appUpdatesTooltip()">
<i class="fal fa-browser"></i>
<em v-html="formatNumber(apps.num_installed)"></em>
<strong>{{ tt('applications') }}</strong>
<span v-if="apps.num_updates_available" class="nc-updates">
<i class="fal fa-download"></i><em>{{ apps.num_updates_available }}</em>
<strong>
{{ tt('updates-available',
{plural: apps.num_updates_available > 1 ? 's' : ''}) }}
</strong>
</span>
<small v-else data-nc-updates class="disabled">{{ tt('no-pending-updates') }}</small>
</p>
<hr />
<!-- server info: storage -->
<p v-tooltip="storagesTooltip()">
<i class="fal fa-file"></i><em v-html="formatNumber(storage.num_files)"></em>
<strong>{{ tt('files', { plural: storage.num_files > 1 ? 's' : '' }) }}</strong>&nbsp;
<small>{{ tt('in') }}</small><em>{{ storage.num_storages }}</em>
<strong>{{ tt('storages', { plural: storage.num_storages > 1 ? 's' : '' }) }}</strong>
&nbsp;&nbsp;<strong v-html="convertBytes(system.freespace)"></strong>&nbsp;
<small>{{ tt('free') }}</small>
</p>
<hr />
<!-- server info: shares -->
<p v-tooltip="sharesTooltip()">
<i class="fal fa-share"></i>
<em v-html="formatNumber(shares.num_shares)"></em>
<strong>{{ tt('local') }}</strong> <small> {{ tt('and') }}</small>
<em v-html="formatNumber(shares.num_fed_shares_sent
+ shares.num_fed_shares_received)"></em>
<strong>
{{ tt('federated-shares') }}
</strong>
</p>
<hr />
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
/**
* NextcloudStats widget - Shows statistics about Nextcloud usage
* Used endpoints
* - serverinfo: requires Nextcloud admin user
*/
export default {
mixins: [WidgetMixin, NextcloudMixin],
components: {},
data() {
return {
nextcloud: {
system: {
freespace: null,
apps: {
num_installed: null,
num_updates_available: 0,
app_updates: [],
},
},
storage: {
num_users: null,
num_files: null,
num_storages: null,
},
shares: {
num_shares: null,
num_shares_user: null,
num_shares_groups: null,
num_shares_link: null,
num_shares_mail: null,
num_shares_room: null,
num_shares_link_no_password: null,
num_fed_shares_sent: null,
num_fed_shares_received: null,
},
},
activeUsers: {
last5minutes: null,
last1hour: null,
last24hours: null,
},
};
},
computed: {
didLoadData() {
return !!(this?.system?.freespace);
},
// data shortcuts
system() {
return this.nextcloud.system;
},
storage() {
return this.nextcloud.storage;
},
shares() {
return this.nextcloud.shares;
},
apps() {
return this.nextcloud.system.apps;
},
},
methods: {
allowedStatuscodes() {
return [200];
},
fetchData() {
if (!this.hasValidCredentials()) return;
this.makeRequest(this.endpoint('serverinfo'), this.headers)
.then(this.processServerInfo)
.finally(this.finishLoading);
},
processServerInfo(serverResponse) {
const data = this.validateResponse(serverResponse);
this.nextcloud = data.nextcloud;
this.activeUsers = data.activeUsers;
},
/* Tooltip generators */
activeUsersTooltip() {
const content = `${parseFloat(this.activeUsers.last5minutes).toLocaleString()}`
+ ` ${this.tt('last-5-minutes')}<br>`
+ `${parseFloat(this.activeUsers.last1hour).toLocaleString()}`
+ ` ${this.tt('last-hour')}<br>`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
appUpdatesTooltip() {
let content = `<strong>${this.tt('updates-available-for')}</strong><ul>`;
Object.entries(this.system.apps.app_updates).forEach(([app, version]) => {
content += `<li>${app}: ${version}</li>`;
});
content += '</ul>';
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
storagesTooltip() {
const content = `<strong>${this.tt('storages-by-type')}</strong><ul><li>`
+ `${parseFloat(this.storage.num_storages_local).toLocaleString()} ${this.tt('local')}</li><li>`
+ `${parseFloat(this.storage.num_storages_home).toLocaleString()} ${this.tt('home')}</li><li>`
+ `${parseFloat(this.storage.num_storages_other).toLocaleString()} ${this.tt('other')}</li></ul>`
+ `${parseFloat(this.storage.num_files).toLocaleString()} ${this.tt('total-files')}`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
sharesTooltip() {
const content = `<strong>${this.tt('local-shares')}</strong><ul><li>`
+ `${parseFloat(this.shares.num_shares_user).toLocaleString()} ${this.tt('user')}</li><li>`
+ `${parseFloat(this.shares.num_shares_groups).toLocaleString()} ${this.tt('groups')}</li><li>`
+ `${parseFloat(this.shares.num_shares_mail).toLocaleString()} ${this.tt('email')}</li><li>`
+ `${parseFloat(this.shares.num_shares_room).toLocaleString()} ${this.tt('chat-room')}</li><li>`
+ `${parseFloat(this.shares.num_shares_link).toLocaleString()} ${this.tt('private-link')}</li><li>`
+ `${parseFloat(this.shares.num_shares_link_no_password).toLocaleString()} ${this.tt('public-link')}</li></ul>`
+ `<strong>${this.tt('federated-shares-ucfirst')}</strong><ul><li>`
+ `${parseFloat(this.shares.num_fed_shares_sent).toLocaleString()} ${this.tt('sent')}</li><li>`
+ `${parseFloat(this.shares.num_fed_shares_received).toLocaleString()} ${this.tt('received')}</li></ul>`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
},
created() {
this.overrideUpdateInterval = 20;
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-stats-wrapper {
div.server-info .nc-updates {
color: var(--success);
margin-left: .5em;
}
}
</style>

View File

@ -0,0 +1,230 @@
<template>
<div v-if="didLoadData" class="nextcloud-widget nextcloud-system-wrapper">
<div class="charts">
<!-- memory gauge -->
<div class="chart-container">
<small>{{ tt('overall') }} {{ tt('memory-utilisation') }}</small>
<GaugeChart :value="memoryGauge.value"
:baseColor="memoryGauge.background"
:gaugeColor="memoryGauge.color">
<p class="percentage">{{ memoryGauge.value }}%</p>
</GaugeChart>
<small>{{ getMemoryGaugeLabel() }}</small>
</div>
<!-- cpu load chart -->
<div>
<div
:id="cpuLoadChartId" class="load-chart"
v-tooltip="$t('widgets.glances.system-load-desc')"></div>
</div>
</div>
<div>
<!-- server info: server -->
<hr />
<p>
<i class="fal fa-server"></i>
<strong>Nextcloud</strong>
<em>{{ server.nextcloud.system.version }}</em> <small> </small>
<strong>{{ server.server.webserver }}/PHP</strong>
<em>{{ server.server.php.version }}</em>
</p>
<hr />
<!-- server info: database -->
<p>
<i class="fal fa-database"></i>
<strong>{{ server.server.database.type }}</strong>
<em>{{ server.server.database.version }}</em> <small>{{ tt('using') }}</small>
<em v-html="convertBytes(server.server.database.size)"></em>
</p>
<hr/>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
import GaugeChart from '@/components/Charts/Gauge';
import ChartingMixin from '@/mixins/ChartingMixin';
/**
* NextcloudSystem widget - Visualises CPU load and memory utilisation and shows server versions
* Used endpoints
* - serverinfo: requires Nextcloud admin user
*/
export default {
mixins: [WidgetMixin, NextcloudMixin, ChartingMixin],
components: { GaugeChart },
data() {
return {
server: {
server: {
database: {
type: null,
version: null,
size: null,
},
webserver: null,
php: {
version: null,
},
},
nextcloud: {
system: {
version: null,
freespace: null,
cpuload: [],
mem_total: null,
mem_free: null,
mem_percent: null,
},
},
},
memoryGauge: {
value: 0,
color: '#272f4d',
showMoreInfo: false,
moreInfo: null,
background: '#16161d',
},
};
},
computed: {
cpuLoadChartId() {
return `nextcloud-cpu-load-chart-${Math.random().toString().slice(-4)}`;
},
didLoadData() {
return !!(this.server?.nextcloud?.system?.version);
},
},
methods: {
allowedStatuscodes() {
return [200];
},
async fetchData() {
if (!this.hasValidCredentials()) return;
this.makeRequest(this.endpoint('serverinfo'), this.headers)
.then(this.processServerInfo)
.finally(() => this.finishLoading());
},
processServerInfo(serverData) {
const data = this.validateResponse(serverData);
if (!data || data.length === 0) return;
this.server.nextcloud.system = data.nextcloud?.system;
this.server.server.php.version = data.server?.php?.version;
this.server.server.database = data.server?.database;
this.server.server.webserver = data.server?.webserver;
},
updateMemoryGauge(sys) {
this.memoryGauge.value = parseFloat(
(((sys.mem_total - sys.mem_free) / sys.mem_total) * 100).toFixed(2),
);
this.memoryGauge.color = this.getMemoryGaugeColor(this.memoryGauge.value);
},
updateOpcacheMemory() {
this.opcache_stats.opcache_hit_rate = parseFloat(
this.opcache_stats.opcache_hit_rate,
).toFixed(3);
this.opcache.memory_usage.total_memory = (
this.opcache.memory_usage.used_memory + this.opcache.memory_usage.free_memory
);
this.opcache.memory_usage.used_memory_percentage = parseFloat(
(this.opcache.memory_usage.used_memory / this.opcache.memory_usage.total_memory) * 100,
).toFixed(1);
},
updateOpcacheInterned() {
this.opcache.interned_strings_usage.total_memory = (
this.opcache.interned_strings_usage.used_memory
+ this.opcache.interned_strings_usage.free_memory
);
this.opcache.interned_strings_usage.used_memory_percentage = parseFloat(
(this.opcache.interned_strings_usage.used_memory
/ this.opcache.interned_strings_usage.total_memory) * 100,
).toFixed(5);
},
getMemoryGaugeColor(memPercent) {
if (memPercent < 50) return this.getColorRgba('widget-text-color', 0.6);
if (memPercent < 60) return this.getColorRgba('warning', 0.75);
if (memPercent < 80) return this.getColorRgba('error', 0.9);
if (memPercent < 100) return this.getColorRgba('danger');
return this.getColorRgba('background');
},
getMemoryGaugeLabel() {
const sys = this.server.nextcloud.system;
return `${this.convertBytes((sys.mem_total - sys.mem_free) * 1024, 2, false)} / `
+ `${this.convertBytes(sys.mem_total * 1024, 2, false)}`;
},
updateCpuLoad(load) {
const chartData = {
labels: ['1m', '5m', '15m'],
datasets: [{ values: [load[0], load[1], load[2]] }],
};
const chartTitle = this.tt('load-averages');
this.renderCpuLoadChart(chartData, chartTitle);
},
renderCpuLoadChart(loadBarChartData, chartTitle) {
return new this.Chart(`#${this.cpuLoadChartId}`, {
title: chartTitle,
data: loadBarChartData,
type: 'bar',
height: 180,
colors: [this.getColorRgba('widget-text-color', 0.6)],
barOptions: {
spaceRatio: 0.2,
},
tooltipOptions: {
formatTooltipY: d => `${d} ${this.tt('tasks')}`,
},
});
},
},
created() {
this.overrideUpdateInterval = 30;
},
updated() {
const load = this.server?.nextcloud?.system?.cpuload;
if (load) this.updateCpuLoad(load);
const sys = this.server.nextcloud.system;
if (sys) this.updateMemoryGauge(sys);
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-system-wrapper {
div.charts {
> div {
float: left;
}
> div:first-child {
max-width: 44%;
small {
font-size: 12px;
color: #666666;
display: inline-block;
width: 100%;
text-align: center;
margin: .9em 0 1.4em 0;
opacity: 1;
}
small:last-child {
margin-top: 2em;
font-size: 10px;
}
}
> div:nth-child(2) {
min-width: 55%;
}
p.percentage {
color: var(--widget-text-color);
text-align: center;
position: absolute;
font-size: 1.3em;
margin: .5em 0;
width: 100%;
bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,206 @@
<template>
<div v-if="didLoadData" class="nextcloud-widget nextcloud-info-wrapper">
<!-- logo, branding, user info -->
<div>
<div class="logo">
<a :href="branding.url" target="_blank">
<img :src="branding.logo" />
</a>
<p>{{ branding.slogan }}</p>
</div>
<div class="info">
<p class="brand">{{ branding.name }}</p>
<p class="version" v-if="version.string">
<small>Nextcloud {{ tt('version') }} {{ version.string }}</small>
</p>
<p class="username">{{ user.displayName }} <em>({{ user.id }})</em></p>
<p class="login" v-tooltip="lastLoginTooltip()">
<span>{{ tt('last-login') }}</span>&nbsp;
<small>{{ getTimeAgo(user.lastLogin) }}</small>
</p>
</div>
</div>
<!-- disk space/quota -->
<div>
<div v-tooltip="quotaTooltip()">
<hr/>
<p>
<i class="fal fa-disc-drive"></i>
<strong v-if="user.quota.quota > 0">{{ tt('disk-quota') }}</strong>
<strong v-else>{{ tt('disk-space') }}</strong>
<em v-html="formatPercent(user.quota.relative)"></em>
<small>{{ tt('of') }}</small><strong v-html="convertBytes(user.quota.total)"></strong>
<span v-if="quotaChart.dataLoaded">
<PercentageChart :values="quotaChart.data" :showLegend="false" />
</span>
</p>
<hr/>
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
import PercentageChart from '@/components/Charts/PercentageChart';
/**
* NextcloudUser widget - Displays branding and user information
* Used endpoints
* - capabilities: this delivers branding info (server name, logo, slogan, etc.)
* - user: name, last login, disk quota info
*/
export default {
mixins: [WidgetMixin, NextcloudMixin],
components: { PercentageChart },
data() {
return {
user: {
id: null,
displayName: null,
email: null,
quota: {
relative: null,
total: null,
used: null,
free: null,
quota: null,
},
},
quotaChart: {
dataLoaded: false,
data: [
{ label: null, size: null, color: null },
{ label: null, size: null, color: null },
],
},
};
},
computed: {
didLoadData() {
return !!this.user.id;
},
},
methods: {
allowedStatuscodes() {
return [100, 200];
},
fetchData() {
if (!this.hasValidCredentials()) return;
this.loadCapabilities()
.then(this.loadUser)
.finally(this.finishLoading);
},
loadUser() {
return this.makeRequest(this.endpoint('user'), this.headers)
.then(this.processUser);
},
processUser(userResponse) {
const user = this.validateResponse(userResponse);
this.user.id = user.id;
this.user.email = user.email;
this.user.quota = user.quota;
this.user.displayName = user.displayname;
this.user.lastLogin = user.lastLogin;
},
getQuotaChartColorUsed(percent) {
if (percent < 0.75) return this.getValueFromCss('widget-text-color');
if (percent < 0.85) return this.getValueFromCss('warning');
if (percent < 0.95) return this.getValueFromCss('error');
return this.getValueFromCss('danger');
},
updateQuotaChart() {
const used = parseFloat(this.user.quota.used / this.user.quota.total);
const free = parseFloat(this.user.quota.free / this.user.quota.total);
const d = this.quotaChart.data;
d[0] = { label: this.tt('used'), size: used, color: this.getQuotaChartColorUsed(used) };
d[1] = { label: this.tt('available'), size: free, color: this.getValueFromCss('success') };
this.quotaChart.dataLoaded = true;
},
/* Tooltip generators */
quotaTooltip() {
const quotaEnabled = this.user.quota.quota > 0;
const content = `${this.tt('quota-enabled', { not: quotaEnabled ? '' : 'not ' })}`
+ `<br><br>${this.convertBytes(this.user.quota.used)} ${this.tt('used')}<br>`
+ `${this.convertBytes(this.user.quota.free)} ${this.tt('free')}<br>`
+ `${this.convertBytes(this.user.quota.total)} ${this.tt('total')}`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
lastLoginTooltip() {
const content = new Date(this.user.lastLogin).toLocaleString();
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
},
created() {
this.overrideUpdateInterval = 120;
},
updated() {
this.updateQuotaChart();
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-info-wrapper {
> div:first-child {
display: flex;
}
> div:nth-child(2) {
border-top: none;
}
div.percentage-chart-wrapper {
margin: 0 .75em;
width: 5em;
position: relative;
top: .2em;
float: right;
}
div.logo {
width: 40%;
text-align: center;
img {
width: 8em;
}
p {
font-size: .9em;
opacity: .85;
}
}
div.info {
width: 56%;
p {
margin: 0 0 1rem 0;
}
p:last-child {
margin: 0;
}
p.brand {
margin: 0;
font-size: 1.35em;
font-weight: 800;
letter-spacing: 3px;
}
p.version small {
font-size: .75em;
}
p.username {
font-size: 1.1em;
em {
font-size: .9em;
}
}
p.login {
span {
font-size: .9em;
margin-right: .25em;
}
}
}
}
</style>

View File

@ -0,0 +1,202 @@
<template>
<div class="nextcloud-widget nextcloud-user-status-wrapper">
<div v-if="didLoadData" class="sep">
<!-- user statuses: list -->
<div v-for="(status, userId) in statuses" :key="userId" class="user">
<div>
<!-- user status: emoji -->
<div>
<i>{{ status.icon }}</i>
</div>
<!-- user status: message -->
<div>
<p v-tooltip="clearAtTooltip(status.clearAt)">
<strong>{{ status.userId }}</strong>&nbsp;
<small v-if="status.clearAt"><i class="fal fa-clock"></i></small>
<span v-else-if="status.message"></span><em>{{ status.message }}</em>
</p>
</div>
<!-- user status: status -->
<div>
<p>
<small :class="`status ${status.status}`">
<i v-if="status.status === 'online' || status.status === 'dnd'"
class="fas fa-circle" v-tooltip="tt(status.status)"></i>
<i v-else class="far fa-circle" v-tooltip="tt(status.status)"></i>
</small>
</p>
</div>
</div>
<hr/>
</div>
</div>
<!-- user statuses: no content -->
<div v-else class="sep"><p>{{ tt('nothing-to-show') }}</p></div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
// Nextcloud User Status API supports getting all user statuses at once
// or a single user's status. {fetchStrategy} determines which of these methods to use.
const fetchStrategies = {
allAtOnce: 'AllAtOnce',
oneByOne: 'OneByOne',
};
/**
* NextcloudUserStatus widget - Displays user statuses
* Used endpoints
* - capabilities: to determine if the User Status API is enabled
* - userstatus: to fetch a single or all user statuses
*/
export default {
mixins: [WidgetMixin, NextcloudMixin],
components: {},
computed: {
didLoadData() {
return !!Object.keys(this?.statuses || {}).length;
},
fetchStrategy() {
if (!this.options.fetchStrategy) {
return fetchStrategies.allAtOnce;
}
if (!Object.values(fetchStrategies).includes(this.options.fetchStrategy)) {
return fetchStrategies.allAtOnce;
}
return this.options.fetchStrategy;
},
users() {
if (!this.options.users || !Array.isArray(this.options.users)) return [];
if (this.options.users.length > 100) return this.options.users.slice(0, 100);
return this.options.users;
},
showEmpty() {
return !!this.options.showEmpty;
},
},
data() {
return {
statuses: {},
};
},
methods: {
allowedStatuscodes() {
return [100, 200];
},
async fetchData() {
if (!this.hasValidCredentials() || !this.users.length) return;
await this.loadCapabilities();
if (!this.capabilities?.userStatus) {
this.error('This Nextcloud server doesn\'t support the User Status API');
return;
}
if (this.fetchStrategy === fetchStrategies.allAtOnce) {
this.makeRequest(this.endpoint('userstatus'), this.headers)
.then(this.processStatuses)
.finally(this.finishLoading);
} else {
const promises = [];
this.newStatuses = {};
this.users.forEach((user) => {
promises.push(
this.makeRequest(`${this.endpoint('userstatus')}/${user}`, this.headers)
.then(this.processStatus),
);
});
Promise.all(promises)
.then(() => {
this.statuses = this.newStatuses;
delete this.newStatuses;
})
.finally(this.finishLoading);
}
},
processStatuses(response) {
const statuses = this.validateResponse(response);
const newStatuses = {};
Object.values(statuses).forEach((status) => {
if (!this.users.includes(status.userId)) return;
if (!status.message && !this.showEmpty) return;
newStatuses[status.userId] = status;
});
this.statuses = newStatuses;
},
processStatus(response) {
const raw = this.validateResponse(response);
const status = Array.isArray(raw) && raw.length ? raw[0] : raw;
if (status && (status.message || this.showEmpty)) {
this.newStatuses[status.userId] = status;
}
},
/* Tooltip generators */
clearAtTooltip(clearAtTime) {
const content = clearAtTime ? `${this.tt('until')}`
+ ` ${new Date(clearAtTime * 1000).toLocaleString()}` : '';
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
},
created() {
this.overrideUpdateInterval = 60;
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-user-status-wrapper {
.status {
float: right;
i {
position: relative;
top: .15rem;
margin: 0;
}
}
.online {
color: var(--success);
}
.offline {
color: var(--medium-grey);
}
.away {
color: var(--error);
}
.dnd {
color: var(--danger);
}
div.user > div {
display: table;
width: 100%;
> div:first-child {
width: 1.75em;
text-align: center;
> i {
font-style: normal;
}
}
> div:nth-child(2) {
p small i {
top: 0;
opacity: .5;
margin: 0;
}
}
> div {
display: table-cell;
text-align: left;
}
}
div.user hr {
margin-top: .3em;
margin-bottom: .3em;
}
div.user > div > div:last-child hr {
margin-bottom: 0;
}
}
</style>

View File

@ -20,421 +20,13 @@
</div>
<!-- Widget -->
<div :class="`widget-wrap ${ error ? 'has-error' : '' }`">
<AdGuardDnsInfo
v-if="widgetType === 'adguard-dns-info'"
<component
v-bind:is="component"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<AdGuardFilterStatus
v-else-if="widgetType === 'adguard-filter-status'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<AdGuardStats
v-else-if="widgetType === 'adguard-stats'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<AdGuardTopDomains
v-else-if="widgetType === 'adguard-top-domains'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<AnonAddy
v-else-if="widgetType === 'anonaddy'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Apod
v-else-if="widgetType === 'apod'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<BlacklistCheck
v-else-if="widgetType === 'blacklist-check'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Clock
v-else-if="widgetType === 'clock'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CryptoPriceChart
v-else-if="widgetType === 'crypto-price-chart'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CryptoWatchList
v-else-if="widgetType === 'crypto-watch-list'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CveVulnerabilities
v-else-if="widgetType === 'cve-vulnerabilities'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<DomainMonitor
v-else-if="widgetType === 'domain-monitor'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CodeStats
v-else-if="widgetType === 'code-stats'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CovidStats
v-else-if="widgetType === 'covid-stats'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<EmbedWidget
v-else-if="widgetType === 'embed'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<EthGasPrices
v-else-if="widgetType === 'eth-gas-prices'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<ExchangeRates
v-else-if="widgetType === 'exchange-rates'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Flights
v-else-if="widgetType === 'flight-data'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GitHubProfile
v-else-if="widgetType === 'github-profile-stats'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GitHubTrending
v-else-if="widgetType === 'github-trending-repos'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlAlerts
v-else-if="widgetType === 'gl-alerts'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlCpuCores
v-else-if="widgetType === 'gl-current-cores'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlCpuGauge
v-else-if="widgetType === 'gl-current-cpu'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlCpuHistory
v-else-if="widgetType === 'gl-cpu-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlDiskIo
v-else-if="widgetType === 'gl-disk-io'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlDiskSpace
v-else-if="widgetType === 'gl-disk-space'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlIpAddress
v-else-if="widgetType === 'gl-ip-address'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlLoadHistory
v-else-if="widgetType === 'gl-load-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlMemGauge
v-else-if="widgetType === 'gl-current-mem'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlMemHistory
v-else-if="widgetType === 'gl-mem-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlNetworkInterfaces
v-else-if="widgetType === 'gl-network-interfaces'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlNetworkTraffic
v-else-if="widgetType === 'gl-network-traffic'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlSystemLoad
v-else-if="widgetType === 'gl-system-load'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlCpuTemp
v-else-if="widgetType === 'gl-cpu-temp'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<HealthChecks
v-else-if="widgetType === 'health-checks'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<IframeWidget
v-else-if="widgetType === 'iframe'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<ImageWidget
v-else-if="widgetType === 'image'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Jokes
v-else-if="widgetType === 'joke'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<MullvadStatus
v-else-if="widgetType === 'mullvad-status'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<NdCpuHistory
v-else-if="widgetType === 'nd-cpu-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<NdLoadHistory
v-else-if="widgetType === 'nd-load-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<NdRamHistory
v-else-if="widgetType === 'nd-ram-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<NewsHeadlines
v-else-if="widgetType === 'news-headlines'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<PiHoleStats
v-else-if="widgetType === 'pi-hole-stats'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<PiHoleTopQueries
v-else-if="widgetType === 'pi-hole-top-queries'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<PiHoleTraffic
v-else-if="widgetType === 'pi-hole-traffic'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<PublicHolidays
v-else-if="widgetType === 'public-holidays'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<PublicIp
v-else-if="widgetType === 'public-ip'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<RssFeed
v-else-if="widgetType === 'rss-feed'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<SportsScores
v-else-if="widgetType === 'sports-scores'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<StatPing
v-else-if="widgetType === 'stat-ping'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<StockPriceChart
v-else-if="widgetType === 'stock-price-chart'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<SynologyDownload
v-else-if="widgetType === 'synology-download'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<SystemInfo
v-else-if="widgetType === 'system-info'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<TflStatus
v-else-if="widgetType === 'tfl-status'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<WalletBalance
v-else-if="widgetType === 'wallet-balance'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Weather
v-else-if="widgetType === 'weather'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<WeatherForecast
v-else-if="widgetType === 'weather-forecast'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<XkcdComic
v-else-if="widgetType === 'xkcd-comic'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<!-- No widget type specified -->
<div v-else>{{ handleError('Widget type was not found') }}</div>
</div>
</div>
</template>
@ -447,6 +39,74 @@ import UpdateIcon from '@/assets/interface-icons/widget-update.svg';
import OpenIcon from '@/assets/interface-icons/open-new-tab.svg';
import LoadingAnimation from '@/assets/interface-icons/loader.svg';
const COMPAT = {
'adguard-dns-info': 'AdGuardDnsInfo',
'adguard-filter-status': 'AdGuardFilterStatus',
'adguard-stats': 'AdGuardStats',
'adguard-top-domains': 'AdGuardTopDomains',
anonaddy: 'AnonAddy',
apod: 'Apod',
'blacklist-check': 'BlacklistCheck',
clock: 'Clock',
'crypto-price-chart': 'CryptoPriceChart',
'crypto-watch-list': 'CryptoWatchList',
'cve-vulnerabilities': 'CveVulnerabilities',
'domain-monitor': 'DomainMonitor',
'code-stats': 'CodeStats',
'covid-stats': 'CovidStats',
embed: 'EmbedWidget',
'eth-gas-prices': 'EthGasPrices',
'exchange-rates': 'ExchangeRates',
'flight-data': 'Flights',
'github-profile-stats': 'GitHubProfile',
'github-trending-repos': 'GitHubTrending',
'gl-alerts': 'GlAlerts',
'gl-current-cores': 'GlCpuCores',
'gl-current-cpu': 'GlCpuGauge',
'gl-cpu-history': 'GlCpuHistory',
'gl-disk-io': 'GlDiskIo',
'gl-disk-space': 'GlDiskSpace',
'gl-ip-address': 'GlIpAddress',
'gl-load-history': 'GlLoadHistory',
'gl-current-mem': 'GlMemGauge',
'gl-mem-history': 'GlMemHistory',
'gl-network-interfaces': 'GlNetworkInterfaces',
'gl-network-traffic': 'GlNetworkTraffic',
'gl-system-load': 'GlSystemLoad',
'gl-cpu-temp': 'GlCpuTemp',
'health-checks': 'HealthChecks',
iframe: 'IframeWidget',
image: 'ImageWidget',
joke: 'Jokes',
'mullvad-status': 'MullvadStatus',
'nd-cpu-history': 'NdCpuHistory',
'nd-load-history': 'NdLoadHistory',
'nd-ram-history': 'NdRamHistory',
'news-headlines': 'NewsHeadlines',
'nextcloud-notifications': 'NextcloudNotifications',
'nextcloud-php-opcache': 'NextcloudPhpOpcache',
'nextcloud-stats': 'NextcloudStats',
'nextcloud-system': 'NextcloudSystem',
'nextcloud-user': 'NextcloudUser',
'nextcloud-user-status': 'NextcloudUserStatus',
'pi-hole-stats': 'PiHoleStats',
'pi-hole-top-queries': 'PiHoleTopQueries',
'pi-hole-traffic': 'PiHoleTraffic',
'public-holidays': 'PublicHolidays',
'public-ip': 'PublicIp',
'rss-feed': 'RssFeed',
'sports-scores': 'SportsScores',
'stat-ping': 'StatPing',
'stock-price-chart': 'StockPriceChart',
'synology-download': 'SynologyDownload',
'system-info': 'SystemInfo',
'tfl-status': 'TflStatus',
'wallet-balance': 'WalletBalance',
weather: 'Weather',
'weather-forecast': 'WeatherForecast',
'xkcd-comic': 'XkcdComic',
};
export default {
name: 'Widget',
components: {
@ -455,66 +115,6 @@ export default {
UpdateIcon,
OpenIcon,
LoadingAnimation,
// Register widget components
AdGuardDnsInfo: () => import('@/components/Widgets/AdGuardDnsInfo.vue'),
AdGuardFilterStatus: () => import('@/components/Widgets/AdGuardFilterStatus.vue'),
AdGuardStats: () => import('@/components/Widgets/AdGuardStats.vue'),
AdGuardTopDomains: () => import('@/components/Widgets/AdGuardTopDomains.vue'),
AnonAddy: () => import('@/components/Widgets/AnonAddy.vue'),
Apod: () => import('@/components/Widgets/Apod.vue'),
BlacklistCheck: () => import('@/components/Widgets/BlacklistCheck.vue'),
Clock: () => import('@/components/Widgets/Clock.vue'),
CodeStats: () => import('@/components/Widgets/CodeStats.vue'),
CovidStats: () => import('@/components/Widgets/CovidStats.vue'),
CryptoPriceChart: () => import('@/components/Widgets/CryptoPriceChart.vue'),
CryptoWatchList: () => import('@/components/Widgets/CryptoWatchList.vue'),
CveVulnerabilities: () => import('@/components/Widgets/CveVulnerabilities.vue'),
DomainMonitor: () => import('@/components/Widgets/DomainMonitor.vue'),
EmbedWidget: () => import('@/components/Widgets/EmbedWidget.vue'),
EthGasPrices: () => import('@/components/Widgets/EthGasPrices.vue'),
ExchangeRates: () => import('@/components/Widgets/ExchangeRates.vue'),
Flights: () => import('@/components/Widgets/Flights.vue'),
GitHubTrending: () => import('@/components/Widgets/GitHubTrending.vue'),
GitHubProfile: () => import('@/components/Widgets/GitHubProfile.vue'),
GlAlerts: () => import('@/components/Widgets/GlAlerts.vue'),
GlCpuCores: () => import('@/components/Widgets/GlCpuCores.vue'),
GlCpuGauge: () => import('@/components/Widgets/GlCpuGauge.vue'),
GlCpuHistory: () => import('@/components/Widgets/GlCpuHistory.vue'),
GlDiskIo: () => import('@/components/Widgets/GlDiskIo.vue'),
GlDiskSpace: () => import('@/components/Widgets/GlDiskSpace.vue'),
GlIpAddress: () => import('@/components/Widgets/GlIpAddress.vue'),
GlLoadHistory: () => import('@/components/Widgets/GlLoadHistory.vue'),
GlMemGauge: () => import('@/components/Widgets/GlMemGauge.vue'),
GlMemHistory: () => import('@/components/Widgets/GlMemHistory.vue'),
GlNetworkInterfaces: () => import('@/components/Widgets/GlNetworkInterfaces.vue'),
GlNetworkTraffic: () => import('@/components/Widgets/GlNetworkTraffic.vue'),
GlSystemLoad: () => import('@/components/Widgets/GlSystemLoad.vue'),
GlCpuTemp: () => import('@/components/Widgets/GlCpuTemp.vue'),
HealthChecks: () => import('@/components/Widgets/HealthChecks.vue'),
IframeWidget: () => import('@/components/Widgets/IframeWidget.vue'),
ImageWidget: () => import('@/components/Widgets/ImageWidget.vue'),
Jokes: () => import('@/components/Widgets/Jokes.vue'),
MullvadStatus: () => import('@/components/Widgets/MullvadStatus.vue'),
NdCpuHistory: () => import('@/components/Widgets/NdCpuHistory.vue'),
NdLoadHistory: () => import('@/components/Widgets/NdLoadHistory.vue'),
NdRamHistory: () => import('@/components/Widgets/NdRamHistory.vue'),
NewsHeadlines: () => import('@/components/Widgets/NewsHeadlines.vue'),
PiHoleStats: () => import('@/components/Widgets/PiHoleStats.vue'),
PiHoleTopQueries: () => import('@/components/Widgets/PiHoleTopQueries.vue'),
PiHoleTraffic: () => import('@/components/Widgets/PiHoleTraffic.vue'),
PublicHolidays: () => import('@/components/Widgets/PublicHolidays.vue'),
PublicIp: () => import('@/components/Widgets/PublicIp.vue'),
RssFeed: () => import('@/components/Widgets/RssFeed.vue'),
SportsScores: () => import('@/components/Widgets/SportsScores.vue'),
StatPing: () => import('@/components/Widgets/StatPing.vue'),
StockPriceChart: () => import('@/components/Widgets/StockPriceChart.vue'),
SynologyDownload: () => import('@/components/Widgets/SynologyDownload.vue'),
SystemInfo: () => import('@/components/Widgets/SystemInfo.vue'),
TflStatus: () => import('@/components/Widgets/TflStatus.vue'),
WalletBalance: () => import('@/components/Widgets/WalletBalance.vue'),
Weather: () => import('@/components/Widgets/Weather.vue'),
WeatherForecast: () => import('@/components/Widgets/WeatherForecast.vue'),
XkcdComic: () => import('@/components/Widgets/XkcdComic.vue'),
},
props: {
widget: Object,
@ -556,6 +156,15 @@ export default {
hideControls() {
return this.widget.hideControls;
},
component() {
const type = COMPAT[this.widgetType] || this.widget.type;
if (!type) {
ErrorHandler('Widget type was not found');
return null;
}
// eslint-disable-next-line prefer-template
return () => import('@/components/Widgets/' + type + '.vue').catch(() => import('@/components/Widgets/Blank.vue'));
},
},
methods: {
/* Calls update data method on widget */

View File

@ -22,6 +22,7 @@ export default {
return {
statusResponse: undefined,
contextMenuOpen: false,
intervalId: undefined, // status-check setInterval() id
contextPos: {
posX: undefined,
posY: undefined,

View File

@ -0,0 +1,208 @@
import { serviceEndpoints } from '@/utils/defaults';
import {
convertBytes, formatNumber, getTimeAgo, timestampToDateTime,
} from '@/utils/MiscHelpers';
/**
* Reusable mixin for Nextcloud widgets
* Nextcloud APIs
* - capabilities: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html#capabilities-api
* - userstatus: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-status-api.html#user-status-retrieve-statuses
* - user: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html#user-metadata
* - notifications: https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
* - serverinfo: https://github.com/nextcloud/serverinfo
*/
export default {
data() {
return {
validCredentials: null,
capabilities: {
notifications: {
enabled: null,
features: [],
},
userStatus: null,
},
capabilitiesLastUpdated: 0,
branding: {
name: null,
logo: null,
url: null,
slogan: null,
},
version: {
string: null,
edition: null,
},
};
},
computed: {
/* The user provided Nextcloud hostname */
hostname() {
if (!this.options.hostname) this.error('A hostname is required');
return this.options.hostname;
},
/* The user provided Nextcloud username */
username() {
if (!this.options.username) this.error('A username is required');
return this.options.username;
},
/* The user provided Nextcloud password */
password() {
if (!this.options.password) this.error('An app-password is required');
// reject Nextcloud user passord (enforce 'app-password')
if (!/^([a-z0-9]{5}-){4}[a-z0-9]{5}$/i.test(this.options.password)) {
this.error('Please use a Nextcloud app-password, not your login password.');
return '';
}
return this.options.password;
},
/* HTTP headers for Nextcloud API requests */
headers() {
const authBase = `${this.username}:${this.password}`;
return {
'OCS-APIREQUEST': true,
Accept: 'application/json',
Authorization: `Basic ${window.btoa(authBase)}`,
};
},
/* TTL for data delivered by the capabilities endpoint, ms */
capabilitiesTtl() {
return (parseInt(this.options.capabilitiesTtl, 10) || 3600) * 1000;
},
proxyReqEndpoint() {
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
return `${baseUrl}${serviceEndpoints.corsProxy}`;
},
},
methods: {
/* Nextcloud API endpoints */
endpoint(id) {
switch (id) {
case 'user':
return `${this.hostname}/ocs/v1.php/cloud/users/${this.username}`;
case 'userstatus':
return `${this.hostname}/ocs/v2.php/apps/user_status/api/v1/statuses`;
case 'serverinfo':
return `${this.hostname}/ocs/v2.php/apps/serverinfo/api/v1/info`;
case 'notifications':
return `${this.hostname}/ocs/v2.php/apps/notifications/api/v2/notifications`;
case 'capabilities':
default:
return `${this.hostname}/ocs/v1.php/cloud/capabilities`;
}
},
/* Helper for widgets to terminate {fetchData} early */
hasValidCredentials() {
return this.validCredentials !== false
&& this.username.length > 0
&& this.password.length > 0;
},
/* Primary handler for every Nextcloud API response */
validateResponse(response) {
const data = response?.ocs?.data;
let meta = response?.ocs?.meta;
const error = response?.error; // Dashy error when cors-proxied
if (error && error.status) {
meta = { statuscode: error.status };
}
if (!meta || !meta.statuscode || !data) {
this.error('Invalid response');
}
switch (meta.statuscode) {
case 401:
this.validCredentials = false;
this.error(
`Access denied for user ${this.username}.`
+ ' Note that some Nextcloud widgets only work with an admin user.',
);
break;
case 429:
this.validCredentials = false;
this.error(
'The server indicated \'rate-limit reached\' error (HTTP 429).'
+ ' The server-info API may return this error for incorrect user/password.',
);
break;
case 993:
case 997:
case 998:
this.validCredentials = false;
this.error(
'The provided app-password is not permitted to access the requested resource or it has'
+ ' been revoked, or the username/password combination is incorrect',
);
break;
default:
this.validCredentials = true;
if (!this.allowedStatuscodes().includes(meta.statuscode)) {
this.error('Unexpected response');
}
break;
}
return data;
},
/* Process the capabilities endpoint if {capabilitiesTtl} has expired */
loadCapabilities() {
if ((new Date().getTime()) - this.capabilitiesLastUpdated > this.capabilitiesTtl) {
return this.makeRequest(this.endpoint('capabilities'), this.headers)
.then(this.processCapabilities);
}
return Promise.resolve();
},
/* Update the sate based on the capabilites response */
processCapabilities(capResponse) {
const ocdata = this.validateResponse(capResponse);
const capNotif = ocdata.capabilities?.notifications?.['ocs-endpoints'];
this.branding = ocdata.capabilities?.theming;
this.capabilities.notifications.enabled = !!(capNotif?.length);
this.capabilities.notifications.features = capNotif || [];
this.capabilities.userStatus = !!(ocdata.capabilities?.user_status?.enabled);
this.version.string = ocdata.version?.string;
this.version.edition = ocdata.version?.edition;
this.capabilitiesLastUpdated = new Date().getTime();
},
/* Shared template helpers */
getTimeAgo(time) {
return getTimeAgo(time);
},
formatDateTime(time) {
return timestampToDateTime(time);
},
/* Add additional formatting to {MiscHelpers.convertBytes()} */
convertBytes(bytes, decimals = 2, formatHtml = true) {
const formatted = convertBytes(bytes, decimals).toString();
if (!formatHtml) return formatted;
const m = formatted.match(/(-?\d+)((\.\d+)?\s(([KMGTPEZY]B|Bytes)))/);
return `${m[1]}<span class="decimals">${m[2]}</span>`;
},
/* Add additional formatting to {MiscHelpers.formatNumber()} */
formatNumber(number, decimals = 1, formatHtml = true) {
const formatted = formatNumber(number, decimals).toString();
if (!formatHtml) return formatted;
const m = formatted.match(/(\d+)((\.\d+)?([KMBT]?))/);
return `${m[1]}<span class="decimals">${m[2]}</span>`;
},
/* Format a number as percentage value */
formatPercent(number, decimals = 2) {
const n = parseFloat(number).toFixed(decimals).split('.');
const d = n.length > 1 ? `.${n[1]}` : '';
return `${n[0]}<span class="decimals">${d}%</span>`;
},
/* Similar to {MiscHelpers.getValueFromCss()} but uses the widget root node to get
* the computed style so widget color is respected in variable widget color themes. */
getValueFromCss(colorVar) {
const cssProps = getComputedStyle(this.$el || document.documentElement);
return cssProps.getPropertyValue(`--${colorVar}`).trim();
},
/* Get {colorVar} CSS property value and return as rgba() */
getColorRgba(colorVar, alpha = 1) {
const [r, g, b] = this.getValueFromCss(colorVar).match(/\w\w/g).map(x => parseInt(x, 16));
return `rgba(${r},${g},${b},${alpha})`;
},
/* Translation shorthand with key prefix */
tt(key, options = null) {
return this.$t(`widgets.nextcloud.${key}`, options);
},
},
};

View File

@ -10,6 +10,7 @@ import { applyItemId } from '@/utils/SectionHelpers';
import filterUserSections from '@/utils/CheckSectionVisibility';
import ErrorHandler, { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
import { isUserAdmin } from '@/utils/Auth';
import { localStorageKeys } from './utils/defaults';
Vue.use(Vuex);
@ -295,6 +296,11 @@ const store = new Vuex.Store({
state.navigateConfToTab = index;
},
[SET_CURRENT_SUB_PAGE](state, subPageObject) {
if (!subPageObject) {
// Set theme back to primary when navigating to index page
const defaulTheme = localStorage.getItem(localStorageKeys.PRIMARY_THEME);
if (defaulTheme) state.config.appConfig.theme = defaulTheme;
}
state.currentConfigInfo = subPageObject;
},
[USE_MAIN_CONFIG](state) {
@ -312,13 +318,22 @@ const store = new Vuex.Store({
commit(SET_REMOTE_CONFIG, yaml.load((await axios.get('/conf.yml')).data));
const deepCopy = (json) => JSON.parse(JSON.stringify(json));
const config = deepCopy(new ConfigAccumulator().config());
if (config.appConfig?.theme) {
// Save theme defined in conf.yml as primary
localStorage.setItem(localStorageKeys.PRIMARY_THEME, config.appConfig.theme);
// This will set theme back to primary in case we were on a themed page
// and the index page is loaded w/o navigation (e.g. modifying browser location)
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
}
commit(SET_CONFIG, config);
},
/* Fetch config for a sub-page (sections and pageInfo only) */
async [INITIALIZE_MULTI_PAGE_CONFIG]({ commit, state }, configPath) {
axios.get(configPath).then((response) => {
const subConfig = yaml.load(response.data);
const pageTheme = subConfig.appConfig?.theme;
subConfig.appConfig = state.config.appConfig; // Always use parent appConfig
if (pageTheme) subConfig.appConfig.theme = pageTheme; // Apply page theme override
commit(SET_CONFIG, subConfig);
}).catch((err) => {
ErrorHandler(`Unable to load config from '${configPath}'`, err);

View File

@ -1542,8 +1542,6 @@ html[data-theme="oblivion-scotch"] {
--primary: #d69e3a;
}
@import url('https://fonts.googleapis.com/css2?family=Shrikhand&display=swap');
html[data-theme='lissy'] {
// --primary: #f0f;
--primary: #ffffffcc;

View File

@ -0,0 +1,64 @@
.nextcloud-widget {
p {
color: var(--widget-text-color);
margin: .5em 0;
}
a {
color: var(--widget-text-color);
}
p i {
font-size: 1.1em;
min-width: 22px;
text-align: center;
}
p em {
font-size: 1.1em;
margin: 0 .24em;
font-weight: 800;
}
strong {
font-weight: 800;
font-size: 1.05em;
margin-left: .25em;
}
small {
opacity: .66;
}
hr {
color: var(--widget-text-color);
border: none;
border-top: 1px solid;
margin-top: .8em;
margin-bottom: .8em;
opacity: .25;
clear: both;
}
hr:last-child {
margin-bottom: 0;
}
div.sep {
border-top: 1px dashed var(--widget-text-color);
width: 100%;
padding: .4em 0 0 0;
margin: .85em 0 0 0;
> div:not(:first-child) {
width: 100%;
position: relative;
}
}
::v-deep span.decimals {
font-size: 85%;
}
::v-deep div.percentage-chart {
margin: 0;
}
}

View File

@ -106,6 +106,18 @@ export const convertBytes = (bytes, decimals = 2) => {
return `${parseFloat((bytes / (k ** i)).toFixed(decimals))} ${sizes[i]}`;
};
/* Round a number to thousands, millions, billions or trillions and suffix
* with K, M, B or T respectively, e.g. 4_294_967_295 => 4.3B */
export const formatNumber = (number, decimals = 1) => {
if (number > -1000 && number < 1000) return number;
const units = ['', 'K', 'M', 'B', 'T'];
const k = 1000;
const i = Math.floor(Math.log(number) / Math.log(k));
const f = parseFloat(number / (k ** i));
const d = f.toFixed(decimals) % 1.0 === 0 ? 0 : decimals; // number of decimals, omit .0
return `${f.toFixed(d)}${units[i]}`;
};
/* Round price to appropriate number of decimals */
export const roundPrice = (price) => {
if (Number.isNaN(price)) return price;

View File

@ -120,6 +120,7 @@ module.exports = {
COLLAPSE_STATE: 'collapseState',
ICON_SIZE: 'iconSize',
THEME: 'theme',
PRIMARY_THEME: 'primaryTheme',
CUSTOM_COLORS: 'customColors',
CONF_SECTIONS: 'confSections',
CONF_WIDGETS: 'confSections',
@ -217,7 +218,7 @@ module.exports = {
/* API endpoints for widgets that need to fetch external data */
widgetApiEndpoints: {
anonAddy: 'https://app.anonaddy.com',
astronomyPictureOfTheDay: 'https://apodapi.herokuapp.com/api',
astronomyPictureOfTheDay: 'https://go-apod.herokuapp.com/apod',
blacklistCheck: 'https://api.blacklistchecker.com/check',
codeStats: 'https://codestats.net/',
covidStats: 'https://disease.sh/v3/covid-19',
@ -242,7 +243,7 @@ module.exports = {
rssToJson: 'https://api.rss2json.com/v1/api.json',
sportsScores: 'https://www.thesportsdb.com/api/v1/json',
stockPriceChart: 'https://www.alphavantage.co/query',
tflStatus: 'https://api.tfl.gov.uk/line/mode/tube/status',
tflStatus: 'https://api.tfl.gov.uk/line/mode/dlr,elizabeth-line,overground,tram,tube/status',
walletBalance: 'https://api.blockcypher.com/v1',
walletQrCode: 'https://www.bitcoinqrcodemaker.com/api',
weather: 'https://api.openweathermap.org/data/2.5/weather',