mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 15:35:55 +01:00
🔀 Merge branch 'dev' into next-13
This commit is contained in:
@@ -1,3 +1,2 @@
|
|||||||
export const REPO_URL = 'ajnart/homarr';
|
export const REPO_URL = 'ajnart/homarr';
|
||||||
export const CURRENT_VERSION = 'v0.11.2';
|
|
||||||
export const ICON_PICKER_SLICE_LIMIT = 36;
|
export const ICON_PICKER_SLICE_LIMIT = 36;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ module.exports = {
|
|||||||
'vi',
|
'vi',
|
||||||
'uk',
|
'uk',
|
||||||
'zh',
|
'zh',
|
||||||
|
'el',
|
||||||
],
|
],
|
||||||
localePath: path.resolve('./public/locales'),
|
localePath: path.resolve('./public/locales'),
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "0.11.2",
|
"version": "0.11.3",
|
||||||
"description": "Homarr - A homepage for your server.",
|
"description": "Homarr - A homepage for your server.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
"@nivo/line": "^0.79.1",
|
"@nivo/line": "^0.79.1",
|
||||||
"@tabler/icons": "^1.106.0",
|
"@tabler/icons": "^1.106.0",
|
||||||
"@tanstack/react-query": "^4.2.1",
|
"@tanstack/react-query": "^4.2.1",
|
||||||
|
"@tanstack/react-query-devtools": "^4.24.4",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"consola": "^2.15.3",
|
"consola": "^2.15.3",
|
||||||
"cookies-next": "^2.1.1",
|
"cookies-next": "^2.1.1",
|
||||||
|
|||||||
7
public/locales/da/layout/modals/icon-picker.json
Normal file
7
public/locales/da/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/de/layout/modals/icon-picker.json
Normal file
7
public/locales/de/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
27
public/locales/el/authentication/login.json
Normal file
27
public/locales/el/authentication/login.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"title": "Καλώς ήρθατε!",
|
||||||
|
"text": "Παρακαλώ εισάγετε τον κωδικό σας",
|
||||||
|
"form": {
|
||||||
|
"fields": {
|
||||||
|
"password": {
|
||||||
|
"label": "Κωδικός",
|
||||||
|
"placeholder": "Ο κωδικός σας"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"submit": "Σύνδεση"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"checking": {
|
||||||
|
"title": "Έλεγχος κωδικού πρόσβασης",
|
||||||
|
"message": "Ο κωδικός πρόσβασής σας ελέγχεται..."
|
||||||
|
},
|
||||||
|
"correct": {
|
||||||
|
"title": "Σύνδεση επιτυχής, ανακατεύθυνση..."
|
||||||
|
},
|
||||||
|
"wrong": {
|
||||||
|
"title": "Ο κωδικός που εισαγάγατε είναι εσφαλμένος. Προσπαθήστε ξανά."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
public/locales/el/common.json
Normal file
28
public/locales/el/common.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"save": "Αποθήκευση",
|
||||||
|
"about": "Σχετικά",
|
||||||
|
"cancel": "Ακύρωση",
|
||||||
|
"close": "Κλείσιμο",
|
||||||
|
"delete": "Διαγραφή",
|
||||||
|
"ok": "ΟΚ",
|
||||||
|
"edit": "Επεξεργασία",
|
||||||
|
"version": "Έκδοση",
|
||||||
|
"changePosition": "Αλλαγή θέσης",
|
||||||
|
"remove": "Αφαίρεση",
|
||||||
|
"removeConfirm": "Είστε σίγουροι ότι θέλετε να καταργήσετε το {{item}} ;",
|
||||||
|
"sections": {
|
||||||
|
"settings": "Ρυθμίσεις",
|
||||||
|
"dangerZone": "Επικίνδυνη Περιοχή"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"apiKey": "Κλειδί Api",
|
||||||
|
"username": "Όνομα Χρήστη",
|
||||||
|
"password": "Κωδικός"
|
||||||
|
},
|
||||||
|
"tip": "Συμβουλές: ",
|
||||||
|
"time": {
|
||||||
|
"seconds": "δευτερόλεπτα",
|
||||||
|
"minutes": "λεπτά",
|
||||||
|
"hours": "ώρες"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
public/locales/el/layout/element-selector/selector.json
Normal file
11
public/locales/el/layout/element-selector/selector.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"modal": {
|
||||||
|
"title": "Προσθήκη νέου πλακιδίου",
|
||||||
|
"text": "Τα πλακάκια είναι το κύριο στοιχείο του Homarr. Χρησιμοποιούνται για την εμφάνιση των εφαρμογών σας και άλλων πληροφοριών. Μπορείτε να προσθέσετε όσα πλακίδια θέλετε."
|
||||||
|
},
|
||||||
|
"widgetDescription": "Τα widgets αλληλεπιδρούν με τις εφαρμογές σας, για να σας παρέχουν περισσότερο έλεγχο των εφαρμογών σας. Συνήθως απαιτούν πρόσθετες ρυθμίσεις πριν από τη χρήση.",
|
||||||
|
"goBack": "Επιστροφή στο προηγούμενο βήμα",
|
||||||
|
"actionIcon": {
|
||||||
|
"tooltip": "Προσθέστε ένα πλακίδιο"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"description": "Στη Λειτουργία επεξεργασίας, μπορείτε να προσαρμόσετε τα πλακίδια και να ρυθμίσετε τις εφαρμογές. Οι αλλαγές δεν αποθηκεύονται μέχρι να βγείτε από τη λειτουργία επεξεργασίας.",
|
||||||
|
"button": {
|
||||||
|
"disabled": "Λειτουργία επεξεργασίας",
|
||||||
|
"enabled": "Έξοδος και Αποθήκευση"
|
||||||
|
},
|
||||||
|
"popover": {
|
||||||
|
"title": "Η λειτουργία επεξεργασίας είναι ενεργοποιημένη για <1>{{size}}</1> μέγεθος",
|
||||||
|
"text": "Μπορείτε να προσαρμόσετε και να ρυθμίσετε τις εφαρμογές σας τώρα. Οι αλλαγές <strong>δεν αποθηκεύονται</strong> μέχρι να βγείτε από τη λειτουργία επεξεργασίας"
|
||||||
|
},
|
||||||
|
"screenSizes": {
|
||||||
|
"small": "μικρό",
|
||||||
|
"medium": "μεσαίο",
|
||||||
|
"large": "μεγάλο"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
public/locales/el/layout/mobile/drawer.json
Normal file
3
public/locales/el/layout/mobile/drawer.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"title": "{{position}} πλαϊνή μπάρα"
|
||||||
|
}
|
||||||
7
public/locales/el/layout/modals/about.json
Normal file
7
public/locales/el/layout/modals/about.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"description": "Το Homarr είναι ένα <strong>κομψό</strong>, <strong>μοντέρνο</strong> ταμπλό που βάζει όλες τις εφαρμογές και τις υπηρεσίες σας στα χέρια σας. Με το Homarr, μπορείτε να έχετε πρόσβαση και να ελέγχετε τα πάντα σε μια βολική τοποθεσία. Το Homarr ενσωματώνεται απρόσκοπτα με τις εφαρμογές που έχετε προσθέσει, παρέχοντάς σας πολύτιμες πληροφορίες και δίνοντάς σας πλήρη έλεγχο. Η εγκατάσταση είναι πανεύκολη και το Homarr υποστηρίζει ένα ευρύ φάσμα μεθόδων ανάπτυξης.",
|
||||||
|
"i18n": "Φορτωμένα πεδία ονομάτων μετάφρασης I18n",
|
||||||
|
"locales": "Διαμορφωμένες τοπικές ρυθμίσεις I18n",
|
||||||
|
"contact": "Έχετε προβλήματα ή ερωτήσεις; Συνδεθείτε μαζί μας!",
|
||||||
|
"addToDashboard": "Προσθήκη στο ταμπλό"
|
||||||
|
}
|
||||||
68
public/locales/el/layout/modals/add-app.json
Normal file
68
public/locales/el/layout/modals/add-app.json
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"tabs": {
|
||||||
|
"general": "Γενικά",
|
||||||
|
"behaviour": "Συμπεριφορά",
|
||||||
|
"network": "Δίκτυο",
|
||||||
|
"appearance": "Εμφάνιση",
|
||||||
|
"integration": "Ενσωμάτωση"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"appname": {
|
||||||
|
"label": "Όνομα εφαρμογής",
|
||||||
|
"description": "Χρησιμοποιείται για την εμφάνιση της εφαρμογής στο ταμπλό."
|
||||||
|
},
|
||||||
|
"internalAddress": {
|
||||||
|
"label": "Εσωτερική διεύθυνση",
|
||||||
|
"description": "Η εσωτερική διεύθυνση IP της εφαρμογής."
|
||||||
|
},
|
||||||
|
"externalAddress": {
|
||||||
|
"label": "Εξωτερική διεύθυνση",
|
||||||
|
"description": "URL που θα ανοίγει όταν κάνετε κλικ στην εφαρμογή."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"behaviour": {
|
||||||
|
"isOpeningNewTab": {
|
||||||
|
"label": "Άνοιγμα σε νέα καρτέλα",
|
||||||
|
"description": "Ανοίξτε την εφαρμογή σε νέα καρτέλα αντί της τρέχουσας."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"statusChecker": {
|
||||||
|
"label": "Έλεγχος κατάστασης",
|
||||||
|
"description": "Ελέγχει αν η εφαρμογή σας είναι συνδεδεμένη χρησιμοποιώντας ένα απλό αίτημα HTTP(S)."
|
||||||
|
},
|
||||||
|
"statusCodes": {
|
||||||
|
"label": "Κωδικοί κατάστασης HTTP",
|
||||||
|
"description": "Οι κωδικοί κατάστασης HTTP που θεωρούνται online."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"icon": {
|
||||||
|
"label": "Εικονίδιο εφαρμογής",
|
||||||
|
"description": "Το εικονίδιο που θα εμφανίζεται στο ταμπλό."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"integration": {
|
||||||
|
"type": {
|
||||||
|
"label": "Διαμόρφωση ενσωμάτωσης",
|
||||||
|
"description": "Η διαμόρφωση ενσωμάτωσης που θα χρησιμοποιηθεί για τη σύνδεση με την εφαρμογή σας.",
|
||||||
|
"placeholder": "Επιλέξτε Ενσωμάτωση",
|
||||||
|
"defined": "Καθορισμένο",
|
||||||
|
"undefined": "Απροσδιόριστο",
|
||||||
|
"public": "Δημόσιο",
|
||||||
|
"private": "Ιδιωτικό",
|
||||||
|
"explanationPrivate": "Ένα ιδιωτικό μυστικό θα αποσταλεί στον διακομιστή μόνο μία φορά. Μόλις το πρόγραμμα περιήγησής σας ανανεώσει τη σελίδα, δεν θα αποσταλεί ποτέ ξανά.",
|
||||||
|
"explanationPublic": "Ένα δημόσιο μυστικό αποστέλλεται πάντα στον πελάτη και είναι προσβάσιμο μέσω του API. Δεν πρέπει να περιέχει εμπιστευτικές τιμές όπως ονόματα χρηστών, κωδικούς πρόσβασης, μάρκες, πιστοποιητικά και παρόμοια!"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"description": "Για να ενημερώσετε ένα μυστικό, εισαγάγετε μια τιμή και κάντε κλικ στο κουμπί αποθήκευσης. Για να διαγράψετε ένα μυστικό, χρησιμοποιήστε το κουμπί διαγραφής.",
|
||||||
|
"warning": "Τα διαπιστευτήριά σας λειτουργούν ως πρόσβαση για τις ενσωματώσεις σας και δεν θα πρέπει <strong>ποτέ </strong> να τα μοιράζεστε με κανέναν άλλον. Η ομάδα Homarr δεν θα σας ζητήσει ποτέ διαπιστευτήρια. Βεβαιωθείτε ότι <strong>αποθηκεύετε και διαχειρίζεστε τα μυστικά σας με ασφάλεια</strong>.",
|
||||||
|
"clear": "Καθαρισμός μυστικού",
|
||||||
|
"save": "Αποθήκευση μυστικού",
|
||||||
|
"update": "Ενημέρωση μυστικού"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"popover": "Η φόρμα σας περιέχει άκυρα δεδομένα. Ως εκ τούτου, δεν μπορεί να αποθηκευτεί. Παρακαλούμε επιλύστε όλα τα προβλήματα και κάντε ξανά κλικ σε αυτό το κουμπί για να αποθηκεύσετε τις αλλαγές σας"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
public/locales/el/layout/modals/change-position.json
Normal file
8
public/locales/el/layout/modals/change-position.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"xPosition": "Θέση του άξονα X",
|
||||||
|
"width": "Πλάτος",
|
||||||
|
"height": "Ύψος",
|
||||||
|
"yPosition": "Θέση του άξονα Y",
|
||||||
|
"zeroOrHigher": "0 ή υψηλότερο",
|
||||||
|
"betweenXandY": "Μεταξύ {min} και {max}"
|
||||||
|
}
|
||||||
7
public/locales/el/layout/modals/icon-picker.json
Normal file
7
public/locales/el/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
15
public/locales/el/modules/calendar.json
Normal file
15
public/locales/el/modules/calendar.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Ημερολόγιο",
|
||||||
|
"description": "Εμφανίζει ένα ημερολόγιο με τις επερχόμενες κυκλοφορίες, από τις υποστηριζόμενες ενσωματώσεις.",
|
||||||
|
"settings": {
|
||||||
|
"title": "Ρυθμίσεις για το widget ημερολογίου",
|
||||||
|
"sundayStart": {
|
||||||
|
"label": "Ξεκινήστε την εβδομάδα από την Κυριακή"
|
||||||
|
},
|
||||||
|
"radarrReleaseType": {
|
||||||
|
"label": "Τύπος κυκλοφορίας Radarr"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
public/locales/el/modules/common-media-cards.json
Normal file
6
public/locales/el/modules/common-media-cards.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"buttons": {
|
||||||
|
"play": "Αναπαραγωγή",
|
||||||
|
"request": "Αίτημα"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
public/locales/el/modules/common.json
Normal file
10
public/locales/el/modules/common.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"label": "Ρυθμίσεις"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"unmappedOptions": {
|
||||||
|
"text": "<b>Εντοπίστηκε αχρησιμοποίητη παράμετρος στη διαμόρφωση</b><br /><code>{{key}}</code>. Το Homarr δεν μπορεί να ερμηνεύσει και να χρησιμοποιήσει αυτή την παράμετρο. Για να αποφύγετε οποιαδήποτε απροσδόκητη συμπεριφορά, δημιουργήστε αντίγραφα ασφαλείας των ρυθμίσεων σας και διορθώστε τις ρυθμίσεις σας."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
public/locales/el/modules/dashdot.json
Normal file
54
public/locales/el/modules/dashdot.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Dash.",
|
||||||
|
"description": "Εμφανίζει τα γραφήματα μιας εξωτερικής Dash. μέσα στο Homarr.",
|
||||||
|
"settings": {
|
||||||
|
"title": "Ρυθμίσεις για το widget Dash",
|
||||||
|
"cpuMultiView": {
|
||||||
|
"label": "Προβολή πολλαπλών πυρήνων CPU"
|
||||||
|
},
|
||||||
|
"storageMultiView": {
|
||||||
|
"label": "Προβολή πολλαπλών μονάδων αποθήκευσης"
|
||||||
|
},
|
||||||
|
"useCompactView": {
|
||||||
|
"label": "Χρήση Συμπαγούς Προβολής"
|
||||||
|
},
|
||||||
|
"graphs": {
|
||||||
|
"label": "Γραφήματα"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"label": "Dash. URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"title": "Dash.",
|
||||||
|
"errors": {
|
||||||
|
"noService": "Δεν βρέθηκε υπηρεσία Dash. Παρακαλούμε προσθέστε μία στο ταμπλό Homarr ή ορίστε μια Dash. URL στις επιλογές της ενότητας",
|
||||||
|
"noInformation": "Δεν μπορεί να αποκτήσει πληροφορίες από το dash. - τρέχετε την τελευταία έκδοση;"
|
||||||
|
},
|
||||||
|
"graphs": {
|
||||||
|
"storage": {
|
||||||
|
"title": "Αποθηκευτικός χώρος",
|
||||||
|
"label": "Αποθηκευτικός χώρος:"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"title": "Δίκτυο",
|
||||||
|
"label": "Δίκτυο:",
|
||||||
|
"metrics": {
|
||||||
|
"download": "Κάτω",
|
||||||
|
"upload": "Πάνω"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cpu": {
|
||||||
|
"title": "CPU"
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"title": "Μνήμη RAM"
|
||||||
|
},
|
||||||
|
"gpu": {
|
||||||
|
"title": "GPU"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
public/locales/el/modules/date.json
Normal file
12
public/locales/el/modules/date.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Ημερομηνία και ώρα",
|
||||||
|
"description": "Εμφανίζει την τρέχουσα ημερομηνία και ώρα.",
|
||||||
|
"settings": {
|
||||||
|
"title": "Ρυθμίσεις για το widget ημερομηνίας και ώρας",
|
||||||
|
"display24HourFormat": {
|
||||||
|
"label": "Εμφάνιση πλήρης ώρας(24-ώρο)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
public/locales/el/modules/dlspeed.json
Normal file
35
public/locales/el/modules/dlspeed.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Ταχύτητα Λήψης",
|
||||||
|
"description": "Εμφανίζει την ταχύτητα λήψης και μεταφόρτωσης των υποστηριζόμενων ενσωματώσεων."
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"table": {
|
||||||
|
"header": {
|
||||||
|
"name": "Όνομα",
|
||||||
|
"size": "Μέγεθος",
|
||||||
|
"download": "Κάτω",
|
||||||
|
"upload": "Πάνω",
|
||||||
|
"estimatedTimeOfArrival": "Εκτιμώμενος χρόνος αναμονής",
|
||||||
|
"progress": "Πρόοδος"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"nothingFound": "Δεν βρέθηκαν torrents"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lineChart": {
|
||||||
|
"title": "Τρέχουσα ταχύτητα λήψης",
|
||||||
|
"download": "Λήψη: {{download}}",
|
||||||
|
"upload": "Ανέβασμα: {{upload}}",
|
||||||
|
"timeSpan": "{{seconds}} δευτερόλεπτα πριν",
|
||||||
|
"totalDownload": "Λήψη: {{download}}/s",
|
||||||
|
"totalUpload": "Ανέβασμα: {{upload}}/s"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"noDownloadClients": {
|
||||||
|
"title": "Δεν βρέθηκαν υποστηριζόμενα προγράμματα λήψης!",
|
||||||
|
"text": "Προσθέστε μια υπηρεσία λήψης για να δείτε τις τρέχουσες λήψεις σας"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
public/locales/el/modules/docker.json
Normal file
83
public/locales/el/modules/docker.json
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Docker",
|
||||||
|
"description": "Σας επιτρέπει να βλέπετε και να διαχειρίζεστε εύκολα όλα τα Docker Containers σας."
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Αναζήτηση με βάση container ή όνομα εικόνας"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"header": {
|
||||||
|
"name": "Όνομα",
|
||||||
|
"image": "Εικόνα",
|
||||||
|
"ports": "Θύρες",
|
||||||
|
"state": "Κατάσταση"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"portCollapse": "{{ports}} περισσότερα"
|
||||||
|
},
|
||||||
|
"states": {
|
||||||
|
"running": "Εκτελείται",
|
||||||
|
"created": "Δημιουργήθηκε",
|
||||||
|
"stopped": "Διακόπηκε",
|
||||||
|
"unknown": "Άγνωστο"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"actionBar": {
|
||||||
|
"addService": {
|
||||||
|
"title": "Προσθήκη εφαρμογής",
|
||||||
|
"message": "Προσθήκη εφαρμογής στο Homarr"
|
||||||
|
},
|
||||||
|
"restart": {
|
||||||
|
"title": "Επανεκκίνηση"
|
||||||
|
},
|
||||||
|
"stop": {
|
||||||
|
"title": "Διακοπή"
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"title": "Έναρξη"
|
||||||
|
},
|
||||||
|
"refreshData": {
|
||||||
|
"title": "Ανανέωση δεδομένων"
|
||||||
|
},
|
||||||
|
"remove": {
|
||||||
|
"title": "Αφαίρεση"
|
||||||
|
},
|
||||||
|
"addToHomarr": {
|
||||||
|
"title": "Προσθήκη στο Homarr"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"start": {
|
||||||
|
"start": "Ξεκινάει",
|
||||||
|
"end": "Ξεκίνησε"
|
||||||
|
},
|
||||||
|
"stop": {
|
||||||
|
"start": "Διακόπτεται",
|
||||||
|
"end": "Διακόπηκε"
|
||||||
|
},
|
||||||
|
"restart": {
|
||||||
|
"start": "Γίνεται επανεκκίνηση",
|
||||||
|
"end": "Επανεκκινήθηκε"
|
||||||
|
},
|
||||||
|
"remove": {
|
||||||
|
"start": "Αφαιρείται",
|
||||||
|
"end": "Αφαιρέθηκε"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"integrationFailed": {
|
||||||
|
"title": "Η ενσωμάτωση του Docker απέτυχε",
|
||||||
|
"message": "Μήπως ξεχάσατε να προσαρτήσετε την υποδοχή docker;"
|
||||||
|
},
|
||||||
|
"unknownError": {
|
||||||
|
"title": "Παρουσιάστηκε σφάλμα"
|
||||||
|
},
|
||||||
|
"oneServiceAtATime": {
|
||||||
|
"title": "Παρακαλώ προσθέστε μόνο μία εφαρμογή ή υπηρεσία τη φορά!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"actionIcon": {
|
||||||
|
"tooltip": "Docker"
|
||||||
|
}
|
||||||
|
}
|
||||||
30
public/locales/el/modules/overseerr.json
Normal file
30
public/locales/el/modules/overseerr.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Overseerr",
|
||||||
|
"description": "Σας επιτρέπει να αναζητήσετε και να προσθέσετε πολυμέσα από το Overseerr ή το Jellyseerr."
|
||||||
|
},
|
||||||
|
"popup": {
|
||||||
|
"item": {
|
||||||
|
"buttons": {
|
||||||
|
"askFor": "Ρωτήστε για {{title}}",
|
||||||
|
"cancel": "Ακύρωση",
|
||||||
|
"request": "Αίτημα"
|
||||||
|
},
|
||||||
|
"alerts": {
|
||||||
|
"automaticApproval": {
|
||||||
|
"title": "Χρήση κλειδιού API",
|
||||||
|
"text": "Το αίτημα αυτό θα εγκριθεί αυτόματα"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seasonSelector": {
|
||||||
|
"caption": "Επιλέξτε τις σεζόν που θέλετε να κατεβάσετε",
|
||||||
|
"table": {
|
||||||
|
"header": {
|
||||||
|
"season": "Σεζόν",
|
||||||
|
"numberOfEpisodes": "Αριθμός επεισοδίων"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
public/locales/el/modules/ping.json
Normal file
11
public/locales/el/modules/ping.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Ping",
|
||||||
|
"description": "Εμφανίζει μια ένδειξη κατάστασης ανάλογα με τον κωδικό απόκρισης HTTP μιας δεδομένης διεύθυνσης URL."
|
||||||
|
},
|
||||||
|
"states": {
|
||||||
|
"online": "Online {{response}}",
|
||||||
|
"offline": "Χωρίς σύνδεση {{response}}",
|
||||||
|
"loading": "Φόρτωση..."
|
||||||
|
}
|
||||||
|
}
|
||||||
30
public/locales/el/modules/search.json
Normal file
30
public/locales/el/modules/search.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Μπάρα Αναζήτησης",
|
||||||
|
"description": "Μια γραμμή αναζήτησης που σας επιτρέπει να κάνετε αναζήτηση στην προσαρμοσμένη μηχανή αναζήτησης σας, στο YouTube και στις υποστηριζόμενες ενσωματώσεις."
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
"placeholder": "Αναζήτηση στον Ιστό..."
|
||||||
|
},
|
||||||
|
"switched-to": "Αλλαγή σε",
|
||||||
|
"searchEngines": {
|
||||||
|
"search": {
|
||||||
|
"name": "Ιστός",
|
||||||
|
"description": "Αναζήτηση..."
|
||||||
|
},
|
||||||
|
"youtube": {
|
||||||
|
"name": "YouTube",
|
||||||
|
"description": "Αναζήτηση στο YouTube"
|
||||||
|
},
|
||||||
|
"torrents": {
|
||||||
|
"name": "Τόρρεντ",
|
||||||
|
"description": "Αναζήτηση για Torrents"
|
||||||
|
},
|
||||||
|
"overseerr": {
|
||||||
|
"name": "Overseerr",
|
||||||
|
"description": "Αναζήτηση για ταινίες και τηλεοπτικές εκπομπές στο Overseerr"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tip": "Μπορείτε να επιλέξετε τη γραμμή αναζήτησης με τη συντόμευση ",
|
||||||
|
"switchedSearchEngine": "Εναλλαγή για αναζήτηση με {{searchEngine}}"
|
||||||
|
}
|
||||||
72
public/locales/el/modules/torrents-status.json
Normal file
72
public/locales/el/modules/torrents-status.json
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Τόρρεντ",
|
||||||
|
"description": "Εμφανίζει μια λίστα με torrents από υποστηριζόμενους εφαρμογές Torrent.",
|
||||||
|
"settings": {
|
||||||
|
"title": "Ρυθμίσεις για το widget Torrent",
|
||||||
|
"refreshInterval": {
|
||||||
|
"label": "Χρονικό διάστημα ανανέωσης (σε δευτερόλεπτα)"
|
||||||
|
},
|
||||||
|
"displayCompletedTorrents": {
|
||||||
|
"label": "Εμφάνιση ολοκληρωμένων torrents"
|
||||||
|
},
|
||||||
|
"displayStaleTorrents": {
|
||||||
|
"label": "Εμφάνιση stale torrents"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"footer": {
|
||||||
|
"error": "Σφάλμα",
|
||||||
|
"lastUpdated": "Τελευταία ενημέρωση {{time}} πριν"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"header": {
|
||||||
|
"name": "Όνομα",
|
||||||
|
"size": "Μέγεθος",
|
||||||
|
"download": "Κάτω",
|
||||||
|
"upload": "Πάνω",
|
||||||
|
"estimatedTimeOfArrival": "Εκτιμώμενος χρόνος αναμονής",
|
||||||
|
"progress": "Πρόοδος"
|
||||||
|
},
|
||||||
|
"item": {
|
||||||
|
"text": "Διαχειρίζεται από {{appName}}, {{ratio}} αναλογία"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"nothingFound": "Δεν βρέθηκαν torrents"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lineChart": {
|
||||||
|
"title": "Τρέχουσα ταχύτητα λήψης",
|
||||||
|
"download": "Λήψη: {{download}}",
|
||||||
|
"upload": "Ανέβασμα: {{upload}}",
|
||||||
|
"timeSpan": "{{seconds}} δευτερόλεπτα πριν",
|
||||||
|
"totalDownload": "Λήψη: {{download}}/s",
|
||||||
|
"totalUpload": "Ανέβασμα: {{upload}}/s"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"noDownloadClients": {
|
||||||
|
"title": "Δεν βρέθηκαν υποστηριζόμενες εφαρμογές Torrent!",
|
||||||
|
"text": "Προσθέστε έναν υποστηριζόμενης εφαρμογής Torrent για να δείτε τις τρέχουσες λήψεις σας"
|
||||||
|
},
|
||||||
|
"generic": {
|
||||||
|
"title": "Παρουσιάστηκε ένα απροσδόκητο σφάλμα",
|
||||||
|
"text": "Το Homarr δεν μπόρεσε να επικοινωνήσει με τις εφαρμογές Torrent. Ελέγξτε τις ρυθμίσεις σας"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loading": {
|
||||||
|
"title": "Φόρτωση..."
|
||||||
|
},
|
||||||
|
"popover": {
|
||||||
|
"introductionPrefix": "Διαχειριζόμενα από",
|
||||||
|
"metrics": {
|
||||||
|
"queuePosition": "Θέση ουράς - {{position}}",
|
||||||
|
"progress": "Πρόοδος - {{progress}}%",
|
||||||
|
"totalSelectedSize": "Σύνολο - {{totalSize}}",
|
||||||
|
"state": "Κατάσταση - {{state}}",
|
||||||
|
"ratio": "Αναλογία -",
|
||||||
|
"completed": "Ολοκληρώθηκε"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
public/locales/el/modules/usenet.json
Normal file
49
public/locales/el/modules/usenet.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Usenet",
|
||||||
|
"description": "Σας επιτρέπει να δείτε και να διαχειριστείτε το Usenet instance σας."
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"errors": {
|
||||||
|
"noDownloadClients": {
|
||||||
|
"title": "Δεν βρέθηκαν υποστηριζόμενα προγράμματα λήψης!",
|
||||||
|
"text": "Προσθέστε έναν υποστηριζόμενο πρόγραμμα λήψης Usenet για να δείτε τις τρέχουσες λήψεις σας"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"queue": "Ουρά",
|
||||||
|
"history": "Ιστορικό"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"sizeLeft": "Μέγεθος που απομένει",
|
||||||
|
"paused": "Σε παύση"
|
||||||
|
},
|
||||||
|
"queue": {
|
||||||
|
"header": {
|
||||||
|
"name": "Όνομα",
|
||||||
|
"size": "Μέγεθος",
|
||||||
|
"eta": "Εκτιμώμενος χρόνος αναμονής",
|
||||||
|
"progress": "Πρόοδος"
|
||||||
|
},
|
||||||
|
"empty": "Άδειο",
|
||||||
|
"error": {
|
||||||
|
"title": "Σφάλμα",
|
||||||
|
"message": "Παρουσιάστηκε σφάλμα"
|
||||||
|
},
|
||||||
|
"paused": "Σε παύση"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"header": {
|
||||||
|
"name": "Όνομα",
|
||||||
|
"size": "Μέγεθος",
|
||||||
|
"duration": "Διάρκεια"
|
||||||
|
},
|
||||||
|
"empty": "Άδειο",
|
||||||
|
"error": {
|
||||||
|
"title": "Σφάλμα",
|
||||||
|
"message": "Σφάλμα φόρτωσης ιστορικού"
|
||||||
|
},
|
||||||
|
"paused": "Σε παύση"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
public/locales/el/modules/weather.json
Normal file
33
public/locales/el/modules/weather.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Καιρός",
|
||||||
|
"description": "Εμφανίζει τις τρέχουσες πληροφορίες καιρού μιας καθορισμένης τοποθεσίας.",
|
||||||
|
"settings": {
|
||||||
|
"title": "Ρυθμίσεις για το widget καιρού",
|
||||||
|
"displayInFahrenheit": {
|
||||||
|
"label": "Εμφάνιση σε Φαρενάιτ"
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"label": "Τοποθεσία καιρού"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"weatherDescriptions": {
|
||||||
|
"clear": "Καθαρός",
|
||||||
|
"mainlyClear": "Κυρίως καθαρός",
|
||||||
|
"fog": "Ομίχλη",
|
||||||
|
"drizzle": "Ψιχάλες",
|
||||||
|
"freezingDrizzle": "Παγωμένο ψιλόβροχο",
|
||||||
|
"rain": "Βροχή",
|
||||||
|
"freezingRain": "Παγωμένη βροχή",
|
||||||
|
"snowFall": "Χιονόπτωση",
|
||||||
|
"snowGrains": "Κόκκοι χιονιού",
|
||||||
|
"rainShowers": "Βροχοπτώσεις",
|
||||||
|
"snowShowers": "Χιονοπτώσεις",
|
||||||
|
"thunderstorm": "Καταιγίδα",
|
||||||
|
"thunderstormWithHail": "Καταιγίδα με χαλάζι",
|
||||||
|
"unknown": "Άγνωστο"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
public/locales/el/settings/common.json
Normal file
29
public/locales/el/settings/common.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"title": "Ρυθμίσεις",
|
||||||
|
"tooltip": "Ρυθμίσεις",
|
||||||
|
"tabs": {
|
||||||
|
"common": "Συχνές επιλογές",
|
||||||
|
"customizations": "Παραμετροποιήσεις"
|
||||||
|
},
|
||||||
|
"tips": {
|
||||||
|
"configTip": "Ανεβάστε το αρχείο ρυθμίσεών σας σύροντάς το στη σελίδα!"
|
||||||
|
},
|
||||||
|
"credits": {
|
||||||
|
"madeWithLove": "Φτιαγμένο με ❤️ από @"
|
||||||
|
},
|
||||||
|
"grow": "Πλέγμα ανάπτυξης (παίρνει όλο το χώρο)",
|
||||||
|
"layout": {
|
||||||
|
"title": "Διάταξη ταμπλό",
|
||||||
|
"main": "Κύριο",
|
||||||
|
"sidebar": "Πλαϊνή μπάρα",
|
||||||
|
"cannotturnoff": "Δεν μπορεί να απενεργοποιηθεί",
|
||||||
|
"dashboardlayout": "Διάταξη ταμπλό",
|
||||||
|
"enablersidebar": "Απόκρυψη δεξιάς πλευρικής στήλης",
|
||||||
|
"enablelsidebar": "Ενεργοποίηση της αριστερής πλευρικής γραμμής",
|
||||||
|
"enablesearchbar": "Ενεργοποίηση της γραμμής αναζήτησης",
|
||||||
|
"enabledocker": "Ενεργοποίηση ενσωμάτωση docker",
|
||||||
|
"enableping": "Ενεργοποίηση pings",
|
||||||
|
"enablelsidebardesc": "Προαιρετικά. Μπορεί να χρησιμοποιηθεί μόνο για εφαρμογές και ενσωματώσεις",
|
||||||
|
"enablersidebardesc": "Προαιρετικά. Μπορεί να χρησιμοποιηθεί μόνο για εφαρμογές και ενσωματώσεις"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
public/locales/el/settings/customization/app-width.json
Normal file
3
public/locales/el/settings/customization/app-width.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"label": "Πλάτος εφαρμογής"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"suffix": "{{color}} χρώμα"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"label": "Αδιαφάνεια εφαρμογής"
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"pageTitle": {
|
||||||
|
"label": "Τίτλος Σελίδας"
|
||||||
|
},
|
||||||
|
"metaTitle": {
|
||||||
|
"label": "Meta Τίτλος"
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"label": "Λογότυπο"
|
||||||
|
},
|
||||||
|
"favicon": {
|
||||||
|
"label": "Έμβλημα"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"label": "Φόντο"
|
||||||
|
},
|
||||||
|
"customCSS": {
|
||||||
|
"label": "Προσαρμοσμένη CSS",
|
||||||
|
"placeholder": "Το προσαρμοσμένο CSS θα εφαρμοστεί τελευταίο"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"submit": "Υποβολή"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"label": "Απόχρωση"
|
||||||
|
}
|
||||||
3
public/locales/el/settings/general/color-schema.json
Normal file
3
public/locales/el/settings/general/color-schema.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"label": "Εναλλαγή στη λειτουργία {{scheme}}"
|
||||||
|
}
|
||||||
86
public/locales/el/settings/general/config-changer.json
Normal file
86
public/locales/el/settings/general/config-changer.json
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"configSelect": {
|
||||||
|
"label": "Αλλαγή παραμέτρων",
|
||||||
|
"description": "{{configCount}} ρυθμίσεις είναι διαθέσιμες",
|
||||||
|
"loadingNew": "Φόρτωση της διαμόρφωσής σας...",
|
||||||
|
"pleaseWait": "Παρακαλώ περιμένετε μέχρι να φορτωθεί η νέα σας διαμόρφωση!"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"copy": {
|
||||||
|
"title": "Επιλέξτε το όνομα της νέας σας διαμόρφωσης",
|
||||||
|
"form": {
|
||||||
|
"configName": {
|
||||||
|
"label": "Όνομα διαμόρφωσης",
|
||||||
|
"validation": {
|
||||||
|
"required": "Απαιτείται όνομα διαμόρφωσης",
|
||||||
|
"notUnique": "Αυτό το όνομα είναι ήδη σε χρήση"
|
||||||
|
},
|
||||||
|
"placeholder": "Το νέο σας όνομα ρυθμίσεων"
|
||||||
|
},
|
||||||
|
"submitButton": "Επιβεβαίωση"
|
||||||
|
},
|
||||||
|
"events": {
|
||||||
|
"configSaved": {
|
||||||
|
"title": "Η διαμόρφωση αποθηκεύτηκε",
|
||||||
|
"message": "Διαμόρφωση αποθηκεύτηκε ως {{configName}}"
|
||||||
|
},
|
||||||
|
"configCopied": {
|
||||||
|
"title": "Η ρύθμιση αντιγράφηκε",
|
||||||
|
"message": "Η διαμόρφωση αντιγράφηκε ως {{configName}}"
|
||||||
|
},
|
||||||
|
"configNotCopied": {
|
||||||
|
"title": "Αδυναμία αντιγραφής αρχείου ρυθμίσεων",
|
||||||
|
"message": "Οι ρυθμίσεις σας δεν αντιγράφηκαν ως {{configName}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirmDeletion": {
|
||||||
|
"title": "Επιβεβαιώστε τη διαγραφή της διαμόρφωσής σας",
|
||||||
|
"warningText": "Πρόκειται να διαγράψετε το '<b>{{configName}}</b>'",
|
||||||
|
"text": "Λάβετε υπόψη ότι η διαγραφή δεν είναι αναστρέψιμη και τα δεδομένα σας θα χαθούν οριστικά. Αφού κάνετε κλικ σε αυτό το κουμπί, το αρχείο θα διαγραφεί οριστικά από το δίσκο σας. Φροντίστε να δημιουργήσετε ένα επαρκές αντίγραφο ασφαλείας της διαμόρφωσής σας.",
|
||||||
|
"buttons": {
|
||||||
|
"confirm": "Ναι, διαγράψτε το '<b>{{configName}}</b>'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"download": "Λήψη ρυθμίσεων",
|
||||||
|
"delete": {
|
||||||
|
"text": "Διαγραφή ρυθμίσεων",
|
||||||
|
"notifications": {
|
||||||
|
"deleted": {
|
||||||
|
"title": "Η ρύθμιση διαγράφηκε",
|
||||||
|
"message": "Η ρύθμιση διαγράφηκε"
|
||||||
|
},
|
||||||
|
"deleteFailed": {
|
||||||
|
"title": "Η διαγραφή ρυθμίσεων απέτυχε",
|
||||||
|
"message": "Η διαγραφή ρυθμίσεων απέτυχε"
|
||||||
|
},
|
||||||
|
"deleteFailedDefaultConfig": {
|
||||||
|
"title": "Η προεπιλεγμένη ρύθμιση παραμέτρων δεν μπορεί να διαγραφεί",
|
||||||
|
"message": "Η διαμόρφωση δεν διαγράφηκε από το σύστημα αρχείων"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"saveCopy": "Αποθηκεύστε ένα αντίγραφο"
|
||||||
|
},
|
||||||
|
"dropzone": {
|
||||||
|
"notifications": {
|
||||||
|
"invalidConfig": {
|
||||||
|
"title": "Αποτυχία φόρτωσης του αρχείου ρυθμίσεων",
|
||||||
|
"message": "Δεν μπόρεσε να φορτώσει τις ρυθμίσεις σας. Μη έγκυρη μορφή JSON."
|
||||||
|
},
|
||||||
|
"loadedSuccessfully": {
|
||||||
|
"title": "Οι ρυθμίσεις {{configName}} φορτώθηκαν επιτυχώς"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accept": {
|
||||||
|
"title": "Φόρτωση ρυθμίσεων",
|
||||||
|
"text": "Σύρετε αρχεία εδώ για να ανεβάσετε μια διαμόρφωση ρυθμίσεων. Υποστήριξη μόνο για αρχεία JSON."
|
||||||
|
},
|
||||||
|
"reject": {
|
||||||
|
"title": "Η μεταφόρτωση απορρίφθηκε",
|
||||||
|
"text": "Αυτή η μορφή αρχείου δεν υποστηρίζεται. Παρακαλούμε ανεβάζετε μόνο αρχεία JSON."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"label": "Γλώσσα"
|
||||||
|
}
|
||||||
19
public/locales/el/settings/general/search-engine.json
Normal file
19
public/locales/el/settings/general/search-engine.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"title": "Μηχανή αναζήτησης",
|
||||||
|
"configurationName": "Διαμόρφωση μηχανής αναζήτησης",
|
||||||
|
"tips": {
|
||||||
|
"generalTip": "Υπάρχουν πολλά προθέματα που μπορείτε να χρησιμοποιήσετε! Προσθέτοντας αυτά μπροστά από το ερώτημά σας θα φιλτράρετε τα αποτελέσματα. !s (Web), !t (Torrents), !y (YouTube) και !m (Media).",
|
||||||
|
"placeholderTip": "%s μπορεί να χρησιμοποιηθεί ως placeholder για το ερώτημα."
|
||||||
|
},
|
||||||
|
"customEngine": {
|
||||||
|
"title": "Προσαρμοσμένη μηχανή αναζήτησης",
|
||||||
|
"label": "Ερώτημα URL",
|
||||||
|
"placeholder": "Προσαρμοσμένο URL ερώτησης"
|
||||||
|
},
|
||||||
|
"searchNewTab": {
|
||||||
|
"label": "Άνοιγμα αποτελεσμάτων αναζήτησης σε νέα καρτέλα"
|
||||||
|
},
|
||||||
|
"searchEnabled": {
|
||||||
|
"label": "Ενεργοποιημένη αναζήτηση"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
public/locales/el/settings/general/theme-selector.json
Normal file
3
public/locales/el/settings/general/theme-selector.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"label": "Εναλλαγή στη λειτουργία {{theme}}"
|
||||||
|
}
|
||||||
3
public/locales/el/settings/general/widget-positions.json
Normal file
3
public/locales/el/settings/general/widget-positions.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"label": "Τοποθετήστε τα widgets στα αριστερά"
|
||||||
|
}
|
||||||
7
public/locales/en/layout/modals/icon-picker.json
Normal file
7
public/locales/en/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "Search something...",
|
||||||
|
"searchLimitationTitle": "Limited to 30 results",
|
||||||
|
"searchLimitationMessage": "Search results were limited to 30 because there were too many matches"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/es/layout/modals/icon-picker.json
Normal file
7
public/locales/es/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/fr/layout/modals/icon-picker.json
Normal file
7
public/locales/fr/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/he/layout/modals/icon-picker.json
Normal file
7
public/locales/he/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/it/layout/modals/icon-picker.json
Normal file
7
public/locales/it/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"card": {
|
"card": {
|
||||||
"footer": {
|
"footer": {
|
||||||
"error": "Errore",
|
"error": "Errore",
|
||||||
"lastUpdated": "Ultimo aggiornamento {{time}} ago"
|
"lastUpdated": "Ultimo aggiornamento {{time}} fa"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"header": {
|
"header": {
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"progress": "Avanzamento"
|
"progress": "Avanzamento"
|
||||||
},
|
},
|
||||||
"item": {
|
"item": {
|
||||||
"text": "Gestito da {{appName}}, rapporto {{ratio}}"
|
"text": "Gestito da {{appName}}, {{ratio}} ratio"
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"nothingFound": "Nessun torrent trovato"
|
"nothingFound": "Nessun torrent trovato"
|
||||||
@@ -61,10 +61,10 @@
|
|||||||
"introductionPrefix": "Gestito da",
|
"introductionPrefix": "Gestito da",
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"queuePosition": "Posizione in coda - {{position}}",
|
"queuePosition": "Posizione in coda - {{position}}",
|
||||||
"progress": "Progressi - {{progress}}%",
|
"progress": "Progresso - {{progress}}%",
|
||||||
"totalSelectedSize": "Totale - {{totalSize}}",
|
"totalSelectedSize": "Totale - {{totalSize}}",
|
||||||
"state": "Stato - {{state}}",
|
"state": "Stato - {{state}}",
|
||||||
"ratio": "Rapporto -",
|
"ratio": "Ratio -",
|
||||||
"completed": "Completato"
|
"completed": "Completato"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
public/locales/ja/layout/modals/icon-picker.json
Normal file
7
public/locales/ja/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/ko/layout/modals/icon-picker.json
Normal file
7
public/locales/ko/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/lol/layout/modals/icon-picker.json
Normal file
7
public/locales/lol/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/nl/layout/modals/icon-picker.json
Normal file
7
public/locales/nl/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/pl/layout/modals/icon-picker.json
Normal file
7
public/locales/pl/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/pt/layout/modals/icon-picker.json
Normal file
7
public/locales/pt/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/ru/layout/modals/icon-picker.json
Normal file
7
public/locales/ru/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/sl/layout/modals/icon-picker.json
Normal file
7
public/locales/sl/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
"i18n": "Laddade namnområden för I18n-översättningar",
|
"i18n": "Laddade namnområden för I18n-översättningar",
|
||||||
"locales": "Konfigurerade I18n lokalspråk",
|
"locales": "Konfigurerade I18n lokalspråk",
|
||||||
"contact": "Har du problem eller frågor? Kontakta oss!",
|
"contact": "Har du problem eller frågor? Kontakta oss!",
|
||||||
"addToDashboard": "Lägg till i instrumentpanel"
|
"addToDashboard": "Lägg till på instrumentpanel"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"icon": {
|
"icon": {
|
||||||
"label": "Appikon",
|
"label": "Appikon",
|
||||||
"description": "Ikonen som kommer att visas på instrumentpanelen."
|
"description": "Ikon som kommer att visas på instrumentpanelen."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"integration": {
|
"integration": {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"xPosition": "X axel position",
|
"xPosition": "Position X-axel",
|
||||||
"width": "Bredd",
|
"width": "Bredd",
|
||||||
"height": "Höjd",
|
"height": "Höjd",
|
||||||
"yPosition": "Y axel position",
|
"yPosition": "Position Y-axel",
|
||||||
"zeroOrHigher": "0 eller högre",
|
"zeroOrHigher": "0 eller högre",
|
||||||
"betweenXandY": "Mellan {{min}} och {{max}}"
|
"betweenXandY": "Mellan {{min}} och {{max}}"
|
||||||
}
|
}
|
||||||
7
public/locales/sv/layout/modals/icon-picker.json
Normal file
7
public/locales/sv/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"label": "Flerkärnig CPU vy"
|
"label": "Flerkärnig CPU vy"
|
||||||
},
|
},
|
||||||
"storageMultiView": {
|
"storageMultiView": {
|
||||||
"label": "Visning av flera lagrings enheter"
|
"label": "Visning av flera lagringsenheter"
|
||||||
},
|
},
|
||||||
"useCompactView": {
|
"useCompactView": {
|
||||||
"label": "Använd kompakt vy"
|
"label": "Använd kompakt vy"
|
||||||
|
|||||||
7
public/locales/uk/layout/modals/icon-picker.json
Normal file
7
public/locales/uk/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,12 +58,12 @@
|
|||||||
"title": "Завантаження..."
|
"title": "Завантаження..."
|
||||||
},
|
},
|
||||||
"popover": {
|
"popover": {
|
||||||
"introductionPrefix": "Під керівництвом",
|
"introductionPrefix": "Керується",
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"queuePosition": "Позиція в черзі - {{position}}",
|
"queuePosition": "Позиція в черзі - {{position}}",
|
||||||
"progress": "Прогрес - {{progress}}%.",
|
"progress": "Прогрес - {{progress}}%.",
|
||||||
"totalSelectedSize": "Всього - {{totalSize}}",
|
"totalSelectedSize": "Всього - {{totalSize}}",
|
||||||
"state": "Держава - {{state}}",
|
"state": "Стан - {{state}}",
|
||||||
"ratio": "Коефіцієнт -",
|
"ratio": "Коефіцієнт -",
|
||||||
"completed": "Завершено"
|
"completed": "Завершено"
|
||||||
}
|
}
|
||||||
|
|||||||
7
public/locales/vi/layout/modals/icon-picker.json
Normal file
7
public/locales/vi/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/locales/zh/layout/modals/icon-picker.json
Normal file
7
public/locales/zh/layout/modals/icon-picker.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"iconPicker": {
|
||||||
|
"textInputPlaceholder": "",
|
||||||
|
"searchLimitationTitle": "",
|
||||||
|
"searchLimitationMessage": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"enablelsidebar": "启用左边的侧边栏",
|
"enablelsidebar": "启用左边的侧边栏",
|
||||||
"enablesearchbar": "启用搜索栏",
|
"enablesearchbar": "启用搜索栏",
|
||||||
"enabledocker": "启用docker集成",
|
"enabledocker": "启用docker集成",
|
||||||
"enableping": "启用平移功能",
|
"enableping": "启用Ping功能",
|
||||||
"enablelsidebardesc": "可选的。只能用于应用程序和集成",
|
"enablelsidebardesc": "可选的。只能用于应用程序和集成",
|
||||||
"enablersidebardesc": "可选的。只能用于应用程序和集成"
|
"enablersidebardesc": "可选的。只能用于应用程序和集成"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
|
IconAnchor,
|
||||||
IconBrandDiscord,
|
IconBrandDiscord,
|
||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
IconFile,
|
IconFile,
|
||||||
@@ -27,9 +28,9 @@ import { InitOptions } from 'i18next';
|
|||||||
import { i18n, Trans, useTranslation } from 'next-i18next';
|
import { i18n, Trans, useTranslation } from 'next-i18next';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { CURRENT_VERSION } from '../../../data/constants';
|
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { useConfigStore } from '../../config/store';
|
import { useConfigStore } from '../../config/store';
|
||||||
|
import { usePackageAttributesStore } from '../../tools/client/zustands/usePackageAttributesStore';
|
||||||
import { usePrimaryGradient } from '../layout/useGradient';
|
import { usePrimaryGradient } from '../layout/useGradient';
|
||||||
import Credits from '../Settings/Common/Credits';
|
import Credits from '../Settings/Common/Credits';
|
||||||
|
|
||||||
@@ -140,6 +141,7 @@ interface ExtendedInitOptions extends InitOptions {
|
|||||||
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
||||||
// TODO: Fix this to not request. Pass it as a prop.
|
// TODO: Fix this to not request. Pass it as a prop.
|
||||||
const colorGradiant = usePrimaryGradient();
|
const colorGradiant = usePrimaryGradient();
|
||||||
|
const { attributes } = usePackageAttributesStore();
|
||||||
|
|
||||||
const { configVersion } = useConfigContext();
|
const { configVersion } = useConfigContext();
|
||||||
const { configs } = useConfigStore();
|
const { configs } = useConfigStore();
|
||||||
@@ -198,7 +200,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
content: (
|
content: (
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="gradient" gradient={colorGradiant}>
|
||||||
{CURRENT_VERSION}
|
{attributes.packageVersion ?? 'Unknown'}
|
||||||
</Badge>
|
</Badge>
|
||||||
{newVersionAvailable && (
|
{newVersionAvailable && (
|
||||||
<HoverCard shadow="md" position="top" withArrow>
|
<HoverCard shadow="md" position="top" withArrow>
|
||||||
@@ -226,13 +228,22 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
{newVersionAvailable}
|
{newVersionAvailable}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</b>{' '}
|
</b>{' '}
|
||||||
is available ! Current version: {CURRENT_VERSION}
|
is available ! Current version: {attributes.packageVersion}
|
||||||
</HoverCard.Dropdown>
|
</HoverCard.Dropdown>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <IconAnchor size={20} />,
|
||||||
|
label: 'Node environment',
|
||||||
|
content: (
|
||||||
|
<Badge variant="gradient" gradient={colorGradiant}>
|
||||||
|
{attributes.environment}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
...items,
|
...items,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,14 @@ export const ChangePositionModal = ({
|
|||||||
const width = parseInt(form.values.width, 10);
|
const width = parseInt(form.values.width, 10);
|
||||||
const height = parseInt(form.values.height, 10);
|
const height = parseInt(form.values.height, 10);
|
||||||
|
|
||||||
if (!form.values.x || !form.values.y || Number.isNaN(width) || Number.isNaN(height)) return;
|
if (
|
||||||
|
form.values.x === null ||
|
||||||
|
form.values.y === null ||
|
||||||
|
Number.isNaN(width) ||
|
||||||
|
Number.isNaN(height)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onSubmit(form.values.x, form.values.y, width, height);
|
onSubmit(form.values.x, form.values.y, width, height);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createStyles, Flex, Tabs, TextInput } from '@mantine/core';
|
import { Autocomplete, createStyles, Flex, Tabs } from '@mantine/core';
|
||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { AppType } from '../../../../../../types/app';
|
import { AppType } from '../../../../../../types/app';
|
||||||
import { DebouncedAppIcon } from '../Shared/DebouncedAppIcon';
|
import { DebouncedAppIcon } from '../Shared/DebouncedAppIcon';
|
||||||
@@ -18,16 +19,21 @@ export const AppearanceTab = ({
|
|||||||
}: AppearanceTabProps) => {
|
}: AppearanceTabProps) => {
|
||||||
const { t } = useTranslation('layout/modals/add-app');
|
const { t } = useTranslation('layout/modals/add-app');
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
|
const { isLoading, error, data } = useQuery({
|
||||||
|
queryKey: ['autocompleteLocale'],
|
||||||
|
queryFn: () => fetch('/api/getLocalImages').then((res) => res.json()),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Panel value="appearance" pt="lg">
|
<Tabs.Panel value="appearance" pt="lg">
|
||||||
<Flex gap={5}>
|
<Flex gap={5}>
|
||||||
<TextInput
|
<Autocomplete
|
||||||
className={classes.textInput}
|
className={classes.textInput}
|
||||||
icon={<DebouncedAppIcon form={form} width={20} height={20} />}
|
icon={<DebouncedAppIcon form={form} width={20} height={20} />}
|
||||||
label={t('appearance.icon.label')}
|
label={t('appearance.icon.label')}
|
||||||
description={t('appearance.icon.description')}
|
description={t('appearance.icon.description')}
|
||||||
variant="default"
|
variant="default"
|
||||||
|
data={data?.files ?? []}
|
||||||
withAsterisk
|
withAsterisk
|
||||||
required
|
required
|
||||||
{...form.getInputProps('appearance.iconUrl')}
|
{...form.getInputProps('appearance.iconUrl')}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ interface IconSelectorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const IconSelector = ({ onChange, allowAppNamePropagation, form }: IconSelectorProps) => {
|
export const IconSelector = ({ onChange, allowAppNamePropagation, form }: IconSelectorProps) => {
|
||||||
const { t } = useTranslation('layout/tools');
|
const { t } = useTranslation('layout/modals/icon-picker');
|
||||||
|
|
||||||
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
|
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
|
||||||
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',
|
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const AvailableElementTypes = ({
|
|||||||
{
|
{
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
// Thank you ChatGPT ;)
|
// Thank you ChatGPT ;)
|
||||||
position: previousConfig.wrappers.length + 1,
|
position: previousConfig.categories.length + 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
categories: [
|
categories: [
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Group, Title } from '@mantine/core';
|
import { Accordion, Title } from '@mantine/core';
|
||||||
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { CategoryType } from '../../../../types/category';
|
import { CategoryType } from '../../../../types/category';
|
||||||
import { HomarrCardWrapper } from '../../Tiles/HomarrCardWrapper';
|
import { useCardStyles } from '../../../layout/useCardStyles';
|
||||||
import { useEditModeStore } from '../../Views/useEditModeStore';
|
import { useEditModeStore } from '../../Views/useEditModeStore';
|
||||||
import { useGridstack } from '../gridstack/use-gridstack';
|
import { useGridstack } from '../gridstack/use-gridstack';
|
||||||
import { WrapperContent } from '../WrapperContent';
|
import { WrapperContent } from '../WrapperContent';
|
||||||
@@ -13,20 +15,47 @@ interface DashboardCategoryProps {
|
|||||||
export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
|
export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
|
||||||
const { refs, apps, widgets } = useGridstack('category', category.id);
|
const { refs, apps, widgets } = useGridstack('category', category.id);
|
||||||
const isEditMode = useEditModeStore((x) => x.enabled);
|
const isEditMode = useEditModeStore((x) => x.enabled);
|
||||||
|
const { config } = useConfigContext();
|
||||||
|
const { classes: cardClasses } = useCardStyles(true);
|
||||||
|
|
||||||
|
const categoryList = config?.categories.map((x) => x.name) ?? [];
|
||||||
|
const [toggledCategories, setToggledCategories] = useLocalStorage({
|
||||||
|
key: `${config?.configProperties.name}-app-shelf-toggled`,
|
||||||
|
// This is a bit of a hack to toggle the categories on the first load, return a string[] of the categories
|
||||||
|
defaultValue: categoryList,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HomarrCardWrapper pt={10} mx={10} isCategory>
|
<Accordion
|
||||||
<Group position="apart" align="center">
|
classNames={{
|
||||||
<Title order={3}>{category.name}</Title>
|
item: cardClasses.card,
|
||||||
{isEditMode ? <CategoryEditMenu category={category} /> : null}
|
}}
|
||||||
</Group>
|
mx={10}
|
||||||
<div
|
chevronPosition="left"
|
||||||
className="grid-stack grid-stack-category"
|
multiple
|
||||||
data-category={category.id}
|
value={isEditMode ? categoryList : toggledCategories}
|
||||||
ref={refs.wrapper}
|
variant="separated"
|
||||||
>
|
radius="lg"
|
||||||
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
|
onChange={(state) => {
|
||||||
</div>
|
// Cancel if edit mode is on
|
||||||
</HomarrCardWrapper>
|
if (isEditMode) return;
|
||||||
|
setToggledCategories([...state]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Accordion.Item value={category.name}>
|
||||||
|
<Accordion.Control icon={isEditMode && <CategoryEditMenu category={category} />}>
|
||||||
|
<Title order={3}>{category.name}</Title>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<div
|
||||||
|
className="grid-stack grid-stack-category"
|
||||||
|
data-category={category.id}
|
||||||
|
ref={refs.wrapper}
|
||||||
|
>
|
||||||
|
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
|
||||||
|
</div>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
|
|||||||
useCategoryActions(configName, category);
|
useCategoryActions(configName, category);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu withinPortal position="left-start" withArrow>
|
<Menu withinPortal withArrow>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<ActionIcon>
|
<ActionIcon>
|
||||||
<IconDots />
|
<IconDots />
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions';
|
import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions';
|
||||||
|
import { AppType } from '../../../../types/app';
|
||||||
import { CategoryType } from '../../../../types/category';
|
import { CategoryType } from '../../../../types/category';
|
||||||
import { WrapperType } from '../../../../types/wrapper';
|
import { WrapperType } from '../../../../types/wrapper';
|
||||||
|
import { IWidget } from '../../../../widgets/widgets';
|
||||||
import { CategoryEditModalInnerProps } from './CategoryEditModal';
|
import { CategoryEditModalInnerProps } from './CategoryEditModal';
|
||||||
|
|
||||||
export const useCategoryActions = (configName: string | undefined, category: CategoryType) => {
|
export const useCategoryActions = (configName: string | undefined, category: CategoryType) => {
|
||||||
@@ -185,29 +187,75 @@ export const useCategoryActions = (configName: string | undefined, category: Cat
|
|||||||
const currentItem = previous.categories.find((x) => x.id === category.id);
|
const currentItem = previous.categories.find((x) => x.id === category.id);
|
||||||
if (!currentItem) return previous;
|
if (!currentItem) return previous;
|
||||||
// Find the main wrapper
|
// Find the main wrapper
|
||||||
const mainWrapper = previous.wrappers.find((x) => x.position === 1);
|
const mainWrapper = previous.wrappers.find((x) => x.position === 0);
|
||||||
|
const mainWrapperId = mainWrapper?.id ?? 'default';
|
||||||
|
|
||||||
// Check that the app has an area.type or "category" and that the area.id is the current category
|
const isAppAffectedFilter = (app: AppType): boolean => {
|
||||||
const appsToMove = previous.apps.filter(
|
if (!app.area) {
|
||||||
(x) => x.area && x.area.type === 'category' && x.area.properties.id === currentItem.id
|
return false;
|
||||||
);
|
}
|
||||||
appsToMove.forEach((x) => {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
x.area = { type: 'wrapper', properties: { id: mainWrapper?.id ?? 'default' } };
|
|
||||||
});
|
|
||||||
|
|
||||||
const widgetsToMove = previous.widgets.filter(
|
if (app.area.type !== 'category') {
|
||||||
(x) => x.area && x.area.type === 'category' && x.area.properties.id === currentItem.id
|
return false;
|
||||||
);
|
}
|
||||||
|
|
||||||
widgetsToMove.forEach((x) => {
|
if (app.area.properties.id === mainWrapperId) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
return false;
|
||||||
x.area = { type: 'wrapper', properties: { id: mainWrapper?.id ?? 'default' } };
|
}
|
||||||
});
|
|
||||||
|
return app.area.properties.id === currentItem.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isWidgetAffectedFilter = (widget: IWidget<string, any>): boolean => {
|
||||||
|
if (!widget.area) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.area.type !== 'category') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.area.properties.id === mainWrapperId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget.area.properties.id === currentItem.id;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...previous,
|
...previous,
|
||||||
apps: previous.apps,
|
apps: [
|
||||||
|
...previous.apps.filter((x) => !isAppAffectedFilter(x)),
|
||||||
|
...previous.apps
|
||||||
|
.filter((x) => isAppAffectedFilter(x))
|
||||||
|
.map((app): AppType => ({
|
||||||
|
...app,
|
||||||
|
area: {
|
||||||
|
...app.area,
|
||||||
|
type: 'wrapper',
|
||||||
|
properties: {
|
||||||
|
...app.area.properties,
|
||||||
|
id: mainWrapperId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
widgets: [
|
||||||
|
...previous.widgets.filter((widget) => !isWidgetAffectedFilter(widget)),
|
||||||
|
...previous.widgets
|
||||||
|
.filter((widget) => isWidgetAffectedFilter(widget))
|
||||||
|
.map((widget): IWidget<string, any> => ({
|
||||||
|
...widget,
|
||||||
|
area: {
|
||||||
|
...widget.area,
|
||||||
|
type: 'wrapper',
|
||||||
|
properties: {
|
||||||
|
...widget.area.properties,
|
||||||
|
id: mainWrapperId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
],
|
||||||
categories: previous.categories.filter((x) => x.id !== category.id),
|
categories: previous.categories.filter((x) => x.id !== category.id),
|
||||||
wrappers: previous.wrappers.filter((x) => x.position !== currentItem.position),
|
wrappers: previous.wrappers.filter((x) => x.position !== currentItem.position),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ActionIcon, Button, Group, Text, Title, Tooltip } from '@mantine/core';
|
|||||||
import { IconEditCircle, IconEditCircleOff } from '@tabler/icons';
|
import { IconEditCircle, IconEditCircleOff } from '@tabler/icons';
|
||||||
import { getCookie } from 'cookies-next';
|
import { getCookie } from 'cookies-next';
|
||||||
import { Trans, useTranslation } from 'next-i18next';
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||||
import { useConfigContext } from '../../../../../config/provider';
|
import { useConfigContext } from '../../../../../config/provider';
|
||||||
import { useScreenSmallerThan } from '../../../../../hooks/useScreenSmallerThan';
|
import { useScreenSmallerThan } from '../../../../../hooks/useScreenSmallerThan';
|
||||||
@@ -23,6 +24,8 @@ export const ToggleEditModeAction = () => {
|
|||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
const { classes } = useCardStyles(true);
|
const { classes } = useCardStyles(true);
|
||||||
|
|
||||||
|
useHotkeys([['ctrl+E', toggleEditMode]]);
|
||||||
|
|
||||||
const toggleButtonClicked = () => {
|
const toggleButtonClicked = () => {
|
||||||
toggleEditMode();
|
toggleEditMode();
|
||||||
if (enabled || config === undefined || config?.schemaVersion === undefined) {
|
if (enabled || config === undefined || config?.schemaVersion === undefined) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Box, createStyles, Group, Header as MantineHeader, Indicator } from '@mantine/core';
|
import { Box, createStyles, Group, Header as MantineHeader, Indicator } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { CURRENT_VERSION, REPO_URL } from '../../../../data/constants';
|
import { REPO_URL } from '../../../../data/constants';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import DockerMenuButton from '../../../modules/Docker/DockerModule';
|
||||||
|
import { usePackageAttributesStore } from '../../../tools/client/zustands/usePackageAttributesStore';
|
||||||
import { Logo } from '../Logo';
|
import { Logo } from '../Logo';
|
||||||
import { useCardStyles } from '../useCardStyles';
|
import { useCardStyles } from '../useCardStyles';
|
||||||
import DockerMenuButton from '../../../modules/Docker/DockerModule';
|
|
||||||
import { ToggleEditModeAction } from './Actions/ToggleEditMode/ToggleEditMode';
|
import { ToggleEditModeAction } from './Actions/ToggleEditMode/ToggleEditMode';
|
||||||
import { Search } from './Search';
|
import { Search } from './Search';
|
||||||
import { SettingsMenu } from './SettingsMenu';
|
import { SettingsMenu } from './SettingsMenu';
|
||||||
@@ -14,23 +14,22 @@ export const HeaderHeight = 64;
|
|||||||
export function Header(props: any) {
|
export function Header(props: any) {
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const { classes: cardClasses } = useCardStyles(false);
|
const { classes: cardClasses } = useCardStyles(false);
|
||||||
|
const { attributes } = usePackageAttributesStore();
|
||||||
const { config } = useConfigContext();
|
|
||||||
|
|
||||||
const [newVersionAvailable, setNewVersionAvailable] = useState<string>('');
|
const [newVersionAvailable, setNewVersionAvailable] = useState<string>('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch Data here when component first mounted
|
// Fetch Data here when component first mounted
|
||||||
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
|
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
|
||||||
res.json().then((data) => {
|
res.json().then((data) => {
|
||||||
if (data.tag_name > CURRENT_VERSION) {
|
if (data.tag_name > `v${attributes.packageVersion}`) {
|
||||||
setNewVersionAvailable(data.tag_name);
|
setNewVersionAvailable(data.tag_name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [CURRENT_VERSION]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineHeader height={HeaderHeight} className={cardClasses.card}>
|
<MantineHeader height="auto" className={cardClasses.card}>
|
||||||
<Group p="xs" noWrap grow>
|
<Group p="xs" noWrap grow>
|
||||||
<Box className={classes.hide}>
|
<Box className={classes.hide}>
|
||||||
<Logo />
|
<Logo />
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import {
|
|||||||
import { useDebouncedValue, useHotkeys } from '@mantine/hooks';
|
import { useDebouncedValue, useHotkeys } from '@mantine/hooks';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { IconBrandYoutube, IconDownload, IconMovie, IconSearch } from '@tabler/icons';
|
import { IconBrandYoutube, IconDownload, IconMovie, IconSearch } from '@tabler/icons';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
import { OverseerrMediaDisplay } from '../../../modules/common';
|
import { OverseerrMediaDisplay } from '../../../modules/common';
|
||||||
import { IModule } from '../../../modules/ModuleTypes';
|
import { IModule } from '../../../modules/ModuleTypes';
|
||||||
|
import { ConfigType } from '../../../types/config';
|
||||||
import { searchUrls } from '../../Settings/Common/SearchEngine/SearchEngineSelector';
|
import { searchUrls } from '../../Settings/Common/SearchEngine/SearchEngineSelector';
|
||||||
import Tip from '../Tip';
|
import Tip from '../Tip';
|
||||||
import { useCardStyles } from '../useCardStyles';
|
import { useCardStyles } from '../useCardStyles';
|
||||||
@@ -54,8 +56,8 @@ export function Search() {
|
|||||||
const { t } = useTranslation('modules/search');
|
const { t } = useTranslation('modules/search');
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [debounced, cancel] = useDebouncedValue(searchQuery, 250);
|
const [debounced] = useDebouncedValue(searchQuery, 250);
|
||||||
const { classes: cardClasses } = useCardStyles(false);
|
const { classes: cardClasses } = useCardStyles(true);
|
||||||
|
|
||||||
const isOverseerrEnabled = config?.apps.some(
|
const isOverseerrEnabled = config?.apps.some(
|
||||||
(x) => x.integration.type === 'overseerr' || x.integration.type === 'jellyseerr'
|
(x) => x.integration.type === 'overseerr' || x.integration.type === 'jellyseerr'
|
||||||
@@ -136,30 +138,40 @@ export function Search() {
|
|||||||
const textInput = useRef<HTMLInputElement>(null);
|
const textInput = useRef<HTMLInputElement>(null);
|
||||||
useHotkeys([['mod+K', () => textInput.current?.focus()]]);
|
useHotkeys([['mod+K', () => textInput.current?.focus()]]);
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const openInNewTab = config?.settings.common.searchEngine.properties.openInNewTab
|
const openTarget = getOpenTarget(config);
|
||||||
? '_blank'
|
|
||||||
: '_self';
|
|
||||||
const [OverseerrResults, setOverseerrResults] = useState<any[]>([]);
|
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
if (debounced !== '' && selectedSearchEngine.value === 'overseerr' && searchQuery.length > 3) {
|
data: OverseerrResults,
|
||||||
axios.get(`/api/modules/overseerr?query=${searchQuery}`).then((res) => {
|
isLoading,
|
||||||
setOverseerrResults(res.data.results ?? []);
|
error,
|
||||||
});
|
} = useQuery(
|
||||||
|
['overseerr', debounced],
|
||||||
|
async () => {
|
||||||
|
if (debounced !== '' && selectedSearchEngine.value === 'overseerr' && debounced.length > 3) {
|
||||||
|
const res = await axios.get(`/api/modules/overseerr?query=${debounced}`);
|
||||||
|
return res.data.results ?? [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchInterval: false,
|
||||||
}
|
}
|
||||||
}, [debounced]);
|
);
|
||||||
|
|
||||||
const isModuleEnabled = config?.settings.customization.layout.enabledSearchbar;
|
const isModuleEnabled = config?.settings.customization.layout.enabledSearchbar;
|
||||||
if (!isModuleEnabled) {
|
if (!isModuleEnabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Fix the bug where clicking anything inside the Modal to ask for a movie
|
//TODO: Fix the bug where clicking anything inside the Modal to ask for a movie
|
||||||
// will close it (Because it closes the underlying Popover)
|
// will close it (Because it closes the underlying Popover)
|
||||||
return (
|
return (
|
||||||
<Box style={{ width: '100%', maxWidth: 400 }}>
|
<Box style={{ width: '100%', maxWidth: 400 }}>
|
||||||
<Popover
|
<Popover
|
||||||
opened={OverseerrResults.length > 0 && opened && searchQuery.length > 3}
|
opened={OverseerrResults && OverseerrResults.length > 0 && opened && searchQuery.length > 3}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
withinPortal
|
withinPortal
|
||||||
shadow="md"
|
shadow="md"
|
||||||
@@ -182,7 +194,7 @@ export function Search() {
|
|||||||
setOpened(false);
|
setOpened(false);
|
||||||
if (item.url) {
|
if (item.url) {
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
window.open(item.openedUrl ? item.openedUrl : item.url, openInNewTab);
|
window.open(item.openedUrl ? item.openedUrl : item.url, openTarget);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
// Replace %s if it is in selectedSearchEngine.url with searchQuery, otherwise append searchQuery at the end of it
|
// Replace %s if it is in selectedSearchEngine.url with searchQuery, otherwise append searchQuery at the end of it
|
||||||
@@ -193,9 +205,9 @@ export function Search() {
|
|||||||
autocompleteData.length === 0
|
autocompleteData.length === 0
|
||||||
) {
|
) {
|
||||||
if (selectedSearchEngine.url.includes('%s')) {
|
if (selectedSearchEngine.url.includes('%s')) {
|
||||||
window.open(selectedSearchEngine.url.replace('%s', searchQuery), openInNewTab);
|
window.open(selectedSearchEngine.url.replace('%s', searchQuery), openTarget);
|
||||||
} else {
|
} else {
|
||||||
window.open(selectedSearchEngine.url + searchQuery, openInNewTab);
|
window.open(selectedSearchEngine.url + searchQuery, openTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -207,16 +219,17 @@ export function Search() {
|
|||||||
/>
|
/>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<div>
|
<ScrollArea style={{ height: '80vh', maxWidth: '90vw' }} offsetScrollbars>
|
||||||
<ScrollArea style={{ height: 400, width: 420 }} offsetScrollbars>
|
{OverseerrResults &&
|
||||||
{OverseerrResults.slice(0, 5).map((result, index) => (
|
OverseerrResults.slice(0, 4).map((result: any, index: number) => (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<OverseerrMediaDisplay key={result.id} media={result} />
|
<OverseerrMediaDisplay key={result.id} media={result} />
|
||||||
{index < OverseerrResults.length - 1 && <Divider variant="dashed" my="xl" />}
|
{index < OverseerrResults.length - 1 && index < 3 && (
|
||||||
|
<Divider variant="dashed" my="xs" />
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -287,3 +300,11 @@ export function Search() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getOpenTarget = (config: ConfigType | undefined): '_blank' | '_self' => {
|
||||||
|
if (!config || config.settings.common.searchEngine.properties.openInNewTab === undefined) {
|
||||||
|
return '_blank';
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.settings.common.searchEngine.properties.openInNewTab ? '_blank' : '_self';
|
||||||
|
};
|
||||||
|
|||||||
11
src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx
Normal file
11
src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { NormalizedDownloadQueueResponse } from '../../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||||
|
|
||||||
|
export const useGetDownloadClientsQueue = () => useQuery({
|
||||||
|
queryKey: ['network-speed'],
|
||||||
|
queryFn: async (): Promise<NormalizedDownloadQueueResponse> => {
|
||||||
|
const response = await fetch('/api/modules/downloads');
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
refetchInterval: 3000,
|
||||||
|
});
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Query, useQuery } from '@tanstack/react-query';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { NormalizedTorrentListResponse } from '../../../types/api/NormalizedTorrentListResponse';
|
|
||||||
|
|
||||||
interface TorrentsDataRequestParams {
|
|
||||||
appId: string;
|
|
||||||
refreshInterval: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGetTorrentData = (params: TorrentsDataRequestParams) =>
|
|
||||||
useQuery({
|
|
||||||
queryKey: ['torrentsData', params.appId],
|
|
||||||
queryFn: fetchData,
|
|
||||||
refetchOnWindowFocus: true,
|
|
||||||
refetchInterval(_: any, query: Query) {
|
|
||||||
if (query.state.fetchFailureCount < 3) {
|
|
||||||
return params.refreshInterval;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
enabled: !!params.appId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchData = async (): Promise<NormalizedTorrentListResponse> => {
|
|
||||||
const response = await axios.post('/api/modules/torrents');
|
|
||||||
return response.data as NormalizedTorrentListResponse;
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ActionIcon, Drawer, Text, Tooltip } from '@mantine/core';
|
import { ActionIcon, Drawer, Text, Tooltip } from '@mantine/core';
|
||||||
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { IconBrandDocker, IconX } from '@tabler/icons';
|
import { IconBrandDocker, IconX } from '@tabler/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@@ -17,6 +18,7 @@ export default function DockerMenuButton(props: any) {
|
|||||||
const [selection, setSelection] = useState<Docker.ContainerInfo[]>([]);
|
const [selection, setSelection] = useState<Docker.ContainerInfo[]>([]);
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
const { classes } = useCardStyles(true);
|
const { classes } = useCardStyles(true);
|
||||||
|
useHotkeys([['mod+B', () => setOpened(!opened)]]);
|
||||||
|
|
||||||
const dockerEnabled = config?.settings.customization.layout.enabledDocker || false;
|
const dockerEnabled = config?.settings.customization.layout.enabledDocker || false;
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ export default function DockerMenuButton(props: any) {
|
|||||||
<>
|
<>
|
||||||
<Drawer
|
<Drawer
|
||||||
opened={opened}
|
opened={opened}
|
||||||
|
trapFocus={false}
|
||||||
onClose={() => setOpened(false)}
|
onClose={() => setOpened(false)}
|
||||||
padding="xl"
|
padding="xl"
|
||||||
position="right"
|
position="right"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ScrollArea,
|
ScrollArea,
|
||||||
TextInput,
|
TextInput,
|
||||||
useMantineTheme,
|
useMantineTheme,
|
||||||
|
Text,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useElementSize } from '@mantine/hooks';
|
import { useElementSize } from '@mantine/hooks';
|
||||||
import { IconSearch } from '@tabler/icons';
|
import { IconSearch } from '@tabler/icons';
|
||||||
@@ -78,8 +79,16 @@ export default function DockerTable({
|
|||||||
transitionDuration={0}
|
transitionDuration={0}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>{element.Names[0].replace('/', '')}</td>
|
<td>
|
||||||
{width > MIN_WIDTH_MOBILE && <td>{element.Image}</td>}
|
<Text size="lg" weight={600}>
|
||||||
|
{element.Names[0].replace('/', '')}
|
||||||
|
</Text>
|
||||||
|
</td>
|
||||||
|
{width > MIN_WIDTH_MOBILE && (
|
||||||
|
<td>
|
||||||
|
<Text size="lg">{element.Image}</Text>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
{width > MIN_WIDTH_MOBILE && (
|
{width > MIN_WIDTH_MOBILE && (
|
||||||
<td>
|
<td>
|
||||||
<Group>
|
<Group>
|
||||||
@@ -111,12 +120,13 @@ export default function DockerTable({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea style={{ height: '80vh' }}>
|
<ScrollArea style={{ height: '90vh' }} offsetScrollbars>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder={t('search.placeholder')}
|
placeholder={t('search.placeholder')}
|
||||||
mt="md"
|
mr="md"
|
||||||
icon={<IconSearch size={14} />}
|
icon={<IconSearch size={14} />}
|
||||||
value={search}
|
value={search}
|
||||||
|
autoFocus
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
<Table ref={ref} captionSide="bottom" highlightOnHover verticalSpacing="sm">
|
<Table ref={ref} captionSide="bottom" highlightOnHover verticalSpacing="sm">
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export function MediaDisplay({ media }: { media: IMedia }) {
|
|||||||
const { t } = useTranslation('modules/common-media-cards');
|
const { t } = useTranslation('modules/common-media-cards');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group mr="xs" align="stretch" noWrap style={{ maxHeight: 250, maxWidth: 400 }} spacing="xs">
|
<Group noWrap style={{ maxHeight: 250, maxWidth: 400 }} p={0} m={0} spacing="xs">
|
||||||
<Image src={media.poster} height={200} width={150} radius="md" fit="cover" />
|
<Image src={media.poster} height={200} width={150} radius="md" fit="cover" />
|
||||||
<Stack justify="space-around">
|
<Stack justify="space-around">
|
||||||
<Stack spacing="sm">
|
<Stack spacing="sm">
|
||||||
@@ -223,7 +223,7 @@ export function MediaDisplay({ media }: { media: IMedia }) {
|
|||||||
{media.overview}
|
{media.overview}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Group noWrap>
|
<Group spacing="xs">
|
||||||
{media.plexUrl && (
|
{media.plexUrl && (
|
||||||
<Button
|
<Button
|
||||||
component="a"
|
component="a"
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import Layout from '../components/layout/Layout';
|
|||||||
import { useInitConfig } from '../config/init';
|
import { useInitConfig } from '../config/init';
|
||||||
import { getFallbackConfig } from '../tools/config/getFallbackConfig';
|
import { getFallbackConfig } from '../tools/config/getFallbackConfig';
|
||||||
import { getFrontendConfig } from '../tools/config/getFrontendConfig';
|
import { getFrontendConfig } from '../tools/config/getFrontendConfig';
|
||||||
import { getServerSideTranslations } from '../tools/getServerSideTranslations';
|
import { getServerSideTranslations } from '../tools/server/getServerSideTranslations';
|
||||||
import { dashboardNamespaces } from '../tools/translation-namespaces';
|
import { dashboardNamespaces } from '../tools/server/translation-namespaces';
|
||||||
import { ConfigType } from '../types/config';
|
import { ConfigType } from '../types/config';
|
||||||
import { DashboardServerSideProps } from '../types/dashboardPageType';
|
import { DashboardServerSideProps } from '../types/dashboardPageType';
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { GetServerSidePropsContext } from 'next';
|
|||||||
import { appWithTranslation } from 'next-i18next';
|
import { appWithTranslation } from 'next-i18next';
|
||||||
import { AppProps } from 'next/app';
|
import { AppProps } from 'next/app';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal';
|
import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal';
|
||||||
import { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal';
|
import { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal';
|
||||||
import { EditAppModal } from '../components/Dashboard/Modals/EditAppModal/EditAppModal';
|
import { EditAppModal } from '../components/Dashboard/Modals/EditAppModal/EditAppModal';
|
||||||
@@ -21,8 +22,16 @@ import '../styles/global.scss';
|
|||||||
import { ColorTheme } from '../tools/color';
|
import { ColorTheme } from '../tools/color';
|
||||||
import { queryClient } from '../tools/queryClient';
|
import { queryClient } from '../tools/queryClient';
|
||||||
import { theme } from '../tools/theme';
|
import { theme } from '../tools/theme';
|
||||||
|
import {
|
||||||
|
getServiceSidePackageAttributes,
|
||||||
|
ServerSidePackageAttributesType,
|
||||||
|
} from '../tools/server/getPackageVersion';
|
||||||
|
import { usePackageAttributesStore } from '../tools/client/zustands/usePackageAttributesStore';
|
||||||
|
|
||||||
function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
function App(
|
||||||
|
this: any,
|
||||||
|
props: AppProps & { colorScheme: ColorScheme; packageAttributes: ServerSidePackageAttributesType }
|
||||||
|
) {
|
||||||
const { Component, pageProps } = props;
|
const { Component, pageProps } = props;
|
||||||
const [primaryColor, setPrimaryColor] = useState<MantineTheme['primaryColor']>('red');
|
const [primaryColor, setPrimaryColor] = useState<MantineTheme['primaryColor']>('red');
|
||||||
const [secondaryColor, setSecondaryColor] = useState<MantineTheme['primaryColor']>('orange');
|
const [secondaryColor, setSecondaryColor] = useState<MantineTheme['primaryColor']>('orange');
|
||||||
@@ -45,6 +54,12 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
|||||||
getInitialValueInEffect: true,
|
getInitialValueInEffect: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { setInitialPackageAttributes } = usePackageAttributesStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInitialPackageAttributes(props.packageAttributes);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleColorScheme = (value?: ColorScheme) =>
|
const toggleColorScheme = (value?: ColorScheme) =>
|
||||||
setColorScheme(value || (colorScheme === 'dark' ? 'light' : 'dark'));
|
setColorScheme(value || (colorScheme === 'dark' ? 'light' : 'dark'));
|
||||||
|
|
||||||
@@ -102,6 +117,7 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
|||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</ColorTheme.Provider>
|
</ColorTheme.Provider>
|
||||||
</ColorSchemeProvider>
|
</ColorSchemeProvider>
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -109,6 +125,7 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
|||||||
|
|
||||||
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({
|
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({
|
||||||
colorScheme: getCookie('color-scheme', ctx) || 'light',
|
colorScheme: getCookie('color-scheme', ctx) || 'light',
|
||||||
|
packageAttributes: getServiceSidePackageAttributes(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default appWithTranslation(App);
|
export default appWithTranslation(App);
|
||||||
|
|||||||
27
src/pages/api/getLocalImages.ts
Normal file
27
src/pages/api/getLocalImages.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
// Get the name of all the files in the /public/icons folder handle if the folder doesn't exist
|
||||||
|
if (!fs.existsSync('./public/icons')) {
|
||||||
|
return res.status(200).json({
|
||||||
|
files: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const files = fs.readdirSync('./public/icons');
|
||||||
|
// Return the list of files with the /public/icons prefix
|
||||||
|
return res.status(200).json({
|
||||||
|
files: files.map((file) => `/icons/${file}`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
// Filter out if the reuqest is a POST or a GET
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
return Get(req, res);
|
||||||
|
}
|
||||||
|
return res.status(405).json({
|
||||||
|
statusCode: 405,
|
||||||
|
message: 'Method not allowed',
|
||||||
|
});
|
||||||
|
};
|
||||||
220
src/pages/api/modules/downloads/index.ts
Normal file
220
src/pages/api/modules/downloads/index.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import { Deluge } from '@ctrl/deluge';
|
||||||
|
import { QBittorrent } from '@ctrl/qbittorrent';
|
||||||
|
import { Transmission } from '@ctrl/transmission';
|
||||||
|
import { AllClientData } from '@ctrl/shared-torrent';
|
||||||
|
import Consola from 'consola';
|
||||||
|
import { getCookie } from 'cookies-next';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { Client } from 'sabnzbd-api';
|
||||||
|
import { getConfig } from '../../../../tools/config/getConfig';
|
||||||
|
import {
|
||||||
|
NormalizedDownloadAppStat,
|
||||||
|
NormalizedDownloadQueueResponse,
|
||||||
|
} from '../../../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||||
|
import { ConfigAppType, IntegrationField } from '../../../../types/app';
|
||||||
|
import { UsenetQueueItem } from '../../../../widgets/useNet/types';
|
||||||
|
import { NzbgetClient } from '../usenet/nzbget/nzbget-client';
|
||||||
|
import { NzbgetQueueItem, NzbgetStatus } from '../usenet/nzbget/types';
|
||||||
|
|
||||||
|
const Get = async (request: NextApiRequest, response: NextApiResponse) => {
|
||||||
|
const configName = getCookie('config-name', { req: request });
|
||||||
|
const config = getConfig(configName?.toString() ?? 'default');
|
||||||
|
|
||||||
|
const failedClients: string[] = [];
|
||||||
|
|
||||||
|
const clientData: Promise<NormalizedDownloadAppStat>[] = config.apps.map(async (app) => {
|
||||||
|
try {
|
||||||
|
const response = await GetDataFromClient(app);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
} as NormalizedDownloadAppStat;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
Consola.error(
|
||||||
|
`Error communicating with your download client '${app.name}' (${app.id}): ${err}`
|
||||||
|
);
|
||||||
|
failedClients.push(app.id);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
} as NormalizedDownloadAppStat;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const settledPromises = await Promise.allSettled(clientData);
|
||||||
|
|
||||||
|
const data: NormalizedDownloadAppStat[] = settledPromises
|
||||||
|
.filter((x) => x.status === 'fulfilled')
|
||||||
|
.map((promise) => (promise as PromiseFulfilledResult<NormalizedDownloadAppStat>).value)
|
||||||
|
.filter((x) => x !== undefined && x.type !== undefined);
|
||||||
|
|
||||||
|
const responseBody = { apps: data, failedApps: failedClients } as NormalizedDownloadQueueResponse;
|
||||||
|
|
||||||
|
if (failedClients.length > 0) {
|
||||||
|
Consola.warn(`${failedClients.length} download clients failed. Please check your configuration and the above log`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.status(200).json(responseBody);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GetDataFromClient = async (
|
||||||
|
app: ConfigAppType
|
||||||
|
): Promise<NormalizedDownloadAppStat | undefined> => {
|
||||||
|
const reduceTorrent = (data: AllClientData): NormalizedDownloadAppStat => ({
|
||||||
|
type: 'torrent',
|
||||||
|
appId: app.id,
|
||||||
|
success: true,
|
||||||
|
torrents: data.torrents,
|
||||||
|
totalDownload: data.torrents
|
||||||
|
.map((torrent) => torrent.downloadSpeed)
|
||||||
|
.reduce((acc, torrent) => acc + torrent),
|
||||||
|
totalUpload: data.torrents
|
||||||
|
.map((torrent) => torrent.uploadSpeed)
|
||||||
|
.reduce((acc, torrent) => acc + torrent),
|
||||||
|
});
|
||||||
|
|
||||||
|
const findField = (app: ConfigAppType, field: IntegrationField) =>
|
||||||
|
app.integration?.properties.find((x) => x.field === field)?.value ?? undefined;
|
||||||
|
|
||||||
|
switch (app.integration?.type) {
|
||||||
|
case 'deluge': {
|
||||||
|
return reduceTorrent(
|
||||||
|
await new Deluge({
|
||||||
|
baseUrl: app.url,
|
||||||
|
password: findField(app, 'password'),
|
||||||
|
}).getAllData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'transmission': {
|
||||||
|
return reduceTorrent(
|
||||||
|
await new Transmission({
|
||||||
|
baseUrl: app.url,
|
||||||
|
username: findField(app, 'username'),
|
||||||
|
password: findField(app, 'password'),
|
||||||
|
}).getAllData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'qBittorrent': {
|
||||||
|
return reduceTorrent(
|
||||||
|
await new QBittorrent({
|
||||||
|
baseUrl: app.url,
|
||||||
|
username: findField(app, 'username'),
|
||||||
|
password: findField(app, 'password'),
|
||||||
|
}).getAllData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'sabnzbd': {
|
||||||
|
const { origin } = new URL(app.url);
|
||||||
|
const client = new Client(origin, findField(app, 'apiKey') ?? '');
|
||||||
|
const queue = await client.queue();
|
||||||
|
const items: UsenetQueueItem[] = queue.slots.map((slot) => {
|
||||||
|
const [hours, minutes, seconds] = slot.timeleft.split(':');
|
||||||
|
const eta = dayjs.duration({
|
||||||
|
hour: parseInt(hours, 10),
|
||||||
|
minutes: parseInt(minutes, 10),
|
||||||
|
seconds: parseInt(seconds, 10),
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: slot.nzo_id,
|
||||||
|
eta: eta.asSeconds(),
|
||||||
|
name: slot.filename,
|
||||||
|
progress: parseFloat(slot.percentage),
|
||||||
|
size: parseFloat(slot.mb) * 1000 * 1000,
|
||||||
|
state: slot.status.toLowerCase() as any,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const killobitsPerSecond = Number(queue.kbpersec);
|
||||||
|
const bytesPerSecond = killobitsPerSecond * 1024; // convert killobytes to bytes
|
||||||
|
return {
|
||||||
|
type: 'usenet',
|
||||||
|
appId: app.id,
|
||||||
|
totalDownload: bytesPerSecond,
|
||||||
|
nzbs: items,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'nzbGet': {
|
||||||
|
const url = new URL(app.url);
|
||||||
|
const options = {
|
||||||
|
host: url.hostname,
|
||||||
|
port: url.port,
|
||||||
|
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
|
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nzbGet = NzbgetClient(options);
|
||||||
|
const nzbgetQueue: NzbgetQueueItem[] = await new Promise((resolve, reject) => {
|
||||||
|
nzbGet.listGroups((err: any, result: NzbgetQueueItem[]) => {
|
||||||
|
if (!err) {
|
||||||
|
resolve(result);
|
||||||
|
} else {
|
||||||
|
Consola.error(`Error while listing groups: ${err}`);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!nzbgetQueue) {
|
||||||
|
throw new Error('Error while getting NZBGet queue');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nzbgetStatus: NzbgetStatus = await new Promise((resolve, reject) => {
|
||||||
|
nzbGet.status((err: any, result: NzbgetStatus) => {
|
||||||
|
if (!err) {
|
||||||
|
resolve(result);
|
||||||
|
} else {
|
||||||
|
Consola.error(`Error while retrieving NZBGet stats: ${err}`);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!nzbgetStatus) {
|
||||||
|
throw new Error('Error while getting NZBGet status');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nzbgetItems: UsenetQueueItem[] = nzbgetQueue.map((item: NzbgetQueueItem) => ({
|
||||||
|
id: item.NZBID.toString(),
|
||||||
|
name: item.NZBName,
|
||||||
|
progress: (item.DownloadedSizeMB / item.FileSizeMB) * 100,
|
||||||
|
eta: (item.RemainingSizeMB * 1000000) / nzbgetStatus.DownloadRate,
|
||||||
|
// Multiple MB to get bytes
|
||||||
|
size: item.FileSizeMB * 1000 * 1000,
|
||||||
|
state: getNzbgetState(item.Status),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'usenet',
|
||||||
|
appId: app.id,
|
||||||
|
nzbs: nzbgetItems,
|
||||||
|
success: true,
|
||||||
|
totalDownload: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (request: NextApiRequest, response: NextApiResponse) => {
|
||||||
|
if (request.method === 'GET') {
|
||||||
|
return Get(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.status(405);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getNzbgetState(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case 'QUEUED':
|
||||||
|
return 'queued';
|
||||||
|
case 'PAUSED ':
|
||||||
|
return 'paused';
|
||||||
|
default:
|
||||||
|
return 'downloading';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import { Deluge } from '@ctrl/deluge';
|
|
||||||
import { QBittorrent } from '@ctrl/qbittorrent';
|
|
||||||
import { AllClientData } from '@ctrl/shared-torrent';
|
|
||||||
import { Transmission } from '@ctrl/transmission';
|
|
||||||
import Consola from 'consola';
|
|
||||||
import { getCookie } from 'cookies-next';
|
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import { getConfig } from '../../../tools/config/getConfig';
|
|
||||||
import { NormalizedTorrentListResponse } from '../../../types/api/NormalizedTorrentListResponse';
|
|
||||||
import { ConfigAppType, IntegrationType } from '../../../types/app';
|
|
||||||
|
|
||||||
const supportedTypes: IntegrationType[] = ['deluge', 'qBittorrent', 'transmission'];
|
|
||||||
|
|
||||||
async function Post(req: NextApiRequest, res: NextApiResponse<NormalizedTorrentListResponse>) {
|
|
||||||
// Get the type of app from the request url
|
|
||||||
const configName = getCookie('config-name', { req });
|
|
||||||
const config = getConfig(configName?.toString() ?? 'default');
|
|
||||||
|
|
||||||
const clientApps = config.apps.filter(
|
|
||||||
(app) =>
|
|
||||||
app.integration && app.integration.type && supportedTypes.includes(app.integration.type)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (clientApps.length < 1) {
|
|
||||||
return res.status(500).json({
|
|
||||||
allSuccess: false,
|
|
||||||
missingDownloadClients: true,
|
|
||||||
labels: [],
|
|
||||||
torrents: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const promiseList: Promise<ConcatenatedClientData>[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < clientApps.length; i += 1) {
|
|
||||||
const app = clientApps[i];
|
|
||||||
const getAllData = getAllDataForClient(app);
|
|
||||||
if (!getAllData) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const concatenatedPromise = async (): Promise<ConcatenatedClientData> => ({
|
|
||||||
clientData: await getAllData,
|
|
||||||
appId: app.id,
|
|
||||||
});
|
|
||||||
promiseList.push(concatenatedPromise());
|
|
||||||
}
|
|
||||||
|
|
||||||
const settledPromises = await Promise.allSettled(promiseList);
|
|
||||||
const fulfilledPromises = settledPromises.filter(
|
|
||||||
(settledPromise) => settledPromise.status === 'fulfilled'
|
|
||||||
);
|
|
||||||
|
|
||||||
const fulfilledClientData: ConcatenatedClientData[] = fulfilledPromises.map(
|
|
||||||
(fulfilledPromise) => (fulfilledPromise as PromiseFulfilledResult<ConcatenatedClientData>).value
|
|
||||||
);
|
|
||||||
|
|
||||||
const notFulfilledClientData = settledPromises
|
|
||||||
.filter((x) => x.status === 'rejected')
|
|
||||||
.map(
|
|
||||||
(fulfilledPromise) =>
|
|
||||||
(fulfilledPromise as PromiseRejectedResult)
|
|
||||||
);
|
|
||||||
|
|
||||||
notFulfilledClientData.forEach((result) => {
|
|
||||||
Consola.error(`Error while communicating with torrent download client: ${result.reason}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
Consola.info(
|
|
||||||
`Successfully fetched data from ${fulfilledPromises.length} torrent download clients`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
labels: fulfilledClientData.flatMap((clientData) => clientData.clientData.labels),
|
|
||||||
torrents: fulfilledClientData.map((clientData) => ({
|
|
||||||
appId: clientData.appId,
|
|
||||||
torrents: clientData.clientData.torrents,
|
|
||||||
})),
|
|
||||||
allSuccess: settledPromises.length === fulfilledPromises.length,
|
|
||||||
missingDownloadClients: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllDataForClient = (app: ConfigAppType) => {
|
|
||||||
switch (app.integration?.type) {
|
|
||||||
case 'deluge': {
|
|
||||||
const password =
|
|
||||||
app.integration?.properties.find((x) => x.field === 'password')?.value ?? undefined;
|
|
||||||
return new Deluge({
|
|
||||||
baseUrl: app.url,
|
|
||||||
password,
|
|
||||||
}).getAllData();
|
|
||||||
}
|
|
||||||
case 'transmission': {
|
|
||||||
return new Transmission({
|
|
||||||
baseUrl: app.url,
|
|
||||||
username:
|
|
||||||
app.integration!.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
|
||||||
password:
|
|
||||||
app.integration!.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
|
||||||
}).getAllData();
|
|
||||||
}
|
|
||||||
case 'qBittorrent': {
|
|
||||||
return new QBittorrent({
|
|
||||||
baseUrl: app.url,
|
|
||||||
username:
|
|
||||||
app.integration!.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
|
||||||
password:
|
|
||||||
app.integration!.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
|
||||||
}).getAllData();
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
Consola.error(`unable to find torrent client of type '${app.integration?.type}'`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type ConcatenatedClientData = {
|
|
||||||
appId: string;
|
|
||||||
clientData: AllClientData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
// Filter out if the reuqest is a POST or a GET
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
return Post(req, res);
|
|
||||||
}
|
|
||||||
return res.status(405).json({
|
|
||||||
statusCode: 405,
|
|
||||||
message: 'Method not allowed',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -39,7 +39,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const url = new URL(app.url);
|
const url = new URL(app.url);
|
||||||
const options = {
|
const options = {
|
||||||
host: url.hostname,
|
host: url.hostname,
|
||||||
port: url.port,
|
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
|
||||||
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const url = new URL(app.url);
|
const url = new URL(app.url);
|
||||||
const options = {
|
const options = {
|
||||||
host: url.hostname,
|
host: url.hostname,
|
||||||
port: url.port,
|
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
|
||||||
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const url = new URL(app.url);
|
const url = new URL(app.url);
|
||||||
const options = {
|
const options = {
|
||||||
host: url.hostname,
|
host: url.hostname,
|
||||||
port: url.port,
|
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
|
||||||
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const url = new URL(app.url);
|
const url = new URL(app.url);
|
||||||
const options = {
|
const options = {
|
||||||
host: url.hostname,
|
host: url.hostname,
|
||||||
port: url.port,
|
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
|
||||||
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const url = new URL(app.url);
|
const url = new URL(app.url);
|
||||||
const options = {
|
const options = {
|
||||||
host: url.hostname,
|
host: url.hostname,
|
||||||
port: url.port,
|
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
|
||||||
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { getCookie, setCookie } from 'cookies-next';
|
import { getCookie, setCookie } from 'cookies-next';
|
||||||
|
import fs from 'fs';
|
||||||
import { GetServerSidePropsContext } from 'next';
|
import { GetServerSidePropsContext } from 'next';
|
||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
import { LoadConfigComponent } from '../components/Config/LoadConfig';
|
import { LoadConfigComponent } from '../components/Config/LoadConfig';
|
||||||
import { Dashboard } from '../components/Dashboard/Dashboard';
|
import { Dashboard } from '../components/Dashboard/Dashboard';
|
||||||
import Layout from '../components/layout/Layout';
|
import Layout from '../components/layout/Layout';
|
||||||
import { useInitConfig } from '../config/init';
|
import { useInitConfig } from '../config/init';
|
||||||
import { getFrontendConfig } from '../tools/config/getFrontendConfig';
|
import { getFrontendConfig } from '../tools/config/getFrontendConfig';
|
||||||
import { getServerSideTranslations } from '../tools/getServerSideTranslations';
|
import { getServerSideTranslations } from '../tools/server/getServerSideTranslations';
|
||||||
import { dashboardNamespaces } from '../tools/translation-namespaces';
|
import { dashboardNamespaces } from '../tools/server/translation-namespaces';
|
||||||
import { DashboardServerSideProps } from '../types/dashboardPageType';
|
import { DashboardServerSideProps } from '../types/dashboardPageType';
|
||||||
|
|
||||||
export async function getServerSideProps({
|
export async function getServerSideProps({
|
||||||
@@ -47,11 +47,14 @@ export async function getServerSideProps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const translations = await getServerSideTranslations(req, res, dashboardNamespaces, locale);
|
const translations = await getServerSideTranslations(req, res, dashboardNamespaces, locale);
|
||||||
|
|
||||||
const config = getFrontendConfig(configName as string);
|
const config = getFrontendConfig(configName as string);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { configName: configName as string, config, ...translations },
|
props: {
|
||||||
|
configName: configName as string,
|
||||||
|
config,
|
||||||
|
...translations,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useRouter } from 'next/router';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
import { loginNamespaces } from '../tools/translation-namespaces';
|
import { loginNamespaces } from '../tools/server/translation-namespaces';
|
||||||
|
|
||||||
// TODO: Add links to the wiki articles about the login process.
|
// TODO: Add links to the wiki articles about the login process.
|
||||||
export default function AuthenticationTitle() {
|
export default function AuthenticationTitle() {
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import Dockerode from 'dockerode';
|
import Dockerode from 'dockerode';
|
||||||
import { MatchingImages, ServiceType, tryMatchPort } from './types';
|
|
||||||
|
|
||||||
async function MatchIcon(name: string) {
|
import { MatchingImages, ServiceType, tryMatchPort } from './types';
|
||||||
const res = await fetch(
|
|
||||||
`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${name
|
|
||||||
.replace(/\s+/g, '-')
|
|
||||||
.toLowerCase()}.png`
|
|
||||||
);
|
|
||||||
return res.ok ? res.url : '/imgs/favicon/favicon.png';
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryMatchType(imageName: string): ServiceType {
|
function tryMatchType(imageName: string): ServiceType {
|
||||||
// Try to find imageName inside MatchingImages
|
// Try to find imageName inside MatchingImages
|
||||||
|
|||||||
15
src/tools/client/zustands/usePackageAttributesStore.ts
Normal file
15
src/tools/client/zustands/usePackageAttributesStore.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import create from 'zustand';
|
||||||
|
|
||||||
|
import { ServerSidePackageAttributesType } from '../../server/getPackageVersion';
|
||||||
|
|
||||||
|
interface PackageAttributesState {
|
||||||
|
attributes: ServerSidePackageAttributesType;
|
||||||
|
setInitialPackageAttributes: (attributes: ServerSidePackageAttributesType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePackageAttributesStore = create<PackageAttributesState>((set) => ({
|
||||||
|
attributes: { packageVersion: undefined, environment: 'test' },
|
||||||
|
setInitialPackageAttributes(attributes) {
|
||||||
|
set((state) => ({ ...state, attributes }));
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -9,7 +9,7 @@ import { ICalendarWidget } from '../../widgets/calendar/CalendarTile';
|
|||||||
import { IDashDotTile } from '../../widgets/dashDot/DashDotTile';
|
import { IDashDotTile } from '../../widgets/dashDot/DashDotTile';
|
||||||
import { IDateWidget } from '../../widgets/date/DateTile';
|
import { IDateWidget } from '../../widgets/date/DateTile';
|
||||||
import { ITorrent } from '../../widgets/torrent/TorrentTile';
|
import { ITorrent } from '../../widgets/torrent/TorrentTile';
|
||||||
import { ITorrentNetworkTraffic } from '../../widgets/torrentNetworkTraffic/TorrentNetworkTrafficTile';
|
import { ITorrentNetworkTraffic } from '../../widgets/download-speed/TorrentNetworkTrafficTile';
|
||||||
import { IUsenetWidget } from '../../widgets/useNet/UseNetTile';
|
import { IUsenetWidget } from '../../widgets/useNet/UseNetTile';
|
||||||
import { IWeatherWidget } from '../../widgets/weather/WeatherTile';
|
import { IWeatherWidget } from '../../widgets/weather/WeatherTile';
|
||||||
import { IWidget } from '../../widgets/widgets';
|
import { IWidget } from '../../widgets/widgets';
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ export const languages: Language[] = [
|
|||||||
translatedName: 'Chinese',
|
translatedName: 'Chinese',
|
||||||
emoji: '🇨🇳',
|
emoji: '🇨🇳',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
shortName: 'el',
|
||||||
|
originalName: 'Ελληνικά',
|
||||||
|
translatedName: 'Greek',
|
||||||
|
emoji: '🇬🇷',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getLanguageByCode = (code: string | null) =>
|
export const getLanguageByCode = (code: string | null) =>
|
||||||
|
|||||||
14
src/tools/server/getPackageVersion.ts
Normal file
14
src/tools/server/getPackageVersion.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const getServerPackageVersion = (): string | undefined => process.env.npm_package_version;
|
||||||
|
|
||||||
|
const getServerNodeEnvironment = (): 'development' | 'production' | 'test' =>
|
||||||
|
process.env.NODE_ENV;
|
||||||
|
|
||||||
|
export const getServiceSidePackageAttributes = (): ServerSidePackageAttributesType => ({
|
||||||
|
packageVersion: getServerPackageVersion(),
|
||||||
|
environment: getServerNodeEnvironment(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ServerSidePackageAttributesType = {
|
||||||
|
packageVersion: string | undefined;
|
||||||
|
environment: 'development' | 'production' | 'test';
|
||||||
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
export const dashboardNamespaces = [
|
export const dashboardNamespaces = [
|
||||||
'common',
|
'common',
|
||||||
'layout/tools',
|
|
||||||
'layout/element-selector/selector',
|
'layout/element-selector/selector',
|
||||||
'layout/modals/add-app',
|
'layout/modals/add-app',
|
||||||
'layout/modals/change-position',
|
'layout/modals/change-position',
|
||||||
|
'layout/modals/icon-picker',
|
||||||
'layout/modals/about',
|
'layout/modals/about',
|
||||||
'layout/header/actions/toggle-edit-mode',
|
'layout/header/actions/toggle-edit-mode',
|
||||||
'layout/mobile/drawer',
|
'layout/mobile/drawer',
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user