mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-05 21:47:48 +01:00
Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffbb1f5f0b | ||
|
|
ad53119088 | ||
|
|
fe1c525fb7 | ||
|
|
323375e8e4 | ||
|
|
8af27ea86d | ||
|
|
9335be0049 | ||
|
|
8e0f265080 | ||
|
|
ae357159ef | ||
|
|
387d40910f | ||
|
|
5344485a4f | ||
|
|
21f2f2a215 | ||
|
|
9586fac665 | ||
|
|
4aedda7ba2 | ||
|
|
f79213c9d3 | ||
|
|
eeddcb26a0 | ||
|
|
725922db78 | ||
|
|
1846dcaba9 | ||
|
|
ab96d88ebe | ||
|
|
a3bf28915d | ||
|
|
47920a5f7a | ||
|
|
1c10903823 | ||
|
|
96e4133517 | ||
|
|
8b5167a911 | ||
|
|
45edab5d88 | ||
|
|
b4375fb6fc | ||
|
|
bd2b28a7ac | ||
|
|
53149df5f1 | ||
|
|
bc2025b3ba | ||
|
|
236450f6f1 | ||
|
|
fb9e03b31d | ||
|
|
31ccb9c933 | ||
|
|
6e01a743df | ||
|
|
ed65c89516 | ||
|
|
b8b8dad9fb | ||
|
|
690d17e132 | ||
|
|
a1e9912b36 | ||
|
|
26914bfb09 | ||
|
|
079fdb3011 | ||
|
|
102cdbd53a | ||
|
|
d861264ecf | ||
|
|
e3237b9022 | ||
|
|
3882dd4f5a | ||
|
|
d66326b41d | ||
|
|
ef1c5dbcc9 | ||
|
|
c45c8e93de | ||
|
|
1e93bf3ec4 | ||
|
|
4bff209bd7 | ||
|
|
e5ee937c38 | ||
|
|
c418efe007 | ||
|
|
5677254b46 | ||
|
|
cb76a8165d | ||
|
|
a7a1eca0cd | ||
|
|
85bc078c46 | ||
|
|
5c347d9427 | ||
|
|
6c17efc2ab | ||
|
|
a6e929ba86 | ||
|
|
f9886f7c63 | ||
|
|
ec937f6212 | ||
|
|
b7427c3409 | ||
|
|
5bd9cf46ea | ||
|
|
c340c42ef3 | ||
|
|
27c5b4227d | ||
|
|
914e869778 | ||
|
|
e4ea30becc | ||
|
|
61f91f0e45 | ||
|
|
c6d8668e69 | ||
|
|
3c73b000df | ||
|
|
ed5a5ae86f | ||
|
|
036fbb0f49 | ||
|
|
13779c5618 | ||
|
|
6802fd0c1d | ||
|
|
7b523501ad | ||
|
|
0c8bbdf02b | ||
|
|
0b43f83daa | ||
|
|
0f2f552e87 | ||
|
|
b3bedc7c31 | ||
|
|
32a2a3f484 | ||
|
|
27de7d1c84 | ||
|
|
c31c2a4c84 | ||
|
|
9fb88eb325 | ||
|
|
7cdc2fa89a | ||
|
|
c06dbddcea | ||
|
|
a4b17d9a8f | ||
|
|
7ffea76b9e | ||
|
|
6a6db91dc9 | ||
|
|
9078fd2302 | ||
|
|
39786c5dd4 | ||
|
|
7c35a88483 | ||
|
|
1b885cb189 | ||
|
|
183bbbe6e7 | ||
|
|
562318f2d3 | ||
|
|
797401a7de | ||
|
|
d4dbfebe72 | ||
|
|
575d233078 | ||
|
|
04a78c07a7 | ||
|
|
59856bc753 | ||
|
|
a8e7a5f912 | ||
|
|
c08d4b7b9c | ||
|
|
d25148c8ae | ||
|
|
b0e640fd03 | ||
|
|
7ae07cb1ee | ||
|
|
c0b34faa79 | ||
|
|
81e3b0bd1e | ||
|
|
f74e8b9d32 | ||
|
|
7f041e8303 | ||
|
|
db05a66f3a | ||
|
|
efc6e86991 | ||
|
|
1af867d4b3 | ||
|
|
43da60595c | ||
|
|
c2849744b8 | ||
|
|
9bef2a8fcb | ||
|
|
6ec73362f2 | ||
|
|
f5bef651d8 | ||
|
|
aa77a274c3 | ||
|
|
4e90bd8e46 | ||
|
|
6a238948c2 | ||
|
|
8e2bebcfd9 | ||
|
|
b023801fe4 | ||
|
|
516b812b2b | ||
|
|
8373057758 | ||
|
|
939f5d7c20 | ||
|
|
36d1a9c738 | ||
|
|
fb845c3e03 | ||
|
|
78c52861c7 | ||
|
|
cf39395924 | ||
|
|
6061d9ec65 | ||
|
|
5a8defb478 | ||
|
|
a265038bf6 | ||
|
|
08afa0b747 | ||
|
|
bad436b858 | ||
|
|
7f0345a56a | ||
|
|
97bf174b78 | ||
|
|
975f79f6cc | ||
|
|
e72efe7fd0 | ||
|
|
d94b3e829d | ||
|
|
10c63939dc | ||
|
|
972ede9249 | ||
|
|
8f001ad88a | ||
|
|
229c5dac59 | ||
|
|
0622395ec7 | ||
|
|
66073ed460 | ||
|
|
533f40b536 | ||
|
|
13afe82fa5 | ||
|
|
10c27dfd84 | ||
|
|
057d5eca8f | ||
|
|
e89f3668a9 | ||
|
|
76d534583b | ||
|
|
7b4f360a5e | ||
|
|
992b18c9de | ||
|
|
6291a5422a | ||
|
|
4581c4eeb0 |
@@ -1,3 +1,19 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
"extends": ["airbnb", "next/core-web-vitals", "prettier"],
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"newlines-between": "always"
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"paths": ["src"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
.github/workflows/docker-publish.yml
vendored
16
.github/workflows/docker-publish.yml
vendored
@@ -54,13 +54,15 @@ jobs:
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
# This step is being disabled because the runner is on a self-hosted machine
|
||||
# where the cache will stick between runs.
|
||||
# - name: Cache Docker layers
|
||||
# uses: actions/cache@v3
|
||||
# with:
|
||||
# path: /tmp/.buildx-cache
|
||||
# key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-buildx-
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
|
||||
78
Dockerfile
78
Dockerfile
@@ -1,4 +1,39 @@
|
||||
FROM node:16-alpine AS base
|
||||
# syntax = docker/dockerfile:latest
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM node:current-alpine AS deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --link package.json pnpm-lock.yaml* ./
|
||||
|
||||
RUN <<EOF
|
||||
set -xe
|
||||
apk add libc6-compat
|
||||
apk add --virtual .gyp python3 make g++
|
||||
yarn global add pnpm
|
||||
EOF
|
||||
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm fetch | grep -v "cross-device link not permitted\|Falling back to copying packages from store"
|
||||
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm install -r --offline
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:current-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY --link --from=deps /app/node_modules ./node_modules/
|
||||
COPY . .
|
||||
|
||||
RUN <<EOF
|
||||
set -xe
|
||||
yarn next telemetry disable
|
||||
mkdir config && echo '-' > config/settings.yaml
|
||||
npm run build
|
||||
EOF
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:current-alpine AS runner
|
||||
LABEL org.opencontainers.image.title "Homepage"
|
||||
LABEL org.opencontainers.image.description "A self-hosted services landing page, with docker and service integrations."
|
||||
LABEL org.opencontainers.image.url="https://github.com/benphelps/homepage"
|
||||
@@ -6,33 +41,22 @@ LABEL org.opencontainers.image.documentation='https://github.com/benphelps/homep
|
||||
LABEL org.opencontainers.image.source='https://github.com/benphelps/homepage'
|
||||
LABEL org.opencontainers.image.licenses='Apache-2.0'
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM node:16-alpine AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk add --no-cache --virtual .gyp python3 make g++
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN yarn global add pnpm
|
||||
RUN pnpm install
|
||||
RUN apk del .gyp
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:16-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:16-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV production
|
||||
COPY --from=builder /app/next.config.js ./
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
EXPOSE 3000
|
||||
WORKDIR /app
|
||||
|
||||
# Copy files from context (this allows the files to copy before the builder stage is done).
|
||||
COPY --link package.json next.config.js ./
|
||||
COPY --link /public ./public
|
||||
|
||||
# Copy files from builder
|
||||
COPY --link --from=builder /app/.next/standalone ./
|
||||
COPY --link --from=builder /app/.next/static/ ./.next/static/
|
||||
|
||||
ENV PORT 3000
|
||||
EXPOSE $PORT
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=20s \
|
||||
CMD wget --no-verbose --tries=1 --spider --no-check-certificate http://localhost:$PORT/api/healthcheck || exit 1
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
31
README.md
31
README.md
@@ -1,21 +1,32 @@
|
||||

|
||||
|
||||
[](https://github.com/benphelps/homepage/actions/workflows/docker-publish.yml)
|
||||
[](https://hosted.weblate.org/engage/homepage/)
|
||||
|
||||
## Features
|
||||
|
||||
* Web Bookmarks
|
||||
* Service Bookmarks
|
||||
- Docker Integration
|
||||
- Status light + CPU, Memory & Network Reporting *(click on the status light)*
|
||||
- Service Integration
|
||||
- Currently supports Sonarr, Radarr, Ombi, Emby, Jellyfin, Jellyseerr ([thanks to ilusi0n](https://github.com/benphelps/homepage/pull/34)), NZBGet, ruTorrent
|
||||
- Portainer, Traefik, Speedtest Tracker, PiHole
|
||||
* Homepage Widgets
|
||||
* Fast! The entire site is statically generated at build time, so you can expect instant load times.
|
||||
* Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6 ([schklom](https://github.com/benphelps/homepage/pull/3) and [modem7](https://github.com/benphelps/homepage/pull/62))
|
||||
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
|
||||
* Full i18n support with automatic language detection.
|
||||
- Human translations for English, Norwegian Bokmål ([comradekingu](https://github.com/benphelps/homepage/commits?author=comradekingu)), Spanish ([AmadeusGraves](https://github.com/benphelps/homepage/commits?author=AmadeusGraves)), French (J. Lavoie), Dutch ([deffcolony](https://github.com/benphelps/homepage/commits?author=deffcolony)), Chinese ([nicedc](https://github.com/nicedc)) and Russian ([desolaris](https://github.com/benphelps/homepage/commits?author=desolaris)).
|
||||
- Machine translations for Portuguese and German.
|
||||
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/).
|
||||
* Service & Web Bookmarks
|
||||
* Docker Integration
|
||||
- Container status (Running / Stopped) & statistics (CPU, Memory, Network)
|
||||
- Automatic service discovery (via labels)
|
||||
* Service Integration
|
||||
- Currently supports Sonarr, Radarr, Ombi, Emby, Jellyfin, Tautulli (Plex), Overseerr, Jellyseerr ([ilusi0n](https://github.com/benphelps/homepage/pull/34)), NZBGet, ruTorrent
|
||||
- Portainer, Traefik, Speedtest Tracker, PiHole, Nginx Proxy Manager ([aidenpwnz](https://github.com/benphelps/homepage/pull/45))
|
||||
* Information & Utility Widgets
|
||||
- System Stats (Disk, CPU, Memory)
|
||||
- Weather via WeatherAPI.com or OpenWeatherMap ([thanks to AlexFullmoon](https://github.com/benphelps/homepage/pull/25))
|
||||
- Weather via WeatherAPI.com or OpenWeatherMap ([AlexFullmoon](https://github.com/benphelps/homepage/pull/25))
|
||||
- Automatic location detection (with HTTPS), or manual location selection
|
||||
- Search Bar ([aidenpwnz](https://github.com/benphelps/homepage/pull/45))
|
||||
* Customizable
|
||||
- 21 theme colors with light and dark mode support
|
||||
- Background image support
|
||||
|
||||
## Support & Suggestions
|
||||
|
||||
@@ -78,6 +89,8 @@ Configuration files will be genereted and placed on the first request.
|
||||
Configuration is done in the /config directory using .yaml files. Refer to each config for
|
||||
the specific configuration options.
|
||||
|
||||
You may also check [the wiki](https://github.com/benphelps/homepage/wiki) for detailed configuration instructions, examples and more.
|
||||
|
||||
## Development
|
||||
|
||||
Install NPM packages, this project uses [pnpm](https://pnpm.io/) (and so should you!):
|
||||
|
||||
23
package.json
23
package.json
@@ -10,26 +10,41 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"dockerode": "^3.3.3",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"classnames": "^2.3.1",
|
||||
"dockerode": "^3.3.4",
|
||||
"i18next": "^21.9.1",
|
||||
"i18next-browser-languagedetector": "^6.1.5",
|
||||
"i18next-http-backend": "^1.4.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-rpc-2.0": "^1.3.0",
|
||||
"json-rpc-2.0": "^1.4.1",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "12.2.5",
|
||||
"node-os-utils": "^1.3.7",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"raw-body": "^2.5.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-i18next": "^11.18.5",
|
||||
"react-icons": "^4.4.0",
|
||||
"rutorrent-promise": "^2.0.0",
|
||||
"shvl": "^3.0.0",
|
||||
"swr": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.8",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-next": "12.2.5",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.4.16",
|
||||
"prettier": "^2.7.1",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
451
pnpm-lock.yaml
generated
451
pnpm-lock.yaml
generated
@@ -2,49 +2,79 @@ lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
'@headlessui/react': ^1.6.6
|
||||
'@tailwindcss/forms': ^0.5.2
|
||||
'@tailwindcss/forms': ^0.5.3
|
||||
autoprefixer: ^10.4.8
|
||||
dockerode: ^3.3.3
|
||||
classnames: ^2.3.1
|
||||
dockerode: ^3.3.4
|
||||
eslint: 8.22.0
|
||||
eslint-config-airbnb: ^19.0.4
|
||||
eslint-config-next: 12.2.5
|
||||
eslint-config-prettier: ^8.5.0
|
||||
eslint-plugin-import: ^2.26.0
|
||||
eslint-plugin-jsx-a11y: ^6.6.1
|
||||
eslint-plugin-prettier: ^4.2.1
|
||||
eslint-plugin-react: ^7.30.1
|
||||
eslint-plugin-react-hooks: ^4.6.0
|
||||
i18next: ^21.9.1
|
||||
i18next-browser-languagedetector: ^6.1.5
|
||||
i18next-http-backend: ^1.4.1
|
||||
js-yaml: ^4.1.0
|
||||
json-rpc-2.0: ^1.3.0
|
||||
json-rpc-2.0: ^1.4.1
|
||||
memory-cache: ^0.2.0
|
||||
next: 12.2.5
|
||||
node-os-utils: ^1.3.7
|
||||
postcss: ^8.4.16
|
||||
prettier: ^2.7.1
|
||||
pretty-bytes: ^6.0.0
|
||||
raw-body: ^2.5.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0
|
||||
react-i18next: ^11.18.5
|
||||
react-icons: ^4.4.0
|
||||
rutorrent-promise: ^2.0.0
|
||||
shvl: ^3.0.0
|
||||
swr: ^1.3.0
|
||||
tailwindcss: ^3.1.8
|
||||
typescript: ^4.7.4
|
||||
typescript: ^4.8.2
|
||||
|
||||
dependencies:
|
||||
'@headlessui/react': 1.6.6_biqbaboplfbrettd7655fr4n2y
|
||||
'@tailwindcss/forms': 0.5.2_tailwindcss@3.1.8
|
||||
dockerode: 3.3.3
|
||||
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
|
||||
classnames: 2.3.1
|
||||
dockerode: 3.3.4
|
||||
i18next: 21.9.1
|
||||
i18next-browser-languagedetector: 6.1.5
|
||||
i18next-http-backend: 1.4.1
|
||||
js-yaml: 4.1.0
|
||||
json-rpc-2.0: 1.3.0
|
||||
json-rpc-2.0: 1.4.1
|
||||
memory-cache: 0.2.0
|
||||
next: 12.2.5_biqbaboplfbrettd7655fr4n2y
|
||||
node-os-utils: 1.3.7
|
||||
pretty-bytes: 6.0.0
|
||||
raw-body: 2.5.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
react-i18next: 11.18.5_4sidbwfhen5r7txudrvpua6nty
|
||||
react-icons: 4.4.0_react@18.2.0
|
||||
rutorrent-promise: 2.0.0
|
||||
shvl: 3.0.0
|
||||
swr: 1.3.0_react@18.2.0
|
||||
|
||||
devDependencies:
|
||||
autoprefixer: 10.4.8_postcss@8.4.16
|
||||
eslint: 8.22.0
|
||||
eslint-config-next: 12.2.5_4rv7y5c6xz3vfxwhbrcxxi73bq
|
||||
eslint-config-airbnb: 19.0.4_ujj5bqj46ej3re666g22wkx75e
|
||||
eslint-config-next: 12.2.5_shit3uhl6a7megkzgoz6xssnfa
|
||||
eslint-config-prettier: 8.5.0_eslint@8.22.0
|
||||
eslint-plugin-import: 2.26.0_eslint@8.22.0
|
||||
eslint-plugin-jsx-a11y: 6.6.1_eslint@8.22.0
|
||||
eslint-plugin-prettier: 4.2.1_i2cojdczqdiurzgttlwdgf764e
|
||||
eslint-plugin-react: 7.31.5_eslint@8.22.0
|
||||
eslint-plugin-react-hooks: 4.6.0_eslint@8.22.0
|
||||
postcss: 8.4.16
|
||||
tailwindcss: 3.1.8
|
||||
typescript: 4.7.4
|
||||
prettier: 2.7.1
|
||||
tailwindcss: 3.1.8_postcss@8.4.16
|
||||
typescript: 4.8.2
|
||||
|
||||
packages:
|
||||
|
||||
@@ -52,7 +82,7 @@ packages:
|
||||
resolution: {integrity: sha512-qZEWeccZCrHA2Au4/X05QW5CMdm4VjUDCrGq5gf1ZDcM4hRqreKrtwAn7yci9zfgAS9apvnsFXiGBHBAxZdK9A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
core-js-pure: 3.24.1
|
||||
core-js-pure: 3.25.0
|
||||
regenerator-runtime: 0.13.9
|
||||
dev: true
|
||||
|
||||
@@ -61,15 +91,18 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.13.9
|
||||
dev: true
|
||||
|
||||
/@eslint/eslintrc/1.3.0:
|
||||
resolution: {integrity: sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==}
|
||||
/@balena/dockerignore/1.0.2:
|
||||
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
|
||||
dev: false
|
||||
|
||||
/@eslint/eslintrc/1.3.1:
|
||||
resolution: {integrity: sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.3.4
|
||||
espree: 9.3.3
|
||||
espree: 9.4.0
|
||||
globals: 13.17.0
|
||||
ignore: 5.2.0
|
||||
import-fresh: 3.3.0
|
||||
@@ -265,21 +298,21 @@ packages:
|
||||
tslib: 2.4.0
|
||||
dev: false
|
||||
|
||||
/@tailwindcss/forms/0.5.2_tailwindcss@3.1.8:
|
||||
resolution: {integrity: sha512-pSrFeJB6Bg1Mrg9CdQW3+hqZXAKsBrSG9MAfFLKy1pVA4Mb4W7C0k7mEhlmS2Dfo/otxrQOET7NJiJ9RrS563w==}
|
||||
/@tailwindcss/forms/0.5.3_tailwindcss@3.1.8:
|
||||
resolution: {integrity: sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==}
|
||||
peerDependencies:
|
||||
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
|
||||
dependencies:
|
||||
mini-svg-data-uri: 1.4.4
|
||||
tailwindcss: 3.1.8
|
||||
tailwindcss: 3.1.8_postcss@8.4.16
|
||||
dev: false
|
||||
|
||||
/@types/json5/0.0.29:
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/parser/5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq:
|
||||
resolution: {integrity: sha512-cgM5cJrWmrDV2KpvlcSkelTBASAs1mgqq+IUGKJvFxWrapHpaRy5EXPQz9YaKF3nZ8KY18ILTiVpUtbIac86/w==}
|
||||
/@typescript-eslint/parser/5.36.1_shit3uhl6a7megkzgoz6xssnfa:
|
||||
resolution: {integrity: sha512-/IsgNGOkBi7CuDfUbwt1eOqUXF9WGVBW9dwEe1pi+L32XrTsZIgmDFIi2RxjzsvB/8i+MIf5JIoTEH8LOZ368A==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -288,31 +321,31 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 5.33.0
|
||||
'@typescript-eslint/types': 5.33.0
|
||||
'@typescript-eslint/typescript-estree': 5.33.0_typescript@4.7.4
|
||||
'@typescript-eslint/scope-manager': 5.36.1
|
||||
'@typescript-eslint/types': 5.36.1
|
||||
'@typescript-eslint/typescript-estree': 5.36.1_typescript@4.8.2
|
||||
debug: 4.3.4
|
||||
eslint: 8.22.0
|
||||
typescript: 4.7.4
|
||||
typescript: 4.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/scope-manager/5.33.0:
|
||||
resolution: {integrity: sha512-/Jta8yMNpXYpRDl8EwF/M8It2A9sFJTubDo0ATZefGXmOqlaBffEw0ZbkbQ7TNDK6q55NPHFshGBPAZvZkE8Pw==}
|
||||
/@typescript-eslint/scope-manager/5.36.1:
|
||||
resolution: {integrity: sha512-pGC2SH3/tXdu9IH3ItoqciD3f3RRGCh7hb9zPdN2Drsr341zgd6VbhP5OHQO/reUqihNltfPpMpTNihFMarP2w==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.33.0
|
||||
'@typescript-eslint/visitor-keys': 5.33.0
|
||||
'@typescript-eslint/types': 5.36.1
|
||||
'@typescript-eslint/visitor-keys': 5.36.1
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/types/5.33.0:
|
||||
resolution: {integrity: sha512-nIMt96JngB4MYFYXpZ/3ZNU4GWPNdBbcB5w2rDOCpXOVUkhtNlG2mmm8uXhubhidRZdwMaMBap7Uk8SZMU/ppw==}
|
||||
/@typescript-eslint/types/5.36.1:
|
||||
resolution: {integrity: sha512-jd93ShpsIk1KgBTx9E+hCSEuLCUFwi9V/urhjOWnOaksGZFbTOxAT47OH2d4NLJnLhkVD+wDbB48BuaycZPLBg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/typescript-estree/5.33.0_typescript@4.7.4:
|
||||
resolution: {integrity: sha512-tqq3MRLlggkJKJUrzM6wltk8NckKyyorCSGMq4eVkyL5sDYzJJcMgZATqmF8fLdsWrW7OjjIZ1m9v81vKcaqwQ==}
|
||||
/@typescript-eslint/typescript-estree/5.36.1_typescript@4.8.2:
|
||||
resolution: {integrity: sha512-ih7V52zvHdiX6WcPjsOdmADhYMDN15SylWRZrT2OMy80wzKbc79n8wFW0xpWpU0x3VpBz/oDgTm2xwDAnFTl+g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
@@ -320,23 +353,23 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.33.0
|
||||
'@typescript-eslint/visitor-keys': 5.33.0
|
||||
'@typescript-eslint/types': 5.36.1
|
||||
'@typescript-eslint/visitor-keys': 5.36.1
|
||||
debug: 4.3.4
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.3.7
|
||||
tsutils: 3.21.0_typescript@4.7.4
|
||||
typescript: 4.7.4
|
||||
tsutils: 3.21.0_typescript@4.8.2
|
||||
typescript: 4.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/visitor-keys/5.33.0:
|
||||
resolution: {integrity: sha512-/XsqCzD4t+Y9p5wd9HZiptuGKBlaZO5showwqODii5C0nZawxWLF+Q6k5wYHBrQv96h6GYKyqqMHCSTqta8Kiw==}
|
||||
/@typescript-eslint/visitor-keys/5.36.1:
|
||||
resolution: {integrity: sha512-ojB9aRyRFzVMN3b5joSYni6FAS10BBSCAfKJhjJAV08t/a95aM6tAhz+O1jF+EtgxktuSO3wJysp2R+Def/IWQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.33.0
|
||||
'@typescript-eslint/types': 5.36.1
|
||||
eslint-visitor-keys: 3.3.0
|
||||
dev: true
|
||||
|
||||
@@ -418,7 +451,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
get-intrinsic: 1.1.2
|
||||
is-string: 1.0.7
|
||||
dev: true
|
||||
@@ -434,7 +467,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
es-shim-unscopables: 1.0.0
|
||||
dev: true
|
||||
|
||||
@@ -444,7 +477,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
es-shim-unscopables: 1.0.0
|
||||
dev: true
|
||||
|
||||
@@ -470,7 +503,7 @@ packages:
|
||||
postcss: ^8.1.0
|
||||
dependencies:
|
||||
browserslist: 4.21.3
|
||||
caniuse-lite: 1.0.30001375
|
||||
caniuse-lite: 1.0.30001388
|
||||
fraction.js: 4.2.0
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.0.0
|
||||
@@ -531,10 +564,10 @@ packages:
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001375
|
||||
electron-to-chromium: 1.4.219
|
||||
caniuse-lite: 1.0.30001388
|
||||
electron-to-chromium: 1.4.241
|
||||
node-releases: 2.0.6
|
||||
update-browserslist-db: 1.0.5_browserslist@4.21.3
|
||||
update-browserslist-db: 1.0.7_browserslist@4.21.3
|
||||
dev: true
|
||||
|
||||
/buffer/5.7.1:
|
||||
@@ -571,8 +604,8 @@ packages:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
/caniuse-lite/1.0.30001375:
|
||||
resolution: {integrity: sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==}
|
||||
/caniuse-lite/1.0.30001388:
|
||||
resolution: {integrity: sha512-znVbq4OUjqgLxMxoNX2ZeeLR0d7lcDiE5uJ4eUiWdml1J1EkxbnQq6opT9jb9SMfJxB0XA16/ziHwni4u1I3GQ==}
|
||||
|
||||
/chalk/4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
@@ -600,6 +633,10 @@ packages:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
dev: false
|
||||
|
||||
/classnames/2.3.1:
|
||||
resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==}
|
||||
dev: false
|
||||
|
||||
/color-convert/2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@@ -621,8 +658,12 @@ packages:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
dev: true
|
||||
|
||||
/core-js-pure/3.24.1:
|
||||
resolution: {integrity: sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg==}
|
||||
/confusing-browser-globals/1.0.11:
|
||||
resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==}
|
||||
dev: true
|
||||
|
||||
/core-js-pure/3.25.0:
|
||||
resolution: {integrity: sha512-IeHpLwk3uoci37yoI2Laty59+YqH9x5uR65/yiA0ARAJrTrN4YU0rmauLWfvqOuk77SlNJXj2rM6oT/dBD87+A==}
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
|
||||
@@ -636,6 +677,14 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/cross-fetch/3.1.5:
|
||||
resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==}
|
||||
dependencies:
|
||||
node-fetch: 2.6.7
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
/cross-spawn/7.0.3:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -734,8 +783,8 @@ packages:
|
||||
/dlv/1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
|
||||
/docker-modem/3.0.5:
|
||||
resolution: {integrity: sha512-x1E6jxWdtoK3+ifAUWj4w5egPdTDGBpesSCErm+aKET5BnnEOvDtTP6GxcnMB1zZiv2iQ0qJZvJie+1wfIRg6Q==}
|
||||
/docker-modem/3.0.6:
|
||||
resolution: {integrity: sha512-h0Ow21gclbYsZ3mkHDfsYNDqtRhXS8fXr51bU0qr1dxgTMJj0XufbzX+jhNOvA8KuEEzn6JbvLVhXyv+fny9Uw==}
|
||||
engines: {node: '>= 8.0'}
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
@@ -746,11 +795,12 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/dockerode/3.3.3:
|
||||
resolution: {integrity: sha512-lvKV6/NGf2/CYLt5V4c0fd6Fl9XZSCo1Z2HBT9ioKrKLMB2o+gA62Uza8RROpzGvYv57KJx2dKu+ZwSpB//OIA==}
|
||||
/dockerode/3.3.4:
|
||||
resolution: {integrity: sha512-3EUwuXnCU+RUlQEheDjmBE0B7q66PV9Rw5NiH1sXwINq0M9c5ERP9fxgkw36ZHOtzf4AGEEYySnkx/sACC9EgQ==}
|
||||
engines: {node: '>= 8.0'}
|
||||
dependencies:
|
||||
docker-modem: 3.0.5
|
||||
'@balena/dockerignore': 1.0.2
|
||||
docker-modem: 3.0.6
|
||||
tar-fs: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -770,8 +820,8 @@ packages:
|
||||
esutils: 2.0.3
|
||||
dev: true
|
||||
|
||||
/electron-to-chromium/1.4.219:
|
||||
resolution: {integrity: sha512-zoQJsXOUw0ZA0YxbjkmzBumAJRtr6je5JySuL/bAoFs0DuLiLJ+5FzRF7/ZayihxR2QcewlRZVm5QZdUhwjOgA==}
|
||||
/electron-to-chromium/1.4.241:
|
||||
resolution: {integrity: sha512-e7Wsh4ilaioBZ5bMm6+F4V5c11dh56/5Jwz7Hl5Tu1J7cnB+Pqx5qIF2iC7HPpfyQMqGSvvLP5bBAIDd2gAtGw==}
|
||||
dev: true
|
||||
|
||||
/emoji-regex/9.2.2:
|
||||
@@ -784,8 +834,8 @@ packages:
|
||||
once: 1.4.0
|
||||
dev: false
|
||||
|
||||
/es-abstract/1.20.1:
|
||||
resolution: {integrity: sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==}
|
||||
/es-abstract/1.20.2:
|
||||
resolution: {integrity: sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
@@ -806,7 +856,7 @@ packages:
|
||||
is-weakref: 1.0.2
|
||||
object-inspect: 1.12.2
|
||||
object-keys: 1.1.1
|
||||
object.assign: 4.1.3
|
||||
object.assign: 4.1.4
|
||||
regexp.prototype.flags: 1.4.3
|
||||
string.prototype.trimend: 1.0.5
|
||||
string.prototype.trimstart: 1.0.5
|
||||
@@ -838,7 +888,42 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/eslint-config-next/12.2.5_4rv7y5c6xz3vfxwhbrcxxi73bq:
|
||||
/eslint-config-airbnb-base/15.0.0_2iahngt3u2tkbdlu6s4gkur3pu:
|
||||
resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^7.32.0 || ^8.2.0
|
||||
eslint-plugin-import: ^2.25.2
|
||||
dependencies:
|
||||
confusing-browser-globals: 1.0.11
|
||||
eslint: 8.22.0
|
||||
eslint-plugin-import: 2.26.0_eslint@8.22.0
|
||||
object.assign: 4.1.4
|
||||
object.entries: 1.1.5
|
||||
semver: 6.3.0
|
||||
dev: true
|
||||
|
||||
/eslint-config-airbnb/19.0.4_ujj5bqj46ej3re666g22wkx75e:
|
||||
resolution: {integrity: sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==}
|
||||
engines: {node: ^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^7.32.0 || ^8.2.0
|
||||
eslint-plugin-import: ^2.25.3
|
||||
eslint-plugin-jsx-a11y: ^6.5.1
|
||||
eslint-plugin-react: ^7.28.0
|
||||
eslint-plugin-react-hooks: ^4.3.0
|
||||
dependencies:
|
||||
eslint: 8.22.0
|
||||
eslint-config-airbnb-base: 15.0.0_2iahngt3u2tkbdlu6s4gkur3pu
|
||||
eslint-plugin-import: 2.26.0_eslint@8.22.0
|
||||
eslint-plugin-jsx-a11y: 6.6.1_eslint@8.22.0
|
||||
eslint-plugin-react: 7.31.5_eslint@8.22.0
|
||||
eslint-plugin-react-hooks: 4.6.0_eslint@8.22.0
|
||||
object.assign: 4.1.4
|
||||
object.entries: 1.1.5
|
||||
dev: true
|
||||
|
||||
/eslint-config-next/12.2.5_shit3uhl6a7megkzgoz6xssnfa:
|
||||
resolution: {integrity: sha512-SOowilkqPzW6DxKp3a3SYlrfPi5Ajs9MIzp9gVfUDxxH9QFM5ElkR1hX5m/iICJuvCbWgQqFBiA3mCMozluniw==}
|
||||
peerDependencies:
|
||||
eslint: ^7.23.0 || ^8.0.0
|
||||
@@ -849,20 +934,29 @@ packages:
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 12.2.5
|
||||
'@rushstack/eslint-patch': 1.1.4
|
||||
'@typescript-eslint/parser': 5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq
|
||||
'@typescript-eslint/parser': 5.36.1_shit3uhl6a7megkzgoz6xssnfa
|
||||
eslint: 8.22.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
eslint-import-resolver-typescript: 2.7.1_2iahngt3u2tkbdlu6s4gkur3pu
|
||||
eslint-plugin-import: 2.26.0_42jdfezp7lcuhr3fexihng3k3a
|
||||
eslint-plugin-import: 2.26.0_oqagwj4pgbbpoeodu52b4a34xi
|
||||
eslint-plugin-jsx-a11y: 6.6.1_eslint@8.22.0
|
||||
eslint-plugin-react: 7.30.1_eslint@8.22.0
|
||||
eslint-plugin-react: 7.31.5_eslint@8.22.0
|
||||
eslint-plugin-react-hooks: 4.6.0_eslint@8.22.0
|
||||
typescript: 4.7.4
|
||||
typescript: 4.8.2
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-config-prettier/8.5.0_eslint@8.22.0:
|
||||
resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
eslint: '>=7.0.0'
|
||||
dependencies:
|
||||
eslint: 8.22.0
|
||||
dev: true
|
||||
|
||||
/eslint-import-resolver-node/0.3.6:
|
||||
resolution: {integrity: sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==}
|
||||
dependencies:
|
||||
@@ -881,7 +975,7 @@ packages:
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
eslint: 8.22.0
|
||||
eslint-plugin-import: 2.26.0_42jdfezp7lcuhr3fexihng3k3a
|
||||
eslint-plugin-import: 2.26.0_oqagwj4pgbbpoeodu52b4a34xi
|
||||
glob: 7.2.3
|
||||
is-glob: 4.0.3
|
||||
resolve: 1.22.1
|
||||
@@ -890,7 +984,7 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-module-utils/2.7.4_2znkvn5grarov6xvamxzzjpwtu:
|
||||
/eslint-module-utils/2.7.4_56k6siz3uc4k5fooi777xiw6ie:
|
||||
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
@@ -911,7 +1005,7 @@ packages:
|
||||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq
|
||||
'@typescript-eslint/parser': 5.36.1_shit3uhl6a7megkzgoz6xssnfa
|
||||
debug: 3.2.7
|
||||
eslint: 8.22.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
@@ -920,7 +1014,35 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-import/2.26.0_42jdfezp7lcuhr3fexihng3k3a:
|
||||
/eslint-module-utils/2.7.4_7gfxlqsjhuntdifxknjgbjwpbu:
|
||||
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': '*'
|
||||
eslint: '*'
|
||||
eslint-import-resolver-node: '*'
|
||||
eslint-import-resolver-typescript: '*'
|
||||
eslint-import-resolver-webpack: '*'
|
||||
peerDependenciesMeta:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
eslint:
|
||||
optional: true
|
||||
eslint-import-resolver-node:
|
||||
optional: true
|
||||
eslint-import-resolver-typescript:
|
||||
optional: true
|
||||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
eslint: 8.22.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-import/2.26.0_eslint@8.22.0:
|
||||
resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
@@ -930,14 +1052,44 @@ packages:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq
|
||||
array-includes: 3.1.5
|
||||
array.prototype.flat: 1.3.0
|
||||
debug: 2.6.9
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.22.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
eslint-module-utils: 2.7.4_2znkvn5grarov6xvamxzzjpwtu
|
||||
eslint-module-utils: 2.7.4_7gfxlqsjhuntdifxknjgbjwpbu
|
||||
has: 1.0.3
|
||||
is-core-module: 2.10.0
|
||||
is-glob: 4.0.3
|
||||
minimatch: 3.1.2
|
||||
object.values: 1.1.5
|
||||
resolve: 1.22.1
|
||||
tsconfig-paths: 3.14.1
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-import/2.26.0_oqagwj4pgbbpoeodu52b4a34xi:
|
||||
resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': '*'
|
||||
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
|
||||
peerDependenciesMeta:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.36.1_shit3uhl6a7megkzgoz6xssnfa
|
||||
array-includes: 3.1.5
|
||||
array.prototype.flat: 1.3.0
|
||||
debug: 2.6.9
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.22.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
eslint-module-utils: 2.7.4_56k6siz3uc4k5fooi777xiw6ie
|
||||
has: 1.0.3
|
||||
is-core-module: 2.10.0
|
||||
is-glob: 4.0.3
|
||||
@@ -973,6 +1125,23 @@ packages:
|
||||
semver: 6.3.0
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-prettier/4.2.1_i2cojdczqdiurzgttlwdgf764e:
|
||||
resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
eslint: '>=7.28.0'
|
||||
eslint-config-prettier: '*'
|
||||
prettier: '>=2.0.0'
|
||||
peerDependenciesMeta:
|
||||
eslint-config-prettier:
|
||||
optional: true
|
||||
dependencies:
|
||||
eslint: 8.22.0
|
||||
eslint-config-prettier: 8.5.0_eslint@8.22.0
|
||||
prettier: 2.7.1
|
||||
prettier-linter-helpers: 1.0.0
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-react-hooks/4.6.0_eslint@8.22.0:
|
||||
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -982,8 +1151,8 @@ packages:
|
||||
eslint: 8.22.0
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-react/7.30.1_eslint@8.22.0:
|
||||
resolution: {integrity: sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==}
|
||||
/eslint-plugin-react/7.31.5_eslint@8.22.0:
|
||||
resolution: {integrity: sha512-y7472VAcqns17rsQUk6tQCnqBi+boYjGdYarX022719+wGd1T4U1fOYJ2T2Trd3Od2q5M92e42zJ2uZOGmWamA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
|
||||
@@ -1038,7 +1207,7 @@ packages:
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@eslint/eslintrc': 1.3.0
|
||||
'@eslint/eslintrc': 1.3.1
|
||||
'@humanwhocodes/config-array': 0.10.4
|
||||
'@humanwhocodes/gitignore-to-minimatch': 1.0.2
|
||||
ajv: 6.12.6
|
||||
@@ -1050,7 +1219,7 @@ packages:
|
||||
eslint-scope: 7.1.1
|
||||
eslint-utils: 3.0.0_eslint@8.22.0
|
||||
eslint-visitor-keys: 3.3.0
|
||||
espree: 9.3.3
|
||||
espree: 9.4.0
|
||||
esquery: 1.4.0
|
||||
esutils: 2.0.3
|
||||
fast-deep-equal: 3.1.3
|
||||
@@ -1081,8 +1250,8 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/espree/9.3.3:
|
||||
resolution: {integrity: sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==}
|
||||
/espree/9.4.0:
|
||||
resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dependencies:
|
||||
acorn: 8.8.0
|
||||
@@ -1118,6 +1287,10 @@ packages:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
dev: true
|
||||
|
||||
/fast-diff/1.2.0:
|
||||
resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==}
|
||||
dev: true
|
||||
|
||||
/fast-glob/3.2.11:
|
||||
resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
@@ -1166,12 +1339,12 @@ packages:
|
||||
resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
dependencies:
|
||||
flatted: 3.2.6
|
||||
flatted: 3.2.7
|
||||
rimraf: 3.0.2
|
||||
dev: true
|
||||
|
||||
/flatted/3.2.6:
|
||||
resolution: {integrity: sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==}
|
||||
/flatted/3.2.7:
|
||||
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
|
||||
dev: true
|
||||
|
||||
/form-data/3.0.1:
|
||||
@@ -1211,7 +1384,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
functions-have-names: 1.2.3
|
||||
dev: true
|
||||
|
||||
@@ -1329,6 +1502,12 @@ packages:
|
||||
dependencies:
|
||||
function-bind: 1.1.1
|
||||
|
||||
/html-parse-stringify/3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
dev: false
|
||||
|
||||
/http-errors/2.0.0:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -1340,6 +1519,26 @@ packages:
|
||||
toidentifier: 1.0.1
|
||||
dev: false
|
||||
|
||||
/i18next-browser-languagedetector/6.1.5:
|
||||
resolution: {integrity: sha512-11t7b39oKeZe4uyMxLSPnfw28BCPNLZgUk7zyufex0zKXZ+Bv+JnmJgoB+IfQLZwDt1d71PM8vwBX1NCgliY3g==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.18.9
|
||||
dev: false
|
||||
|
||||
/i18next-http-backend/1.4.1:
|
||||
resolution: {integrity: sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==}
|
||||
dependencies:
|
||||
cross-fetch: 3.1.5
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
/i18next/21.9.1:
|
||||
resolution: {integrity: sha512-ITbDrAjbRR73spZAiu6+ex5WNlHRr1mY+acDi2ioTHuUiviJqSz269Le1xHAf0QaQ6GgIHResUhQNcxGwa/PhA==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.18.9
|
||||
dev: false
|
||||
|
||||
/iconv-lite/0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1498,8 +1697,8 @@ packages:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
/json-rpc-2.0/1.3.0:
|
||||
resolution: {integrity: sha512-pA85D9LG4B+b8njdb8DzktltNfGPcxGJJydEg6jRqePfwhK008lRdbX7bco6aokfw9oDFw7fEe+LRxxXwP11HA==}
|
||||
/json-rpc-2.0/1.4.1:
|
||||
resolution: {integrity: sha512-OX1NJhpIfuK4GjDnJ/gKtZy1HOYo0l4eL0a4rb0rNeQheX1xlyQ9+JMmPzs/sFNthpS/TXKPWlGo09X7B5l81A==}
|
||||
dev: false
|
||||
|
||||
/json-schema-traverse/0.4.1:
|
||||
@@ -1522,7 +1721,7 @@ packages:
|
||||
engines: {node: '>=4.0'}
|
||||
dependencies:
|
||||
array-includes: 3.1.5
|
||||
object.assign: 4.1.3
|
||||
object.assign: 4.1.4
|
||||
dev: true
|
||||
|
||||
/language-subtag-registry/0.3.22:
|
||||
@@ -1661,7 +1860,7 @@ packages:
|
||||
dependencies:
|
||||
'@next/env': 12.2.5
|
||||
'@swc/helpers': 0.4.3
|
||||
caniuse-lite: 1.0.30001375
|
||||
caniuse-lite: 1.0.30001388
|
||||
postcss: 8.4.14
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
@@ -1733,8 +1932,8 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
dev: true
|
||||
|
||||
/object.assign/4.1.3:
|
||||
resolution: {integrity: sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==}
|
||||
/object.assign/4.1.4:
|
||||
resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
@@ -1749,7 +1948,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/object.fromentries/2.0.5:
|
||||
@@ -1758,14 +1957,14 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/object.hasown/1.1.1:
|
||||
resolution: {integrity: sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==}
|
||||
dependencies:
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/object.values/1.1.5:
|
||||
@@ -1774,7 +1973,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/once/1.4.0:
|
||||
@@ -1926,6 +2125,24 @@ packages:
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dev: true
|
||||
|
||||
/prettier-linter-helpers/1.0.0:
|
||||
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dependencies:
|
||||
fast-diff: 1.2.0
|
||||
dev: true
|
||||
|
||||
/prettier/2.7.1:
|
||||
resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/pretty-bytes/6.0.0:
|
||||
resolution: {integrity: sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==}
|
||||
engines: {node: ^14.13.1 || >=16.0.0}
|
||||
dev: false
|
||||
|
||||
/prop-types/15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
dependencies:
|
||||
@@ -1973,6 +2190,26 @@ packages:
|
||||
scheduler: 0.23.0
|
||||
dev: false
|
||||
|
||||
/react-i18next/11.18.5_4sidbwfhen5r7txudrvpua6nty:
|
||||
resolution: {integrity: sha512-cKcyuuzIv0YUZ4l9WORflVNuhISPAqQShOAsxwFyYuJoCA7HlLmHm7XnvO6hfAGmGpDNRhJHoBX8hG49Cb2xZQ==}
|
||||
peerDependencies:
|
||||
i18next: '>= 19.0.0'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.18.9
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 21.9.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
dev: false
|
||||
|
||||
/react-icons/4.4.0_react@18.2.0:
|
||||
resolution: {integrity: sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==}
|
||||
peerDependencies:
|
||||
@@ -2014,7 +2251,6 @@ packages:
|
||||
|
||||
/regenerator-runtime/0.13.9:
|
||||
resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==}
|
||||
dev: true
|
||||
|
||||
/regexp.prototype.flags/1.4.3:
|
||||
resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
|
||||
@@ -2120,6 +2356,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/shvl/3.0.0:
|
||||
resolution: {integrity: sha512-5IomAM3ykE/g9K9L6lhODc+TpCuN03rrhlboegeKyyfh66DDdpRD5JN37DYhNHH+RaYjiIDx64K/Ms/xQYOR5w==}
|
||||
dev: false
|
||||
|
||||
/side-channel/1.0.4:
|
||||
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
||||
dependencies:
|
||||
@@ -2163,7 +2403,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
get-intrinsic: 1.1.2
|
||||
has-symbols: 1.0.3
|
||||
internal-slot: 1.0.3
|
||||
@@ -2176,7 +2416,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/string.prototype.trimstart/1.0.5:
|
||||
@@ -2184,7 +2424,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/string_decoder/1.3.0:
|
||||
@@ -2245,10 +2485,12 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/tailwindcss/3.1.8:
|
||||
/tailwindcss/3.1.8_postcss@8.4.16:
|
||||
resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
postcss: ^8.0.9
|
||||
dependencies:
|
||||
arg: 5.0.2
|
||||
chokidar: 3.5.3
|
||||
@@ -2331,14 +2573,14 @@ packages:
|
||||
resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
|
||||
dev: false
|
||||
|
||||
/tsutils/3.21.0_typescript@4.7.4:
|
||||
/tsutils/3.21.0_typescript@4.8.2:
|
||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||
engines: {node: '>= 6'}
|
||||
peerDependencies:
|
||||
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
|
||||
dependencies:
|
||||
tslib: 1.14.1
|
||||
typescript: 4.7.4
|
||||
typescript: 4.8.2
|
||||
dev: true
|
||||
|
||||
/tweetnacl/0.14.5:
|
||||
@@ -2357,8 +2599,8 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/typescript/4.7.4:
|
||||
resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
|
||||
/typescript/4.8.2:
|
||||
resolution: {integrity: sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==}
|
||||
engines: {node: '>=4.2.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
@@ -2377,8 +2619,8 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/update-browserslist-db/1.0.5_browserslist@4.21.3:
|
||||
resolution: {integrity: sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==}
|
||||
/update-browserslist-db/1.0.7_browserslist@4.21.3:
|
||||
resolution: {integrity: sha512-iN/XYesmZ2RmmWAiI4Z5rq0YqSiv0brj9Ce9CfhNE4xIW2h+MFxcgkxIzZ+ShkFPUkjU3gQ+3oypadD3RAMtrg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
@@ -2409,6 +2651,11 @@ packages:
|
||||
resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
|
||||
dev: true
|
||||
|
||||
/void-elements/3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/webidl-conversions/3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: false
|
||||
|
||||
100
public/locales/de/common.json
Normal file
100
public/locales/de/common.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Fehlender Widget-Typ: {{type}}",
|
||||
"api_error": "API-Fehler",
|
||||
"status": "Status"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Suche…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Gesamt",
|
||||
"free": "Frei",
|
||||
"used": "Gebraucht"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
"tx": "Tx",
|
||||
"mem": "Mem",
|
||||
"cpu": "Prozessor",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Spielen",
|
||||
"transcoding": "Transcodierung",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Spielen",
|
||||
"transcoding": "Transcodierung",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Verblieben",
|
||||
"downloaded": "Heruntergeladen"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktiv",
|
||||
"upload": "Hochladen",
|
||||
"download": "Download"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Gesucht",
|
||||
"queued": "In Warteschlange",
|
||||
"series": "Serie"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Gesucht",
|
||||
"queued": "In Warteschlange",
|
||||
"movies": "Filme"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Ausstehend",
|
||||
"approved": "Genehmigt",
|
||||
"available": "Verfügbar"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Ausstehend",
|
||||
"approved": "Genehmigt",
|
||||
"available": "Verfügbar"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Abfragen",
|
||||
"blocked": "Blockiert",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Betrieb",
|
||||
"stopped": "Gestoppt",
|
||||
"total": "Gesamt"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Router",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"total": "Gesamt"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Aktueller Standort",
|
||||
"allow": "Zum Zulassen anklicken",
|
||||
"updating": "Aktualisieren",
|
||||
"wait": "Bitte warten"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
}
|
||||
}
|
||||
111
public/locales/en/common.json
Normal file
111
public/locales/en/common.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"common": {
|
||||
"bytes": "{{value, bytes}}",
|
||||
"bits": "{{value, bytes(bits: true)}}",
|
||||
"bbytes": "{{value, bytes(binary: true)}}",
|
||||
"bbits": "{{value, bytes(bits: true, binary: true)}}",
|
||||
"byterate": "{{value, rate}}",
|
||||
"bitrate": "{{value, rate(bits: true)}}",
|
||||
"percent": "{{value, percent}}",
|
||||
"number": "{{value, number}}",
|
||||
"ms": "{{value, number}}"
|
||||
},
|
||||
"widget": {
|
||||
"missing_type": "Missing Widget Type: {{type}}",
|
||||
"api_error": "API Error",
|
||||
"status": "Status"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Free",
|
||||
"used": "Used"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Active",
|
||||
"upload": "Upload",
|
||||
"download": "Download"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"movies": "Movies"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"total": "Total"
|
||||
}
|
||||
}
|
||||
100
public/locales/es/common.json
Normal file
100
public/locales/es/common.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Tipo de widget faltante: {{type}}",
|
||||
"api_error": "Error de API",
|
||||
"status": "Estado"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Buscar…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Libre",
|
||||
"used": "Usado"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Recibido",
|
||||
"tx": "Transmitido",
|
||||
"mem": "Memoria",
|
||||
"cpu": "Procesador",
|
||||
"offline": "Desconectado"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "En ejecución",
|
||||
"transcoding": "Transcodificando",
|
||||
"bitrate": "Tasa de Bits",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "En ejecución",
|
||||
"transcoding": "Transcodificación",
|
||||
"bitrate": "Tasa de bits",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Velocidad",
|
||||
"remaining": "Restante",
|
||||
"downloaded": "Descargado"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Activo",
|
||||
"upload": "Subir",
|
||||
"download": "Descargar"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Más deseado",
|
||||
"queued": "Puesto en cola",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Más deseado",
|
||||
"queued": "Puesto en cola",
|
||||
"movies": "Películas"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pendiente",
|
||||
"approved": "Aprobado",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pendiente",
|
||||
"approved": "Aprobado",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Consultas",
|
||||
"blocked": "Bloqueado",
|
||||
"gravity": "Gravedad"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Subir",
|
||||
"download": "Descargar",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "En ejecución",
|
||||
"stopped": "Detenido",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Enrutadores",
|
||||
"services": "Servicios",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Activado",
|
||||
"disabled": "Desactivado",
|
||||
"total": "Total"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Ubicación Actual",
|
||||
"allow": "Haga clic para permitir",
|
||||
"updating": "Actualizando",
|
||||
"wait": "Espere, por favor"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
}
|
||||
}
|
||||
111
public/locales/fr/common.json
Normal file
111
public/locales/fr/common.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Type de widget manquant: {{type}}",
|
||||
"api_error": "Erreur de l'API",
|
||||
"status": "Statut"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Recherche…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Totale",
|
||||
"free": "Libre",
|
||||
"used": "Utilisée"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
"tx": "TX",
|
||||
"mem": "Mem",
|
||||
"cpu": "CPU",
|
||||
"offline": "Hors ligne"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "En jouant",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Débiter",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "En jouant",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Débiter",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Évaluer",
|
||||
"remaining": "Restante",
|
||||
"downloaded": "Téléchargé"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Active",
|
||||
"upload": "Téléverser",
|
||||
"download": "Télécharger"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Recherchée",
|
||||
"queued": "En queue",
|
||||
"series": "Série"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Recherchée",
|
||||
"queued": "En queue",
|
||||
"movies": "Films"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "En attente",
|
||||
"approved": "Approuvée",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "En attente",
|
||||
"approved": "Approuvée",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Requêtes",
|
||||
"blocked": "Bloquée",
|
||||
"gravity": "La gravité"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Téléversement",
|
||||
"download": "Téléchargement",
|
||||
"ping": "Ping-ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Fonctionnement",
|
||||
"stopped": "Arrêté",
|
||||
"total": "Totale"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routeurs",
|
||||
"services": "Prestations de service",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Activé",
|
||||
"disabled": "Handicapée",
|
||||
"total": "Totale"
|
||||
},
|
||||
"common": {
|
||||
"bbytes": "{{value, bytes(binary: true)}}",
|
||||
"bytes": "{{value, bytes}}",
|
||||
"bits": "{{value, bytes(bits: true)}}",
|
||||
"bbits": "{{value, bytes(bits: true, binary: true)}}",
|
||||
"number": "{{value, number}}",
|
||||
"byterate": "{{value, bytes}}",
|
||||
"bitrate": "{{value, bytes(bits: true)}}",
|
||||
"percent": "{{value, percent}}",
|
||||
"ms": "{{value, number}}"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Localisation actuelle",
|
||||
"allow": "Cliquez pour autoriser",
|
||||
"updating": "Mise à jour",
|
||||
"wait": "Veuillez patienter"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
}
|
||||
}
|
||||
100
public/locales/it/common.json
Normal file
100
public/locales/it/common.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"docker": {
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline",
|
||||
"rx": "RX"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"widget": {
|
||||
"missing_type": "Missing Widget Type: {{type}}",
|
||||
"api_error": "API Error",
|
||||
"status": "Status"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Free",
|
||||
"used": "Used"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Active",
|
||||
"upload": "Upload",
|
||||
"download": "Download"
|
||||
},
|
||||
"sonarr": {
|
||||
"series": "Series",
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"movies": "Movies"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"total": "Total"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
}
|
||||
}
|
||||
100
public/locales/nb-NO/common.json
Normal file
100
public/locales/nb-NO/common.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Manglende miniprogramstype: {{type}}",
|
||||
"api_error": "API-feil",
|
||||
"status": "Status"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Søk …"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Totalt",
|
||||
"free": "Ledig",
|
||||
"used": "Brukt"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Mottatt",
|
||||
"tx": "Sendt",
|
||||
"mem": "Minne",
|
||||
"cpu": "Prosessor",
|
||||
"offline": "Frakoblet"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Spiller",
|
||||
"transcoding": "Transkoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Spiller",
|
||||
"transcoding": "Transkoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Takt",
|
||||
"remaining": "Gjenstående",
|
||||
"downloaded": "Nedlastet"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktiv",
|
||||
"upload": "Opplasting",
|
||||
"download": "Nedlasting"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Ønsket",
|
||||
"queued": "I kø",
|
||||
"series": "Serie"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Ønsket",
|
||||
"queued": "I kø",
|
||||
"movies": "Filmer"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Venter",
|
||||
"approved": "Godkjent",
|
||||
"available": "Tilgjengelig"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Venter",
|
||||
"approved": "Godkjent",
|
||||
"available": "Tilgjengelig"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Spørringer",
|
||||
"blocked": "Blokkert",
|
||||
"gravity": "Gravitet"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Opplasting",
|
||||
"download": "Nedlasting",
|
||||
"ping": "Ekkoforespørsel"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Kjører",
|
||||
"stopped": "Stoppet",
|
||||
"total": "Totalt"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Rutere",
|
||||
"services": "Tjenester",
|
||||
"middleware": "Midtvare"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Påskrudd",
|
||||
"disabled": "Avskrudd",
|
||||
"total": "Totalt"
|
||||
},
|
||||
"weather": {
|
||||
"allow": "Klikk for å tillate",
|
||||
"updating": "Oppdaterer …",
|
||||
"wait": "Vent litt …",
|
||||
"current": "Nåværende posisjon"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
}
|
||||
}
|
||||
100
public/locales/nl/common.json
Normal file
100
public/locales/nl/common.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Missing Widget Type: {{type}}",
|
||||
"api_error": "API Error",
|
||||
"status": "Status"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Totaal",
|
||||
"free": "Vrij",
|
||||
"used": "Gebruikt"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Overgebleven",
|
||||
"downloaded": "Gedownload"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Draaiend",
|
||||
"stopped": "Gestopt",
|
||||
"total": "Totaal"
|
||||
},
|
||||
"weather": {
|
||||
"updating": "Updaten",
|
||||
"wait": "Even geduld",
|
||||
"current": "Huidige Locatie",
|
||||
"allow": "Klik om toe te staan"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Zoeken…"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Afspelen",
|
||||
"transcoding": "Transcodering",
|
||||
"bitrate": "Bitsnelheid",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Afspelen",
|
||||
"transcoding": "Transcodering",
|
||||
"bitrate": "Bitsnelheid",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Actief",
|
||||
"upload": "Upload",
|
||||
"download": "Download"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Gezocht",
|
||||
"queued": "In de wachtrij",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"movies": "Films",
|
||||
"wanted": "Gezocht",
|
||||
"queued": "In de wachtrij"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "In afwachting",
|
||||
"approved": "Goedgekeurd",
|
||||
"available": "Beschikbaar"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "In afwachting",
|
||||
"approved": "Goedgekeurd",
|
||||
"available": "Beschikbaar"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Geblokkeerd",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"total": "Totaal"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
}
|
||||
}
|
||||
111
public/locales/pt/common.json
Normal file
111
public/locales/pt/common.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Tipo de widget ausente: {{type}}",
|
||||
"api_error": "Erro da API",
|
||||
"status": "Status"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Pesquisar…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Livre",
|
||||
"used": "Usada"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
"tx": "Tx",
|
||||
"mem": "Mem",
|
||||
"cpu": "CPU",
|
||||
"offline": "Desligada"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "A reproduzir",
|
||||
"transcoding": "Transcodificação",
|
||||
"bitrate": "Taxa de bits",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Reproduzindo",
|
||||
"transcoding": "Transcodificação",
|
||||
"bitrate": "Taxa de bits",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Avaliar",
|
||||
"remaining": "Em falta",
|
||||
"downloaded": "Baixada"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Ativa",
|
||||
"upload": "Envio",
|
||||
"download": "ReceçãoDownload"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Desejada",
|
||||
"queued": "Em fila",
|
||||
"series": "Séries"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Desejado",
|
||||
"queued": "Enfileiradas",
|
||||
"movies": "Filmes"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pendente",
|
||||
"approved": "Aprovada",
|
||||
"available": "Disponível"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pendente",
|
||||
"approved": "Aprovada",
|
||||
"available": "Disponível"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Consultas",
|
||||
"blocked": "Bloqueado",
|
||||
"gravity": "Gravidade"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Envio",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Corrida",
|
||||
"stopped": "Parou",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Roteadores",
|
||||
"services": "Serviços",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Habilitada",
|
||||
"disabled": "Desabilitada",
|
||||
"total": "Total"
|
||||
},
|
||||
"common": {
|
||||
"bytes": "{{value, bytes}}",
|
||||
"bbytes": "{{value, bytes(binary: true)}}",
|
||||
"bits": "{{value, bytes(bits: true)}}",
|
||||
"bbits": "{{value, bytes(bits: true, binary: true)}}",
|
||||
"number": "{{value, number}}",
|
||||
"byterate": "{{value, bytes}}",
|
||||
"ms": "{{value, number}}",
|
||||
"bitrate": "{{value, bytes(bits: true)}}",
|
||||
"percent": "{{value, percent}}"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Localização atual",
|
||||
"allow": "Clicar para permitir",
|
||||
"updating": "A atualizar",
|
||||
"wait": "Por favor aguarde"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
}
|
||||
}
|
||||
100
public/locales/ru/common.json
Normal file
100
public/locales/ru/common.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Отсутствует тип виджета: {{type}}",
|
||||
"api_error": "Ошибка API",
|
||||
"status": "Статус"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Поиск…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Общий",
|
||||
"free": "Свободно",
|
||||
"used": "Использовано"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
"tx": "Тx",
|
||||
"mem": "Память",
|
||||
"cpu": "Процессор",
|
||||
"offline": "Не в сети"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Воспроизведение",
|
||||
"transcoding": "Транскодирование",
|
||||
"bitrate": "Битрейт",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Воспроизведение",
|
||||
"transcoding": "Транскодирование",
|
||||
"bitrate": "Битрейт",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Оценка",
|
||||
"remaining": "Осталось",
|
||||
"downloaded": "Загружено"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Активный",
|
||||
"upload": "Загрузить",
|
||||
"download": "Скачать"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Хотел",
|
||||
"queued": "В очереди",
|
||||
"series": "Серии"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Хотел",
|
||||
"queued": "В очереди",
|
||||
"movies": "Фильмы"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Ожидание",
|
||||
"approved": "Одобрено",
|
||||
"available": "Доступно"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Ожидание",
|
||||
"approved": "Одобрено",
|
||||
"available": "Доступно"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Запросы",
|
||||
"blocked": "Заблокировано",
|
||||
"gravity": "Сила тяжести"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Загрузка",
|
||||
"download": "Скачать",
|
||||
"ping": "пинг"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Запущено",
|
||||
"stopped": "Остановлено",
|
||||
"total": "Всего"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Маршрутизаторы",
|
||||
"services": "Сервисы",
|
||||
"middleware": "Промежуточное программное обеспечение"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Включено",
|
||||
"disabled": "Отключено",
|
||||
"total": "Всего"
|
||||
},
|
||||
"weather": {
|
||||
"wait": "Пожалуйста подождите",
|
||||
"current": "Текущее местоположение",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Обновление"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
}
|
||||
}
|
||||
100
public/locales/vi/common.json
Normal file
100
public/locales/vi/common.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Thiếu loại Widget: {{type}}",
|
||||
"api_error": "Lỗi API",
|
||||
"status": "Trạng thái"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Tìm kiếm…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Tổng",
|
||||
"free": "Dư",
|
||||
"used": "Đã dùng"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "BỘ NHỚ",
|
||||
"cpu": "CPU",
|
||||
"offline": "Ngoại tuyến"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Đang chơi",
|
||||
"transcoding": "Chuyển định dạng",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Đang chơi",
|
||||
"transcoding": "Chuyển định dạng",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Còn lại",
|
||||
"downloaded": "Đã tải"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Hoạt động",
|
||||
"upload": "Tải lên",
|
||||
"download": "Tải xuống"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"movies": "Movies"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"total": "Total"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
}
|
||||
}
|
||||
100
public/locales/zh-CN/common.json
Normal file
100
public/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "缺少小部件类型:{{type}}",
|
||||
"api_error": "API错误",
|
||||
"status": "地位"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "全部的",
|
||||
"free": "自由的",
|
||||
"used": "用过的"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "rx",
|
||||
"tx": "TX",
|
||||
"mem": "mem",
|
||||
"cpu": "中央处理器",
|
||||
"offline": "离线"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "玩",
|
||||
"transcoding": "转码",
|
||||
"bitrate": "比特率",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "玩",
|
||||
"transcoding": "转码",
|
||||
"bitrate": "比特率",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "速度",
|
||||
"remaining": "其余的",
|
||||
"downloaded": "下载"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "积极的",
|
||||
"upload": "上传",
|
||||
"download": "下载"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "通缉",
|
||||
"queued": "排队",
|
||||
"series": "系列"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "通缉",
|
||||
"queued": "排队",
|
||||
"movies": "电影"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "待办的",
|
||||
"approved": "得到正式认可的",
|
||||
"available": "可用的"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "待办的",
|
||||
"approved": "得到正式认可的",
|
||||
"available": "可用的"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "查询",
|
||||
"blocked": "阻止",
|
||||
"gravity": "重力"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "上传",
|
||||
"download": "下载",
|
||||
"ping": "ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "跑步",
|
||||
"stopped": "停了下来",
|
||||
"total": "全部的"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "路由器",
|
||||
"services": "服务",
|
||||
"middleware": "中间件"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "禁用",
|
||||
"total": "全部的"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,22 @@ export default function Item({ bookmark }) {
|
||||
const { hostname } = new URL(bookmark.href);
|
||||
|
||||
return (
|
||||
<li
|
||||
onClick={() => {
|
||||
window.open(bookmark.href, "_blank").focus();
|
||||
}}
|
||||
key={bookmark.name}
|
||||
className="mb-3 cursor-pointer flex rounded-md font-medium text-theme-700 hover:text-theme-800 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/50 bg-white/50 hover:bg-theme-300/10 dark:bg-white/5 dark:hover:bg-white/10"
|
||||
>
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md">
|
||||
{bookmark.abbr}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between rounded-r-md ">
|
||||
<div className="flex-1 grow pl-3 py-2 text-xs">{bookmark.name}</div>
|
||||
<div className="px-2 py-2 truncate text-theme-500 dark:text-theme-400 opacity-50 text-xs">{hostname}</div>
|
||||
</div>
|
||||
<li key={bookmark.name}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(bookmark.href, "_blank").focus()}
|
||||
className="w-full text-left mb-3 cursor-pointer rounded-md font-medium text-theme-700 hover:text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/10 dark:bg-white/10 dark:hover:bg-white/20 backdrop-blur-md"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 hover:text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md">
|
||||
{bookmark.abbr}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between rounded-r-md ">
|
||||
<div className="flex-1 grow pl-3 py-2 text-xs">{bookmark.name}</div>
|
||||
<div className="px-2 py-2 truncate text-theme-500 dark:text-theme-400 opacity-50 text-xs">{hostname}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Item from "components/bookmarks/item";
|
||||
|
||||
export default function List({ bookmarks }) {
|
||||
return (
|
||||
<ul role="list" className="mt-3 flex flex-col">
|
||||
<ul className="mt-3 flex flex-col">
|
||||
{bookmarks.map((bookmark) => (
|
||||
<Item key={bookmark.name} bookmark={bookmark} />
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from "react";
|
||||
import { useContext, Fragment } from "react";
|
||||
import { IoColorPalette } from "react-icons/io5";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Fragment } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { ColorContext } from "utils/color-context";
|
||||
|
||||
@@ -27,6 +27,7 @@ const colors = [
|
||||
"pink",
|
||||
"rose",
|
||||
"red",
|
||||
"white",
|
||||
];
|
||||
|
||||
export default function ColorToggle() {
|
||||
@@ -39,42 +40,38 @@ export default function ColorToggle() {
|
||||
return (
|
||||
<div className="w-full self-center">
|
||||
<Popover className="relative flex items-center">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button className="outline-none">
|
||||
<IoColorPalette
|
||||
className="h-5 w-5 text-theme-800 dark:text-theme-200 transition duration-150 ease-in-out"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute -top-[75px] left-0">
|
||||
<div className="rounded-md shadow-lg ring-1 ring-black ring-opacity-5">
|
||||
<div className="relative grid gap-2 p-2 grid-cols-11 shadow-theme-900/10 dark:shadow-theme-900 rounded-md shadow-md">
|
||||
{colors.map((color) => (
|
||||
<button role="button" onClick={() => setColor(color)} key={color}>
|
||||
<div
|
||||
className={
|
||||
(active == color ? "border-2" : "border-0") +
|
||||
` rounded-md w-5 h-5 border-black/50 dark:border-white/50 theme-${color} bg-theme-500`
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
<Popover.Button className="outline-none">
|
||||
<IoColorPalette
|
||||
className="h-5 w-5 text-theme-800 dark:text-theme-200 transition duration-150 ease-in-out"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute -top-[75px] left-0">
|
||||
<div className="rounded-md shadow-lg ring-1 ring-black ring-opacity-5 w-[85vw] sm:w-full">
|
||||
<div className="relative grid gap-2 p-2 grid-cols-11 bg-white/50 dark:bg-white/10 shadow-black/10 dark:shadow-black/20 rounded-md shadow-md">
|
||||
{colors.map((color) => (
|
||||
<button type="button" onClick={() => setColor(color)} key={color}>
|
||||
<div
|
||||
className={classNames(
|
||||
active === color ? "border-2" : "border-0",
|
||||
`rounded-md w-5 h-5 border-black/50 dark:border-white/50 theme-${color} bg-theme-400`
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
export default function Greeting() {
|
||||
const name = process.env.NEXT_PUBLIC_DISPLAY_NAME;
|
||||
const hour = new Date().getHours();
|
||||
|
||||
let day = "day";
|
||||
|
||||
if (hour < 12) {
|
||||
day = "morning";
|
||||
} else if (hour < 17) {
|
||||
day = "afternoon";
|
||||
} else {
|
||||
day = "evening";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="self-end grow text-2xl font-thin text-theme-800 dark:text-theme-200">
|
||||
Good {day}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Fragment, useRef, useState, Children } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
const Modal = ({ Toggle, Content }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const cancelButtonRef = useRef(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toggle open={open} setOpen={setOpen} />
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
initialFocus={cancelButtonRef}
|
||||
onClose={setOpen}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-theme-900/90 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed z-10 inset-0 overflow-y-auto">
|
||||
<div className="flex items-center justify-center min-h-full">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative rounded-lg shadow-xl transform transition-all my-8 max-w-lg w-full">
|
||||
<Content open={open} setOpen={setOpen} />
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ModalToggle = ({ open, setOpen, children }) => (
|
||||
<div onClick={() => setOpen(!open)}>{children}</div>
|
||||
);
|
||||
|
||||
const ModalContent = ({ open, setOpen, children }) => (
|
||||
<div className="body">{children}</div>
|
||||
);
|
||||
|
||||
Modal.Toggle = ModalToggle;
|
||||
Modal.Content = ModalContent;
|
||||
|
||||
export default Modal;
|
||||
17
src/components/revalidate.jsx
Normal file
17
src/components/revalidate.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { MdRefresh } from "react-icons/md";
|
||||
|
||||
export default function Revalidate() {
|
||||
const revalidate = () => {
|
||||
fetch("/api/revalidate").then((res) => {
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-full flex align-middle self-center mr-3">
|
||||
<MdRefresh onClick={() => revalidate()} className="text-theme-800 dark:text-theme-200 w-6 h-6 cursor-pointer" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,28 +8,32 @@ import Docker from "./widgets/service/docker";
|
||||
function resolveIcon(icon) {
|
||||
if (icon.startsWith("http")) {
|
||||
return `/api/proxy?url=${encodeURIComponent(icon)}`;
|
||||
} else if (icon.startsWith("/")) {
|
||||
return icon;
|
||||
} else {
|
||||
if (icon.endsWith(".png")) {
|
||||
return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}`;
|
||||
} else {
|
||||
return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}.png`;
|
||||
}
|
||||
}
|
||||
|
||||
if (icon.startsWith("/")) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
if (icon.endsWith(".png")) {
|
||||
return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}`;
|
||||
}
|
||||
|
||||
return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}.png`;
|
||||
}
|
||||
|
||||
export default function Item({ service }) {
|
||||
return (
|
||||
<li key={service.name}>
|
||||
<Disclosure>
|
||||
<div className={
|
||||
(service.href && service.href !== "#" ? 'cursor-pointer ' : 'cursor-default ') +
|
||||
'transition-all h-15 overflow-hidden mb-3 p-1 rounded-md font-medium text-theme-700 hover:text-theme-800 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/40 bg-white/50 hover:bg-theme-300/10 dark:bg-white/5 dark:hover:bg-white/10'
|
||||
}>
|
||||
<div
|
||||
className={`${
|
||||
service.href && service.href !== "#" ? "cursor-pointer " : "cursor-default "
|
||||
}transition-all h-15 mb-3 p-1 rounded-md font-medium text-theme-700 hover:text-theme-700/70 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/20 dark:bg-white/10 dark:hover:bg-white/20 backdrop-blur-md`}
|
||||
>
|
||||
<div className="flex">
|
||||
{service.icon && (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (service.href && service.href !== "#") {
|
||||
window.open(service.href, "_blank").focus();
|
||||
@@ -38,10 +42,11 @@ export default function Item({ service }) {
|
||||
className="flex-shrink-0 flex items-center justify-center w-12 "
|
||||
>
|
||||
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (service.href && service.href !== "#") {
|
||||
window.open(service.href, "_blank").focus();
|
||||
@@ -49,13 +54,16 @@ export default function Item({ service }) {
|
||||
}}
|
||||
className="flex-1 flex items-center justify-between rounded-r-md "
|
||||
>
|
||||
<div className="flex-1 px-2 py-2 text-sm">
|
||||
<div className="flex-1 px-2 py-2 text-sm text-left">
|
||||
{service.name}
|
||||
<p className="text-theme-500 dark:text-theme-400 text-xs font-extralight">{service.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{service.container && (
|
||||
<Disclosure.Button as="div" className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer">
|
||||
<Disclosure.Button
|
||||
as="div"
|
||||
className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer"
|
||||
>
|
||||
<Status service={service} />
|
||||
</Disclosure.Button>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Item from "components/services/item";
|
||||
|
||||
export default function List({ services }) {
|
||||
return (
|
||||
<ul role="list" className="mt-3 flex flex-col">
|
||||
<ul className="mt-3 flex flex-col">
|
||||
{services.map((service) => (
|
||||
<Item key={service.name} service={service} />
|
||||
))}
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function Status({ service }) {
|
||||
const { data, error } = useSWR(
|
||||
`/api/docker/status/${service.container}/${service.server || ""}`
|
||||
);
|
||||
const { data, error } = useSWR(`/api/docker/status/${service.container}/${service.server || ""}`);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="w-3 h-3 bg-rose-300 dark:bg-rose-500 rounded-full" />
|
||||
);
|
||||
return <div className="w-3 h-3 bg-rose-300 dark:bg-rose-500 rounded-full" />;
|
||||
}
|
||||
|
||||
if (data && data.status === "running") {
|
||||
return (
|
||||
<div className="w-3 h-3 bg-emerald-300 dark:bg-emerald-500 rounded-full" />
|
||||
);
|
||||
return <div className="w-3 h-3 bg-emerald-300 dark:bg-emerald-500 rounded-full" />;
|
||||
}
|
||||
|
||||
if (data && data.status === "not found") {
|
||||
return (
|
||||
<>
|
||||
<div className="h-2.5 w-2.5 bg-orange-400/50 dark:bg-yellow-200/40 -rotate-45"></div>
|
||||
</>
|
||||
);
|
||||
return <div className="h-2.5 w-2.5 bg-orange-400/50 dark:bg-yellow-200/40 -rotate-45" />;
|
||||
}
|
||||
|
||||
return <div className="w-3 h-3 bg-black/20 dark:bg-white/40 rounded-full" />;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Sonarr from "./widgets/service/sonarr";
|
||||
import Radarr from "./widgets/service/radarr";
|
||||
import Ombi from "./widgets/service/ombi";
|
||||
@@ -11,6 +13,9 @@ import Jellyfin from "./widgets/service/jellyfin";
|
||||
import Speedtest from "./widgets/service/speedtest";
|
||||
import Traefik from "./widgets/service/traefik";
|
||||
import Jellyseerr from "./widgets/service/jellyseerr";
|
||||
import Overseerr from "./widgets/service/overseerr";
|
||||
import Npm from "./widgets/service/npm";
|
||||
import Tautulli from "./widgets/service/tautulli";
|
||||
|
||||
const widgetMappings = {
|
||||
docker: Docker,
|
||||
@@ -25,10 +30,15 @@ const widgetMappings = {
|
||||
rutorrent: Rutorrent,
|
||||
speedtest: Speedtest,
|
||||
traefik: Traefik,
|
||||
jellyseerr: Jellyseerr
|
||||
jellyseerr: Jellyseerr,
|
||||
overseerr: Overseerr,
|
||||
npm: Npm,
|
||||
tautulli: Tautulli,
|
||||
};
|
||||
|
||||
export default function Widget({ service }) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
const ServiceWidget = widgetMappings[service.widget.type];
|
||||
|
||||
if (ServiceWidget) {
|
||||
@@ -37,9 +47,7 @@ export default function Widget({ service }) {
|
||||
|
||||
return (
|
||||
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
|
||||
<div className="font-thin text-sm">
|
||||
Missing Widget Type: <strong>{service.widget.type}</strong>
|
||||
</div>
|
||||
<div className="font-thin text-sm">{t("widget.missing_type", { type: service.widget.type })}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +1,38 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import calculateCPUPercent from "utils/stats-helpers";
|
||||
|
||||
export default function Docker({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(
|
||||
`/api/docker/status/${config.container}/${config.server || ""}`,
|
||||
{
|
||||
refreshInterval: 1500,
|
||||
refreshInterval: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(
|
||||
`/api/docker/stats/${config.container}/${config.server || ""}`,
|
||||
{
|
||||
refreshInterval: 1500,
|
||||
refreshInterval: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
if (statsError || statusError) {
|
||||
return <Widget error="Error Fetching Data" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (statusData && statusData.status !== "running") {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Status" value="Offline" />
|
||||
<Block label={t("widget.status")} value={t("docker.offline")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -37,22 +40,22 @@ export default function Docker({ service }) {
|
||||
if (!statsData || !statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="CPU" />
|
||||
<Block label="MEM" />
|
||||
<Block label="RX" />
|
||||
<Block label="TX" />
|
||||
<Block label={t("docker.cpu")} />
|
||||
<Block label={t("docker.mem")} />
|
||||
<Block label={t("docker.rx")} />
|
||||
<Block label={t("docker.tx")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="CPU" value={`${calculateCPUPercent(statsData.stats)}%`} />
|
||||
<Block label="MEM" value={formatBytes(statsData.stats.memory_stats.usage, 0)} />
|
||||
<Block label={t("docker.cpu")} value={t("common.percent", { value: calculateCPUPercent(statsData.stats) })} />
|
||||
<Block label={t("docker.mem")} value={t("common.bytes", { value: statsData.stats.memory_stats.usage })} />
|
||||
{statsData.stats.networks && (
|
||||
<>
|
||||
<Block label="RX" value={formatBytes(statsData.stats.networks.eth0.rx_bytes, 0)} />
|
||||
<Block label="TX" value={formatBytes(statsData.stats.networks.eth0.tx_bytes, 0)} />
|
||||
<Block label={t("docker.rx")} value={t("common.bytes", { value: statsData.stats.networks.eth0.rx_bytes })} />
|
||||
<Block label={t("docker.tx")} value={t("common.bytes", { value: statsData.stats.networks.eth0.tx_bytes })} />
|
||||
</>
|
||||
)}
|
||||
</Widget>
|
||||
|
||||
@@ -1,45 +1,212 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill } from "react-icons/bs";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
export default function Emby({ service, title = "Emby" }) {
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
function ticksToTime(ticks) {
|
||||
const milliseconds = ticks / 10000;
|
||||
const seconds = Math.floor((milliseconds / 1000) % 60);
|
||||
const minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
|
||||
const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
|
||||
return { hours, minutes, seconds };
|
||||
}
|
||||
|
||||
function ticksToString(ticks) {
|
||||
const { hours, minutes, seconds } = ticksToTime(ticks);
|
||||
const parts = [];
|
||||
if (hours > 0) {
|
||||
parts.push(hours);
|
||||
}
|
||||
parts.push(minutes);
|
||||
parts.push(seconds);
|
||||
|
||||
return parts.map((part) => part.toString().padStart(2, "0")).join(":");
|
||||
}
|
||||
|
||||
function SingleSessionEntry({ playCommand, session }) {
|
||||
const {
|
||||
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
const percent = (PositionTicks / RunTimeTicks) * 100;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div className="text-xs z-10 self-center ml-2">
|
||||
<span>
|
||||
{Name}
|
||||
{SeriesName && ` - ${SeriesName}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grow" />
|
||||
<div className="self-center text-xs flex justify-end mr-1">{IsMuted && <BsVolumeMuteFill />}</div>
|
||||
</div>
|
||||
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div
|
||||
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{IsPaused && (
|
||||
<BsFillPlayFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Unpause");
|
||||
}}
|
||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||
/>
|
||||
)}
|
||||
{!IsPaused && (
|
||||
<BsPauseFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Pause");
|
||||
}}
|
||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-2">{ticksToString(PositionTicks)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionEntry({ playCommand, session }) {
|
||||
const {
|
||||
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
const percent = (PositionTicks / RunTimeTicks) * 100;
|
||||
|
||||
return (
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div
|
||||
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{IsPaused && (
|
||||
<BsFillPlayFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Unpause");
|
||||
}}
|
||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||
/>
|
||||
)}
|
||||
{!IsPaused && (
|
||||
<BsPauseFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Pause");
|
||||
}}
|
||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{Name}
|
||||
{SeriesName && ` - ${SeriesName}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-1">{IsMuted && <BsVolumeMuteFill />}</div>
|
||||
<div className="self-center text-xs flex justify-end mr-2">{ticksToString(PositionTicks)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Emby({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, key } = config;
|
||||
return `${url}/emby/${endpoint}?api_key=${key}`;
|
||||
}
|
||||
|
||||
const { data: sessionsData, error: sessionsError } = useSWR(buildApiUrl(`Sessions`), {
|
||||
refreshInterval: 1000,
|
||||
const {
|
||||
data: sessionsData,
|
||||
error: sessionsError,
|
||||
mutate: sessionMutate,
|
||||
} = useSWR(formatApiUrl(config, "Sessions"), {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
async function handlePlayCommand(session, command) {
|
||||
const url = formatApiUrl(config, `Sessions/${session.Id}/Playing/${command}`);
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
}).then(() => {
|
||||
sessionMutate();
|
||||
});
|
||||
}
|
||||
|
||||
if (sessionsError) {
|
||||
return <Widget error={`${title} API Error`} />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!sessionsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" />
|
||||
<Block label="Transcoding" />
|
||||
<Block label="Bitrate" />
|
||||
</Widget>
|
||||
<div className="flex flex-col pb-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const playing = sessionsData.filter((session) => session.hasOwnProperty("NowPlayingItem"));
|
||||
const transcoding = sessionsData.filter(
|
||||
(session) => session.hasOwnProperty("PlayState") && session.PlayState.PlayMethod === "Transcode"
|
||||
);
|
||||
const bitrate = playing.reduce((acc, session) => acc + session.NowPlayingItem.Bitrate, 0);
|
||||
const playing = sessionsData
|
||||
.filter((session) => session?.NowPlayingItem)
|
||||
.sort((a, b) => {
|
||||
if (a.PlayState.PositionTicks > b.PlayState.PositionTicks) {
|
||||
return 1;
|
||||
}
|
||||
if (a.PlayState.PositionTicks < b.PlayState.PositionTicks) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (playing.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col pb-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">{t("emby.no_active")}</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (playing.length === 1) {
|
||||
const session = playing[0];
|
||||
return (
|
||||
<div className="flex flex-col pb-1">
|
||||
<SingleSessionEntry
|
||||
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
|
||||
session={session}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" value={playing.length} />
|
||||
<Block label="Transcoding" value={transcoding.length} />
|
||||
<Block label="Bitrate" value={`${Math.round((bitrate / 1024 / 1024) * 100) / 100} Mbps`} />
|
||||
</Widget>
|
||||
<div className="flex flex-col pb-1">
|
||||
{playing.map((session) => (
|
||||
<SessionEntry
|
||||
key={session.Id}
|
||||
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
|
||||
session={session}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Emby from "./emby";
|
||||
|
||||
// Jellyfin and Emby share the same API, so proxy the Emby widget to Jellyfin.
|
||||
export default function Jellyfin({ service }) {
|
||||
return <Emby service={service} title="Jellyfin" />;
|
||||
return <Emby service={service} />;
|
||||
}
|
||||
|
||||
@@ -1,51 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Jellyseerr({ service }) {
|
||||
const config = service.widget;
|
||||
const { t } = useTranslation();
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url } = config;
|
||||
const reqUrl = new URL(`/api/v1/${endpoint}`, url);
|
||||
return `/api/proxy?url=${encodeURIComponent(reqUrl)}`;
|
||||
}
|
||||
const config = service.widget;
|
||||
|
||||
const fetcher = async (url) => {
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
withCredentials: true,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"X-Api-Key": `${config.key}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
return await res.json();
|
||||
};
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(buildApiUrl(`request/count`), fetcher);
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error="Jellyseerr API Error" />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" />
|
||||
<Block label="Approved" />
|
||||
<Block label="Available" />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
if (statsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" value={statsData.pending} />
|
||||
<Block label="Approved" value={statsData.approved} />
|
||||
<Block label="Available" value={statsData.available} />
|
||||
</Widget>
|
||||
<Widget>
|
||||
<Block label={t("jellyseerr.pending")} />
|
||||
<Block label={t("jellyseerr.approved")} />
|
||||
<Block label={t("jellyseerr.available")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("jellyseerr.pending")} value={statsData.pending} />
|
||||
<Block label={t("jellyseerr.approved")} value={statsData.approved} />
|
||||
<Block label={t("jellyseerr.available")} value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
41
src/components/services/widgets/service/npm.jsx
Normal file
41
src/components/services/widgets/service/npm.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Npm({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts"));
|
||||
|
||||
if (infoError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!infoData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("npm.enabled")} />
|
||||
<Block label={t("npm.disabled")} />
|
||||
<Block label={t("npm.total")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const enabled = infoData.filter((c) => c.enabled === 1).length;
|
||||
const disabled = infoData.filter((c) => c.enabled === 0).length;
|
||||
const total = infoData.length;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("npm.enabled")} value={enabled} />
|
||||
<Block label={t("npm.disabled")} value={disabled} />
|
||||
<Block label={t("npm.total")} value={total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +1,43 @@
|
||||
import useSWR from "swr";
|
||||
import { JSONRPCClient } from "json-rpc-2.0";
|
||||
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Nzbget({ service }) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const constructedUrl = new URL(config.url);
|
||||
constructedUrl.pathname = "jsonrpc";
|
||||
|
||||
const client = new JSONRPCClient((jsonRPCRequest) =>
|
||||
fetch(constructedUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: `Basic ${btoa(`${config.username}:${config.password}`)}`,
|
||||
},
|
||||
body: JSON.stringify(jsonRPCRequest),
|
||||
}).then(async (response) => {
|
||||
if (response.status === 200) {
|
||||
const jsonRPCResponse = await response.json();
|
||||
return client.receive(jsonRPCResponse);
|
||||
} else if (jsonRPCRequest.id !== undefined) {
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(
|
||||
"status",
|
||||
(resource) => {
|
||||
return client.request(resource).then((response) => response);
|
||||
},
|
||||
{
|
||||
refreshInterval: 1000,
|
||||
}
|
||||
);
|
||||
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status"));
|
||||
|
||||
if (statusError) {
|
||||
return <Widget error="Nzbget API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Rate" />
|
||||
<Block label="Remaining" />
|
||||
<Block label="Downloaded" />
|
||||
<Block label={t("nzbget.rate")} />
|
||||
<Block label={t("nzbget.remaining")} />
|
||||
<Block label={t("nzbget.downloaded")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Rate" value={`${formatBytes(statusData.DownloadRate)}/s`} />
|
||||
<Block label="Remaining" value={`${Math.round((statusData.RemainingSizeMB / 1024) * 100) / 100} GB`} />
|
||||
<Block label="Downloaded" value={`${Math.round((statusData.DownloadedSizeMB / 1024) * 100) / 100} GB`} />
|
||||
<Block label={t("nzbget.rate")} value={t("common.bitrate", { value: statusData.DownloadRate })} />
|
||||
<Block
|
||||
label={t("nzbget.remaining")}
|
||||
value={t("common.bytes", { value: statusData.RemainingSizeMB * 1024 * 1024 })}
|
||||
/>
|
||||
<Block
|
||||
label={t("nzbget.downloaded")}
|
||||
value={t("common.bytes", { value: statusData.DownloadedSizeMB * 1024 * 1024 })}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Ombi({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url } = config;
|
||||
return `${url}/api/v1/${endpoint}`;
|
||||
}
|
||||
|
||||
const fetcher = (url) => {
|
||||
return fetch(url, {
|
||||
method: "GET",
|
||||
withCredentials: true,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
ApiKey: `${config.key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
};
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(buildApiUrl(`Request/count`), fetcher);
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error="Ombi API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" />
|
||||
<Block label="Approved" />
|
||||
<Block label="Available" />
|
||||
<Block label={t("ombi.pending")} />
|
||||
<Block label={t("ombi.approved")} />
|
||||
<Block label={t("ombi.available")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" value={statsData.pending} />
|
||||
<Block label="Approved" value={statsData.approved} />
|
||||
<Block label="Available" value={statsData.available} />
|
||||
<Block label={t("ombi.pending")} value={statsData.pending} />
|
||||
<Block label={t("ombi.approved")} value={statsData.approved} />
|
||||
<Block label={t("ombi.available")} value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
37
src/components/services/widgets/service/overseerr.jsx
Normal file
37
src/components/services/widgets/service/overseerr.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Overseerr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("overseerr.pending")} />
|
||||
<Block label={t("overseerr.approved")} />
|
||||
<Block label={t("overseerr.available")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("overseerr.pending")} value={statsData.pending} />
|
||||
<Block label={t("overseerr.approved")} value={statsData.approved} />
|
||||
<Block label={t("overseerr.available")} value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Pihole({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, proxy } = config;
|
||||
|
||||
if (proxy) {
|
||||
const fullUrl = `${url}/admin/${endpoint}`;
|
||||
return "/api/proxy?url=" + encodeURIComponent(fullUrl);
|
||||
}
|
||||
|
||||
return `${url}/admin/${endpoint}`;
|
||||
}
|
||||
|
||||
const { data: piholeData, error: piholeError } = useSWR(buildApiUrl("api.php"));
|
||||
const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php"));
|
||||
|
||||
if (piholeError) {
|
||||
return <Widget error="PiHole API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!piholeData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Queries" />
|
||||
<Block label="Blocked" />
|
||||
<Block label="Gravity" />
|
||||
<Block label={t("pihole.queries")} />
|
||||
<Block label={t("pihole.blocked")} />
|
||||
<Block label={t("pihole.gravity")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Queries" value={piholeData.dns_queries_today.toLocaleString()} />
|
||||
<Block label="Blocked" value={piholeData.ads_blocked_today.toLocaleString()} />
|
||||
<Block label="Gravity" value={piholeData.domains_being_blocked.toLocaleString()} />
|
||||
<Block label={t("pihole.queries")} value={t("common.number", { value: piholeData.dns_queries_today })} />
|
||||
<Block label={t("pihole.blocked")} value={t("common.number", { value: piholeData.ads_blocked_today })} />
|
||||
<Block label={t("pihole.gravity")} value={t("common.number", { value: piholeData.domains_being_blocked })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,48 +1,34 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Portainer({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, env } = config;
|
||||
const reqUrl = new URL(`/api/endpoints/${env}/${endpoint}`, url);
|
||||
return `/api/proxy?url=${encodeURIComponent(reqUrl)}`;
|
||||
}
|
||||
|
||||
const fetcher = async (url) => {
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
withCredentials: true,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"X-API-Key": `${config.key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return await res.json();
|
||||
};
|
||||
|
||||
const { data: containersData, error: containersError } = useSWR(buildApiUrl(`docker/containers/json?all=1`), fetcher);
|
||||
const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`));
|
||||
|
||||
if (containersError) {
|
||||
return <Widget error="Portainer API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!containersData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Running" />
|
||||
<Block label="Stopped" />
|
||||
<Block label="Total" />
|
||||
<Block label={t("portainer.running")} />
|
||||
<Block label={t("portainer.stopped")} />
|
||||
<Block label={t("portainer.total")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
if (containersData.error) {
|
||||
return <Widget error="Portainer API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
const running = containersData.filter((c) => c.State === "running").length;
|
||||
@@ -51,9 +37,9 @@ export default function Portainer({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Running" value={running} />
|
||||
<Block label="Stopped" value={stopped} />
|
||||
<Block label="Total" value={total} />
|
||||
<Block label={t("portainer.running")} value={running} />
|
||||
<Block label={t("portainer.stopped")} value={stopped} />
|
||||
<Block label={t("portainer.total")} value={total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Radarr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, key } = config;
|
||||
return `${url}/api/v3/${endpoint}?apikey=${key}`;
|
||||
}
|
||||
|
||||
const { data: moviesData, error: moviesError } = useSWR(buildApiUrl("movie"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue/status"));
|
||||
const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status"));
|
||||
|
||||
if (moviesError || queuedError) {
|
||||
return <Widget error="Radarr API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!moviesData || !queuedData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" />
|
||||
<Block label="Queued" />
|
||||
<Block label="Movies" />
|
||||
<Block label={t("radarr.wanted")} />
|
||||
<Block label={t("radarr.queued")} />
|
||||
<Block label={t("radarr.movies")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -33,9 +33,9 @@ export default function Radarr({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" value={wanted.length} />
|
||||
<Block label="Queued" value={queuedData.totalCount} />
|
||||
<Block label="Movies" value={moviesData.length} />
|
||||
<Block label={t("radarr.wanted")} value={wanted.length} />
|
||||
<Block label={t("radarr.queued")} value={queuedData.totalCount} />
|
||||
<Block label={t("radarr.movies")} value={have.length} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,62 +1,43 @@
|
||||
import useSWR from "swr";
|
||||
import RuTorrent from "rutorrent-promise";
|
||||
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Rutorrent({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl() {
|
||||
const { url, username, password } = config;
|
||||
|
||||
const options = {
|
||||
url: `${url}/plugins/httprpc/action.php`,
|
||||
};
|
||||
|
||||
if (username && password) {
|
||||
options.username = username;
|
||||
options.password = password;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(options);
|
||||
|
||||
return `/api/widgets/rutorrent?${params.toString()}`;
|
||||
}
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(buildApiUrl());
|
||||
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config));
|
||||
|
||||
if (statusError) {
|
||||
return <Widget error="Nzbget API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Active" />
|
||||
<Block label="Upload" />
|
||||
<Block label="Download" />
|
||||
<Block label={t("rutorrent.active")} />
|
||||
<Block label={t("rutorrent.upload")} />
|
||||
<Block label={t("rutorrent.download")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const upload = statusData.reduce((acc, torrent) => {
|
||||
return acc + parseInt(torrent["d.get_up_rate"]);
|
||||
}, 0);
|
||||
const upload = statusData.reduce((acc, torrent) => acc + parseInt(torrent["d.get_up_rate"], 10), 0);
|
||||
|
||||
const download = statusData.reduce((acc, torrent) => {
|
||||
return acc + parseInt(torrent["d.get_down_rate"]);
|
||||
}, 0);
|
||||
const download = statusData.reduce((acc, torrent) => acc + parseInt(torrent["d.get_down_rate"], 10), 0);
|
||||
|
||||
const active = statusData.filter((torrent) => torrent["d.get_state"] === "1");
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Active" value={active.length} />
|
||||
<Block label="Upload" value={`${formatBytes(upload)}/s`} />
|
||||
<Block label="Download" value={`${formatBytes(download)}/s`} />
|
||||
<Block label={t("rutorrent.active")} value={active.length} />
|
||||
<Block label={t("rutorrent.upload")} value={t("common.bitrate", { value: upload })} />
|
||||
<Block label={t("rutorrent.download")} value={t("common.bitrate", { value: download })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Sonarr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, key } = config;
|
||||
return `${url}/api/v3/${endpoint}?apikey=${key}`;
|
||||
}
|
||||
|
||||
const { data: wantedData, error: wantedError } = useSWR(buildApiUrl("wanted/missing"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue"));
|
||||
const { data: seriesData, error: seriesError } = useSWR(buildApiUrl("series"));
|
||||
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue"));
|
||||
const { data: seriesData, error: seriesError } = useSWR(formatApiUrl(config, "series"));
|
||||
|
||||
if (wantedError || queuedError || seriesError) {
|
||||
return <Widget error="Sonar API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!wantedData || !queuedData || !seriesData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" />
|
||||
<Block label="Queued" />
|
||||
<Block label="Series" />
|
||||
<Block label={t("sonarr.wanted")} />
|
||||
<Block label={t("sonarr.queued")} />
|
||||
<Block label={t("sonarr.series")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" value={wantedData.totalRecords} />
|
||||
<Block label="Queued" value={queuedData.totalRecords} />
|
||||
<Block label="Series" value={seriesData.length} />
|
||||
<Block label={t("sonarr.wanted")} value={wantedData.totalRecords} />
|
||||
<Block label={t("sonarr.queued")} value={queuedData.totalRecords} />
|
||||
<Block label={t("sonarr.series")} value={seriesData.length} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatBits } from "utils/stats-helpers";
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Speedtest({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url } = config;
|
||||
return `${url}/api/${endpoint}`;
|
||||
}
|
||||
|
||||
const { data: speedtestData, error: speedtestError } = useSWR(buildApiUrl("speedtest/latest"));
|
||||
const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest"));
|
||||
|
||||
if (speedtestError) {
|
||||
return <Widget error="Speedtest API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!speedtestData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Download" />
|
||||
<Block label="Upload" />
|
||||
<Block label="Ping" />
|
||||
<Block label={t("speedtest.download")} />
|
||||
<Block label={t("speedtest.upload")} />
|
||||
<Block label={t("speedtest.ping")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Download" value={`${formatBits(speedtestData.data.download * 1024 * 1024)}ps`} />
|
||||
<Block label="Upload" value={`${formatBits(speedtestData.data.upload * 1024 * 1024)}ps`} />
|
||||
<Block label="Ping" value={`${speedtestData.data.ping} ms`} />
|
||||
<Block
|
||||
label={t("speedtest.download")}
|
||||
value={t("common.bitrate", { value: speedtestData.data.download * 1024 * 1024 })}
|
||||
/>
|
||||
<Block
|
||||
label={t("speedtest.upload")}
|
||||
value={t("common.bitrate", { value: speedtestData.data.upload * 1024 * 1024 })}
|
||||
/>
|
||||
<Block
|
||||
label={t("speedtest.ping")}
|
||||
value={t("common.ms", { value: speedtestData.data.ping, style: "unit", unit: "millisecond" })}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
157
src/components/services/widgets/service/tautulli.jsx
Normal file
157
src/components/services/widgets/service/tautulli.jsx
Normal file
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable camelcase */
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsFillPlayFill, BsPauseFill } from "react-icons/bs";
|
||||
|
||||
import Widget from "../widget";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
function millisecondsToTime(milliseconds) {
|
||||
const seconds = Math.floor((milliseconds / 1000) % 60);
|
||||
const minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
|
||||
const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
|
||||
return { hours, minutes, seconds };
|
||||
}
|
||||
|
||||
function millisecondsToString(milliseconds) {
|
||||
const { hours, minutes, seconds } = millisecondsToTime(milliseconds);
|
||||
const parts = [];
|
||||
if (hours > 0) {
|
||||
parts.push(hours);
|
||||
}
|
||||
parts.push(minutes);
|
||||
parts.push(seconds);
|
||||
|
||||
return parts.map((part) => part.toString().padStart(2, "0")).join(":");
|
||||
}
|
||||
|
||||
function SingleSessionEntry({ session }) {
|
||||
const { full_title, duration, view_offset, progress_percent, state, year, grandparent_year } = session;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div className="text-xs z-10 self-center ml-2">
|
||||
<span>{full_title}</span>
|
||||
</div>
|
||||
<div className="grow" />
|
||||
<div className="self-center text-xs flex justify-end mr-2">{year || grandparent_year}</div>
|
||||
</div>
|
||||
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div
|
||||
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
|
||||
style={{
|
||||
width: `${progress_percent}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{state === "paused" && (
|
||||
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
{state !== "paused" && (
|
||||
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
</div>
|
||||
<div className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-2">
|
||||
{millisecondsToString(view_offset)} / {millisecondsToString(duration)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionEntry({ session }) {
|
||||
const { full_title, view_offset, progress_percent, state } = session;
|
||||
|
||||
return (
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div
|
||||
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
|
||||
style={{
|
||||
width: `${progress_percent}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{state === "paused" && (
|
||||
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
{state !== "paused" && (
|
||||
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
<span>{full_title}</span>
|
||||
</div>
|
||||
<div className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-2">{millisecondsToString(view_offset)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Tautulli({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: activityData, error: activityError } = useSWR(formatApiUrl(config, "get_activity"), {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
if (activityError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!activityData) {
|
||||
return (
|
||||
<div className="flex flex-col pb-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const playing = activityData.response.data.sessions.sort((a, b) => {
|
||||
if (a.view_offset > b.view_offset) {
|
||||
return 1;
|
||||
}
|
||||
if (a.view_offset < b.view_offset) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (playing.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col pb-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">{t("tautulli.no_active")}</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (playing.length === 1) {
|
||||
const session = playing[0];
|
||||
return (
|
||||
<div className="flex flex-col pb-1">
|
||||
<SingleSessionEntry session={session} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col pb-1">
|
||||
{playing.map((session) => (
|
||||
<SessionEntry key={session.Id} session={session} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Traefik({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url } = config;
|
||||
const fullUrl = `${url}/api/${endpoint}`;
|
||||
return `/api/proxy?url=${encodeURIComponent(fullUrl)}`;
|
||||
}
|
||||
|
||||
const { data: traefikData, error: traefikError } = useSWR(buildApiUrl("overview"));
|
||||
const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview"));
|
||||
|
||||
if (traefikError) {
|
||||
return <Widget error="Traefik API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!traefikData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Routers" />
|
||||
<Block label="Services" />
|
||||
<Block label="Middleware" />
|
||||
<Block label={t("traefik.routers")} />
|
||||
<Block label={t("traefik.services")} />
|
||||
<Block label={t("traefik.middleware")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Routers" value={traefikData.http.routers.total} />
|
||||
<Block label="Services" value={traefikData.http.services.total} />
|
||||
<Block label="Middleware" value={traefikData.http.middlewares.total} />
|
||||
<Block label={t("traefik.routers")} value={traefikData.http.routers.total} />
|
||||
<Block label={t("traefik.services")} value={traefikData.http.services.total} />
|
||||
<Block label={t("traefik.middleware")} value={traefikData.http.middlewares.total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { useContext } from "react";
|
||||
import {
|
||||
MdDarkMode,
|
||||
MdLightMode,
|
||||
MdToggleOff,
|
||||
MdToggleOn,
|
||||
} from "react-icons/md";
|
||||
import { MdDarkMode, MdLightMode, MdToggleOff, MdToggleOn } from "react-icons/md";
|
||||
|
||||
import { ThemeContext } from "utils/theme-context";
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import WeatherApi from "components/widgets/weather/weather";
|
||||
import OpenWeatherMap from "components/widgets/openweathermap/weather";
|
||||
import Resources from "components/widgets/resources/resources";
|
||||
import Search from "components/widgets/search/search";
|
||||
|
||||
const widgetMappings = {
|
||||
weather: WeatherApi, // This key will be deprecated in the future
|
||||
weatherapi: WeatherApi,
|
||||
openweathermap: OpenWeatherMap,
|
||||
resources: Resources,
|
||||
search: Search,
|
||||
};
|
||||
|
||||
export default function Widget({ widget }) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import mapIcon from "utils/owm-condition-map";
|
||||
|
||||
export default function Icon({ condition, timeOfDay }) {
|
||||
const Icon = mapIcon(condition, timeOfDay);
|
||||
const IconComponent = mapIcon(condition, timeOfDay);
|
||||
|
||||
return <Icon className="w-10 h-10 text-theme-800 dark:text-theme-200"></Icon>;
|
||||
return <IconComponent className="w-10 h-10 text-theme-800 dark:text-theme-200" />;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { WiCloudDown } from "react-icons/wi";
|
||||
import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Icon from "./icon";
|
||||
|
||||
export default function OpenWeatherMap({ options }) {
|
||||
function Widget({ options }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(
|
||||
`/api/widgets/openweathermap?lat=${options.latitude}&lon=${options.longitude}&apiKey=${options.apiKey}&duration=${options.cache}&units=${options.units}`
|
||||
`/api/widgets/openweathermap?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`
|
||||
);
|
||||
|
||||
if (error || data?.cod == 401) {
|
||||
if (error || data?.cod === 401 || data?.error) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">API</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Error</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,15 +31,25 @@ export default function OpenWeatherMap({ options }) {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div className="flex flex-row items-center"></div>;
|
||||
return (
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.updating")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.wait")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
return <div className="flex flex-row items-center"></div>;
|
||||
}
|
||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<Icon
|
||||
@@ -44,13 +60,63 @@ export default function OpenWeatherMap({ options }) {
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||
{options.label && `${options.label}, `}
|
||||
{data.main.temp.toFixed(1)}°
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{data.weather[0].description.charAt(0).toUpperCase() + data.weather[0].description.slice(1)}
|
||||
{t("common.number", { value: data.main.temp, style: "unit", unit })}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.weather[0].description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OpenWeatherMap({ options }) {
|
||||
const { t } = useTranslation();
|
||||
const [location, setLocation] = useState(false);
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
|
||||
if (!location && options.latitude && options.longitude) {
|
||||
setLocation({ latitude: options.latitude, longitude: options.longitude });
|
||||
}
|
||||
|
||||
const requestLocation = () => {
|
||||
setRequesting(true);
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
setLocation({ latitude: position.coords.latitude, longitude: position.coords.longitude });
|
||||
setRequesting(false);
|
||||
},
|
||||
() => {
|
||||
setRequesting(false);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
maximumAge: 1000 * 60 * 60 * 3,
|
||||
timeout: 1000 * 30,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (!requesting && !location) requestLocation();
|
||||
|
||||
if (!location) {
|
||||
return (
|
||||
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
{requesting ? (
|
||||
<MdLocationSearching className="w-6 h-6 text-theme-800 dark:text-theme-200 animate-pulse" />
|
||||
) : (
|
||||
<MdLocationDisabled className="w-6 h-6 text-theme-800 dark:text-theme-200" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.current")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.allow")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <Widget options={{ ...location, ...options }} />;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import useSWR from "swr";
|
||||
import { FiCpu } from "react-icons/fi";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Cpu() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Resources</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Error</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -21,26 +25,26 @@ export default function Cpu() {
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- Usage</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- Load</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const percent = data.cpu.usage;
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
<span className="whitespace-pre">{`${Math.round(data.cpu.usage)}%`.padEnd(3, " ")} Usage</span>
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{`${(Math.round(data.cpu.load * 100) / 100).toFixed(1)}`.padEnd(3, " ")} Load
|
||||
</span>
|
||||
<div className="flex flex-col ml-3 text-left font-mono min-w-[80px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{t("common.number", { value: data.cpu.usage, style: "unit", unit: "percent", maximumFractionDigits: 0 })}{" "}
|
||||
{t("docker.cpu")}
|
||||
</div>
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import useSWR from "swr";
|
||||
import { FiHardDrive } from "react-icons/fi";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Disk({ options }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=disk&target=${options.disk}`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Resources</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Error</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -22,26 +25,28 @@ export default function Disk({ options }) {
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- Free</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- Used</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const percent = Math.round((data.drive.usedGb / data.drive.totalGb) * 100);
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5 group">
|
||||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{formatBytes(data.drive.freeGb * 1024 * 1024 * 1024)} Free
|
||||
<div className="flex flex-col ml-3 text-left min-w-[80px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
||||
{t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })} {t("resources.free")}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{formatBytes(data.drive.usedGb * 1024 * 1024 * 1024)} Used
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs hidden group-hover:block">
|
||||
{t("common.bytes", { value: data.drive.totalGb * 1024 * 1024 * 1024 })} {t("resources.total")}
|
||||
</span>
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import useSWR from "swr";
|
||||
import { FaMemory } from "react-icons/fa";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Memory() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=memory`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Resources</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Error</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -22,26 +25,28 @@ export default function Memory() {
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- GB Used</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- GB Free</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const percent = Math.round((data.memory.usedMemMb / data.memory.totalMemMb) * 100);
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5 group">
|
||||
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{formatBytes(data.memory.usedMemMb * 1024 * 1024)} Used
|
||||
<div className="flex flex-col ml-3 text-left min-w-[80px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
||||
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024 })} {t("resources.free")}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{formatBytes(data.memory.freeMemMb * 1024 * 1024)} Free
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs hidden group-hover:block">
|
||||
{t("common.bytes", { value: data.memory.usedMemMb * 1024 * 1024 })} {t("resources.used")}
|
||||
</span>
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,19 +4,17 @@ import Memory from "./memory";
|
||||
|
||||
export default function Resources({ options }) {
|
||||
return (
|
||||
<>
|
||||
<div className="pr-2 flex flex-col">
|
||||
<div className="flex flex-row space-x-4">
|
||||
{options.disk && <Disk options={options} />}
|
||||
{options.cpu && <Cpu />}
|
||||
{options.memory && <Memory />}
|
||||
</div>
|
||||
{options.label && (
|
||||
<div className="border-t-2 border-theme-800 dark:border-theme-200 mt-1 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">
|
||||
{options.label}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center m-auto flex-wrap">
|
||||
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||
{options.cpu && <Cpu />}
|
||||
{options.memory && <Memory />}
|
||||
{Array.isArray(options.disk)
|
||||
? options.disk.map((disk) => <Disk key={disk} options={{ disk }} />)
|
||||
: options.disk && <Disk options={options} />}
|
||||
</div>
|
||||
</>
|
||||
{options.label && (
|
||||
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
12
src/components/widgets/resources/usage-bar.jsx
Normal file
12
src/components/widgets/resources/usage-bar.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export default function UsageBar({ percent }) {
|
||||
return (
|
||||
<div className="mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-white/20 backdrop-blur-md">
|
||||
<div
|
||||
className="bg-theme-800/70 h-1 rounded-full dark:bg-white/50"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
86
src/components/widgets/search/search.jsx
Normal file
86
src/components/widgets/search/search.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FiSearch } from "react-icons/fi";
|
||||
import { SiDuckduckgo, SiMicrosoftbing, SiGoogle } from "react-icons/si";
|
||||
|
||||
const providers = {
|
||||
google: {
|
||||
name: "Google",
|
||||
url: "https://www.google.com/search?q=",
|
||||
icon: SiGoogle,
|
||||
},
|
||||
duckduckgo: {
|
||||
name: "DuckDuckGo",
|
||||
url: "https://duckduckgo.com/?q=",
|
||||
icon: SiDuckduckgo,
|
||||
},
|
||||
bing: {
|
||||
name: "Bing",
|
||||
url: "https://www.bing.com/search?q=",
|
||||
icon: SiMicrosoftbing,
|
||||
},
|
||||
custom: {
|
||||
name: "Custom",
|
||||
url: false,
|
||||
icon: FiSearch,
|
||||
},
|
||||
};
|
||||
|
||||
export default function Search({ options }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const provider = providers[options.provider];
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
const q = encodeURIComponent(query);
|
||||
|
||||
if (provider.url) {
|
||||
window.open(`${provider.url}${q}`, options.target || "_blank");
|
||||
} else {
|
||||
window.open(`${options.url}${q}`, options.target || "_blank");
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.target.reset();
|
||||
setQuery("");
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="flex-col relative h-8 my-4 min-w-full md:min-w-fit grow mr-4" onSubmit={handleSubmit}>
|
||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
|
||||
<input
|
||||
type="text"
|
||||
className="
|
||||
overflow-hidden w-full h-full rounded-md
|
||||
text-xs text-theme-900 dark:text-white
|
||||
placeholder-theme-900 dark:placeholder-white/80
|
||||
bg-white/50 dark:bg-white/10
|
||||
focus:ring-theme-500 dark:focus:ring-white/50
|
||||
focus:border-theme-500 dark:focus:border-white/50
|
||||
border border-theme-300 dark:border-theme-200/50
|
||||
backdrop-blur-md"
|
||||
placeholder={t("search.placeholder")}
|
||||
onChange={(s) => setQuery(s.currentTarget.value)}
|
||||
required
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="
|
||||
absolute right-0.5 bottom-0.5 rounded-r-md px-4 py-2 border-1
|
||||
text-white font-medium text-sm
|
||||
bg-theme-600/40 dark:bg-white/10
|
||||
focus:ring-theme-500 dark:focus:ring-white/50"
|
||||
>
|
||||
<provider.icon className="text-white w-3 h-3" />
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import mapIcon from "utils/condition-map";
|
||||
|
||||
export default function Icon({ condition, timeOfDay }) {
|
||||
const Icon = mapIcon(condition, timeOfDay);
|
||||
const IconComponent = mapIcon(condition, timeOfDay);
|
||||
|
||||
return <Icon className="w-10 h-10 text-theme-800 dark:text-theme-200"></Icon>;
|
||||
return <IconComponent className="w-10 h-10 text-theme-800 dark:text-theme-200" />;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { WiCloudDown } from "react-icons/wi";
|
||||
import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Icon from "./icon";
|
||||
|
||||
export default function WeatherApi({ options }) {
|
||||
function Widget({ options }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(
|
||||
`/api/widgets/weather?lat=${options.latitude}&lon=${options.longitude}&apiKey=${options.apiKey}&duration=${options.cache}`
|
||||
`/api/widgets/weather?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">API</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Error</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,15 +31,25 @@ export default function WeatherApi({ options }) {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div className="flex flex-row items-center justify-end"></div>;
|
||||
return (
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.updating")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.wait")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
return <div className="flex flex-row items-center justify-end"></div>;
|
||||
}
|
||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<Icon condition={data.current.condition.code} timeOfDay={data.current.is_day ? "day" : "night"} />
|
||||
@@ -41,7 +57,11 @@ export default function WeatherApi({ options }) {
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||
{options.label && `${options.label}, `}
|
||||
{options.units === "metric" ? data.current.temp_c : data.current.temp_f}°
|
||||
{t("common.number", {
|
||||
value: options.units === "metric" ? data.current.temp_c : data.current.temp_f,
|
||||
style: "unit",
|
||||
unit,
|
||||
})}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.current.condition.text}</span>
|
||||
</div>
|
||||
@@ -49,3 +69,55 @@ export default function WeatherApi({ options }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WeatherApi({ options }) {
|
||||
const { t } = useTranslation();
|
||||
const [location, setLocation] = useState(false);
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
|
||||
if (!location && options.latitude && options.longitude) {
|
||||
setLocation({ latitude: options.latitude, longitude: options.longitude });
|
||||
}
|
||||
|
||||
const requestLocation = () => {
|
||||
setRequesting(true);
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
setLocation({ latitude: position.coords.latitude, longitude: position.coords.longitude });
|
||||
setRequesting(false);
|
||||
},
|
||||
() => {
|
||||
setRequesting(false);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
maximumAge: 1000 * 60 * 60 * 3,
|
||||
timeout: 1000 * 30,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (!requesting && !location) requestLocation();
|
||||
|
||||
if (!location) {
|
||||
return (
|
||||
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
{requesting ? (
|
||||
<MdLocationSearching className="w-6 h-6 text-theme-800 dark:text-theme-200 animate-pulse" />
|
||||
) : (
|
||||
<MdLocationDisabled className="w-6 h-6 text-theme-800 dark:text-theme-200" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.current")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.allow")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <Widget options={{ ...location, ...options }} />;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { SWRConfig } from "swr";
|
||||
|
||||
import "styles/globals.css";
|
||||
import "styles/weather-icons.css";
|
||||
import "styles/theme.css";
|
||||
|
||||
import "utils/i18n";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<SWRConfig
|
||||
@@ -11,7 +11,7 @@ export default function Document() {
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
<body className="w-full h-full bg-theme-50 dark:bg-theme-800 transition duration-150 ease-in-out">
|
||||
<body className="relative w-full h-full bg-theme-50 dark:bg-theme-800 transition duration-150 ease-in-out">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
@@ -1,6 +1,8 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import checkAndCopyConfig from "utils/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
@@ -11,17 +13,13 @@ export default async function handler(req, res) {
|
||||
const bookmarks = yaml.load(fileContents);
|
||||
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
const bookmarksArray = bookmarks.map((group) => {
|
||||
return {
|
||||
name: Object.keys(group)[0],
|
||||
bookmarks: group[Object.keys(group)[0]].map((entries) => {
|
||||
return {
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]][0],
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
const bookmarksArray = bookmarks.map((group) => ({
|
||||
name: Object.keys(group)[0],
|
||||
bookmarks: group[Object.keys(group)[0]].map((entries) => ({
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]][0],
|
||||
})),
|
||||
}));
|
||||
|
||||
res.send(bookmarksArray);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Docker from "dockerode";
|
||||
|
||||
import getDockerArguments from "utils/docker";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
@@ -13,7 +14,7 @@ export default async function handler(req, res) {
|
||||
}
|
||||
|
||||
try {
|
||||
const docker = new Docker(await getDockerArguments(containerServer));
|
||||
const docker = new Docker(getDockerArguments(containerServer));
|
||||
const containers = await docker.listContainers({
|
||||
all: true,
|
||||
});
|
||||
@@ -21,30 +22,30 @@ export default async function handler(req, res) {
|
||||
// bad docker connections can result in a <Buffer ...> object?
|
||||
// in any case, this ensures the result is the expected array
|
||||
if (!Array.isArray(containers)) {
|
||||
return res.status(500).send({
|
||||
res.status(500).send({
|
||||
error: "query failed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const containerNames = containers.map((container) => {
|
||||
return container.Names[0].replace(/^\//, "");
|
||||
});
|
||||
const containerNames = containers.map((container) => container.Names[0].replace(/^\//, ""));
|
||||
const containerExists = containerNames.includes(containerName);
|
||||
|
||||
if (!containerExists) {
|
||||
return res.status(200).send({
|
||||
res.status(200).send({
|
||||
error: "not found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const container = docker.getContainer(containerName);
|
||||
const stats = await container.stats({ stream: false });
|
||||
|
||||
return res.status(200).json({
|
||||
stats: stats,
|
||||
res.status(200).json({
|
||||
stats,
|
||||
});
|
||||
} catch {
|
||||
return res.status(500).send({
|
||||
res.status(500).send({
|
||||
error: "unknown error",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Docker from "dockerode";
|
||||
|
||||
import getDockerArguments from "utils/docker";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
@@ -12,7 +13,7 @@ export default async function handler(req, res) {
|
||||
}
|
||||
|
||||
try {
|
||||
const docker = new Docker(await getDockerArguments(containerServer));
|
||||
const docker = new Docker(getDockerArguments(containerServer));
|
||||
const containers = await docker.listContainers({
|
||||
all: true,
|
||||
});
|
||||
@@ -25,9 +26,7 @@ export default async function handler(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
const containerNames = containers.map((container) => {
|
||||
return container.Names[0].replace(/^\//, "");
|
||||
});
|
||||
const containerNames = containers.map((container) => container.Names[0].replace(/^\//, ""));
|
||||
const containerExists = containerNames.includes(containerName);
|
||||
|
||||
if (!containerExists) {
|
||||
|
||||
3
src/pages/api/healthcheck.js
Normal file
3
src/pages/api/healthcheck.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function handler(req, res) {
|
||||
res.send("up");
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import https from "https";
|
||||
|
||||
import getRawBody from "raw-body";
|
||||
|
||||
import { httpRequest, httpsRequest } from "utils/http";
|
||||
@@ -11,7 +12,8 @@ export const config = {
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const headers = ["X-API-Key", "Authorization"].reduce((obj, key) => {
|
||||
if (req.headers && req.headers.hasOwnProperty(key.toLowerCase())) {
|
||||
if (req.headers && Object.prototype.hasOwnProperty.call(req.headers, key.toLowerCase())) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
obj[key] = req.headers[key.toLowerCase()];
|
||||
}
|
||||
return obj;
|
||||
@@ -29,23 +31,9 @@ export default async function handler(req, res) {
|
||||
const [status, contentType, data] = await httpsRequest(url, {
|
||||
agent: httpsAgent,
|
||||
method: req.method,
|
||||
headers: headers,
|
||||
headers,
|
||||
body:
|
||||
req.method == "GET" || req.method == "HEAD"
|
||||
? null
|
||||
: await getRawBody(req, {
|
||||
encoding: "utf8",
|
||||
}),
|
||||
});
|
||||
|
||||
res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(data);
|
||||
} else {
|
||||
const [status, contentType, data] = await httpRequest(url, {
|
||||
method: req.method,
|
||||
headers: headers,
|
||||
body:
|
||||
req.method == "GET" || req.method == "HEAD"
|
||||
req.method === "GET" || req.method === "HEAD"
|
||||
? null
|
||||
: await getRawBody(req, {
|
||||
encoding: "utf8",
|
||||
@@ -55,4 +43,17 @@ export default async function handler(req, res) {
|
||||
res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
const [status, contentType, data] = await httpRequest(url, {
|
||||
method: req.method,
|
||||
headers,
|
||||
body:
|
||||
req.method === "GET" || req.method === "HEAD"
|
||||
? null
|
||||
: await getRawBody(req, {
|
||||
encoding: "utf8",
|
||||
}),
|
||||
});
|
||||
|
||||
res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
|
||||
8
src/pages/api/revalidate.js
Normal file
8
src/pages/api/revalidate.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default async function handler(req, res) {
|
||||
try {
|
||||
await res.revalidate("/");
|
||||
return res.json({ revalidated: true });
|
||||
} catch (err) {
|
||||
return res.status(500).send("Error revalidating");
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,41 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import yaml from "js-yaml";
|
||||
import checkAndCopyConfig from "utils/config";
|
||||
/* eslint-disable no-console */
|
||||
import { servicesFromConfig, servicesFromDocker, cleanServiceGroups } from "utils/service-helpers";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
checkAndCopyConfig("services.yaml");
|
||||
let discoveredServices;
|
||||
let configuredServices;
|
||||
|
||||
const servicesYaml = path.join(process.cwd(), "config", "services.yaml");
|
||||
const fileContents = await fs.readFile(servicesYaml, "utf8");
|
||||
const services = yaml.load(fileContents);
|
||||
try {
|
||||
discoveredServices = cleanServiceGroups(await servicesFromDocker());
|
||||
} catch {
|
||||
console.error("Failed to discover services, please check docker.yaml for errors");
|
||||
discoveredServices = [];
|
||||
}
|
||||
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
const servicesArray = services.map((group) => {
|
||||
return {
|
||||
name: Object.keys(group)[0],
|
||||
services: group[Object.keys(group)[0]].map((entries) => {
|
||||
return {
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]],
|
||||
};
|
||||
}),
|
||||
try {
|
||||
configuredServices = cleanServiceGroups(await servicesFromConfig());
|
||||
} catch {
|
||||
console.error("Failed to load services.yaml, please check for errors");
|
||||
configuredServices = [];
|
||||
}
|
||||
|
||||
const mergedGroupsNames = [
|
||||
...new Set([discoveredServices.map((group) => group.name), configuredServices.map((group) => group.name)].flat()),
|
||||
];
|
||||
|
||||
const mergedGroups = [];
|
||||
|
||||
mergedGroupsNames.forEach((groupName) => {
|
||||
const discoveredGroup = discoveredServices.find((group) => group.name === groupName) || { services: [] };
|
||||
const configuredGroup = configuredServices.find((group) => group.name === groupName) || { services: [] };
|
||||
|
||||
const mergedGroup = {
|
||||
name: groupName,
|
||||
services: [...discoveredGroup.services, ...configuredGroup.services].filter((service) => service),
|
||||
};
|
||||
|
||||
mergedGroups.push(mergedGroup);
|
||||
});
|
||||
|
||||
res.send(servicesArray);
|
||||
res.send(mergedGroups);
|
||||
}
|
||||
|
||||
38
src/pages/api/services/proxy.js
Normal file
38
src/pages/api/services/proxy.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import genericProxyHandler from "utils/proxies/generic";
|
||||
import credentialedProxyHandler from "utils/proxies/credentialed";
|
||||
import rutorrentProxyHandler from "utils/proxies/rutorrent";
|
||||
import nzbgetProxyHandler from "utils/proxies/nzbget";
|
||||
import npmProxyHandler from "utils/proxies/npm";
|
||||
|
||||
const serviceProxyHandlers = {
|
||||
// uses query param auth
|
||||
emby: genericProxyHandler,
|
||||
jellyfin: genericProxyHandler,
|
||||
pihole: genericProxyHandler,
|
||||
radarr: genericProxyHandler,
|
||||
sonarr: genericProxyHandler,
|
||||
speedtest: genericProxyHandler,
|
||||
tautulli: genericProxyHandler,
|
||||
traefik: genericProxyHandler,
|
||||
// uses X-API-Key header auth
|
||||
portainer: credentialedProxyHandler,
|
||||
jellyseerr: credentialedProxyHandler,
|
||||
overseerr: credentialedProxyHandler,
|
||||
ombi: credentialedProxyHandler,
|
||||
// super specific handlers
|
||||
rutorrent: rutorrentProxyHandler,
|
||||
nzbget: nzbgetProxyHandler,
|
||||
npm: npmProxyHandler,
|
||||
};
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { type } = req.query;
|
||||
|
||||
const serviceProxyHandler = serviceProxyHandlers[type];
|
||||
|
||||
if (serviceProxyHandler) {
|
||||
return serviceProxyHandler(req, res);
|
||||
}
|
||||
|
||||
return res.status(403).json({ error: "Unkown proxy service type" });
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import checkAndCopyConfig from "utils/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
@@ -11,12 +13,10 @@ export default async function handler(req, res) {
|
||||
const widgets = yaml.load(fileContents);
|
||||
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
const widgetsArray = widgets.map((group) => {
|
||||
return {
|
||||
type: Object.keys(group)[0],
|
||||
options: { ...group[Object.keys(group)[0]] },
|
||||
};
|
||||
});
|
||||
const widgetsArray = widgets.map((group) => ({
|
||||
type: Object.keys(group)[0],
|
||||
options: { ...group[Object.keys(group)[0]] },
|
||||
}));
|
||||
|
||||
res.send(widgetsArray);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import cachedFetch from "utils/cached-fetch";
|
||||
import { getSettings } from "utils/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { lat, lon, apiKey, duration, units } = req.query;
|
||||
const { latitude, longitude, units, provider, cache, lang } = req.query;
|
||||
let { apiKey } = req.query;
|
||||
|
||||
const api_url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=${units}`;
|
||||
if (!apiKey && !provider) {
|
||||
return res.status(400).json({ error: "Missing API key or provider" });
|
||||
}
|
||||
|
||||
res.send(await cachedFetch(api_url, duration));
|
||||
if (!apiKey && provider !== "openweathermap") {
|
||||
return res.status(400).json({ error: "Invalid provider for endpoint" });
|
||||
}
|
||||
|
||||
if (!apiKey && provider) {
|
||||
const settings = await getSettings();
|
||||
apiKey = settings?.providers?.openweathermap;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(400).json({ error: "Missing API key" });
|
||||
}
|
||||
|
||||
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}&lang=${lang}`;
|
||||
|
||||
return res.send(await cachedFetch(apiUrl, cache));
|
||||
}
|
||||
|
||||
@@ -1,26 +1,38 @@
|
||||
import { cpu, drive, mem, netstat } from "node-os-utils";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
import { cpu, drive, mem } from "node-os-utils";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { type, target } = req.query;
|
||||
|
||||
if (type == "cpu") {
|
||||
if (type === "cpu") {
|
||||
return res.status(200).json({
|
||||
cpu: {
|
||||
usage: await cpu.usage(1000),
|
||||
load: cpu.loadavgTime(5),
|
||||
},
|
||||
});
|
||||
} else if (type == "disk") {
|
||||
}
|
||||
|
||||
if (type === "disk") {
|
||||
if (!existsSync(target)) {
|
||||
return res.status(404).json({
|
||||
error: "Target not found",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
drive: await drive.info(target || "/"),
|
||||
});
|
||||
} else if (type == "memory") {
|
||||
}
|
||||
|
||||
if (type === "memory") {
|
||||
return res.status(200).json({
|
||||
memory: await mem.info(),
|
||||
});
|
||||
} else {
|
||||
return res.status(400).json({
|
||||
error: "invalid type",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).json({
|
||||
error: "invalid type",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import RuTorrent from "rutorrent-promise";
|
||||
|
||||
// TODO: Remove the 3rd party dependency once I figure out how to
|
||||
// call this myself with fetch. Just need to destruct the package.
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { url, username, password } = req.query;
|
||||
|
||||
const constructedUrl = new URL(url);
|
||||
|
||||
const rutorrent = new RuTorrent({
|
||||
host: constructedUrl.hostname, // default: localhost
|
||||
port: constructedUrl.port, // default: 80
|
||||
path: constructedUrl.pathname, // default: /rutorrent
|
||||
ssl: constructedUrl.protocol === "https:", // default: false
|
||||
username: username, // default: none
|
||||
password: password, // default: none
|
||||
});
|
||||
|
||||
const data = await rutorrent.get(["d.get_down_rate", "d.get_up_rate", "d.get_state"]);
|
||||
|
||||
res.status(200).send(data);
|
||||
}
|
||||
@@ -1,9 +1,28 @@
|
||||
import cachedFetch from "utils/cached-fetch";
|
||||
import { getSettings } from "utils/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { lat, lon, apiKey, duration } = req.query;
|
||||
const { latitude, longitude, provider, cache, lang } = req.query;
|
||||
let { apiKey } = req.query;
|
||||
|
||||
const api_url = `http://api.weatherapi.com/v1/current.json?q=${lat},${lon}&key=${apiKey}`;
|
||||
if (!apiKey && !provider) {
|
||||
return res.status(400).json({ error: "Missing API key or provider" });
|
||||
}
|
||||
|
||||
res.send(await cachedFetch(api_url, duration));
|
||||
if (!apiKey && provider !== "weatherapi") {
|
||||
return res.status(400).json({ error: "Invalid provider for endpoint" });
|
||||
}
|
||||
|
||||
if (!apiKey && provider) {
|
||||
const settings = await getSettings();
|
||||
apiKey = settings?.providers?.weatherapi;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(400).json({ error: "Missing API key" });
|
||||
}
|
||||
|
||||
const apiUrl = `http://api.weatherapi.com/v1/current.json?q=${latitude},${longitude}&key=${apiKey}&lang=${lang}`;
|
||||
|
||||
return res.send(await cachedFetch(apiUrl, cache));
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import useSWR from "swr";
|
||||
import Head from "next/head";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { ThemeProvider } from "utils/theme-context";
|
||||
|
||||
import ServicesGroup from "components/services/group";
|
||||
import BookmarksGroup from "components/bookmarks/group";
|
||||
import Widget from "components/widget";
|
||||
import Revalidate from "components/revalidate";
|
||||
import { getSettings } from "utils/config";
|
||||
import { ColorProvider } from "utils/color-context";
|
||||
import { ThemeProvider } from "utils/theme-context";
|
||||
|
||||
const ThemeToggle = dynamic(() => import("components/theme-toggle"), {
|
||||
ssr: false,
|
||||
@@ -17,21 +19,37 @@ const ColorToggle = dynamic(() => import("components/color-toggle"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather"];
|
||||
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search"];
|
||||
|
||||
export default function Home() {
|
||||
const { data: services, error: servicesError } = useSWR("/api/services");
|
||||
const { data: bookmarks, error: bookmarksError } = useSWR("/api/bookmarks");
|
||||
const { data: widgets, error: widgetsError } = useSWR("/api/widgets");
|
||||
export async function getStaticProps() {
|
||||
const settings = await getSettings();
|
||||
|
||||
return {
|
||||
props: {
|
||||
settings,
|
||||
},
|
||||
};
|
||||
}
|
||||
export default function Home({ settings }) {
|
||||
const { data: services } = useSWR("/api/services");
|
||||
const { data: bookmarks } = useSWR("/api/bookmarks");
|
||||
const { data: widgets } = useSWR("/api/widgets");
|
||||
|
||||
const wrappedStyle = {};
|
||||
if (settings.background) {
|
||||
wrappedStyle.backgroundImage = `url(${settings.background})`;
|
||||
wrappedStyle.backgroundSize = "cover";
|
||||
}
|
||||
|
||||
return (
|
||||
<ColorProvider>
|
||||
<ThemeProvider>
|
||||
<Head>
|
||||
<title>Welcome</title>
|
||||
<title>{settings.title || "Homepage"}</title>
|
||||
</Head>
|
||||
<div className="w-full container m-auto flex flex-col h-screen justify-between">
|
||||
<div className="flex flex-wrap space-x-4 m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200">
|
||||
<div className="fixed w-full h-full m-0 p-0" style={wrappedStyle} />
|
||||
<div className="relative w-full container m-auto flex flex-col h-screen justify-between">
|
||||
<div className="flex flex-row flex-wrap m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200 justify-between">
|
||||
{widgets && (
|
||||
<>
|
||||
{widgets
|
||||
@@ -39,12 +57,14 @@ export default function Home() {
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} />
|
||||
))}
|
||||
<div className="grow"></div>
|
||||
{widgets
|
||||
.filter((widget) => rightAlignedWidgets.includes(widget.type))
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} />
|
||||
))}
|
||||
|
||||
<div className="ml-4 flex flex-wrap basis-full grow sm:basis-auto justify-between md:justify-end mt-2 md:mt-0">
|
||||
{widgets
|
||||
.filter((widget) => rightAlignedWidgets.includes(widget.type))
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -67,6 +87,7 @@ export default function Home() {
|
||||
|
||||
<div className="rounded-full flex p-8 w-full justify-between">
|
||||
<ColorToggle />
|
||||
<Revalidate />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,15 +3,15 @@
|
||||
|
||||
- My First Group:
|
||||
- My First Service:
|
||||
href: http://localhosdt/
|
||||
href: http://localhost/
|
||||
description: Homepage is awesome
|
||||
|
||||
- My Second Group:
|
||||
- My Second Service:
|
||||
href: http://localhosdt/
|
||||
href: http://localhost/
|
||||
description: Homepage is the best
|
||||
|
||||
- My Third Group:
|
||||
- My Third Service:
|
||||
href: http://localhosdt/
|
||||
href: http://localhost/
|
||||
description: Homepage is 😎
|
||||
|
||||
6
src/skeleton/settings.yaml
Normal file
6
src/skeleton/settings.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# For configuration options and examples, please see:
|
||||
# https://github.com/benphelps/homepage/wiki/Settings
|
||||
|
||||
providers:
|
||||
openweathermap: openweathermapapikey
|
||||
weatherapi: weatherapiapikey
|
||||
@@ -5,3 +5,7 @@
|
||||
cpu: true
|
||||
memory: true
|
||||
disk: /
|
||||
|
||||
- search:
|
||||
provider: duckduckgo
|
||||
target: _blank
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
.theme-white {
|
||||
--color-50: 255 255 255;
|
||||
--color-100: 255 255 255;
|
||||
--color-200: 255 255 255;
|
||||
--color-300: 255 255 255;
|
||||
--color-400: 255 255 255;
|
||||
--color-500: 60 60 60;
|
||||
--color-600: 255 255 255;
|
||||
--color-700: 40 40 40;
|
||||
--color-800: 255 255 255;
|
||||
--color-900: 255 255 255;
|
||||
}
|
||||
|
||||
.theme-slate {
|
||||
--color-50: 248 250 252;
|
||||
--color-100: 241 245 249;
|
||||
|
||||
36
src/utils/api-helpers.js
Normal file
36
src/utils/api-helpers.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const formats = {
|
||||
emby: `{url}/emby/{endpoint}?api_key={key}`,
|
||||
jellyfin: `{url}/emby/{endpoint}?api_key={key}`,
|
||||
pihole: `{url}/admin/{endpoint}`,
|
||||
radarr: `{url}/api/v3/{endpoint}?apikey={key}`,
|
||||
sonarr: `{url}/api/v3/{endpoint}?apikey={key}`,
|
||||
speedtest: `{url}/api/{endpoint}`,
|
||||
tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`,
|
||||
traefik: `{url}/api/{endpoint}`,
|
||||
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
|
||||
rutorrent: `{url}/plugins/httprpc/action.php`,
|
||||
jellyseerr: `{url}/api/v1/{endpoint}`,
|
||||
overseerr: `{url}/api/v1/{endpoint}`,
|
||||
ombi: `{url}/api/v1/{endpoint}`,
|
||||
npm: `{url}/api/{endpoint}`,
|
||||
};
|
||||
|
||||
export function formatApiCall(api, args) {
|
||||
const find = /\{.*?\}/g;
|
||||
const replace = (match) => {
|
||||
const key = match.replace(/\{|\}/g, "");
|
||||
return args[key];
|
||||
};
|
||||
|
||||
return formats[api].replace(find, replace);
|
||||
}
|
||||
|
||||
export function formatApiUrl(widget, endpoint) {
|
||||
const params = new URLSearchParams({
|
||||
type: widget.type,
|
||||
group: widget.service_group,
|
||||
service: widget.service_name,
|
||||
endpoint,
|
||||
});
|
||||
return `/api/services/proxy?${params.toString()}`;
|
||||
}
|
||||
@@ -5,9 +5,9 @@ export default async function cachedFetch(url, duration) {
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
} else {
|
||||
const data = await fetch(url).then((res) => res.json());
|
||||
cache.put(url, data, duration * 1000 * 60);
|
||||
return data;
|
||||
}
|
||||
|
||||
const data = await fetch(url).then((res) => res.json());
|
||||
cache.put(url, data, duration * 1000 * 60);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useState, useEffect } from "react";
|
||||
import { createContext, useState, useEffect, useMemo } from "react";
|
||||
|
||||
let lastColor = false;
|
||||
|
||||
@@ -16,7 +16,7 @@ const getInitialColor = () => {
|
||||
|
||||
export const ColorContext = createContext();
|
||||
|
||||
export const ColorProvider = ({ initialTheme, children }) => {
|
||||
export function ColorProvider({ initialTheme, children }) {
|
||||
const [color, setColor] = useState(getInitialColor);
|
||||
|
||||
const rawSetColor = (rawColor) => {
|
||||
@@ -38,5 +38,7 @@ export const ColorProvider = ({ initialTheme, children }) => {
|
||||
rawSetColor(color);
|
||||
}, [color]);
|
||||
|
||||
return <ColorContext.Provider value={{ color, setColor }}>{children}</ColorContext.Provider>;
|
||||
};
|
||||
const value = useMemo(() => ({ color, setColor }), [color]);
|
||||
|
||||
return <ColorContext.Provider value={value}>{children}</ColorContext.Provider>;
|
||||
}
|
||||
@@ -348,7 +348,9 @@ export default function mapIcon(weatherStatusCode, timeOfDay) {
|
||||
if (mapping) {
|
||||
if (timeOfDay === "day") {
|
||||
return mapping.icon.day;
|
||||
} else if (timeOfDay === "night") {
|
||||
}
|
||||
|
||||
if (timeOfDay === "night") {
|
||||
return mapping.icon.night;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/* eslint-disable no-console */
|
||||
import { join } from "path";
|
||||
import { existsSync, copyFile } from "fs";
|
||||
import { existsSync, copyFile, promises as fs } from "fs";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
|
||||
export default function checkAndCopyConfig(config) {
|
||||
const configYaml = join(process.cwd(), "config", config);
|
||||
@@ -7,10 +10,18 @@ export default function checkAndCopyConfig(config) {
|
||||
const configSkeleton = join(process.cwd(), "src", "skeleton", config);
|
||||
copyFile(configSkeleton, configYaml, (err) => {
|
||||
if (err) {
|
||||
console.log("error copying config", err);
|
||||
console.error("error copying config", err);
|
||||
throw err;
|
||||
}
|
||||
console.info("%s was copied to the config folder", config);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSettings() {
|
||||
checkAndCopyConfig("settings.yaml");
|
||||
|
||||
const settingsYaml = join(process.cwd(), "config", "settings.yaml");
|
||||
const fileContents = await fs.readFile(settingsYaml, "utf8");
|
||||
return yaml.load(fileContents);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
import yaml from "js-yaml";
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import checkAndCopyConfig from "utils/config";
|
||||
|
||||
export default async function getDockerArguments(server) {
|
||||
export default function getDockerArguments(server) {
|
||||
checkAndCopyConfig("docker.yaml");
|
||||
|
||||
const configFile = path.join(process.cwd(), "config", "docker.yaml");
|
||||
const configData = await fs.readFile(configFile, "utf8");
|
||||
const configData = readFileSync(configFile, "utf8");
|
||||
const servers = yaml.load(configData);
|
||||
|
||||
if (!server) {
|
||||
if (process.platform !== "win32" && process.platform !== "darwin") {
|
||||
return { socketPath: "/var/run/docker.sock" };
|
||||
} else {
|
||||
return { host: "127.0.0.1" };
|
||||
}
|
||||
} else if (servers[server]) {
|
||||
|
||||
return { host: "127.0.0.1" };
|
||||
}
|
||||
|
||||
if (servers[server]) {
|
||||
if (servers[server].socket) {
|
||||
return { socketPath: servers[server].socket };
|
||||
} else if (servers[server].host) {
|
||||
return { host: servers[server].host, port: servers[server].port || null };
|
||||
} else {
|
||||
return servers[server];
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
|
||||
if (servers[server].host) {
|
||||
return { host: servers[server].host, port: servers[server].port || null };
|
||||
}
|
||||
|
||||
return servers[server];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/* eslint-disable prefer-promise-reject-errors */
|
||||
import https from "https";
|
||||
import http from "http";
|
||||
|
||||
export function httpsRequest(url, params) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var request = https.request(url, params, function (response) {
|
||||
var data = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = https.request(url, params, (response) => {
|
||||
const data = [];
|
||||
|
||||
response.on("data", (chunk) => {
|
||||
data.push(chunk);
|
||||
@@ -24,9 +25,9 @@ export function httpsRequest(url, params) {
|
||||
}
|
||||
|
||||
export function httpRequest(url, params) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var request = http.request(url, params, function (response) {
|
||||
var data = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = http.request(url, params, (response) => {
|
||||
const data = [];
|
||||
|
||||
response.on("data", (chunk) => {
|
||||
data.push(chunk);
|
||||
@@ -44,3 +45,19 @@ export function httpRequest(url, params) {
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
export function httpProxy(url, params = {}) {
|
||||
const constructedUrl = new URL(url);
|
||||
|
||||
if (constructedUrl.protocol === "https:") {
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
return httpsRequest(constructedUrl, {
|
||||
agent: httpsAgent,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
return httpRequest(constructedUrl, params);
|
||||
}
|
||||
|
||||
50
src/utils/i18n.js
Normal file
50
src/utils/i18n.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import Backend from "i18next-http-backend";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
ns: ["common"],
|
||||
// debug: process.env.NODE_ENV === "development",
|
||||
defaultNS: "common",
|
||||
nonExplicitSupportedLngs: true,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
backend: {
|
||||
loadPath: "/locales/{{lng}}/{{ns}}.json",
|
||||
},
|
||||
});
|
||||
|
||||
i18n.services.formatter.add("bytes", (value, lng, options) =>
|
||||
prettyBytes(parseFloat(value), { locale: lng, ...options })
|
||||
);
|
||||
|
||||
i18n.services.formatter.add("rate", (value, lng, options) => {
|
||||
if (value === 0) return "0 Bps";
|
||||
|
||||
const bits = options.bits ? value : value / 8;
|
||||
const k = 1024;
|
||||
const dm = options.decimals ? options.decimals : 0;
|
||||
const sizes = ["Bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps", "Ybps"];
|
||||
|
||||
const i = Math.floor(Math.log(bits) / Math.log(k));
|
||||
|
||||
const formatted = new Intl.NumberFormat(lng, { maximumFractionDigits: dm, minimumFractionDigits: dm }).format(
|
||||
parseFloat(bits / k ** i)
|
||||
);
|
||||
|
||||
return `${formatted} ${sizes[i]}`;
|
||||
});
|
||||
|
||||
i18n.services.formatter.add("percent", (value, lng, options) =>
|
||||
new Intl.NumberFormat(lng, { style: "percent", ...options }).format(parseFloat(value) / 100.0)
|
||||
);
|
||||
|
||||
export default i18n;
|
||||
@@ -135,7 +135,7 @@ const conditions = [
|
||||
night: Icons.WiNightAltShowers,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
code: 500,
|
||||
icon: {
|
||||
@@ -393,18 +393,17 @@ const conditions = [
|
||||
night: Icons.WiCloudy,
|
||||
},
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
export default function mapIcon(weatherStatusCode, timeOfDay) {
|
||||
const mapping = conditions.find(
|
||||
(condition) => condition.code === weatherStatusCode
|
||||
);
|
||||
const mapping = conditions.find((condition) => condition.code === weatherStatusCode);
|
||||
|
||||
if (mapping) {
|
||||
if (timeOfDay === "day") {
|
||||
return mapping.icon.day;
|
||||
} else if (timeOfDay === "night") {
|
||||
}
|
||||
|
||||
if (timeOfDay === "night") {
|
||||
return mapping.icon.night;
|
||||
}
|
||||
}
|
||||
|
||||
33
src/utils/proxies/credentialed.js
Normal file
33
src/utils/proxies/credentialed.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import getServiceWidget from "utils/service-helpers";
|
||||
import { formatApiCall } from "utils/api-helpers";
|
||||
import { httpProxy } from "utils/http";
|
||||
|
||||
export default async function credentialedProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (widget) {
|
||||
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||
const [status, contentType, data] = await httpProxy(url, {
|
||||
method: req.method,
|
||||
withCredentials: true,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"X-API-Key": `${widget.key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (status === 204 || status === 304) {
|
||||
return res.status(status).end();
|
||||
}
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
28
src/utils/proxies/generic.js
Normal file
28
src/utils/proxies/generic.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import getServiceWidget from "utils/service-helpers";
|
||||
import { formatApiCall } from "utils/api-helpers";
|
||||
import { httpProxy } from "utils/http";
|
||||
|
||||
export default async function genericProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (widget) {
|
||||
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||
const [status, contentType, data] = await httpProxy(url, {
|
||||
method: req.method,
|
||||
});
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
|
||||
if (status === 204 || status === 304) {
|
||||
return res.status(status).end();
|
||||
}
|
||||
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
37
src/utils/proxies/npm.js
Normal file
37
src/utils/proxies/npm.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import getServiceWidget from "utils/service-helpers";
|
||||
import { formatApiCall } from "utils/api-helpers";
|
||||
|
||||
export default async function npmProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (widget) {
|
||||
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||
|
||||
const loginUrl = `${widget.url}/api/tokens`;
|
||||
const body = { identity: widget.username, secret: widget.password };
|
||||
|
||||
const authResponse = await fetch(loginUrl, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((response) => response.json());
|
||||
|
||||
const apiResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${authResponse.token}`,
|
||||
},
|
||||
}).then((response) => response.json());
|
||||
|
||||
return res.send(apiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
40
src/utils/proxies/nzbget.js
Normal file
40
src/utils/proxies/nzbget.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { JSONRPCClient } from "json-rpc-2.0";
|
||||
|
||||
import getServiceWidget from "utils/service-helpers";
|
||||
|
||||
export default async function nzbgetProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (widget) {
|
||||
const constructedUrl = new URL(widget.url);
|
||||
constructedUrl.pathname = "jsonrpc";
|
||||
|
||||
const authorization = Buffer.from(`${widget.username}:${widget.password}`).toString("base64");
|
||||
|
||||
const client = new JSONRPCClient((jsonRPCRequest) =>
|
||||
fetch(constructedUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: `Basic ${authorization}`,
|
||||
},
|
||||
body: JSON.stringify(jsonRPCRequest),
|
||||
}).then(async (response) => {
|
||||
if (response.status === 200) {
|
||||
const jsonRPCResponse = await response.json();
|
||||
return client.receive(jsonRPCResponse);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
})
|
||||
);
|
||||
|
||||
return res.send(await client.request(endpoint));
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
30
src/utils/proxies/rutorrent.js
Normal file
30
src/utils/proxies/rutorrent.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import RuTorrent from "rutorrent-promise";
|
||||
|
||||
import getServiceWidget from "utils/service-helpers";
|
||||
|
||||
export default async function rutorrentProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (widget) {
|
||||
const constructedUrl = new URL(widget.url);
|
||||
|
||||
const rutorrent = new RuTorrent({
|
||||
host: constructedUrl.hostname,
|
||||
port: constructedUrl.port,
|
||||
path: constructedUrl.pathname,
|
||||
ssl: constructedUrl.protocol === "https:",
|
||||
username: widget.username,
|
||||
password: widget.password,
|
||||
});
|
||||
|
||||
const data = await rutorrent.get(["d.get_down_rate", "d.get_up_rate", "d.get_state"]);
|
||||
|
||||
return res.status(200).send(data);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
151
src/utils/service-helpers.js
Normal file
151
src/utils/service-helpers.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import Docker from "dockerode";
|
||||
import * as shvl from "shvl";
|
||||
|
||||
import checkAndCopyConfig from "utils/config";
|
||||
import getDockerArguments from "utils/docker";
|
||||
|
||||
export async function servicesFromConfig() {
|
||||
checkAndCopyConfig("services.yaml");
|
||||
|
||||
const servicesYaml = path.join(process.cwd(), "config", "services.yaml");
|
||||
const fileContents = await fs.readFile(servicesYaml, "utf8");
|
||||
const services = yaml.load(fileContents);
|
||||
|
||||
if (!services) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
const servicesArray = services.map((servicesGroup) => ({
|
||||
name: Object.keys(servicesGroup)[0],
|
||||
services: servicesGroup[Object.keys(servicesGroup)[0]].map((entries) => ({
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]],
|
||||
})),
|
||||
}));
|
||||
|
||||
return servicesArray;
|
||||
}
|
||||
|
||||
export async function servicesFromDocker() {
|
||||
checkAndCopyConfig("docker.yaml");
|
||||
|
||||
const dockerYaml = path.join(process.cwd(), "config", "docker.yaml");
|
||||
const dockerFileContents = await fs.readFile(dockerYaml, "utf8");
|
||||
const servers = yaml.load(dockerFileContents);
|
||||
|
||||
if (!servers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const serviceServers = await Promise.all(
|
||||
Object.keys(servers).map(async (serverName) => {
|
||||
const docker = new Docker(getDockerArguments(serverName));
|
||||
const containers = await docker.listContainers({
|
||||
all: true,
|
||||
});
|
||||
|
||||
// bad docker connections can result in a <Buffer ...> object?
|
||||
// in any case, this ensures the result is the expected array
|
||||
if (!Array.isArray(containers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const discovered = containers.map((container) => {
|
||||
let constructedService = null;
|
||||
|
||||
Object.keys(container.Labels).forEach((label) => {
|
||||
if (label.startsWith("homepage")) {
|
||||
if (!constructedService) {
|
||||
constructedService = {
|
||||
container: container.Names[0].replace(/^\//, ""),
|
||||
server: serverName,
|
||||
};
|
||||
}
|
||||
shvl.set(constructedService, label.replace("homepage.", ""), container.Labels[label]);
|
||||
}
|
||||
});
|
||||
|
||||
return constructedService;
|
||||
});
|
||||
|
||||
return { server: serverName, services: discovered.filter((filteredService) => filteredService) };
|
||||
})
|
||||
);
|
||||
|
||||
const mappedServiceGroups = [];
|
||||
|
||||
serviceServers.forEach((server) => {
|
||||
server.services.forEach((serverService) => {
|
||||
let serverGroup = mappedServiceGroups.find((searchedGroup) => searchedGroup.name === serverService.group);
|
||||
if (!serverGroup) {
|
||||
mappedServiceGroups.push({
|
||||
name: serverService.group,
|
||||
services: [],
|
||||
});
|
||||
serverGroup = mappedServiceGroups[mappedServiceGroups.length - 1];
|
||||
}
|
||||
|
||||
const { name: serviceName, group: serverServiceGroup, ...pushedService } = serverService;
|
||||
const result = {
|
||||
name: serviceName,
|
||||
...pushedService,
|
||||
};
|
||||
|
||||
serverGroup.services.push(result);
|
||||
});
|
||||
});
|
||||
|
||||
return mappedServiceGroups;
|
||||
}
|
||||
|
||||
export function cleanServiceGroups(groups) {
|
||||
return groups.map((serviceGroup) => ({
|
||||
name: serviceGroup.name,
|
||||
services: serviceGroup.services.map((service) => {
|
||||
const cleanedService = { ...service };
|
||||
|
||||
if (cleanedService.widget) {
|
||||
const { type } = cleanedService.widget;
|
||||
|
||||
cleanedService.widget = {
|
||||
type,
|
||||
service_name: service.name,
|
||||
service_group: serviceGroup.name,
|
||||
};
|
||||
}
|
||||
|
||||
return cleanedService;
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function getServiceWidget(group, service) {
|
||||
const configuredServices = await servicesFromConfig();
|
||||
|
||||
const serviceGroup = configuredServices.find((g) => g.name === group);
|
||||
if (serviceGroup) {
|
||||
const serviceEntry = serviceGroup.services.find((s) => s.name === service);
|
||||
if (serviceEntry) {
|
||||
const { widget } = serviceEntry;
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
|
||||
const discoveredServices = await servicesFromDocker();
|
||||
|
||||
const dockerServiceGroup = discoveredServices.find((g) => g.name === group);
|
||||
if (dockerServiceGroup) {
|
||||
const dockerServiceEntry = dockerServiceGroup.services.find((s) => s.name === service);
|
||||
if (dockerServiceEntry) {
|
||||
const { widget } = dockerServiceEntry;
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export function calculateCPUPercent(stats) {
|
||||
export default function calculateCPUPercent(stats) {
|
||||
let cpuPercent = 0.0;
|
||||
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
|
||||
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
|
||||
@@ -9,27 +9,3 @@ export function calculateCPUPercent(stats) {
|
||||
|
||||
return Math.round(cpuPercent * 10) / 10;
|
||||
}
|
||||
|
||||
export function formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat(bytes / Math.pow(k, i)).toFixed(dm) + " " + sizes[i];
|
||||
}
|
||||
|
||||
export function formatBits(bytes, decimals = 2) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ["B", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb"];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat(bytes / Math.pow(k, i)).toFixed(dm) + " " + sizes[i];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useState, useEffect } from "react";
|
||||
import { createContext, useState, useEffect, useMemo } from "react";
|
||||
|
||||
const getInitialTheme = () => {
|
||||
if (typeof window !== "undefined" && window.localStorage) {
|
||||
@@ -18,7 +18,7 @@ const getInitialTheme = () => {
|
||||
|
||||
export const ThemeContext = createContext();
|
||||
|
||||
export const ThemeProvider = ({ initialTheme, children }) => {
|
||||
export function ThemeProvider({ initialTheme, children }) {
|
||||
const [theme, setTheme] = useState(getInitialTheme);
|
||||
|
||||
const rawSetTheme = (rawTheme) => {
|
||||
@@ -39,5 +39,7 @@ export const ThemeProvider = ({ initialTheme, children }) => {
|
||||
rawSetTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
|
||||
};
|
||||
const value = useMemo(() => ({ theme, setTheme }), [theme]);
|
||||
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
||||
}
|
||||
Reference in New Issue
Block a user