This commit is contained in:
Johannes7k75 2023-04-26 22:59:19 +02:00
commit 00ed4858fe
114 changed files with 2300 additions and 3519 deletions

View File

@ -2,6 +2,47 @@
This changelog starts with the restructured 1.0.0 release that happened after context isolation changes. The changelogs here should more-or-less mirror the ones that get shown in the client but probably with less formatting and pizzazz.
## 1.9.2
### Added
### Removed
### Changed
### Fixed
- Fixed context menu crash & api
## 1.9.1
### Added
- SourceURL for the renderer
### Removed
### Changed
### Fixed
- Fixed client crashes
## 1.9.0
### Added
- Remove minimum window size now remembers desired size
- Basic semver comparison
### Removed
- Public Servers
- Old DataStore functions that are no longer used
### Changed
- All main react components are now functional with hooks
- Mac now uses cmd instead of ctrl
### Fixed
- Fixed dropdowns
- Fixed markdown parser
## 1.8.5
### Added

View File

@ -2,7 +2,8 @@
"Panels": {
"plugins": "Pluginy",
"themes": "Témata",
"customcss": "Vlastní CSS"
"customcss": "Vlastní CSS",
"updates": "Aktualizace"
},
"Collections": {
"settings": {
@ -13,10 +14,6 @@
"name": "Systém emotů",
"note": "Povolí systém emotů BD"
},
"publicServers": {
"name": "Veřejné servery",
"note": "Zobrazí tlačítko veřejných serverů"
},
"voiceDisconnect": {
"name": "Odpojení z hlas. kanálu",
"note": "Odpojit z hlasového kanálu při zavření Discordu"
@ -109,6 +106,42 @@
"debugLogs": {
"name": "Ladicí protokoly",
"note": "Vypíše vše z konzole do souboru debug.log ve složce BetterDiscordu"
},
"devTools": {
"name": "DevTools",
"note": "Povolí přepnutí DevTools při stisku Ctrl+Shift+I"
}
},
"editor": {
"name": "Předvolby editoru",
"lineNumbers": {
"name": "Čísla řádků",
"note": "Povolí zobrazování čísel řádků vedle editoru"
},
"fontSize": {
"name": "Velikost písma",
"note": "Velikost písma (pt) k použití v editoru"
},
"minimap": {
"name": "Minimapa",
"note": "Povolí zobrazování kódu v minimapě vedle editoru"
},
"hover": {
"name": "Referenční tooltipy",
"note": "Povolí zobrazování referenčních tooltipů při přechodu přes pravidla a selektory"
},
"quickSuggestions": {
"name": "Rychlé návrhy",
"note": "Povolí zobrazování automaticky dokončovaných návrhů, zatímco píšete"
},
"renderWhitespace": {
"name": "Zobrazit neviditelnou mezeru",
"note": "Zda v editoru zobrazit neviditelnou mezeru",
"options": {
"all": "Vždy",
"none": "Nikdy",
"selection": "Výběr"
}
}
}
},
@ -200,8 +233,10 @@
"metaNotFound": "META nebyl nalezen.",
"compileError": "Nepodařilo se zkompilovat.",
"wasUnloaded": "Doplněk {{name}} byl odnačten.",
"blankSlateHeader": "Nemáš žádné/žádný {{type}}!",
"blankSlateMessage": "Najdi si nějaké na [tomto webu]({{link}}) a přidej je do své {{type}} složky."
"blankSlateHeader": "Nemáš žádné doplňky typu {{type}}!",
"blankSlateMessage": "Najdi si nějaké na [tomto webu]({{link}}) a přidej je do své {{type}} složky.",
"isEnabled": "Povoleno",
"wasLoaded": "Doplněk {{name}} v{{version}} byl načten."
},
"CustomCSS": {
"confirmationText": "Máš neuložené změny ve vlastním CSS. Zavřením tohoto okna je všechny ztratíš.",
@ -220,27 +255,6 @@
"downloadFailed": "Stahování selhalo",
"failureMessage": "BetterDiscordu se nepodařilo stáhnout emoty, zkontroluj prosím své internetové připojení a firewall."
},
"PublicServers": {
"button": "veřejné",
"join": "Připojit se",
"joining": "Připojuji se",
"joined": "Připojen",
"loading": "Načítání",
"loadMore": "Načíst více",
"notConnected": "Nepřipojen",
"connectionRequired": "Aby ses mohl/a připojovat na servery, musíš připojit svůj účet.",
"connectionError": "Chyba spojení",
"connectionErrorMessage": "Při připojování k DiscordServers.com se vyskytla chyba, je možné, že jejich web/API je offline. Zkus to prosím znovu později.",
"pagination": "Strana {{page}} z {{count}}",
"search": "Hledat",
"connect": "Připojit",
"reconnect": "Znovu připojit",
"categories": "Kategorie",
"keywords": "Klíčová slova",
"connection": "Připojen/a jako: {{username}}#{{discriminator}}",
"results": "Zobrazování {{start}}-{{end}} z {{total}} výsledků v kategorii {{category}}",
"query": "pro dotaz {{query}}"
},
"Modals": {
"confirmAction": "Opravdu?",
"okay": "Dobře",
@ -271,5 +285,24 @@
"WindowPrefs": {
"enabledInfo": "Tato možnost vyžaduje průhledné téma, aby fungovala správně. Na Windows to může rozbít Areo přichytávání a maximalizování.\n\nAby se tato změna projevila, bude potřeba restartovat Discord. Chceš jej restartovat nyní?",
"disabledInfo": "Aby se tato změna projevila, bude potřeba restartovat Discord. Chceš jej restartovat nyní?"
},
"Notices": {
"moreInfo": "Více informací"
},
"Updater": {
"updateFailed": "Aktualizace selhala!",
"updateFailedMessage": "Aktualizace BettterDiscordu se nezdařila. Stáhněte si prosím nejnovější verzi instalátoru z našeho webu (https://betterdiscord.app) a přeinstalujte jej.",
"updateSuccessful": "Aktualizace byla úspěšná!",
"updateAvailable": "BetterDiscord má novou aktualizaci (v{{version}})",
"addonUpdatesAvailable": "BetterDiscord nalezl aktualizace {{count}} vašich doplňků typu {{type}}!",
"addonUpdated": "Doplněk {{name}} byl aktualizován na verzi {{version}}!",
"checking": "Kontrola aktualizací!",
"finishedChecking": "Kontrola aktualizací dokončena!",
"checkForUpdates": "Zkontrolovat aktualizace!",
"updateAll": "Aktualizovat vše!",
"noUpdatesAvailable": "Nejsou dostupné žádné aktualizace.",
"versionAvailable": "Je dostupná verze {{version}}!",
"upToDateBlankslate": "Všechny vaše doplňky typu {{type}} jsou aktuální!",
"updateButton": "Aktualizovat!"
}
}

View File

@ -13,10 +13,6 @@
"name": "Emotesystem",
"note": "Aktiviert das BD-Emotesystem"
},
"publicServers": {
"name": "Öffentliche Server",
"note": "Zeigt den Knopf für öffentliche Server"
},
"voiceDisconnect": {
"name": "Verbindung zum Sprachchat trennen",
"note": "Trennt beim Schließen von Discord die Verbindung zum Sprachchat"
@ -220,27 +216,6 @@
"downloadFailed": "Download fehlgeschlagen",
"failureMessage": "BetterDiscord konnte die Emotes nicht herunterladen, bitte überprüfe deine Internetverbindung und Firewall."
},
"PublicServers": {
"button": "public",
"join": "Beitreten",
"joining": "Trete bei",
"joined": "Beigetreten",
"loading": "Laden",
"loadMore": "Mehr laden",
"notConnected": "Nicht mit DiscordServers.com verbunden!",
"connectionRequired": "Du musst dein Konto verbinden, um Server beitreten zu können.",
"connectionError": "Verbindungsfehler",
"connectionErrorMessage": "Bei der Verbindung zu DiscordServers.com ist ein Fehler aufgetreten. Es ist möglich, dass die Website/Api offline ist. Versuch es später noch einmal.",
"pagination": "Seite {{page}} von {{count}}",
"search": "Suchen",
"connect": "Verbinden",
"reconnect": "Wiederverbinden",
"categories": "Kategorien",
"keywords": "Schlüsselwörter",
"connection": "Verbunden als: {{username}}#{{discriminator}}",
"results": "{{start}}-{{end}} von {{total}} Ergebnissen in {{category}} angezeigt",
"query": "für {{query}}"
},
"Modals": {
"confirmAction": "Bist du dir sicher?",
"okay": "Okay",

View File

@ -1 +1,308 @@
{}
{
"Panels": {
"plugins": "Πρόσθετα",
"themes": "Θέματα",
"customcss": "Προσαρμοσμένο CSS",
"updates": "Ενημερώσεις"
},
"Collections": {
"settings": {
"name": "Ρυθμίσεις",
"general": {
"name": "Γενικά",
"emotes": {
"name": "Σύστημα Συναισθημάτων",
"note": "Ενεργοποίηση συστήματος συναισθημάτων BD"
},
"voiceDisconnect": {
"name": "Αποσύνδεση Φωνής",
"note": "Αποσύνδεση από τον διακομιστή φωνής με το κλείσιμο του Discord"
},
"showToasts": {
"name": "Εμφάνιση Ειδοποιήσεων",
"note": "Εμφάνιση μικρής ειδοποίησης για σημαντικές πληροφορίες"
},
"mediaKeys": {
"name": "Απενεργοποίηση Πλήκτρων Πολυμέσων",
"note": "Αποτροπή παρέμβασης του Discord στα πλήκτρα πολυμέσων μετά την αναπαραγωγή βίντεο."
}
},
"window": {
"removeMinimumSize": {
"name": "Απομάκρυνση Ελάχιστου Μεγέθους",
"note": "Απομακρύνει τον περιορισμό του Discord για ελάχιστες διαστάσεις παραθύρου στα 940 x 500"
},
"name": "Ιδιότητες Παραθύρου",
"transparency": {
"name": "Ενεργοποίηση Διαφάνειας",
"note": "Ενεργοποιεί τη διαφάνεια του βασικού παραθύρου (απαιτείται επανεκκίνηση)"
},
"frame": {
"name": "Πλαίσιο Παραθύρου",
"note": "Προσθέτει το πλαίσιο του συστήματος στο βασικό παράθυρο"
}
},
"addons": {
"name": "Διαχειριστής Προσθέτων",
"addonErrors": {
"name": "Εμφάνιση Σφαλμάτων Προσθέτων",
"note": "Εμφάνιση σφαλμάτων προσθέτων/θεμάτων σε παράθυρο"
},
"editAction": {
"name": "Ενέργεια Επεξεργασίας",
"note": "Η θέση εμφάνισης προσθέτων και θεμάτων κατά την επεξεργασία",
"options": {
"detached": "Αποσπώμενο Παράθυρο",
"system": "Επεξεργαστής Συστήματος"
}
}
},
"customcss": {
"name": "Προσαρμοσμένο CSS",
"customcss": {
"name": "Προσαρμοσμένο CSS",
"note": "Ενεργοποιεί την καρτέλα Προσαρμοσμένου CSS"
},
"liveUpdate": {
"name": "Ενημέρωση σε πραγματικό χρόνο",
"note": "Ενημερώνεται το CSS ενώ πληκτρολογείτε"
},
"startDetached": {
"name": "Εκκίνηση ως Αποσπώμενο",
"note": "Πατώντας την καρτέλα Προσαρμοσμένου CSS ανοίγει τον επεξεργαστή σε χωριστό παράθυρο"
},
"nativeOpen": {
"name": "Άνοιγμα στον Επεξεργαστή του Συστήματος",
"note": "Πατώντας την καρτέλα του Προσαρμοσμένου CSS ανοίγει το προσαρμοσμένο CSS στον επεξεργαστή συστήματος"
},
"openAction": {
"name": "Θέση επεξεργαστή",
"note": "Που πρέπει να ανοίγει το Προσαρμοσμένο CSS από προεπιλογή",
"options": {
"settings": "Μενού Ρυθμίσεων",
"detached": "Αποσπώμενο παράθυρο",
"system": "Επεξεργαστής Συστήματος"
}
}
},
"developer": {
"name": "Ρυθμίσεις Δημιουργού",
"debuggerHotkey": {
"name": "Συντόμευση πληκτρολογίου για Αποσφαλμάτωση",
"note": "Επιτρέπει την ενεργοποίηση της Αποσφαλμάτωσης, πατώντας το πλήκτρο F8 με ανοιχτά τα Εργαλεία Δημιουργού"
},
"reactDevTools": {
"name": "Εργαλεία Δημιουργού React",
"note": "Ανοίγει την τοπική εγκατάσταση των Εργαλείων Δημιουργού React στο Discord"
},
"inspectElement": {
"name": "Συντόμευση πληκτρολογίου Παρακολούθησης Στοιχείου",
"note": "Ενεργοποιεί τη συντόμευση πληκτρολογίου παρακολούθησης στοιχείου (Ctrl+Shift+C) που είναι κοινή στους περισσότερους φυλλομετρητές"
},
"devToolsWarning": {
"name": "Σταμάτημα Προειδοποίησης Εργαλείων Δημιουργού",
"note": "Αποτρέπει το Discord να εμφανίζει το μήνυμα «Αναμείνατε!»"
},
"debugLogs": {
"name": "Καταγραφές Αποσφαλμάτωσης",
"note": "Εξάγει τα πάντα από την κονσόλα στο αρχείο debug.log στο φάκελο του BetterDiscord"
},
"devTools": {
"name": "Εργαλεία Δημιουργού",
"note": "Ενεργοποιεί την εναλλαγή των Εργαλείων Δημιουργού με το Ctrl+Shift+I"
}
},
"editor": {
"name": "Ιδιότητες Επεξεργαστή",
"lineNumbers": {
"name": "Αριθμοί γραμμών",
"note": "Ενεργοποιείται η εμφάνιση των αριθμών γραμμών στη πλευρά του επεξεργαστή"
},
"fontSize": {
"name": "Μέγεθος Γραμματοσειράς",
"note": "Μεγεθος γραμματοσειράς (pt) για χρήση στον επεξεργαστή"
},
"minimap": {
"name": "Εποπτικός χάρτης",
"note": "Ενεργοποιεί την εμφάνιση του εποπτικού χάρτη του κώδικα στην πλευρά του επεξεργαστή"
},
"hover": {
"name": "Επεξηγήσεις Αναφοράς",
"note": "Ενεργοποιεί την εμφάνιση των επεξηγήσεων αναφοράς όταν πάτε με το ποντίκι κανόνων και επιλογέων"
},
"quickSuggestions": {
"name": "Γρήγορες Προτάσεις",
"note": "Ενεργοποιεί την εμφάνιση των αυτοσυμπληρούμενων προτάσεων κατά την δακτυλογράφηση"
},
"renderWhitespace": {
"name": "Εμφάνιση Διαστήματος",
"note": "Όταν τα διαστήματα πρέπει να φαίνονται από τον επεξεργαστή",
"options": {
"all": "Πάντα",
"none": "Ποτέ",
"selection": "Επιλογή"
}
}
}
},
"emotes": {
"name": "Συναισθήματα",
"general": {
"name": "Γενικά",
"download": {
"name": "Λήψη Συναισθημάτων",
"note": "Λήψη συναισθημάτων όταν είναι παλαιά"
},
"emoteMenu": {
"name": "Μενού Συναισθημάτων",
"note": "Εμφάνιση συναισθημάτων Αγαπημένων / Twitch στο μενού συναισθημάτων"
},
"hideEmojiMenu": {
"name": "Απόκρυψη Μενού Εικονιδίων Emoji",
"note": "Αποκρύπτει το μενού των εικονιδίων του Discord όταν γίνεται χρήση του μενού συναισθημάτων"
},
"autoCaps": {
"name": "Αυτοκεφαλαιοποίηση Συναισθημάτων",
"note": "Αυτόματη κεφαλαιοποίηση εντολών συναισθημάτων"
},
"modifiers": {
"name": "Εμφάνιση Τροποποιητών Συναισθημάτων",
"note": "Ενεργοποιεί τους τροποποιητές συναισθημάτων (flip, spin, pulse, spin2, spin3, 1spin, 2spin, 3spin, tr, bl, br, shake, shake2, shake3, flap)"
},
"animateOnHover": {
"name": "Κίνηση με την Κϊνηση του Ποντικιού",
"note": "Οι τροποποιητές συναισθημάτων κινούνται μόνο με την κίνηση του ποντικιού"
}
},
"categories": {
"name": "Κατηγορίες",
"twitchglobal": {
"name": "Καθολικά Twitch",
"note": "Εμφάνιση καθολικών συναισθημάτων Twitch"
},
"twitchsubscriber": {
"name": "Συνδρομητές Twitch",
"note": "Εμφάνιση συναισθημάτων συνδρομητή Twitch"
},
"frankerfacez": {
"name": "FrankerFaceZ",
"note": "Εμφάνιση συναισθημάτων από FFZ"
},
"bttv": {
"name": "BetterTTV",
"note": "Εμφάνιση συναισθημάτων από BTTV"
}
}
}
},
"Addons": {
"title": "{{name}} v{{version}} από {{author}}",
"byline": "από {{author}}",
"openFolder": "Άνοιγμα Φακέλου τύπου {{type}}",
"reload": "Επαναφόρτωση",
"addonSettings": "Ρυθμίσεις",
"website": "Ιστοσελίδα",
"source": "Προέλευση",
"invite": "Διακομιστής Υποστήριξης",
"donate": "Δωρεά",
"patreon": "Patreon",
"name": "Ονομασία",
"author": "Δημιουργός",
"version": "Έκδοση",
"added": "Ημερομηνία που Προστέθηκε",
"modified": "Ημερομηνία που Τροποποιήθηκε",
"search": "Αναζήτηση για {{type}}",
"editAddon": "Επεξεργασία",
"deleteAddon": "Διαγραφή",
"confirmDelete": "Θέλετε να διαγράψετε το πρόσθετο {{name}};",
"confirmationText": "Έχετε μη αποθηκευμένες αλλαγές στο πρόσθετο {{name}}. Με το κλείσιμο του παραθύρου θα χαθούν όλες αυτές οι αλλαγές.",
"enabled": "Το πρόσθετο {{name}} έχει ενεργοποιηθεί.",
"disabled": "Το πρόσθετο {{name}} έχει απενεργοποιηθεί.",
"couldNotEnable": "Το πρόσθετο {{name}} δεν μπορεί να ενεργοποιηθεί.",
"couldNotDisable": "Το πρόσθετο {{name}} δεν μπορεί να απενεργοποιηθεί.",
"couldNotStart": "Το πρόσθετο {{name}} δεν μπορεί να εκκινηθεί.",
"couldNotStop": "Το πρόσθετο {{name}} δεν μπορεί να τερματιστεί.",
"settingsError": "Αδύνατο το άνοιγμα των ρυθμίσεων για το πρόσθετο {{name}}",
"methodError": "Η μέθοδος {{method}} δεν μπορεί να ενεργοποιηθεί.",
"unknownAuthor": "Άγνωστος Δημιουργός",
"noDescription": "Η περιγραφή δεν παρέχεται.",
"alreadyExists": "Υπάρχει ήδη πρότυπο τύπου {{type}} με το όνομα {{name}}",
"alreadWatching": "Ήδη παρακολουθούμενη πρόσθετα.",
"metaError": "Τα μεταδεδομένα δεν μπορούν να αναγνωστούν.",
"missingNameData": "Τα μεταδεδομένα απώλεσαν δεδομένα ονόματος.",
"metaNotFound": "Τα μεταδεδομένα δεν βρέθηκαν.",
"compileError": "Αδύνατη η μετατροπή. Δείτε την κονσόλα για λεπτομέρειες.",
"wasUnloaded": "Το πρόσθετο {{name}} αποφορτώθηκε.",
"blankSlateHeader": "Δεν έχετε κάποιο πρόσθετο τύπου {{type}}!",
"blankSlateMessage": "Λάβετε μερικά από [αυτή την ιστοσελίδα]({{link}}) και προσθέστε τα στον φάκελο τύπου {{type}}.",
"isEnabled": "Ενεργοποιημένα",
"wasLoaded": "Το πρόσθετο {{name}} v{{version}} φορτώθηκε."
},
"CustomCSS": {
"confirmationText": "Έχετε μη αποθηκευμένες αλλαγές στο Προσαρμοσμένο CSS. Με το κλείσιμο αυτού του παραθύρου, θα χάσετε όλες αυτές τις αλλαγές.",
"update": "Ενημέρωση",
"save": "Αποθήκευση",
"openNative": "Άνοιγμα στον Επεξεργαστή Συστήματος",
"openDetached": "Αποσπώμενο Παράθυρο",
"settings": "Ρυθμίσεις Επεξεργαστή",
"editorTitle": "Επεξεργαστής Προσαρμοσμένου CSS"
},
"Emotes": {
"loading": "Η φόρτωση συναισθημάτων στο υπόβαθρο δεν επαναφορτώνουν.",
"loaded": "Όλα τα συναισθήματα φόρτωσαν επιτυχώς.",
"clearEmotes": "Εκκαθάριση Δεδομένων Συναισθημάτων",
"favoriteAction": "Αγαπημένο!",
"downloadFailed": "Η λήψη απέτυχε",
"failureMessage": "Το BetterDiscord απέτυχε να λάβει συναισθήματα, ελέγξτε τη σύνδεσή σας και το τείχος προστασίας."
},
"Modals": {
"confirmAction": "Σίγουρα;",
"okay": "Εντάξει",
"done": "Ολοκληρώθηκε",
"cancel": "Άκυρο",
"nevermind": "Δεν πειράζει",
"close": "Κλείσιμο",
"name": "Ονομασία",
"message": "Μήνυμα",
"error": "Σφάλμα",
"addonErrors": "Σφάλματα Πρόσθετου",
"restartRequired": "Απαιτείται Επανεκκίνηση",
"restartNow": "Επανεκκίνηση Τώρα",
"restartLater": "Επανεκκίνηση Αργότερα",
"additionalInfo": "Πρόσθετες Πληροφορίες",
"restartPrompt": "Για να εφαρμοστεί, το Discord πρέπει να επανεκκινηθεί. Θέλετε επανεκκίνηση τώρα;"
},
"ReactDevTools": {
"notFound": "Η Επέκταση Δεν Βρέθηκε",
"notFoundDetails": "Αδύνατη η εύρεση της επέκτασης Εργαλείων Δημιουργού React στον υπολογιστή σας. Εγκαταστήστε την επέκταση στην τοπική εγκατάσταση του Chrome."
},
"Sorting": {
"sortBy": "Ταξινόμηση κατά",
"order": "Ταξινόμηση",
"ascending": "Αύξουσα",
"descending": "Φθίνουσα"
},
"WindowPrefs": {
"enabledInfo": "Αυτή η επιλογή απαιτεί διαφανές θέμα ώστε να λειτουργεί σωστά. Στα Windows ίσως επηρεάσει την προσαρμογή aero ή τη μεγιστοποίηση.\n\nΓια να ενεργοποιηθεί, το Discord πρέπει να επανεκκινηθεί. Επανεκκίνηση τώρα;",
"disabledInfo": "Για να ενεργοποιηθεί, το Discord πρέπει να επανεκκινηθεί. Επανεκκίνηση τώρα;"
},
"Notices": {
"moreInfo": "Περισσότερες Πληροφορίες"
},
"Updater": {
"updateFailed": "Η ενημέρωση Απέτυχε!",
"updateFailedMessage": "Το BetterDiscord απέτυχε να ενημερωθεί. Λάβετε την τελευταία έκδοση του προγράμματος εγκατάστασης από την ιστοσελίδα μας (https://betterdiscord.app/) και κάντε επανεγκατάσταση.",
"updateSuccessful": "Η Ενημέρωση ήταν Επιτυχής!",
"updateAvailable": "Το BetterDiscord έχει μια νέα ενημερωση (v{{version}})",
"addonUpdatesAvailable": "Το BetterDiscord έχει ενημερώσεις για {{count}} από τα {{type}}!",
"addonUpdated": "Το {{name}} έχει ενημερωθεί στην έκδοση {{version}}!",
"checking": "Έλεγχος για ενημερώσεις!",
"finishedChecking": "Ολοκλήρωση ελέγχου για ενημερώσεις!",
"checkForUpdates": "Έλεγχος Για Ενημερώσεις!",
"updateAll": "Ενημέρωση Όλων!",
"noUpdatesAvailable": "Καμιά διαθέσιμη ενημέρωση.",
"versionAvailable": "Η έκδοση {{version}} είναι διαθέσιμη!",
"upToDateBlankslate": "Όλα σας τα {{type}} φαίνεται ότι είναι ενημερωμένα!",
"updateButton": "Ενημέρωση!"
}
}

View File

@ -14,10 +14,6 @@
"name": "Emote System",
"note": "Enables BD's emote system"
},
"publicServers": {
"name": "Public Servers",
"note": "Display public servers button"
},
"voiceDisconnect": {
"name": "Voice Disconnect",
"note": "Disconnect from voice server when closing Discord"
@ -259,27 +255,6 @@
"downloadFailed": "Download Failed",
"failureMessage": "BetterDiscord failed to download emotes, please check your internet connection and firewall."
},
"PublicServers": {
"button": "Public Servers",
"join": "Join",
"joining": "Joining",
"joined": "Joined",
"loading": "Loading",
"loadMore": "Load More",
"notConnected": "Not Connected",
"connectionRequired": "You must connect your account in order to join servers.",
"connectionError": "Connection Error",
"connectionErrorMessage": "There was an error connecting to DiscordServers.com, it's possible their website/api is down. Please try again later.",
"pagination": "Page {{page}} of {{count}}",
"search": "Search",
"connect": "Connect",
"reconnect": "Reconnect",
"categories": "Categories",
"keywords": "Keywords",
"connection": "Connected as: {{username}}#{{discriminator}}",
"results": "Showing {{start}}-{{end}} of {{total}} results in {{category}}",
"query": "for {{query}}"
},
"Modals": {
"confirmAction": "Are You Sure?",
"okay": "Okay",

View File

@ -13,10 +13,6 @@
"name": "Sistema de Emoticonos",
"note": "Activa el sistema de emoticonos de BD"
},
"publicServers": {
"name": "Servidores Públicos",
"note": "Muestra el botón de servidores públicos"
},
"voiceDisconnect": {
"name": "Desconexión de Voz",
"note": "Desconectarse del servidor de voz al cerrar Discord"
@ -220,27 +216,6 @@
"downloadFailed": "Descarga Fallida",
"failureMessage": "BetterDiscord no ha podido descargar los emoticonos, por favor, comprueba tu conexión a internet y tu cortafuegos."
},
"PublicServers": {
"button": "Públicos",
"join": "Unirse",
"joining": "Uniéndose",
"joined": "Unido",
"loading": "Cargando",
"loadMore": "Cargar Más",
"notConnected": "No está Conectado",
"connectionRequired": "Debes conectar tu cuenta para poder unirte a los servidores.",
"connectionError": "Error de Conexión",
"connectionErrorMessage": "Se ha producido un error al conectar con DiscordServers.com, es posible que su sitio web/api esté caído. Vuelve a intentarlo más tarde.",
"pagination": "Página {{page}} de {{count}}",
"search": "Buscar",
"connect": "Conectar",
"reconnect": "Reconectar",
"categories": "Categorías",
"keywords": "Palabras clave",
"connection": "Conectado como: {{username}}#{{discriminator}}",
"results": "Mostrando {{start}}-{{end}} de {{total}} resultados en {{category}}",
"query": "por {{query}}"
},
"Modals": {
"confirmAction": "¿Estás seguro?",
"okay": "Vale",

View File

@ -2,7 +2,8 @@
"Panels": {
"plugins": "Lisäosat",
"themes": "Teemat",
"customcss": "Mukautettu CSS"
"customcss": "Mukautettu CSS",
"updates": "Päivitykset"
},
"Collections": {
"settings": {
@ -13,10 +14,6 @@
"name": "Hymiö-järjestelmä",
"note": "Ottaa käyttöön BD:n hymiö-järjestelmän."
},
"publicServers": {
"name": "Julkiset Palvelimet",
"note": "Näytä julkisten palvelimien nappi"
},
"voiceDisconnect": {
"name": "Katkaise puhelu",
"note": "Katkaise puhelu, kun suljet Discordin"
@ -236,7 +233,8 @@
"wasUnloaded": "{{name}} purettiin.",
"blankSlateHeader": "Sinulla ei ole yhtään {{type}}!",
"blankSlateMessage": "Nappaa joitain [tältä verkkosivustolta]({{link}}) ja lisää ne kansioon {{type}}.",
"isEnabled": "Otettu käyttöön"
"isEnabled": "Otettu käyttöön",
"wasLoaded": "\n{{name}} v{{version}} ladattiin."
},
"CustomCSS": {
"confirmationText": "Muokatussa CSS:ssä on tallentamattomia muutoksia. Tämän ikkunan sulkeminen menettää kaikki muutokset.",
@ -255,27 +253,6 @@
"downloadFailed": "Lataus epäonnistui",
"failureMessage": "BetterDiscord ei pystynyt lataamaan hymiöitä. Tarkista Internet-yhteytesi ja palomuurisi."
},
"PublicServers": {
"button": "julkinen",
"join": "Liity",
"joining": "Liitytään",
"joined": "Liitytty",
"loading": "Ladataan",
"loadMore": "Ladataan lisää",
"notConnected": "Et ole yhdistetty",
"connectionRequired": "Sinun on yhdistettävä tilisi, jotta voit liittyä palvelimiin.",
"connectionError": "Yhteysvika",
"connectionErrorMessage": "Yhteyden muodostamisessa DiscordServers.comiin tapahtui virhe. On mahdollista, että heidän verkkosivustonsa/api on poissa käytöstä. Yritä uudelleen myöhemmin.",
"pagination": "Sivu {{sivu}} / {{count}}",
"search": "Etsi",
"connect": "Yhdistä",
"reconnect": "Yhdistä uudelleen",
"categories": "Kategoriat",
"keywords": "Avainsanat",
"connection": "Yhdistetty nimellä: {{username}}#{{discrinator}}",
"results": "Näytetään {{start}}-{{end}} / {{total}} tulosta luokassa {{category}}",
"query": "haulle {{kysely}}"
},
"Modals": {
"confirmAction": "Oletko varma?",
"okay": "Okei",
@ -306,5 +283,24 @@
"WindowPrefs": {
"enabledInfo": "Tämä vaihtoehto vaatii läpinäkyvän teeman toimiakseen kunnolla. Windowsissa tämä voi katkaista aerosnapsauksen ja maksimoimisen.\n\nJotta Discord tulisi voimaan, se on käynnistettävä uudelleen. Haluatko käynnistää uudelleen nyt?",
"disabledInfo": "Jotta se tulisi voimaan, Discord on käynnistettävä uudelleen. Haluatko käynnistää uudelleen nyt?"
},
"Notices": {
"moreInfo": "Enemmän tietoa"
},
"Updater": {
"updateFailed": "Päivitys epäonnistui",
"updateFailedMessage": "BetterDiscordin päivitys epäonnistui. Lataa asennusohjelman uusin versio verkkosivustoltamme (https://betterdiscord.app/) ja asenna se uudelleen.",
"updateSuccessful": "Päivittäminen onnistui!",
"updateAvailable": "BetterDiscordilla on uusi päivitys",
"addonUpdatesAvailable": "BetterDiscord on löytänyt päivityksiä {{count}}:lle {{type}}:lle!",
"addonUpdated": "{{name}} on päivitetty versioon {{version}}!",
"checking": "Tarkistetaan päivityksiä!",
"finishedChecking": "Päivitysten tarkistaminen on valmis!",
"checkForUpdates": "Tarkista päivitykset!",
"updateAll": "Päivitä kaikki!",
"noUpdatesAvailable": "Päivityksiä ei ole saatavilla.",
"versionAvailable": "Versio {{version}} on saatavilla!",
"upToDateBlankslate": "Kaikki sinun {{type}} näyttävät olevan ajan tasalla!",
"updateButton": "Päivitä!"
}
}

View File

@ -13,10 +13,6 @@
"name": "Système D'émojis",
"note": "Active le système d'émojis de BD"
},
"publicServers": {
"name": "Serveurs publics",
"note": "Afficher le bouton des serveurs publics"
},
"voiceDisconnect": {
"name": "Déconnexion vocale",
"note": "Déconnexion du serveur vocal lors de la fermeture de Discord"
@ -220,27 +216,6 @@
"downloadFailed": "Échec Téléchargement",
"failureMessage": "BetterDiscord n'a pas réussi à télécharger les émotes, veuillez vérifier votre connexion Internet et votre pare-feu."
},
"PublicServers": {
"button": "public",
"join": "Rejoindre",
"joining": "En train de rejoindre",
"joined": "Rejoint",
"loading": "Chargement",
"loadMore": "Charger plus",
"notConnected": "Vous n'êtes pas connecté",
"connectionRequired": "Vous devez connecter votre compte afin de rejoindre des serveurs.",
"connectionError": "Erreur de Connexion",
"connectionErrorMessage": "Une erreur s'est produite lors de la connexion à DiscordServers.com, il est possible que leur site web/api soit en panne. Veuillez réessayer plus tard.",
"pagination": "Page {{page}} sur {{count}}",
"search": "Rechercher",
"connect": "Connecter",
"reconnect": "Reconnecter",
"categories": "Catégories",
"keywords": "Mots Clés",
"connection": "Connecté en tant que: {{username}}#{{discriminator}}",
"results": "Afficher {{start}}-{{end}} sur les {{total}} résultats dans la catégorie {{category}}",
"query": "pour {{query}}"
},
"Modals": {
"confirmAction": "Êtes-vous sûr?",
"okay": "D'accord",

View File

@ -13,10 +13,6 @@
"name": "भावना प्रणाली",
"note": "BD के इमोशन सिस्टम को सक्षम करता है"
},
"publicServers": {
"name": " सार्वजनिक सर्वर",
"note": "सार्वजनिक सर्वर बटन प्रदर्शित करें"
},
"voiceDisconnect": {
"name": "आवाज डिस्कनेक्ट",
"note": "डिसॉर्डर को बंद करते समय वॉयस सर्वर से डिस्कनेक्ट करें"

View File

@ -14,10 +14,6 @@
"name": "Emotikon rendszer",
"note": "BD emotikon rendszer engedélyezése"
},
"publicServers": {
"name": "Nyilvános szerverek",
"note": "Nyilvános szerverek gomb megjelenítése"
},
"voiceDisconnect": {
"name": "Hangkapcsolat megszakítása",
"note": "Kapcsolat megszakítása a hangkiszolgálóval amikor bezárod a Discordot"
@ -259,27 +255,6 @@
"downloadFailed": "Letöltés sikertelen",
"failureMessage": "A BetterDiscord nem tudta letölteni az emotikonokat, kérjük ellenőrizze az internetkapcsolatát és a tűzfalat."
},
"PublicServers": {
"button": "nyilvános",
"join": "Csatlakozás",
"joining": "Csatlakozás",
"joined": "Csatlakozva",
"loading": "Betöltés",
"loadMore": "Több betöltése",
"notConnected": "Sikertelen csatlakozás",
"connectionRequired": "A szerverekhez való csatlakozáshoz csatlakoztatnod kell a fiókodat.",
"connectionError": "Csatlakozási hiba",
"connectionErrorMessage": "Hiba történt a DiscordServers.com-hoz való csatlakozásban, lehetséges, hogy a weboldaluk/api nem elérhető. Kérjük, próbálja meg később.",
"pagination": "{{page}} oldal a {{count}} -ból",
"search": "Keresés",
"connect": "Csatlakozás",
"reconnect": "Újracsatlakozás",
"categories": "Kategóriák",
"keywords": "Kulcsszavak",
"connection": "Csatlakoztatva mint: {{username}}#{{discriminator}}",
"results": "{{start}}-{{end}} eredmény megjelenítése a {{total}} {{category}} -ból",
"query": "a {{query}}"
},
"Modals": {
"confirmAction": "Biztos vagy benne?",
"okay": "Oké",
@ -319,7 +294,7 @@
"updateFailedMessage": "A BetterDiscord frissítése sikertelen. Kérjük, töltse le a telepítő legújabb verzióját erről a weboldalról: (https://betterdiscord.app/), majd telepítse újra.",
"updateSuccessful": "Frissítés sikeres!",
"updateAvailable": "Új frissítés erélhető a BetterDiscordhoz: (v{{version}})",
"addonUpdatesAvailable": "A BetterDiscord {{count}}db frissítést talált ebben a témában: {{type}}!",
"addonUpdatesAvailable": "A BetterDiscord {{count}} frissítést talált ebben a témában: {{type}}!",
"addonUpdated": "A {{name}} frissült {{version}} verzióra!",
"checking": "Frissítések keresése!",
"finishedChecking": "Frissítések ellenőrzése sikeres!",

View File

@ -13,10 +13,6 @@
"name": "Sistema di Emotes",
"note": "Abilita il sistema di Emotes di BD"
},
"publicServers": {
"name": "Servers Pubblici",
"note": "Mostra il pulsante di servers pubblici"
},
"voiceDisconnect": {
"name": "Esci dalla chat vocale",
"note": "Esci automaticamente dalla chat vocale quando chiudi Discord"
@ -216,27 +212,6 @@
"downloadFailed": "Download fallito",
"failureMessage": "BetterDiscord non è riuscito a scaricare le emote, controlla la tua connessione internet e il firerwall."
},
"PublicServers": {
"button": "pubblico",
"join": "Unisciti",
"joining": "Ti stai unendo",
"joined": "Ti sei unito",
"loading": "Caricamento in corso",
"loadMore": "Carica di più",
"notConnected": "Non connesso",
"connectionRequired": "Devi connettere il tuo account per poter unirti ai server.",
"connectionError": "Errore di connessione",
"connectionErrorMessage": "C'è stato un errore di connessione verso DiscordServers.com, è possibile che il loro sito web/API siano non raggiungibili. Riprova più tardi.",
"pagination": "Pagina {{page}} di {{count}}",
"search": "Cerca",
"connect": "Connettiti",
"reconnect": "Riconnettiti",
"categories": "Categorie",
"keywords": "Parole chiave",
"connection": "Connesso come: {{username}}#{{discriminator}}",
"results": "Mostrando {{start}}-{{end}} di {{total}} risultati in {{category}}",
"query": "per {{query}}"
},
"Modals": {
"confirmAction": "Sei sicuro?",
"okay": "Ok",

View File

@ -14,10 +14,6 @@
"name": "絵文字機能",
"note": "BetterDiscordの絵文字機能を有効にします。"
},
"publicServers": {
"name": "公開サーバー",
"note": "公開サーバーボタンを表示します。"
},
"voiceDisconnect": {
"name": "ボイスチャンネルの切断",
"note": "Discordを終了すると自動的にボイスチャンネルから切断されます。"
@ -80,12 +76,12 @@
"note": "カスタムCSSを外部のエディタで起動します"
},
"openAction": {
"name": "Editor Location",
"note": "Where Custom CSS should open by default",
"name": "エディター",
"note": "カスタムCSSがデフォルトで開かれる場所",
"options": {
"settings": "Settings Menu",
"detached": "Detached Window",
"system": "System Editor"
"settings": "設定メニュー",
"detached": "内部エディター",
"system": "システム既定のエディター"
}
}
},
@ -96,16 +92,16 @@
"note": "デベロッパーツールを開いた状態でF8キーを押すとデバッガが起動します。"
},
"reactDevTools": {
"name": "React Developer Tools",
"note": "Injects your local installation of React Developer Tools into Discord"
"name": "React開発者向けツール",
"note": "ローカルにインストールしたReact Developer ToolsをDiscordに挿入します。"
},
"inspectElement": {
"name": "Inspect Element Hotkey",
"note": "Enables the inspect element hotkey (ctrl + shift + c) that is common in most browsers"
"name": "インスペクト・エレメント・ホットキー",
"note": "ほとんどのブラウザで一般的なinspect要素のホットキー(Ctrl + Shift + C)を有効にします。"
},
"devToolsWarning": {
"name": "Stop DevTools Warning",
"note": "Stops Discord from printing out their \"Hold Up!\" message"
"name": "DevToolsの警告を停止",
"note": "Discordの \"Hold Up!\"メッセージの表示を停止します。"
},
"debugLogs": {
"name": "デバッグログ",
@ -181,8 +177,8 @@
"categories": {
"name": "カテゴリ",
"twitchglobal": {
"name": "Twitch Globals",
"note": "Show Twitch global emotes"
"name": "Twitchグローバル",
"note": "Twitchのグローバル絵文字を表示する"
},
"twitchsubscriber": {
"name": "Twitchの登録者",
@ -230,15 +226,15 @@
"methodError": "{{method}}を起動できませんでした。",
"unknownAuthor": "作者不明",
"noDescription": "説明はありません。",
"alreadyExists": "There is already a {{type}} with name {{name}}",
"alreadWatching": "Already watching addons.",
"metaError": "META could not be parsed.",
"missingNameData": "META missing name data.",
"metaNotFound": "META was not found.",
"alreadyExists": "同じく{{name}}という名前を持つ{{type}}がすでに存在します。",
"alreadWatching": "すでにアドオン済み。",
"metaError": "METAを解析できませんでした。",
"missingNameData": "METAの名前データがありません。",
"metaNotFound": "METAが見つかりませんでした。",
"compileError": "コンパイルできませんでした。詳しくはコンソールをご覧ください。",
"wasUnloaded": "{{name}} was unloaded.",
"blankSlateHeader": "You don't have any {{type}}s!",
"blankSlateMessage": "Grab some from [this website]({{link}}) and add them to your {{type}} folder.",
"wasUnloaded": "{{name}}が解除されました。",
"blankSlateHeader": "{{type}}がありません。",
"blankSlateMessage": "このサイト({{link}})からいくつか入手して、{{type}}フォルダに追加してください。",
"isEnabled": "有効",
"wasLoaded": "{{name}}のv{{version}}が読み込まれました。"
},
@ -252,34 +248,13 @@
"editorTitle": "カスタムCSSエディター"
},
"Emotes": {
"loading": "Loading emotes in the background do not reload.",
"loading": "バックグラウンドで絵文字をロードしても、リロードされないようにします。",
"loaded": "すべての絵文字の読み込みに成功しました。",
"clearEmotes": "絵文字データを削除",
"favoriteAction": "お気に入り!",
"favoriteAction": "お気に入り",
"downloadFailed": "ダウンロードに失敗",
"failureMessage": "BetterDiscordは絵文字のダウンロードに失敗しました。インターネット接続とファイアウォールを確認してください。"
},
"PublicServers": {
"button": "公開サーバー",
"join": "参加",
"joining": "参加中",
"joined": "参加済",
"loading": "読み込み中",
"loadMore": "もっと読み込む",
"notConnected": "接続できませんでした。",
"connectionRequired": "サーバーに参加するためには、アカウントを接続する必要があります。",
"connectionError": "接続エラー",
"connectionErrorMessage": "DiscordServers.comへの接続にエラーが発生しました。ウェブサイト又はAPIがダウンしている可能性があります。後でもう一度試してみてください。",
"pagination": "Page {{page}} of {{count}}",
"search": "検索",
"connect": "接続",
"reconnect": "再接続",
"categories": "カテゴリ",
"keywords": "キーワード",
"connection": "接続済:{{username}}#{{discriminator}}",
"results": "{{category}}カテゴリの{{total}}中、{{start}}-{{end}}を表示しています。",
"query": "クエリ:{{query}}"
},
"Modals": {
"confirmAction": "本当によろしいですか?",
"okay": "はい",
@ -309,7 +284,7 @@
},
"WindowPrefs": {
"enabledInfo": "このオプションが正しく動作するためには、透明なテーマが必要です。Windows では、エアロスナップと最大化が壊れる可能性があります。\n\nこのオプションを有効にするには、Discord を再起動する必要があります。今すぐ再起動しますか?",
"disabledInfo": "有効にするためには、Discord再起動する必要があります。今すぐ再起動しますか?"
"disabledInfo": "有効にするためには、Discord<EFBFBD><EFBFBD><EFBFBD>再起動する必要があります。今すぐ再起動しますか?"
},
"Notices": {
"moreInfo": "詳細"
@ -319,15 +294,15 @@
"updateFailedMessage": "BetterDiscordのアップデートに失敗しました。ホームページ(https://betterdiscord.app/)から最新版のインストーラーをダウンロードして、再インストールしてください。",
"updateSuccessful": "アップデートに成功しました!",
"updateAvailable": "BetterDiscordに新しいアップデート(v{{version}})があります。",
"addonUpdatesAvailable": "BetterDiscordは{{.type}}のアップデートを{{count}}個見つけました!",
"addonUpdatesAvailable": "BetterDiscordは{{.type}}のアップデートを{{count}}個見つけました",
"addonUpdated": "{{name}}がバージョン{{version}}にアップデートされました!",
"checking": "アップデートを確認",
"finishedChecking": "アップデートの確認が終了しました!",
"checkForUpdates": "アップデートを確認",
"updateAll": "全てアップデート",
"checkForUpdates": "アップデートを確認",
"updateAll": "全てアップデート",
"noUpdatesAvailable": "アップデートはありません。",
"versionAvailable": "バージョン{{version}}を公開しました。",
"upToDateBlankslate": "{{type}}は全て最新版です。",
"updateButton": "アップデート"
"updateButton": "アップデート"
}
}

