🔀 Merge branch 'dev' into next-13

This commit is contained in:
Manuel
2023-01-31 22:21:15 +01:00
110 changed files with 1900 additions and 566 deletions

View File

@@ -1,3 +1,2 @@
export const REPO_URL = 'ajnart/homarr';
export const CURRENT_VERSION = 'v0.11.2';
export const ICON_PICKER_SLICE_LIMIT = 36;

View File

@@ -24,6 +24,7 @@ module.exports = {
'vi',
'uk',
'zh',
'el',
],
localePath: path.resolve('./public/locales'),
fallbackLng: 'en',

View File

@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "0.11.2",
"version": "0.11.3",
"description": "Homarr - A homepage for your server.",
"license": "MIT",
"repository": {
@@ -45,6 +45,7 @@
"@nivo/line": "^0.79.1",
"@tabler/icons": "^1.106.0",
"@tanstack/react-query": "^4.2.1",
"@tanstack/react-query-devtools": "^4.24.4",
"axios": "^0.27.2",
"consola": "^2.15.3",
"cookies-next": "^2.1.1",

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,27 @@
{
"title": "Καλώς ήρθατε!",
"text": "Παρακαλώ εισάγετε τον κωδικό σας",
"form": {
"fields": {
"password": {
"label": "Κωδικός",
"placeholder": "Ο κωδικός σας"
}
},
"buttons": {
"submit": "Σύνδεση"
}
},
"notifications": {
"checking": {
"title": "Έλεγχος κωδικού πρόσβασης",
"message": "Ο κωδικός πρόσβασής σας ελέγχεται..."
},
"correct": {
"title": "Σύνδεση επιτυχής, ανακατεύθυνση..."
},
"wrong": {
"title": "Ο κωδικός που εισαγάγατε είναι εσφαλμένος. Προσπαθήστε ξανά."
}
}
}

View 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": "ώρες"
}
}

View File

@@ -0,0 +1,11 @@
{
"modal": {
"title": "Προσθήκη νέου πλακιδίου",
"text": "Τα πλακάκια είναι το κύριο στοιχείο του Homarr. Χρησιμοποιούνται για την εμφάνιση των εφαρμογών σας και άλλων πληροφοριών. Μπορείτε να προσθέσετε όσα πλακίδια θέλετε."
},
"widgetDescription": "Τα widgets αλληλεπιδρούν με τις εφαρμογές σας, για να σας παρέχουν περισσότερο έλεγχο των εφαρμογών σας. Συνήθως απαιτούν πρόσθετες ρυθμίσεις πριν από τη χρήση.",
"goBack": "Επιστροφή στο προηγούμενο βήμα",
"actionIcon": {
"tooltip": "Προσθέστε ένα πλακίδιο"
}
}

View File

@@ -0,0 +1,16 @@
{
"description": "Στη Λειτουργία επεξεργασίας, μπορείτε να προσαρμόσετε τα πλακίδια και να ρυθμίσετε τις εφαρμογές. Οι αλλαγές δεν αποθηκεύονται μέχρι να βγείτε από τη λειτουργία επεξεργασίας.",
"button": {
"disabled": "Λειτουργία επεξεργασίας",
"enabled": "Έξοδος και Αποθήκευση"
},
"popover": {
"title": "Η λειτουργία επεξεργασίας είναι ενεργοποιημένη για <1>{{size}}</1> μέγεθος",
"text": "Μπορείτε να προσαρμόσετε και να ρυθμίσετε τις εφαρμογές σας τώρα. Οι αλλαγές <strong>δεν αποθηκεύονται</strong> μέχρι να βγείτε από τη λειτουργία επεξεργασίας"
},
"screenSizes": {
"small": "μικρό",
"medium": "μεσαίο",
"large": "μεγάλο"
}
}

View File

@@ -0,0 +1,3 @@
{
"title": "{{position}} πλαϊνή μπάρα"
}

View File

