diff --git a/.github/AUTHORS.txt b/.github/AUTHORS.txt index 3b76f6f1..e99650f7 100644 --- a/.github/AUTHORS.txt +++ b/.github/AUTHORS.txt @@ -13,6 +13,7 @@ Jeremy - 1 commits Kieren - 1 commits Leonardo - 1 commits M - 1 commits +Markus - 1 commits PlusaN <61884717+PlusaN@users.noreply.github.com> - 1 commits Rune - 1 commits Ryan - 1 commits @@ -24,7 +25,9 @@ deepsource-io[bot] - 1 commits dr460nf1r3 - 1 commits icy-comet <50461557+icy-comet@users.noreply.github.com> - 1 commits jnach <33467747+jnach@users.noreply.github.com> - 1 commits +pablomalo - 1 commits tazboyz16 - 1 commits +zcq100 - 1 commits Alejandro - 2 commits Alessandro - 2 commits BOZG - 2 commits @@ -32,10 +35,13 @@ Brendan <'Lear> - 2 commits CHAIYEON - 2 commits Dan - 2 commits Ruben - 2 commits +k073l <21180271+k073l@users.noreply.github.com> - 2 commits liss-bot <87835202+liss-bot@users.noreply.github.com> - 2 commits +patrickheeney - 2 commits ᗪєνιη <υн> - 2 commits Walkx <71191962+walkxcode@users.noreply.github.com> - 3 commits aterox - 3 commits +bogyeong - 3 commits stanly0726 <37040069+stanly0726@users.noreply.github.com> - 3 commits Niklas - 4 commits Rémy - 4 commits @@ -53,13 +59,14 @@ github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> - 16 snyk-bot - 18 commits aterox - 19 commits EVOTk <45015615+EVOTk@users.noreply.github.com> - 22 commits +Marcell <ülö> - 23 commits Alicia - 28 commits -repo-visualizer - 39 commits -snyk-bot - 44 commits -Alicia - 78 commits +repo-visualizer - 43 commits +snyk-bot - 50 commits Lissy93 - 78 commits -liss-bot - 89 commits -Alicia - 123 commits -Lissy93 - 207 commits -Alicia - 439 commits +Alicia - 84 commits +liss-bot - 95 commits +Alicia - 148 commits +Lissy93 - 208 commits +Alicia - 440 commits Alicia - 1488 commits \ No newline at end of file diff --git a/.github/workflows/issue-spam-control.yml b/.github/workflows/issue-spam-control.yml deleted file mode 100644 index 13d6357b..00000000 --- a/.github/workflows/issue-spam-control.yml +++ /dev/null @@ -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 🤖 diff --git a/.github/workflows/new-issues-check.yml b/.github/workflows/new-issues-check.yml new file mode 100644 index 00000000..e90314bf --- /dev/null +++ b/.github/workflows/new-issues-check.yml @@ -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 ⭐
_🤖 I'm a bot, and this message was automated_" diff --git a/Procfile b/Procfile index 4d76c4cd..7ddabf6b 100644 --- a/Procfile +++ b/Procfile @@ -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 \ No newline at end of file +web: npm run build-and-start diff --git a/README.md b/README.md index 5d001e0f..965cb916 100644 --- a/README.md +++ b/README.md @@ -535,6 +535,13 @@ Huge thanks to the sponsors helping to support Dashy's development! Eddy Lazzarin + + + UlisesGascon +
+ Ulises Gascón +
+ BOZG @@ -542,13 +549,21 @@ Huge thanks to the sponsors helping to support Dashy's development! Stephen Rigney + + + bmcgonag +
+ Brian McGonagill +
+ Robert-Ernst
Robert Ernst
- + + vlad-timofeev @@ -562,8 +577,14 @@ Huge thanks to the sponsors helping to support Dashy's development!
Kit L.
- - + + + + mDafox +
+ Manu Devos +
+ Byolock @@ -584,7 +605,8 @@ Huge thanks to the sponsors helping to support Dashy's development!
Hugalafutro
- + + shadowking001 @@ -605,8 +627,7 @@ Huge thanks to the sponsors helping to support Dashy's development!
Robin Candau
- - + ced4568 @@ -627,7 +648,8 @@ Huge thanks to the sponsors helping to support Dashy's development!
Undefined
- + + jtfinley72 diff --git a/app.json b/app.json index 6d674ad9..ced11578 100644 --- a/app.json +++ b/app.json @@ -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" -} \ No newline at end of file +} diff --git a/docs/assets/CONTRIBUTORS.svg b/docs/assets/CONTRIBUTORS.svg index 1a583d00..47481da1 100644 --- a/docs/assets/CONTRIBUTORS.svg +++ b/docs/assets/CONTRIBUTORS.svg @@ -1,120 +1,141 @@ - + - + + + + - + - + - + - + - + - + - + - + + + + - + - + - + - + - + - + - + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + + + + - + - + - + - + - + - + - + - + - + + + + - + + + + \ No newline at end of file diff --git a/docs/assets/repo-visualization.svg b/docs/assets/repo-visualization.svg index a8610168..1ca5a198 100644 --- a/docs/assets/repo-visualization.svg +++ b/docs/assets/repo-visualization.svg @@ -1 +1 @@ -viewsviewsutilsutilsstylesstylesmixinsmixinsdirectivesdirectivescomponentscomponentsassetsassetsWorkspaceWorkspaceWidgetsWidgetsSettingsSettingsPageStrcturePageStrctureMinimalViewMinimalViewLinkItemsLinkItemsInteractiveEditorInteractiveEditorFormElementsFormElementsConfigurationConfigurationChartsChartslocaleslocalesinterface-iconsinterface-iconsLogin.vueLogin.vueLogin.vueemojis.jsonemojis.jsonemojis.jsonConfigSch...ConfigSch...ConfigSch...defaults.jsdefaults.jsdefaults.jscolor-the...color-the...color-the...store.jsstore.jsstore.jsWidgetBas...WidgetBas...WidgetBas...AnonAddy.vueAnonAddy.vueAnonAddy.vueCustomTh...CustomTh...CustomTh...Section.vueSection.vueSection.vueItemIcon...ItemIcon...ItemIcon...Item.vueItem.vueItem.vueEditItem...EditItem...EditItem...ConfigCon...ConfigCon...ConfigCon...Gauge.vueGauge.vueGauge.vuebg.jsonbg.jsonbg.jsonhi.jsonhi.jsonhi.jsonpt.jsonpt.jsonpt.jsonit.jsonit.jsonit.jsonfr.jsonfr.jsonfr.jsonen.jsonen.jsonen.jsonsv.jsonsv.jsonsv.jsonru.jsonru.jsonru.jsonsl.jsonsl.jsonsl.json.js.json.scss.svg.vueeach dot sized by file size \ No newline at end of file +viewsviewsutilsutilsstylesstylesmixinsmixinsdirectivesdirectivescomponentscomponentsassetsassetsWorkspaceWorkspaceWidgetsWidgetsSettingsSettingsPageStrcturePageStrctureMinimalViewMinimalViewLinkItemsLinkItemsInteractiveEditorInteractiveEditorFormElementsFormElementsConfigurationConfigurationChartsChartslocaleslocalesinterface-iconsinterface-iconsemojis.jsonemojis.jsonemojis.jsonConfigSch...ConfigSch...ConfigSch...color-the...color-the...color-the...store.jsstore.jsstore.jsAnonAddy...AnonAddy...AnonAddy...CustomTh...CustomTh...CustomTh...Section.vueSection.vueSection.vueItemIcon...ItemIcon...ItemIcon...Item.vueItem.vueItem.vueEditItem...EditItem...EditItem...ConfigCo...ConfigCo...ConfigCo...en.jsonen.jsonen.jsonbg.jsonbg.jsonbg.jsonhi.jsonhi.jsonhi.jsonko.jsonko.jsonko.jsonpt.jsonpt.jsonpt.jsonit.jsonit.jsonit.jsonfr.jsonfr.jsonfr.jsonsv.jsonsv.jsonsv.jsonzh-CN.jsonzh-CN.jsonzh-CN.jsonru.jsonru.jsonru.jsonsl.jsonsl.jsonsl.json.js.json.scss.svg.vueeach dot sized by file size \ No newline at end of file diff --git a/docs/credits.md b/docs/credits.md index 1aef17e6..12871226 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -18,6 +18,13 @@ Eddy Lazzarin + + + UlisesGascon +
+ Ulises Gascón +
+ BOZG @@ -25,13 +32,21 @@ Stephen Rigney + + + bmcgonag +
+ Brian McGonagill +
+ Robert-Ernst
Robert Ernst
- + + vlad-timofeev @@ -45,8 +60,14 @@
Kit L.
- - + + + + mDafox +
+ Manu Devos +
+ Byolock @@ -67,7 +88,8 @@
Hugalafutro
- + + shadowking001 @@ -88,8 +110,7 @@
Robin Candau
- - + ced4568 @@ -110,7 +131,8 @@
Undefined
- + + jtfinley72 @@ -203,14 +225,21 @@ Remygrandin + + + boggy-cs +
+ Bogyeong Kim +
+ + stanly0726
Stanly0726
- - + onedr0p @@ -245,15 +274,15 @@
Dan Gilbert
- + + rubenandre
Rúben Silva
- - + Singebob @@ -288,15 +317,15 @@
DeepSource Bot
- + + emiran-orange
Emiran-orange
- - + FormatToday @@ -324,13 +353,6 @@
Jemy SCHNEPP
- - - - KierenConnell -
- Kieren Connell -
diff --git a/docs/readme.md b/docs/readme.md index fd470e36..b67b92c2 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -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 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index c23fc908..d464a919 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,8 +1,9 @@ # Troubleshooting -> _**This document contains common problems and their solutions.**_ +> _**This document contains common problems and their solutions.**_
+> 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 F12), 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. diff --git a/docs/widgets.md b/docs/widgets.md index 8a6cb456..11557f77 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -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). + +