View File

@ -14,13 +14,9 @@
"name": "이모트 시스템",
"note": "BD의 이모트 시스템을 활성화합니다"
},
"publicServers": {
"name": "공개 서버",
"note": "공개 서버 버튼을 표시합니다"
},
"voiceDisconnect": {
"name": "음성 연결 끊기",
"note": "Discord를 닫을 때 음성 채널의 연결을 끊습니다"
"note": "디스코드를 닫을 때 음성 채널의 연결을 끊습니다"
},
"showToasts": {
"name": "토스트 메시지 표시",
@ -28,13 +24,13 @@
},
"mediaKeys": {
"name": "미디어 키 비활성화",
"note": "Discord가 영상을 재생한 후에 미디어 키를 가져가는 것을 방지합니다"
"note": "영상을 재생한 후, 디스코드가 미디어 키를 가로채는 것을 방지합니다"
}
},
"window": {
"removeMinimumSize": {
"name": "최소 크기 제거",
"note": "디스코드의 강제 창 최소 크기인 940x500을 제거합니다"
"note": "디스코드의 강제 창 최소 크기 제한 (940x500) 을 제거합니다"
},
"name": "창 선호",
"transparency": {
@ -96,16 +92,16 @@
"note": "F8을 누를 때 디버거를 활성화합니다"
},
"reactDevTools": {
"name": "반응 개발자 도구",
"note": "반응 개발자 도구를 로컬 설치에 합니다"
"name": "React 개발자 도구",
"note": "로컬 환경에 설치된 React 개발자 도구를 디스코드에 삽입합니다"
},
"inspectElement": {
"name": "요소 선택 단축키",
"note": "대부분의 브라우저에서 가장 흔한 요소 선택 단축키를 활성화합니다 (ctrl + shift + c)"
},
"devToolsWarning": {
"name": "데브툴 경고 멈추기",
"note": "Discord가 \"Hold Up!\" 메시지를 표시하는 것을 멈춥니다"
"name": "DevTools 경고 멈추기",
"note": "디스코드가 \\\"Hold Up!\\\" 메시지를 표시하는 것을 멈춥니다"
},
"debugLogs": {
"name": "디버그 로그",
@ -163,7 +159,7 @@
},
"hideEmojiMenu": {
"name": "이모트 메뉴 숨기기",
"note": "이모트 메뉴를 사용 중일 때 Discord의 이모지 메뉴를 숨깁니다"
"note": "이모트 메뉴를 사용 중일 때 디스코드의 이모지 메뉴를 숨깁니다"
},
"autoCaps": {
"name": "이모트 자동 대문자",
@ -222,10 +218,10 @@
"confirmationText": "{{name}}에 저장되지 않은 수정사항이 있습니다. 이 창을 닫으면 모든 수정이 취소됩니다.",
"enabled": "{{name}}이 활성화되었습니다",
"disabled": "{{name}}이 비활성화되었습니다",
"couldNotEnable": "{{name}} 활성화에 실패했습니다",
"couldNotDisable": "{{name}} 비활성화에 실패했습니다.",
"couldNotStart": "{{name}} 시작에 실패했습니다",
"couldNotStop": "{{name}} 정지에 실패했습니다",
"couldNotEnable": "{{name}}(을)를 활성화하지 못했습니다",
"couldNotDisable": "{{name}}(을)를 비활성화하지 못했습니다",
"couldNotStart": "{{name}}(을)를 시작하지 못했습니다",
"couldNotStop": "{{name}}(을)를 정지하지 못했습니다",
"settingsError": "{{name}} 의 설정을 열 수 없습니다",
"methodError": "{{method}}(을)를 실행할 수 없습니다",
"unknownAuthor": "알 수 없는 제작자",
@ -259,27 +255,6 @@
"downloadFailed": "다운로드 실패",
"failureMessage": "BetterDiscord가 이모트 다운로드에 실패했습니다. 인터넷 상태와 방화벽을 확인해주세요"
},
"PublicServers": {
"button": "공개",
"join": "참가",
"joining": "참가 중",
"joined": "참가함",
"loading": "로딩 중",
"loadMore": "더 불러오기",
"notConnected": "연결되지 않음",
"connectionRequired": "서버에 참가하기 위해서 계정을 연결해야 합니다",
"connectionError": "연결 오류",
"connectionErrorMessage": "DiscordServers.com 연결에 오류가 있습니다. 웹사이트/API가 꺼져있을 수 있습니다. 나중에 다시 시도하세요.",
"pagination": "{{page}} 페이지 중 {{count}}",
"search": "검색",
"connect": "연결",
"reconnect": "재연결",
"categories": "카테고리",
"keywords": "키워드",
"connection": "{{username}}#{{discriminator}} 연결됨",
"results": "{{category}} 의 검색결과에서 {{total}} 중 {{start}}-{{end}} 표시 중",
"query": "쿼리: {{query}}"
},
"Modals": {
"confirmAction": "확실합니까?",
"okay": "확인",
@ -295,7 +270,7 @@
"restartNow": "지금 재시작하기",
"restartLater": "나중에 재시작하기",
"additionalInfo": "추가 정보",
"restartPrompt": "적용을 위해 Discord가 재시작되어야 합니다. 지금 재시작하시겠습니까?"
"restartPrompt": "적용을 위해 디스코드가 재시작되어야 합니다. 지금 재시작하시겠습니까?"
},
"ReactDevTools": {
"notFound": "확장 프로그램을 찾을 수 없음",
@ -308,8 +283,8 @@
"descending": "내림차순"
},
"WindowPrefs": {
"enabledInfo": "이 옵션은 제대로 작동하려면 투명한 테마가 필요합니다. Windows가 애어로 스내핑과 최대화를 고장낼 수 있습니다.\n\n적용을 위해 Discord가 재시작되어야 합니다. 지금 재시작하시겠습니까?",
"disabledInfo": "적용을 위해 Discord가 재시작되어야 합니다. 지금 재시작하시겠습니까?"
"enabledInfo": "이 옵션은 제대로 작동하려면 투명한 테마가 필요합니다. Windows에서는 에어로 스내핑과 최대화가 고장날 수 있습니다.\n\n적용을 위해 디스코드를 재시작해야 합니다. 지금 재시작하시겠습니까?",
"disabledInfo": "적용을 위해 디스코드를 재시작해야 합니다. 지금 재시작하시겠습니까?"
},
"Notices": {
"moreInfo": "더 많은 정보 보기"

View File

@ -13,10 +13,6 @@
"name": "Emote systeem",
"note": "Schakel het emote-systeem van BD in"
},
"publicServers": {
"name": "Openbare servers",
"note": "Knop openbare servers weergeven"
},
"voiceDisconnect": {
"name": "Stem Verbroken",
"note": "Verbreek de verbinding met de oproep"
@ -77,15 +73,6 @@
"Emotes": {
"favoriteAction": "Favorieten"
},
"PublicServers": {
"button": "openbaar",
"loading": "Aan het laden...",
"loadMore": "Laad meer...",
"notConnected": "Niet verbonden.",
"search": "Zoeken",
"connect": "Verbindeen",
"categories": "Categorieën"
},
"Modals": {
"confirmAction": "Weet je het zeker?",
"okay": "Oké",

View File

@ -10,9 +10,6 @@
"emotes": {
"name": "Emote-system"
},
"publicServers": {
"name": "Offentlige Servere"
},
"showToasts": {
"name": "Vis Toasts"
}
@ -61,10 +58,6 @@
"Emotes": {
"favoriteAction": "Favoritt!"
},
"PublicServers": {
"button": "offentlig",
"search": "Søk"
},
"Modals": {
"confirmAction": "Er Du Sikker?",
"okay": "Okay",

View File

@ -2,7 +2,8 @@
"Panels": {
"plugins": "Wtyczki",
"themes": "Motywy",
"customcss": "Niestandardowy CSS"
"customcss": "Niestandardowy CSS",
"updates": "Aktualizacje"
},
"Collections": {
"settings": {
@ -13,13 +14,9 @@
"name": "System emoji",
"note": "Aktywuje system emoji BetterDiscorda"
},
"publicServers": {
"name": "Publiczne serwery",
"note": "Wyświetla przycisk do publicznych serwerów"
},
"voiceDisconnect": {
"name": "Rozłączaj z czatem głosowym",
"note": "Rozłącza z serwerem czatu głosowego przy wyłączaniu Discorda"
"note": "Rozłącza z serwerem czatu głosowego przy wyłączeniu Discorda"
},
"showToasts": {
"name": "Wyświetlaj okienka powiadomień",
@ -64,7 +61,7 @@
"name": "Niestandardowy CSS",
"customcss": {
"name": "Niestandardowy CSS",
"note": "Wyświetla zakładkę z niestandardoweym kodem CSS"
"note": "Wyświetla zakładkę z edytorem niestandardowego kodu CSS"
},
"liveUpdate": {
"name": "Automatyczny podgląd",
@ -72,11 +69,11 @@
},
"startDetached": {
"name": "Osobne okno edytora",
"note": "Kliknięcie na zakładkę z niestandardowym kodem CSS uruchomi edytor w osobnym oknie"
"note": "Kliknięcie na zakładkę „Niestandardowy CSS” uruchomi edytor w osobnym oknie"
},
"nativeOpen": {
"name": "Otwórz w edytorze systemowym",
"note": "Kliknięcie na zakładkę z niestandardowym kodem CSS uruchomi plik w edytorze systemowym"
"note": "Kliknięcie na zakładkę „Niestandardowy CSS” uruchomi plik w edytorze systemowym"
},
"openAction": {
"name": "Domyślny edytor",
@ -92,11 +89,11 @@
"name": "Ustawienia dla programistów",
"debuggerHotkey": {
"name": "Debugger",
"note": "Uruchamia debugger po naciśnięciu F8"
"note": "Uruchamia debugger po naciśnięciu F8 przy otworzonym panelu narzędzi dla programistów"
},
"reactDevTools": {
"name": "React Developer Tools",
"note": "Wprowadza zainstalowane na twoim komputerze rozszerzenie React Developer Tools do Discorda"
"note": "Wprowadza zainstalowane na Twoim komputerze rozszerzenie React Developer Tools do Discorda"
},
"inspectElement": {
"name": "Narzędzie „Zbadaj element”",
@ -109,6 +106,42 @@
"debugLogs": {
"name": "Dziennik zdarzeń",
"note": "Przekierowuje informacje z konsoli do pliku debug.log w folderze BetterDiscorda"
},
"devTools": {
"name": "Narzędzia dla programistów",
"note": "Umożliwia wyświetlenie panelu narzędzi dla programistów po użyciu skrótu klawiszowego Ctrl+Shift+I"
}
},
"editor": {
"name": "Ustawienia edytora",
"lineNumbers": {
"name": "Numery wierszy",
"note": "Wyświetla numery wierszy po lewej stronie edytora"
},
"fontSize": {
"name": "Rozmiar czcionki",
"note": "Ustawia rozmiar czcionki (pt) w edytorze"
},
"minimap": {
"name": "Podgląd",
"note": "Wyświetla mały podgląd kodu po prawej stronie edytora"
},
"hover": {
"name": "Etykiety",
"note": "Wyświetla etykiety z dodatkowymi informacjami po najechaniu na element kodu"
},
"quickSuggestions": {
"name": "Podpowiadanie składni",
"note": "Włącza automatyczne podpowiadanie składni podczas pisania kodu"
},
"renderWhitespace": {
"name": "Pokaż białe znaki",
"note": "Określa, kiedy białe znaki będą wyświetlane w edytorze",
"options": {
"all": "Zawsze",
"none": "Nigdy",
"selection": "Przy zaznaczaniu"
}
}
}
},
@ -182,14 +215,14 @@
"editAddon": "Edytuj",
"deleteAddon": "Usuń",
"confirmDelete": "Czy na pewno chcesz usunąć {{name}}?",
"confirmationText": "Masz niezapisane zmiany w kodzie {{name}}. Zamknięcie tego okna spowoduje utratę danych.",
"confirmationText": "Masz niezapisane zmiany w kodzie {{name}}. Zamknięcie tego okna spowoduje odrzucenie wszystkich zmian.",
"enabled": "Dodatek {{name}} został włączony.",
"disabled": "Dodatek {{name}} został wyłączony.",
"couldNotEnable": "Dodatek {{name}} nie może zostać włączony.",
"couldNotDisable": "Dodatek {{name}} nie może zostać wyłączony.",
"couldNotStart": "Dodatek {{name}} nie może zostać włączony.",
"couldNotStop": "Dodatek {{name}} nie może zostać wyłączony.",
"settingsError": "Nie udało się otworzyć ustawień {{name}}",
"couldNotStart": "Dodatek {{name}} nie może zostać uruchomiony.",
"couldNotStop": "Dodatek {{name}} nie może zostać zatrzymany.",
"settingsError": "Nie udało się otworzyć ustawień dodatku {{name}}",
"methodError": "Metoda {{method}} nie może zostać wywołana.",
"unknownAuthor": "Nieznany twórca",
"noDescription": "Brak opisu.",
@ -198,13 +231,15 @@
"metaError": "Nie udało się przetworzyć metadanych.",
"missingNameData": "Brak nazwy dodatku w metadanych.",
"metaNotFound": "Nie odnaleziono metadanych.",
"compileError": "Błąd kompilacji.",
"compileError": "Błąd kompilacji. Sprawdź konsolę, aby dowiedzieć się więcej.",
"wasUnloaded": "Dodatek {{name}} został odładowany.",
"blankSlateHeader": "Wygląda na to, że nic tu nie ma!",
"blankSlateMessage": "Pobierz nowe dodatki z [tej strony]({{link}}) i przenieś je do odpowiedniego folderu."
"blankSlateMessage": "Pobierz nowe dodatki z [tej strony]({{link}}) i przenieś je do odpowiedniego folderu.",
"isEnabled": "Stan",
"wasLoaded": "Dodatek {{name}} (wersja {{version}}) został załadowany."
},
"CustomCSS": {
"confirmationText": "Masz niezapisane zmiany w swoim niestandardowym kodzie CSS. Zamknięcie tego okienka spowoduje utratę danych.",
"confirmationText": "Masz niezapisane zmiany w swoim niestandardowym kodzie CSS. Zamknięcie tego okienka spowoduje odrzucenie wszystkich zmian.",
"update": "Zaktualizuj",
"save": "Zapisz",
"openNative": "Otwórz w edytorze systemowym",
@ -218,32 +253,11 @@
"clearEmotes": "Wyczyść dane emoji",
"favoriteAction": "Dodaj do ulubionych",
"downloadFailed": "Pobieranie nie powiodło się",
"failureMessage": "Nie udało się pobrać emoji, sprawdź swoje połączenie z internetem lub zaporę ogniową."
},
"PublicServers": {
"button": "serwery",
"join": "Dołącz",
"joining": "Dołączanie",
"joined": "Dołączono",
"loading": "Ładowanie",
"loadMore": "Załaduj więcej",
"notConnected": "Nie połączono",
"connectionRequired": "Musisz połączyć swoje konto, by wejść na serwer.",
"connectionError": "Błąd połączenia",
"connectionErrorMessage": "Wystąpił błąd podczas łączenia z DiscordServers.com (najprawdopodobniej ich strona lub API nie działa). Spróbuj ponownie później.",
"pagination": "Strona {{page}} z {{count}}",
"search": "Wyszukaj",
"connect": "Połącz",
"reconnect": "Odnów połączenie",
"categories": "Kategorie",
"keywords": "Słowa klucze",
"connection": "Połączono jako: {{username}}#{{discriminator}}",
"results": "Wyświetlanie {{start}}-{{end}} z {{total}} wszystkich wyników w {{category}}",
"query": "dla {{query}}"
"failureMessage": "Nie udało się pobrać emoji, sprawdź swoje połączenie z internetem oraz zaporę ogniową."
},
"Modals": {
"confirmAction": "Czy na pewno chcesz to zrobić?",
"okay": "Zamknij",
"okay": "Tak",
"done": "Potwierdź",
"cancel": "Anuluj",
"nevermind": "Anuluj",
@ -253,14 +267,14 @@
"error": "Błąd",
"addonErrors": "Błędy wtyczek",
"restartRequired": "Wymagane ponowne uruchomienie",
"restartNow": "Zrestartuj teraz",
"restartLater": "Zrestartuj później",
"restartNow": "Uruchom ponownie teraz",
"restartLater": "Uruchom ponownie później",
"additionalInfo": "Dodatkowe informacje",
"restartPrompt": "Uruchom ponownie Discorda, by zmiany odniosły efekty. Czy chcesz to zrobić teraz?"
"restartPrompt": "Uruchom ponownie Discorda, by zastosować zmiany. Czy chcesz to zrobić teraz?"
},
"ReactDevTools": {
"notFound": "Nie znaleziono rozszerzenia",
"notFoundDetails": "Nie udało się znaleźć React Developer Tools na twoim urządzeniu. By kontynuuować, zainstaluj to rozszerzenie w swoim Chrome."
"notFoundDetails": "Nie udało się znaleźć React Developer Tools na twoim urządzeniu. By kontynuować, zainstaluj to rozszerzenie w swojej przeglądarce Chrome."
},
"Sorting": {
"sortBy": "Sortuj wg",
@ -269,7 +283,26 @@
"descending": "Malejąca"
},
"WindowPrefs": {
"enabledInfo": "Ta opcja wymaga przezroczystego motywu do poprawnego działania. Na Windowsie mogą pojawić się problemy ze zmianą wielkości okna.\n\nUruchom ponownie Discorda, by zmiany odniosły efekty. Czy chcesz to zrobić teraz?",
"enabledInfo": "Ta opcja wymaga przezroczystego motywu do poprawnego działania. Na Windowsie mogą pojawić się problemy ze zmianą wielkości okna.\n\nUruchom ponownie Discorda, by zastosować zmiany. Czy chcesz to zrobić teraz?",
"disabledInfo": "Uruchom ponownie Discorda, by zmiany odniosły efekty. Czy chcesz to zrobić teraz?"
},
"Notices": {
"moreInfo": "Dowiedz się więcej"
},
"Updater": {
"updateFailed": "Aktualizacja nie powiodła się!",
"updateFailedMessage": "Nie udało się zaktualizować BetterDiscorda. Pobierz najnowszy instalator z naszej strony (https://betterdiscord.app/) i zainstaluj aplikację ponownie.",
"updateSuccessful": "Aktualizacja powiodła się!",
"updateAvailable": "Dostępna jest nowa aktualizacja BetterDiscorda (wersja {{version}})",
"addonUpdatesAvailable": "Są dostępne nowe aktualizacje dla twoich dodatków ({{count}})!",
"addonUpdated": "Dodatek {{name}} został zaktualizowany do wersji {{version}}!",
"checking": "Sprawdzanie dostępności aktualizacji!",
"finishedChecking": "Zakończono sprawdzanie dostępności aktualizacji!",
"checkForUpdates": "Sprawdź dostępność aktualizacji",
"updateAll": "Zaktualizuj wszystko",
"noUpdatesAvailable": "Brak dostępnych aktualizacji.",
"versionAvailable": "Dostępna jest nowa wersja ({{version}})!",
"upToDateBlankslate": "Wszystkie dodatki są aktualne!",
"updateButton": "Zaktualizuj"
}
}

View File

@ -13,10 +13,6 @@
"name": "Sistema de Emotes",
"note": "Ativa o sistema de emotes do BetterDiscord"
},
"publicServers": {
"name": "Servidores Públicos",
"note": "Mostra o botão de servidores públicos"
},
"voiceDisconnect": {
"name": "Desconexão de Voz",
"note": "Desconecta do servidor de voz quando o discord é fechado"
@ -220,27 +216,6 @@
"downloadFailed": "Download Falhou",
"failureMessage": "BetterDiscord não conseguiu baixar os emotes, por favor verifique sua conexão da internet e seu firewall."
},
"PublicServers": {
"button": "público",
"join": "Entrar",
"joining": "Entrando",
"joined": "Entrou",
"loading": "Carregando",
"loadMore": "Carregar Mais",
"notConnected": "Não Conectado",
"connectionRequired": "Você precisa conectar na sua conta para entrar em servidores.",
"connectionError": "Erro de Conexão",
"connectionErrorMessage": "Algum erro ocorreu enquanto conectava no DiscordServers.com, é possível que o site/api deles esteja fora do ar. Tente novamente mais tarde.",
"pagination": "Página {{page}} de {{count}}",
"search": "Pesquisar",
"connect": "Conectar",
"reconnect": "Reconectar",
"categories": "Categorias",
"keywords": "Palavras Chave",
"connection": "Conectado como: {{username}}#{{discriminator}}",
"results": "Mostrando {{start}}-{{end}} de {{total}} resultados da categoria {{category}}",
"query": "por {{query}}"
},
"Modals": {
"confirmAction": "Você Tem Certeza?",
"okay": "Okay",

View File

@ -13,10 +13,6 @@
"name": "Sistema de Emotes",
"note": "Ativa o sistema de emotes do BD"
},
"publicServers": {
"name": "Servidores Publicos",
"note": "Mostra o botão de servidores publicos"
},
"voiceDisconnect": {
"name": "Disconectar Voz",
"note": "Disconectar do servidor de voz quando fechar o discord"
@ -220,27 +216,6 @@
"downloadFailed": "Transfêrencia falhou",
"failureMessage": "BetterDiscord falhou a transfêrencia de emotes, por favor verifique a sua conexão à Internet e à Firewall."
},
"PublicServers": {
"button": "público",
"join": "Entrar",
"joining": "Entrando",
"joined": "Entrou",
"loading": "A Carregar",
"loadMore": "Carregar Mais",
"notConnected": "Não Conectado",
"connectionRequired": "Tem que estár conectado a sua conta para se entrar nos servidores.",
"connectionError": "Erro de conexão",
"connectionErrorMessage": "Houve um error ao tentar conectar com os DiscordServers.com, é possível que o website/api esteja em baixo. Por favor tente mais tarde.",
"pagination": "Página {{page}} de {{count}}",
"search": "Procurar",
"connect": "Conectar",
"reconnect": "Reconectar",
"categories": "Categorias",
"keywords": "Palavras-Chave",
"connection": "Conectado com: {{username}}#{{discriminator}}",
"results": "Mostrar {{start}}-{{end}} do {{total}} resultados na {{category}}",
"query": "para {{query}}"
},
"Modals": {
"confirmAction": "Tem a certeza?",
"okay": "Okay",

View File

@ -13,10 +13,6 @@
"name": "Sistemul de emote-uri",
"note": "Activează sistemul de emote-uri din cadrul BD"
},
"publicServers": {
"name": "servere publice",
"note": "Afișează butonul de servere publice"
},
"voiceDisconnect": {
"name": "Deconectare de la voce",
"note": "Deconectează-te de la un canal de voce când inchizi Discord-ul"
@ -220,27 +216,6 @@
"downloadFailed": "Descărcarea a eșuat",
"failureMessage": "BetterDiscord nu a putut încărca emote-urile, te rugăm să verifici conexiunea la internet si firewall-ul."
},
"PublicServers": {
"button": "public",
"join": "Alătură-te",
"joining": "În curs de alăturare",
"joined": "Alăturat",
"loading": "Se încarcă",
"loadMore": "Încarcă mai multe",
"notConnected": "Deconectat",
"connectionRequired": "Trebuie să vă conectați pentru a vă alătura la servere.",
"connectionError": "Eroare de conexiune",
"connectionErrorMessage": "A apărut o eroare în conectarea la DiscordServers.com, este posibil ca website-ul/API-ul să fie offline. Vă rugăm să încercațti mai târziu.",
"pagination": "Pagina {{page}} din {{count}}",
"search": "Caută",
"connect": "Conectează-te",
"reconnect": "Reconectează-te",
"categories": "Categorii",
"keywords": "Cuvinte cheie",
"connection": "Conectat ca: {{username}}#{{discriminator}}",
"results": "Arătând {{start}}-{{end}} din {{total}} rezultate in {{category}}",
"query": "pentru {{query}}"
},
"Modals": {
"confirmAction": "Ești sigur ?",
"okay": "Okay",

View File

@ -14,10 +14,6 @@
"name": "Система смайликов",
"note": "Включает систему смайликов BD"
},
"publicServers": {
"name": "Публичные сервера",
"note": "Отображает кнопку публичных серверов"
},
"voiceDisconnect": {
"name": "Отключение голосового чата",
"note": "Отключает от голосового чата, когда Discord закрыт"
@ -259,27 +255,6 @@
"downloadFailed": "Загрузка не удалась",
"failureMessage": "BetterDiscord'у не удалось загрузить смайлики, проверьте подключение к интернету и брандмауэр."
},
"PublicServers": {
"button": "Публичные сервера",
"join": "Присоединился",
"joining": "Присоединение",
"joined": "Присоединился",
"loading": "Загрузка",
"loadMore": "Загрузить больше",
"notConnected": "Не подключен",
"connectionRequired": "Вы должны подключить свою учетную запись, чтобы присоединиться к серверам.",
"connectionError": "Ошибка соединения",
"connectionErrorMessage": "Произошла ошибка при подключении к DiscordServers.com, возможно, их сайт/api не упали. Пожалуйста, повторите попытку позже.",
"pagination": "Страница {{page}} и {{count}}",
"search": "Поиск",
"connect": "Подключить",
"reconnect": "Переподключиться",
"categories": "Категории",
"keywords": "Ключевые слова",
"connection": "Подключён как: {{username}}#{{discriminator}}",
"results": "Показано {{start}}-{{end}} из {{total}} результатов в {{category}}",
"query": "для {{query}}"
},
"Modals": {
"confirmAction": "Ты уверен?",
"okay": "Ок",

View File

@ -13,10 +13,6 @@
"name": "Emote Systém",
"note": "Povoliť BD emote systém"
},
"publicServers": {
"name": "Verejné servery",
"note": "Zobraziť tlačidlo verejných serverov"
},
"voiceDisconnect": {
"name": "Odpojenie hovoru",
"note": "Odpojiť z hovoru pri vypnutí Discordu"
@ -220,27 +216,6 @@
"downloadFailed": "Sťahovanie Zlyhalo",
"failureMessage": "BetterDiscord nedokázal načítať emoty, prosím skontrolujte internetové pripojenie a firewall."
},
"PublicServers": {
"button": "verejné",
"join": "Pridať",
"joining": "Pridávanie",
"joined": "Pridaný",
"loading": "Načítavanie",
"loadMore": "Načítať Viac",
"notConnected": "Nepripojený",
"connectionRequired": "Musíte pripojiť svoj účet pre pripojenie sa na servery.",
"connectionError": "Chyba spojenia",
"connectionErrorMessage": "Pri pripájaní k serveru DiscordServers.com sa vyskytla chyba. Je možné, že ich webové stránky / rozhranie API nefunguje. Skúste neskôr prosím.",
"pagination": "Stránka {{page}} z {{count}}",
"search": "Hľadať",
"connect": "Pripojiť",
"reconnect": "Znova pripojiť",
"categories": "Kategórie",
"keywords": "Kľúčové slová",
"connection": "Pripojený ako: {{username}}#{{discriminator}}",
"results": "Zobrazených {{start}}-{{end}} z {{total}} výsledkov v {{category}}",
"query": "pre {{query}}"
},
"Modals": {
"confirmAction": "Ste si istý?",
"okay": "Ok",

View File

@ -2,35 +2,51 @@
"Panels": {
"plugins": "Tillägg",
"themes": "Teman",
"customcss": "Egen CSS"
"customcss": "Egen CSS",
"updates": "Uppdateringar"
},
"Collections": {
"settings": {
"name": "Inställningar",
"general": {
"name": "Generellt",
"name": "Allmänt",
"emotes": {
"name": "Emotessystem",
"note": "Aktiverar BD's emotessystem"
},
"publicServers": {
"name": "Publika Servrar",
"note": "Visa knappen för publika servrar"
"note": "Aktiverar BDs emotessystem"
},
"voiceDisconnect": {
"name": "Lämna Röst",
"name": "Lämna röstsamtal",
"note": "Lämna röstsamtal när Discord stängs ner"
},
"showToasts": {
"name": "Visa Toasts",
"note": "Visar en liten notification för viktig information"
"note": "Visar en liten notifikation för viktig information"
},
"mediaKeys": {
"name": "Inaktivera mediaknapparna",
"note": "Hindrar Discord från att ta över mediaknapparna efter att du har spelat upp en video."
}
},
"window": {
"removeMinimumSize": {
"name": "Ta bort minimistorleken",
"note": "Tar bort Discords minimifönsterstorlek på 940x500"
},
"name": "Fönsterinställningar",
"transparency": {
"name": "Aktivera Genomskinlighet",
"note": "Gör att huvudfönstret blir genomskinligt (kräver omstart)"
},
"frame": {
"name": "Fönsterram",
"note": "Lägger till operativsystemets standard fönsterram runt huvudfönstret"
}
},
"addons": {
"name": "Tilläggshanterare",
"addonErrors": {
"name": "Visa tilläggsfel",
"note": "Visar en modal med tillägg- och temafel"
"note": "Visar en modalruta med tillägg- och temafel"
}
},
"customcss": {
@ -45,7 +61,7 @@
},
"startDetached": {
"name": "Starta Frånkopplad",
"note": "Öppnar CSS redigeraren i ett separat fönster när \"Egen CSS\"-fliken klickas"
"note": "Öppnar CSS-redigeraren i ett separat fönster när \"Egen CSS\"-fliken klickas"
},
"nativeOpen": {
"name": "Öppna I Standardredigerare",
@ -53,17 +69,39 @@
}
},
"developer": {
"name": "Utvecklarinställningar"
},
"window": {
"name": "Fönsterinställningar",
"transparency": {
"name": "Aktivera Genomskinlighet",
"note": "Gör att huvudfönstret blir genomskinligt (kräver omstart)"
"name": "Utvecklarinställningar",
"devToolsWarning": {
"name": "Stäng av utvecklarverktygsvarningar",
"note": "Hindrar Discord från att skriva ut sitt \"Vänta!\"-meddelande"
},
"frame": {
"name": "Fönsterram",
"note": "Lägger till operativsystemets standard fönsterram runt huvudfönstret"
"debugLogs": {
"name": "Felsökningsloggar",
"note": "Allt från konsolen hamnar i filen debug.log i BetterDiscord mappen"
},
"devTools": {
"name": "Utvecklarverktyg"
}
},
"editor": {
"lineNumbers": {
"name": "Radnummer",
"note": "Visar radnummer i marginalen i redigeringsfönstret"
},
"fontSize": {
"name": "Teckenstorlek",
"note": "Storlek på typsnittet som används i redigeringsfönstret"
},
"quickSuggestions": {
"name": "Snabbförslag",
"note": "Aktiverar autokompletteringsförslag när du skriver"
},
"renderWhitespace": {
"name": "Visa blanksteg",
"note": "När blanksteg ska markeras i redigeringsfönstret",
"options": {
"all": "Alltid",
"none": "Aldrig"
}
}
}
},
@ -105,6 +143,37 @@
}
}
},
"Addons": {
"title": "{{name}} v{{version}} av {{author}}",
"byline": "av {{author}}",
"openFolder": "Öppna mappen för {{type}}",
"reload": "Ladda om",
"addonSettings": "Inställningar",
"website": "Webbplats",
"source": "Källa",
"invite": "Supportserver",
"donate": "Donera",
"patreon": "Patreon",
"name": "Namn",
"author": "Upphovsman",
"version": "Version",
"added": "Tilläggsdatum",
"modified": "Ändringsdatum",
"search": "Sök efter {{type}}",
"editAddon": "Redigera",
"deleteAddon": "Ta bort",
"confirmDelete": "Är du säker på att du vill ta bort {{name}}?",
"enabled": "{{name}} har aktiverats.",
"disabled": "{{name}} har inaktiverats.",
"couldNotEnable": "{{name}} kunde inte aktiverats.",
"couldNotDisable": "{{name}} kunde inte inaktiverats.",
"couldNotStart": "{{name}} kunde inte startas.",
"couldNotStop": "{{name}} kunde inte stoppas.",
"settingsError": "Kunde inte öppna inställningarna för {{namn}}",
"unknownAuthor": "Okänd upphovsman",
"noDescription": "Beskrivning saknas.",
"isEnabled": "Aktiverad"
},
"CustomCSS": {
"confirmationText": "Du har osparade ändringar av egna CSS:en. Alla ändringar kommer försvinna om du stänger det här fönstret",
"update": "Uppdatera",
@ -112,34 +181,43 @@
"openNative": "Öppna I Standardredigerare",
"openDetached": "Koppla Loss Fönster",
"settings": "Inställningar för redigerare",
"editorTitle": "Egen CSS Redigerare"
"editorTitle": "Egen CSS-redigerare"
},
"Emotes": {
"clearEmotes": "Rensa Emote Data",
"favoriteAction": "Favorit!"
},
"PublicServers": {
"button": "Publik",
"join": "Gå med",
"joining": "Går med",
"joined": "Gick med",
"loading": "Laddar",
"loadMore": "ladda mer",
"notConnected": "Ingen anslutning till DiscordServers.com!",
"search": "Sök",
"connect": "Anslut",
"reconnect": "Återanslut",
"categories": "Kategorier",
"connection": "Ansluten som: {{username}}#{{discriminator}}",
"results": "Visar {{start}}-{{end}} av {{total}} resultat i {{category}}",
"query": "för {{query}}"
},
"Modals": {
"confirmAction": "Är du säker?",
"okay": "Okej",
"cancel": "Avbryt",
"nevermind": "Avbryt",
"close": "Stäng",
"name": "Namn",
"message": "Meddelande",
"error": "Fel",
"addonErrors": "Tilläggsfel"
"addonErrors": "Tilläggsfel",
"restartRequired": "Omstart krävs",
"restartNow": "Starta om nu",
"restartLater": "Starta om senare",
"restartPrompt": "För att ändringarna ska träda i kraft måste Discord startas om. Vill du starta om nu?"
},
"Sorting": {
"sortBy": "Sortera efter",
"order": "Ordning",
"ascending": "Stigande",
"descending": "Fallande"
},
"Updater": {
"updateFailed": "Uppdateringen misslyckades!",
"updateFailedMessage": "BetterDiscord kunde inte uppdateras. Ladda ned den senaste versionen av installationsprogrammet från vår webbplats (https://betterdiscord.app/) och installera om BetterDiscord.",
"updateSuccessful": "Uppdateringen lyckades!",
"checking": "Söker efter uppdateringar!",
"finishedChecking": "Sökningen efter uppdateringar är slutförd!",
"checkForUpdates": "Sök efter uppdateringar!",
"updateAll": "Uppdatera alla!",
"noUpdatesAvailable": "Det finns inga uppdateringar tillgängliga.",
"upToDateBlankslate": "Alla dina {{type}} verkar vara uppdaterade!",
"updateButton": "Uppdatera!"
}
}

View File

@ -13,10 +13,6 @@
"name": "Emoji Sistemi",
"note": "BD'nin emoji sistemini etkileştirir"
},
"publicServers": {
"name": "Herkese-açık Sunucular",
"note": "Herkese-açık Sunucu düğmesini göster"
},
"voiceDisconnect": {
"name": "Ses Kanalından Ayrılma",
"note": "Discord'u kapatırken aktif ses kanalından ayrılır"
@ -220,27 +216,6 @@
"downloadFailed": "Yükleme başarısız oldu",
"failureMessage": "BetterDiscord emojileri yüklemede başarısız oldu, lütfen internet bağlantınızı kontrol ediniz."
},
"PublicServers": {
"button": "Herkese Açık",
"join": "Katıl",
"joining": "Katılınıyor",
"joined": "Katılındı",
"loading": "Yükleniyor",
"loadMore": "Daha Fazla Yükle",
"notConnected": "Bağlı Değil",
"connectionRequired": "Sunuculara katılmak için hesabını bağlamalısın.",
"connectionError": "Bağlantı Hatası.",
"connectionErrorMessage": "DiscordServers.com'a bağlanırken hata oldu, websitelerini kapalı olması mümkün. Lütfen daha sonra tekrar deneyiniz.",
"pagination": "{{count}} içerisinden {{page}}. sayfa",
"search": "Ara",
"connect": "Bağlan",
"reconnect": "Geri Bağlan",
"categories": "Kategoriler",
"keywords": "Anahtar Kelimeler",
"connection": "{{username}}#{{discriminator}} olarak bağlanıldı",
"results": "{{category}} kategorisinde {{total}} üzerinden {{start}}-{{end}} gösteriliyor",
"query": "{{query}} için"
},
"Modals": {
"confirmAction": "Emin Misin?",
"okay": "Tamam",

View File

@ -13,10 +13,6 @@
"name": "Система емодзі",
"note": "Активація системи емодзі BetterDiscord"
},
"publicServers": {
"name": "Публічні сервери",
"note": "Відображення кнопки переліку публічних серверів"
},
"voiceDisconnect": {
"name": "Голосовий чат під час закриття",
"note": "Відключатися від голосового сервера під час закриття Discord"
@ -220,32 +216,11 @@
"downloadFailed": "Завантаження не вдалося",
"failureMessage": "BetterDiscord не вдалося завантажити емодзі. Будь ласка, перевір підключення до Інтернету та брандмауер."
},
"PublicServers": {
"button": "Публічні сервери",
"join": "Приєднатися",
"joining": "Приєднання",
"joined": "Приєднався",
"loading": "Завантаження",
"loadMore": "Показати більше",
"notConnected": "Немає зв'язку",
"connectionRequired": "Ти повинен під'єднати свій обліковий запис, щоб приєднатися до серверів.",
"connectionError": "Помилка підключення",
"connectionErrorMessage": "Виникла помилка при підключенні до DiscordServers.com. Можливо, їхній сайт/api не працює. Будь ласка, спробуй ще раз пізніше.",
"pagination": "{{page}} сторінка із {{count}}",
"search": "Пошук",
"connect": "Приєднатися",
"reconnect": "Приєднатися знову",
"categories": "Категорії",
"keywords": "Ключові слова",
"connection": "Підключений як: {{username}}#{{discriminator}}",
"results": "Відображення {{start}}-{{end}} із {{total}} результатів у {{category}}",
"query": "для {{query}}"
},
"Modals": {
"confirmAction": "Ти впевнений?",
"okay": "Так",
"done": "Готово",
"cancel": "Скасуват<EFBFBD><EFBFBD>",
"cancel": "Скасувати",
"nevermind": "Не звертати уваги",
"close": "Закрити",
"name": "Ім'я",

View File

@ -13,10 +13,6 @@
"name": "表情系统",
"note": "启用 BetterDiscord 表情系统"
},
"publicServers": {
"name": "公共服务器",
"note": "显示公共服务器按钮"
},
"voiceDisconnect": {
"name": "自动断开语音",
"note": "自动在关闭 Discord 时断开语音"
@ -220,27 +216,6 @@
"downloadFailed": "下载失败",
"failureMessage": "BetterDiscord 无法下载表情,请检查你的网络连接和防火墙。"
},
"PublicServers": {
"button": "公共",
"join": "加入",
"joining": "加入中",
"joined": "已加入",
"loading": "加载中",
"loadMore": "加载更多",
"notConnected": "尚未连接",
"connectionRequired": "你必须先连接你的账号才能加入公共服务器",
"connectionError": "连接错误",
"connectionErrorMessage": "连接到D iscordServers.com 时发生错误,可能是他们的网站/接口暂时不可用。请稍后再试。",
"pagination": "共{{count}}页中的第{{page}}页",
"search": "搜索",
"connect": "连接",
"reconnect": "重新连接",
"categories": "分类",
"keywords": "关键词",
"connection": "使用 {{username}}#{{discriminator}} 进行连接",
"results": "显示{{category}}分类下的{{total}}个结果,当前正在显示{{start}}-{{end}}个结果",
"query": "为了 {{query}}"
},
"Modals": {
"confirmAction": "你确定吗?",
"okay": "确定",

View File

@ -1,95 +1,92 @@
{
"Panels": {
"plugins": "附加元件",
"plugins": "擴充功能",
"themes": "佈景主題",
"customcss": "客製化樣式"
"customcss": "客製化 CSS",
"updates": "更新"
},
"Collections": {
"settings": {
"name": "設",
"name": "設",
"general": {
"name": "一般",
"emotes": {
"name": "表情系統",
"note": "啟用 BetterDiscord 表情系統"
},
"publicServers": {
"name": "公共伺服器",
"note": "顯示公共伺服器按鈕"
"name": "表情符號系統",
"note": "啟用 BetterDiscord 的表情符號系統"
},
"voiceDisconnect": {
"name": "自動語音",
"note": "自動在關閉 Discord 時語音"
"name": "自動中斷語音連線",
"note": "自動在關閉 Discord 時中斷語音連線"
},
"showToasts": {
"name": "顯示彈出提示",
"note": "在有重要通知時彈出提示"
},
"mediaKeys": {
"name": "用媒體控制鍵",
"name": "用媒體控制鍵",
"note": "防止 Discord 在播放影片時劫持媒體控制鍵"
}
},
"window": {
"removeMinimumSize": {
"name": "移除最小化限制",
"note": "移除 Discord 限制的最小尺寸940x500"
"note": "移除 Discord 限制的最小化視窗尺寸940x500"
},
"name": "窗口設置",
"name": "視窗設定",
"transparency": {
"name": "透明化",
"note": "允許主窗口透明(需要重啟以生效)"
"note": "啟用主視窗透明(需要重新啟動以生效)"
},
"frame": {
"name": "窗口框",
"note": "向主窗口添加原生系統窗口框"
"name": "視窗邊框",
"note": "向主視窗新增原版系統視窗邊框"
}
},
"addons": {
"name": "附加元件管理",
"name": "擴充功能管理員",
"addonErrors": {
"name": "顯示附加元件錯誤",
"note": "在附加元件/佈景主題發生錯誤時顯示彈窗"
"name": "顯示擴充功能錯誤",
"note": "在擴充功能/佈景主題發生錯誤時顯示彈出視窗"
},
"editAction": {
"name": "編輯器",
"note": "在何處编辑附加元件及佈景主題",
"note": "在何處編輯擴充功能及佈景主題",
"options": {
"detached": "獨立",
"detached": "獨立窗",
"system": "系統編輯器"
}
}
},
"customcss": {
"name": "客製化樣式",
"name": "客製化 CSS",
"customcss": {
"name": "客製化樣式",
"note": "啟用客製化樣式選項卡"
"name": "客製化 CSS",
"note": "啟用客製化 CSS 選項標籤"
},
"liveUpdate": {
"name": "更新",
"note": "自動在 CSS 發生變更時重新載"
"name": "實時更新",
"note": "自動在 CSS 發生變更時重新"
},
"startDetached": {
"name": "在獨立開",
"note": "在獨立窗口中編輯客製化樣式"
"name": "在獨立窗中開",
"note": "在獨立視窗中編輯客製化 CSS"
},
"nativeOpen": {
"name": "在系統編輯器中開",
"note": "在系統編輯器中編輯客製化樣式"
"name": "在系統編輯器中",
"note": "在系統編輯器中編輯客製化 CSS"
},
"openAction": {
"name": "編輯器",
"note": "在何處編輯客製化樣式",
"note": "在何處編輯客製化 CSS",
"options": {
"settings": "設置菜單",
"detached": "獨立",
"settings": "設定選單",
"detached": "獨立窗",
"system": "系統編輯器"
}
}
},
"developer": {
"name": "開發人員設",
"name": "開發人員設",
"debuggerHotkey": {
"name": "調試器快捷鍵",
"note": "按下 F8 時啟動調試器"
@ -99,42 +96,78 @@
"note": "向 Discord 注入 React 開發人員工具"
},
"inspectElement": {
"name": "元素檢快捷鍵",
"note": "啟用在大多數瀏覽器中常用的元素檢快捷鍵Ctrl + Shift + C"
"name": "元素檢快捷鍵",
"note": "啟用在大多數瀏覽器中常用的元素檢快捷鍵Ctrl + Shift + C"
},
"devToolsWarning": {
"name": "屏蔽開發人員工具警告",
"name": "隱藏開發人員工具警告",
"note": "禁止 Discord 輸出「Hold Up!」警告"
},
"debugLogs": {
"name": "調試日誌",
"note": "輸出控制台中的所有信息到 BetterDiscord 資料夾下的 debug.log 檔案"
"name": "調試記錄檔",
"note": "輸出控制台中的所有資訊到 BetterDiscord 資料夾下的 debug.log"
},
"devTools": {
"name": "DevTools",
"note": "啟用切換 DevToolsCtrl+Shift+I"
}
},
"editor": {
"name": "編輯器偏好",
"lineNumbers": {
"name": "行數編號",
"note": "啟用在編輯器一側顯示行數"
},
"fontSize": {
"name": "字型大小",
"note": "在編輯器中使用的字體大小(點)"
},
"minimap": {
"name": "小地圖",
"note": "啟用在編輯器一側顯示代碼小地圖"
},
"hover": {
"name": "參考工具提示",
"note": "允許在懸停規則和選擇器時顯示參考工具提示"
},
"quickSuggestions": {
"name": "快速建議",
"note": "啟用在您輸入時顯示自動完成建議"
},
"renderWhitespace": {
"name": "顯示空格",
"note": "編輯器何時應顯示空格",
"options": {
"all": "總是",
"none": "永不",
"selection": "選擇"
}
}
}
},
"emotes": {
"name": "表情",
"name": "表情符號",
"general": {
"name": "一般",
"download": {
"name": "下載表情",
"note": "自動下載即將過期的表情"
"name": "下載表情符號",
"note": "自動下載即將過期的表情符號"
},
"emoteMenu": {
"name": "表情菜單",
"note": "在表情菜單中顯示 Twitch/我的最愛 表情"
"name": "表情符號選單",
"note": "在表情符號選單中顯示 Twitch/我的最愛 表情符號"
},
"hideEmojiMenu": {
"name": "隐藏表情符號菜单",
"note": "在使用表情表情時隐藏 Discord 的表情符號菜單"
"name": "隱藏表情符號選單",
"note": "在使用表情符號時隱藏 Discord 的表情符號選單"
},
"autoCaps": {
"name": "表情自動大寫",
"note": "自動大寫表情指令"
"name": "表情符號自動大寫",
"note": "自動大寫表情符號指令"
},
"modifiers": {
"name": "顯示表情修飾符",
"note": "啟用表情修飾符flip, spin, pulse, spin2, spin3, 1spin, 2spin, 3spin, tr, bl, br, shake, shake2, shake3, flap"
"note": "啟用表情修飾符flip、spin、pulse、spin2、spin3、1spin、2spin、3spin、tr、bl、br、shake、shake2、shake3、flap"
},
"animateOnHover": {
"name": "懸停動畫",
@ -144,8 +177,8 @@
"categories": {
"name": "分類",
"twitchglobal": {
"name": "Twitch 全",
"note": "顯示 Twitch 全表情"
"name": "Twitch 全",
"note": "顯示 Twitch 全表情"
},
"twitchsubscriber": {
"name": "Twitch 訂閱",
@ -163,14 +196,14 @@
}
},
"Addons": {
"title": "{{name}} 版本{{version}} 作者:{{author}}",
"byline": "作者 {{author}}",
"openFolder": "開啟{{type}}資料夾",
"reload": "重新載",
"addonSettings": "設",
"title": "{{name}} 版本 {{version}} 作者 {{author}}",
"byline": "作者 {{author}}",
"openFolder": "開啟 {{type}} 資料夾",
"reload": "重新",
"addonSettings": "設",
"website": "網站",
"source": "源碼",
"invite": "支持器",
"source": "碼",
"invite": "支持服器",
"donate": "捐贈",
"patreon": "Patreon",
"name": "名稱",
@ -178,89 +211,70 @@
"version": "版本",
"added": "安裝時間",
"modified": "最後更新時間",
"search": "搜{{type}}",
"search": "搜{{type}}",
"editAddon": "編輯",
"deleteAddon": "刪除",
"confirmDelete": "你真的要刪除{{name}}嗎?",
"confirmationText": "你还有未保存的更改,關閉此窗口將失去所有對{{name}}所做的更改。",
"enabled": "已啟用{{name}}",
"disabled": "已禁用{{name}}",
"couldNotEnable": "無法啟用{{name}}。",
"couldNotDisable": "無法禁用{{name}}。",
"couldNotStart": "無法启动{{name}}。",
"couldNotStop": "無法停止{{name}}。",
"settingsError": "無法打开{{name}}设置",
"methodError": "無法调用{{method}}。",
"confirmDelete": "您真的要刪除 {{name}} 嗎?",
"confirmationText": "您還有未儲存的更改,關閉此視窗將失去所有對 {{name}} 所做的更改。",
"enabled": "已啟用 {{name}}",
"disabled": "已停用 {{name}}。",
"couldNotEnable": "無法啟用 {{name}}。",
"couldNotDisable": "無法停用 {{name}}。",
"couldNotStart": "無法啟動 {{name}}。",
"couldNotStop": "無法停止 {{name}}。",
"settingsError": "無法開啟設定 {{name}}。",
"methodError": "無法調用 {{method}}。",
"unknownAuthor": "未知的作者",
"noDescription": "作者没有留下任何描述",
"alreadyExists": "已經存在相同名稱「{{name}}」的{{type}}了",
"alreadWatching": "已經在監聽附加元件了",
"metaError": "無法解析元數據",
"missingNameData": "元數據缺少名稱。",
"metaNotFound": "無法找到元數據",
"noDescription": "作者没有留下任何描述",
"alreadyExists": "已存在相同名稱 {{name}} 的 {{type}}。",
"alreadWatching": "已在監聽擴充功能了。",
"metaError": "無法解析元資料。",
"missingNameData": "元資料缺少名稱。",
"metaNotFound": "無法找到元資料。",
"compileError": "無法編譯。",
"wasUnloaded": "已卸載{{name}}。",
"blankSlateHeader": "你还未添加任何{{type}}",
"blankSlateMessage": "你可以在[此网站]({{link}})中獲取,並將其添加到你的{{type}}資料夾中。"
"wasUnloaded": "已解除安裝 {{name}}。",
"blankSlateHeader": "您還未新增任何 {{type}}",
"blankSlateMessage": "您可以在[此網站]{{link}})中獲得,並將其新增到您的 {{type}} 資料夾中。",
"isEnabled": "已啟用",
"wasLoaded": "已載入 {{name}} 版本 {{version}}。"
},
"CustomCSS": {
"confirmationText": "你还有未保存的更改,關閉此窗口將失去所有對客製化樣式所做的更改。",
"confirmationText": "您還有未儲存的更改,關閉此視窗將失去所有對客製化 CSS 所做的更改。",
"update": "更新",
"save": "存",
"openNative": "在系統編輯器中開",
"openDetached": "在獨立開",
"settings": "編輯器設",
"editorTitle": "客製化樣式編輯器"
"save": "存",
"openNative": "在系統編輯器中",
"openDetached": "在獨立窗中開",
"settings": "編輯器設",
"editorTitle": "客製化 CSS 編輯器"
},
"Emotes": {
"loading": "在後台加載表情時不會重新加載。",
"loaded": "所有表情均已成功載。",
"clearEmotes": "清除表情數據",
"favoriteAction": "添加到我的最愛",
"loading": "在背景載入表情時不會重新載入。",
"loaded": "所有表情均已成功。",
"clearEmotes": "清除表情資料",
"favoriteAction": "新增到我的最愛",
"downloadFailed": "下載失敗",
"failureMessage": "BetterDiscord 無法下載表情,請檢查你的網絡連接和防火牆。"
},
"PublicServers": {
"button": "公共",
"join": "加入",
"joining": "加入中",
"joined": "已加入",
"loading": "加載中",
"loadMore": "加載更多",
"notConnected": "尚未連接",
"connectionRequired": "你必須先連接你的賬號才能加入公共伺服器",
"connectionError": "連接錯誤",
"connectionErrorMessage": "連接到 DiscordServers.com 時發生錯誤,可能是他们的網站/接口暫時不可用。請稍後再試。",
"pagination": "共{{count}}頁中的第{{page}}頁",
"search": "搜索",
"connect": "連接",
"reconnect": "重新連接",
"categories": "分類",
"keywords": "關鍵詞",
"connection": "使用 {{username}}#{{discriminator}} 進行連接",
"results": "線上{{category}}分類下的{{total}}個結果,當前正在線上{{start}}-{{end}}個結果",
"query": "為了 {{query}}"
"failureMessage": "BetterDiscord 無法下載表情,請檢查您的網絡連線和防火牆。"
},
"Modals": {
"confirmAction": "確定嗎?",
"confirmAction": "您確定嗎?",
"okay": "確定",
"done": "完成",
"cancel": "取消",
"nevermind": "別在意",
"close": "關閉",
"name": "名稱",
"message": "息",
"message": "訊息",
"error": "錯誤",
"addonErrors": "附加元件錯誤",
"restartRequired": "需要重啟以生效",
"restartNow": "立即重啟",
"restartLater": "稍候自行重啟",
"additionalInfo": "額外信息",
"restartPrompt": "Discord 需要重啟以應用更新。立刻進行重啟嗎?"
"addonErrors": "擴充功能錯誤",
"restartRequired": "需要重新啟動以生效",
"restartNow": "立即重新啟動",
"restartLater": "稍後自行重新啟動",
"additionalInfo": "額外資訊",
"restartPrompt": "Discord 需要重新啟動以套用更新。立刻進行重新啟動嗎?"
},
"ReactDevTools": {
"notFound": "未找到框架",
"notFoundDetails": "無法在你的電腦上找到 React 開發者工具擴充套件。請在本地的 Chrome 上安裝該擴充套件。"
"notFoundDetails": "無法在您的電腦上找到 React 開發者工具擴充套件。請在本機的 Chrome 上安裝該擴充套件。"
},
"Sorting": {
"sortBy": "排序方式",
@ -269,7 +283,26 @@
"descending": "倒序"
},
"WindowPrefs": {
"enabledInfo": "此選項需要透明佈景主題才能正常工作。在 Windows 上,這可能會破壞 AERO 的捕捉和最大化。\n\n該變更需要重啟以生效。你要立即進行重啟嗎",
"disabledInfo": "Discord 需要重啟以應用最新變更。立刻進行重啟嗎?"
"enabledInfo": "此選項需要透明佈景主題才能正常工作。在 Windows 上,這可能會破壞 Aero 的捕捉和最大化。\\n\\n該變更需要重新啟動以生效。您要立即進行重新啟動嗎",
"disabledInfo": "Discord 需要重新啟動以套用最新變更。立刻進行重新啟動嗎?"
},
"Notices": {
"moreInfo": "更多資訊"
},
"Updater": {
"updateFailed": "更新失敗!",
"updateFailedMessage": "BetterDiscord 更新失敗。請從我們的網站 (https://betterdiscord.app/) 下載最新版本的安裝程式並重新安裝。",
"updateSuccessful": "更新成功!",
"updateAvailable": "BetterDiscord 已有新的版本({{version}}",
"addonUpdatesAvailable": "BetterDiscord 已為您 {{type}} 中的 {{count}} 找到更新!",
"addonUpdated": "{{name}} 已更新到版本 {{version}}",
"checking": "正在檢查更新!",
"finishedChecking": "已完成檢查更新!",
"checkForUpdates": "檢查更新!",
"updateAll": "全部更新!",
"noUpdatesAvailable": "沒有可供使用的更新。",
"versionAvailable": "版本 {{version}} 已可供使用!",
"upToDateBlankslate": "您所有的 {{type}} 似乎都是最新的!",
"updateButton": "更新!"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "betterdiscord",
"version": "1.8.5",
"version": "1.9.2",
"description": "Enhances Discord by adding functionality and themes.",
"main": "src/index.js",
"scripts": {
@ -21,6 +21,7 @@
"dotenv": "^16.0.3",
"eslint": "^8.23.0",
"eslint-plugin-react": "^7.31.6",
"eslint-plugin-react-hooks": "^4.6.0",
"mocha": "^10.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"

View File

@ -8,6 +8,7 @@ importers:
dotenv: ^16.0.3
eslint: ^8.23.0
eslint-plugin-react: ^7.31.6
eslint-plugin-react-hooks: ^4.6.0
mocha: ^10.0.0
webpack: ^5.74.0
webpack-cli: ^4.10.0
@ -16,6 +17,7 @@ importers:
dotenv: 16.0.3
eslint: 8.23.0
eslint-plugin-react: 7.31.6_eslint@8.23.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.23.0
mocha: 10.0.0
webpack: 5.74.0_webpack-cli@4.10.0
webpack-cli: 4.10.0_webpack@5.74.0
@ -2343,6 +2345,15 @@ packages:
engines: {node: '>=10'}
dev: true
/eslint-plugin-react-hooks/4.6.0_eslint@8.23.0:
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
engines: {node: '>=10'}
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
dependencies:
eslint: 8.23.0
dev: true
/eslint-plugin-react/7.31.6_eslint@8.23.0:
resolution: {integrity: sha512-CXu4eu28sb8Sd2+cyUYsJVyDvpTlaXPG+bOzzpS9IzZKtye96AYX3ZmHQ6ayn/OAIQ/ufDJP8ElPWd63Pepn9w==}
engines: {node: '>=4'}

View File

@ -1,7 +1,11 @@
{
"extends": ["plugin:react/recommended"],
"extends": [
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"plugins": [
"react"
"react",
"react-hooks"
],
"settings": {
"react": {

View File

@ -2,7 +2,6 @@
export {default as CustomCSS} from "./customcss";
export {default as PublicServers} from "./general/publicservers";
export {default as VoiceDisconnect} from "./general/voicedisconnect";
export {default as MediaKeys} from "./general/mediakeys";

View File

@ -1,138 +0,0 @@
import Builtin from "../../structs/builtin";
import {DiscordModules, WebpackModules, Strings, DOMManager, React, ReactDOM} from "modules";
import PublicServersMenu from "../../ui/publicservers/menu";
import Globe from "../../ui/icons/globe";
const LayerManager = {
pushLayer(component) {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
},
popLayer() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
},
popAllLayers() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
}
};
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {hasError: false};
}
componentDidCatch() {
this.setState({hasError: true});
}
render() {
if (this.state.hasError) return null;
return this.props.children;
}
}
export default new class PublicServers extends Builtin {
get name() {return "PublicServers";}
get category() {return "general";}
get id() {return "publicServers";}
enabled() {
const PrivateChannelList = WebpackModules.getModule(m => m?.toString().includes("privateChannelIds"), {defaultExport: false});
if (!PrivateChannelList || !PrivateChannelList.Z) return this.warn("Could not find PrivateChannelList", PrivateChannelList);
const PrivateChannelButton = WebpackModules.getModule(m => m?.prototype?.render?.toString().includes("linkButton"), {searchExports: true});
if (!PrivateChannelButton) return this.warn("Could not find PrivateChannelButton", PrivateChannelButton);
this.after(PrivateChannelList, "Z", (_, __, returnValue) => {
const destination = returnValue?.props?.children?.props?.children;
if (!destination || !Array.isArray(destination)) return;
if (destination.find(b => b?.props?.children?.props?.id === "public-servers-button")) return; // If it exists, don't try to add again
destination.push(
React.createElement(ErrorBoundary, null,
React.createElement(PrivateChannelButton,
{
id: "public-servers-button",
onClick: () => this.openPublicServers(),
text: Strings.PublicServers.button,
icon: () => React.createElement(Globe, {color: "currentColor"})
}
)
)
);
});
/**
* On being first enabled, we have no way of forceUpdating the list,
* so clone and modify an existing button and add it to the end
* of the button list.
*/
const header = document.querySelector(`[class*="privateChannelsHeaderContainer-"]`);
if (!header) return; // No known element
const oldButton = header.previousElementSibling;
if (!oldButton.className.includes("channel-")) return; // Not what we expected to be there
// Clone existing button and set click handler
const newButton = oldButton.cloneNode(true);
newButton.addEventListener("click", (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
this.openPublicServers();
});
// Remove existing route and id
const aSlot = newButton.querySelector("a");
aSlot.href = "";
aSlot.dataset.listItemId = "public-servers";
// Remove any badges
const premiumBadge = newButton.querySelector(`[class*="premiumTrial"]`);
premiumBadge?.remove?.();
const numberBadge = newButton.querySelector(`[class*="numberBadge-"]`);
numberBadge?.remove?.();
// Render our icon in the avatar slot
const avatarSlot = newButton.querySelector(`[class*="avatar-"]`);
avatarSlot.replaceChildren();
ReactDOM.render(React.createElement(Globe, {color: "currentColor"}), avatarSlot);
DOMManager.onRemoved(avatarSlot, () => ReactDOM.unmountComponentAtNode(avatarSlot));
// Replace the existing name
const nameSlot = newButton.querySelector(`[class*="name-"]`);
nameSlot.textContent = Strings.PublicServers.button;
// Insert before the header, end of the list
header.parentNode.insertBefore(newButton, header);
this.button = newButton;
}
disabled() {
this.unpatchAll();
this.button?.remove?.();
document.querySelector("#public-servers-button")?.parentElement?.parentElement?.remove?.();
}
async _appendButton() {
await new Promise(r => setTimeout(r, 1000));
const existing = document.querySelector("#bd-pub-li");
if (existing) return;
const guilds = document.querySelector(`.${DiscordModules.GuildClasses.guilds} .${DiscordModules.GuildClasses.listItem}`);
if (!guilds) return;
guilds.parentNode.insertBefore(this.button, guilds.nextSibling);
}
openPublicServers() {
LayerManager.pushLayer(() => DiscordModules.React.createElement(PublicServersMenu, {close: LayerManager.popLayer}));
}
};

View File

@ -1,25 +1,20 @@
// fixed, improved, added, progress
export default {
description: "Discord changed a lot of things internally once again! Please have patience while plugins and even themes catch up!",
description: "Hotfix!",
changes: [
{
title: "What's New?",
type: "improved",
items: [
"Added support for a custom version of React DevTools. (Thanks @Zerthox)",
"We are now using a custom header component in settings to prevent future crashes.",
"Plugins now have a new experimental API for component access, `BdApi.Components`. Currently only `Tooltip` exists."
]
},
// {
// title: "What's New?",
// type: "improved",
// items: [
// "Added SourceURL for the renderer. This makes it easier for developers to identify BD in call stacks.",
// ]
// },
{
title: "Bug Fixes",
type: "fixed",
items: [
"Fixed crashing when opening settings.",
"Fixed modals either not opening and/or crashing.",
"Fixed context menus not working and/or crashing.",
"Fixed coloring for `danger` context menus. (Thanks @samfundev)"
"Fixed context menu crashes & api",
]
}
]
};
};

View File

@ -4,7 +4,6 @@ export default [
id: "general",
collapsible: true,
settings: [
{type: "switch", id: "publicServers", value: true},
{type: "switch", id: "voiceDisconnect", value: false},
{type: "switch", id: "showToasts", value: true},
{type: "switch", id: "mediaKeys", value: false}

View File

@ -14,39 +14,39 @@
get folder() {return this.#manager.addonFolder;}
/**
* Determines if a particular adon is enabled.
* @param {string} idOrFile Addon id or filename.
* Determines if a particular addon is enabled.
* @param {string} idOrFile Addon ID or filename
* @returns {boolean}
*/
isEnabled(idOrFile) {return this.#manager.isEnabled(idOrFile);}
/**
* Enables the given addon.
* @param {string} idOrFile Addon id or filename.
* @param {string} idOrFile Addon ID or filename
*/
enable(idOrAddon) {return this.#manager.enableAddon(idOrAddon);}
/**
* Disables the given addon.
* @param {string} idOrFile Addon id or filename.
* @param {string} idOrFile Addon ID or filename
*/
disable(idOrAddon) {return this.#manager.disableAddon(idOrAddon);}
/**
* Toggles if a particular addon is enabled.
* @param {string} idOrFile Addon id or filename.
* @param {string} idOrFile Addon ID or filename
*/
toggle(idOrAddon) {return this.#manager.toggleAddon(idOrAddon);}
/**
* Reloads if a particular addon is enabled.
* @param {string} idOrFile Addon id or filename.
* @param {string} idOrFile Addon ID or filename
*/
reload(idOrFileOrAddon) {return this.#manager.reloadAddon(idOrFileOrAddon);}
/**
* Gets a particular addon.
* @param {string} idOrFile Addon id or filename.
* @param {string} idOrFile Addon ID or filename
* @returns {object} Addon instance
*/
get(idOrFile) {return this.#manager.getAddon(idOrFile);}

View File

@ -5,43 +5,18 @@ import {React} from "../modules";
let startupComplete = false;
const MenuComponents = (() => {
const out = {};
const componentMap = {
separator: "Separator",
checkbox: "CheckboxItem",
radio: "RadioItem",
control: "ControlItem",
groupstart: "Group",
customitem: "Item"
};
const ModulesBundle = WebpackModules.getByProps("MenuItem", "Menu");
const MenuComponents = {
Separator: ModulesBundle?.MenuSeparator,
CheckboxItem: ModulesBundle?.MenuCheckboxItem,
RadioItem: ModulesBundle?.MenuRadioItem,
ControlItem: ModulesBundle?.MenuControlItem,
Group: ModulesBundle?.MenuGroup,
Item: ModulesBundle?.MenuItem,
Menu: ModulesBundle?.Menu,
};
// exportKey:()=>identifier
const getExportIdentifier = (string, id) => new RegExp(`(\\w+):\\(\\)=>${id}`).exec(string)?.[1];
try {
let contextMenuId = Object.keys(WebpackModules.require.m).find(e => WebpackModules.require.m[e]?.toString().includes("menuitemcheckbox"));
const ContextMenuModule = WebpackModules.getModule((m, t, id) => id === contextMenuId);
const rawMatches = WebpackModules.require.m[contextMenuId].toString().matchAll(/if\(\w+\.type===(\w+)\)[\s\S]+?type:"(.+?)"/g);
const moduleString = WebpackModules.require.m[contextMenuId].toString();
out.Menu = Object.values(ContextMenuModule).find(v => v.toString().includes(".isUsingKeyboardNavigation"));
for (const [, identifier, type] of rawMatches) {
out[componentMap[type]] = ContextMenuModule[getExportIdentifier(moduleString, identifier)];
}
startupComplete = Object.values(componentMap).every(k => out[k]) && !!out.Menu;
} catch (error) {
startupComplete = false;
Logger.stacktrace("ContextMenu~Components", "Fatal startup error:", error);
Object.assign(out, Object.fromEntries(
Object.values(componentMap).map(k => [k, () => null])
));
}
return out;
})();
startupComplete = Object.values(MenuComponents).every(v => v);
const ContextMenuActions = (() => {
const out = {};
@ -178,9 +153,9 @@ class ContextMenu {
/**
* Allows you to patch a given context menu. Acts as a wrapper around the `Patcher`.
*
* @param {string} navId Discord's internal navId used to identify context menus
* @param {function} callback callback function that accepts the react render tree
* @returns {function} a function that automatically unpatches
* @param {string} navId Discord's internal `navId` used to identify context menus
* @param {function} callback Callback function that accepts the React render tree
* @returns {function} A function that automatically unpatches
*/
patch(navId, callback) {
MenuPatcher.patch(navId, callback);
@ -191,8 +166,8 @@ class ContextMenu {
/**
* Allows you to remove the patch added to a given context menu.
*
* @param {string} navId the original navId from patching
* @param {function} callback the original callback from patching
* @param {string} navId The original `navId` from patching
* @param {function} callback The original callback from patching
*/
unpatch(navId, callback) {
MenuPatcher.unpatch(navId, callback);
@ -203,9 +178,9 @@ class ContextMenu {
* match the actual component being built. View those to see what options exist
* for each, they often have less in common than you might think.
*
* @param {object} props - props used to build the item
* @param {string} [props.type="text"] - type of the item, options: text, submenu, toggle, radio, custom, separator
* @returns {object} the created component
* @param {object} props Props used to build the item
* @param {string} [props.type="text"] Type of the item, options: text, submenu, toggle, radio, custom, separator
* @returns {object} The created component
*
* @example
* // Creates a single menu item that prints "MENU ITEM" on click
@ -262,8 +237,9 @@ class ContextMenu {
* Creates the all the items **and groups** of a context menu recursively.
* There is no hard limit to the number of groups within groups or number
* of items in a menu.
* @param {Array<object>} setup - array of item props used to build items. See {@link ContextMenu.buildItem}
* @returns {Array<object>} array of the created component
*
* @param {Array<object>} setup Array of item props used to build items. See {@link ContextMenu.buildItem}.
* @returns {Array<object>} Array of the created component
*
* @example
* // Creates a single item group item with a toggle item
@ -317,8 +293,9 @@ class ContextMenu {
* Creates the menu *component* including the wrapping `ContextMenu`.
* Calls {@link ContextMenu.buildMenuChildren} under the covers.
* Used to call in combination with {@link ContextMenu.open}.
* @param {Array<object>} setup - array of item props used to build items. See {@link ContextMenu.buildMenuChildren}
* @returns {function} the unique context menu component
*
* @param {Array<object>} setup Array of item props used to build items. See {@link ContextMenu.buildMenuChildren}.
* @returns {function} The unique context menu component
*/
buildMenu(setup) {
return (props) => {return React.createElement(MenuComponents.Menu, props, this.buildMenuChildren(setup));};
@ -327,13 +304,12 @@ class ContextMenu {
/**
* Function that allows you to open an entire context menu. Recommended to build the menu with this module.
*
* @param {MouseEvent} event - The context menu event. This can be emulated, requires target, and all X, Y locations.
* @param {function} menuComponent - Component to render. This can be any react component or output of {@link ContextMenu.buildMenu}
* @param {object} config - configuration/props for the context menu
* @param {string} [config.position="right"] - default position for the menu, options: "left", "right"
* @param {string} [config.align="top"] - default alignment for the menu, options: "bottom", "top"
* @param {function} [config.onClose] - function to run when the menu is closed
* @param {boolean} [config.noBlurEvent=false] - No clue
* @param {MouseEvent} event The context menu event. This can be emulated, requires target, and all X, Y locations.
* @param {function} menuComponent Component to render. This can be any React component or output of {@link ContextMenu.buildMenu}.
* @param {object} config Configuration/props for the context menu
* @param {string} [config.position="right"] Default position for the menu, options: "left", "right"
* @param {string} [config.align="top"] Default alignment for the menu, options: "bottom", "top"
* @param {function} [config.onClose] Function to run when the menu is closed
*/
open(event, menuComponent, config) {
return ContextMenuActions.openContextMenu(event, function(e) {

View File

@ -47,10 +47,10 @@ class Data {
}
/**
* Deletes a piece of stored data, this is different than saving as null or undefined.
* Deletes a piece of stored data. This is different than saving `null` or `undefined`.
*
* @param {string} pluginName Name of the plugin deleting data
* @param {string} key Which piece of data to delete
* @param {string} key Which piece of data to delete.
*/
delete(pluginName, key) {
if (this.#callerName) {

View File

@ -49,7 +49,7 @@ class DOM {
/**
* Removes a `<style>` from the document corresponding to the given ID.
*
* @param {string} id ID uses for the style element
* @param {string} id ID used for the style element
*/
removeStyle(id) {
if (this.#callerName && arguments.length === 1) {
@ -66,18 +66,19 @@ class DOM {
* Adds a listener for when the node is removed from the document body.
*
* @param {HTMLElement} node Node to be observed
* @param {function} callback Function to run when fired
* @param {function} callback Function to run when removed
*/
onRemoved(node, callback) {
return DOMManager.onRemoved(node, callback);
}
/**
* Utility to help smoothly animate using JavaScript
* Utility to help smoothly animate using JavaScript.
*
* @param {function} update render function indicating the style should be updates
* @param {number} duration duration in ms to animate for
* @param {object} [options] option to customize the animation
* @param {function} update Render function indicating the style should be updated
* @param {number} duration Duration in ms to animate for
* @param {object} [options] Options to customize the animation
* @param {function} [options.timing] Optional function calculating progress based on current time fraction. Linear by default.
*/
animate(update, duration, options = {}) {
return DOMManager.animate({update, duration, timing: options.timing});
@ -88,12 +89,12 @@ class DOM {
* to `React.createElement`
*
* @param {string} tag HTML tag name to create
* @param {object} [options] options object to customize the element
* @param {string} [options.className] class name to add to the element
* @param {string} [options.id] id to set for the element
* @param {HTMLElement} [options.target] target element to automatically append to
* @param {HTMLElement} [child] child node to add
* @returns HTMLElement
* @param {object} [options] Options object to customize the element
* @param {string} [options.className] Class name to add to the element
* @param {string} [options.id] ID to set for the element
* @param {HTMLElement} [options.target] Target element to automatically append to
* @param {HTMLElement} [child] Child node to add
* @returns {HTMLElement} The created HTML element
*/
createElement(tag, options = {}, child = null) {
return DOMManager.createElement(tag, options, child);
@ -106,9 +107,10 @@ class DOM {
*
* If the second parameter is false, then the return value will be the list of parsed
* nodes and there were multiple top level nodes, otherwise the single node is returned.
* @param {string} html - HTML to be parsed
* @param {boolean} [fragment=false] - Whether or not the return should be the raw `DocumentFragment`
* @returns {(DocumentFragment|NodeList|HTMLElement)} - The result of HTML parsing
*
* @param {string} html HTML to be parsed
* @param {boolean} [fragment=false] Whether or not the return should be the raw `DocumentFragment`
* @returns {(DocumentFragment|NodeList|HTMLElement)} The result of HTML parsing
*/
parseHTML(html, fragment = false) {
return DOMManager.parseHTML(html, fragment);

View File

@ -117,7 +117,7 @@ BdApi.Utils = Utils;
BdApi.DOM = DOMAPI;
/**
* An instance of {@link ContextMenu} for interacting with context menus
* An instance of {@link ContextMenu} for interacting with context menus.
* @type ContextMenu
*/
BdApi.ContextMenu = ContextMenuAPI;

View File

@ -100,8 +100,8 @@ function unlinkJS(id) {
* Shows a generic but very customizable modal.
*
* @deprecated
* @param {string} title title of the modal
* @param {(string|ReactElement|Array<string|ReactElement>)} content a string of text to display in the modal
* @param {string} title Title of the modal
* @param {(string|ReactElement|Array<string|ReactElement>)} content A string of text to display in the modal
* @memberof BdApi
*/
function alert(title, content) {
@ -112,14 +112,15 @@ function alert(title, content) {
* Shows a generic but very customizable confirmation modal with optional confirm and cancel callbacks.
*
* @deprecated
* @param {string} title title of the modal
* @param {(string|ReactElement|Array<string|ReactElement>)} children a single or mixed array of react elements and strings. Everything is wrapped in Discord's `TextElement` component so strings will show and render properly.
* @param {object} [options] options to modify the modal
* @param {boolean} [options.danger=false] whether the main button should be red or not
* @param {string} [options.confirmText=Okay] text for the confirmation/submit button
* @param {string} [options.cancelText=Cancel] text for the cancel button
* @param {callable} [options.onConfirm=NOOP] callback to occur when clicking the submit button
* @param {callable} [options.onCancel=NOOP] callback to occur when clicking the cancel button
* @param {string} title Title of the modal
* @param {(string|ReactElement|Array<string|ReactElement>)} children Single or mixed array of React elements and strings. Everything is wrapped in Discord's `TextElement` component so strings will show and render properly.
* @param {object} [options] Options to modify the modal
* @param {boolean} [options.danger=false] Whether the main button should be red or not
* @param {string} [options.confirmText=Okay] Text for the confirmation/submit button
* @param {string} [options.cancelText=Cancel] Text for the cancel button
* @param {callable} [options.onConfirm=NOOP] Callback to occur when clicking the submit button
* @param {callable} [options.onCancel=NOOP] Callback to occur when clicking the cancel button
* @returns {string} The key used for this modal
* @memberof BdApi
*/
function showConfirmationModal(title, content, options = {}) {
@ -130,12 +131,12 @@ function showConfirmationModal(title, content, options = {}) {
* Shows a toast similar to android towards the bottom of the screen.
*
* @deprecated
* @param {string} content The string to show in the toast.
* @param {object} options Options object. Optional parameter.
* @param {string} [options.type=""] Changes the type of the toast stylistically and semantically. Choices: "", "info", "success", "danger"/"error", "warning"/"warn". Default: ""
* @param {boolean} [options.icon=true] Determines whether the icon should show corresponding to the type. A toast without type will always have no icon. Default: `true`
* @param {number} [options.timeout=3000] Adjusts the time (in ms) the toast should be shown for before disappearing automatically. Default: `3000`
* @param {boolean} [options.forceShow=false] Whether to force showing the toast and ignore the bd setting
* @param {string} content The string to show in the toast
* @param {object} options Options object. Optional parameter
* @param {string} [options.type=""] Changes the type of the toast stylistically and semantically. Choices: "", "info", "success", "danger"/"error", "warning"/"warn". Default: "".
* @param {boolean} [options.icon=true] Determines whether the icon should show corresponding to the type. A toast without type will always have no icon. Default: `true`.
* @param {number} [options.timeout=3000] Adjusts the time (in ms) the toast should be shown for before disappearing automatically. Default: `3000`.
* @param {boolean} [options.forceShow=false] Whether to force showing the toast and ignore the BD setting
* @memberof BdApi
*/
function showToast(content, options = {}) {
@ -147,10 +148,10 @@ function showToast(content, options = {}) {
*
* @deprecated
* @param {string|Node} content Content of the notice
* @param {object} options Options for the notice.
* @param {object} options Options for the notice
* @param {string} [options.type="info" | "error" | "warning" | "success"] Type for the notice. Will affect the color.
* @param {Array<{label: string, onClick: function}>} [options.buttons] Buttons that should be added next to the notice text.
* @param {number} [options.timeout=10000] Timeout until the notice is closed. Won't fire if it's set to 0;
* @param {Array<{label: string, onClick: function}>} [options.buttons] Buttons that should be added next to the notice text
* @param {number} [options.timeout=10000] Timeout until the notice is closed. Will not fire when set to `0`.
* @returns {function} A callback for closing the notice. Passing `true` as first parameter closes immediately without transitioning out.
* @memberof BdApi
*/
@ -159,7 +160,7 @@ function showToast(content, options = {}) {
}
/**
* Finds a webpack module using a filter
* Finds a webpack module using a filter.
*
* @deprecated
* @param {function} filter A filter given the exports, module, and moduleId. Returns `true` if the module matches.
@ -171,7 +172,7 @@ function findModule(filter) {
}
/**
* Finds multiple webpack modules using a filter
* Finds multiple webpack modules using a filter.
*
* @deprecated
* @param {function} filter A filter given the exports, module, and moduleId. Returns `true` if the module matches.
@ -208,7 +209,7 @@ function findModuleByPrototypes(...protos) {
}
/**
* Finds a webpack module by `displayName` property
* Finds a webpack module by `displayName` property.
*
* @deprecated
* @param {string} name Desired `displayName` property
@ -220,11 +221,11 @@ function findModuleByDisplayName(name) {
}
/**
* Get the internal react data of a specified node.
* Gets the internal React data of a specified node.
*
* @deprecated
* @param {HTMLElement} node Node to get the react data from
* @returns {object|undefined} Either the found data or `undefined`
* @param {HTMLElement} node Node to get the internal React data from.
* @returns {object|undefined} Either the found data or `undefined`
* @memberof BdApi
*/
function getInternalInstance(node) {
@ -259,7 +260,7 @@ function saveData(pluginName, key, data) {
}
/**
* Deletes a piece of stored data, this is different than saving as null or undefined.
* Deletes a piece of stored data. This is different than saving `null` or `undefined`.
*
* @deprecated
* @param {string} pluginName Name of the plugin deleting data
@ -278,13 +279,13 @@ function deleteData(pluginName, key) {
*
* @deprecated
* @param {object} what Object to be patched. You can can also pass class prototypes to patch all class instances.
* @param {string} methodName Name of the function to be patched.
* @param {object} options Options object to configure the patch.
* @param {string} methodName Name of the function to be patched
* @param {object} options Options object to configure the patch
* @param {function} [options.after] Callback that will be called after original target method call. You can modify return value here, so it will be passed to external code which calls target method. Can be combined with `before`.
* @param {function} [options.before] Callback that will be called before original target method call. You can modify arguments here, so it will be passed to original method. Can be combined with `after`.
* @param {function} [options.instead] Callback that will be called instead of original target method call. You can get access to original method using `originalMethod` parameter if you want to call it, but you do not have to. Can't be combined with `before` or `after`.
* @param {boolean} [options.once=false] Set to `true` if you want to automatically unpatch method after first call.
* @param {boolean} [options.silent=false] Set to `true` if you want to suppress log messages about patching and unpatching.
* @param {boolean} [options.once=false] Set to `true` if you want to automatically unpatch method after first call
* @param {boolean} [options.silent=false] Set to `true` if you want to suppress log messages about patching and unpatching
* @returns {function} A function that cancels the monkey patch
* @memberof BdApi
*/
@ -318,7 +319,7 @@ function monkeyPatch(what, methodName, options) {
*
* @deprecated
* @param {HTMLElement} node Node to be observed
* @param {function} callback Function to run when fired
* @param {function} callback Function to run when removed
* @memberof BdApi
*/
function onRemoved(node, callback) {
@ -330,7 +331,7 @@ function onRemoved(node, callback) {
*
* @deprecated
* @param {function} method Function to wrap
* @param {string} message Additional messasge to print when an error occurs
* @param {string} message Additional message to print when an error occurs
* @returns {function} The new wrapped function
* @memberof BdApi
*/
@ -373,7 +374,7 @@ function isSettingEnabled(collection, category, id) {
}
/**
* Enables a BetterDiscord setting by ids.
* Enables a BetterDiscord setting by IDs.
*
* @deprecated
* @param {string} [collection="settings"] Collection ID
@ -386,7 +387,7 @@ function enableSetting(collection, category, id) {
}
/**
* Disables a BetterDiscord setting by ids.
* Disables a BetterDiscord setting by IDs.
*
* @deprecated
* @param {string} [collection="settings"] Collection ID
@ -399,7 +400,7 @@ function disableSetting(collection, category, id) {
}
/**
* Toggles a BetterDiscord setting by ids.
* Toggles a BetterDiscord setting by IDs.
*
* @deprecated
* @param {string} [collection="settings"] Collection ID
@ -415,7 +416,7 @@ function toggleSetting(collection, category, id) {
* Gets some data in BetterDiscord's misc data.
*
* @deprecated
* @param {string} key Key of the data to load.
* @param {string} key Key of the data to load
* @returns {any} The stored data
* @memberof BdApi
*/
@ -427,7 +428,7 @@ function getBDData(key) {
* Sets some data in BetterDiscord's misc data.
*
* @deprecated
* @param {string} key Key of the data to store.
* @param {string} key Key of the data to store
* @returns {any} The stored data
* @memberof BdApi
*/
@ -440,19 +441,19 @@ function setBDData(key, data) {
* Returns a `Promise` that resolves to an `object` that has a `boolean` cancelled and a `filePath` string for saving and a `filePaths` string array for opening.
*
* @deprecated
* @param {object} options Options object to configure the dialog.
* @param {"open"|"save"} [options.mode="open"] Determines whether the dialog should open or save files.
* @param {string} [options.defaultPath=~] Path the dialog should show on launch.
* @param {Array<object<string, string[]>>} [options.filters=[]] An array of [file filters](https://www.electronjs.org/docs/latest/api/structures/file-filter).
* @param {string} [options.title] Title for the titlebar.
* @param {string} [options.message] Message for the dialog.
* @param {boolean} [options.showOverwriteConfirmation=false] Whether the user should be prompted when overwriting a file.
* @param {boolean} [options.showHiddenFiles=false] Whether hidden files should be shown in the dialog.
* @param {boolean} [options.promptToCreate=false] Whether the user should be prompted to create non-existant folders.
* @param {boolean} [options.openDirectory=false] Whether the user should be able to select a directory as a target.
* @param {boolean} [options.openFile=true] Whether the user should be able to select a file as a target.
* @param {boolean} [options.multiSelections=false] Whether the user should be able to select multiple targets.
* @param {boolean} [options.modal=false] Whether the dialog should act as a modal to the main window.
* @param {object} options Options object to configure the dialog
* @param {"open"|"save"} [options.mode="open"] Determines whether the dialog should open or save files
* @param {string} [options.defaultPath=~] Path the dialog should show on launch
* @param {Array<object<string, string[]>>} [options.filters=[]] An array of [file filters](https://www.electronjs.org/docs/latest/api/structures/file-filter)
* @param {string} [options.title] Title for the titlebar
* @param {string} [options.message] Message for the dialog
* @param {boolean} [options.showOverwriteConfirmation=false] Whether the user should be prompted when overwriting a file
* @param {boolean} [options.showHiddenFiles=false] Whether hidden files should be shown in the dialog
* @param {boolean} [options.promptToCreate=false] Whether the user should be prompted to create non-existant folders
* @param {boolean} [options.openDirectory=false] Whether the user should be able to select a directory as a target
* @param {boolean} [options.openFile=true] Whether the user should be able to select a file as a target
* @param {boolean} [options.multiSelections=false] Whether the user should be able to select multiple targets
* @param {boolean} [options.modal=false] Whether the dialog should act as a modal to the main window
* @returns {Promise<object>} Result of the dialog
* @memberof BdApi
*/

View File

@ -19,11 +19,12 @@ class Patcher {
/**
* This method patches onto another function, allowing your code to run beforehand.
* Using this, you are also able to modify the incoming arguments before the original method is run.
* @param {string} caller Name of the caller of the patch function.
*
* @param {string} caller Name of the caller of the patch function
* @param {object} moduleToPatch Object with the function to be patched. Can also be an object's prototype.
* @param {string} functionName Name of the function to be patched.
* @param {string} functionName Name of the function to be patched
* @param {function} callback Function to run before the original method. The function is given the `this` context and the `arguments` of the original function.
* @returns {function} Function that cancels the original patch.
* @returns {function} Function that cancels the original patch
*/
before(caller, moduleToPatch, functionName, callback) {
if (this.#callerName) {
@ -38,11 +39,12 @@ class Patcher {
/**
* This method patches onto another function, allowing your code to run instead.
* Using this, you are able to replace the original completely. You can still call the original manually if needed.
* @param {string} caller Name of the caller of the patch function.
*
* @param {string} caller Name of the caller of the patch function
* @param {object} moduleToPatch Object with the function to be patched. Can also be an object's prototype.
* @param {string} functionName Name of the function to be patched.
* @param {string} functionName Name of the function to be patched
* @param {function} callback Function to run before the original method. The function is given the `this` context, `arguments` of the original function, and also the original function.
* @returns {function} Function that cancels the original patch.
* @returns {function} Function that cancels the original patch
*/
instead(caller, moduleToPatch, functionName, callback) {
if (this.#callerName) {
@ -57,11 +59,12 @@ class Patcher {
/**
* This method patches onto another function, allowing your code to run afterwards.
* Using this, you are able to modify the return value after the original method is run.
* @param {string} caller Name of the caller of the patch function.
*
* @param {string} caller Name of the caller of the patch function
* @param {object} moduleToPatch Object with the function to be patched. Can also be an object's prototype.
* @param {string} functionName Name of the function to be patched.
* @param {string} functionName Name of the function to be patched
* @param {function} callback Function to run after the original method. The function is given the `this` context, the `arguments` of the original function, and the `return` value of the original function.
* @returns {function} Function that cancels the original patch.
* @returns {function} Function that cancels the original patch
*/
after(caller, moduleToPatch, functionName, callback) {
if (this.#callerName) {
@ -75,8 +78,9 @@ class Patcher {
/**
* Returns all patches by a particular caller. The patches all have an `unpatch()` method.
*
* @param {string} caller ID of the original patches
* @returns {Array<function>} Array of all the patch objects.
* @returns {Array<function>} Array of all the patch objects
*/
getPatchesByCaller(caller) {
if (this.#callerName) caller = this.#callerName;
@ -86,6 +90,7 @@ class Patcher {
/**
* Automatically cancels all patches created with a specific ID.
*
* @param {string} caller ID of the original patches
*/
unpatchAll(caller) {

View File

@ -12,9 +12,9 @@ const ReactUtils = {
get rootInstance() {return document.getElementById("app-mount")?._reactRootContainer?._internalRoot?.current;},
/**
* Gets the internal react data of a specified node
* Gets the internal React data of a specified node.
*
* @param {HTMLElement} node Node to get the react data from
* @param {HTMLElement} node Node to get the internal React data from
* @returns {object|undefined} Either the found data or `undefined`
*/
getInternalInstance(node) {
@ -24,13 +24,14 @@ const ReactUtils = {
/**
* Attempts to find the "owner" node to the current node. This is generally
* a node with a stateNode--a class component.
* @param {HTMLElement} node - node to obtain react instance of
* @param {object} options - options for the search
* @param {array} [options.include] - list of items to include from the search
* @param {array} [options.exclude=["Popout", "Tooltip", "Scroller", "BackgroundFlash"]] - list of items to exclude from the search
* @param {callable} [options.filter=_=>_] - filter to check the current instance with (should return a boolean)
* @return {(*|null)} the owner instance or undefined if not found.
* a node with a `stateNode` - a class component.
*
* @param {HTMLElement} node Node to obtain React instance of
* @param {object} options Options for the search
* @param {array} [options.include] List of items to include in the search
* @param {array} [options.exclude=["Popout", "Tooltip", "Scroller", "BackgroundFlash"]] List of items to exclude from the search.
* @param {callable} [options.filter=_=>_] Filter to check the current instance with (should return a boolean)
* @return {object|undefined} The owner instance or `undefined` if not found
*/
getOwnerInstance(node, {include, exclude = ["Popout", "Tooltip", "Scroller", "BackgroundFlash"], filter = _ => _} = {}) {
if (node === undefined) return undefined;
@ -57,19 +58,21 @@ const ReactUtils = {
},
/**
* Creates an unrendered react component that wraps dom elements.
* @param {HTMLElement} element - element or array of elements to wrap into a react component
* @returns {object} - unrendered react component
* Creates an unrendered React component that wraps HTML elements.
*
* @param {HTMLElement} element Element or array of elements to wrap
* @returns {object} Unrendered React component
*/
wrapElement(element) {
return class ReactWrapper extends DiscordModules.React.Component {
constructor(props) {
super(props);
this.element = element;
this.state = {hasError: false};
}
componentDidCatch() {this.setState({hasError: true});}
componentDidMount() {this.refs.element.appendChild(this.element);}
render() {return DiscordModules.React.createElement("div", {className: "react-wrapper", ref: "element"});}
render() {return this.state.hasError ? null : DiscordModules.React.createElement("div", {className: "react-wrapper", ref: "element"});}
};
}

View File

@ -5,18 +5,17 @@ import Tooltip from "../../ui/tooltip";
import ipc from "../ipc";
/**
* `UI` is a utility class for getting internal webpack modules. Instance is accessible through the {@link BdApi}.
* This is extremely useful for interacting with the internals of Discord.
* `UI` is a utility class for creating user interfaces. Instance is accessible through the {@link BdApi}.
* @type UI
* @summary {@link UI} is a utility class for getting internal webpack modules.
* @summary {@link UI} is a utility class for creating user interfaces.
* @name UI
*/
const UI = {
/**
* Shows a generic but very customizable modal.
*
* @param {string} title title of the modal
* @param {(string|ReactElement|Array<string|ReactElement>)} content a string of text to display in the modal
* @param {string} title Title of the modal
* @param {(string|ReactElement|Array<string|ReactElement>)} content A string of text to display in the modal
*/
alert(title, content) {
Modals.alert(title, content);
@ -25,14 +24,14 @@ const UI = {
/**
* Creates a tooltip to automatically show on hover.
*
* @param {HTMLElement} node - DOM node to monitor and show the tooltip on
* @param {string|HTMLElement} content - string to show in the tooltip
* @param {object} options - additional options for the tooltip
* @param {"primary"|"info"|"success"|"warn"|"danger"} [options.style="primary"] - correlates to the discord styling/colors
* @param {"top"|"right"|"bottom"|"left"} [options.side="top"] - can be any of top, right, bottom, left
* @param {boolean} [options.preventFlip=false] - prevents moving the tooltip to the opposite side if it is too big or goes offscreen
* @param {boolean} [options.disabled=false] - whether the tooltip should be disabled from showing on hover
* @returns {Tooltip} the tooltip that was generated
* @param {HTMLElement} node DOM node to monitor and show the tooltip on
* @param {string|HTMLElement} content String to show in the tooltip
* @param {object} options Additional options for the tooltip
* @param {"primary"|"info"|"success"|"warn"|"danger"} [options.style="primary"] Correlates to the Discord styling/colors
* @param {"top"|"right"|"bottom"|"left"} [options.side="top"] Can be any of top, right, bottom, left
* @param {boolean} [options.preventFlip=false] Prevents moving the tooltip to the opposite side if it is too big or goes offscreen
* @param {boolean} [options.disabled=false] Whether the tooltip should be disabled from showing on hover
* @returns {Tooltip} The tooltip that was generated.
*/
createTooltip(node, content, options = {}) {
return Tooltip.create(node, content, options);
@ -41,14 +40,15 @@ const UI = {
/**
* Shows a generic but very customizable confirmation modal with optional confirm and cancel callbacks.
*
* @param {string} title title of the modal
* @param {(string|ReactElement|Array<string|ReactElement>)} children a single or mixed array of react elements and strings. Everything is wrapped in Discord's `TextElement` component so strings will show and render properly.
* @param {object} [options] options to modify the modal
* @param {boolean} [options.danger=false] whether the main button should be red or not
* @param {string} [options.confirmText=Okay] text for the confirmation/submit button
* @param {string} [options.cancelText=Cancel] text for the cancel button
* @param {callable} [options.onConfirm=NOOP] callback to occur when clicking the submit button
* @param {callable} [options.onCancel=NOOP] callback to occur when clicking the cancel button
* @param {string} title Title of the modal.
* @param {(string|ReactElement|Array<string|ReactElement>)} children Single or mixed array of React elements and strings. Everything is wrapped in Discord's `TextElement` component so strings will show and render properly.
* @param {object} [options] Options to modify the modal
* @param {boolean} [options.danger=false] Whether the main button should be red or not
* @param {string} [options.confirmText=Okay] Text for the confirmation/submit button
* @param {string} [options.cancelText=Cancel] Text for the cancel button
* @param {callable} [options.onConfirm=NOOP] Callback to occur when clicking the submit button
* @param {callable} [options.onCancel=NOOP] Callback to occur when clicking the cancel button
* @returns {string} The key used for this modal.
*/
showConfirmationModal(title, content, options = {}) {
return Modals.showConfirmationModal(title, content, options);
@ -57,12 +57,12 @@ const UI = {
/**
* This shows a toast similar to android towards the bottom of the screen.
*
* @param {string} content The string to show in the toast.
* @param {object} options Options object. Optional parameter.
* @param {string} [options.type=""] Changes the type of the toast stylistically and semantically. Choices: "", "info", "success", "danger"/"error", "warning"/"warn". Default: ""
* @param {boolean} [options.icon=true] Determines whether the icon should show corresponding to the type. A toast without type will always have no icon. Default: `true`
* @param {number} [options.timeout=3000] Adjusts the time (in ms) the toast should be shown for before disappearing automatically. Default: `3000`
* @param {boolean} [options.forceShow=false] Whether to force showing the toast and ignore the bd setting
* @param {string} content The string to show in the toast
* @param {object} options Options for the toast
* @param {string} [options.type=""] Changes the type of the toast stylistically and semantically. Choices: "", "info", "success", "danger"/"error", "warning"/"warn". Default: "".
* @param {boolean} [options.icon=true] Determines whether the icon should show corresponding to the type. A toast without type will always have no icon. Default: `true`.
* @param {number} [options.timeout=3000] Adjusts the time (in ms) the toast should be shown for before disappearing automatically. Default: `3000`.
* @param {boolean} [options.forceShow=false] Whether to force showing the toast and ignore the BD setting
*/
showToast(content, options = {}) {
Toasts.show(content, options);
@ -72,11 +72,11 @@ const UI = {
* Shows a notice above Discord's chat layer.
*
* @param {string|Node} content Content of the notice
* @param {object} options Options for the notice.
* @param {object} options Options for the notice
* @param {string} [options.type="info" | "error" | "warning" | "success"] Type for the notice. Will affect the color.
* @param {Array<{label: string, onClick: function}>} [options.buttons] Buttons that should be added next to the notice text.
* @param {number} [options.timeout=10000] Timeout until the notice is closed. Won't fire if it's set to 0;
* @returns {function}
* @param {Array<{label: string, onClick: function}>} [options.buttons] Buttons that should be added next to the notice text
* @param {number} [options.timeout=10000] Timeout until the notice is closed. Will not fire when set to `0`.
* @returns {function} A callback for closing the notice. Passing `true` as first parameter closes immediately without transitioning out.
*/
showNotice(content, options = {}) {
return Notices.show(content, options);
@ -86,19 +86,19 @@ const UI = {
* Gives access to the [Electron Dialog](https://www.electronjs.org/docs/latest/api/dialog/) api.
* Returns a `Promise` that resolves to an `object` that has a `boolean` cancelled and a `filePath` string for saving and a `filePaths` string array for opening.
*
* @param {object} options Options object to configure the dialog.
* @param {"open"|"save"} [options.mode="open"] Determines whether the dialog should open or save files.
* @param {string} [options.defaultPath=~] Path the dialog should show on launch.
* @param {Array<object<string, string[]>>} [options.filters=[]] An array of [file filters](https://www.electronjs.org/docs/latest/api/structures/file-filter).
* @param {string} [options.title] Title for the titlebar.
* @param {string} [options.message] Message for the dialog.
* @param {boolean} [options.showOverwriteConfirmation=false] Whether the user should be prompted when overwriting a file.
* @param {boolean} [options.showHiddenFiles=false] Whether hidden files should be shown in the dialog.
* @param {boolean} [options.promptToCreate=false] Whether the user should be prompted to create non-existant folders.
* @param {boolean} [options.openDirectory=false] Whether the user should be able to select a directory as a target.
* @param {boolean} [options.openFile=true] Whether the user should be able to select a file as a target.
* @param {boolean} [options.multiSelections=false] Whether the user should be able to select multiple targets.
* @param {boolean} [options.modal=false] Whether the dialog should act as a modal to the main window.
* @param {object} options Options object to configure the dialog
* @param {"open"|"save"} [options.mode="open"] Determines whether the dialog should open or save files
* @param {string} [options.defaultPath=~] Path the dialog should show on launch
* @param {Array<object<string, string[]>>} [options.filters=[]] An array of [file filters](https://www.electronjs.org/docs/latest/api/structures/file-filter)
* @param {string} [options.title] Title for the titlebar
* @param {string} [options.message] Message for the dialog
* @param {boolean} [options.showOverwriteConfirmation=false] Whether the user should be prompted when overwriting a file
* @param {boolean} [options.showHiddenFiles=false] Whether hidden files should be shown in the dialog
* @param {boolean} [options.promptToCreate=false] Whether the user should be prompted to create non-existent folders
* @param {boolean} [options.openDirectory=false] Whether the user should be able to select a directory as a target
* @param {boolean} [options.openFile=true] Whether the user should be able to select a file as a target
* @param {boolean} [options.multiSelections=false] Whether the user should be able to select multiple targets
* @param {boolean} [options.modal=false] Whether the dialog should act as a modal to the main window
* @returns {Promise<object>} Result of the dialog
*/
async openDialog(options) {

View File

@ -9,11 +9,12 @@ import Utilities from "../utilities";
const Utils = {
/**
* Finds a value, subobject, or array from a tree that matches a specific filter. This is a DFS.
*
* @param {object} tree Tree that should be walked
* @param {callable} searchFilter Filter to check against each object and subobject
* @param {object} options Additional options to customize the search
* @param {Array<string>|null} [options.walkable=null] Array of strings to use as keys that are allowed to be walked on. Null value indicates all keys are walkable
* @param {Array<string>} [options.ignore=[]] Array of strings to use as keys to exclude from the search, most helpful when `walkable = null`.
* @param {Array<string>|null} [options.walkable=null] Array of strings to use as keys that are allowed to be walked on. `null` indicates all keys are walkable.
* @param {Array<string>} [options.ignore=[]] Array of strings to use as keys to exclude from the search. Most helpful when `walkable = null`.
*/
findInTree(tree, searchFilter, options = {}) {
return Utilities.findInTree(tree, searchFilter, options);
@ -24,9 +25,10 @@ const Utils = {
* of `extenders` have priority, that is to say if one sets a key to be a primitive,
* it will be overwritten with the next one with the same key. If it is an object,
* and the keys match, the object is extended. This happens recursively.
* @param {object} extendee - Object to be extended
* @param {...object} extenders - Objects to extend with
* @returns {object} - A reference to `extendee`
*
* @param {object} extendee Object to be extended
* @param {...object} extenders Objects to extend with
* @returns {object} A reference to `extendee`
*/
extend(extendee, ...extenders) {
return Utilities.extend(extendee, ...extenders);
@ -35,20 +37,23 @@ const Utils = {
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds.
* `delay` milliseconds. It is called at the end of the sequence (trailing edge).
*
* Adapted from the version by David Walsh (https://davidwalsh.name/javascript-debounce-function)
*
* @param {function} executor
* @param {number} delay
* @param {function} executor The function to be debounced
* @param {number} delay Number of ms to delay calls
* @return {function} A debounced version of the function
*/
debounce(executor, delay) {
return Utilities.debounce(executor, delay);
},
/**
* Takes a string of html and escapes it using the brower's own escaping mechanism.
* @param {String} html - html to be escaped
* Takes a string of HTML and escapes it using the browser's own escaping mechanism.
*
* @param {string} html HTML to be escaped
* @return {string} Escaped HTML string
*/
escapeHTML(html) {
return Utilities.escapeHTML(html);
@ -59,7 +64,9 @@ const Utils = {
* When given an array all values from the array are added to the list.
* When given an object they keys are added as the classnames if the value is truthy.
* Copyright (c) 2018 Jed Watson https://github.com/JedWatson/classnames MIT License
* @param {...Any} argument - anything that should be used to add classnames.
*
* @param {...any} argument Anything that should be used to add classnames
* @returns {string} Joined classname
*/
className() {
return Utilities.className(...arguments);

View File

@ -40,7 +40,7 @@ const Webpack = {
/**
* Generates a function that filters by strings.
* @param {...String} strings A list of strings
* @param {...string} strings A list of strings
* @returns {function} A filter that checks for a set of strings
*/
byStrings(...strings) {return Filters.byStrings(...strings);},
@ -65,9 +65,9 @@ const Webpack = {
* @memberof Webpack
* @param {function} filter A function to use to filter modules. It is given exports, module, and moduleID. Return `true` to signify match.
* @param {object} [options] Options to configure the search
* @param {Boolean} [options.first=true] Whether to return only the first matching module
* @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export
* @param {Boolean} [options.searchExports=false] Whether to execute the filter on webpack exports
* @param {boolean} [options.first=true] Whether to return only the first matching module
* @param {boolean} [options.defaultExport=true] Whether to return default export when matching the default export
* @param {boolean} [options.searchExports=false] Whether to execute the filter on webpack exports
* @return {any}
*/
getModule(filter, options = {}) {
@ -81,10 +81,10 @@ const Webpack = {
* Finds multiple modules using multiple filters.
* @memberof Webpack
* @param {...object} queries Object representing the query to perform
* @param {Function} queries.filter A function to use to filter modules
* @param {Boolean} [queries.first=true] Whether to return only the first matching module
* @param {Boolean} [queries.defaultExport=true] Whether to return default export when matching the default export
* @param {Boolean} [queries.searchExports=false] Whether to execute the filter on webpack exports
* @param {function} queries.filter A function to use to filter modules
* @param {boolean} [queries.first=true] Whether to return only the first matching module
* @param {boolean} [queries.defaultExport=true] Whether to return default export when matching the default export
* @param {boolean} [queries.searchExports=false] Whether to execute the filter on webpack exports
* @return {any}
*/
getBulk(...queries) {return WebpackModules.getBulk(...queries);},
@ -95,8 +95,8 @@ const Webpack = {
* @param {function} filter A function to use to filter modules. It is given exports. Return `true` to signify match.
* @param {object} [options] Options for configuring the listener
* @param {AbortSignal} [options.signal] AbortSignal of an AbortController to cancel the promise
* @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export
* @param {Boolean} [options.searchExports=false] Whether to execute the filter on webpack exports
* @param {boolean} [options.defaultExport=true] Whether to return default export when matching the default export
* @param {boolean} [options.searchExports=false] Whether to execute the filter on webpack exports
* @returns {Promise<any>}
*/
waitForModule(filter, options = {}) {

View File

@ -8,6 +8,7 @@ import ThemeManager from "./thememanager";
import Settings from "./settingsmanager";
import * as Builtins from "builtins";
import Modals from "../ui/modals";
import FloatingWindows from "../ui/floatingwindows";
import DataStore from "./datastore";
import DiscordModules from "./discordmodules";
import LoadingIcon from "../loadingicon";
@ -47,6 +48,7 @@ export default new class Core {
await Editor.initialize();
Modals.initialize();
FloatingWindows.initialize();
Logger.log("Startup", "Initializing Builtins");
for (const module in Builtins) {

View File

@ -3,7 +3,6 @@ import Logger from "common/logger";
const fs = require("fs");
const path = require("path");
const releaseChannel = window?.DiscordNative?.app?.getReleaseChannel?.() ?? "stable";
const discordVersion = window?.DiscordNative?.remoteApp?.getVersion?.() ?? "0.0.309";
// Schema
// =======================
@ -44,54 +43,6 @@ export default new class DataStore {
catch (e) {Logger.stacktrace("DataStore", `Could not load file ${file}`, e);}
this.data[file.split(".")[0]] = data;
}
if (newStorageExists) return;
try {this.convertOldData();} // Convert old data if it exists (routine checks existence and removes existence)
catch (e) {Logger.stacktrace("DataStore", `Could not convert old data.`, e);}
}
convertOldData() {
const oldFile = path.join(Config.dataPath, "bdstorage.json");
if (!fs.existsSync(oldFile)) return;
const oldData = __non_webpack_require__(oldFile); // got the data
fs.renameSync(oldFile, `${oldFile}.bak`); // rename file after grabbing data to prevent loop
const setChannelData = (channel, key, value, ext = "json") => fs.writeFileSync(path.resolve(this.baseFolder, channel, `${key}.${ext}`), JSON.stringify(value, null, 4));
const channels = ["stable", "canary", "ptb"];
let customcss = "";
try {customcss = oldData.bdcustomcss ? atob(oldData.bdcustomcss) : "";}
catch (e) {Logger.stacktrace("DataStore:convertOldData", "Error decoding custom css", e);}
for (const channel of channels) {
if (!fs.existsSync(path.resolve(this.baseFolder, channel))) fs.mkdirSync(path.resolve(this.baseFolder, channel));
const channelData = oldData.settings[channel];
if (!channelData || !channelData.settings) continue;
const oldSettings = channelData.settings;
const newSettings = {
general: {publicServers: oldSettings["bda-gs-1"], voiceDisconnect: oldSettings["bda-dc-0"], showToasts: oldSettings["fork-ps-2"]},
appearance: {twentyFourHour: oldSettings["bda-gs-6"], minimalMode: oldSettings["bda-gs-2"], coloredText: oldSettings["bda-gs-7"]},
addons: {addonErrors: oldSettings["fork-ps-1"], autoReload: oldSettings["fork-ps-5"]},
developer: {debuggerHotkey: oldSettings["bda-gs-8"], reactDevTools: oldSettings.reactDevTools}
};
setChannelData(channel, "settings", newSettings); // settingsCookie
setChannelData(channel, "plugins", channelData.plugins || {}); // pluginCookie
setChannelData(channel, "themes", channelData.themes || {}); // themeCookie
fs.writeFileSync(path.resolve(this.baseFolder, channel, "custom.css"), customcss); // customcss
}
this.initialize(); // Reinitialize data store with the converted data
}
get injectionPath() {
if (this._injectionPath) return this._injectionPath;
const base = Config.appPath;
const roamingBase = Config.userData;
const roamingLocation = path.resolve(roamingBase, discordVersion, "modules", "discord_desktop_core", "injector");
const location = path.resolve(base, "..", "app");
const realLocation = fs.existsSync(location) ? location : fs.existsSync(roamingLocation) ? roamingLocation : null;
if (!realLocation) return this._injectionPath = null;
return this._injectionPath = realLocation;
}
get pluginFolder() {return this._pluginFolder || (this._pluginFolder = path.resolve(Config.dataPath, "plugins"));}

View File

@ -44,7 +44,7 @@ export default new class PluginManager extends AddonManager {
this.setupFunctions();
Settings.registerPanel("plugins", Strings.Panels.plugins, {
order: 3,
element: () => SettingsRenderer.getAddonPanel(Strings.Panels.plugins, this.addonList, this.state, {
element: SettingsRenderer.getAddonPanel(Strings.Panels.plugins, this.addonList, this.state, {
type: this.prefix,
folder: this.addonFolder,
onChange: this.togglePlugin.bind(this),

View File

@ -23,7 +23,7 @@ export default new class ThemeManager extends AddonManager {
const errors = super.initialize();
Settings.registerPanel("themes", Strings.Panels.themes, {
order: 4,
element: () => SettingsRenderer.getAddonPanel(Strings.Panels.themes, this.addonList, this.state, {
element: SettingsRenderer.getAddonPanel(Strings.Panels.themes, this.addonList, this.state, {
type: this.prefix,
folder: this.addonFolder,
onChange: this.toggleTheme.bind(this),

View File

@ -159,14 +159,19 @@ export default class WebpackModules {
for (let i = 0; i < indices.length; i++) {
const index = indices[i];
if (!modules.hasOwnProperty(index)) continue;
const module = modules[index];
const {exports} = module;
if (!exports || exports === window || exports === document.documentElement) continue;
if (typeof(exports) === "object" && searchExports) {
let module = null;
try {module = modules[index]} catch {continue;};
const {exports} = module;
if (!exports || exports === window || exports === document.documentElement || exports[Symbol.toStringTag] === "DOMTokenList") continue;
if (typeof(exports) === "object" && searchExports && !exports.TypedArray) {
for (const key in exports) {
let foundModule = null;
const wrappedExport = exports[key];
let wrappedExport = null;
try {wrappedExport = exports[key];} catch {continue;}
if (!wrappedExport) continue;
if (wrappedFilter(wrappedExport, module, index)) foundModule = wrappedExport;
if (!foundModule) continue;
@ -210,7 +215,7 @@ export default class WebpackModules {
if (!modules.hasOwnProperty(index)) continue;
const module = modules[index];
const {exports} = module;
if (!exports || exports === window || exports === document.documentElement) continue;
if (!exports || exports === window || exports === document.documentElement || exports[Symbol.toStringTag] === "DOMTokenList") continue;
for (let q = 0; q < queries.length; q++) {
const query = queries[q];
@ -220,7 +225,7 @@ export default class WebpackModules {
const wrappedFilter = wrapFilter(filter);
if (typeof(exports) === "object" && searchExports) {
if (typeof(exports) === "object" && searchExports && !exports.TypedArray) {
for (const key in exports) {
let foundModule = null;
const wrappedExport = exports[key];
@ -345,10 +350,10 @@ export default class WebpackModules {
return new Promise((resolve) => {
const cancel = () => this.removeListener(listener);
const listener = function(exports) {
if (!exports || exports === window || exports === document.documentElement) return;
if (!exports || exports === window || exports === document.documentElement || exports[Symbol.toStringTag] === "DOMTokenList") return;
let foundModule = null;
if (typeof(exports) === "object" && searchExports) {
if (typeof(exports) === "object" && searchExports && !exports.TypedArray) {
for (const key in exports) {
foundModule = null;
const wrappedExport = exports[key];
@ -481,4 +486,4 @@ export default class WebpackModules {
}
}
WebpackModules.initialize();
WebpackModules.initialize();

View File

@ -17,6 +17,11 @@ export default class SimpleMarkdownExt {
return original;
}}});
for (const type in newRules) {
if (!newRules[type].requiredFirstCharacters) continue;
newRules[type].requiredFirstCharacters = Object.values(newRules[type].requiredFirstCharacters);
}
this._parse = SMD.parserFor(newRules);
this._renderer = SMD.reactFor(SMD.ruleOutput(newRules, "react"));
}

View File

@ -1,166 +0,0 @@
import Logger from "common/logger";
import {WebpackModules, IPC} from "modules";
const SortedGuildStore = WebpackModules.getByProps("getSortedGuilds");
const AvatarDefaults = WebpackModules.getByProps("DEFAULT_AVATARS");
const InviteActions = WebpackModules.getByProps("acceptInvite");
// const BrowserWindow = require("electron").remote.BrowserWindow;
const betterDiscordServer = {
name: "BetterDiscord",
members: 110000,
categories: ["community", "programming", "support"],
description: "Official BetterDiscord server for plugins, themes, support, etc",
identifier: "86004744966914048",
iconUrl: "https://cdn.discordapp.com/icons/86004744966914048/babd1af3fa6011a50e418a80f4970ceb.webp",
nativejoin: true,
invite_code: "BJD2yvJ",
pinned: true,
insertDate: 1517806800
};
const ITEMS_PER_PAGE = 50;
export default new class PublicServersConnection {
constructor() {
this.cache = new Map();
this.cache.set("featured", [betterDiscordServer]);
this.cache.set("popular", []);
this.cache.set("keywords", []);
this.cache.set("accessToken", "");
}
get endPoint() {return "https://search.discordservers.com";}
get joinEndPoint() {return "https://j.discordservers.com";}
get connectEndPoint() {return "https://auth.discordservers.com/info";}
get authorizeEndPoint() {return `https://auth.discordservers.com/connect?scopes=guilds.join&previousUrl=${this.connectEndPoint}`;}
getDefaultAvatar() {
return AvatarDefaults.DEFAULT_AVATARS[Math.floor(Math.random() * 5)];
}
hasJoined(id) {
return SortedGuildStore.getFlattenedGuildIds().includes(id);
}
async search({term = "", keyword = "", page = 1} = {}) {
if (this.cache.has(term + keyword + page)) return this.cache.get(term + keyword + page);
const from = (page - 1) * ITEMS_PER_PAGE;
const queries = [];
if (keyword) queries.push(`keyword=${keyword.replace(/ /g, "%20").toLowerCase()}`);
if (term) queries.push(`term=${term.replace(/ /g, "%20")}`);
if (from) queries.push(`from=${from}`);
const query = `?${queries.join("&")}`;
try {
const response = await fetch(`${this.endPoint}${query}`, {method: "GET"});
const data = await response.json();
const results = {
servers: data.results,
size: data.size,
total: data.total,
page: Math.ceil(from / ITEMS_PER_PAGE) + 1,
numPages: Math.ceil(data.total / ITEMS_PER_PAGE)
};
this.cache.set(term + keyword + page, results);
return results;
}
catch (error) {
Logger.stacktrace("PublicServers", "Could not reach search endpoint.", error);
}
}
async getDashboard() {
const cachedKeywords = this.cache.get("keywords");
if (cachedKeywords && cachedKeywords.length) return {featured: this.cache.get("featured"), popular: this.cache.get("popular"), keywords: cachedKeywords};
try {
const response = await fetch(`${this.endPoint}/dashboard`, {method: "GET"});
const data = await response.json();
const featuredFirst = data.results[0].key === "featured";
const featuredServers = data.results[featuredFirst ? 0 : 1].response.hits;
const popularServers = data.results[featuredFirst ? 1 : 0].response.hits;
const mainKeywords = data.mainKeywords.map(k => k.charAt(0).toUpperCase() + k.slice(1)).sort();
featuredServers.unshift(betterDiscordServer);
this.cache.set("featured", featuredServers);
this.cache.set("popular", popularServers);
this.cache.set("keywords", mainKeywords);
return {featured: this.cache.get("featured"), popular: this.cache.get("popular"), keywords: this.cache.get("keywords")};
}
catch (error) {
Logger.stacktrace("PublicServers", "Could not download dashboard.", error);
return {featured: this.cache.get("featured"), popular: this.cache.get("popular"), keywords: this.cache.get("keywords")};
}
}
async join(id, native = false) {
if (native) return InviteActions.acceptInvite({inviteKey: id});
try {
await fetch(`${this.joinEndPoint}/${id}`,{
method: "GET",
credentials: "include",
mode: "cors",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
}
});
return true;
}
catch (error) {
Logger.warn("PublicServers", "Could not join server.");
return false;
}
}
async checkConnection() {
try {
const response = await fetch(this.connectEndPoint, {
method: "GET",
credentials: "include",
mode: "cors",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
}
});
const data = await response.json();
this._accessToken = data.access_token;
return data;
}
catch (error) {
Logger.warn("PublicServers", "Could not verify connection.");
return false;
}
}
async connect() {
await IPC.openWindow(this.authorizeEndPoint, {
windowOptions: this.windowOptions,
closeOnUrl: this.connectEndPoint
});
}
get windowOptions() {
return {
width: 520,
height: 580,
backgroundColor: "#282b30",
show: true,
resizable: true,
maximizable: false,
minimizable: false,
alwaysOnTop: true,
frame: false,
center: true,
webPreferences: {
nodeIntegration: false
}
};
}
};

View File

@ -1,287 +0,0 @@
@keyframes bd-placeholder-card-pulse {
0% {
opacity: 0.6;
}
50% {
opacity: 0.8;
}
to {
opacity: 0.6;
}
}
#bd-pub-li {
height: 24px;
overflow: hidden;
}
#bd-pub-button {
border-radius: 4px;
background-color: var(--background-primary);
color: var(--text-normal);
text-align: center;
font-size: 12px;
line-height: 24px;
height: 24px;
transition: background-color 0.15s ease-out, color 0.15s ease-out;
}
#bd-pub-button:hover {
color: #FFFFFF;
background-color: #3E82E5;
}
#bd-connection {
margin-left: 10px;
}
.bd-footnote {
color: #B9BBBE;
font-size: 11px;
}
.bd-button-next,
.bd-button-reconnect {
margin: 5px 10px 10px 0;
width: 100%;
min-height: 20px;
}
/* Rewrite */
.bd-server-search {
margin-bottom: 5px;
}
.bd-card-list {
display: grid;
grid-gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
}
.bd-server-card {
display: flex;
flex-direction: column;
height: 320px;
width: 100%;
overflow: hidden;
border-radius: 8px;
position: relative;
transition: box-shadow 0.2s ease-out, transform 0.2s ease-out, background 0.2s ease-out, opacity 0.2s ease-in;
cursor: pointer;
background-color: var(--activity-card-background);
}
.theme-light .bd-server-card {
box-shadow: 0 0 0 1px rgba(185, 187, 190, 0.3), var(--elevation-medium);
}
.theme-dark .bd-server-card {
background-color: var(--background-secondary-alt);
}
.bd-server-card:hover,
.bd-server-card:focus,
.theme-light .bd-server-card:hover,
.theme-light .bd-server-card:focus {
transform: translateY(-1px);
box-shadow: var(--elevation-high);
}
.theme-dark .bd-server-card:hover,
.theme-dark .bd-server-card:focus {
background-color: var(--background-tertiary);
}
.bd-placeholder-card {
background-color: var(--background-secondary-alt);
animation: bd-placeholder-card-pulse 1.8s ease-in-out infinite;
height: 320px;
width: 100%;
overflow: hidden;
border-radius: 8px;
position: relative;
}
.bd-server-header {
height: 143px;
position: relative;
display: block;
overflow: visible;
margin-bottom: 32px;
}
.bd-server-splash-container {
width: 100%;
height: 100%;
display: block;
position: absolute;
top: 0;
left: 0;
transition: opacity 0.2s, transform 0.2s ease-out;
transform: scale(1);
overflow: hidden;
}
.bd-server-card:hover .bd-server-splash-container {
-webkit-transform: scale(1.01) translateZ(0);
transform: scale(1.01) translateZ(0);
}
.bd-server-splash {
object-fit: cover;
width: 100%;
height: 100%;
filter: blur(20px);
}
.bd-server-icon {
position: absolute;
bottom: -21px;
left: 12px;
width: 40px;
background: var(--background-secondary-alt);
border: 4px solid var(--background-secondary-alt);
border-radius: 25%;
transition: background 0.2s ease-out, transform 0.2s ease-out, border-color 0.2s ease-out;
}
.bd-server-card:hover .bd-server-icon {
border-color: var(--background-tertiary);
background: var(--background-tertiary);
}
.bd-server-info {
display: flex;
flex: 1 1 auto;
position: relative;
flex-direction: column;
align-content: stretch;
padding: 0 16px 16px;
overflow: hidden;
}
.bd-server-title {
display: flex;
align-items: center;
width: 100%;
font-weight: 600;
}
.bd-server-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--header-primary);
font-size: 16px;
line-height: 20px;
}
.bd-server-description {
flex: 1 1 auto;
overflow: hidden;
margin: 4px 0 16px;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
color: var(--header-secondary);
font-size: 14px;
line-height: normal;
}
.bd-server-footer {
display: flex;
align-items: center;
}
.bd-server-count {
display: flex;
align-items: center;
font-size: 0.75rem;
line-height: 1rem;
margin-right: 16px;
}
.bd-server-count-dot {
background-color: #43B581;
border-radius: 50%;
width: 8px;
height: 8px;
margin-right: 4px;
flex-shrink: 0;
}
.bd-server-count + .bd-server-count .bd-server-count-dot {
background-color: #B9BBBE;
}
.bd-server-count-text {
color: var(--header-secondary);
font-size: 12px;
line-height: 16px;
}
.bd-server-tag {
margin-left: 5px;
font-size: 10px;
text-transform: uppercase;
vertical-align: top;
display: inline-flex;
align-items: center;
flex-shrink: 0;
text-indent: 0;
height: 15px;
padding: 0 4px;
margin-top: 1px;
border-radius: 3px;
background: #3E82E5;
color: #FFFFFF;
}
.bd-pagination {
display: flex;
justify-content: space-between;
align-items: center;
margin: 15px;
color: var(--header-primary);
}
.bd-pagination span {
font-weight: 600;
}
.bd-pagination button {
background: none;
opacity: 0.7;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--background-tertiary);
border-radius: 3px;
padding: 0;
}
.bd-pagination button:hover,
.bd-pagination button:active {
opacity: 1;
background: var(--background-accent);
}
.bd-pagination button:active {
opacity: 1;
background: var(--background-secondary);
}
.bd-pagination button svg {
fill: var(--header-primary);
}
.bd-pagination button[disabled] {
opacity: 0.2;
cursor: not-allowed;
}
.bd-pagination + .bd-settings-title {
margin-top: 20px;
}

View File

@ -165,7 +165,7 @@
}
.bd-settings-title {
color: #FFFFFF;
color: var(--header-primary, #FFFFFF);
font-weight: 600;
cursor: default;
flex: 1;

View File

@ -3,24 +3,19 @@ import Extension from "./icons/extension";
import ThemeIcon from "./icons/theme";
import Divider from "./divider";
const Parser = Object(WebpackModules.getByProps("defaultRules", "parse")).defaultRules;
const {useState, useCallback, useMemo} = React;
const joinClassNames = (...classNames) => classNames.filter(e => e).join(" ");
class AddonError extends React.Component {
constructor(props) {
super(props);
function AddonError({err, index}) {
const [expanded, setExpanded] = useState(false);
const toggle = useCallback(() => setExpanded(!expanded), [expanded]);
this.state = {
expanded: false
};
}
toggle() {
this.setState({expanded: !this.state.expanded});
}
renderErrorBody(err) {
function renderErrorBody() {
const stack = err?.error?.stack ?? err.stack;
if (!this.state.expanded || !stack) return null;
if (!expanded || !stack) return null;
return <div className="bd-addon-error-body">
<Divider />
<div className="bd-addon-error-stack">
@ -28,92 +23,57 @@ class AddonError extends React.Component {
</div>
</div>;
}
render() {
const err = this.props.err;
return <div key={`${err.type}-${this.props.index}`} className={joinClassNames("bd-addon-error", (this.state.expanded) ? "expanded" : "collapsed")}>
<div className="bd-addon-error-header" onClick={() => {this.toggle();}} >
<div className="bd-addon-error-icon">
{err.type == "plugin" ? <Extension /> : <ThemeIcon />}
</div>
<div className="bd-addon-error-header-inner">
<h3 className={`bd-addon-error-file ${DiscordClasses.Text.colorHeaderPrimary} ${DiscordClasses.Integrations.secondaryHeader} ${DiscordClasses.Text.size16}`}>{err.name}</h3>
<div className={`bd-addon-error-details ${DiscordClasses.Integrations.detailsWrapper}`}>
<svg className={DiscordClasses.Integrations.detailsIcon} aria-hidden="false" width="16" height="16" viewBox="0 0 12 12">
<path fill="currentColor" d="M6 1C3.243 1 1 3.244 1 6c0 2.758 2.243 5 5 5s5-2.242 5-5c0-2.756-2.243-5-5-5zm0 2.376a.625.625 0 110 1.25.625.625 0 010-1.25zM7.5 8.5h-3v-1h1V6H5V5h1a.5.5 0 01.5.5v2h1v1z"></path>
</svg>
<div className={`${DiscordClasses.Text.colorHeaderSecondary} ${DiscordClasses.Text.size12}`}>{err.message}</div>
</div>
</div>
<svg className="bd-addon-error-expander" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M7 10L12 15 17 10" aria-hidden="true"></path>
</svg>
return <div key={`${err.type}-${index}`} className={joinClassNames("bd-addon-error", (expanded) ? "expanded" : "collapsed")}>
<div className="bd-addon-error-header" onClick={toggle} >
<div className="bd-addon-error-icon">
{err.type == "plugin" ? <Extension /> : <ThemeIcon />}
</div>
{this.renderErrorBody(err)}
</div>;
}
<div className="bd-addon-error-header-inner">
<h3 className={`bd-addon-error-file ${DiscordClasses.Text.colorHeaderPrimary} ${DiscordClasses.Integrations.secondaryHeader} ${DiscordClasses.Text.size16}`}>{err.name}</h3>
<div className={`bd-addon-error-details ${DiscordClasses.Integrations.detailsWrapper}`}>
<svg className={DiscordClasses.Integrations.detailsIcon} aria-hidden="false" width="16" height="16" viewBox="0 0 12 12">
<path fill="currentColor" d="M6 1C3.243 1 1 3.244 1 6c0 2.758 2.243 5 5 5s5-2.242 5-5c0-2.756-2.243-5-5-5zm0 2.376a.625.625 0 110 1.25.625.625 0 010-1.25zM7.5 8.5h-3v-1h1V6H5V5h1a.5.5 0 01.5.5v2h1v1z"></path>
</svg>
<div className={`${DiscordClasses.Text.colorHeaderSecondary} ${DiscordClasses.Text.size12}`}>{err.message}</div>
</div>
</div>
<svg className="bd-addon-error-expander" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M7 10L12 15 17 10" aria-hidden="true"></path>
</svg>
</div>
{renderErrorBody(err)}
</div>;
}
export default class AddonErrorModal extends React.Component {
constructor(props) {
super(props);
const tabs = this.getTabs();
this.state = {
selectedTab: tabs[0].id,
};
}
function generateTab(id, errors) {
return {id, errors, name: Strings.Panels[id]};
}
mergeErrors(errors1 = [], errors2 = []) {
const list = [];
const allErrors = [...errors2, ...errors1];
for (const error of allErrors) {
if (list.find(e => e.file === error.file)) continue;
list.push(error);
}
return list;
}
export default function AddonErrorModal({pluginErrors, themeErrors}) {
const tabs = useMemo(() => {
return [
pluginErrors.length && generateTab("plugins", pluginErrors),
themeErrors.length && generateTab("themes", themeErrors)
].filter(e => e);
}, [pluginErrors, themeErrors]);
refreshTabs(pluginErrors, themeErrors) {
this._tabs = null;
this.props.pluginErrors = this.mergeErrors(this.props.pluginErrors, pluginErrors);
this.props.themeErrors = this.mergeErrors(this.props.themeErrors, themeErrors);
this.forceUpdate();
}
const [tabId, setTab] = useState(tabs[0].id);
const switchToTab = useCallback((id) => setTab(id), []);
const selectedTab = tabs.find(e => e.id === tabId);
generateTab(id, errors) {
return {
id: id,
name: Strings.Panels[id],
errors: errors
};
}
getTabs() {
return this._tabs || (this._tabs = [
this.props.pluginErrors.length && this.generateTab("plugins", this.props.pluginErrors),
this.props.themeErrors.length && this.generateTab("themes", this.props.themeErrors)
].filter(e => e));
}
switchToTab(id) {
this.setState({selectedTab: id});
}
render() {
const selectedTab = this.getTabs().find(e => this.state.selectedTab === e.id);
const tabs = this.getTabs();
return <>
<div className={`bd-error-modal-header ${DiscordClasses.Modal.header} ${DiscordClasses.Modal.separator}`}>
<h4 className={`${DiscordClasses.Titles.defaultColor} ${DiscordClasses.Text.size14} ${DiscordClasses.Titles.h4} ${DiscordClasses.Margins.marginBottom8}`}>{Strings.Modals.addonErrors}</h4>
<div className="bd-tab-bar">
{tabs.map(tab => <div onClick={() => {this.switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
</div>
return <>
<div className={`bd-error-modal-header ${DiscordClasses.Modal.header} ${DiscordClasses.Modal.separator}`}>
<h4 className={`${DiscordClasses.Titles.defaultColor} ${DiscordClasses.Text.size14} ${DiscordClasses.Titles.h4} ${DiscordClasses.Margins.marginBottom8}`}>{Strings.Modals.addonErrors}</h4>
<div className="bd-tab-bar">
{tabs.map(tab => <div onClick={() => {switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
</div>
<div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}>
<div className="bd-addon-errors">
{selectedTab.errors.map((error, index) => <AddonError index={index} err={error} />)}
</div>
</div>
<div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}>
<div className="bd-addon-errors">
{selectedTab.errors.map((error, index) => <AddonError index={index} err={error} />)}
</div>
</>;
}
</div>
</>;
}

View File

@ -1,17 +1,15 @@
import {React, DiscordClasses} from "modules";
import SimpleMarkdown from "../../structs/markdown";
export default class EmptyImage extends React.Component {
render() {
return <div className={`bd-empty-image-container ${DiscordClasses.EmptyImage.emptyContainer}` + (this.props.className ? ` ${this.props.className}` : "")}>
<div className={`bd-empty-image ${DiscordClasses.EmptyImage.emptyImage}`}></div>
<div className={`bd-empty-image-header ${DiscordClasses.EmptyImage.emptyHeader}`}>
{this.props.title || "You don't have anything!"}
</div>
<div className={`bd-empty-image-message`}>
{SimpleMarkdown.parseToReact(this.props.message || "You should probably get something.")}
</div>
{this.props.children}
</div>;
}
export default function EmptyImage(props) {
return <div className={`bd-empty-image-container ${DiscordClasses.EmptyImage.emptyContainer}` + (props.className ? ` ${props.className}` : "")}>
<div className={`bd-empty-image ${DiscordClasses.EmptyImage.emptyImage}`}></div>
<div className={`bd-empty-image-header ${DiscordClasses.EmptyImage.emptyHeader}`}>
{props.title || "You don't have anything!"}
</div>
<div className={`bd-empty-image-message`}>
{SimpleMarkdown.parseToReact(props.message || "You should probably get something.")}
</div>
{props.children}
</div>;
}

View File

@ -1,13 +1,11 @@
import {React, DiscordModules} from "modules";
import MagnifyingGlass from "../icons/magnifyingglass";
export default class NoResults extends React.Component {
render() {
return <div className={"bd-empty-results" + (this.props.className ? ` ${this.props.className}` : "")}>
<MagnifyingGlass />
<div className="bd-empty-results-text">
{this.props.text || DiscordModules.Strings.SEARCH_NO_RESULTS || ""}
</div>
</div>;
}
export default function NoResults(props) {
return <div className={"bd-empty-results" + (props.className ? ` ${props.className}` : "")}>
<MagnifyingGlass />
<div className="bd-empty-results-text">
{props.text || DiscordModules.Strings.SEARCH_NO_RESULTS || ""}
</div>
</div>;
}

View File

@ -1,27 +1,23 @@
import {React} from "modules";
export default class Checkbox extends React.Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.state = {checked: this.props.checked || false};
}
const {useState, useCallback} = React;
render() {
return <div className="checkbox-item">
<div className="checkbox-label label-JWQiNe da-label">{this.props.text}</div>
<div className="checkbox-wrapper checkbox-3kaeSU da-checkbox checkbox-3EVISJ da-checkbox" onClick={this.onClick}>
export default function Checkbox({checked: initialState, text, onChange: notifyParent}) {
const [checked, setChecked] = useState(initialState);
const onClick = useCallback(() => {
notifyParent?.(!checked);
setChecked(!checked);
}, [notifyParent, checked]);
return <div className="checkbox-item">
<div className="checkbox-label label-JWQiNe da-label">{text}</div>
<div className="checkbox-wrapper checkbox-3kaeSU da-checkbox checkbox-3EVISJ da-checkbox" onClick={onClick}>
<div className="checkbox-inner checkboxInner-3yjcPe da-checkboxInner">
<input className="checkbox checkboxElement-1qV33p da-checkboxElement" checked={this.state.checked} type="checkbox" />
<input className="checkbox checkboxElement-1qV33p da-checkboxElement" checked={checked} type="checkbox" />
<span></span>
</div>
<span></span>
</div>
</div>;
}
onClick() {
this.props.onChange(!this.state.checked);
this.setState({checked: !this.state.checked});
}
}

View File

@ -1,91 +1,65 @@
import {React, Settings, Events, Strings} from "modules";
import Editor from "./editor";
// import Checkbox from "./checkbox";
import Refresh from "../icons/reload";
import Save from "../icons/save";
import Edit from "../icons/edit";
import Detach from "../icons/detach";
export default class CssEditor extends React.Component {
const {useState, useCallback, useEffect, forwardRef, useImperativeHandle, useRef} = React;
constructor(props) {
super(props);
this.hasUnsavedChanges = false;
export default forwardRef(function CssEditor({css, openNative, update, save, onChange: notifyParent, readOnly = false, id = "bd-customcss-editor", openDetached = false}, ref) {
const editorRef = useRef(null);
const [hasUnsavedChanges, setUnsaved] = useState(false);
this.onChange = this.onChange.bind(this);
this.toggleLiveUpdate = this.toggleLiveUpdate.bind(this);
this.updateCss = this.updateCss.bind(this);
this.saveCss = this.saveCss.bind(this);
this.openDetached = this.props.openDetached ? this.openDetached.bind(this) : null;
this.openNative = this.openNative.bind(this);
this.updateEditor = this.updateEditor.bind(this);
const updateEditor = useCallback((newCSS) => {
editorRef.current.value = newCSS;
}, [editorRef]);
this.controls = [
{label: React.createElement(Refresh, {size: "18px"}), tooltip: Strings.CustomCSS.update, onClick: this.updateCss},
{label: React.createElement(Save, {size: "18px"}), tooltip: Strings.CustomCSS.save, onClick: this.saveCss},
{label: React.createElement(Edit, {size: "18px"}), tooltip: Strings.CustomCSS.openNative, onClick: this.openNative},
{label: Strings.Collections.settings.customcss.liveUpdate.name, type: "checkbox", onChange: this.toggleLiveUpdate, checked: Settings.get("settings", "customcss", "liveUpdate"), side: "right"}
];
if (this.openDetached) this.controls.push({label: React.createElement(Detach, {size: "18px"}), tooltip: Strings.CustomCSS.openDetached, onClick: this.openDetached, side: "right"});
}
useImperativeHandle(ref, () => {
return {
resize() {editorRef.current.resize();},
showSettings() {editorRef.current.showSettings();},
get value() {return editorRef.current.getValue();},
set value(newValue) {editorRef.current.setValue(newValue);},
get hasUnsavedChanges() {return hasUnsavedChanges;}
};
}, [hasUnsavedChanges]);
componentDidMount() {
Events.on("customcss-updated", this.updateEditor);
}
useEffect(() => {
Events.on("customcss-updated", updateEditor);
return () => Events.off("customcss-updated", updateEditor);
}, [updateEditor]);
componentWillUnmount() {
Events.off("customcss-updated", this.updateEditor);
}
const toggleLiveUpdate = useCallback((checked) => Settings.set("settings", "customcss", "liveUpdate", checked), []);
const updateCss = useCallback((event, newCSS) => update?.(newCSS), [update]);
const popoutNative = useCallback(() => openNative?.(), [openNative]);
const popout = useCallback((event, currentCSS) => openDetached?.(currentCSS), [openDetached]);
updateEditor(newCSS) {
if (!this.editor) return;
this.editor.value = newCSS;
}
const onChange = useCallback((newCSS) => {
notifyParent?.(newCSS);
setUnsaved(true);
}, [notifyParent]);
get value() {return this.editor.session.getValue();}
set value(newValue) {
this.editor.setValue(newValue);
}
const saveCss = useCallback((event, newCSS) => {
save?.(newCSS);
setUnsaved(false);
}, [save]);
showSettings() {return this.editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(this.editor);}
resize() {return this.editor.resize();}
setEditorRef(editor) {
this.editor = editor;
if (this.props.editorRef && typeof(this.props.editorRef.current) !== "undefined") this.props.editorRef.current = editor;
else if (this.props.editorRef) this.props.editorRef = editor;
}
onChange() {
this.hasUnsavedChanges = true;
if (this.props.onChange) this.props.onChange(...arguments);
}
render() {
return <Editor ref={this.setEditorRef.bind(this)} readOnly={this.props.readOnly} id={this.props.id || "bd-customcss-editor"} onChange={this.onChange} controls={this.controls} value={this.props.css} />;
}
toggleLiveUpdate(checked) {
Settings.set("settings", "customcss", "liveUpdate", checked);
}
updateCss(event, newCss) {
if (this.props.update) this.props.update(newCss);
}
saveCss(event, newCss) {
this.hasUnsavedChanges = false;
if (this.props.save) this.props.save(newCss);
}
openDetached(event, currentCSS) {
if (!this.props.openDetached) return;
this.props.openDetached(currentCSS);
}
openNative() {
if (this.props.openNative) this.props.openNative();
}
}
return <Editor
ref={editorRef}
readOnly={readOnly}
id={id}
onChange={onChange}
controls={[
{label: <Refresh size="18px" />, tooltip: Strings.CustomCSS.update, onClick: updateCss},
{label: <Save size="18px" />, tooltip: Strings.CustomCSS.save, onClick: saveCss},
{label: <Edit size="18px" />, tooltip: Strings.CustomCSS.openNative, onClick: popoutNative},
{label: Strings.Collections.settings.customcss.liveUpdate.name, type: "checkbox", onChange: toggleLiveUpdate, checked: Settings.get("settings", "customcss", "liveUpdate"), side: "right"},
openDetached && {label: <Detach size="18px" />, tooltip: Strings.CustomCSS.openDetached, onClick: popout, side: "right"}
].filter(c => c)}
value={css}
/>;
});

View File

@ -2,41 +2,79 @@ import {React, DiscordModules, Settings} from "modules";
import Checkbox from "./checkbox";
const {useState, useCallback, useEffect, forwardRef, useMemo, useImperativeHandle} = React;
const ThemeStore = DiscordModules.ThemeStore;
const languages = ["abap", "abc", "actionscript", "ada", "apache_conf", "asciidoc", "assembly_x86", "autohotkey", "batchfile", "bro", "c_cpp", "c9search", "cirru", "clojure", "cobol", "coffee", "coldfusion", "csharp", "csound_document", "csound_orchestra", "csound_score", "css", "curly", "d", "dart", "diff", "dockerfile", "dot", "drools", "dummy", "dummysyntax", "eiffel", "ejs", "elixir", "elm", "erlang", "forth", "fortran", "ftl", "gcode", "gherkin", "gitignore", "glsl", "gobstones", "golang", "graphqlschema", "groovy", "haml", "handlebars", "haskell", "haskell_cabal", "haxe", "hjson", "html", "html_elixir", "html_ruby", "ini", "io", "jack", "jade", "java", "javascript", "json", "jsoniq", "jsp", "jssm", "jsx", "julia", "kotlin", "latex", "less", "liquid", "lisp", "livescript", "logiql", "lsl", "lua", "luapage", "lucene", "makefile", "markdown", "mask", "matlab", "maze", "mel", "mushcode", "mysql", "nix", "nsis", "objectivec", "ocaml", "pascal", "perl", "pgsql", "php", "pig", "powershell", "praat", "prolog", "properties", "protobuf", "python", "r", "razor", "rdoc", "red", "rhtml", "rst", "ruby", "rust", "sass", "scad", "scala", "scheme", "scss", "sh", "sjs", "smarty", "snippets", "soy_template", "space", "sql", "sqlserver", "stylus", "svg", "swift", "tcl", "tex", "text", "textile", "toml", "tsx", "twig", "typescript", "vala", "vbscript", "velocity", "verilog", "vhdl", "wollok", "xml", "xquery", "yaml", "django"];
export default class CodeEditor extends React.Component {
static get defaultId() {return "bd-editor";}
function makeButton(button, value) {
return <DiscordModules.Tooltip color="primary" position="top" text={button.tooltip}>
{props => {
return <button {...props} className="btn btn-primary" onClick={(event) => {button.onClick(event, value?.());}}>{button.label}</button>;
}}
</DiscordModules.Tooltip>;
}
constructor(props) {
super(props);
function makeCheckbox(checkbox) {
return <Checkbox text={checkbox.label} onChange={checkbox.onChange} checked={checkbox.checked} />;
}
this.props.theme = ThemeStore?.theme === "light" ? "vs" : "vs-dark";
function buildControl(value, control) {
if (control.type == "checkbox") return makeCheckbox(control);
return makeButton(control, value);
}
this.props.language = this.props.language.toLowerCase().replace(/ /g, "_");
if (!languages.includes(this.props.language)) this.props.language = CodeEditor.defaultProps.language;
export default forwardRef(function CodeEditor({value, language: requestedLang = "css", id = "bd-editor", controls = [], onChange: notifyParent}, ref) {
const language = useMemo(() => {
const requested = requestedLang.toLowerCase().replace(/ /g, "_");
if (!languages.includes(requested)) return "css";
return requested;
}, [requestedLang]);
this.bindings = [];
this.resize = this.resize.bind(this);
this.onChange = this.onChange.bind(this);
this.onThemeChange = this.onThemeChange.bind(this);
}
const [theme, setTheme] = useState(() => ThemeStore?.theme === "light" ? "vs" : "vs-dark");
const [editor, setEditor] = useState(null);
const [, setBindings] = useState([]);
static get defaultProps() {
const onThemeChange = useCallback(() => {
const newTheme = ThemeStore?.theme === "light" ? "vs" : "vs-dark";
if (newTheme === theme) return;
if (window.monaco?.editor) window.monaco.editor.setTheme(newTheme);
setTheme(newTheme);
}, [theme]);
const onChange = useCallback(() => {
notifyParent?.(editor?.getValue());
}, [editor, notifyParent]);
const resize = useCallback(() => editor.layout(), [editor]);
const showSettings = useCallback(() => editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(editor), [editor]);
useImperativeHandle(ref, () => {
return {
controls: [],
language: "css",
id: this.defaultId
resize,
showSettings,
get value() {return editor.getValue();},
set value(newValue) {editor.setValue(newValue);}
};
}
}, [editor, resize, showSettings]);
componentDidMount() {
useEffect(() => {
setBindings(bins => [...bins, editor?.onDidChangeModelContent(onChange)]);
return () => {
setBindings(bins => {
for (const binding of bins) binding?.dispose();
return [];
});
};
}, [editor, onChange]);
useEffect(() => {
let toDispose = null;
if (window.monaco?.editor) {
this.editor = window.monaco.editor.create(document.getElementById(this.props.id), {
value: this.props.value,
language: this.props.language,
theme: ThemeStore?.theme == "light" ? "vs" : "vs-dark",
const monacoEditor = window.monaco.editor.create(document.getElementById(id), {
value: value,
language: language,
theme: theme,
fontSize: Settings.get("settings", "editor", "fontSize"),
lineNumbers: Settings.get("settings", "editor", "lineNumbers"),
minimap: {enabled: Settings.get("settings", "editor", "minimap")},
@ -49,89 +87,61 @@ export default class CodeEditor extends React.Component {
renderWhitespace: Settings.get("settings", "editor", "renderWhitespace")
});
this.bindings.push(this.editor.onDidChangeModelContent(this.onChange));
toDispose = monacoEditor;
setEditor(monacoEditor);
}
else {
const textarea = document.createElement("textarea");
textarea.className = "bd-fallback-editor";
textarea.value = this.props.value;
textarea.onchange = (e) => this.onChange(e.target.value);
textarea.oninput = (e) => this.onChange(e.target.value);
textarea.value = value;
this.editor = {
setEditor({
dispose: () => textarea.remove(),
getValue: () => textarea.value,
setValue: (value) => textarea.value = value,
setValue: (val) => textarea.value = val,
layout: () => {},
};
onDidChangeModelContent: (cb) => {
textarea.onchange = cb;
textarea.oninput = cb;
}
});
document.getElementById(this.props.id).appendChild(textarea);
document.getElementById(id).appendChild(textarea);
}
ThemeStore?.addChangeListener?.(this.onThemeChange);
window.addEventListener("resize", this.resize);
}
return () => {
toDispose?.dispose?.();
};
}, [id, language, theme, value]);
componentWillUnmount() {
window.removeEventListener("resize", this.resize);
ThemeStore?.removeChangeListener?.(this.onThemeChange);
for (const binding of this.bindings) binding.dispose();
this.editor.dispose();
}
useEffect(() => {
ThemeStore?.addChangeListener?.(onThemeChange);
window.addEventListener("resize", resize);
onThemeChange() {
const newTheme = ThemeStore?.theme === "light" ? "vs" : "vs-dark";
if (newTheme === this.props.theme) return;
this.props.theme = newTheme;
if (window.monaco?.editor) window.monaco.editor.setTheme(this.props.theme);
}
return () => {
window.removeEventListener("resize", resize);
ThemeStore?.removeChangeListener?.(onThemeChange);
};
}, [onThemeChange, resize]);
get value() {return this.editor.getValue();}
set value(newValue) {this.editor.setValue(newValue);}
onChange() {
if (this.props.onChange) this.props.onChange(this.value);
}
if (editor && editor.layout) editor.layout();
showSettings() {return this.editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(this.editor);}
resize() {this.editor.layout();}
const controlsLeft = controls.filter(c => c.side != "right").map(buildControl.bind(null, () => editor?.getValue()));
const controlsRight = controls.filter(c => c.side == "right").map(buildControl.bind(null, () => editor?.getValue()));
buildControl(control) {
if (control.type == "checkbox") return this.makeCheckbox(control);
return this.makeButton(control);
}
makeCheckbox(checkbox) {
return <Checkbox text={checkbox.label} onChange={checkbox.onChange} checked={checkbox.checked} />;
}
makeButton(button) {
return <DiscordModules.Tooltip color="primary" position="top" text={button.tooltip}>
{props => {
return <button {...props} className="btn btn-primary" onClick={(event) => {button.onClick(event, this.value);}}>{button.label}</button>;
}}
</DiscordModules.Tooltip>;
}
render() {
if (this.editor && this.editor.layout) this.editor.layout();
const controlsLeft = this.props.controls.filter(c => c.side != "right").map(this.buildControl.bind(this));
const controlsRight = this.props.controls.filter(c => c.side == "right").map(this.buildControl.bind(this));
return <div id="bd-editor-panel" className={this.props.theme}>
<div id="bd-editor-controls">
<div className="controls-section controls-left">
{controlsLeft}
</div>
<div className="controls-section controls-right">
{controlsRight}
</div>
return <div id="bd-editor-panel" className={theme}>
<div id="bd-editor-controls">
<div className="controls-section controls-left">
{controlsLeft}
</div>
<div className="editor-wrapper">
<div id={this.props.id} className={"editor " + this.props.theme}></div>
<div className="controls-section controls-right">
{controlsRight}
</div>
</div>;
}
}
</div>
<div className="editor-wrapper">
<div id={id} className={"editor " + theme}></div>
</div>
</div>;
});

View File

@ -1,54 +1,36 @@
import {React} from "modules";
import {React, Events} from "modules";
import FloatingWindow from "./window";
class FloatingWindowContainer extends React.Component {
const {useState, useCallback, useEffect} = React;
constructor(props) {
super(props);
this.state = {windows: []};
}
get minY() {
const appContainer = document.querySelector(`#app-mount > div[class*="app-"]`);
if (appContainer) return appContainer.offsetTop;
return 0;
}
render() {
return this.state.windows.map(window =>
<FloatingWindow {...window} close={this.close.bind(this, window.id)} minY={this.minY} key={window.id}>
{window.children}
</FloatingWindow>
);
}
open(window) {
this.setState(state => {
state.windows.push(window);
return {windows: state.windows};
});
}
close(id) {
this.setState(state => {
return {
windows: state.windows.filter(w => {
if (w.id == id && w.onClose) w.onClose();
return w.id != id;
})
};
});
}
static get id() {return "floating-windows";}
static get root() {
if (this._root) return this._root;
const container = document.createElement("div");
container.id = this.id;
document.body.append(container);
return this._root = container;
}
function minY() {
const appContainer = document.querySelector(`#app-mount > div[class*="app-"]`);
if (appContainer) return appContainer.offsetTop;
return 0;
}
export default FloatingWindowContainer;
export default function FloatingWindowContainer() {
const [windows, setWindows] = useState([]);
const open = useCallback(window => {
setWindows(wins => [...wins, window]);
}, []);
const close = useCallback(id => {
setWindows(windows.filter(w => {
if (w.id === id && w.onClose) w.onClose();
return w.id !== id;
}));
}, [windows]);
useEffect(() => {
Events.on("open-window", open);
return () => Events.off("open-window", open);
}, [open]);
return windows.map(window =>
<FloatingWindow {...window} close={() => close(window.id)} minY={minY()} key={window.id}>
{window.children}
</FloatingWindow>
);
}

View File

@ -5,174 +5,153 @@ import CloseButton from "../icons/close";
import MaximizeIcon from "../icons/fullscreen";
import Modals from "../modals";
// const Draggable = WebpackModules.getByDisplayName("Draggable");
// {
// "dragAnywhere": true,
// "className": "pictureInPictureWindow-1B5qSe",
// "maxX": 1969,
// "maxY": this.maxY,
// "onDragStart": "ƒ () {}",
// "onDrag": "ƒ () {}",
// "onDragEnd": "ƒ () {}",
// "children": "<div />",
// "initialX": 0,
// "initialY": 0
// }
const {useState, useCallback, useEffect, useRef} = React;
export default class FloatingWindow extends React.Component {
constructor(props) {
super(props);
function confirmClose(confirmationText) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, confirmationText, {
danger: true,
confirmText: Strings.Modals.close,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
this.state = {modalOpen: false};
export default function FloatingWindow({id, title, resizable, children, className, center, top: initialTop, left: initialLeft, width: initialWidth, height: initialHeight, minX = 0, minY = 0, maxX = Screen.width, maxY = Screen.height, onResize, close: doClose, confirmClose: doConfirmClose, confirmationText}) {
const [modalOpen, setOpen] = useState(false);
const [isDragging, setDragging] = useState(false);
const [position, setPosition] = useState({x: center ? (Screen.width / 2) - (initialWidth / 2) : initialLeft, y: center ? (Screen.height / 2) - (initialHeight / 2) : initialTop});
const [offset, setOffset] = useState({x: 0, y: 0});
const [size, setSize] = useState({width: 0, height: 0});
this.offX = 0;
this.offY = 0;
const titlebar = useRef(null);
const window = useRef(null);
this.maxX = this.props.maxX || Screen.width;
this.maxY = this.props.maxY || Screen.height;
this.minX = this.props.minX || 0;
this.minY = this.props.minY || 0;
this.titlebar = React.createRef();
this.window = React.createRef();
const onResizeStart = useCallback(() => {
setSize({width: window.current.offsetWidth, height: window.current.offsetHeight});
}, [window]);
this.close = this.close.bind(this);
this.maximize = this.maximize.bind(this);
this.onDrag = this.onDrag.bind(this);
this.onDragStart = this.onDragStart.bind(this);
this.onDragStop = this.onDragStop.bind(this);
this.onResizeStart = this.onResizeStart.bind(this);
}
componentDidMount() {
this.window.current.addEventListener("mousedown", this.onResizeStart, false);
this.titlebar.current.addEventListener("mousedown", this.onDragStart, false);
document.addEventListener("mouseup", this.onDragStop, false);
}
const onDrag = useCallback((e) => {
if (!isDragging) return;
let newTop = (e.clientY - offset.y);
if (newTop <= minY) newTop = minY;
if (newTop + size.height >= maxY) newTop = maxY - size.height;
onResizeStart() {
this.currentWidth = this.window.current.offsetWidth;
this.currentHeight = this.window.current.offsetHeight;
}
let newLeft = (e.clientX - offset.x);
if (newLeft <= minX) newLeft = minX;
if (newLeft + size.width >= maxX) newLeft = maxX - size.width;
onDragStop() {
document.removeEventListener("mousemove", this.onDrag, true);
const width = this.window.current.offsetWidth;
const height = this.window.current.offsetHeight;
if (width != this.currentWidth || height != this.currentHeight) {
if (this.props.onResize) this.props.onResize();
const left = parseInt(this.window.current.style.left);
const top = parseInt(this.window.current.style.top);
if (left + width >= this.maxX) this.window.current.style.width = (this.maxX - left) + "px";
if (top + height >= this.maxY) this.window.current.style.height = (this.maxY - top) + "px";
setPosition({x: newLeft, y: newTop});
}, [offset, size, isDragging, minX, minY, maxX, maxY]);
const onDragStart = useCallback((e) => {
const div = window.current;
setOffset({x: e.clientX - parseInt(div.offsetLeft), y: e.clientY - parseInt(div.offsetTop)});
setDragging(true);
}, [window]);
const onDragStop = useCallback(() => {
setDragging(false);
const width = window.current.offsetWidth;
const height = window.current.offsetHeight;
if (width != size.width || height != size.height) {
if (onResize) onResize();
const left = parseInt(window.current.style.left);
const top = parseInt(window.current.style.top);
if (left + width >= maxX) window.current.style.width = (maxX - left) + "px";
if (top + height >= maxY) window.current.style.height = (maxY - top) + "px";
}
this.currentWidth = width;
this.currentHeight = height;
}
onDragStart(e) {
const div = this.window.current;
this.offY = e.clientY - parseInt(div.offsetTop);
this.offX = e.clientX - parseInt(div.offsetLeft);
document.addEventListener("mousemove", this.onDrag, true);
}
setSize({width, height});
}, [window, size, maxX, maxY, onResize]);
onDrag(e) {
const div = this.window.current;
let newTop = (e.clientY - this.offY);
if (newTop <= this.minY) newTop = this.minY;
if (newTop + this.currentHeight >= this.maxY) newTop = this.maxY - this.currentHeight;
let newLeft = (e.clientX - this.offX);
if (newLeft <= this.minX) newLeft = this.minX;
if (newLeft + this.currentWidth >= this.maxX) newLeft = this.maxX - this.currentWidth;
useEffect(() => {
const winRef = window.current;
const titleRef = titlebar.current;
winRef.addEventListener("mousedown", onResizeStart, false);
titleRef.addEventListener("mousedown", onDragStart, false);
document.addEventListener("mouseup", onDragStop, false);
document.addEventListener("mousemove", onDrag, true);
div.style.top = newTop + "px";
div.style.left = newLeft + "px";
}
return () => {
document.removeEventListener("mouseup", onDragStop, false);
document.removeEventListener("mousemove", onDrag, true);
winRef.removeEventListener("mousedown", onResizeStart, false);
titleRef.removeEventListener("mousedown", onDragStart, false);
};
}, [titlebar, window, onDragStart, onDragStop, onDrag, onResizeStart]);
componentWillUnmount() {
this.titlebar.current.removeEventListener("mousedown", this.onDragStart, false);
document.removeEventListener("mouseup", this.onDragStop, false);
}
render() {
const top = this.props.center ? (Screen.height / 2) - (this.props.height / 2) : this.props.top;
const left = this.props.center ? (Screen.width / 2) - (this.props.width / 2) : this.props.left;
// console.log(top, left);
const className = `floating-window${` ${this.props.className}` || ""}${this.props.resizable ? " resizable" : ""}${this.state.modalOpen ? " modal-open" : ""}`;
const styles = {height: this.props.height, width: this.props.width, left: left || 0, top: top || 0};
return <div id={this.props.id} className={className} ref={this.window} style={styles}>
<div className="floating-window-titlebar" ref={this.titlebar}>
<span className="title">{this.props.title}</span>
<div className="floating-window-buttons">
<div className="button maximize-button" onClick={this.maximize}>
<MaximizeIcon size="18px" />
</div>
<div className="button close-button" onClick={this.close}>
<CloseButton />
</div>
</div>
</div>
<div className="floating-window-content">
{this.props.children}
</div>
</div>;
}
const maximize = useCallback(() => {
window.current.style.width = "100%";
window.current.style.height = "100%";
if (onResize) onResize();
maximize() {
this.window.current.style.width = "100%";
this.window.current.style.height = "100%";
if (this.props.onResize) this.props.onResize();
const width = this.window.current.offsetWidth;
const height = this.window.current.offsetHeight;
const left = parseInt(this.window.current.style.left);
const top = parseInt(this.window.current.style.top);
const width = window.current.offsetWidth;
const height = window.current.offsetHeight;
const left = parseInt(window.current.style.left);
const top = parseInt(window.current.style.top);
const right = left + width;
const bottom = top + height;
// Prevent expanding off the bottom and right and readjust position
if (bottom > this.maxY) this.window.current.style.top = (this.maxY - height) + "px";
if (right > this.maxX) this.window.current.style.left = (this.maxX - width) + "px";
if (bottom > maxY) window.current.style.top = (maxY - height) + "px";
if (right > maxX) window.current.style.left = (maxX - width) + "px";
const newLeft = parseInt(this.window.current.style.left);
const newTop = parseInt(this.window.current.style.top);
const newLeft = parseInt(window.current.style.left);
const newTop = parseInt(window.current.style.top);
// For small screens it's possible this pushes us off the other direction... we need to readjust size
if (newTop < this.minY) {
const difference = this.minY - newTop;
this.window.current.style.top = this.minY + "px";
this.window.current.style.height = (height - difference) + "px";
// For small screens it's possible pushes us off the other direction... we need to readjust size
if (newTop < minY) {
const difference = minY - newTop;
window.current.style.top = minY + "px";
window.current.style.height = (height - difference) + "px";
}
if (newLeft < this.minX) {
const difference = this.minX - newLeft;
this.window.current.style.left = this.minX + "px";
this.window.current.style.height = (width - difference) + "px";
if (newLeft < minX) {
const difference = minX - newLeft;
window.current.style.left = minX + "px";
window.current.style.height = (width - difference) + "px";
}
}
}, [window, minX, minY, maxX, maxY, onResize]);
async close() {
const close = useCallback(async () => {
let shouldClose = true;
const confirmClose = typeof(this.props.confirmClose) == "function" ? this.props.confirmClose() : this.props.confirmClose;
if (confirmClose) {
this.setState({modalOpen: true});
shouldClose = await this.confirmClose();
this.setState({modalOpen: false});
const didConfirmClose = typeof(doConfirmClose) == "function" ? doConfirmClose() : doConfirmClose;
if (didConfirmClose) {
setOpen(true);
shouldClose = await confirmClose(confirmationText);
setOpen(false);
}
if (this.props.close && shouldClose) this.props.close();
}
if (doClose && shouldClose) doClose();
}, [confirmationText, doClose, doConfirmClose]);
confirmClose() {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, this.props.confirmationText, {
danger: true,
confirmText: Strings.Modals.close,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
const finalClassname = `floating-window${` ${className}` || ""}${resizable ? " resizable" : ""}${modalOpen ? " modal-open" : ""}`;
const styles = {height: initialHeight, width: initialWidth, left: position.x || 0, top: position.y || 0};
return <div id={id} className={finalClassname} ref={window} style={styles}>
<div className="floating-window-titlebar" ref={titlebar}>
<span className="title">{title}</span>
<div className="floating-window-buttons">
<div className="button maximize-button" onClick={maximize}>
<MaximizeIcon size="18px" />
</div>
<div className="button close-button" onClick={close}>
<CloseButton />
</div>
</div>
</div>
<div className="floating-window-content">
{children}
</div>
</div>;
}

View File

@ -1,25 +1,25 @@
import {WebpackModules, React, ReactDOM, DOMManager} from "modules";
import {WebpackModules, React, ReactDOM, DOMManager, Events} from "modules";
import FloatingWindowContainer from "./floating/container";
/* eslint-disable new-cap */
const AppLayerProvider = WebpackModules.getByDisplayName("AppLayerProvider");
let hasInitialized = false;
export default class FloatingWindows {
static initialize() {
const containerRef = React.createRef();
const container = <FloatingWindowContainer ref={containerRef} />;
const container = <FloatingWindowContainer />;
const wrapped = AppLayerProvider
? React.createElement(AppLayerProvider().props.layerContext.Provider, {value: [document.querySelector("#app-mount > .layerContainer-2v_Sit")]}, container) // eslint-disable-line new-cap
: container;
const div = DOMManager.parseHTML(`<div id="floating-windows-layer">`);
DOMManager.bdBody.append(div);
ReactDOM.render(wrapped, div);
this.ref = containerRef;
hasInitialized = true;
}
static open(window) {
if (!this.ref) this.initialize();
return this.ref.current.open(window);
if (!hasInitialized) this.initialize();
return Events.emit("open-window", window);
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class BDLogo extends React.Component {
render() {
return <svg className={"bd-logo " + this.props.className} height="100%" width={this.props.size || "16px"} viewBox="0 0 2000 2000">
<g>
<path fill="#3E82E5" d="M1402.2,631.7c-9.7-353.4-286.2-496-642.6-496H68.4v714.1l442,398V490.7h257c274.5,0,274.5,344.9,0,344.9H597.6v329.5h169.8c274.5,0,274.5,344.8,0,344.8h-699v354.9h691.2c356.3,0,632.8-142.6,642.6-496c0-162.6-44.5-284.1-122.9-368.6C1357.7,915.8,1402.2,794.3,1402.2,631.7z"/><path fill="#FFFFFF" d="M1262.5,135.2L1262.5,135.2l-76.8,0c26.6,13.3,51.7,28.1,75,44.3c70.7,49.1,126.1,111.5,164.6,185.3c39.9,76.6,61.5,165.6,64.3,264.6l0,1.2v1.2c0,141.1,0,596.1,0,737.1v1.2l0,1.2c-2.7,99-24.3,188-64.3,264.6c-38.5,73.8-93.8,136.2-164.6,185.3c-22.6,15.7-46.9,30.1-72.6,43.1h72.5c346.2,1.9,671-171.2,671-567.9V716.7C1933.5,312.2,1608.7,135.2,1262.5,135.2z"/>
</g>
</svg>;
}
}
export default function BDLogo(props) {
return <svg className={"bd-logo " + props.className} height="100%" width={props.size || "16px"} viewBox="0 0 2000 2000">
<g>
<path fill="#3E82E5" d="M1402.2,631.7c-9.7-353.4-286.2-496-642.6-496H68.4v714.1l442,398V490.7h257c274.5,0,274.5,344.9,0,344.9H597.6v329.5h169.8c274.5,0,274.5,344.8,0,344.8h-699v354.9h691.2c356.3,0,632.8-142.6,642.6-496c0-162.6-44.5-284.1-122.9-368.6C1357.7,915.8,1402.2,794.3,1402.2,631.7z"/><path fill="#FFFFFF" d="M1262.5,135.2L1262.5,135.2l-76.8,0c26.6,13.3,51.7,28.1,75,44.3c70.7,49.1,126.1,111.5,164.6,185.3c39.9,76.6,61.5,165.6,64.3,264.6l0,1.2v1.2c0,141.1,0,596.1,0,737.1v1.2l0,1.2c-2.7,99-24.3,188-64.3,264.6c-38.5,73.8-93.8,136.2-164.6,185.3c-22.6,15.7-46.9,30.1-72.6,43.1h72.5c346.2,1.9,671-171.2,671-567.9V716.7C1933.5,312.2,1608.7,135.2,1262.5,135.2z"/>
</g>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Checkmark extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" className={this.props.className || ""} style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
</svg>;
}
export default function Checkmark(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" className={props.className || ""} style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
</svg>;
}

View File

@ -1,13 +1,11 @@
import {React} from "modules";
export default class CloseButton extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="0 0 12 12" style={{width: size, height: size}}>
<g className="background" fill="none" fillRule="evenodd">
<path d="M0 0h12v12H0" />
<path className="fill" fill="#dcddde" d="M9.5 3.205L8.795 2.5 6 5.295 3.205 2.5l-.705.705L5.295 6 2.5 8.795l.705.705L6 6.705 8.795 9.5l.705-.705L6.705 6" />
</g>
</svg>;
}
export default function Close(props) {
const size = props.size || "18px";
return <svg viewBox="0 0 12 12" style={{width: size, height: size}}>
<g className="background" fill="none" fillRule="evenodd">
<path d="M0 0h12v12H0" />
<path className="fill" fill="#dcddde" d="M9.5 3.205L8.795 2.5 6 5.295 3.205 2.5l-.705.705L5.295 6 2.5 8.795l.705.705L6 6.705 8.795 9.5l.705-.705L6.705 6" />
</g>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Cog extends React.Component {
render() {
const size = this.props.size || "20px";
return <svg viewBox="0 0 20 20" style={{width: size, height: size}}>
<path fill="none" d="M0 0h20v20H0V0z" />
<path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z" />
</svg>;
}
export default function Cog(props) {
const size = props.size || "20px";
return <svg viewBox="0 0 20 20" style={{width: size, height: size}}>
<path fill="none" d="M0 0h20v20H0V0z" />
<path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z" />
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Delete extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={this.props.onClick}>
<path fill="none" d="M0 0h24v24H0V0z"/><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z"/>
<path fill="none" d="M0 0h24v24H0z"/>
</svg>;
}
export default function Delete(props) {
const size = props.size || "24px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={props.onClick}>
<path fill="none" d="M0 0h24v24H0V0z"/><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z"/>
<path fill="none" d="M0 0h24v24H0z"/>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Detach extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z" />
</svg>;
}
export default function Detach(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z" />
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class DollarSign extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"/>
export default function DollarSign(props) {
const size = props.size || "18px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"/>
</svg>;
}
}

View File

@ -1,10 +1,8 @@
import {React} from "modules";
export default class DownArrow extends React.Component {
render() {
const size = this.props.size || "16px";
return <svg className={this.props.className || ""} fill="currentColor" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M8.12 9.29L12 13.17l3.88-3.88c.39-.39 1.02-.39 1.41 0 .39.39.39 1.02 0 1.41l-4.59 4.59c-.39.39-1.02.39-1.41 0L6.7 10.7c-.39-.39-.39-1.02 0-1.41.39-.38 1.03-.39 1.42 0z"/>
</svg>;
}
export default function DownArrow(props) {
const size = props.size || "16px";
return <svg className={props.className || ""} fill="currentColor" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M8.12 9.29L12 13.17l3.88-3.88c.39-.39 1.02-.39 1.41 0 .39.39.39 1.02 0 1.41l-4.59 4.59c-.39.39-1.02.39-1.41 0L6.7 10.7c-.39-.39-.39-1.02 0-1.41.39-.38 1.03-.39 1.42 0z"/>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Edit extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>;
}
export default function Edit(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>;
}

View File

@ -1,12 +1,9 @@
import {React} from "modules";
export default class Error extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick} className={this.props.className}>
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
</svg>;
}
export default function Error(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick} className={props.className}>
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Extension extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick} className={this.props.className}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/>
</svg>;
}
}
export default function Extension(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick} className={props.className}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/>
</svg>;
}

View File

@ -1,9 +1,8 @@
import {React} from "modules";
export default class Favorite extends React.Component {
render() {
return <svg aria-hidden="false" width="16" height="16" viewBox="0 0 24 24">
<path fill="currentColor" d="M21.924 8.61789C21.77 8.24489 21.404 8.00089 21 8.00089H15.618L12.894 2.55389C12.555 1.87689 11.444 1.87689 11.105 2.55389L8.38199 8.00089H2.99999C2.59599 8.00089 2.22999 8.24489 2.07599 8.61789C1.92199 8.99089 2.00699 9.42289 2.29299 9.70789L6.87699 14.2919L5.03899 20.7269C4.92399 21.1299 5.07199 21.5619 5.40999 21.8089C5.74999 22.0569 6.20699 22.0659 6.55399 21.8329L12 18.2029L17.445 21.8329C17.613 21.9449 17.806 22.0009 18 22.0009C18.207 22.0009 18.414 21.9369 18.59 21.8089C18.928 21.5619 19.076 21.1299 18.961 20.7269L17.123 14.2919L21.707 9.70789C21.993 9.42289 22.078 8.99089 21.924 8.61789Z"></path>
export default function Favorite(props) {
const size = props.size || "24px";
return <svg aria-hidden="false" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path fill="currentColor" d="M21.924 8.61789C21.77 8.24489 21.404 8.00089 21 8.00089H15.618L12.894 2.55389C12.555 1.87689 11.444 1.87689 11.105 2.55389L8.38199 8.00089H2.99999C2.59599 8.00089 2.22999 8.24489 2.07599 8.61789C1.92199 8.99089 2.00699 9.42289 2.29299 9.70789L6.87699 14.2919L5.03899 20.7269C4.92399 21.1299 5.07199 21.5619 5.40999 21.8089C5.74999 22.0569 6.20699 22.0659 6.55399 21.8329L12 18.2029L17.445 21.8329C17.613 21.9449 17.806 22.0009 18 22.0009C18.207 22.0009 18.414 21.9369 18.59 21.8089C18.928 21.5619 19.076 21.1299 18.961 20.7269L17.123 14.2919L21.707 9.70789C21.993 9.42289 22.078 8.99089 21.924 8.61789Z"></path>
</svg>;
}
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class FullScreen extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={this.props.onClick}>
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>;
}
}
export default function FullScreen(props) {
const size = props.size || "24px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={props.onClick}>
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>;
}

View File

@ -1,10 +1,8 @@
import {React} from "modules";
export default class GitHub extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="m12 .5c-6.63 0-12 5.28-12 11.792 0 5.211 3.438 9.63 8.205 11.188.6.111.82-.254.82-.567 0-.28-.01-1.022-.015-2.005-3.338.711-4.042-1.582-4.042-1.582-.546-1.361-1.335-1.725-1.335-1.725-1.087-.731.084-.716.084-.716 1.205.082 1.838 1.215 1.838 1.215 1.07 1.803 2.809 1.282 3.495.981.108-.763.417-1.282.76-1.577-2.665-.295-5.466-1.309-5.466-5.827 0-1.287.465-2.339 1.235-3.164-.135-.298-.54-1.497.105-3.121 0 0 1.005-.316 3.3 1.209.96-.262 1.98-.392 3-.398 1.02.006 2.04.136 3 .398 2.28-1.525 3.285-1.209 3.285-1.209.645 1.624.24 2.823.12 3.121.765.825 1.23 1.877 1.23 3.164 0 4.53-2.805 5.527-5.475 5.817.42.354.81 1.077.81 2.182 0 1.578-.015 2.846-.015 3.229 0 .309.21.678.825.56 4.801-1.548 8.236-5.97 8.236-11.173 0-6.512-5.373-11.792-12-11.792z" />
</svg>;
}
}
export default function GitHub(props) {
const size = props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick}>
<path d="m12 .5c-6.63 0-12 5.28-12 11.792 0 5.211 3.438 9.63 8.205 11.188.6.111.82-.254.82-.567 0-.28-.01-1.022-.015-2.005-3.338.711-4.042-1.582-4.042-1.582-.546-1.361-1.335-1.725-1.335-1.725-1.087-.731.084-.716.084-.716 1.205.082 1.838 1.215 1.838 1.215 1.07 1.803 2.809 1.282 3.495.981.108-.763.417-1.282.76-1.577-2.665-.295-5.466-1.309-5.466-5.827 0-1.287.465-2.339 1.235-3.164-.135-.298-.54-1.497.105-3.121 0 0 1.005-.316 3.3 1.209.96-.262 1.98-.392 3-.398 1.02.006 2.04.136 3 .398 2.28-1.525 3.285-1.209 3.285-1.209.645 1.624.24 2.823.12 3.121.765.825 1.23 1.877 1.23 3.164 0 4.53-2.805 5.527-5.475 5.817.42.354.81 1.077.81 2.182 0 1.578-.015 2.846-.015 3.229 0 .309.21.678.825.56 4.801-1.548 8.236-5.97 8.236-11.173 0-6.512-5.373-11.792-12-11.792z" />
</svg>;
}

View File

@ -1,12 +1,10 @@
import {React} from "modules";
export default class Globe extends React.Component {
render() {
const size = this.props.size || "18px";
const color = this.props.color || "#FFFFFF";
return <svg viewBox="2 2 20 20" fill={color} style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"/>
</svg>;
}
}
export default function Globe(props) {
const size = props.size || "18px";
const color = props.color || "#FFFFFF";
return <svg viewBox="2 2 20 20" fill={color} style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"/>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Grid extends React.Component {
render() {
const size = this.props.size || "20px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M4 11h5V5H4v6zm0 7h5v-6H4v6zm6 0h5v-6h-5v6zm6 0h5v-6h-5v6zm-6-7h5V5h-5v6zm6-6v6h5V5h-5z"/>
</svg>;
}
}
export default function Grid(props) {
const size = props.size || "20px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M4 11h5V5H4v6zm0 7h5v-6H4v6zm6 0h5v-6h-5v6zm6 0h5v-6h-5v6zm-6-7h5V5h-5v6zm6-6v6h5V5h-5z"/>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class History extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" className={this.props.className || ""} style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/>
</svg>;
}
}
export default function History(props) {
const size = props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" className={props.className || ""} style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Keyboard extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className} viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}}>
<path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm9 7H8v-2h8v2zm0-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z" />
<path fill="none" d="M0 0h24v24H0zm0 0h24v24H0z" />
</svg>;
}
}
export default function Keyboard(props) {
const size = props.size || "24px";
return <svg className={props.className} viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}}>
<path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm9 7H8v-2h8v2zm0-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z" />
<path fill="none" d="M0 0h24v24H0zm0 0h24v24H0z" />
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class List extends React.Component {
render() {
const size = this.props.size || "20px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M4 18h17v-6H4v6zM4 5v6h17V5H4z"/>
</svg>;
}
}
export default function List(props) {
const size = props.size || "20px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M4 18h17v-6H4v6zM4 5v6h17V5H4z"/>
</svg>;
}

View File

@ -1,11 +1,10 @@
import {React} from "modules";
export default class MagnifyingGlass extends React.Component {
render() {
const size = this.props.size || "160px";
return <svg xmlns="http://www.w3.org/2000/svg" style={{width: size, height: size}} viewBox="0 0 160 160">
<g fill="none" fillRule="evenodd">
<g transform="translate(9 9)">
export default function MagnifyingGlass(props) {
const size = props.size || "160px";
return <svg xmlns="http://www.w3.org/2000/svg" style={{width: size, height: size}} viewBox="0 0 160 160">
<g fill="none" fillRule="evenodd">
<g transform="translate(9 9)">
<path fill="rgba(0,0,0,0.1)" d="M42.1262,100.7598 C25.1382,83.7718 25.1382,56.2288 42.1262,39.2408 C59.1142,22.2538 86.6572,22.2538 103.6452,39.2408 C120.6322,56.2288 120.6322,83.7718 103.6452,100.7598 C86.6572,117.7478 59.1142,117.7478 42.1262,100.7598"/>
<path stroke="#1E2126" strokeWidth="2" d="M121.8938,119.4976 C94.5578,146.8346 50.2358,146.8346 22.8988,119.4976 C-4.4382,92.1616 -4.4382,47.8396 22.8988,20.5026 C50.2358,-6.8334 94.5578,-6.8344 121.8938,20.5026 C149.2308,47.8396 149.2308,92.1616 121.8938,119.4976 Z" strokeLinecap="round" strokeLinejoin="round" strokeDasharray="4 5"/>
<path fill="#C9D2F0" d="M1.8313,140.566 L1.8313,140.566 C-0.6097,138.125 -0.6097,134.166 1.8313,131.725 L38.6023,94.954 L47.4433,103.795 L10.6723,140.566 C8.2303,143.007 4.2723,143.007 1.8313,140.566"/>
@ -15,9 +14,8 @@ export default class MagnifyingGlass extends React.Component {
<path fill="#F3F9FF" d="M44.112,98.2847 C28.491,82.6637 28.491,57.3377 44.112,41.7167 C59.733,26.0957 85.06,26.0957 100.681,41.7157 C116.302,57.3367 116.302,82.6637 100.681,98.2847 C85.06,113.9057 59.733,113.9057 44.112,98.2847 M108.007,34.3897 C88.34,14.7227 56.453,14.7227 36.786,34.3897 C17.119,54.0567 17.119,85.9437 36.786,105.6107 C56.453,125.2777 88.34,125.2777 108.007,105.6107 C127.674,85.9437 127.674,54.0567 108.007,34.3897"/>
<path stroke="#1E2126" strokeWidth="2" d="M116.386 94.545C115.853 95.498 115.287 96.438 114.688 97.362M108.0071 105.6109C88.3401 125.2779 56.4531 125.2779 36.7861 105.6109 17.1191 85.9439 17.1191 54.0569 36.7861 34.3899 56.4531 14.7229 88.3401 14.7229 108.0071 34.3899 122.7701 49.1529 126.4511 70.7999 119.0511 88.9969" strokeLinecap="round" strokeLinejoin="round"/>
<path stroke="#1E2126" strokeWidth="2" d="M44.112,98.2847 C28.491,82.6637 28.491,57.3377 44.112,41.7167 C59.733,26.0957 85.06,26.0957 100.681,41.7157 C116.302,57.3367 116.302,82.6637 100.681,98.2847 C85.06,113.9057 59.733,113.9057 44.112,98.2847 Z" strokeLinecap="round" strokeLinejoin="round"/>
</g>
<rect width="160" height="160" y="-1"/>
</g>
</svg>;
}
}
</g>
<rect width="160" height="160" y="-1"/>
</g>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class ArrowRight extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M10 17l5-5-5-5v10z" />
<path d="M0 24V0h24v24H0z" fill="none" />
export default function ArrowRight(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M10 17l5-5-5-5v10z" />
<path d="M0 24V0h24v24H0z" fill="none" />
</svg>;
}
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Patreon extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="m0 .5h4.219v23h-4.219z"/>
<path d="m15.384.5c-4.767 0-8.644 3.873-8.644 8.633 0 4.75 3.877 8.61 8.644 8.61 4.754 0 8.616-3.865 8.616-8.61 0-4.759-3.863-8.633-8.616-8.633z"/>
</svg>;
}
}
export default function Patreon(props) {
const size = props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick}>
<path d="m0 .5h4.219v23h-4.219z"/>
<path d="m15.384.5c-4.767 0-8.644 3.873-8.644 8.633 0 4.75 3.877 8.61 8.644 8.61 4.754 0 8.616-3.865 8.616-8.61 0-4.759-3.863-8.633-8.616-8.633z"/>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class ArrowLeft extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M14 7l-5 5 5 5V7z" />
<path d="M24 0v24H0V0h24z" fill="none" />
export default function ArrowLeft(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M14 7l-5 5 5 5V7z" />
<path d="M24 0v24H0V0h24z" fill="none" />
</svg>;
}
}
}

View File

@ -1,12 +1,10 @@
import {React} from "modules";
export default class Radio extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className} viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0z" />
{this.props.checked && <path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />}
{!this.props.checked && <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />}
</svg>;
}
}
export default function Radio(props) {
const size = props.size || "24px";
return <svg className={props.className} viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0z" />
{props.checked && <path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />}
{!props.checked && <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />}
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class ReloadIcon extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className || ""} onClick={this.props.onClick} fill="#dcddde" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
<path fill="none" d="M0 0h24v24H0z" />
export default function ReloadIcon(props) {
const size = props.size || "24px";
return <svg className={props.className || ""} onClick={props.onClick} fill="#dcddde" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
<path fill="none" d="M0 0h24v24H0z" />
</svg>;
}
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Save extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm2 16H5V5h11.17L19 7.83V19zm-7-7c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3zM6 6h9v4H6z" />
</svg>;
}
}
export default function Save(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm2 16H5V5h11.17L19 7.83V19zm-7-7c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3zM6 6h9v4H6z" />
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Search extends React.Component {
render() {
const size = this.props.size || "16px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>;
}
}
export default function Search(props) {
const size = props.size || "16px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Support extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/>
</svg>;
}
}
export default function Support(props) {
const size = props.size || "18px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/>
</svg>;
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Theme extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick} className={this.props.className}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>
</svg>;
}
}
export default function Theme(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick} className={props.className}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>
</svg>;
}

View File

@ -1,9 +1,8 @@
import {React} from "modules";
export default class Twitch extends React.Component {
render() {
return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path fill="currentColor" d="M40.1 32L10 108.9v314.3h107V480h60.2l56.8-56.8h87l117-117V32H40.1zm357.8 254.1L331 353H224l-56.8 56.8V353H76.9V72.1h321v214zM331 149v116.9h-40.1V149H331zm-107 0v116.9h-40.1V149H224z"/>
export default function Twitch(props) {
const size = props.size || "18px";
return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" style={{width: size, height: size}}>
<path fill="currentColor" d="M40.1 32L10 108.9v314.3h107V480h60.2l56.8-56.8h87l117-117V32H40.1zm357.8 254.1L331 353H224l-56.8 56.8V353H76.9V72.1h321v214zM331 149v116.9h-40.1V149H331zm-107 0v116.9h-40.1V149H224z"/>
</svg>;
}
}

View File

@ -4,60 +4,39 @@ import Editor from "../customcss/editor";
import Save from "../icons/save";
import Edit from "../icons/edit";
export default class AddonEditor extends React.Component {
const {useState, useCallback, forwardRef, useImperativeHandle, useRef} = React;
constructor(props) {
super(props);
this.hasUnsavedChanges = false;
this.onChange = this.onChange.bind(this);
this.save = this.save.bind(this);
this.openNative = this.openNative.bind(this);
this.update = this.update.bind(this);
export default forwardRef(function AddonEditor({content, language, save, openNative, id = "bd-addon-editor"}, ref) {
const editorRef = useRef(null);
const [hasUnsavedChanges, setUnsaved] = useState(false);
this.controls = [
{label: React.createElement(Save, {size: "18px"}), tooltip: Strings.CustomCSS.save, onClick: this.save},
{label: React.createElement(Edit, {size: "18px"}), tooltip: Strings.CustomCSS.openNative, onClick: this.openNative}
];
}
useImperativeHandle(ref, () => {
return {
resize() {editorRef.current.resize();},
showSettings() {editorRef.current.showSettings();},
get value() {return editorRef.current.getValue();},
set value(newValue) {editorRef.current.setValue(newValue);},
get hasUnsavedChanges() {return hasUnsavedChanges;}
};
}, [hasUnsavedChanges]);
update() {
this.forceUpdate();
}
const popoutNative = useCallback(() => openNative?.(), [openNative]);
const onChange = useCallback(() => setUnsaved(true), []);
const saveAddon = useCallback((event, newCSS) => {
save?.(newCSS);
setUnsaved(false);
}, [save]);
updateEditor(newCSS) {
if (!this.editor) return;
this.editor.value = newCSS;
}
get value() {return this.editor.session.getValue();}
set value(newValue) {
this.editor.setValue(newValue);
}
showSettings() {return this.editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(this.editor);}
resize() {return this.editor.resize();}
setEditorRef(editor) {
this.editor = editor;
if (this.props.editorRef && typeof(this.props.editorRef.current) !== "undefined") this.props.editorRef.current = editor;
else if (this.props.editorRef) this.props.editorRef = editor;
}
render() {
return <Editor ref={this.setEditorRef.bind(this)} language={this.props.language} id={this.props.id || "bd-addon-editor"} controls={this.controls} value={this.props.content} onChange={this.onChange} />;
}
onChange() {
this.hasUnsavedChanges = true;
}
save(event, content) {
this.hasUnsavedChanges = false;
if (this.props.save) this.props.save(content);
}
openNative() {
if (this.props.openNative) this.props.openNative();
}
}
return <Editor
ref={editorRef}
language={language}
id={id}
controls={[
{label: <Save size="18px" />, tooltip: Strings.CustomCSS.save, onClick: saveAddon},
{label: <Edit size="18px" />, tooltip: Strings.CustomCSS.openNative, onClick: popoutNative}
]}
value={content}
onChange={onChange}
/>;
});

View File

@ -293,6 +293,11 @@ export default class Modals {
super(props);
this.elementRef = React.createRef();
this.element = panel;
this.state = {hasError: false};
}
componentDidCatch() {
this.setState({hasError: true});
}
componentDidMount() {
@ -300,6 +305,7 @@ export default class Modals {
}
render() {
if (this.state.hasError) return null;
const props = {
className: "bd-addon-settings-wrap",
ref: this.elementRef

View File

@ -1,70 +0,0 @@
import {React, Strings} from "modules";
const badge = <div className="flowerStarContainer-3zDVtj verified-1eC5dy background-2uufRq guildBadge-RlDbED"
style={{width: "16px", height: "16px"}}>
<svg aria-label="Verified &amp; Partnered" className="flowerStar-1GeTsn"
aria-hidden="false" width="16" height="16" viewBox="0 0 16 15.2">
<path fill="currentColor" fillRule="evenodd"
d="m16 7.6c0 .79-1.28 1.38-1.52 2.09s.44 2 0 2.59-1.84.35-2.46.8-.79 1.84-1.54 2.09-1.67-.8-2.47-.8-1.75 1-2.47.8-.92-1.64-1.54-2.09-2-.18-2.46-.8.23-1.84 0-2.59-1.54-1.3-1.54-2.09 1.28-1.38 1.52-2.09-.44-2 0-2.59 1.85-.35 2.48-.8.78-1.84 1.53-2.12 1.67.83 2.47.83 1.75-1 2.47-.8.91 1.64 1.53 2.09 2 .18 2.46.8-.23 1.84 0 2.59 1.54 1.3 1.54 2.09z">
</path>
</svg>
<div className="childContainer-1wxZNh">
<svg className="icon-1ihkOt" aria-hidden="false" width="16" height="16" viewBox="0 0 16 15.2">
<path d="M7.4,11.17,4,8.62,5,7.26l2,1.53L10.64,4l1.36,1Z" fill="currentColor"></path>
</svg>
</div>
</div>;
export default class ServerCard extends React.Component {
constructor(props) {
super(props);
if (!this.props.server.iconUrl) this.props.server.iconUrl = this.props.defaultAvatar();
this.state = {
joined: this.props.joined
};
this.join = this.join.bind(this);
this.handleError = this.handleError.bind(this);
}
render() {
const {server} = this.props;
const addedDate = new Date(server.insertDate * 1000); // Convert from unix timestamp
const buttonText = typeof(this.state.joined) == "string" ? `${Strings.PublicServers.joining}...` : this.state.joined ? Strings.PublicServers.joined : Strings.PublicServers.join;
return <div className="bd-server-card" role="button" tabIndex="0" onClick={this.join}>
<div className="bd-server-header">
<div className="bd-server-splash-container"><img src={server.iconUrl} onError={this.handleError} className="bd-server-splash" /></div>
<img src={server.iconUrl} onError={this.handleError} className="bd-server-icon" />
</div>
<div className="bd-server-info">
<div className="bd-server-title">
{server.pinned && badge}
<div className="bd-server-name">{server.name}</div>
{this.state.joined && <div className="bd-server-tag">{buttonText}</div>}
</div>
<div className="bd-server-description">{server.description}</div>
<div className="bd-server-footer">
<div className="bd-server-count">
<div className="bd-server-count-dot"></div>
<div className="bd-server-count-text">{server.members.toLocaleString()} Members</div>
</div>
<div className="bd-server-count">
<div className="bd-server-count-dot"></div>
<div className="bd-server-count-text">Added {addedDate.toLocaleDateString()}</div>
</div>
</div>
</div>
</div>;
}
handleError() {
this.props.server.iconUrl = this.props.defaultAvatar();
}
async join() {
if (this.state.joined) return this.props.navigateTo(this.props.server.identifier);
this.setState({joined: "joining"});
const didJoin = await this.props.join(this.props.server.identifier, this.props.server.nativejoin);
this.setState({joined: didJoin});
}
}

View File

@ -1,251 +0,0 @@
import {React, WebpackModules, Strings, DiscordModules} from "modules";
import Modals from "../modals";
import SettingsTitle from "../settings/title";
import ServerCard from "./card";
import EmptyResults from "../blankslates/noresults";
import Connection from "../../structs/psconnection";
import Search from "../settings/components/search";
import Previous from "../icons/previous";
import Next from "../icons/next";
const SettingsView = WebpackModules.getByPrototypes("renderSidebar");
const GuildActions = WebpackModules.getByProps("transitionToGuildSync");
const LayerManager = {
pushLayer(component) {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
},
popLayer() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
},
popAllLayers() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
}
};
const EMPTY_RESULTS = {
servers: [],
size: 0,
total: 0,
page: 1,
numPages: 1
};
export default class PublicServers extends React.Component {
constructor(props) {
super(props);
this.state = {
tab: "Featured",
query: "",
loading: true,
user: null,
results: Object.assign({}, EMPTY_RESULTS)
};
this.featured = [];
this.popular = [];
this.keywords = [];
this.changeTab = this.changeTab.bind(this);
this.searchKeyDown = this.searchKeyDown.bind(this);
this.connect = this.connect.bind(this);
this.loadPreviousPage = this.loadPreviousPage.bind(this);
this.loadNextPage = this.loadNextPage.bind(this);
this.join = this.join.bind(this);
this.navigateTo = this.navigateTo.bind(this);
}
componentDidMount() {
this.getDashboard();
this.checkConnection();
}
async checkConnection() {
const userData = await Connection.checkConnection();
if (!userData) return this.setState({user: null});
this.setState({user: userData});
}
async getDashboard() {
const dashboardData = await Connection.getDashboard();
this.featured = dashboardData.featured;
this.popular = dashboardData.popular;
this.keywords = dashboardData.keywords;
this.setState({loading: false});
this.changeTab(this.state.tab);
if (!this.keywords || !this.keywords.length) Modals.showConfirmationModal(Strings.PublicServers.connectionError, Strings.PublicServers.connectionErrorMessage);
}
async connect() {
await Connection.connect();
this.checkConnection();
}
searchKeyDown(e) {
if (this.state.loading || e.key !== "Enter") return;
const term = e.target.value;
if (this.state.tab == "Featured" || this.state.tab == "Popular") this.setState({tab: "All"}, () => this.search(term));
else this.search(term);
}
async search(term = "", page = 1) {
this.setState({query: term, loading: true});
const results = await Connection.search({term, keyword: this.state.tab == "All" || this.state.tab == "Featured" || this.state.tab == "Popular" ? "" : this.state.tab, page});
if (!results) return this.setState({results: Object.assign({}, EMPTY_RESULTS)});
this.setState({loading: false, results});
}
async changeTab(id) {
if (this.state.loading) return;
await new Promise(resolve => this.setState({tab: id}, resolve));
if (this.state.tab === "Featured" || this.state.tab == "Popular") {
const fakeResults = {
servers: this[this.state.tab.toLowerCase()],
size: this[this.state.tab.toLowerCase()].length,
total: this[this.state.tab.toLowerCase()].length,
page: 1,
numPages: 1
};
return this.setState({results: fakeResults});
}
this.search();
}
get hasPrevious() {return this.state.results.page > 1;}
get hasNext() {return this.state.results.page < this.state.results.numPages;}
loadPreviousPage() {
if (this.state.loading || !this.hasPrevious) return;
this.search(this.state.query, this.state.results.page - 1);
}
loadNextPage() {
if (this.state.loading || !this.hasNext) return;
this.search(this.state.query, this.state.results.page + 1);
}
async join(id, native = false) {
if (!this.state.user && !native) {
return Modals.showConfirmationModal(Strings.PublicServers.notConnected, Strings.PublicServers.connectionRequired, {
cancelText: Strings.Modals.nevermind,
confirmText: Strings.Modals.okay,
onConfirm: () => {
this.connect().then(() => Connection.join(id, native));
}
});
}
return await Connection.join(id, native);
}
navigateTo(id) {
if (GuildActions) GuildActions.transitionToGuildSync(id);
if (LayerManager) LayerManager.popLayer();
}
get searchBox() {
return <Search onKeyDown={this.searchKeyDown} className="bd-server-search" placeholder={`${Strings.PublicServers.search}...`} value={this.state.query} />;
}
get title() {
if (this.state.loading) return `${Strings.PublicServers.loading}...`;
if (this.state.query) {
const start = ((this.state.results.page - 1) * this.state.results.size) + 1;
const total = this.state.results.total;
const end = this.hasNext ? (start - 1) + this.state.results.size : total;
let title = Strings.PublicServers.results.format({start, end, total, category: this.state.tab});
if (this.state.query) title += " " + Strings.PublicServers.query.format({query: this.state.query});
return title;
}
return this.state.tab;
}
get content() {
const connectButton = this.state.user ? null : {title: Strings.PublicServers.connect, onClick: this.connect};
const servers = this.state.results.servers.map((server) => {
return React.createElement(ServerCard, {key: server.identifier, server: server, joined: Connection.hasJoined(server.identifier), join: this.join, navigateTo: this.navigateTo, defaultAvatar: Connection.getDefaultAvatar});
});
let content = React.createElement(EmptyResults);
if (this.state.loading) content = this.loadingScreen;
else if (this.state.results.total) content = React.createElement("div", {className: "bd-card-list"}, servers);
return [React.createElement(SettingsTitle, {text: this.title, button: connectButton}),
this.state.results.numPages > 1 && this.pagination,
content,
this.state.results.numPages > 1 && this.pagination
];
}
get loadingScreen() {
return <div className="bd-card-list">
<div className="bd-placeholder-card"></div>
<div className="bd-placeholder-card"></div>
<div className="bd-placeholder-card"></div>
<div className="bd-placeholder-card"></div>
<div className="bd-placeholder-card"></div>
<div className="bd-placeholder-card"></div>
<div className="bd-placeholder-card"></div>
<div className="bd-placeholder-card"></div>
</div>;
}
get pagination() {
return React.createElement("div", {className: "bd-pagination"},
React.createElement("button", {type: "button", className: "bd-button bd-pagination-previous", disabled: !this.hasPrevious, onClick: this.loadPreviousPage}, <Previous />),
React.createElement("span", {className: "bd-pagination-info"}, Strings.PublicServers.pagination.format({page: this.state.results.page, count: this.state.results.numPages})),
React.createElement("button", {type: "button", className: "bd-button bd-pagination-next", disabled: !this.hasNext, onClick: this.loadNextPage}, <Next />)
);
}
get connection() {
const {user} = this.state;
if (!user) return React.createElement("div", {id: "bd-connection"});
return React.createElement("div", {id: "bd-connection"},
React.createElement("div", {className: "bd-footnote"}, Strings.PublicServers.connection.format(user)),
React.createElement("button", {type: "button", className: "bd-button bd-button-reconnect", onClick: this.connect}, Strings.PublicServers.reconnect)
);
}
render() {
const keywords = this.keywords.map(name => ({
section: name,
label: name,
element: () => this.content
})
);
return React.createElement(SettingsView, {
onClose: this.props.close,
onSetSection: this.changeTab,
section: this.state.tab,
sections: [
{section: "HEADER", label: Strings.PublicServers.search},
{section: "CUSTOM", element: () => this.searchBox},
{section: "DIVIDER"},
{section: "HEADER", label: Strings.PublicServers.categories},
{section: "All", label: "All", element: () => this.content},
{section: "Featured", label: "Featured", element: () => this.content},
{section: "Popular", label: "Popular", element: () => this.content},
{section: "DIVIDER"},
{section: "HEADER", label: Strings.PublicServers.keywords},
...keywords,
{section: "DIVIDER"},
{section: "HEADER", label: React.createElement("a", {href: "https://discordservers.com", target: "_blank"}, "DiscordServers.com")},
{section: "DIVIDER"},
{section: "CUSTOM", element: () => this.connection}
],
theme: "dark"
});
}
}

View File

@ -54,7 +54,7 @@ export default new class SettingsRenderer {
}
getAddonPanel(title, addonList, addonState, options = {}) {
return React.createElement(AddonList, Object.assign({}, {
return () => React.createElement(AddonList, Object.assign({}, {
title: title,
addonList: addonList,
addonState: addonState

View File

@ -17,6 +17,9 @@ import ThemeIcon from "../icons/theme";
import Modals from "../modals";
import Toasts from "../toasts";
const {useState, useCallback, useMemo} = React;
const LinkIcons = {
website: WebIcon,
source: GitHubIcon,
@ -27,168 +30,128 @@ const LinkIcons = {
const LayerManager = {
pushLayer(component) {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
},
popLayer() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
},
popAllLayers() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
}
};
};
const UserStore = WebpackModules.getByProps("getCurrentUser");
const ChannelStore = WebpackModules.getByProps("getDMFromUserId");
const PrivateChannelActions = WebpackModules.getByProps("openPrivateChannel");
const ChannelActions = WebpackModules.getByProps("selectPrivateChannel");
const getString = value => typeof value == "string" ? value : value.toString();
export default class AddonCard extends React.Component {
function makeButton(title, children, action, {isControl = false, danger = false, disabled = false} = {}) {
const ButtonType = isControl ? "button" : "div";
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <ButtonType {...props} className={(isControl ? "bd-button bd-addon-button" : "bd-addon-button") + (danger ? " bd-button-danger" : "") + (disabled ? " bd-button-disabled" : "")} onClick={action}>{children}</ButtonType>;
}}
</DiscordModules.Tooltip>;
}
constructor(props) {
super(props);
this.settingsPanel = "";
this.panelRef = React.createRef();
this.onChange = this.onChange.bind(this);
this.showSettings = this.showSettings.bind(this);
this.messageAuthor = this.messageAuthor.bind(this);
function buildLink(type, url) {
if (!url) return null;
const icon = React.createElement(LinkIcons[type]);
const link = <a className="bd-link bd-link-website" href={url} target="_blank" rel="noopener noreferrer">{icon}</a>;
if (type == "invite") {
link.props.onClick = function(event) {
event.preventDefault();
event.stopPropagation();
let code = url;
const tester = /\.gg\/(.*)$/;
if (tester.test(code)) code = code.match(tester)[1];
LayerManager.popLayer();
DiscordModules.InviteActions?.acceptInviteAndTransitionToInviteChannel({inviteKey: code});
};
}
return makeButton(Strings.Addons[type], link);
}
showSettings() {
if (!this.props.hasSettings || !this.props.enabled) return;
const name = this.getString(this.props.addon.name);
export default function AddonCard({addon, type, disabled, enabled: initialValue, onChange: parentChange, hasSettings, editAddon, deleteAddon, getSettingsPanel}) {
const [isEnabled, setEnabled] = useState(initialValue);
const onChange = useCallback(() => {
setEnabled(!isEnabled);
if (parentChange) parentChange(addon.id);
}, [addon.id, parentChange, isEnabled]);
const showSettings = useCallback(() => {
if (!hasSettings || !isEnabled) return;
const name = getString(addon.name);
try {
Modals.showAddonSettingsModal(name, this.props.getSettingsPanel());
Modals.showAddonSettingsModal(name, getSettingsPanel());
}
catch (err) {
Toasts.show(Strings.Addons.settingsError.format({name}), {type: "error"});
Logger.stacktrace("Addon Settings", "Unable to get settings panel for " + name + ".", err);
}
}
}, [hasSettings, isEnabled, addon.name, getSettingsPanel]);
getString(value) {return typeof value == "string" ? value : value.toString();}
onChange() {
this.props.onChange && this.props.onChange(this.props.addon.id);
this.props.enabled = !this.props.enabled;
this.forceUpdate();
}
messageAuthor() {
if (!this.props.addon.authorId) return;
const messageAuthor = useCallback(() => {
if (!addon.authorId) return;
if (LayerManager) LayerManager.popLayer();
if (!UserStore || !ChannelActions || !ChannelStore || !PrivateChannelActions) return;
const selfId = UserStore.getCurrentUser().id;
if (selfId == this.props.addon.authorId) return;
const privateChannelId = ChannelStore.getDMFromUserId(this.props.addon.authorId);
if (selfId == addon.authorId) return;
const privateChannelId = ChannelStore.getDMFromUserId(addon.authorId);
if (privateChannelId) return ChannelActions.selectPrivateChannel(privateChannelId);
PrivateChannelActions.openPrivateChannel(selfId, this.props.addon.authorId);
}
PrivateChannelActions.openPrivateChannel(selfId, addon.authorId);
}, [addon.authorId]);
buildTitle(name, version, author) {
const title = useMemo(() => {
const authorArray = Strings.Addons.byline.split(/({{[A-Za-z]+}})/);
const authorComponent = author.link || author.id
? <a className="bd-link bd-link-website" href={author.link || null} onClick={this.messageAuthor} target="_blank" rel="noopener noreferrer">{author.name}</a>
: <span className="bd-author">{author.name}</span>;
const authorComponent = addon.authorLink || addon.authorId
? <a className="bd-link bd-link-website" href={addon.authorLink || null} onClick={messageAuthor} target="_blank" rel="noopener noreferrer">{getString(addon.author)}</a>
: <span className="bd-author">{getString(addon.author)}</span>;
const authorIndex = authorArray.findIndex(s => s == "{{author}}");
if (authorIndex) authorArray[authorIndex] = authorComponent;
return [
React.createElement("div", {className: "bd-name"}, name),
React.createElement("div", {className: "bd-meta"},
React.createElement("span", {className: "bd-version"}, `v${version}`),
...authorArray
)
<div className="bd-name">{getString(addon.name)}</div>,
<div className="bd-meta">
<span className="bd-version">v{getString(addon.version)}</span>
{authorArray}
</div>
];
}
}, [addon.name, addon.version, addon.authorLink, addon.authorId, addon.author, messageAuthor]);
buildLink(which) {
const url = this.props.addon[which];
if (!url) return null;
const icon = React.createElement(LinkIcons[which]);
const link = <a className="bd-link bd-link-website" href={url} target="_blank" rel="noopener noreferrer">{icon}</a>;
if (which == "invite") {
link.props.onClick = function(event) {
event.preventDefault();
event.stopPropagation();
let code = url;
const tester = /\.gg\/(.*)$/;
if (tester.test(code)) code = code.match(tester)[1];
LayerManager.popLayer();
DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel({inviteKey: code});
};
}
return this.makeButton(Strings.Addons[which], link);
}
get controls() { // {this.props.hasSettings && <button onClick={this.showSettings} className="bd-button bd-button-addon-settings" disabled={!this.props.enabled}>{Strings.Addons.addonSettings}</button>}
return <div className="bd-controls">
{this.props.hasSettings && this.makeControlButton(Strings.Addons.addonSettings, <CogIcon size={"20px"} />, this.showSettings, {disabled: !this.props.enabled})}
{this.props.editAddon && this.makeControlButton(Strings.Addons.editAddon, <EditIcon size={"20px"} />, this.props.editAddon)}
{this.props.deleteAddon && this.makeControlButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, this.props.deleteAddon, {danger: true})}
</div>;
}
get footer() {
const links = ["website", "source", "invite", "donate", "patreon"];
const linkComponents = links.map(this.buildLink.bind(this)).filter(c => c);// linkComponents.map((comp, i) => i < linkComponents.length - 1 ? [comp, " | "] : comp).flat()
const footer = useMemo(() => {
const links = Object.keys(LinkIcons);
const linkComponents = links.map(l => buildLink(l, addon[l])).filter(c => c);
return <div className="bd-footer">
<span className="bd-links">{linkComponents}</span>
{this.controls}
</div>;
}
makeButton(title, children, action) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <div {...props} className="bd-addon-button" onClick={action}>{children}</div>;
}}
</DiscordModules.Tooltip>;
}
makeControlButton(title, children, action, {danger = false, disabled = false} = {}) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <button {...props} className={"bd-button bd-addon-button" + (danger ? " bd-button-danger" : "") + (disabled ? " bd-button-disabled" : "")} onClick={action}>{children}</button>;
}}
</DiscordModules.Tooltip>;
}
render() {
const addon = this.props.addon;
const name = this.getString(addon.name);
const author = this.getString(addon.author);
const description = this.getString(addon.description);
const version = this.getString(addon.version);
return <div id={`${addon.id}-card`} className={"bd-addon-card" + (this.props.disabled ? " bd-addon-card-disabled" : "")}>
<div className="bd-addon-header">
{this.props.type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />}
<div className="bd-title">{this.buildTitle(name, version, {name: author, id: this.props.addon.authorId, link: this.props.addon.authorLink})}</div>
<Switch disabled={this.props.disabled} checked={this.props.enabled} onChange={this.onChange} />
<div className="bd-controls">
{hasSettings && makeButton(Strings.Addons.addonSettings, <CogIcon size={"20px"} />, showSettings, {isControl: true, disabled: !isEnabled})}
{editAddon && makeButton(Strings.Addons.editAddon, <EditIcon size={"20px"} />, editAddon, {isControl: true})}
{deleteAddon && makeButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, deleteAddon, {isControl: true, danger: true})}
</div>
<div className="bd-description-wrap">
{this.props.disabled && <div className="banner banner-danger"><ErrorIcon className="bd-icon" />{`An error was encountered while trying to load this ${this.props.type}.`}</div>}
<div className="bd-description">{SimpleMarkdown.parseToReact(description)}</div>
</div>
{this.footer}
</div>;
}
}, [hasSettings, editAddon, deleteAddon, addon, isEnabled, showSettings]);
return <div id={`${addon.id}-card`} className={"bd-addon-card" + (disabled ? " bd-addon-card-disabled" : "")}>
<div className="bd-addon-header">
{type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />}
<div className="bd-title">{title}</div>
<Switch disabled={disabled} checked={isEnabled} onChange={onChange} />
</div>
<div className="bd-description-wrap">
{disabled && <div className="banner banner-danger"><ErrorIcon className="bd-icon" />{`An error was encountered while trying to load this ${type}.`}</div>}
<div className="bd-description">{SimpleMarkdown.parseToReact(getString(addon.description))}</div>
</div>
{footer}
</div>;
}
const originalRender = AddonCard.prototype.render;
Object.defineProperty(AddonCard.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("AddonCard", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");},
get: () => originalRender
});

View File

@ -1,4 +1,3 @@
import Logger from "common/logger";
import {React, Strings, Events, DataStore, DiscordModules} from "modules";
import Modals from "../modals";
@ -13,120 +12,119 @@ import GridIcon from "../icons/grid";
import NoResults from "../blankslates/noresults";
import EmptyImage from "../blankslates/emptyimage";
export default class AddonList extends React.Component {
const {useState, useCallback, useEffect, useReducer, useMemo} = React;
constructor(props) {
super(props);
this.state = {query: "", sort: this.getControlState("sort", "name"), ascending: this.getControlState("ascending", true), view: this.getControlState("view", "list")};
this.sort = this.sort.bind(this);
this.reverse = this.reverse.bind(this);
this.search = this.search.bind(this);
this.update = this.update.bind(this);
this.listView = this.listView.bind(this);
this.gridView = this.gridView.bind(this);
this.openFolder = this.openFolder.bind(this);
}
const SORT_OPTIONS = [
{label: Strings.Addons.name, value: "name"},
{label: Strings.Addons.author, value: "author"},
{label: Strings.Addons.version, value: "version"},
{label: Strings.Addons.added, value: "added"},
{label: Strings.Addons.modified, value: "modified"},
{label: Strings.Addons.isEnabled, value: "isEnabled"}
];
componentDidMount() {
Events.on(`${this.props.prefix}-loaded`, this.update);
Events.on(`${this.props.prefix}-unloaded`, this.update);
}
const DIRECTIONS = [
{label: Strings.Sorting.ascending, value: true},
{label: Strings.Sorting.descending, value: false}
];
componentWillUnmount() {
Events.off(`${this.props.prefix}-loaded`, this.update);
Events.off(`${this.props.prefix}-unloaded`, this.update);
}
onControlChange(control, value) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[this.props.type]) addonlistControls[this.props.type] = {};
addonlistControls[this.props.type][control] = value;
DataStore.setBDData("addonlistControls", addonlistControls);
}
function openFolder(folder) {
const shell = require("electron").shell;
const open = shell.openItem || shell.openPath;
open(folder);
}
getControlState(control, defaultValue) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[this.props.type]) return defaultValue;
if (!addonlistControls[this.props.type].hasOwnProperty(control)) return defaultValue;
return addonlistControls[this.props.type][control];
}
function blankslate(type, onClick) {
const message = Strings.Addons.blankSlateMessage.format({link: `https://betterdiscord.app/${type}s`, type}).toString();
return <EmptyImage title={Strings.Addons.blankSlateHeader.format({type})} message={message}>
<button className="bd-button" onClick={onClick}>{Strings.Addons.openFolder.format({type})}</button>
</EmptyImage>;
}
update() {
this.forceUpdate();
}
function makeControlButton(title, children, action, selected = false) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <button {...props} className={"bd-button bd-view-button" + (selected ? " selected" : "")} onClick={action}>{children}</button>;
}}
</DiscordModules.Tooltip>;
}
reload() {
if (this.props.refreshList) this.props.refreshList();
this.forceUpdate();
}
function getState(type, control, defaultValue) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[type]) return defaultValue;
if (!addonlistControls[type].hasOwnProperty(control)) return defaultValue;
return addonlistControls[type][control];
}
listView() {this.changeView("list");}
gridView() {this.changeView("grid");}
changeView(view) {
this.onControlChange("view", view);
this.setState({view});
}
function saveState(type, control, value) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[type]) addonlistControls[type] = {};
addonlistControls[type][control] = value;
DataStore.setBDData("addonlistControls", addonlistControls);
}
reverse(value) {
this.onControlChange("ascending", value);
this.setState({ascending: value});
}
function confirmDelete(addon) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.confirmDelete.format({name: addon.name}), {
danger: true,
confirmText: Strings.Addons.deleteAddon,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
sort(value) {
this.onControlChange("sort", value);
this.setState({sort: value});
}
search(event) {
this.setState({query: event.target.value.toLocaleLowerCase()});
}
export default function AddonList({prefix, type, title, folder, addonList, addonState, onChange, reload, editAddon, deleteAddon}) {
const [query, setQuery] = useState("");
const [sort, setSort] = useState(getState.bind(null, type, "sort", "name"));
const [ascending, setAscending] = useState(getState.bind(null, type, "ascending", true));
const [view, setView] = useState(getState.bind(null, type, "view", "list"));
const [forced, forceUpdate] = useReducer(x => x + 1, 0);
openFolder() {
const shell = require("electron").shell;
const open = shell.openItem || shell.openPath;
open(this.props.folder);
}
useEffect(() => {
Events.on(`${prefix}-loaded`, forceUpdate);
Events.on(`${prefix}-unloaded`, forceUpdate);
return () => {
Events.off(`${prefix}-loaded`, forceUpdate);
Events.off(`${prefix}-unloaded`, forceUpdate);
};
}, [prefix]);
get sortOptions() {
return [
{label: Strings.Addons.name, value: "name"},
{label: Strings.Addons.author, value: "author"},
{label: Strings.Addons.version, value: "version"},
{label: Strings.Addons.added, value: "added"},
{label: Strings.Addons.modified, value: "modified"},
{label: Strings.Addons.isEnabled, value: "isEnabled"}
];
}
const changeView = useCallback((value) => {
saveState(type, "view", value);
setView(value);
}, [type]);
get directions() {
return [
{label: Strings.Sorting.ascending, value: true},
{label: Strings.Sorting.descending, value: false}
];
}
const listView = useCallback(() => changeView("list"), [changeView]);
const gridView = useCallback(() => changeView("grid"), [changeView]);
get emptyImage() {
const message = Strings.Addons.blankSlateMessage.format({link: `https://betterdiscord.app/${this.props.type}s`, type: this.props.type}).toString();
return <EmptyImage title={Strings.Addons.blankSlateHeader.format({type: this.props.type})} message={message}>
<button className="bd-button" onClick={this.openFolder}>{Strings.Addons.openFolder.format({type: this.props.type})}</button>
</EmptyImage>;
}
const changeDirection = useCallback((value) => {
saveState(type, "ascending", value);
setAscending(value);
}, [type]);
makeControlButton(title, children, action, selected = false) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <button {...props} className={"bd-button bd-view-button" + (selected ? " selected" : "")} onClick={action}>{children}</button>;
}}
</DiscordModules.Tooltip>;
}
const changeSort = useCallback((value) => {
saveState(type, "sort", value);
setSort(value);
}, [type]);
render() {
const {title, folder, addonList, addonState, onChange, reload} = this.props;
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: this.openFolder} : null;
let sortedAddons = addonList.sort((a, b) => {
const sortByEnabled = this.state.sort === "isEnabled";
const first = sortByEnabled ? addonState[a.id] : a[this.state.sort];
const second = sortByEnabled ? addonState[b.id] : b[this.state.sort];
const search = useCallback((e) => setQuery(e.target.value.toLocaleLowerCase()), []);
const triggerEdit = useCallback((id) => editAddon?.(id), [editAddon]);
const triggerDelete = useCallback(async (id) => {
const addon = addonList.find(a => a.id == id);
const shouldDelete = await confirmDelete(addon);
if (!shouldDelete) return;
if (deleteAddon) deleteAddon(addon);
}, [addonList, deleteAddon]);
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: openFolder.bind(null, folder)} : null;
const renderedCards = useMemo(() => {
let sorted = addonList.sort((a, b) => {
const sortByEnabled = sort === "isEnabled";
const first = sortByEnabled ? addonState[a.id] : a[sort];
const second = sortByEnabled ? addonState[b.id] : b[sort];
const stringSort = (str1, str2) => str1.toLocaleLowerCase().localeCompare(str2.toLocaleLowerCase());
if (typeof(first) == "string") return stringSort(first, second);
if (typeof(first) == "boolean") return (first === second) ? stringSort(a.name, b.name) : first ? -1 : 1;
@ -134,81 +132,53 @@ export default class AddonList extends React.Component {
if (second > first) return -1;
return 0;
});
if (!this.state.ascending) sortedAddons.reverse();
if (this.state.query) {
sortedAddons = sortedAddons.filter(addon => {
let matches = addon.name.toLocaleLowerCase().includes(this.state.query);
matches = matches || addon.author.toLocaleLowerCase().includes(this.state.query);
matches = matches || addon.description.toLocaleLowerCase().includes(this.state.query);
if (!ascending) sorted.reverse();
if (query) {
sorted = sorted.filter(addon => {
let matches = addon.name.toLocaleLowerCase().includes(query);
matches = matches || addon.author.toLocaleLowerCase().includes(query);
matches = matches || addon.description.toLocaleLowerCase().includes(query);
if (!matches) return false;
return true;
});
}
const renderedCards = sortedAddons.map(addon => {
return sorted.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary><AddonCard disabled={addon.partial} type={this.props.type} editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
});
}, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps
const hasAddonsInstalled = this.props.addonList.length !== 0;
const isSearching = !!this.state.query;
const hasResults = sortedAddons.length !== 0;
const hasAddonsInstalled = addonList.length !== 0;
const isSearching = !!query;
const hasResults = renderedCards.length !== 0;
return [
<SettingsTitle key="title" text={title} button={button} />,
<div className={"bd-controls bd-addon-controls"}>
<Search onChange={this.search} placeholder={`${Strings.Addons.search.format({type: this.props.title})}...`} />
<div className="bd-controls-advanced">
<div className="bd-addon-dropdowns">
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.sortBy}:</label>
<Dropdown options={this.sortOptions} value={this.state.sort} onChange={this.sort} style="transparent" />
</div>
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.order}:</label>
<Dropdown options={this.directions} value={this.state.ascending} onChange={this.reverse} style="transparent" />
</div>
return [
<SettingsTitle key="title" text={title} button={button} />,
<div className={"bd-controls bd-addon-controls"}>
<Search onChange={search} placeholder={`${Strings.Addons.search.format({type: title})}...`} />
<div className="bd-controls-advanced">
<div className="bd-addon-dropdowns">
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.sortBy}:</label>
<Dropdown options={SORT_OPTIONS} value={sort} onChange={changeSort} style="transparent" />
</div>
<div className="bd-addon-views">
{this.makeControlButton("List View", <ListIcon />, this.listView, this.state.view === "list")}
{this.makeControlButton("Grid View", <GridIcon />, this.gridView, this.state.view === "grid")}
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.order}:</label>
<Dropdown options={DIRECTIONS} value={ascending} onChange={changeDirection} style="transparent" />
</div>
</div>
</div>,
!hasAddonsInstalled && this.emptyImage,
isSearching && !hasResults && hasAddonsInstalled && <NoResults />,
hasAddonsInstalled && <div key="addonList" className={"bd-addon-list" + (this.state.view == "grid" ? " bd-grid-view" : "")}>{renderedCards}</div>
];
}
editAddon(id) {
if (this.props.editAddon) this.props.editAddon(id);
}
async deleteAddon(id) {
const addon = this.props.addonList.find(a => a.id == id);
const shouldDelete = await this.confirmDelete(addon);
if (!shouldDelete) return;
if (this.props.deleteAddon) this.props.deleteAddon(addon);
}
confirmDelete(addon) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.confirmDelete.format({name: addon.name}), {
danger: true,
confirmText: Strings.Addons.deleteAddon,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
<div className="bd-addon-views">
{makeControlButton("List View", <ListIcon />, listView, view === "list")}
{makeControlButton("Grid View", <GridIcon />, gridView, view === "grid")}
</div>
</div>
</div>,
!hasAddonsInstalled && blankslate(type, () => openFolder(folder)),
isSearching && !hasResults && hasAddonsInstalled && <NoResults />,
hasAddonsInstalled && <div key="addonList" className={"bd-addon-list" + (view == "grid" ? " bd-grid-view" : "")}>{renderedCards}</div>
];
}
const originalRender = AddonList.prototype.render;
Object.defineProperty(AddonList.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("AddonList", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");},
get: () => originalRender
});

View File

@ -1,5 +1,8 @@
import {DiscordModules, React} from "modules";
const {useState, useCallback} = React;
const Checkmark = React.memo((props) => (
<svg width="16" height="16" viewBox="0 0 24 24" {...props}>
<path fillRule="evenodd" clipRule="evenodd" fill={props.color ?? "#fff"} d="M8.99991 16.17L4.82991 12L3.40991 13.41L8.99991 19L20.9999 7.00003L19.5899 5.59003L8.99991 16.17Z" />
@ -28,7 +31,6 @@ const resolveColor = (color, hex = true) => {
}
};
const getRGB = (color) => {
let result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color);
if (result) return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])];
@ -52,56 +54,47 @@ const getContrastColor = (color) => {
return (luma(color) >= 165) ? "#000" : "#fff";
};
export default class Color extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value};
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.setState({value: e.target.value});
if (this.props.onChange) this.props.onChange(resolveColor(e.target.value));
}
export default function Color({value: initialValue, onChange, colors = defaultColors, defaultValue}) {
const [value, setValue] = useState(initialValue);
const change = useCallback((e) => {
onChange?.(resolveColor(e.target.value));
setValue(e.target.value);
}, [onChange]);
render() {
const intValue = resolveColor(this.state.value, false);
const {colors = defaultColors, defaultValue} = this.props;
return <div className="bd-color-picker-container">
<div className="bd-color-picker-controls">
<DiscordModules.Tooltip text="Default" position="bottom">
{props => (
<div {...props} className="bd-color-picker-default" style={{backgroundColor: resolveColor(defaultValue)}} onClick={() => this.onChange({target: {value: defaultValue}})}>
{intValue === resolveColor(defaultValue, false)
? <Checkmark width="25" height="25" />
: null
}
</div>
)}
</DiscordModules.Tooltip>
<DiscordModules.Tooltip text="Custom Color" position="bottom">
{props => (
<div className="bd-color-picker-custom">
<Dropper color={getContrastColor(resolveColor(this.state.value, true))} />
<input {...props} style={{backgroundColor: resolveColor(this.state.value)}} type="color" className="bd-color-picker" value={resolveColor(this.state.value)} onChange={this.onChange} />
</div>
)}
</DiscordModules.Tooltip>
</div>
<div className="bd-color-picker-swatch">
{
colors.map((int, index) => (
<div key={index} className="bd-color-picker-swatch-item" style={{backgroundColor: resolveColor(int)}} onClick={() => this.onChange({target: {value: int}})}>
{intValue === int
? <Checkmark color={getContrastColor(resolveColor(this.state.value, true))} />
: null
}
</div>
))
}
</div>
</div>;
}
}
const intValue = resolveColor(value, false);
return <div className="bd-color-picker-container">
<div className="bd-color-picker-controls">
<DiscordModules.Tooltip text="Default" position="bottom">
{props => (
<div {...props} className="bd-color-picker-default" style={{backgroundColor: resolveColor(defaultValue)}} onClick={() => change({target: {value: defaultValue}})}>
{intValue === resolveColor(defaultValue, false)
? <Checkmark width="25" height="25" />
: null
}
</div>
)}
</DiscordModules.Tooltip>
<DiscordModules.Tooltip text="Custom Color" position="bottom">
{props => (
<div className="bd-color-picker-custom">
<Dropper color={getContrastColor(resolveColor(value, true))} />
<input {...props} style={{backgroundColor: resolveColor(value)}} type="color" className="bd-color-picker" value={resolveColor(value)} onChange={change} />
</div>
)}
</DiscordModules.Tooltip>
</div>
<div className="bd-color-picker-swatch">
{
colors.map((int, index) => (
<div key={index} className="bd-color-picker-swatch-item" style={{backgroundColor: resolveColor(int)}} onClick={() => change({target: {value: int}})}>
{intValue === int
? <Checkmark color={getContrastColor(resolveColor(value, true))} />
: null
}
</div>
))
}
</div>
</div>;
}

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