@@ -0,0 +1,7 @@
{
"description": "Το Homarr είναι ένα <strong>κομψό</strong>, <strong>μοντέρνο</strong> ταμπλό που βάζει όλες τις εφαρμογές και τις υπηρεσίες σας στα χέρια σας. Με το Homarr, μπορείτε να έχετε πρόσβαση και να ελέγχετε τα πάντα σε μια βολική τοποθεσία. Το Homarr ενσωματώνεται απρόσκοπτα με τις εφαρμογές που έχετε προσθέσει, παρέχοντάς σας πολύτιμες πληροφορίες και δίνοντάς σας πλήρη έλεγχο. Η εγκατάσταση είναι πανεύκολη και το Homarr υποστηρίζει ένα ευρύ φάσμα μεθόδων ανάπτυξης.",
"i18n": "Φορτωμένα πεδία ονομάτων μετάφρασης I18n",
"locales": "Διαμορφωμένες τοπικές ρυθμίσεις I18n",
"contact": "Έχετε προβλήματα ή ερωτήσεις; Συνδεθείτε μαζί μας!",
"addToDashboard": "Προσθήκη στο ταμπλό"
}

View 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": "Η φόρμα σας περιέχει άκυρα δεδομένα. Ως εκ τούτου, δεν μπορεί να αποθηκευτεί. Παρακαλούμε επιλύστε όλα τα προβλήματα και κάντε ξανά κλικ σε αυτό το κουμπί για να αποθηκεύσετε τις αλλαγές σας"
}
}

View File

@@ -0,0 +1,8 @@
{
"xPosition": "Θέση του άξονα X",
"width": "Πλάτος",
"height": "Ύψος",
"yPosition": "Θέση του άξονα Y",
"zeroOrHigher": "0 ή υψηλότερο",
"betweenXandY": "Μεταξύ {min} και {max}"
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,15 @@
{
"descriptor": {
"name": "Ημερολόγιο",
"description": "Εμφανίζει ένα ημερολόγιο με τις επερχόμενες κυκλοφορίες, από τις υποστηριζόμενες ενσωματώσεις.",
"settings": {
"title": "Ρυθμίσεις για το widget ημερολογίου",
"sundayStart": {
"label": "Ξεκινήστε την εβδομάδα από την Κυριακή"
},
"radarrReleaseType": {
"label": "Τύπος κυκλοφορίας Radarr"
}
}
}
}

View File

@@ -0,0 +1,6 @@
{
"buttons": {
"play": "Αναπαραγωγή",
"request": "Αίτημα"
}
}

View File

@@ -0,0 +1,10 @@
{
"settings": {
"label": "Ρυθμίσεις"
},
"errors": {
"unmappedOptions": {
"text": "<b>Εντοπίστηκε αχρησιμοποίητη παράμετρος στη διαμόρφωση</b><br /><code>{{key}}</code>. Το Homarr δεν μπορεί να ερμηνεύσει και να χρησιμοποιήσει αυτή την παράμετρο. Για να αποφύγετε οποιαδήποτε απροσδόκητη συμπεριφορά, δημιουργήστε αντίγραφα ασφαλείας των ρυθμίσεων σας και διορθώστε τις ρυθμίσεις σας."
}
}
}

View 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"
}
}
}
}

View File

@@ -0,0 +1,12 @@
{
"descriptor": {
"name": "Ημερομηνία και ώρα",
"description": "Εμφανίζει την τρέχουσα ημερομηνία και ώρα.",
"settings": {
"title": "Ρυθμίσεις για το widget ημερομηνίας και ώρας",
"display24HourFormat": {
"label": "Εμφάνιση πλήρης ώρας(24-ώρο)"
}
}
}
}

View 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": "Προσθέστε μια υπηρεσία λήψης για να δείτε τις τρέχουσες λήψεις σας"
}
}
}
}

View 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"
}
}

View 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": "Αριθμός επεισοδίων"
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
{
"descriptor": {
"name": "Ping",
"description": "Εμφανίζει μια ένδειξη κατάστασης ανάλογα με τον κωδικό απόκρισης HTTP μιας δεδομένης διεύθυνσης URL."
},
"states": {
"online": "Online {{response}}",
"offline": "Χωρίς σύνδεση {{response}}",
"loading": "Φόρτωση..."
}
}

View 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}}"
}

View 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": "Ολοκληρώθηκε"
}
}
}
}

View 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": "Σε παύση"
}
}

View 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": "Άγνωστο"
}
}
}

View 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": "Προαιρετικά. Μπορεί να χρησιμοποιηθεί μόνο για εφαρμογές και ενσωματώσεις"
}
}

View File

@@ -0,0 +1,3 @@
{
"label": "Πλάτος εφαρμογής"
}