nextcloud-user

+ +##### 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. + +

nextcloud-userstatus

+ +##### 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. + +

nextcloud-notifications

+ +##### 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. + +

nextcloud-system

+ +##### 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. + +

nextcloud-stats

+ +##### 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. + +

nextcloud-phpopcache

+ +##### 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/). diff --git a/services/ssl-server.js b/services/ssl-server.js index 13695bd6..c63b7db3 100644 --- a/services/ssl-server.js +++ b/services/ssl-server.js @@ -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) => { diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index a064eb67..efdaf631 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -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" } } } diff --git a/src/components/LinkItems/Item.vue b/src/components/LinkItems/Item.vue index 75cf3563..24df237e 100644 --- a/src/components/LinkItems/Item.vue +++ b/src/components/LinkItems/Item.vue @@ -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); + }, }; diff --git a/src/components/Widgets/Apod.vue b/src/components/Widgets/Apod.vue index 2d53c39e..cbf68189 100644 --- a/src/components/Widgets/Apod.vue +++ b/src/components/Widgets/Apod.vue @@ -1,15 +1,15 @@ @@ -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; diff --git a/src/components/Widgets/NextcloudNotifications.vue b/src/components/Widgets/NextcloudNotifications.vue new file mode 100644 index 00000000..dd1d4f34 --- /dev/null +++ b/src/components/Widgets/NextcloudNotifications.vue @@ -0,0 +1,208 @@ + + + + diff --git a/src/components/Widgets/NextcloudPhpOpcache.vue b/src/components/Widgets/NextcloudPhpOpcache.vue new file mode 100644 index 00000000..f361d7b0 --- /dev/null +++ b/src/components/Widgets/NextcloudPhpOpcache.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/src/components/Widgets/NextcloudStats.vue b/src/components/Widgets/NextcloudStats.vue new file mode 100644 index 00000000..8860434f --- /dev/null +++ b/src/components/Widgets/NextcloudStats.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/src/components/Widgets/NextcloudSystem.vue b/src/components/Widgets/NextcloudSystem.vue new file mode 100644 index 00000000..c07ded6e --- /dev/null +++ b/src/components/Widgets/NextcloudSystem.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/src/components/Widgets/NextcloudUser.vue b/src/components/Widgets/NextcloudUser.vue new file mode 100644 index 00000000..1d84f825 --- /dev/null +++ b/src/components/Widgets/NextcloudUser.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/src/components/Widgets/NextcloudUserStatus.vue b/src/components/Widgets/NextcloudUserStatus.vue new file mode 100644 index 00000000..37f8ae7c --- /dev/null +++ b/src/components/Widgets/NextcloudUserStatus.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/src/components/Widgets/WidgetBase.vue b/src/components/Widgets/WidgetBase.vue index 3353029f..bb052bff 100644 --- a/src/components/Widgets/WidgetBase.vue +++ b/src/components/Widgets/WidgetBase.vue @@ -20,421 +20,13 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ handleError('Widget type was not found') }}
@@ -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 */ diff --git a/src/mixins/ItemMixin.js b/src/mixins/ItemMixin.js index 47148c4c..83113752 100644 --- a/src/mixins/ItemMixin.js +++ b/src/mixins/ItemMixin.js @@ -22,6 +22,7 @@ export default { return { statusResponse: undefined, contextMenuOpen: false, + intervalId: undefined, // status-check setInterval() id contextPos: { posX: undefined, posY: undefined, diff --git a/src/mixins/NextcloudMixin.js b/src/mixins/NextcloudMixin.js new file mode 100644 index 00000000..bdbe753e --- /dev/null +++ b/src/mixins/NextcloudMixin.js @@ -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]}${m[2]}`; + }, + /* 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]}${m[2]}`; + }, + /* 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]}${d}%`; + }, + /* 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); + }, + }, +}; diff --git a/src/store.js b/src/store.js index 4eea3763..a8d92f3a 100644 --- a/src/store.js +++ b/src/store.js @@ -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); diff --git a/src/styles/color-themes.scss b/src/styles/color-themes.scss index 5cd1f420..1893a01f 100644 --- a/src/styles/color-themes.scss +++ b/src/styles/color-themes.scss @@ -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; diff --git a/src/styles/widgets/nextcloud-shared.scss b/src/styles/widgets/nextcloud-shared.scss new file mode 100644 index 00000000..77360681 --- /dev/null +++ b/src/styles/widgets/nextcloud-shared.scss @@ -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; + } +} diff --git a/src/utils/MiscHelpers.js b/src/utils/MiscHelpers.js index c3f59a8b..11e834bc 100644 --- a/src/utils/MiscHelpers.js +++ b/src/utils/MiscHelpers.js @@ -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; diff --git a/src/utils/defaults.js b/src/utils/defaults.js index e1b41128..273dde06 100644 --- a/src/utils/defaults.js +++ b/src/utils/defaults.js @@ -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',