mirror of
https://github.com/ajnart/homarr.git
synced 2026-01-06 23:52:16 +01:00
🔀 Merge pull request #357 from manuel-rw/add-i18n-translations
🌎 Add translations !
This commit is contained in:
10
next-i18next.config.js
Normal file
10
next-i18next.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
// https://www.i18next.com/overview/configuration-options#logging
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'de'],
|
||||
localeDetection: true,
|
||||
},
|
||||
reloadOnPrerender: process.env.NODE_ENV === 'development',
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
const { env } = require('process');
|
||||
|
||||
const { i18n } = require('./next-i18next.config');
|
||||
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
@@ -13,4 +15,5 @@ module.exports = withBundleAnalyzer({
|
||||
outputStandalone: true,
|
||||
},
|
||||
output: 'standalone',
|
||||
i18n,
|
||||
});
|
||||
|
||||
@@ -53,8 +53,12 @@
|
||||
"dockerode": "^3.3.2",
|
||||
"embla-carousel-react": "^7.0.0",
|
||||
"framer-motion": "^6.5.1",
|
||||
"i18next": "^21.9.1",
|
||||
"i18next-browser-languagedetector": "^6.1.5",
|
||||
"i18next-http-backend": "^1.4.1",
|
||||
"js-file-download": "^0.4.12",
|
||||
"next": "12.1.6",
|
||||
"next-i18next": "^11.3.0",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -87,5 +91,9 @@
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.2",
|
||||
"@types/react-dom": "17.0.2"
|
||||
},
|
||||
"packageManager": "yarn@3.2.1"
|
||||
}
|
||||
|
||||
BIN
public/imgs/flags/de.png
Normal file
BIN
public/imgs/flags/de.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 151 B |
BIN
public/imgs/flags/en.png
Normal file
BIN
public/imgs/flags/en.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
6
public/locales/de/common.json
Normal file
6
public/locales/de/common.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"actions": {
|
||||
"save": "Speichern"
|
||||
},
|
||||
"tip": "Tipp: "
|
||||
}
|
||||
118
public/locales/de/layout/add-service-app-shelf.json
Normal file
118
public/locales/de/layout/add-service-app-shelf.json
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"actionIcon": {
|
||||
"tooltip": "Einen Service hinzufügen"
|
||||
},
|
||||
"modal": {
|
||||
"title": "Service hinzufügen",
|
||||
"form": {
|
||||
"validation": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"noStatusCodeSelected": "Please select a status code"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"options": {
|
||||
"title": "Optionen",
|
||||
"form": {
|
||||
"serviceName": {
|
||||
"label": "Service Namen",
|
||||
"placeholder": "Plex"
|
||||
},
|
||||
"iconUrl": {
|
||||
"label": "Icon URL"
|
||||
},
|
||||
"serviceUrl": {
|
||||
"label": "Service URL"
|
||||
},
|
||||
"onClickUrl": {
|
||||
"label": "URL bei einem Klick"
|
||||
},
|
||||
"serviceType": {
|
||||
"label": "Service Typ",
|
||||
"defaultValue": "Andere",
|
||||
"placeholder": "Wähle einen Typ aus"
|
||||
},
|
||||
"category": {
|
||||
"label": "Kategorie",
|
||||
"placeholder": "Whle eine Kategorie oder erstelle eine neue",
|
||||
"nothingFound": "Nichts gefunden",
|
||||
"createLabel": "+ Erstelle {{query}}"
|
||||
},
|
||||
"integrations": {
|
||||
"apiKey": {
|
||||
"label": "API Schlüssel",
|
||||
"placeholder": "Dein API Schlüssel",
|
||||
"validation": {
|
||||
"noKey": "Invalider Schlüssel"
|
||||
},
|
||||
"tip": {
|
||||
"text": "Erhalte deinen API Schlüssel",
|
||||
"link": "hier."
|
||||
}
|
||||
},
|
||||
"qBittorrent": {
|
||||
"username": {
|
||||
"label": "Benutzernamen",
|
||||
"placeholder": "admin",
|
||||
"validation": {
|
||||
"invalidUsername": "Invalider Benutzername"
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"label": "Passwort",
|
||||
"placeholder": "adminadmin",
|
||||
"validation": {
|
||||
"invalidPassword": "Invalides Passwort"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deluge": {
|
||||
"password": {
|
||||
"label": "Passwort",
|
||||
"placeholder": "password",
|
||||
"validation": {
|
||||
"invalidPassword": "Invalides Passwort"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transmission": {
|
||||
"username": {
|
||||
"label": "Benutzername",
|
||||
"placeholder": "admin",
|
||||
"validation": {
|
||||
"invalidUsername": "Invalider Benutzername"
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"label": "Passwort",
|
||||
"placeholder": "adminadmin",
|
||||
"validation": {
|
||||
"invalidPassword": "Invalides Passwort"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedOptions": {
|
||||
"title": "Weitere Optionen",
|
||||
"form": {
|
||||
"httpStatusCodes": {
|
||||
"label": "HTTP Status Nummern",
|
||||
"placeholder": "Wähle eine valide Status Nummer",
|
||||
"clearButtonLabel": "Auswahl löschen",
|
||||
"nothingFound": "Nichts gefunden"
|
||||
},
|
||||
"openServiceInNewTab": {
|
||||
"label": "Serivce in einem neuen Tab öffnen"
|
||||
},
|
||||
"buttons": {
|
||||
"submit": {
|
||||
"content": "Service hinzufügen"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
public/locales/de/layout/app-shelf-menu.json
Normal file
18
public/locales/de/layout/app-shelf-menu.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"modal": {
|
||||
"title": "Einen Service bearbeiten",
|
||||
"buttons": {
|
||||
"save": "Service speichern"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"labels": {
|
||||
"settings": "Einstellungen",
|
||||
"dangerZone": "Gefahrenzone"
|
||||
},
|
||||
"actions": {
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
public/locales/de/layout/app-shelf.json
Normal file
10
public/locales/de/layout/app-shelf.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"accordions": {
|
||||
"downloads": {
|
||||
"text": "Deine Downloads"
|
||||
},
|
||||
"others": {
|
||||
"text": "Andere"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
public/locales/de/modules/calendar.json
Normal file
11
public/locales/de/modules/calendar.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Kalender",
|
||||
"description": "Ein Kalender Modul welches bevorstehende Shows anzeigt. Es interagiert mit der API von Sonarr, Radarr, Readarr und Lidarr.",
|
||||
"settings": {
|
||||
"sundayStart": {
|
||||
"label": "Wochenstart am Sonntag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
public/locales/de/modules/common-media-cards.json
Normal file
6
public/locales/de/modules/common-media-cards.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"buttons": {
|
||||
"play": "Abspielen",
|
||||
"request": "Anfragen"
|
||||
}
|
||||
}
|
||||
32
public/locales/de/modules/dashdot.json
Normal file
32
public/locales/de/modules/dashdot.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"card": {
|
||||
"title": "Dash.",
|
||||
"errors": {
|
||||
"noService": "Kein Dash. Service gefunden. Bitte füge einen zu deinem Homarr Dashboard hinzu oder setze eine Dash. URL in den Modul-Optionen.",
|
||||
"noInformation": "Informationen konnten nicht von Dash. geladen werden. Betriebst du die neuste Version?"
|
||||
},
|
||||
"graphs": {
|
||||
"storage": {
|
||||
"title": "Speicher",
|
||||
"label": "Speicher:"
|
||||
},
|
||||
"network": {
|
||||
"title": "Netzwerk",
|
||||
"label": "Netzwerk:",
|
||||
"metrics": {
|
||||
"download": "Eingehend",
|
||||
"upload": "Ausgehend"
|
||||
}
|
||||
},
|
||||
"cpu": {
|
||||
"title": "CPU"
|
||||
},
|
||||
"memory": {
|
||||
"title": "RAM"
|
||||
},
|
||||
"gpu": {
|
||||
"title": "GPU"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
public/locales/de/modules/docker-module.json
Normal file
65
public/locales/de/modules/docker-module.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"search": {
|
||||
"placeholder": "Suche nach Conainer oder Image Namen"
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "Name",
|
||||
"image": "Image",
|
||||
"ports": "Ports",
|
||||
"state": "Status"
|
||||
},
|
||||
"body": {
|
||||
"portCollapse": "{{ports}} weitere"
|
||||
},
|
||||
"states": {
|
||||
"running": "Läuft",
|
||||
"created": "Erstellt",
|
||||
"stopped": "Gestopped",
|
||||
"unknown": "Unbekannt"
|
||||
}
|
||||
},
|
||||
"actionBar": {
|
||||
"addService": {
|
||||
"title": "Service hinzufügen",
|
||||
"message": "Service zu Homarr hinzufügen"
|
||||
},
|
||||
"restart": {
|
||||
"title": "Neustarten"
|
||||
},
|
||||
"stop": {
|
||||
"title": "Stoppen"
|
||||
},
|
||||
"start": {
|
||||
"title": "Starten"
|
||||
},
|
||||
"refreshData": "Daten aktualisieren",
|
||||
"addToHomarr": {
|
||||
"title": "Zu Homarr hinzufügen"
|
||||
},
|
||||
"remove": {
|
||||
"title": "Entfernen"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"successfullyExecuted": {
|
||||
"title": "Container {{containerName}} {{action}}ed",
|
||||
"message": "Your container was successfully {{action}}ed"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"integrationFailed": {
|
||||
"title": "Docker Integration schlug fehl",
|
||||
"message": "Hast du vergessen, den Docker Socket zu verbinden?"
|
||||
},
|
||||
"unknownError": {
|
||||
"title": "Es ist ein Fehler aufgetreten"
|
||||
},
|
||||
"oneServiceAtATime": {
|
||||
"title": "Bitte füge nur einen Service zur Zeit hinzu."
|
||||
}
|
||||
},
|
||||
"actionIcon": {
|
||||
"tooltip": "Docker"
|
||||
}
|
||||
}
|
||||
26
public/locales/de/modules/overseerr.json
Normal file
26
public/locales/de/modules/overseerr.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"popup": {
|
||||
"item": {
|
||||
"buttons": {
|
||||
"askFor": "Fragen für {{title}}",
|
||||
"cancel": "Abbrechen",
|
||||
"request": "Anfragen"
|
||||
},
|
||||
"alerts": {
|
||||
"automaticApproval": {
|
||||
"title": "Einen API Schlüssel benutzen",
|
||||
"text": "Diese Anfrage wird automatisch genehmigt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"seasonSelector": {
|
||||
"caption": "Kreuze die Staffeln an, die heruntergeladen werden sollen.",
|
||||
"table": {
|
||||
"header": {
|
||||
"season": "Staffel",
|
||||
"numberOfEpisodes": "Anzahl von Episoden"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
public/locales/de/modules/ping.json
Normal file
11
public/locales/de/modules/ping.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Ping Services",
|
||||
"description": "Pings your services and shows their status as an indicator"
|
||||
},
|
||||
"states": {
|
||||
"online": "Online {{response}}",
|
||||
"offline": "Offline {{response}}",
|
||||
"loading": "Laden..."
|
||||
}
|
||||
}
|
||||
5
public/locales/de/modules/search.json
Normal file
5
public/locales/de/modules/search.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"input": {
|
||||
"placeholder": "Das Internet durchsuchen..."
|
||||
}
|
||||
}
|
||||
31
public/locales/de/modules/torrents-status.json
Normal file
31
public/locales/de/modules/torrents-status.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"card": {
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "Name",
|
||||
"size": "Grösse",
|
||||
"download": "Eingehend",
|
||||
"upload": "Ausgehend",
|
||||
"estimatedTimeOfArrival": "Vorraussichtlicher Abschluss",
|
||||
"progress": "Fortschritt"
|
||||
},
|
||||
"body": {
|
||||
"nothingFound": "Keine Torrents gefunden"
|
||||
}
|
||||
},
|
||||
"lineChart": {
|
||||
"title": "Derzeitige Download Geschwindigkeit",
|
||||
"download": "Download: {{download}}",
|
||||
"upload": "Upload: {{upload}}",
|
||||
"timeSpan": "{{seconds}} Sekunden zuvor",
|
||||
"totalDownload": "Download: {{download}}/s",
|
||||
"totalUpload": "Upload: {{upload}}/s"
|
||||
},
|
||||
"errors": {
|
||||
"noDownloadClients": {
|
||||
"title": "Keine unterstützten Download Clients gefunden",
|
||||
"text": "Füge einen Download Service hinzu, um deine derzeitigen Downloads zu sehen"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
public/locales/de/modules/weather.json
Normal file
20
public/locales/de/modules/weather.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"card": {
|
||||
"weatherDescriptions": {
|
||||
"clear": "Klar",
|
||||
"mainlyClear": "Überwiegend klar",
|
||||
"fog": "Nebel",
|
||||
"drizzle": "Niesel",
|
||||
"freezingDrizzle": "Eisiger Nieselregen",
|
||||
"rain": "Regen",
|
||||
"freezingRain": "Eisiger Regen",
|
||||
"snowFall": "Schneefall",
|
||||
"snowGrains": "Schneekörner",
|
||||
"rainShowers": "Regenschauer",
|
||||
"snowShowers": "Schneeschauer",
|
||||
"thunderstorm": "Gewitter",
|
||||
"thunderstormWithHail": "Gewitter mit Hagel",
|
||||
"unknown": "Unbekannt"
|
||||
}
|
||||
}
|
||||
}
|
||||
14
public/locales/de/settings/common.json
Normal file
14
public/locales/de/settings/common.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": "Einstellungen",
|
||||
"tooltip": "Einstellungen",
|
||||
"tabs": {
|
||||
"common": "Gewöhnlich",
|
||||
"customizations": "Anpassungen"
|
||||
},
|
||||
"tips": {
|
||||
"configTip": "Lade eine neue Konfiguration hoch, indem du eine neue auf die Seite ziehst!"
|
||||
},
|
||||
"credits": {
|
||||
"madeWithLove": "Gemacht mit ❤️ von @"
|
||||
}
|
||||
}
|
||||
3
public/locales/de/settings/customization/app-width.json
Normal file
3
public/locales/de/settings/customization/app-width.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Applikations Breite"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"suffix": "{{color}} Farbe"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Applikation Deckkraft"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"pageTitle": {
|
||||
"label": "Seiten Titel",
|
||||
"placeholder": "Homarr 🦞"
|
||||
},
|
||||
"logo": {
|
||||
"label": "Logo",
|
||||
"placeholder": "/img/logo.png"
|
||||
},
|
||||
"favicon": {
|
||||
"label": "Favicon",
|
||||
"placeholder": "/favicon.png"
|
||||
},
|
||||
"background": {
|
||||
"label": "Hintergrund",
|
||||
"placeholder": "/img/background.png"
|
||||
},
|
||||
"buttons": {
|
||||
"submit": "Absenden"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Shatten"
|
||||
}
|
||||
3
public/locales/de/settings/general/color-schema.json
Normal file
3
public/locales/de/settings/general/color-schema.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Wechseln zu {{scheme}} Modus"
|
||||
}
|
||||
55
public/locales/de/settings/general/config-changer.json
Normal file
55
public/locales/de/settings/general/config-changer.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"configSelect": {
|
||||
"label": "Konfigurations Lader"
|
||||
},
|
||||
"modal": {
|
||||
"title": "Wähle einen Namen für deine neue Konfiguration",
|
||||
"form": {
|
||||
"configName": {
|
||||
"label": "Konfigurations Name",
|
||||
"placeholder": "Dein neuer Konfigurtionsname"
|
||||
},
|
||||
"submitButton": "Bestätigen"
|
||||
},
|
||||
"events": {
|
||||
"configSaved": {
|
||||
"title": "Konfiguration gespeichert",
|
||||
"message": "Konfiguration gespeichert als {{configName}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"download": "Konfiguration herunterladen",
|
||||
"delete": {
|
||||
"text": "Konfiguration löschen",
|
||||
"notifications": {
|
||||
"deleted": {
|
||||
"title": "Konfiguration gelöscht",
|
||||
"message": "Konfiguration wurde gelöscht"
|
||||
},
|
||||
"deleteFailed": {
|
||||
"title": "Config delete failed",
|
||||
"message": "Config delete failed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"saveCopy": "Eine Kopie speichern"
|
||||
},
|
||||
"dropzone": {
|
||||
"notifications": {
|
||||
"invalidConfig": {
|
||||
"title": "Konfiguration konnte nicht geladen werden",
|
||||
"message": "Konfiguration konnte nicht geladen werden. Invalides JSON"
|
||||
},
|
||||
"loadedSuccessfully": {
|
||||
"title": "Konfiguration {{configName}} wurde erfolgreich geladen"
|
||||
}
|
||||
},
|
||||
"accept": {
|
||||
"text": "Ziehe Konfigurationen hier um sie hochzuladen. Es werden nur JSON Dateien unterstützt."
|
||||
},
|
||||
"reject": {
|
||||
"text": "Dieses Dateiformat wird nicht unterstützt. Bitte lade nur JSON hoch."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Sprache"
|
||||
}
|
||||
3
public/locales/de/settings/general/module-enabler.json
Normal file
3
public/locales/de/settings/general/module-enabler.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"title": "Modul-Enabler"
|
||||
}
|
||||
11
public/locales/de/settings/general/search-engine.json
Normal file
11
public/locales/de/settings/general/search-engine.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"title": "Suchmaschine",
|
||||
"tips": {
|
||||
"generalTip": "Verwenden die Präfixe !yt und !t vor deiner Suchanfrage, um auf YouTube bzw. nach einem Torrent zu suchen.",
|
||||
"placeholderTip": "%s kann als Platzhalter für deine Suchanfrage verwendet werden."
|
||||
},
|
||||
"customEngine": {
|
||||
"label": "Suchanfrage URL",
|
||||
"placeholder": "Benutzerdefinierte Adresse"
|
||||
}
|
||||
}
|
||||
3
public/locales/de/settings/general/theme-selector.json
Normal file
3
public/locales/de/settings/general/theme-selector.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Wechsel zu {{theme}} Modus"
|
||||
}
|
||||
3
public/locales/de/settings/general/widget-positions.json
Normal file
3
public/locales/de/settings/general/widget-positions.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Widgets auf der linken Seite"
|
||||
}
|
||||
6
public/locales/en/common.json
Normal file
6
public/locales/en/common.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"actions": {
|
||||
"save": "Save"
|
||||
},
|
||||
"tip": "Tip: "
|
||||
}
|
||||
118
public/locales/en/layout/add-service-app-shelf.json
Normal file
118
public/locales/en/layout/add-service-app-shelf.json
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"actionIcon": {
|
||||
"tooltip": "Add a service"
|
||||
},
|
||||
"modal": {
|
||||
"title": "Add service",
|
||||
"form": {
|
||||
"validation": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"noStatusCodeSelected": "Please select a status code"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"form": {
|
||||
"serviceName": {
|
||||
"label": "Service name",
|
||||
"placeholder": "Plex"
|
||||
},
|
||||
"iconUrl": {
|
||||
"label": "Icon URL"
|
||||
},
|
||||
"serviceUrl": {
|
||||
"label": "Service URL"
|
||||
},
|
||||
"onClickUrl": {
|
||||
"label": "On Click URL"
|
||||
},
|
||||
"serviceType": {
|
||||
"label": "Service type",
|
||||
"defaultValue": "Other",
|
||||
"placeholder": "Pick one"
|
||||
},
|
||||
"category": {
|
||||
"label": "Category",
|
||||
"placeholder": "Select a category or create a new one",
|
||||
"nothingFound": "Nothing found",
|
||||
"createLabel": "+ Create {{query}}"
|
||||
},
|
||||
"integrations": {
|
||||
"apiKey": {
|
||||
"label": "API key",
|
||||
"placeholder": "Your API key",
|
||||
"validation": {
|
||||
"noKey": "Invalid Key"
|
||||
},
|
||||
"tip": {
|
||||
"text": "Get your API key",
|
||||
"link": "here."
|
||||
}
|
||||
},
|
||||
"qBittorrent": {
|
||||
"username": {
|
||||
"label": "Username",
|
||||
"placeholder": "admin",
|
||||
"validation": {
|
||||
"invalidUsername": "Invalid username"
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"placeholder": "adminadmin",
|
||||
"validation": {
|
||||
"invalidPassword": "Invalid password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deluge": {
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"placeholder": "password",
|
||||
"validation": {
|
||||
"invalidPassword": "Invalid password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transmission": {
|
||||
"username": {
|
||||
"label": "Username",
|
||||
"placeholder": "admin",
|
||||
"validation": {
|
||||
"invalidUsername": "Invalid username"
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"placeholder": "adminadmin",
|
||||
"validation": {
|
||||
"invalidPassword": "Invalid password"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedOptions": {
|
||||
"title": "Advanced options",
|
||||
"form": {
|
||||
"httpStatusCodes": {
|
||||
"label": "HTTP Status Codes",
|
||||
"placeholder": "Select valid status codes",
|
||||
"clearButtonLabel": "Clear selection",
|
||||
"nothingFound": "Nothing found"
|
||||
},
|
||||
"openServiceInNewTab": {
|
||||
"label": "Open service in new tab"
|
||||
},
|
||||
"buttons": {
|
||||
"submit": {
|
||||
"content": "Add service"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
public/locales/en/layout/app-shelf-menu.json
Normal file
18
public/locales/en/layout/app-shelf-menu.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"modal": {
|
||||
"title": "Modify a service",
|
||||
"buttons": {
|
||||
"save": "Save service"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"labels": {
|
||||
"settings": "Settings",
|
||||
"dangerZone": "Danger zone"
|
||||
},
|
||||
"actions": {
|
||||
"edit": "Edit",
|
||||
"delete": "Delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
public/locales/en/layout/app-shelf.json
Normal file
10
public/locales/en/layout/app-shelf.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"accordions": {
|
||||
"downloads": {
|
||||
"text": "Your downloads"
|
||||
},
|
||||
"others": {
|
||||
"text": "Others"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
public/locales/en/modules/calendar.json
Normal file
11
public/locales/en/modules/calendar.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Calendar",
|
||||
"description": "A calendar module for displaying upcoming releases. It interacts with the Sonarr and Radarr API.",
|
||||
"settings": {
|
||||
"sundayStart": {
|
||||
"label": "Start the week on Sunday"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
public/locales/en/modules/common-media-cards.json
Normal file
6
public/locales/en/modules/common-media-cards.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"buttons": {
|
||||
"play": "Play",
|
||||
"request": "Request"
|
||||
}
|
||||
}
|
||||
5
public/locales/en/modules/common.json
Normal file
5
public/locales/en/modules/common.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"settings": {
|
||||
"label": "Settings"
|
||||
}
|
||||
}
|
||||
60
public/locales/en/modules/dashdot.json
Normal file
60
public/locales/en/modules/dashdot.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Dash.",
|
||||
"description": "A module for displaying the graphs of your running Dash. instance.",
|
||||
"settings": {
|
||||
"cpuMultiView": {
|
||||
"label": "CPU Multi-Core View"
|
||||
},
|
||||
"storageMultiView": {
|
||||
"label": "Storage Multi-Drive View"
|
||||
},
|
||||
"useCompactView": {
|
||||
"label": "Use Compact View"
|
||||
},
|
||||
"graphs": {
|
||||
"label": "Graphs",
|
||||
"options": {
|
||||
"cpu": "CPU",
|
||||
"ram": "RAM",
|
||||
"storage": "Storage",
|
||||
"network": "Network",
|
||||
"gpu": "GPU"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"label": "Dash. URL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
"title": "Dash.",
|
||||
"errors": {
|
||||
"noService": "No dash. service found. Please add one to your Homarr dashboard or set a dashdot URL in the module options",
|
||||
"noInformation": "Cannot acquire information from dash. - are you running the latest version?"
|
||||
},
|
||||
"graphs": {
|
||||
"storage": {
|
||||
"title": "Storage",
|
||||
"label": "Storage:"
|
||||
},
|
||||
"network": {
|
||||
"title": "Network",
|
||||
"label": "Network:",
|
||||
"metrics": {
|
||||
"download": "Down",
|
||||
"upload": "Up"
|
||||
}
|
||||
},
|
||||
"cpu": {
|
||||
"title": "CPU"
|
||||
},
|
||||
"memory": {
|
||||
"title": "RAM"
|
||||
},
|
||||
"gpu": {
|
||||
"title": "GPU"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
public/locales/en/modules/date.json
Normal file
11
public/locales/en/modules/date.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Date",
|
||||
"description": "Show the current time and date in a card",
|
||||
"settings": {
|
||||
"display24HourFormat": {
|
||||
"label": "Display full time (24-hour)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
public/locales/en/modules/dlspeed.json
Normal file
6
public/locales/en/modules/dlspeed.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Download Speed",
|
||||
"description": "Show the current download speed of supported services"
|
||||
}
|
||||
}
|
||||
69
public/locales/en/modules/docker.json
Normal file
69
public/locales/en/modules/docker.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Docker",
|
||||
"description": "Allows you to easily manage your torrents"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search by container or image name"
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "Name",
|
||||
"image": "Image",
|
||||
"ports": "Ports",
|
||||
"state": "State"
|
||||
},
|
||||
"body": {
|
||||
"portCollapse": "{{ports}} more"
|
||||
},
|
||||
"states": {
|
||||
"running": "Running",
|
||||
"created": "Created",
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown"
|
||||
}
|
||||
},
|
||||
"actionBar": {
|
||||
"addService": {
|
||||
"title": "Add service",
|
||||
"message": "Add service to Homarr"
|
||||
},
|
||||
"restart": {
|
||||
"title": "Restart"
|
||||
},
|
||||
"stop": {
|
||||
"title": "Stop"
|
||||
},
|
||||
"start": {
|
||||
"title": "Start"
|
||||
},
|
||||
"refreshData": "Refresh data",
|
||||
"addToHomarr": {
|
||||
"title": "Add to Homarr"
|
||||
},
|
||||
"remove": {
|
||||
"title": "Remove"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"successfullyExecuted": {
|
||||
"title": "Container {{containerName}} {{action}}ed",
|
||||
"message": "Your container was successfully {{action}}ed"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"integrationFailed": {
|
||||
"title": "Docker integration failed",
|
||||
"message": "Did you forget to mount the docker socket ?"
|
||||
},
|
||||
"unknownError": {
|
||||
"title": "There was an error"
|
||||
},
|
||||
"oneServiceAtATime": {
|
||||
"title": "Please only add one service at a time!"
|
||||
}
|
||||
},
|
||||
"actionIcon": {
|
||||
"tooltip": "Docker"
|
||||
}
|
||||
}
|
||||
30
public/locales/en/modules/overseerr.json
Normal file
30
public/locales/en/modules/overseerr.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Overseerr",
|
||||
"description": "Allows you to search and add media from Overseerr/Jellyseerr"
|
||||
},
|
||||
"popup": {
|
||||
"item": {
|
||||
"buttons": {
|
||||
"askFor": "Ask for {{title}}",
|
||||
"cancel": "Cancel",
|
||||
"request": "Request"
|
||||
},
|
||||
"alerts": {
|
||||
"automaticApproval": {
|
||||
"title": "Using API key",
|
||||
"text": "This request will be automatically approved"
|
||||
}
|
||||
}
|
||||
},
|
||||
"seasonSelector": {
|
||||
"caption": "Tick the seasons that you want to be downloaded",
|
||||
"table": {
|
||||
"header": {
|
||||
"season": "Season",
|
||||
"numberOfEpisodes": "Number of episodes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
public/locales/en/modules/ping.json
Normal file
11
public/locales/en/modules/ping.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Ping",
|
||||
"description": "Allows you to check if the service is up or returns a specific HTTP status code."
|
||||
},
|
||||
"states": {
|
||||
"online": "Online {{response}}",
|
||||
"offline": "Offline {{response}}",
|
||||
"loading": "Loading..."
|
||||
}
|
||||
}
|
||||
9
public/locales/en/modules/search.json
Normal file
9
public/locales/en/modules/search.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Search Bar",
|
||||
"description": "Search bar to search the web, youtube, torrents or overseerr"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Search the web..."
|
||||
}
|
||||
}
|
||||
40
public/locales/en/modules/torrents-status.json
Normal file
40
public/locales/en/modules/torrents-status.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Torrent",
|
||||
"description": "Show the current download speed of supported services",
|
||||
"settings": {
|
||||
"hideComplete": {
|
||||
"label": "Hide completed torrents"
|
||||
}
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "Name",
|
||||
"size": "Size",
|
||||
"download": "Down",
|
||||
"upload": "Up",
|
||||
"estimatedTimeOfArrival": "ETA",
|
||||
"progress": "Progress"
|
||||
},
|
||||
"body": {
|
||||
"nothingFound": "No torrents found"
|
||||
}
|
||||
},
|
||||
"lineChart": {
|
||||
"title": "Current download speed",
|
||||
"download": "Download: {{download}}",
|
||||
"upload": "Upload: {{upload}}",
|
||||
"timeSpan": "{{seconds}} seconds ago",
|
||||
"totalDownload": "Download: {{download}}/s",
|
||||
"totalUpload": "Upload: {{upload}}/s"
|
||||
},
|
||||
"errors": {
|
||||
"noDownloadClients": {
|
||||
"title": "No supported download clients found!",
|
||||
"text": "Add a download service to view your current downloads"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
public/locales/en/modules/weather.json
Normal file
32
public/locales/en/modules/weather.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Weather",
|
||||
"description": "Look up the current weather in your location",
|
||||
"settings": {
|
||||
"displayInFahrenheit": {
|
||||
"label": "Display in Fahrenheit"
|
||||
},
|
||||
"location": {
|
||||
"label": "Weather location"
|
||||
}
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
"weatherDescriptions": {
|
||||
"clear": "Clear",
|
||||
"mainlyClear": "Mainly clear",
|
||||
"fog": "Fog",
|
||||
"drizzle": "Drizzle",
|
||||
"freezingDrizzle": "Freezing drizzle",
|
||||
"rain": "Rain",
|
||||
"freezingRain": "Freezing rain",
|
||||
"snowFall": "Snow fall",
|
||||
"snowGrains": "Snow grains",
|
||||
"rainShowers": "Rain showers",
|
||||
"snowShowers": "Snow showers",
|
||||
"thunderstorm": "Thunderstorm",
|
||||
"thunderstormWithHail": "Thunderstorm with hail",
|
||||
"unknown": "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
14
public/locales/en/settings/common.json
Normal file
14
public/locales/en/settings/common.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": "Settings",
|
||||
"tooltip": "Settings",
|
||||
"tabs": {
|
||||
"common": "Common",
|
||||
"customizations": "Customizations"
|
||||
},
|
||||
"tips": {
|
||||
"configTip": "Upload your config file by dragging and dropping it onto the page!"
|
||||
},
|
||||
"credits": {
|
||||
"madeWithLove": "Made with ❤️ by @"
|
||||
}
|
||||
}
|
||||
3
public/locales/en/settings/customization/app-width.json
Normal file
3
public/locales/en/settings/customization/app-width.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "App Width"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"suffix": "{{color}} color"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "App Opacity"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"pageTitle": {
|
||||
"label": "Page Title",
|
||||
"placeholder": "Homarr 🦞"
|
||||
},
|
||||
"logo": {
|
||||
"label": "Logo",
|
||||
"placeholder": "/img/logo.png"
|
||||
},
|
||||
"favicon": {
|
||||
"label": "Favicon",
|
||||
"placeholder": "/favicon.png"
|
||||
},
|
||||
"background": {
|
||||
"label": "Background",
|
||||
"placeholder": "/img/background.png"
|
||||
},
|
||||
"buttons": {
|
||||
"submit": "Submit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Shade"
|
||||
}
|
||||
3
public/locales/en/settings/general/color-schema.json
Normal file
3
public/locales/en/settings/general/color-schema.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Switch to {{scheme}} mode"
|
||||
}
|
||||
55
public/locales/en/settings/general/config-changer.json
Normal file
55
public/locales/en/settings/general/config-changer.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"configSelect": {
|
||||
"label": "Config loader"
|
||||
},
|
||||
"modal": {
|
||||
"title": "Choose the name of your new config",
|
||||
"form": {
|
||||
"configName": {
|
||||
"label": "Config name",
|
||||
"placeholder": "Your new config name"
|
||||
},
|
||||
"submitButton": "Confirm"
|
||||
},
|
||||
"events": {
|
||||
"configSaved": {
|
||||
"title": "Config saved",
|
||||
"message": "Config saved as {{configName}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"download": "Download config",
|
||||
"delete": {
|
||||
"text": "Delete config",
|
||||
"notifications": {
|
||||
"deleted": {
|
||||
"title": "Config deleted",
|
||||
"message": "Config deleted"
|
||||
},
|
||||
"deleteFailed": {
|
||||
"title": "Config delete failed",
|
||||
"message": "Config delete failed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"saveCopy": "Save a copy"
|
||||
},
|
||||
"dropzone": {
|
||||
"notifications": {
|
||||
"invalidConfig": {
|
||||
"title": "Unable to load config",
|
||||
"message": "Could not load your config. Invalid JSON format."
|
||||
},
|
||||
"loadedSuccessfully": {
|
||||
"title": "Config {{configName}} loaded successfully"
|
||||
}
|
||||
},
|
||||
"accept": {
|
||||
"text": "Drag files here to upload a config. Support for JSON only."
|
||||
},
|
||||
"reject": {
|
||||
"text": "This file format is not supported. Please only upload JSON."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Language"
|
||||
}
|
||||
3
public/locales/en/settings/general/module-enabler.json
Normal file
3
public/locales/en/settings/general/module-enabler.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"title": "Module enabler"
|
||||
}
|
||||
11
public/locales/en/settings/general/search-engine.json
Normal file
11
public/locales/en/settings/general/search-engine.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"title": "Search engine",
|
||||
"tips": {
|
||||
"generalTip": "Use the prefixes !yt and !t in front of your query to search on YouTube or for a Torrent respectively.",
|
||||
"placeholderTip": "%s can be used as a placeholder for the query."
|
||||
},
|
||||
"customEngine": {
|
||||
"label": "Query URL",
|
||||
"placeholder": "Custom query URL"
|
||||
}
|
||||
}
|
||||
3
public/locales/en/settings/general/theme-selector.json
Normal file
3
public/locales/en/settings/general/theme-selector.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Switch to {{theme}} mode"
|
||||
}
|
||||
3
public/locales/en/settings/general/widget-positions.json
Normal file
3
public/locales/en/settings/general/widget-positions.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "Position widgets on left"
|
||||
}
|
||||
@@ -22,24 +22,26 @@ import { IconApps } from '@tabler/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { tryMatchPort, ServiceTypeList, StatusCodes } from '../../tools/types';
|
||||
import Tip from '../layout/Tip';
|
||||
|
||||
export function AddItemShelfButton(props: any) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { t } = useTranslation('layout/add-service-app-shelf');
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
size="xl"
|
||||
radius="md"
|
||||
title={<Title order={3}>Add service</Title>}
|
||||
title={<Title order={3}>{t('modal.title')}</Title>}
|
||||
opened={props.opened || opened}
|
||||
onClose={() => setOpened(false)}
|
||||
>
|
||||
<AddAppShelfItemForm setOpened={setOpened} />
|
||||
</Modal>
|
||||
<Tooltip withinPortal label="Add a service">
|
||||
<Tooltip withinPortal label={t('actionIcon.tooltip')}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
radius="md"
|
||||
@@ -84,6 +86,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
const { setOpened } = props;
|
||||
const { config, setConfig } = useConfig();
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const { t } = useTranslation('layout/add-service-app-shelf');
|
||||
|
||||
// Extract all the categories from the services in config
|
||||
const InitialCategories = config.services.reduce((acc, cur) => {
|
||||
@@ -120,13 +123,13 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
try {
|
||||
const _isValid = new URL(value);
|
||||
} catch (e) {
|
||||
return 'Please enter a valid URL';
|
||||
return t('modal.form.validation.invalidUrl');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
status: (value: string[]) => {
|
||||
if (!value.length) {
|
||||
return 'Please select a status code';
|
||||
return t('modal.form.validation.noStatusCodeSelected');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -203,48 +206,48 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
>
|
||||
<Tabs defaultValue="Options">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="Options">Options</Tabs.Tab>
|
||||
<Tabs.Tab value="Advanced Options">Advanced options</Tabs.Tab>
|
||||
<Tabs.Tab value="Options">{t('modal.tabs.options.title')}</Tabs.Tab>
|
||||
<Tabs.Tab value="Advanced Options">{t('modal.tabs.advancedOptions.title')}</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="Options">
|
||||
<Stack>
|
||||
<TextInput
|
||||
required
|
||||
label="Service name"
|
||||
placeholder="Plex"
|
||||
label={t('modal.tabs.options.form.serviceName.label')}
|
||||
placeholder={t('modal.tabs.options.form.serviceName.placeholder')}
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label="Icon URL"
|
||||
label={t('modal.tabs.options.form.iconUrl.label')}
|
||||
placeholder={DEFAULT_ICON}
|
||||
{...form.getInputProps('icon')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label="Service URL"
|
||||
label={t('modal.tabs.options.form.serviceUrl.label')}
|
||||
placeholder="http://localhost:7575"
|
||||
{...form.getInputProps('url')}
|
||||
/>
|
||||
<TextInput
|
||||
label="On Click URL"
|
||||
label={t('modal.tabs.options.form.onClickUrl.label')}
|
||||
placeholder="http://sonarr.example.com"
|
||||
{...form.getInputProps('openedUrl')}
|
||||
/>
|
||||
<Select
|
||||
label="Service type"
|
||||
defaultValue="Other"
|
||||
placeholder="Pick one"
|
||||
label={t('modal.tabs.options.form.serviceType.label')}
|
||||
defaultValue={t('modal.tabs.options.form.serviceType.defaultValue')}
|
||||
placeholder={t('modal.tabs.options.form.serviceType.placeholder')}
|
||||
required
|
||||
searchable
|
||||
data={ServiceTypeList}
|
||||
{...form.getInputProps('type')}
|
||||
/>
|
||||
<Select
|
||||
label="Category"
|
||||
label={t('modal.tabs.options.form.category.label')}
|
||||
data={categories}
|
||||
placeholder="Select a category or create a new one"
|
||||
nothingFound="Nothing found"
|
||||
placeholder={t('modal.tabs.options.form.category.placeholder')}
|
||||
nothingFound={t('modal.tabs.options.form.category.nothingFound')}
|
||||
searchable
|
||||
clearable
|
||||
creatable
|
||||
@@ -253,7 +256,11 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
setCategories([...InitialCategories, query]);
|
||||
return item;
|
||||
}}
|
||||
getCreateLabel={(query) => `+ Create "${query}"`}
|
||||
getCreateLabel={(query) =>
|
||||
t('modal.tabs.options.form.category.createLabel', {
|
||||
query,
|
||||
})
|
||||
}
|
||||
{...form.getInputProps('category')}
|
||||
/>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
@@ -266,23 +273,26 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
<>
|
||||
<TextInput
|
||||
required
|
||||
label="API key"
|
||||
placeholder="Your API key"
|
||||
label={t('modal.tabs.options.form.integrations.apiKey.label')}
|
||||
placeholder={t('modal.tabs.options.form.integrations.apiKey.placeholder')}
|
||||
value={form.values.apiKey}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('apiKey', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.apiKey && 'Invalid API key'}
|
||||
error={
|
||||
form.errors.apiKey &&
|
||||
t('modal.tabs.options.form.integrations.apiKey.validation.noKey')
|
||||
}
|
||||
/>
|
||||
<Tip>
|
||||
Get your API key{' '}
|
||||
{t('modal.tabs.options.form.integrations.apiKey.tip.text')}{' '}
|
||||
<Anchor
|
||||
target="_blank"
|
||||
weight="bold"
|
||||
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||
href={`${hostname}/settings/general`}
|
||||
>
|
||||
here.
|
||||
{t('modal.tabs.options.form.integrations.apiKey.tip.link')}
|
||||
</Anchor>
|
||||
</Tip>
|
||||
</>
|
||||
@@ -291,79 +301,116 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
<>
|
||||
<TextInput
|
||||
required
|
||||
label="Username"
|
||||
placeholder="admin"
|
||||
label={t('modal.tabs.options.form.integrations.qBittorrent.username.label')}
|
||||
placeholder={t(
|
||||
'modal.tabs.options.form.integrations.qBittorrent.username.placeholder'
|
||||
)}
|
||||
value={form.values.username}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('username', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.username && 'Invalid username'}
|
||||
error={
|
||||
form.errors.username &&
|
||||
t(
|
||||
'modal.tabs.options.form.integrations.qBittorrent.username.validation.invalidUsername'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<PasswordInput
|
||||
required
|
||||
label="Password"
|
||||
placeholder="adminadmin"
|
||||
label={t('modal.tabs.options.form.integrations.qBittorrent.password.label')}
|
||||
placeholder={t(
|
||||
'modal.tabs.options.form.integrations.qBittorrent.password.placeholder'
|
||||
)}
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
error={
|
||||
form.errors.password &&
|
||||
t(
|
||||
'modal.tabs.options.form.integrations.qBittorrent.password.validation.invalidPassword'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{form.values.type === 'Deluge' && (
|
||||
<>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="password"
|
||||
label={t('modal.tabs.options.form.integrations.deluge.password.label')}
|
||||
placeholder={t(
|
||||
'modal.tabs.options.form.integrations.deluge.password.placeholder'
|
||||
)}
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
error={
|
||||
form.errors.password &&
|
||||
t(
|
||||
'modal.tabs.options.form.integrations.deluge.password.validation.invalidPassword'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{form.values.type === 'Transmission' && (
|
||||
<>
|
||||
<TextInput
|
||||
label="Username"
|
||||
placeholder="admin"
|
||||
label={t('modal.tabs.options.form.integrations.transmission.username.label')}
|
||||
placeholder={t(
|
||||
'modal.tabs.options.form.integrations.transmission.username.placeholder'
|
||||
)}
|
||||
value={form.values.username}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('username', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.username && 'Invalid username'}
|
||||
error={
|
||||
form.errors.username &&
|
||||
t(
|
||||
'modal.tabs.options.form.integrations.transmission.username.validation.invalidUsername'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="adminadmin"
|
||||
label={t('modal.tabs.options.form.integrations.transmission.password.label')}
|
||||
placeholder={t(
|
||||
'modal.tabs.options.form.integrations.transmission.password.placeholder'
|
||||
)}
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
error={
|
||||
form.errors.password &&
|
||||
t(
|
||||
'modal.tabs.options.form.integrations.transmission.password.validation.invalidPassword'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="Advanced Options">
|
||||
<Tabs.Panel value={t('modal.tabs.advancedOptions.title')}>
|
||||
<Stack>
|
||||
<MultiSelect
|
||||
required
|
||||
label="HTTP Status Codes"
|
||||
label={t('modal.tabs.advancedOptions.form.httpStatusCodes.label')}
|
||||
data={StatusCodes}
|
||||
placeholder="Select valid status codes"
|
||||
clearButtonLabel="Clear selection"
|
||||
nothingFound="Nothing found"
|
||||
placeholder={t('modal.tabs.advancedOptions.form.httpStatusCodes.placeholder')}
|
||||
clearButtonLabel={t(
|
||||
'modal.tabs.advancedOptions.form.httpStatusCodes.clearButtonLabel'
|
||||
)}
|
||||
nothingFound={t('modal.tabs.advancedOptions.form.httpStatusCodes.nothingFound')}
|
||||
defaultValue={['200']}
|
||||
clearable
|
||||
searchable
|
||||
{...form.getInputProps('status')}
|
||||
/>
|
||||
<Switch
|
||||
label="Open service in new tab"
|
||||
label={t('modal.tabs.advancedOptions.form.openServiceInNewTab.label')}
|
||||
defaultChecked={form.values.newTab}
|
||||
{...form.getInputProps('newTab')}
|
||||
/>
|
||||
@@ -371,7 +418,9 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
<Group grow position="center" mt="xl">
|
||||
<Button type="submit">{props.message ?? 'Add service'}</Button>
|
||||
<Button type="submit">
|
||||
{props.message ?? t('modal.tabs.advancedOptions.form.buttons.submit.content')}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext } from '@dnd-kit/sortable';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
import { SortableAppShelfItem, AppShelfItem } from './AppShelfItem';
|
||||
@@ -36,6 +37,8 @@ const AppShelf = (props: any) => {
|
||||
const [activeId, setActiveId] = useState(null);
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
const { t } = useTranslation('layout/app-shelf');
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
@@ -123,7 +126,7 @@ const AppShelf = (props: any) => {
|
||||
const noCategory = config.services.filter(
|
||||
(e) => e.category === undefined || e.category === null
|
||||
);
|
||||
const downloadEnabled = config.modules?.[DownloadsModule.title]?.enabled ?? false;
|
||||
const downloadEnabled = config.modules?.[DownloadsModule.id]?.enabled ?? false;
|
||||
// Create an item with 0: true, 1: true, 2: true... For each category
|
||||
return (
|
||||
// TODO: Style accordion so that the bar is transparent to the user settings
|
||||
@@ -147,13 +150,13 @@ const AppShelf = (props: any) => {
|
||||
{/* Return the item for all services without category */}
|
||||
{noCategory && noCategory.length > 0 ? (
|
||||
<Accordion.Item key="Other" value="Other">
|
||||
<Accordion.Control>Other</Accordion.Control>
|
||||
<Accordion.Control>{t('accordions.others.text')}</Accordion.Control>
|
||||
<Accordion.Panel>{getItems()}</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
) : null}
|
||||
{downloadEnabled ? (
|
||||
<Accordion.Item key="Downloads" value="Your downloads">
|
||||
<Accordion.Control>Your downloads</Accordion.Control>
|
||||
<Accordion.Control>{t('accordions.downloads.text')}</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Paper
|
||||
p="lg"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ActionIcon, Menu, Modal, Text, useMantineTheme } from '@mantine/core';
|
||||
import { ActionIcon, Menu, Modal, Text } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { useState } from 'react';
|
||||
import { IconCheck as Check, IconEdit as Edit, IconMenu, IconTrash as Trash } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { serviceItem } from '../../tools/types';
|
||||
import { AddAppShelfItemForm } from './AddAppShelfItem';
|
||||
@@ -11,7 +12,7 @@ export default function AppShelfMenu(props: any) {
|
||||
const { service }: { service: serviceItem } = props;
|
||||
const { config, setConfig } = useConfig();
|
||||
const { secondaryColor } = useColorTheme();
|
||||
const theme = useMantineTheme();
|
||||
const { t } = useTranslation('layout/app-shelf-menu');
|
||||
const [opened, setOpened] = useState(false);
|
||||
return (
|
||||
<>
|
||||
@@ -20,9 +21,9 @@ export default function AppShelfMenu(props: any) {
|
||||
radius="md"
|
||||
opened={props.opened || opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title="Modify a service"
|
||||
title={t('modal.title')}
|
||||
>
|
||||
<AddAppShelfItemForm setOpened={setOpened} {...service} message="Save service" />
|
||||
<AddAppShelfItemForm setOpened={setOpened} {...service} message={t('modal.buttons.save')} />
|
||||
</Modal>
|
||||
<Menu
|
||||
withinPortal
|
||||
@@ -44,11 +45,11 @@ export default function AppShelfMenu(props: any) {
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Settings</Menu.Label>
|
||||
<Menu.Label>{t('menu.labels.settings')}</Menu.Label>
|
||||
<Menu.Item color={secondaryColor} icon={<Edit />} onClick={() => setOpened(true)}>
|
||||
Edit
|
||||
{t('menu.actions.edit')}
|
||||
</Menu.Item>
|
||||
<Menu.Label>Danger zone</Menu.Label>
|
||||
<Menu.Label>{t('menu.labels.dangerZone')}</Menu.Label>
|
||||
<Menu.Item
|
||||
color="red"
|
||||
onClick={(e: any) => {
|
||||
@@ -70,7 +71,7 @@ export default function AppShelfMenu(props: any) {
|
||||
}}
|
||||
icon={<Trash />}
|
||||
>
|
||||
Delete
|
||||
{t('menu.actions.delete')}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { createStyles, Switch, Group, useMantineColorScheme, Kbd } from '@mantine/core';
|
||||
import { IconSun as Sun, IconMoonStars as MoonStars } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
@@ -33,6 +34,7 @@ export function ColorSchemeSwitch() {
|
||||
const { config } = useConfig();
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
const { classes, cx } = useStyles();
|
||||
const { t } = useTranslation('settings/general/theme-selector');
|
||||
|
||||
return (
|
||||
<Group>
|
||||
@@ -41,7 +43,9 @@ export function ColorSchemeSwitch() {
|
||||
<MoonStars className={cx(classes.icon, classes.iconDark)} size={18} />
|
||||
<Switch checked={colorScheme === 'dark'} onChange={() => toggleColorScheme()} size="md" />
|
||||
</div>
|
||||
Switch to {colorScheme === 'dark' ? 'light' : 'dark'} mode
|
||||
{t('label', {
|
||||
theme: colorScheme === 'dark' ? 'light' : 'dark',
|
||||
})}
|
||||
<Group spacing={2}>
|
||||
<Kbd>Ctrl</Kbd>+<Kbd>J</Kbd>
|
||||
</Group>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Center, Loader, Select, Tooltip } from '@mantine/core';
|
||||
import { setCookie } from 'cookies-next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
@@ -7,6 +8,8 @@ export default function ConfigChanger() {
|
||||
const { config, loadConfig, setConfig, getConfigs } = useConfig();
|
||||
const [configList, setConfigList] = useState<string[]>([]);
|
||||
const [value, setValue] = useState(config.name);
|
||||
const { t } = useTranslation('settings/general/config-changer');
|
||||
|
||||
useEffect(() => {
|
||||
getConfigs().then((configs) => setConfigList(configs));
|
||||
}, [config]);
|
||||
@@ -23,7 +26,7 @@ export default function ConfigChanger() {
|
||||
// return <Select data={[{ value: '1', label: '1' },]} onChange={(e) => console.log(e)} value="1" />;
|
||||
return (
|
||||
<Select
|
||||
label="Config loader"
|
||||
label={t('configSelect.label')}
|
||||
value={value}
|
||||
defaultValue={config.name}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IconX as X, IconCheck as Check, IconX, IconPhoto, IconUpload } from '@t
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { setCookie } from 'cookies-next';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { Config } from '../../tools/types';
|
||||
import { migrateToIdConfig } from '../../tools/migrate';
|
||||
@@ -10,6 +11,7 @@ import { migrateToIdConfig } from '../../tools/migrate';
|
||||
export default function LoadConfigComponent(props: any) {
|
||||
const { setConfig } = useConfig();
|
||||
const theme = useMantineTheme();
|
||||
const { t } = useTranslation('settings/general/config-changer');
|
||||
|
||||
return (
|
||||
<Dropzone.FullScreen
|
||||
@@ -20,10 +22,10 @@ export default function LoadConfigComponent(props: any) {
|
||||
} catch (e) {
|
||||
showNotification({
|
||||
autoClose: 5000,
|
||||
title: <Text>Error</Text>,
|
||||
title: <Text>{t('dropzone.notifications.invalidConfig.title')}</Text>,
|
||||
color: 'red',
|
||||
icon: <X />,
|
||||
message: 'could not load your config. Invalid JSON format.',
|
||||
message: t('dropzone.notifications.invalidConfig.message'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -33,7 +35,9 @@ export default function LoadConfigComponent(props: any) {
|
||||
radius: 'md',
|
||||
title: (
|
||||
<Text>
|
||||
Config <b>{newConfig.name}</b> loaded successfully
|
||||
{t('dropzone.notifications.loadedSuccessfully.title', {
|
||||
configName: newConfig.name,
|
||||
})}
|
||||
</Text>
|
||||
),
|
||||
color: 'green',
|
||||
@@ -58,7 +62,7 @@ export default function LoadConfigComponent(props: any) {
|
||||
stroke={1.5}
|
||||
color={theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 4 : 6]}
|
||||
/>
|
||||
Drag files here to upload a config. Support for JSON only.
|
||||
{t('dropzone.accept.text')}
|
||||
</Text>
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
@@ -68,7 +72,7 @@ export default function LoadConfigComponent(props: any) {
|
||||
stroke={1.5}
|
||||
color={theme.colors.red[theme.colorScheme === 'dark' ? 4 : 6]}
|
||||
/>
|
||||
This file format is not supported. Please only upload JSON.
|
||||
{t('dropzone.reject.text')}
|
||||
</Text>
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { showNotification } from '@mantine/notifications';
|
||||
import axios from 'axios';
|
||||
import fileDownload from 'js-file-download';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
IconCheck as Check,
|
||||
IconDownload as Download,
|
||||
@@ -16,6 +17,7 @@ import { useConfig } from '../../tools/state';
|
||||
export default function SaveConfigComponent(props: any) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation('settings/general/config-changer');
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
configName: config.name,
|
||||
@@ -28,39 +30,34 @@ export default function SaveConfigComponent(props: any) {
|
||||
}
|
||||
return (
|
||||
<Group spacing="xs">
|
||||
<Modal
|
||||
radius="md"
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title="Choose the name of your new config"
|
||||
>
|
||||
<Modal radius="md" opened={opened} onClose={() => setOpened(false)} title={t('modal.title')}>
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
setConfig({ ...config, name: values.configName });
|
||||
setOpened(false);
|
||||
showNotification({
|
||||
title: 'Config saved',
|
||||
title: t('modal.events.configSaved.title'),
|
||||
icon: <Check />,
|
||||
color: 'green',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: `Config saved as ${values.configName}`,
|
||||
message: t('modal.events.configSaved.message', { configName: values.configName }),
|
||||
});
|
||||
})}
|
||||
>
|
||||
<TextInput
|
||||
required
|
||||
label="Config name"
|
||||
placeholder="Your new config name"
|
||||
label={t('modal.form.configName.label')}
|
||||
placeholder={t('modal.form.configName.placeholder')}
|
||||
{...form.getInputProps('configName')}
|
||||
/>
|
||||
<Group position="right" mt="md">
|
||||
<Button type="submit">Confirm</Button>
|
||||
<Button type="submit">{t('modal.form.submitButton')}</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Modal>
|
||||
<Button size="xs" leftIcon={<Download />} variant="outline" onClick={onClick}>
|
||||
Download config
|
||||
{t('buttons.download')}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -71,31 +68,31 @@ export default function SaveConfigComponent(props: any) {
|
||||
.delete(`/api/configs/${config.name}`)
|
||||
.then(() => {
|
||||
showNotification({
|
||||
title: 'Config deleted',
|
||||
title: t('buttons.delete.notifications.deleted.title'),
|
||||
icon: <Check />,
|
||||
color: 'green',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: 'Config deleted',
|
||||
message: t('buttons.delete.notifications.deleted.message'),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
showNotification({
|
||||
title: 'Config delete failed',
|
||||
title: t('buttons.delete.notifications.deleteFailed.title'),
|
||||
icon: <X />,
|
||||
color: 'red',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: 'Config delete failed',
|
||||
message: t('buttons.delete.notifications.deleteFailed.message'),
|
||||
});
|
||||
});
|
||||
setConfig({ ...config, name: 'default' });
|
||||
}}
|
||||
>
|
||||
Delete config
|
||||
{t('buttons.delete.text')}
|
||||
</Button>
|
||||
<Button size="xs" leftIcon={<Plus />} variant="outline" onClick={() => setOpened(true)}>
|
||||
Save a copy
|
||||
{t('buttons.saveCopy')}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TextInput, Button, Stack } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { ColorSelector } from './ColorSelector';
|
||||
import { OpacitySelector } from './OpacitySelector';
|
||||
@@ -8,6 +9,7 @@ import { ShadeSelector } from './ShadeSelector';
|
||||
|
||||
export default function TitleChanger() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
@@ -40,19 +42,27 @@ export default function TitleChanger() {
|
||||
<Stack mb="md" mr="sm" mt="xs">
|
||||
<form onSubmit={form.onSubmit((values) => saveChanges(values))}>
|
||||
<Stack>
|
||||
<TextInput label="Page title" placeholder="Homarr 🦞" {...form.getInputProps('title')} />
|
||||
<TextInput label="Logo" placeholder="/img/logo.png" {...form.getInputProps('logo')} />
|
||||
<TextInput
|
||||
label="Favicon"
|
||||
placeholder="/favicon.png"
|
||||
label={t('pageTitle.label')}
|
||||
placeholder={t('pageTitle.placeholder')}
|
||||
{...form.getInputProps('title')}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('logo.label')}
|
||||
placeholder={t('logo.placeholder')}
|
||||
{...form.getInputProps('logo')}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('favicon.label')}
|
||||
placeholder={t('favicon.placeholder')}
|
||||
{...form.getInputProps('favicon')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Background"
|
||||
placeholder="/img/background.png"
|
||||
label={t('background.label')}
|
||||
placeholder={t('background.placeholder')}
|
||||
{...form.getInputProps('background')}
|
||||
/>
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit">{t('buttons.submit')}</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
<ColorSelector type="primary" />
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Text, Slider, Stack } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export function AppCardWidthSelector() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation('settings/customization/app-width');
|
||||
|
||||
const setappCardWidth = (appCardWidth: number) => {
|
||||
setConfig({
|
||||
@@ -17,7 +19,7 @@ export function AppCardWidthSelector() {
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Text>App Width</Text>
|
||||
<Text>{t('label')}</Text>
|
||||
<Slider
|
||||
label={null}
|
||||
defaultValue={config.settings.appCardWidth}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ColorSwatch, Grid, Group, Popover, Text, useMantineTheme } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
|
||||
@@ -10,8 +11,8 @@ interface ColorControlProps {
|
||||
export function ColorSelector({ type }: ColorControlProps) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const { primaryColor, secondaryColor, setPrimaryColor, setSecondaryColor } = useColorTheme();
|
||||
const { t } = useTranslation('settings/customization/color-selector');
|
||||
|
||||
const theme = useMantineTheme();
|
||||
const colors = Object.keys(theme.colors).map((color) => ({
|
||||
@@ -82,7 +83,11 @@ export function ColorSelector({ type }: ColorControlProps) {
|
||||
</Grid>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Text>{type[0].toUpperCase() + type.slice(1)} color</Text>
|
||||
<Text>
|
||||
{t('suffix', {
|
||||
color: type[0].toUpperCase() + type.slice(1),
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Text, SegmentedControl, TextInput, Stack } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
|
||||
import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch';
|
||||
@@ -7,9 +8,11 @@ import ConfigChanger from '../Config/ConfigChanger';
|
||||
import SaveConfigComponent from '../Config/SaveConfig';
|
||||
import ModuleEnabler from './ModuleEnabler';
|
||||
import Tip from '../layout/Tip';
|
||||
import LanguageSwitch from './LanguageSwitch';
|
||||
|
||||
export default function CommonSettings(args: any) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation(['settings/general/search-engine', 'settings/common']);
|
||||
|
||||
const matches = [
|
||||
{ label: 'Google', value: 'https://google.com/search?q=' },
|
||||
@@ -26,15 +29,12 @@ export default function CommonSettings(args: any) {
|
||||
return (
|
||||
<Stack mb="md" mr="sm">
|
||||
<Stack spacing={0} mt="xs">
|
||||
<Text>Search engine</Text>
|
||||
<Tip>
|
||||
Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube or
|
||||
for a Torrent respectively.
|
||||
</Tip>
|
||||
<Text>{t('title')}</Text>
|
||||
<Tip>{t('tips.generalTip')}</Tip>
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
mb="sm"
|
||||
title="Search engine"
|
||||
title={t('title')}
|
||||
value={
|
||||
// Match config.settings.searchUrl with a key in the matches array
|
||||
searchUrl
|
||||
@@ -56,10 +56,10 @@ export default function CommonSettings(args: any) {
|
||||
/>
|
||||
{searchUrl === 'Custom' && (
|
||||
<>
|
||||
<Tip>%s can be used as a placeholder for the query.</Tip>
|
||||
<Tip>{t('tips.placeholderTip')}</Tip>
|
||||
<TextInput
|
||||
label="Query URL"
|
||||
placeholder="Custom query URL"
|
||||
label={t('customEngine.label')}
|
||||
placeholder={t('customEngine.placeholder')}
|
||||
value={customSearchUrl}
|
||||
onChange={(event) => {
|
||||
setCustomSearchUrl(event.currentTarget.value);
|
||||
@@ -78,9 +78,10 @@ export default function CommonSettings(args: any) {
|
||||
<ColorSchemeSwitch />
|
||||
<WidgetsPositionSwitch />
|
||||
<ModuleEnabler />
|
||||
<LanguageSwitch />
|
||||
<ConfigChanger />
|
||||
<SaveConfigComponent />
|
||||
<Tip>Upload your config file by dragging and dropping it onto the page!</Tip>
|
||||
<Tip>{t('settings/common:tips.configTip')}</Tip>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Group, ActionIcon, Anchor, Text } from '@mantine/core';
|
||||
import { IconBrandDiscord, IconBrandGithub } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { CURRENT_VERSION } from '../../../data/constants';
|
||||
|
||||
export default function Credits(props: any) {
|
||||
const { t } = useTranslation('settings/common');
|
||||
|
||||
return (
|
||||
<Group position="center" mt="xs">
|
||||
<Group spacing={0}>
|
||||
@@ -27,7 +31,7 @@ export default function Credits(props: any) {
|
||||
color: 'gray',
|
||||
}}
|
||||
>
|
||||
Made with ❤️ by @
|
||||
{t('credits.madeWithLove')}
|
||||
<Anchor
|
||||
href="https://github.com/ajnart"
|
||||
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||
|
||||
112
src/components/Settings/LanguageSwitch.tsx
Normal file
112
src/components/Settings/LanguageSwitch.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Group, Image, Select, Stack, Text } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getCookie, setCookie } from 'cookies-next';
|
||||
import { getLanguageByCode, Language } from '../../languages/language';
|
||||
|
||||
export default function LanguageSwitch() {
|
||||
const { t, i18n } = useTranslation('settings/general/internationalization');
|
||||
const { changeLanguage } = i18n;
|
||||
const configLocale = getCookie('config-locale');
|
||||
const { locale, locales } = useRouter();
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>(
|
||||
(configLocale as string) ?? locale
|
||||
);
|
||||
|
||||
const data = locales
|
||||
? locales.map((localeItem) => ({
|
||||
value: localeItem,
|
||||
label: getLanguageByCode(localeItem).originalName,
|
||||
image: `imgs/flags/${localeItem}.png`,
|
||||
language: getLanguageByCode(localeItem),
|
||||
}))
|
||||
: [];
|
||||
|
||||
const onChangeSelect = (value: string) => {
|
||||
setSelectedLanguage(value);
|
||||
|
||||
const newLanguage = getLanguageByCode(value);
|
||||
changeLanguage(value)
|
||||
.then(() => {
|
||||
setCookie('config-locale', value, {
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
showNotification({
|
||||
title: 'Language changed',
|
||||
message: `You changed the language to '${newLanguage.originalName}'`,
|
||||
color: 'green',
|
||||
autoClose: 5000,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
showNotification({
|
||||
title: 'Failed to change language',
|
||||
message: `Failed to change to '${newLanguage.originalName}', Error:'${err}`,
|
||||
color: 'red',
|
||||
autoClose: 5000,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Select
|
||||
icon={
|
||||
<Image
|
||||
width={30}
|
||||
height={18}
|
||||
src={`/imgs/flags/${selectedLanguage}.png`}
|
||||
alt="country flag"
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: 1.5,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={t('label')}
|
||||
data={data}
|
||||
itemComponent={SelectItem}
|
||||
nothingFound="Nothing found"
|
||||
onChange={onChangeSelect}
|
||||
value={selectedLanguage}
|
||||
defaultValue={locale}
|
||||
styles={{
|
||||
icon: {
|
||||
width: 42,
|
||||
},
|
||||
input: {
|
||||
paddingLeft: '45px !important',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
image: string;
|
||||
language: Language;
|
||||
}
|
||||
|
||||
const SelectItem = forwardRef<HTMLDivElement, ItemProps>(
|
||||
({ language, image, ...others }: ItemProps, ref) => (
|
||||
<div ref={ref} {...others}>
|
||||
<Group noWrap>
|
||||
<Image src={image} width={30} height={20} radius="xs" />
|
||||
|
||||
<div>
|
||||
<Text size="sm">
|
||||
{language.originalName} ({language.translatedName})
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
@@ -1,35 +1,56 @@
|
||||
import { Checkbox, SimpleGrid, Stack, Title } from '@mantine/core';
|
||||
import { Checkbox, HoverCard, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import * as Modules from '../../modules';
|
||||
import { IModule } from '../../modules/ModuleTypes';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export default function ModuleEnabler(props: any) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation('settings/general/module-enabler');
|
||||
const modules = Object.values(Modules).map((module) => module);
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={4}>Module enabler</Title>
|
||||
<SimpleGrid cols={3} spacing="xs">
|
||||
<Title order={4}>{t('title')}</Title>
|
||||
<SimpleGrid cols={3} spacing="sm">
|
||||
{modules.map((module) => (
|
||||
<Checkbox
|
||||
key={module.title}
|
||||
size="md"
|
||||
checked={config.modules?.[module.title]?.enabled ?? false}
|
||||
label={`${module.title}`}
|
||||
onChange={(e) => {
|
||||
setConfig({
|
||||
...config,
|
||||
modules: {
|
||||
...config.modules,
|
||||
[module.title]: {
|
||||
...config.modules?.[module.title],
|
||||
enabled: e.currentTarget.checked,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ModuleToggle key={module.id} module={module} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const ModuleToggle = ({ module }: { module: IModule }) => {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation(`modules/${module.id}`);
|
||||
|
||||
return (
|
||||
<HoverCard withArrow withinPortal width={200} shadow="md" openDelay={200}>
|
||||
<HoverCard.Target>
|
||||
<Checkbox
|
||||
key={module.id}
|
||||
size="md"
|
||||
checked={config.modules?.[module.id]?.enabled ?? false}
|
||||
label={t('descriptor.name', {
|
||||
defaultValue: 'Unknown',
|
||||
})}
|
||||
onChange={(e) => {
|
||||
setConfig({
|
||||
...config,
|
||||
modules: {
|
||||
...config.modules,
|
||||
[module.id]: {
|
||||
...config.modules?.[module.id],
|
||||
enabled: e.currentTarget.checked,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>
|
||||
<Title order={4}>{t('descriptor.name')}</Title>
|
||||
<Text size="sm">{t('descriptor.description')}</Text>
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Text, Slider, Stack } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export function OpacitySelector() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation('settings/customization/opacity-selector');
|
||||
|
||||
const MARKS = [
|
||||
{ value: 10, label: '10' },
|
||||
@@ -30,7 +32,7 @@ export function OpacitySelector() {
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Text>App Opacity</Text>
|
||||
<Text>{t('label')}</Text>
|
||||
<Slider
|
||||
defaultValue={config.settings.appOpacity || 100}
|
||||
step={10}
|
||||
|
||||
@@ -2,16 +2,20 @@ import { ActionIcon, Title, Tooltip, Drawer, Tabs, ScrollArea } from '@mantine/c
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import { useState } from 'react';
|
||||
import { IconSettings } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import AdvancedSettings from './AdvancedSettings';
|
||||
import CommonSettings from './CommonSettings';
|
||||
import Credits from './Credits';
|
||||
|
||||
function SettingsMenu(props: any) {
|
||||
const { t } = useTranslation('settings/common');
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="Common">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="Common">Common</Tabs.Tab>
|
||||
<Tabs.Tab value="Customizations">Customizations</Tabs.Tab>
|
||||
<Tabs.Tab value="Common">{t('tabs.common')}</Tabs.Tab>
|
||||
<Tabs.Tab value="Customizations">{t('tabs.customizations')}</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel data-autofocus value="Common">
|
||||
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
|
||||
@@ -29,22 +33,24 @@ function SettingsMenu(props: any) {
|
||||
|
||||
export function SettingsMenuButton(props: any) {
|
||||
useHotkeys([['ctrl+L', () => setOpened(!opened)]]);
|
||||
const { t } = useTranslation('settings/common');
|
||||
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
size="xl"
|
||||
padding="lg"
|
||||
position="right"
|
||||
title={<Title order={5}>Settings</Title>}
|
||||
title={<Title order={5}>{t('title')}</Title>}
|
||||
opened={props.opened || opened}
|
||||
onClose={() => setOpened(false)}
|
||||
>
|
||||
<SettingsMenu />
|
||||
<Credits />
|
||||
</Drawer>
|
||||
<Tooltip label="Settings">
|
||||
<Tooltip label={t('tooltip')}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
radius="md"
|
||||
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
Stack,
|
||||
Grid,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
|
||||
export function ShadeSelector() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { t } = useTranslation('settings/customization/shade-selector');
|
||||
|
||||
const { primaryColor, secondaryColor, primaryShade, setPrimaryShade } = useColorTheme();
|
||||
|
||||
@@ -94,7 +96,7 @@ export function ShadeSelector() {
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Text>Shade</Text>
|
||||
<Text>{t('label')}</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createStyles, Switch, Group } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
@@ -33,6 +34,7 @@ export function WidgetsPositionSwitch() {
|
||||
const { classes, cx } = useStyles();
|
||||
const defaultPosition = config?.settings?.widgetPosition || 'right';
|
||||
const [widgetPosition, setWidgetPosition] = useState(defaultPosition);
|
||||
const { t } = useTranslation('settings/general/widget-positions');
|
||||
const toggleWidgetPosition = () => {
|
||||
const position = widgetPosition === 'right' ? 'left' : 'right';
|
||||
setWidgetPosition(position);
|
||||
@@ -54,7 +56,7 @@ export function WidgetsPositionSwitch() {
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
Position widgets on left
|
||||
{t('label')}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Text } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
interface TipProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Tip(props: TipProps) {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
@@ -13,7 +16,8 @@ export default function Tip(props: TipProps) {
|
||||
marginBottom: '0.5rem',
|
||||
}}
|
||||
>
|
||||
Tip: {props.children}
|
||||
{t('tip')}
|
||||
{props.children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
27
src/languages/language.ts
Normal file
27
src/languages/language.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export class Language {
|
||||
shortName: string;
|
||||
originalName: string;
|
||||
translatedName: string;
|
||||
|
||||
constructor(shortName: string, originalName: string, translatedName: string) {
|
||||
this.shortName = shortName;
|
||||
this.originalName = originalName;
|
||||
this.translatedName = translatedName;
|
||||
}
|
||||
}
|
||||
|
||||
const languages: Language[] = [
|
||||
{
|
||||
shortName: 'de',
|
||||
originalName: 'Deutsch',
|
||||
translatedName: 'German',
|
||||
},
|
||||
{
|
||||
shortName: 'en',
|
||||
originalName: 'English',
|
||||
translatedName: 'English',
|
||||
},
|
||||
];
|
||||
|
||||
export const getLanguageByCode = (code: string | null) =>
|
||||
languages.find((language) => language.shortName === code) ?? languages[-1];
|
||||
2
src/modules/ModuleTypes.d.ts
vendored
2
src/modules/ModuleTypes.d.ts
vendored
@@ -6,8 +6,8 @@ import { TablerIcon } from '@tabler/icons';
|
||||
|
||||
// Note: Maybe use context to keep track of the modules
|
||||
export interface IModule {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: TablerIcon;
|
||||
component: React.ComponentType;
|
||||
options?: Option;
|
||||
|
||||
@@ -26,16 +26,15 @@ import { useColorTheme } from '../../tools/color';
|
||||
|
||||
export const CalendarModule: IModule = {
|
||||
title: 'Calendar',
|
||||
description:
|
||||
'A calendar module for displaying upcoming releases. It interacts with the Sonarr and Radarr API.',
|
||||
icon: CalendarIcon,
|
||||
component: CalendarComponent,
|
||||
options: {
|
||||
sundaystart: {
|
||||
name: 'Start the week on Sunday',
|
||||
name: 'descriptor.settings.sundayStart.label',
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
id: 'calendar',
|
||||
};
|
||||
|
||||
export default function CalendarComponent(props: any) {
|
||||
@@ -128,7 +127,7 @@ export default function CalendarComponent(props: any) {
|
||||
}, [config.services]);
|
||||
|
||||
const weekStartsAtSunday =
|
||||
(config?.modules?.[CalendarModule.title]?.options?.sundaystart?.value as boolean) ?? false;
|
||||
(config?.modules?.[CalendarModule.id]?.options?.sundaystart?.value as boolean) ?? false;
|
||||
return (
|
||||
<Calendar
|
||||
firstDayOfWeek={weekStartsAtSunday ? 'sunday' : 'monday'}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Badge, Button, Group, Image, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconDownload, IconExternalLink, IconPlayerPlay } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
import { useConfig } from '../../tools/state';
|
||||
@@ -159,6 +160,7 @@ export function SonarrMediaDisplay(props: any) {
|
||||
export function MediaDisplay({ media }: { media: IMedia }) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { secondaryColor } = useColorTheme();
|
||||
const { t } = useTranslation('modules/common-media-cards');
|
||||
|
||||
return (
|
||||
<Group mr="xs" align="stretch" noWrap style={{ maxHeight: 200 }}>
|
||||
@@ -209,7 +211,7 @@ export function MediaDisplay({ media }: { media: IMedia }) {
|
||||
size="sm"
|
||||
rightIcon={<IconPlayerPlay size={15} />}
|
||||
>
|
||||
Play
|
||||
{t('buttons.play')}
|
||||
</Button>
|
||||
)}
|
||||
{media.imdbId && (
|
||||
@@ -249,7 +251,7 @@ export function MediaDisplay({ media }: { media: IMedia }) {
|
||||
size="sm"
|
||||
rightIcon={<IconDownload size={15} />}
|
||||
>
|
||||
Request
|
||||
{t('buttons.request')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createStyles, Stack, Title, useMantineColorScheme, useMantineTheme } from '@mantine/core';
|
||||
import { IconCalendar as CalendarIcon } from '@tabler/icons';
|
||||
import axios from 'axios';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { serviceItem } from '../../tools/types';
|
||||
@@ -9,32 +10,38 @@ import { IModule } from '../ModuleTypes';
|
||||
const asModule = <T extends IModule>(t: T) => t;
|
||||
export const DashdotModule = asModule({
|
||||
title: 'Dash.',
|
||||
description: 'A module for displaying the graphs of your running Dash. instance.',
|
||||
icon: CalendarIcon,
|
||||
component: DashdotComponent,
|
||||
options: {
|
||||
cpuMultiView: {
|
||||
name: 'CPU Multi-Core View',
|
||||
name: 'descriptor.settings.cpuMultiView.label',
|
||||
value: false,
|
||||
},
|
||||
storageMultiView: {
|
||||
name: 'Storage Multi-Drive View',
|
||||
name: 'descriptor.settings.storageMultiView.label',
|
||||
value: false,
|
||||
},
|
||||
useCompactView: {
|
||||
name: 'Use Compact View',
|
||||
name: 'descriptor.settings.useCompactView.label',
|
||||
value: false,
|
||||
},
|
||||
graphs: {
|
||||
name: 'Graphs',
|
||||
name: 'descriptor.settings.graphs.label',
|
||||
value: ['CPU', 'RAM', 'Storage', 'Network'],
|
||||
options: ['CPU', 'RAM', 'Storage', 'Network', 'GPU'],
|
||||
options: [
|
||||
'descriptor.settings.graphs.options.cpu',
|
||||
'descriptor.settings.graphs.options.ram',
|
||||
'descriptor.settings.graphs.options.storage',
|
||||
'descriptor.settings.graphs.options.network',
|
||||
'descriptor.settings.graphs.options.GPU',
|
||||
],
|
||||
},
|
||||
url: {
|
||||
name: 'Dash. URL',
|
||||
name: 'descriptor.settings.url.label',
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
id: 'dashdot',
|
||||
});
|
||||
|
||||
const useStyles = createStyles((theme, _params) => ({
|
||||
@@ -119,7 +126,7 @@ export function DashdotComponent() {
|
||||
const { classes } = useStyles();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
const dashConfig = config.modules?.[DashdotModule.title]
|
||||
const dashConfig = config.modules?.[DashdotModule.id]
|
||||
.options as typeof DashdotModule['options'];
|
||||
const isCompact = dashConfig?.useCompactView?.value ?? false;
|
||||
const dashdotService: serviceItem | undefined = config.services.filter(
|
||||
@@ -141,32 +148,34 @@ export function DashdotComponent() {
|
||||
const totalSize =
|
||||
(info?.storage?.layout as any[])?.reduce((acc, curr) => (curr.size ?? 0) + acc, 0) ?? 0;
|
||||
|
||||
const { t } = useTranslation('modules/dashdot');
|
||||
|
||||
const graphs = [
|
||||
{
|
||||
name: 'CPU',
|
||||
name: t('card.graphs.cpu.title'),
|
||||
enabled: cpuEnabled,
|
||||
params: {
|
||||
multiView: dashConfig?.cpuMultiView?.value ?? false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Storage',
|
||||
name: t('card.graphs.cpu.title'),
|
||||
enabled: storageEnabled && !isCompact,
|
||||
params: {
|
||||
multiView: dashConfig?.storageMultiView?.value ?? false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'RAM',
|
||||
name: t('card.graphs.memory.title'),
|
||||
enabled: ramEnabled,
|
||||
},
|
||||
{
|
||||
name: 'Network',
|
||||
name: t('card.graphs.network.title'),
|
||||
enabled: networkEnabled,
|
||||
spanTwo: true,
|
||||
},
|
||||
{
|
||||
name: 'GPU',
|
||||
name: t('card.graphs.gpu.title'),
|
||||
enabled: gpuEnabled,
|
||||
spanTwo: true,
|
||||
},
|
||||
@@ -175,27 +184,24 @@ export function DashdotComponent() {
|
||||
if (dashdotUrl === '') {
|
||||
return (
|
||||
<div>
|
||||
<h2 className={classes.heading}>Dash.</h2>
|
||||
<p>
|
||||
No dash. service found. Please add one to your Homarr dashboard or set a dashdot URL in
|
||||
the module options
|
||||
</p>
|
||||
<h2 className={classes.heading}>{t('card.title')}</h2>
|
||||
<p>{t('card.errors.noService')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className={classes.heading}>Dash.</h2>
|
||||
<h2 className={classes.heading}>{t('card.title')}</h2>
|
||||
|
||||
{!info ? (
|
||||
<p>Cannot acquire information from dash. - are you running the latest version?</p>
|
||||
<p>{t('card.errors.noInformation')}</p>
|
||||
) : (
|
||||
<div className={classes.graphsContainer}>
|
||||
<div className={classes.table}>
|
||||
{storageEnabled && isCompact && (
|
||||
<div className={classes.tableRow}>
|
||||
<p className={classes.tableLabel}>Storage:</p>
|
||||
<p className={classes.tableLabel}>{t('card.graphs.storage.label')}</p>
|
||||
<p className={classes.tableValue}>
|
||||
{((100 * totalUsed) / (totalSize || 1)).toFixed(1)}%{'\n'}
|
||||
{bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)}
|
||||
@@ -204,10 +210,12 @@ export function DashdotComponent() {
|
||||
)}
|
||||
{networkEnabled && (
|
||||
<div className={classes.tableRow}>
|
||||
<p className={classes.tableLabel}>Network:</p>
|
||||
<p className={classes.tableLabel}>{t('card.graphs.network.label')}</p>
|
||||
<p className={classes.tableValue}>
|
||||
{bpsPrettyPrint(info?.network?.speedUp)} Up{'\n'}
|
||||
{bpsPrettyPrint(info?.network?.speedDown)} Down
|
||||
{bpsPrettyPrint(info?.network?.speedUp)} {t('card.graphs.network.metrics.upload')}
|
||||
{'\n'}
|
||||
{bpsPrettyPrint(info?.network?.speedDown)}
|
||||
{t('card.graphs.network.metrics.download')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -8,22 +8,22 @@ import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval';
|
||||
|
||||
export const DateModule: IModule = {
|
||||
title: 'Date',
|
||||
description: 'Show the current time and date in a card',
|
||||
icon: Clock,
|
||||
component: DateComponent,
|
||||
options: {
|
||||
full: {
|
||||
name: 'Display full time (24-hour)',
|
||||
name: 'descriptor.settings.display24HourFormat.label',
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
id: 'date',
|
||||
};
|
||||
|
||||
export default function DateComponent(props: any) {
|
||||
const [date, setDate] = useState(new Date());
|
||||
const setSafeInterval = useSetSafeInterval();
|
||||
const { config } = useConfig();
|
||||
const isFullTime = config?.modules?.[DateModule.title]?.options?.full?.value ?? true;
|
||||
const isFullTime = config?.modules?.[DateModule.id]?.options?.full?.value ?? true;
|
||||
const formatString = isFullTime ? 'HH:mm' : 'h:mm A';
|
||||
// Change date on minute change
|
||||
// Note: Using 10 000ms instead of 1000ms to chill a little :)
|
||||
|
||||
@@ -11,9 +11,10 @@ import {
|
||||
} from '@tabler/icons';
|
||||
import axios from 'axios';
|
||||
import Dockerode from 'dockerode';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { tryMatchService } from '../../tools/addToHomarr';
|
||||
import { AddAppShelfItemForm } from '../../components/AppShelf/AddAppShelfItem';
|
||||
import { useState } from 'react';
|
||||
|
||||
function sendDockerCommand(
|
||||
action: string,
|
||||
@@ -21,6 +22,8 @@ function sendDockerCommand(
|
||||
containerName: string,
|
||||
reload: () => void
|
||||
) {
|
||||
const { t } = useTranslation('modules/docker');
|
||||
|
||||
showNotification({
|
||||
id: containerId,
|
||||
loading: true,
|
||||
@@ -34,8 +37,8 @@ function sendDockerCommand(
|
||||
.then((res) => {
|
||||
updateNotification({
|
||||
id: containerId,
|
||||
title: `Container ${containerName} ${action}ed`,
|
||||
message: `Your container was successfully ${action}ed`,
|
||||
title: t('messages.successfullyExecuted.message', { containerName, action }),
|
||||
message: t('messages.successfullyExecuted.message', { action }),
|
||||
icon: <IconCheck />,
|
||||
autoClose: 2000,
|
||||
});
|
||||
@@ -44,7 +47,7 @@ function sendDockerCommand(
|
||||
updateNotification({
|
||||
id: containerId,
|
||||
color: 'red',
|
||||
title: 'There was an error',
|
||||
title: t('errors.unknownError.title'),
|
||||
message: err.response.data.reason,
|
||||
autoClose: 2000,
|
||||
});
|
||||
@@ -61,6 +64,8 @@ export interface ContainerActionBarProps {
|
||||
|
||||
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
|
||||
const [opened, setOpened] = useState<boolean>(false);
|
||||
const { t } = useTranslation('modules/docker');
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Modal
|
||||
@@ -68,12 +73,12 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
||||
radius="md"
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title="Add service"
|
||||
title={t('actionBar.addService.title')}
|
||||
>
|
||||
<AddAppShelfItemForm
|
||||
setOpened={setOpened}
|
||||
{...tryMatchService(selected.at(0))}
|
||||
message="Add service to homarr"
|
||||
message={t('actionBar.addService.message')}
|
||||
/>
|
||||
</Modal>
|
||||
<Button
|
||||
@@ -89,7 +94,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
||||
color="orange"
|
||||
radius="md"
|
||||
>
|
||||
Restart
|
||||
{t('actionBar.restart.title')}
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<IconPlayerStop />}
|
||||
@@ -104,7 +109,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
||||
color="red"
|
||||
radius="md"
|
||||
>
|
||||
Stop
|
||||
{t('actionBar.stop.title')}
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<IconPlayerPlay />}
|
||||
@@ -119,10 +124,10 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
||||
color="green"
|
||||
radius="md"
|
||||
>
|
||||
Start
|
||||
{t('actionBar.start.title')}
|
||||
</Button>
|
||||
<Button leftIcon={<IconRefresh />} onClick={() => reload()} variant="light" radius="md">
|
||||
Refresh data
|
||||
{t('actionBar.refreshData.title')}
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<IconPlus />}
|
||||
@@ -133,7 +138,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
||||
if (selected.length !== 1) {
|
||||
showNotification({
|
||||
autoClose: 5000,
|
||||
title: <Title order={5}>Please only add one service at a time!</Title>,
|
||||
title: <Title order={5}>{t('errors.oneServiceAtATime')}</Title>,
|
||||
color: 'red',
|
||||
message: undefined,
|
||||
});
|
||||
@@ -142,7 +147,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add to Homarr
|
||||
{t('actionBar.addToHomarr.title')}
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<IconTrash />}
|
||||
@@ -157,7 +162,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
||||
)
|
||||
}
|
||||
>
|
||||
Remove
|
||||
{t('actionBar.remove.title')}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Badge, BadgeVariant, MantineSize } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Dockerode from 'dockerode';
|
||||
|
||||
export interface ContainerStateProps {
|
||||
@@ -7,6 +8,9 @@ export interface ContainerStateProps {
|
||||
|
||||
export default function ContainerState(props: ContainerStateProps) {
|
||||
const { state } = props;
|
||||
|
||||
const { t } = useTranslation('modules/docker');
|
||||
|
||||
const options: {
|
||||
size: MantineSize;
|
||||
radius: MantineSize;
|
||||
@@ -20,28 +24,28 @@ export default function ContainerState(props: ContainerStateProps) {
|
||||
case 'running': {
|
||||
return (
|
||||
<Badge color="green" {...options}>
|
||||
Running
|
||||
{t('table.states.running')}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
case 'created': {
|
||||
return (
|
||||
<Badge color="cyan" {...options}>
|
||||
Created
|
||||
{t('table.states.created')}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
case 'exited': {
|
||||
return (
|
||||
<Badge color="red" {...options}>
|
||||
Stopped
|
||||
{t('table.states.stopped')}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<Badge color="purple" {...options}>
|
||||
Unknown
|
||||
{t('table.states.unknown')}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ActionIcon, Drawer, Group, LoadingOverlay, Text, Tooltip } from '@mantine/core';
|
||||
import { ActionIcon, Drawer, Text, Tooltip } from '@mantine/core';
|
||||
import axios from 'axios';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Docker from 'dockerode';
|
||||
import { IconBrandDocker, IconX } from '@tabler/icons';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import ContainerActionBar from './ContainerActionBar';
|
||||
import DockerTable from './DockerTable';
|
||||
import { useConfig } from '../../tools/state';
|
||||
@@ -11,9 +13,9 @@ import { IModule } from '../ModuleTypes';
|
||||
|
||||
export const DockerModule: IModule = {
|
||||
title: 'Docker',
|
||||
description: 'Allows you to easily manage your torrents',
|
||||
icon: IconBrandDocker,
|
||||
component: DockerMenuButton,
|
||||
id: 'docker',
|
||||
};
|
||||
|
||||
export default function DockerMenuButton(props: any) {
|
||||
@@ -21,7 +23,9 @@ export default function DockerMenuButton(props: any) {
|
||||
const [containers, setContainers] = useState<Docker.ContainerInfo[]>([]);
|
||||
const [selection, setSelection] = useState<Docker.ContainerInfo[]>([]);
|
||||
const { config } = useConfig();
|
||||
const moduleEnabled = config.modules?.[DockerModule.title]?.enabled ?? false;
|
||||
const moduleEnabled = config.modules?.[DockerModule.id]?.enabled ?? false;
|
||||
|
||||
const { t } = useTranslation('modules/docker');
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
@@ -42,15 +46,15 @@ export default function DockerMenuButton(props: any) {
|
||||
// Send an Error notification
|
||||
showNotification({
|
||||
autoClose: 1500,
|
||||
title: <Text>Docker integration failed</Text>,
|
||||
title: <Text>{t('errors.integrationFailed.title')}</Text>,
|
||||
color: 'red',
|
||||
icon: <IconX />,
|
||||
message: 'Did you forget to mount the docker socket ?',
|
||||
message: t('errors.integrationFailed.message'),
|
||||
})
|
||||
);
|
||||
}, 300);
|
||||
}
|
||||
const exists = config.modules?.[DockerModule.title]?.enabled ?? false;
|
||||
const exists = config.modules?.[DockerModule.id]?.enabled ?? false;
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
@@ -67,7 +71,7 @@ export default function DockerMenuButton(props: any) {
|
||||
>
|
||||
<DockerTable containers={containers} selection={selection} setSelection={setSelection} />
|
||||
</Drawer>
|
||||
<Tooltip label="Docker">
|
||||
<Tooltip label={t('actionIcon.tooltip')}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
radius="md"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Table, Checkbox, Group, Badge, createStyles, ScrollArea, TextInput } from '@mantine/core';
|
||||
import { IconSearch } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import Dockerode from 'dockerode';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ContainerState from './ContainerState';
|
||||
@@ -26,6 +28,8 @@ export default function DockerTable({
|
||||
const { classes, cx } = useStyles();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const { t } = useTranslation('modules/docker');
|
||||
|
||||
useEffect(() => {
|
||||
setContainers(containers);
|
||||
}, [containers]);
|
||||
@@ -80,7 +84,9 @@ export default function DockerTable({
|
||||
</Badge>
|
||||
))}
|
||||
{element.Ports.length > 3 && (
|
||||
<Badge variant="filled">{element.Ports.length - 3} more</Badge>
|
||||
<Badge variant="filled">
|
||||
{t('table.body.portCollapse', { ports: element.Ports.length - 3 })}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</td>
|
||||
@@ -94,7 +100,7 @@ export default function DockerTable({
|
||||
return (
|
||||
<ScrollArea style={{ height: '80vh' }}>
|
||||
<TextInput
|
||||
placeholder="Search by container or image name"
|
||||
placeholder={t('search.placeholder')}
|
||||
mt="md"
|
||||
icon={<IconSearch size={14} />}
|
||||
value={search}
|
||||
@@ -111,10 +117,10 @@ export default function DockerTable({
|
||||
transitionDuration={0}
|
||||
/>
|
||||
</th>
|
||||
<th>Name</th>
|
||||
<th>Image</th>
|
||||
<th>Ports</th>
|
||||
<th>State</th>
|
||||
<th>{t('table.header.name')}</th>
|
||||
<th>{t('table.header.image')}</th>
|
||||
<th>{t('table.header.ports')}</th>
|
||||
<th>{t('table.header.state')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Skeleton,
|
||||
ScrollArea,
|
||||
Center,
|
||||
Stack,
|
||||
} from '@mantine/core';
|
||||
import { IconDownload as Download } from '@tabler/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -15,6 +16,7 @@ import axios from 'axios';
|
||||
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
||||
import { useViewportSize } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { IModule } from '../ModuleTypes';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem';
|
||||
@@ -23,15 +25,15 @@ import { humanFileSize } from '../../tools/humanFileSize';
|
||||
|
||||
export const DownloadsModule: IModule = {
|
||||
title: 'Torrent',
|
||||
description: 'Show the current download speed of supported services',
|
||||
icon: Download,
|
||||
component: DownloadComponent,
|
||||
options: {
|
||||
hidecomplete: {
|
||||
name: 'Hide completed torrents',
|
||||
name: 'descriptor.settings.hideComplete',
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
id: 'torrents-status',
|
||||
};
|
||||
|
||||
export default function DownloadComponent() {
|
||||
@@ -45,10 +47,13 @@ export default function DownloadComponent() {
|
||||
service.type === 'Deluge'
|
||||
) ?? [];
|
||||
const hideComplete: boolean =
|
||||
(config?.modules?.[DownloadsModule.title]?.options?.hidecomplete?.value as boolean) ?? false;
|
||||
(config?.modules?.[DownloadsModule.id]?.options?.hidecomplete?.value as boolean) ?? false;
|
||||
const [torrents, setTorrents] = useState<NormalizedTorrent[]>([]);
|
||||
const setSafeInterval = useSetSafeInterval();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const { t } = useTranslation(`modules/${DownloadsModule.id}`);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
if (downloadServices.length === 0) return;
|
||||
@@ -81,13 +86,13 @@ export default function DownloadComponent() {
|
||||
|
||||
if (downloadServices.length === 0) {
|
||||
return (
|
||||
<Group>
|
||||
<Title order={3}>No supported download clients found!</Title>
|
||||
<Stack>
|
||||
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
||||
<Group>
|
||||
<Text>Add a download service to view your current downloads</Text>
|
||||
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
||||
<AddItemShelfButton />
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,12 +110,12 @@ export default function DownloadComponent() {
|
||||
const DEVICE_WIDTH = 576;
|
||||
const ths = (
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
{width > 576 ? <th>Down</th> : ''}
|
||||
{width > 576 ? <th>Up</th> : ''}
|
||||
<th>ETA</th>
|
||||
<th>Progress</th>
|
||||
<th>{t('card.table.header.name')}</th>
|
||||
<th>{t('card.table.header.size')}</th>
|
||||
{width > 576 ? <th>{t('card.table.header.download')}</th> : ''}
|
||||
{width > 576 ? <th>{t('card.table.header.upload')}</th> : ''}
|
||||
<th>{t('card.table.header.estimatedTimeOfArrival')}</th>
|
||||
<th>{t('card.table.header.progress')}</th>
|
||||
</tr>
|
||||
);
|
||||
// Convert Seconds to readable format.
|
||||
@@ -195,7 +200,7 @@ export default function DownloadComponent() {
|
||||
</Table>
|
||||
) : (
|
||||
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Title order={3}>No torrents found</Title>
|
||||
<Title order={3}>{t('card.table.body.nothingFound')}</Title>
|
||||
</Center>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
||||
import { linearGradientDef } from '@nivo/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Datum, ResponsiveLine } from '@nivo/line';
|
||||
import { useListState } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
@@ -15,9 +16,9 @@ import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval';
|
||||
|
||||
export const TotalDownloadsModule: IModule = {
|
||||
title: 'Download Speed',
|
||||
description: 'Show the current download speed of supported services',
|
||||
icon: Download,
|
||||
component: TotalDownloadsComponent,
|
||||
id: 'dlspeed',
|
||||
};
|
||||
|
||||
interface torrentHistory {
|
||||
@@ -36,6 +37,7 @@ export default function TotalDownloadsComponent() {
|
||||
service.type === 'Transmission' ||
|
||||
service.type === 'Deluge'
|
||||
) ?? [];
|
||||
const { t } = useTranslation(`modules/${TotalDownloadsModule.id}`);
|
||||
|
||||
const [torrentHistory, torrentHistoryHandlers] = useListState<torrentHistory>([]);
|
||||
const [torrents, setTorrents] = useState<NormalizedTorrent[]>([]);
|
||||
@@ -80,14 +82,14 @@ export default function TotalDownloadsComponent() {
|
||||
if (downloadServices.length === 0) {
|
||||
return (
|
||||
<Group>
|
||||
<Title order={4}>No supported download clients found!</Title>
|
||||
<Title order={4}>{t('card.errors.noDownloadClients.title')}</Title>
|
||||
<div>
|
||||
<AddItemShelfButton
|
||||
style={{
|
||||
float: 'inline-end',
|
||||
}}
|
||||
/>
|
||||
Add a download service to view your current downloads
|
||||
{t('card.errors.noDownloadClients.text')}
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
@@ -107,15 +109,19 @@ export default function TotalDownloadsComponent() {
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={4}>Current download speed</Title>
|
||||
<Title order={4}>{t('card.lineChart.title')}</Title>
|
||||
<Stack>
|
||||
<Group>
|
||||
<ColorSwatch size={12} color={theme.colors.green[5]} />
|
||||
<Text>Download: {humanFileSize(totalDownloadSpeed)}/s</Text>
|
||||
<Text>
|
||||
{t('card.lineChart.totalDownload', { download: humanFileSize(totalDownloadSpeed) })}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group>
|
||||
<ColorSwatch size={12} color={theme.colors.blue[5]} />
|
||||
<Text>Upload: {humanFileSize(totalUploadSpeed)}/s</Text>
|
||||
<Text>
|
||||
{t('card.lineChart.totalUpload', { upload: humanFileSize(totalUploadSpeed) })}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
<Box
|
||||
@@ -136,16 +142,20 @@ export default function TotalDownloadsComponent() {
|
||||
const roundedSeconds = Math.round(seconds);
|
||||
return (
|
||||
<Card p="sm" radius="md" withBorder>
|
||||
<Text size="md">{roundedSeconds} seconds ago</Text>
|
||||
<Text size="md">{t('card.lineChart.timeSpan', { seconds: roundedSeconds })}</Text>
|
||||
<Card.Section p="sm">
|
||||
<Stack>
|
||||
<Group>
|
||||
<ColorSwatch size={10} color={theme.colors.green[5]} />
|
||||
<Text size="md">Download: {humanFileSize(Download)}</Text>
|
||||
<Text size="md">
|
||||
{t('card.lineChart.download', { download: humanFileSize(Download) })}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group>
|
||||
<ColorSwatch size={10} color={theme.colors.blue[5]} />
|
||||
<Text size="md">Upload: {humanFileSize(Upload)}</Text>
|
||||
<Text size="md">
|
||||
{t('card.lineChart.upload', { upload: humanFileSize(Upload) })}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
|
||||
@@ -11,12 +11,15 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { IconAdjustments } from '@tabler/icons';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { useConfig } from '../tools/state';
|
||||
import { IModule } from './ModuleTypes';
|
||||
|
||||
function getItems(module: IModule) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation([module.id, 'common']);
|
||||
|
||||
const items: JSX.Element[] = [];
|
||||
if (module.options) {
|
||||
const keys = Object.keys(module.options);
|
||||
@@ -25,8 +28,8 @@ function getItems(module: IModule) {
|
||||
const types = values.map((v) => typeof v.value);
|
||||
// Loop over all the types with a for each loop
|
||||
types.forEach((type, index) => {
|
||||
const optionName = `${module.title}.${keys[index]}`;
|
||||
const moduleInConfig = config.modules?.[module.title];
|
||||
const optionName = `${module.id}.${keys[index]}`;
|
||||
const moduleInConfig = config.modules?.[module.id];
|
||||
if (type === 'object') {
|
||||
items.push(
|
||||
<MultiSelect
|
||||
@@ -43,7 +46,7 @@ function getItems(module: IModule) {
|
||||
...config,
|
||||
modules: {
|
||||
...config.modules,
|
||||
[module.title]: {
|
||||
[module.id]: {
|
||||
...moduleInConfig,
|
||||
options: {
|
||||
...moduleInConfig?.options,
|
||||
@@ -68,12 +71,12 @@ function getItems(module: IModule) {
|
||||
...config,
|
||||
modules: {
|
||||
...config.modules,
|
||||
[module.title]: {
|
||||
...config.modules[module.title],
|
||||
[module.id]: {
|
||||
...config.modules[module.id],
|
||||
options: {
|
||||
...config.modules[module.title].options,
|
||||
...config.modules[module.id].options,
|
||||
[keys[index]]: {
|
||||
...config.modules[module.title].options?.[keys[index]],
|
||||
...config.modules[module.id].options?.[keys[index]],
|
||||
value: (e.target as any)[0].value,
|
||||
},
|
||||
},
|
||||
@@ -96,7 +99,7 @@ function getItems(module: IModule) {
|
||||
onChange={(e) => {}}
|
||||
/>
|
||||
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit">{t('actions.save')}</Button>
|
||||
</Group>
|
||||
</form>
|
||||
);
|
||||
@@ -117,12 +120,12 @@ function getItems(module: IModule) {
|
||||
...config,
|
||||
modules: {
|
||||
...config.modules,
|
||||
[module.title]: {
|
||||
...config.modules[module.title],
|
||||
[module.id]: {
|
||||
...config.modules[module.id],
|
||||
options: {
|
||||
...config.modules[module.title].options,
|
||||
...config.modules[module.id].options,
|
||||
[keys[index]]: {
|
||||
...config.modules[module.title].options?.[keys[index]],
|
||||
...config.modules[module.id].options?.[keys[index]],
|
||||
value: e.currentTarget.checked,
|
||||
},
|
||||
},
|
||||
@@ -130,7 +133,7 @@ function getItems(module: IModule) {
|
||||
},
|
||||
});
|
||||
}}
|
||||
label={values[index].name}
|
||||
label={t(values[index].name)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -145,9 +148,10 @@ export function ModuleWrapper(props: any) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const enabledModules = config.modules ?? {};
|
||||
// Remove 'Module' from enabled modules titles
|
||||
const isShown = enabledModules[module.title]?.enabled ?? false;
|
||||
const isShown = enabledModules[module.id]?.enabled ?? false;
|
||||
//TODO: fix the hover problem
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const { t } = useTranslation('modules');
|
||||
|
||||
if (!isShown) {
|
||||
return null;
|
||||
@@ -156,7 +160,7 @@ export function ModuleWrapper(props: any) {
|
||||
return (
|
||||
<Card
|
||||
{...props}
|
||||
key={module.title}
|
||||
key={module.id}
|
||||
hidden={!isShown}
|
||||
withBorder
|
||||
radius="lg"
|
||||
@@ -186,11 +190,12 @@ export function ModuleWrapper(props: any) {
|
||||
export function ModuleMenu(props: any) {
|
||||
const { module, styles, hovered } = props;
|
||||
const items: JSX.Element[] = getItems(module);
|
||||
const { t } = useTranslation('modules/common');
|
||||
return (
|
||||
<>
|
||||
{module.options && (
|
||||
<Menu
|
||||
key={module.title}
|
||||
key={module.id}
|
||||
withinPortal
|
||||
width="lg"
|
||||
shadow="xl"
|
||||
@@ -217,7 +222,7 @@ export function ModuleMenu(props: any) {
|
||||
</motion.div>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Settings</Menu.Label>
|
||||
<Menu.Label>{t('settings.label')}</Menu.Label>
|
||||
{items.map((item) => (
|
||||
<Menu.Item key={item.key}>{item}</Menu.Item>
|
||||
))}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { IModule } from '../ModuleTypes';
|
||||
|
||||
export const OverseerrModule: IModule = {
|
||||
title: 'Overseerr',
|
||||
description: 'Allows you to search and add media from Overseerr/Jellyseerr',
|
||||
icon: IconEyeglass,
|
||||
component: OverseerrMediaDisplay,
|
||||
id: 'overseerr',
|
||||
};
|
||||
|
||||
export interface OverseerSearchProps {
|
||||
|
||||
@@ -3,6 +3,8 @@ import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import { IconAlertCircle, IconCheck, IconDownload } from '@tabler/icons';
|
||||
import axios from 'axios';
|
||||
import Consola from 'consola';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
import { MovieResult } from './Movie.d';
|
||||
@@ -27,6 +29,7 @@ const useStyles = createStyles((theme) => ({
|
||||
export function RequestModal({ base, opened, setOpened }: RequestModalProps) {
|
||||
const [result, setResult] = useState<MovieResult | TvShowResult>();
|
||||
const { secondaryColor } = useColorTheme();
|
||||
|
||||
function getResults(base: Result) {
|
||||
axios.get(`/api/modules/overseerr/${base.id}?type=${base.mediaType}`).then((res) => {
|
||||
setResult(res.data);
|
||||
@@ -55,6 +58,8 @@ export function MovieRequestModal({
|
||||
setOpened: (opened: boolean) => void;
|
||||
}) {
|
||||
const { secondaryColor } = useColorTheme();
|
||||
const { t } = useTranslation('modules/overseerr');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={() => setOpened(false)}
|
||||
@@ -67,23 +72,23 @@ export function MovieRequestModal({
|
||||
title={
|
||||
<Group>
|
||||
<IconDownload />
|
||||
Ask for {result.title}
|
||||
{t('popup.item.buttons.askFor', { title: result.title })}
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack>
|
||||
<Alert
|
||||
icon={<IconAlertCircle size={16} />}
|
||||
title="Using API key"
|
||||
title={t('popup.item.alerts.automaticApproval.title')}
|
||||
color={secondaryColor}
|
||||
radius="md"
|
||||
variant="filled"
|
||||
>
|
||||
This request will be automatically approved
|
||||
{t('popup.item.alerts.automaticApproval.text')}
|
||||
</Alert>
|
||||
<Group>
|
||||
<Button variant="outline" color="gray" onClick={() => setOpened(false)}>
|
||||
Cancel
|
||||
{t('popup.item.buttons.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -91,7 +96,7 @@ export function MovieRequestModal({
|
||||
askForMedia(MediaType.Movie, result.id, result.title, []);
|
||||
}}
|
||||
>
|
||||
Request
|
||||
{t('popup.item.buttons.request')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -110,6 +115,7 @@ export function TvRequestModal({
|
||||
}) {
|
||||
const [selection, setSelection] = useState<TvShowResultSeason[]>(result.seasons);
|
||||
const { classes, cx } = useStyles();
|
||||
const { t } = useTranslation('modules/overseerr');
|
||||
|
||||
const toggleRow = (container: TvShowResultSeason) =>
|
||||
setSelection((current: TvShowResultSeason[]) =>
|
||||
@@ -148,22 +154,24 @@ export function TvRequestModal({
|
||||
title={
|
||||
<Group>
|
||||
<IconDownload />
|
||||
Ask for {result.name ?? result.originalName ?? 'a TV show'}
|
||||
{t('popup.item.buttons.askFor', {
|
||||
title: result.name ?? result.originalName ?? 'a TV show',
|
||||
})}
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack>
|
||||
<Alert
|
||||
icon={<IconAlertCircle size={16} />}
|
||||
title="Using API key"
|
||||
title={t('popup.item.alerts.automaticApproval.title')}
|
||||
color={secondaryColor}
|
||||
radius="md"
|
||||
variant="filled"
|
||||
>
|
||||
This request will be automatically approved
|
||||
{t('popup.item.alerts.automaticApproval.text')}
|
||||
</Alert>
|
||||
<Table captionSide="bottom" highlightOnHover>
|
||||
<caption>Tick the seasons that you want to be downloaded</caption>
|
||||
<caption>{t('popup.seasonSelector.caption')}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
@@ -174,15 +182,15 @@ export function TvRequestModal({
|
||||
transitionDuration={0}
|
||||
/>
|
||||
</th>
|
||||
<th>Season</th>
|
||||
<th>Number of episodes</th>
|
||||
<th>{t('popup.seasonSelector.table.header.season')}</th>
|
||||
<th>{t('popup.seasonSelector.table.header.numberOfEpisodes')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</Table>
|
||||
<Group>
|
||||
<Button variant="outline" color="gray" onClick={() => setOpened(false)}>
|
||||
Cancel
|
||||
{t('popup.item.buttons.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -196,7 +204,7 @@ export function TvRequestModal({
|
||||
);
|
||||
}}
|
||||
>
|
||||
Request
|
||||
{t('popup.item.buttons.request')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -3,14 +3,15 @@ import axios, { AxiosResponse } from 'axios';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IconPlug as Plug } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { IModule } from '../ModuleTypes';
|
||||
|
||||
export const PingModule: IModule = {
|
||||
title: 'Ping Services',
|
||||
description: 'Pings your services and shows their status as an indicator',
|
||||
icon: Plug,
|
||||
component: PingComponent,
|
||||
id: 'ping',
|
||||
};
|
||||
|
||||
export default function PingComponent(props: any) {
|
||||
@@ -20,7 +21,9 @@ export default function PingComponent(props: any) {
|
||||
const { url }: { url: string } = props;
|
||||
const [isOnline, setOnline] = useState<State>('loading');
|
||||
const [response, setResponse] = useState(500);
|
||||
const exists = config.modules?.[PingModule.title]?.enabled ?? false;
|
||||
const exists = config.modules?.[PingModule.id]?.enabled ?? false;
|
||||
|
||||
const { t } = useTranslation('modules/ping');
|
||||
|
||||
function statusCheck(response: AxiosResponse) {
|
||||
const { status }: { status: string[] } = props;
|
||||
@@ -51,7 +54,7 @@ export default function PingComponent(props: any) {
|
||||
.catch((error) => {
|
||||
statusCheck(error.response);
|
||||
});
|
||||
}, [config.modules?.[PingModule.title]?.enabled]);
|
||||
}, [config.modules?.[PingModule.id]?.enabled]);
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
@@ -68,10 +71,10 @@ export default function PingComponent(props: any) {
|
||||
radius="lg"
|
||||
label={
|
||||
isOnline === 'loading'
|
||||
? 'Loading...'
|
||||
? t('states.loading')
|
||||
: isOnline === 'online'
|
||||
? `Online - ${response}`
|
||||
: `Offline - ${response}`
|
||||
? t('states.online', { response })
|
||||
: t('states.offline', { response })
|
||||
}
|
||||
>
|
||||
<Indicator
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IconDownload as Download,
|
||||
IconMovie,
|
||||
} from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import axios from 'axios';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { useConfig } from '../../tools/state';
|
||||
@@ -26,18 +27,18 @@ const useStyles = createStyles((theme) => ({
|
||||
}));
|
||||
|
||||
export const SearchModule: IModule = {
|
||||
title: 'Search Bar',
|
||||
description: 'Search bar to search the web, youtube, torrents or overseerr',
|
||||
title: 'Search',
|
||||
icon: Search,
|
||||
component: SearchBar,
|
||||
id: 'search',
|
||||
};
|
||||
|
||||
export default function SearchBar(props: any) {
|
||||
const { classes, cx } = useStyles();
|
||||
// Config
|
||||
const { config } = useConfig();
|
||||
const isModuleEnabled = config.modules?.[SearchModule.title]?.enabled ?? false;
|
||||
const isOverseerrEnabled = config.modules?.[OverseerrModule.title]?.enabled ?? false;
|
||||
const isModuleEnabled = config.modules?.[SearchModule.id]?.enabled ?? false;
|
||||
const isOverseerrEnabled = config.modules?.[OverseerrModule.id]?.enabled ?? false;
|
||||
const OverseerrService = config.services.find(
|
||||
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr'
|
||||
);
|
||||
@@ -59,6 +60,7 @@ export default function SearchBar(props: any) {
|
||||
},
|
||||
});
|
||||
const [debounced, cancel] = useDebouncedValue(form.values.query, 250);
|
||||
const { t } = useTranslation('modules/search');
|
||||
|
||||
useEffect(() => {
|
||||
if (OverseerrService === undefined && isOverseerrEnabled) {
|
||||
@@ -175,7 +177,7 @@ export default function SearchBar(props: any) {
|
||||
radius="md"
|
||||
size="md"
|
||||
styles={{ rightSection: { pointerEvents: 'none' } }}
|
||||
placeholder="Search the web..."
|
||||
placeholder={t('input.placeholder')}
|
||||
{...props}
|
||||
{...form.getInputProps('query')}
|
||||
/>
|
||||
|
||||
@@ -13,25 +13,26 @@ import {
|
||||
IconSnowflake as Snowflake,
|
||||
IconSun as Sun,
|
||||
} from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { IModule } from '../ModuleTypes';
|
||||
import { WeatherResponse } from './WeatherInterface';
|
||||
|
||||
export const WeatherModule: IModule = {
|
||||
title: 'Weather',
|
||||
description: 'Look up the current weather in your location',
|
||||
icon: Sun,
|
||||
component: WeatherComponent,
|
||||
options: {
|
||||
freedomunit: {
|
||||
name: 'Display in Fahrenheit',
|
||||
name: 'descriptor.settings.displayInFahrenheit.label',
|
||||
value: false,
|
||||
},
|
||||
location: {
|
||||
name: 'Current location',
|
||||
name: 'descriptor.settings.location.label',
|
||||
value: 'Paris',
|
||||
},
|
||||
},
|
||||
id: 'weather',
|
||||
};
|
||||
|
||||
// 0 Clear sky
|
||||
@@ -48,79 +49,87 @@ export const WeatherModule: IModule = {
|
||||
// 95 *Thunderstorm: Slight or moderate
|
||||
// 96, 99 *Thunderstorm with slight and heavy hail
|
||||
export function WeatherIcon(props: any) {
|
||||
const { t } = useTranslation('modules/weather');
|
||||
|
||||
const { code } = props;
|
||||
let data: { icon: any; name: string };
|
||||
switch (code) {
|
||||
case 0: {
|
||||
data = { icon: Sun, name: 'Clear' };
|
||||
data = { icon: Sun, name: t('card.weatherDescriptions.clear') };
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
case 2:
|
||||
case 3: {
|
||||
data = { icon: Cloud, name: 'Mainly clear' };
|
||||
data = { icon: Cloud, name: t('card.weatherDescriptions.mainlyClear') };
|
||||
break;
|
||||
}
|
||||
case 45:
|
||||
case 48: {
|
||||
data = { icon: CloudFog, name: 'Fog' };
|
||||
data = { icon: CloudFog, name: t('card.weatherDescriptions.fog') };
|
||||
break;
|
||||
}
|
||||
case 51:
|
||||
case 53:
|
||||
case 55: {
|
||||
data = { icon: Cloud, name: 'Drizzle' };
|
||||
data = { icon: Cloud, name: t('card.weatherDescriptions.drizzle') };
|
||||
break;
|
||||
}
|
||||
case 56:
|
||||
case 57: {
|
||||
data = { icon: Snowflake, name: 'Freezing drizzle' };
|
||||
data = {
|
||||
icon: Snowflake,
|
||||
name: t('card.weatherDescriptions.freezingDrizzle'),
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 61:
|
||||
case 63:
|
||||
case 65: {
|
||||
data = { icon: CloudRain, name: 'Rain' };
|
||||
data = { icon: CloudRain, name: t('card.weatherDescriptions.rain') };
|
||||
break;
|
||||
}
|
||||
case 66:
|
||||
case 67: {
|
||||
data = { icon: CloudRain, name: 'Freezing rain' };
|
||||
data = { icon: CloudRain, name: t('card.weatherDescriptions.freezingRain') };
|
||||
break;
|
||||
}
|
||||
case 71:
|
||||
case 73:
|
||||
case 75: {
|
||||
data = { icon: CloudSnow, name: 'Snow fall' };
|
||||
data = { icon: CloudSnow, name: t('card.weatherDescriptions.snowFall') };
|
||||
break;
|
||||
}
|
||||
case 77: {
|
||||
data = { icon: CloudSnow, name: 'Snow grains' };
|
||||
data = { icon: CloudSnow, name: t('card.weatherDescriptions.snowGrains') };
|
||||
break;
|
||||
}
|
||||
case 80:
|
||||
case 81:
|
||||
case 82: {
|
||||
data = { icon: CloudRain, name: 'Rain showers' };
|
||||
data = { icon: CloudRain, name: t('card.weatherDescriptions.rainShowers') };
|
||||
|
||||
break;
|
||||
}
|
||||
case 85:
|
||||
case 86: {
|
||||
data = { icon: CloudSnow, name: 'Snow showers' };
|
||||
data = { icon: CloudSnow, name: t('card.weatherDescriptions.snowShowers') };
|
||||
break;
|
||||
}
|
||||
case 95: {
|
||||
data = { icon: CloudStorm, name: 'Thunderstorm' };
|
||||
data = { icon: CloudStorm, name: t('card.weatherDescriptions.thunderstorm') };
|
||||
break;
|
||||
}
|
||||
case 96:
|
||||
case 99: {
|
||||
data = { icon: CloudStorm, name: 'Thunderstorm with hail' };
|
||||
data = {
|
||||
icon: CloudStorm,
|
||||
name: t('card.weatherDescriptions.thunderstormWithHail'),
|
||||
};
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
data = { icon: QuestionMark, name: 'Unknown' };
|
||||
data = { icon: QuestionMark, name: t('card.weatherDescriptions.unknown') };
|
||||
}
|
||||
}
|
||||
return (
|
||||
@@ -137,9 +146,9 @@ export default function WeatherComponent(props: any) {
|
||||
const { config } = useConfig();
|
||||
const [weather, setWeather] = useState({} as WeatherResponse);
|
||||
const cityInput: string =
|
||||
(config?.modules?.[WeatherModule.title]?.options?.location?.value as string) ?? 'Paris';
|
||||
(config?.modules?.[WeatherModule.id]?.options?.location?.value as string) ?? 'Paris';
|
||||
const isFahrenheit: boolean =
|
||||
(config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value as boolean) ?? false;
|
||||
(config?.modules?.[WeatherModule.id]?.options?.freedomunit?.value as boolean) ?? false;
|
||||
|
||||
useEffect(() => {
|
||||
axios
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { NextLink } from '@mantine/next';
|
||||
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
paddingTop: 80,
|
||||
@@ -92,3 +94,12 @@ export default function Custom404() {
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStaticProps({ locale }: { locale: string }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['404'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import { MantineProvider, ColorScheme, ColorSchemeProvider, MantineTheme } from
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
import { ConfigProvider } from '../tools/state';
|
||||
import { theme } from '../tools/theme';
|
||||
import { ColorTheme } from '../tools/color';
|
||||
|
||||
export default function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
||||
function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
||||
const { Component, pageProps } = props;
|
||||
const [colorScheme, setColorScheme] = useState<ColorScheme>(props.colorScheme);
|
||||
|
||||
@@ -83,3 +84,5 @@ export default function App(this: any, props: AppProps & { colorScheme: ColorSch
|
||||
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({
|
||||
colorScheme: getCookie('color-scheme', ctx) || 'light',
|
||||
});
|
||||
|
||||
export default appWithTranslation(App);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { getCookie, setCookie } from 'cookies-next';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import { useEffect } from 'react';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
import AppShelf from '../components/AppShelf/AppShelf';
|
||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||
import { Config } from '../tools/types';
|
||||
@@ -13,18 +15,51 @@ import Layout from '../components/layout/Layout';
|
||||
export async function getServerSideProps({
|
||||
req,
|
||||
res,
|
||||
locale,
|
||||
}: GetServerSidePropsContext): Promise<{ props: { config: Config } }> {
|
||||
let cookie = getCookie('config-name', { req, res });
|
||||
if (!cookie) {
|
||||
let configName = getCookie('config-name', { req, res });
|
||||
const configLocale = getCookie('config-locale', { req, res });
|
||||
if (!configName) {
|
||||
setCookie('config-name', 'default', {
|
||||
req,
|
||||
res,
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
cookie = 'default';
|
||||
configName = 'default';
|
||||
}
|
||||
return getConfig(cookie as string);
|
||||
|
||||
const translations = await serverSideTranslations((configLocale ?? locale) as string, [
|
||||
'common',
|
||||
'layout/app-shelf',
|
||||
'layout/add-service-app-shelf',
|
||||
'layout/app-shelf-menu',
|
||||
'settings/common',
|
||||
'settings/general/theme-selector',
|
||||
'settings/general/config-changer',
|
||||
'settings/general/internationalization',
|
||||
'settings/general/module-enabler',
|
||||
'settings/general/search-engine',
|
||||
'settings/general/widget-positions',
|
||||
'settings/customization/color-selector',
|
||||
'settings/customization/page-appearance',
|
||||
'settings/customization/shade-selector',
|
||||
'settings/customization/app-width',
|
||||
'settings/customization/opacity-selector',
|
||||
'modules/common',
|
||||
'modules/date',
|
||||
'modules/calendar',
|
||||
'modules/dlspeed',
|
||||
'modules/search',
|
||||
'modules/torrents-status',
|
||||
'modules/weather',
|
||||
'modules/ping',
|
||||
'modules/docker',
|
||||
'modules/dashdot',
|
||||
'modules/overseerr',
|
||||
'modules/common-media-cards',
|
||||
]);
|
||||
return getConfig(configName as string, translations);
|
||||
}
|
||||
|
||||
export default function HomePage(props: any) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user