View File

@@ -0,0 +1,3 @@
{
"suffix": "{{color}} χρώμα"
}

View File

@@ -0,0 +1,3 @@
{
"label": "Αδιαφάνεια εφαρμογής"
}

View File

@@ -0,0 +1,24 @@
{
"pageTitle": {
"label": "Τίτλος Σελίδας"
},
"metaTitle": {
"label": "Meta Τίτλος"
},
"logo": {
"label": "Λογότυπο"
},
"favicon": {
"label": "Έμβλημα"
},
"background": {
"label": "Φόντο"
},
"customCSS": {
"label": "Προσαρμοσμένη CSS",
"placeholder": "Το προσαρμοσμένο CSS θα εφαρμοστεί τελευταίο"
},
"buttons": {
"submit": "Υποβολή"
}
}

View File

@@ -0,0 +1,3 @@
{
"label": "Απόχρωση"
}

View File

@@ -0,0 +1,3 @@
{
"label": "Εναλλαγή στη λειτουργία {{scheme}}"
}

View 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."
}
}
}

View File

@@ -0,0 +1,3 @@
{
"label": "Γλώσσα"
}

View 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": "Ενεργοποιημένη αναζήτηση"
}
}

View File

@@ -0,0 +1,3 @@
{
"label": "Εναλλαγή στη λειτουργία {{theme}}"
}

View File

@@ -0,0 +1,3 @@
{
"label": "Τοποθετήστε τα widgets στα αριστερά"
}

View 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"
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -18,7 +18,7 @@
"card": {
"footer": {
"error": "Errore",
"lastUpdated": "Ultimo aggiornamento {{time}} ago"
"lastUpdated": "Ultimo aggiornamento {{time}} fa"
},
"table": {
"header": {
@@ -30,7 +30,7 @@
"progress": "Avanzamento"
},
"item": {
"text": "Gestito da {{appName}}, rapporto {{ratio}}"
"text": "Gestito da {{appName}}, {{ratio}} ratio"
},
"body": {
"nothingFound": "Nessun torrent trovato"
@@ -61,10 +61,10 @@
"introductionPrefix": "Gestito da",
"metrics": {
"queuePosition": "Posizione in coda - {{position}}",
"progress": "Progressi - {{progress}}%",
"progress": "Progresso - {{progress}}%",
"totalSelectedSize": "Totale - {{totalSize}}",
"state": "Stato - {{state}}",
"ratio": "Rapporto -",
"ratio": "Ratio -",
"completed": "Completato"
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -3,5 +3,5 @@
"i18n": "Laddade namnområden för I18n-översättningar",
"locales": "Konfigurerade I18n lokalspråk",
"contact": "Har du problem eller frågor? Kontakta oss!",
"addToDashboard": "Lägg till i instrumentpanel"
"addToDashboard": "Lägg till instrumentpanel"
}

View File

@@ -39,7 +39,7 @@
"appearance": {
"icon": {
"label": "Appikon",
"description": "Ikonen som kommer att visas på instrumentpanelen."
"description": "Ikon som kommer att visas på instrumentpanelen."
}
},
"integration": {

View File

@@ -1,8 +1,8 @@
{
"xPosition": "X axel position",
"xPosition": "Position X-axel",
"width": "Bredd",
"height": "Höjd",
"yPosition": "Y axel position",
"yPosition": "Position Y-axel",
"zeroOrHigher": "0 eller högre",
"betweenXandY": "Mellan {{min}} och {{max}}"
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -8,7 +8,7 @@
"label": "Flerkärnig CPU vy"
},
"storageMultiView": {
"label": "Visning av flera lagrings enheter"
"label": "Visning av flera lagringsenheter"
},
"useCompactView": {
"label": "Använd kompakt vy"

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -58,12 +58,12 @@
"title": "Завантаження..."
},
"popover": {
"introductionPrefix": "Під керівництвом",
"introductionPrefix": "Керується",
"metrics": {
"queuePosition": "Позиція в черзі - {{position}}",
"progress": "Прогрес - {{progress}}%.",
"totalSelectedSize": "Всього - {{totalSize}}",
"state": "Держава - {{state}}",
"state": "Стан - {{state}}",
"ratio": "Коефіцієнт -",
"completed": "Завершено"
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"iconPicker": {
"textInputPlaceholder": "",
"searchLimitationTitle": "",
"searchLimitationMessage": ""
}
}

View File

@@ -22,7 +22,7 @@
"enablelsidebar": "启用左边的侧边栏",
"enablesearchbar": "启用搜索栏",
"enabledocker": "启用docker集成",
"enableping": "启用平移功能",
"enableping": "启用Ping功能",
"enablelsidebardesc": "可选的。只能用于应用程序和集成",
"enablersidebardesc": "可选的。只能用于应用程序和集成"
}

View File

@@ -13,6 +13,7 @@ import {
Title,
} from '@mantine/core';
import {
IconAnchor,
IconBrandDiscord,
IconBrandGithub,
IconFile,
@@ -27,9 +28,9 @@ import { InitOptions } from 'i18next';
import { i18n, Trans, useTranslation } from 'next-i18next';
import Image from 'next/image';
import { ReactNode } from 'react';
import { CURRENT_VERSION } from '../../../data/constants';
import { useConfigContext } from '../../config/provider';
import { useConfigStore } from '../../config/store';
import { usePackageAttributesStore } from '../../tools/client/zustands/usePackageAttributesStore';
import { usePrimaryGradient } from '../layout/useGradient';
import Credits from '../Settings/Common/Credits';
@@ -140,6 +141,7 @@ interface ExtendedInitOptions extends InitOptions {
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
// TODO: Fix this to not request. Pass it as a prop.
const colorGradiant = usePrimaryGradient();
const { attributes } = usePackageAttributesStore();
const { configVersion } = useConfigContext();
const { configs } = useConfigStore();
@@ -198,7 +200,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
content: (
<Group position="right">
<Badge variant="gradient" gradient={colorGradiant}>
{CURRENT_VERSION}
{attributes.packageVersion ?? 'Unknown'}
</Badge>
{newVersionAvailable && (
<HoverCard shadow="md" position="top" withArrow>
@@ -226,13 +228,22 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
{newVersionAvailable}
</Anchor>
</b>{' '}
is available ! Current version: {CURRENT_VERSION}
is available ! Current version: {attributes.packageVersion}
</HoverCard.Dropdown>
</HoverCard>
)}
</Group>
),
},
{
icon: <IconAnchor size={20} />,
label: 'Node environment',
content: (
<Badge variant="gradient" gradient={colorGradiant}>
{attributes.environment}
</Badge>
),
},
...items,
];

View File

@@ -45,7 +45,14 @@ export const ChangePositionModal = ({
const width = parseInt(form.values.width, 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);
};

View File

@@ -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 { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import { AppType } from '../../../../../../types/app';
import { DebouncedAppIcon } from '../Shared/DebouncedAppIcon';
@@ -18,16 +19,21 @@ export const AppearanceTab = ({
}: AppearanceTabProps) => {
const { t } = useTranslation('layout/modals/add-app');
const { classes } = useStyles();
const { isLoading, error, data } = useQuery({
queryKey: ['autocompleteLocale'],
queryFn: () => fetch('/api/getLocalImages').then((res) => res.json()),
});
return (
<Tabs.Panel value="appearance" pt="lg">
<Flex gap={5}>
<TextInput
<Autocomplete
className={classes.textInput}
icon={<DebouncedAppIcon form={form} width={20} height={20} />}
label={t('appearance.icon.label')}
description={t('appearance.icon.description')}
variant="default"
data={data?.files ?? []}
withAsterisk
required
{...form.getInputProps('appearance.iconUrl')}

View File

@@ -31,7 +31,7 @@ interface 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>({
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',

View File

@@ -50,7 +50,7 @@ export const AvailableElementTypes = ({
{
id: uuidv4(),
// Thank you ChatGPT ;)
position: previousConfig.wrappers.length + 1,
position: previousConfig.categories.length + 1,
},
],
categories: [

View File

@@ -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 { HomarrCardWrapper } from '../../Tiles/HomarrCardWrapper';
import { useCardStyles } from '../../../layout/useCardStyles';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { useGridstack } from '../gridstack/use-gridstack';
import { WrapperContent } from '../WrapperContent';
@@ -13,13 +15,38 @@ interface DashboardCategoryProps {
export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
const { refs, apps, widgets } = useGridstack('category', category.id);
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 (
<HomarrCardWrapper pt={10} mx={10} isCategory>
<Group position="apart" align="center">
<Accordion
classNames={{
item: cardClasses.card,
}}
mx={10}
chevronPosition="left"
multiple
value={isEditMode ? categoryList : toggledCategories}
variant="separated"
radius="lg"
onChange={(state) => {
// Cancel if edit mode is on
if (isEditMode) return;
setToggledCategories([...state]);
}}
>
<Accordion.Item value={category.name}>
<Accordion.Control icon={isEditMode && <CategoryEditMenu category={category} />}>
<Title order={3}>{category.name}</Title>
{isEditMode ? <CategoryEditMenu category={category} /> : null}
</Group>
</Accordion.Control>
<Accordion.Panel>
<div
className="grid-stack grid-stack-category"
data-category={category.id}
@@ -27,6 +54,8 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
>
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
</div>
</HomarrCardWrapper>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
};

View File

@@ -22,7 +22,7 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
useCategoryActions(configName, category);
return (
<Menu withinPortal position="left-start" withArrow>
<Menu withinPortal withArrow>
<Menu.Target>
<ActionIcon>
<IconDots />

View File

@@ -1,8 +1,10 @@
import { v4 as uuidv4 } from 'uuid';
import { useConfigStore } from '../../../../config/store';
import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions';
import { AppType } from '../../../../types/app';
import { CategoryType } from '../../../../types/category';
import { WrapperType } from '../../../../types/wrapper';
import { IWidget } from '../../../../widgets/widgets';
import { CategoryEditModalInnerProps } from './CategoryEditModal';
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);
if (!currentItem) return previous;
// 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 appsToMove = previous.apps.filter(
(x) => x.area && x.area.type === 'category' && x.area.properties.id === currentItem.id
);
appsToMove.forEach((x) => {
// eslint-disable-next-line no-param-reassign
x.area = { type: 'wrapper', properties: { id: mainWrapper?.id ?? 'default' } };
});
const isAppAffectedFilter = (app: AppType): boolean => {
if (!app.area) {
return false;
}
const widgetsToMove = previous.widgets.filter(
(x) => x.area && x.area.type === 'category' && x.area.properties.id === currentItem.id
);
if (app.area.type !== 'category') {
return false;
}
widgetsToMove.forEach((x) => {
// eslint-disable-next-line no-param-reassign
x.area = { type: 'wrapper', properties: { id: mainWrapper?.id ?? 'default' } };
});
if (app.area.properties.id === mainWrapperId) {
return false;
}
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 {
...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),
wrappers: previous.wrappers.filter((x) => x.position !== currentItem.position),
};

View File

@@ -4,6 +4,7 @@ import { ActionIcon, Button, Group, Text, Title, Tooltip } from '@mantine/core';
import { IconEditCircle, IconEditCircleOff } from '@tabler/icons';
import { getCookie } from 'cookies-next';
import { Trans, useTranslation } from 'next-i18next';
import { useHotkeys } from '@mantine/hooks';
import { hideNotification, showNotification } from '@mantine/notifications';
import { useConfigContext } from '../../../../../config/provider';
import { useScreenSmallerThan } from '../../../../../hooks/useScreenSmallerThan';
@@ -23,6 +24,8 @@ export const ToggleEditModeAction = () => {
const { config } = useConfigContext();
const { classes } = useCardStyles(true);
useHotkeys([['ctrl+E', toggleEditMode]]);
const toggleButtonClicked = () => {
toggleEditMode();
if (enabled || config === undefined || config?.schemaVersion === undefined) {

View File

@@ -1,10 +1,10 @@
import { Box, createStyles, Group, Header as MantineHeader, Indicator } from '@mantine/core';
import { useEffect, useState } from 'react';
import { CURRENT_VERSION, REPO_URL } from '../../../../data/constants';
import { useConfigContext } from '../../../config/provider';
import { REPO_URL } from '../../../../data/constants';
import DockerMenuButton from '../../../modules/Docker/DockerModule';
import { usePackageAttributesStore } from '../../../tools/client/zustands/usePackageAttributesStore';
import { Logo } from '../Logo';
import { useCardStyles } from '../useCardStyles';
import DockerMenuButton from '../../../modules/Docker/DockerModule';
import { ToggleEditModeAction } from './Actions/ToggleEditMode/ToggleEditMode';
import { Search } from './Search';
import { SettingsMenu } from './SettingsMenu';
@@ -14,23 +14,22 @@ export const HeaderHeight = 64;
export function Header(props: any) {
const { classes } = useStyles();
const { classes: cardClasses } = useCardStyles(false);
const { config } = useConfigContext();
const { attributes } = usePackageAttributesStore();
const [newVersionAvailable, setNewVersionAvailable] = useState<string>('');
useEffect(() => {
// Fetch Data here when component first mounted
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
res.json().then((data) => {
if (data.tag_name > CURRENT_VERSION) {
if (data.tag_name > `v${attributes.packageVersion}`) {
setNewVersionAvailable(data.tag_name);
}
});
});
}, [CURRENT_VERSION]);
}, []);
return (
<MantineHeader height={HeaderHeight} className={cardClasses.card}>
<MantineHeader height="auto" className={cardClasses.card}>
<Group p="xs" noWrap grow>
<Box className={classes.hide}>
<Logo />

View File

@@ -13,12 +13,14 @@ import {
import { useDebouncedValue, useHotkeys } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import { IconBrandYoutube, IconDownload, IconMovie, IconSearch } from '@tabler/icons';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { useTranslation } from 'next-i18next';
import React, { forwardRef, useEffect, useRef, useState } from 'react';
import { useConfigContext } from '../../../config/provider';
import { OverseerrMediaDisplay } from '../../../modules/common';
import { IModule } from '../../../modules/ModuleTypes';
import { ConfigType } from '../../../types/config';
import { searchUrls } from '../../Settings/Common/SearchEngine/SearchEngineSelector';
import Tip from '../Tip';
import { useCardStyles } from '../useCardStyles';
@@ -54,8 +56,8 @@ export function Search() {
const { t } = useTranslation('modules/search');
const { config } = useConfigContext();
const [searchQuery, setSearchQuery] = useState('');
const [debounced, cancel] = useDebouncedValue(searchQuery, 250);
const { classes: cardClasses } = useCardStyles(false);
const [debounced] = useDebouncedValue(searchQuery, 250);
const { classes: cardClasses } = useCardStyles(true);
const isOverseerrEnabled = config?.apps.some(
(x) => x.integration.type === 'overseerr' || x.integration.type === 'jellyseerr'
@@ -136,30 +138,40 @@ export function Search() {
const textInput = useRef<HTMLInputElement>(null);
useHotkeys([['mod+K', () => textInput.current?.focus()]]);
const { classes } = useStyles();
const openInNewTab = config?.settings.common.searchEngine.properties.openInNewTab
? '_blank'
: '_self';
const [OverseerrResults, setOverseerrResults] = useState<any[]>([]);
const openTarget = getOpenTarget(config);
const [opened, setOpened] = useState(false);
useEffect(() => {
if (debounced !== '' && selectedSearchEngine.value === 'overseerr' && searchQuery.length > 3) {
axios.get(`/api/modules/overseerr?query=${searchQuery}`).then((res) => {
setOverseerrResults(res.data.results ?? []);
});
const {
data: OverseerrResults,
isLoading,
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 ?? [];
}
}, [debounced]);
return [];
},
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchInterval: false,
}
);
const isModuleEnabled = config?.settings.customization.layout.enabledSearchbar;
if (!isModuleEnabled) {
return null;
}
//TODO: Fix the bug where clicking anything inside the Modal to ask for a movie
// will close it (Because it closes the underlying Popover)
return (
<Box style={{ width: '100%', maxWidth: 400 }}>
<Popover
opened={OverseerrResults.length > 0 && opened && searchQuery.length > 3}
opened={OverseerrResults && OverseerrResults.length > 0 && opened && searchQuery.length > 3}
position="bottom"
withinPortal
shadow="md"
@@ -182,7 +194,7 @@ export function Search() {
setOpened(false);
if (item.url) {
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
@@ -193,9 +205,9 @@ export function Search() {
autocompleteData.length === 0
) {
if (selectedSearchEngine.url.includes('%s')) {
window.open(selectedSearchEngine.url.replace('%s', searchQuery), openInNewTab);
window.open(selectedSearchEngine.url.replace('%s', searchQuery), openTarget);
} else {
window.open(selectedSearchEngine.url + searchQuery, openInNewTab);
window.open(selectedSearchEngine.url + searchQuery, openTarget);
}
}
}}
@@ -207,16 +219,17 @@ export function Search() {
/>
</Popover.Target>
<Popover.Dropdown>
<div>
<ScrollArea style={{ height: 400, width: 420 }} offsetScrollbars>
{OverseerrResults.slice(0, 5).map((result, index) => (
<ScrollArea style={{ height: '80vh', maxWidth: '90vw' }} offsetScrollbars>
{OverseerrResults &&
OverseerrResults.slice(0, 4).map((result: any, index: number) => (
<React.Fragment key={index}>
<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>
))}
</ScrollArea>
</div>
</Popover.Dropdown>
</Popover>
</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';
};

View 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,
});

View File

@@ -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;
};

View File

@@ -1,4 +1,5 @@
import { ActionIcon, Drawer, Text, Tooltip } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import { IconBrandDocker, IconX } from '@tabler/icons';
import axios from 'axios';
@@ -17,6 +18,7 @@ export default function DockerMenuButton(props: any) {
const [selection, setSelection] = useState<Docker.ContainerInfo[]>([]);
const { config } = useConfigContext();
const { classes } = useCardStyles(true);
useHotkeys([['mod+B', () => setOpened(!opened)]]);
const dockerEnabled = config?.settings.customization.layout.enabledDocker || false;
@@ -60,6 +62,7 @@ export default function DockerMenuButton(props: any) {
<>
<Drawer
opened={opened}
trapFocus={false}
onClose={() => setOpened(false)}
padding="xl"
position="right"

View File

@@ -7,6 +7,7 @@ import {
ScrollArea,
TextInput,
useMantineTheme,
Text,
} from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons';
@@ -78,8 +79,16 @@ export default function DockerTable({
transitionDuration={0}
/>
</td>
<td>{element.Names[0].replace('/', '')}</td>
{width > MIN_WIDTH_MOBILE && <td>{element.Image}</td>}
<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 && (
<td>
<Group>
@@ -111,12 +120,13 @@ export default function DockerTable({
});
return (
<ScrollArea style={{ height: '80vh' }}>
<ScrollArea style={{ height: '90vh' }} offsetScrollbars>
<TextInput
placeholder={t('search.placeholder')}
mt="md"
mr="md"
icon={<IconSearch size={14} />}
value={search}
autoFocus
onChange={handleSearchChange}
/>
<Table ref={ref} captionSide="bottom" highlightOnHover verticalSpacing="sm">

View File

@@ -180,7 +180,7 @@ export function MediaDisplay({ media }: { media: IMedia }) {
const { t } = useTranslation('modules/common-media-cards');
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" />
<Stack justify="space-around">
<Stack spacing="sm">
@@ -223,7 +223,7 @@ export function MediaDisplay({ media }: { media: IMedia }) {
{media.overview}
</Text>
</Stack>
<Group noWrap>
<Group spacing="xs">
{media.plexUrl && (
<Button
component="a"

View File

@@ -8,8 +8,8 @@ import Layout from '../components/layout/Layout';
import { useInitConfig } from '../config/init';
import { getFallbackConfig } from '../tools/config/getFallbackConfig';
import { getFrontendConfig } from '../tools/config/getFrontendConfig';
import { getServerSideTranslations } from '../tools/getServerSideTranslations';
import { dashboardNamespaces } from '../tools/translation-namespaces';
import { getServerSideTranslations } from '../tools/server/getServerSideTranslations';
import { dashboardNamespaces } from '../tools/server/translation-namespaces';
import { ConfigType } from '../types/config';
import { DashboardServerSideProps } from '../types/dashboardPageType';

View File

@@ -8,7 +8,8 @@ import { GetServerSidePropsContext } from 'next';
import { appWithTranslation } from 'next-i18next';
import { AppProps } from 'next/app';
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 { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal';
import { EditAppModal } from '../components/Dashboard/Modals/EditAppModal/EditAppModal';
@@ -21,8 +22,16 @@ import '../styles/global.scss';
import { ColorTheme } from '../tools/color';
import { queryClient } from '../tools/queryClient';
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 [primaryColor, setPrimaryColor] = useState<MantineTheme['primaryColor']>('red');
const [secondaryColor, setSecondaryColor] = useState<MantineTheme['primaryColor']>('orange');
@@ -45,6 +54,12 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
getInitialValueInEffect: true,
});
const { setInitialPackageAttributes } = usePackageAttributesStore();
useEffect(() => {
setInitialPackageAttributes(props.packageAttributes);
}, []);
const toggleColorScheme = (value?: ColorScheme) =>
setColorScheme(value || (colorScheme === 'dark' ? 'light' : 'dark'));
@@ -102,6 +117,7 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
</MantineProvider>
</ColorTheme.Provider>
</ColorSchemeProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</>
);
@@ -109,6 +125,7 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({
colorScheme: getCookie('color-scheme', ctx) || 'light',
packageAttributes: getServiceSidePackageAttributes(),
});
export default appWithTranslation(App);

View 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',
});
};

View 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';
}
}

View File

@@ -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',
});
};

View File

@@ -39,7 +39,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
const url = new URL(app.url);
const options = {
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,
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
};

View File

@@ -38,7 +38,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
const url = new URL(app.url);
const options = {
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,
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
};

View File

@@ -30,7 +30,7 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
const url = new URL(app.url);
const options = {
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,
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
};

View File

@@ -39,7 +39,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
const url = new URL(app.url);
const options = {
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,
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
};

View File

@@ -31,7 +31,7 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
const url = new URL(app.url);
const options = {
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,
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
};

View File

@@ -1,14 +1,14 @@
import { getCookie, setCookie } from 'cookies-next';
import fs from 'fs';
import { GetServerSidePropsContext } from 'next';
import fs from 'fs';
import { LoadConfigComponent } from '../components/Config/LoadConfig';
import { Dashboard } from '../components/Dashboard/Dashboard';
import Layout from '../components/layout/Layout';
import { useInitConfig } from '../config/init';
import { getFrontendConfig } from '../tools/config/getFrontendConfig';
import { getServerSideTranslations } from '../tools/getServerSideTranslations';
import { dashboardNamespaces } from '../tools/translation-namespaces';
import { getServerSideTranslations } from '../tools/server/getServerSideTranslations';
import { dashboardNamespaces } from '../tools/server/translation-namespaces';
import { DashboardServerSideProps } from '../types/dashboardPageType';
export async function getServerSideProps({
@@ -47,11 +47,14 @@ export async function getServerSideProps({
}
const translations = await getServerSideTranslations(req, res, dashboardNamespaces, locale);
const config = getFrontendConfig(configName as string);
return {
props: { configName: configName as string, config, ...translations },
props: {
configName: configName as string,
config,
...translations,
},
};
}

View File

@@ -8,7 +8,7 @@ import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useForm } from '@mantine/form';
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.
export default function AuthenticationTitle() {

View File

@@ -1,14 +1,6 @@
import Dockerode from 'dockerode';
import { MatchingImages, ServiceType, tryMatchPort } from './types';
async function MatchIcon(name: string) {
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';
}
import { MatchingImages, ServiceType, tryMatchPort } from './types';
function tryMatchType(imageName: string): ServiceType {
// Try to find imageName inside MatchingImages

View 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 }));
},
}));

View File

@@ -9,7 +9,7 @@ import { ICalendarWidget } from '../../widgets/calendar/CalendarTile';
import { IDashDotTile } from '../../widgets/dashDot/DashDotTile';
import { IDateWidget } from '../../widgets/date/DateTile';
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 { IWeatherWidget } from '../../widgets/weather/WeatherTile';
import { IWidget } from '../../widgets/widgets';

View File

@@ -131,6 +131,12 @@ export const languages: Language[] = [
translatedName: 'Chinese',
emoji: '🇨🇳',
},
{
shortName: 'el',
originalName: 'Ελληνικά',
translatedName: 'Greek',
emoji: '🇬🇷',
},
];
export const getLanguageByCode = (code: string | null) =>

View 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';
};

View File

@@ -1,9 +1,9 @@
export const dashboardNamespaces = [
'common',
'layout/tools',
'layout/element-selector/selector',
'layout/modals/add-app',
'layout/modals/change-position',
'layout/modals/icon-picker',
'layout/modals/about',
'layout/header/actions/toggle-edit-mode',
'layout/mobile/drawer',

Some files were not shown because too many files have changed in this diff Show More