From 874d5ac4ec5113cf1aedfd4295e26456dc053fdc Mon Sep 17 00:00:00 2001 From: F04C Date: Thu, 14 Dec 2023 01:28:09 -0500 Subject: [PATCH 01/16] Adjust locale fallback This fixes #1701 --- renderer/src/modules/localemanager.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/renderer/src/modules/localemanager.js b/renderer/src/modules/localemanager.js index dcbd7a8..09f10c4 100644 --- a/renderer/src/modules/localemanager.js +++ b/renderer/src/modules/localemanager.js @@ -11,7 +11,6 @@ export default new class LocaleManager { get defaultLocale() {return "en-US";} constructor() { - this.locale = ""; this.strings = Utilities.extend({}, Locales[this.defaultLocale]); } @@ -21,16 +20,13 @@ export default new class LocaleManager { } setLocale() { - let newStrings; - if (this.discordLocale != this.defaultLocale) { - newStrings = Locales[this.discordLocale]; - if (!newStrings) return this.setLocale(this.defaultLocale); - } - else { - newStrings = Locales[this.defaultLocale]; - } - this.locale = this.discordLocale; - Utilities.extendTruthy(this.strings, newStrings); + // Reset to the default locale in case a language is incomplete + Utilities.extend(this.strings, Locales[this.defaultLocale]); + + // Get the strings of the new language and extend if a translation exists + const newStrings = Locales[this.discordLocale]; + if (newStrings) Utilities.extendTruthy(this.strings, newStrings); + Events.emit("strings-updated"); } }; \ No newline at end of file From c87605db7bfc66e73235b3aab63ac617331d7a1d Mon Sep 17 00:00:00 2001 From: F04C Date: Thu, 14 Dec 2023 15:16:26 -0500 Subject: [PATCH 02/16] Add es-419 support --- assets/locales/es-419.json | 189 +++++++++++++++++++++++++++++++++++++ assets/locales/index.js | 1 + assets/locales/vi.json | 46 ++++----- scripts/translations.js | 1 + 4 files changed, 214 insertions(+), 23 deletions(-) create mode 100644 assets/locales/es-419.json diff --git a/assets/locales/es-419.json b/assets/locales/es-419.json new file mode 100644 index 0000000..bcc90ba --- /dev/null +++ b/assets/locales/es-419.json @@ -0,0 +1,189 @@ +{ + "Panels": { + "plugins": "Plugins", + "themes": "Temas", + "customcss": "CSS Personalizado" + }, + "Collections": { + "settings": { + "name": "Ajustes", + "general": { + "name": "General", + "voiceDisconnect": { + "name": "Desconexión de Voz", + "note": "Desconectarse del servidor de voz al cerrar Discord" + }, + "showToasts": { + "name": "Mostrar Notificaciones", + "note": "Muestra una pequeña notificación de información importante" + }, + "mediaKeys": { + "name": "Desactivar las Teclas Multimedia", + "note": "Evita que Discord se apropie de tus teclas multimedia después de reproducir un vídeo" + } + }, + "window": { + "removeMinimumSize": { + "name": "Eliminar Tamaño Mínimo", + "note": "Elimina el tamaño mínimo de Discord de 940x500" + }, + "name": "Preferencias de la Ventana", + "transparency": { + "name": "Activar Transparencia", + "note": "Hace que la ventana principal pueda ser transparente (requiere reinicio)" + }, + "frame": { + "name": "Marco de la Ventana", + "note": "Añade el marco de ventana nativo de tu sistema operativo a la ventana principal" + } + }, + "addons": { + "name": "Gestor de Complementos", + "addonErrors": { + "name": "Mostrar Errores de Complementos", + "note": "Muestra una ventana con los errores de plugin/temas" + }, + "editAction": { + "name": "Acción al Editar", + "note": "Donde aparecerán los plugins y temas al editarlos", + "options": { + "detached": "Ventana Independiente", + "system": "Editor del Sistema" + } + } + }, + "customcss": { + "name": "CSS Personalizado", + "customcss": { + "name": "CSS Personalizado", + "note": "Activa la pestaña de CSS Personalizado" + }, + "liveUpdate": { + "name": "Actualización en Vivo", + "note": "Actualiza el CSS a medida que se escribe" + }, + "startDetached": { + "name": "Comenzar en Ventana Independiente", + "note": "Al hacer clic en la pestaña de CSS Personalizado se abre el editor en una ventana independiente" + }, + "nativeOpen": { + "name": "Abrir en Editor Nativo", + "note": "Al hacer clic en la pestaña de CSS Personalizado se abre el editor en tu editor nativo" + }, + "openAction": { + "name": "Ubicación del Editor", + "note": "Donde deberá el CSS Personalizado abrirse por defecto", + "options": { + "settings": "Menú de Ajustes", + "detached": "Ventana Independiente", + "system": "Editor del Sistema" + } + } + }, + "developer": { + "name": "Ajustes de Desarrollador", + "debuggerHotkey": { + "name": "Tecla de Acceso Rápido al Depurador", + "note": "Permite activar el depurador al presionar la tecla F8" + }, + "reactDevTools": { + "name": "React Developer Tools", + "note": "Inyecta tu instalación local de React Developer Tools en Discord" + }, + "inspectElement": { + "name": "Tecla de Acceso Rápido al Inspector de Elementos", + "note": "Activa la tecla de acceso rápido al inspector de elementos (ctrl + shift + c) que es común en la mayoria de navegadores" + }, + "devToolsWarning": { + "name": "Quitar el Aviso del Inspector de Elementos", + "note": "Previene que Discord muestre su mensaje \"¡Espera!\"" + }, + "debugLogs": { + "name": "Registros de Depuración", + "note": "Envía todo lo que aparece en la consola a un archivo llamado debug.log en la carpeta de BetterDiscord" + } + } + } + }, + "Addons": { + "title": "{{name}} v{{version}} por {{author}}", + "byline": "por {{author}}", + "openFolder": "Abrir Carpeta de {{type}}", + "reload": "Recargar", + "addonSettings": "Ajustes", + "website": "Sitio web", + "source": "Fuente", + "invite": "Servidor de Soporte", + "donate": "Donar", + "patreon": "Patreon", + "name": "Nombre", + "author": "Autor", + "version": "Versión", + "added": "Fecha de Adición", + "modified": "Fecha de Modificación", + "search": "Buscar {{type}}", + "editAddon": "Editar", + "deleteAddon": "Eliminar", + "confirmDelete": "¿Estás seguro de que quieres borrar {{name}}?", + "confirmationText": "Tiene cambios no guardados en {{name}}. Al cerrar esta ventana se perderán todos los cambios.", + "enabled": "{{name}} ha sido activado.", + "disabled": "{{name}} ha sido desactivado.", + "couldNotEnable": "{{name}} no pudo ser activado.", + "couldNotDisable": "{{name}} no pudo ser desactivado.", + "couldNotStart": "{{name}} no se pudo iniciar.", + "couldNotStop": "{{name}} no se pudo detener.", + "settingsError": "No se pudieron abrir los ajustes de {{name}}", + "methodError": "{{method}} no pudo ser lanzado.", + "unknownAuthor": "Autor Desconocido", + "noDescription": "Descripción no proporcionada.", + "alreadyExists": "Ya existe un {{type}} con nombre {{name}}", + "alreadWatching": "Ya está viendo los complementos.", + "metaError": "El META no pudo ser analizado.", + "missingNameData": "El META no contiene datos del nombre.", + "metaNotFound": "El META no ha sido encontrado.", + "compileError": "No se ha podido compilar.", + "wasUnloaded": "{{name}} ha sido descargado.", + "blankSlateHeader": "¡No tienes {{type}}s!", + "blankSlateMessage": "Consigue alguno en [esta página web]({{link}}) y añadelos a tu carpeta de {{type}}." + }, + "CustomCSS": { + "confirmationText": "Tienes cambios sin guardar en tu CSS Personalizado. Al cerrar esta ventana se perderán todos los cambios.", + "update": "Actualizar", + "save": "Guardar", + "openNative": "Abrir en el Editor del Sistema", + "openDetached": "Mostrar en Ventana Independiente", + "settings": "Ajustes del Editor", + "editorTitle": "Editor de CSS Personalizado" + }, + "Modals": { + "confirmAction": "¿Estás seguro?", + "okay": "Vale", + "done": "Hecho", + "cancel": "Cancelar", + "nevermind": "No importa", + "close": "Cerrar", + "name": "Nombre", + "message": "Mensaje", + "error": "Error", + "addonErrors": "Errores de Complementos", + "restartRequired": "Reinicio Requerido", + "restartNow": "Reiniciar Ahora", + "restartLater": "Reiniciar más Tarde", + "additionalInfo": "Información Adicional", + "restartPrompt": "Para que surta efecto, es necesario reiniciar Discord. ¿Quieres reiniciar ahora?" + }, + "ReactDevTools": { + "notFound": "Extensión no Encontrada", + "notFoundDetails": "No se puede encontrar la extensión React Developer Tools en su PC. Por favor, instale la extensión en su instalación local de Chrome." + }, + "Sorting": { + "sortBy": "Ordenar por", + "order": "Orden", + "ascending": "Ascendente", + "descending": "Descendente" + }, + "WindowPrefs": { + "enabledInfo": "Esta opción requiere un tema transparente para que funcione correctamente. En Windows esto podría hacer que el ajuste automático de la ventana (Aero Snap) y la maximización dejen de funcionar.\n\nPara que surta efecto, es necesario reiniciar Discord. ¿Quieres reiniciar ahora?", + "disabledInfo": "Para que surta efecto, es necesario reiniciar Discord. ¿Quieres reiniciar ahora?" + } +} \ No newline at end of file diff --git a/assets/locales/index.js b/assets/locales/index.js index 01a8000..87a9404 100644 --- a/assets/locales/index.js +++ b/assets/locales/index.js @@ -21,6 +21,7 @@ module.exports = { "ru": require("./ru.json"), // Russian "sk": require("./sk.json"), // Slovak "es-ES": require("./es-es.json"), // Spanish (Spain) + "es-419": require("./es-419.json"), // Spanish (LATAM) "sv-SE": require("./sv-se.json"), // Swedish "tr": require("./tr.json"), // Turkish "bg": require("./bg.json"), // Bulgarian diff --git a/assets/locales/vi.json b/assets/locales/vi.json index a47052e..7ef5bc3 100644 --- a/assets/locales/vi.json +++ b/assets/locales/vi.json @@ -1,6 +1,6 @@ { "Panels": { - "plugins": "Tiện Ích", + "plugins": "Tiện ích", "themes": "Chủ đề", "customcss": "CSS Tùy Chỉnh", "updates": "Cập nhật" @@ -11,8 +11,8 @@ "general": { "name": "Chung", "voiceDisconnect": { - "name": "Ngắt Kết Nối Voice", - "note": "Ngắt kết nối kênh voice khi đóng Discord" + "name": "Ngắt Kết Nối Kênh Đàm Thoại", + "note": "Ngắt kết nối kênh đàm thoại khi đóng Discord" }, "showToasts": { "name": "HIển Thị Thông Báo", @@ -49,7 +49,7 @@ "note": "Nơi trình chỉnh sửa tiện ích và chủ đề hiển thị khi sửa", "options": { "detached": "Cửa Sổ Tách Rời", - "system": "Trình Chỉnh Sửa Của Hệ Thống" + "system": "Trình Chỉnh Sửa" } } }, @@ -93,7 +93,7 @@ }, "inspectElement": { "name": "Phím tắt Kiểm tra Thành Phần", - "note": "Kích hoạt phím tắt Kiểm tra Thành Phần (Ctrl + Shift + C) mà thường thấy ở những trình duyệt" + "note": "Kích hoạt phím tắt Kiểm tra Thành Phần (Ctrl + Shift + C) tương tự như trong hầu hết trình duyệt" }, "devToolsWarning": { "name": "Tắt Cảnh Báo DevTools", @@ -105,7 +105,7 @@ }, "devTools": { "name": "DevTools", - "note": "Kích hoạt DevTools bằng tổ hợp Ctrl + Shift + I" + "note": "Kích hoạt DevTools bằng tổ hợp Ctrl + Shift + i" } }, "editor": { @@ -115,8 +115,8 @@ "note": "Hiển thị số dòng ở cạnh của trình chỉnh sửa" }, "fontSize": { - "name": "Kích Cỡ Chữ", - "note": "Kích cỡ chữ (pt) được sử dụng trong trình chỉnh sửa" + "name": "Kích Thước Phông Chữ", + "note": "Kích thước phông chữ (pt) được sử dụng trong trình chỉnh sửa" }, "minimap": { "name": "Minimap", @@ -131,12 +131,12 @@ "note": "Hiển thị những đề xuất tự hoàn thành khi bạn gõ" }, "renderWhitespace": { - "name": "Hiển Thị Khoảng Cách Trắng", - "note": "Khi nào khoảng cách trắng sẽ được hiển thị trong trình chỉnh sửa", + "name": "Hiển Thị Khoảng Trắng", + "note": "Khi nào khoảng trắng sẽ được hiển thị trong trình chỉnh sửa", "options": { "all": "Luôn luôn", "none": "Không bao giờ", - "selection": "Lựa chọn" + "selection": "Tùy chọn" } } } @@ -145,7 +145,7 @@ "Addons": { "title": "{{name}} v{{version}} bởi {{author}}", "byline": "bởi {{author}}", - "openFolder": "Mở Thư Mục {{type}}", + "openFolder": "Mở thư mục {{type}}", "reload": "Tải lại", "addonSettings": "Cài đặt", "website": "Trang web", @@ -157,7 +157,7 @@ "author": "Tác giả", "version": "Phiên bản", "added": "Ngày thêm", - "modified": "Ngày chỉnh sửa", + "modified": "Ngày sửa đổi", "search": "Tìm kiếm {{type}}", "editAddon": "Chỉnh sửa", "deleteAddon": "Xóa", @@ -167,23 +167,23 @@ "disabled": "{{name}} đã được vô hiệu hóa.", "couldNotEnable": "{{name}} không thể được kích hoạt.", "couldNotDisable": "{{name}} không thể được vô hiệu hóa.", - "couldNotStart": "{{name}} không thể được bắt đầu.", - "couldNotStop": "{{name}} không thể được dừng lại.", + "couldNotStart": "{{name}} không thể bắt đầu.", + "couldNotStop": "{{name}} không thể dừng lại.", "settingsError": "Không thể mở cài đặt cho {{name}}", "methodError": "{{method}} không thể được kích hoạt.", "unknownAuthor": "Tác giả không xác định", "noDescription": "Không có miêu tả.", "alreadyExists": "Đã có {{type}} với tên {{name}} rồi!", "alreadWatching": "Đã đang xem tiện ích.", - "metaError": "META không thể được phân tích.", - "missingNameData": "META đang thiếu tên dữ liệu.", + "metaError": "Không thể phân tích META.", + "missingNameData": "Thiếu dữ liệu tên META.", "metaNotFound": "Không thể tìm thấy META.", "compileError": "Không thể biên dịch. Vui lòng kiểm tra Console để biết thêm chi tiết.", - "wasUnloaded": "{{name}} đã được tắt.", + "wasUnloaded": "{{name}} đã được gỡ.", "blankSlateHeader": "Bạn không có {{type}}!", "blankSlateMessage": "Lấy một số ở [trang web]({{link}}) và thêm vào thư mục {{type}}.", "isEnabled": "Đã kích hoạt", - "wasLoaded": "{{name}} v{{version}} đã được bật.", + "wasLoaded": "{{name}} v{{version}} đã được thêm.", "listView": "Dạng Danh Sách", "gridView": "Dạng Ô" }, @@ -191,7 +191,7 @@ "confirmationText": "Bạn có những thay đổi chưa lưu cho CSS Tùy Chỉnh của bạn. Đóng cửa sổ này sẽ xóa hết những thay đổi của bạn.", "update": "Cập nhật", "save": "Lưu", - "openNative": "Ở trong Trình Chỉnh Sửa", + "openNative": "Mở trong Trình Chỉnh Sửa", "openDetached": "Cửa Sổ Riêng", "settings": "Cài đặt Trình Chỉnh Sửa", "editorTitle": "Trình Chỉnh Sửa CSS Tùy Chỉnh" @@ -225,7 +225,7 @@ "descending": "Dưới lên trên" }, "WindowPrefs": { - "enabledInfo": "Lựa chọn này cần phải có chủ đề trong suốt để có thể hoạt động đúng cách. Ở Windows tính năng Aero Snapping và toàn cửa sổ có thể không hoạt động.\n\nĐể có hiệu lực, Discord cần phải được khởi động lại. Bạn có muốn khởi động lại ngay bây giờ?", + "enabledInfo": "Lựa chọn này cần phải có một chủ đề trong suốt để có thể hoạt động đúng cách. Trên Windows, tính năng sắp xếp và thu phóng cửa số (Aero Snapping) và toàn cửa sổ có thể không hoạt động.\n\nĐể có hiệu lực, Discord cần phải được khởi động lại. Bạn có muốn khởi động lại ngay bây giờ?", "disabledInfo": "Để có hiệu lực, Discord cần phải được khởi động lại. Bạn có muốn khởi động lại ngay bây giờ?" }, "Notices": { @@ -233,7 +233,7 @@ }, "Updater": { "updateFailed": "Cập Nhật Thất Bại!", - "updateFailedMessage": "BetterDiscord không thể cập nhật. Vui lòng tải trình cài đặt mới nhất ở trang web (https://betterdiscord.app/) và cài đặt lại.", + "updateFailedMessage": "Không thể cập nhật BetterDiscord. Vui lòng tải trình cài đặt mới nhất ở trang web (https://betterdiscord.app/) và cài đặt lại.", "updateSuccessful": "Cập Nhật Thành Công!", "updateAvailable": "BetterDiscord có cập nhật mới (v{{version}})", "addonUpdatesAvailable": "BetterDiscord đã phát hiện {{count}} cho {{type}} của bạn!", @@ -244,7 +244,7 @@ "updateAll": "Cập Nhật Mọi Thứ!", "noUpdatesAvailable": "Không có cập nhật mới.", "versionAvailable": "Phiên bản {{version}} đã có sẵn!", - "upToDateBlankslate": "Tất cả những {{type}} của bạn đều ở phiên bản mới nhất!", + "upToDateBlankslate": "Tất cả {{type}} của bạn đều ở phiên bản mới nhất!", "updateButton": "Cập nhật!" } } \ No newline at end of file diff --git a/scripts/translations.js b/scripts/translations.js index 6a0a345..439c08d 100644 --- a/scripts/translations.js +++ b/scripts/translations.js @@ -52,6 +52,7 @@ const editorMap = { "ru": "ru.json", // Russian "sk": "sk.json", // Slovak "es": "es-es.json", // Spanish (Spain) + "es-419": "es-419.json", // Spanish (LATAM) "sv": "sv-se.json", // Swedish "tr": "tr.json", // Turkish "bg": "bg.json", // Bulgarian From f10edf9c3e18b0b37d636c1b3eb02e576909cfab Mon Sep 17 00:00:00 2001 From: F04C Date: Fri, 15 Dec 2023 01:39:30 -0500 Subject: [PATCH 03/16] Ignore relative requires --- renderer/src/polyfill/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/renderer/src/polyfill/index.js b/renderer/src/polyfill/index.js index 07b710d..de3839d 100644 --- a/renderer/src/polyfill/index.js +++ b/renderer/src/polyfill/index.js @@ -22,6 +22,12 @@ originalFs.writeFile = (path, data, options) => fs.writeFile(path, data, Object. export const createRequire = function (path) { return mod => { + // Ignore relative require attempts because Discord + // erroneously does this a lot apparently which + // causes us to do filesystem accesses in our default + // switch statement mainly used for absolute paths + if (typeof(mod) === "string" && mod.startsWith("./")) return; + if (deprecated.has(mod)) { Logger.warn("Remote~Require", `The "${mod}" module is marked as deprecated. ${deprecated.get(mod)}`); } From 86b292a14ad0e3b7920dd07e8e0a5bb653c60011 Mon Sep 17 00:00:00 2001 From: F04C Date: Mon, 18 Dec 2023 17:40:43 -0500 Subject: [PATCH 04/16] Update version and changelog --- package.json | 2 +- renderer/src/data/changelog.js | 33 +++++++-------------------------- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 15286d8..a482918 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "betterdiscord", - "version": "1.9.6", + "version": "1.9.7", "description": "Enhances Discord by adding functionality and themes.", "main": "src/index.js", "scripts": { diff --git a/renderer/src/data/changelog.js b/renderer/src/data/changelog.js index d8964c2..7d8489d 100644 --- a/renderer/src/data/changelog.js +++ b/renderer/src/data/changelog.js @@ -1,35 +1,16 @@ // fixed, improved, added, progress export default { - description: "There are some small but important fixes and changes in this update to keep things running smoothly!", + description: "This is a small but very important update to fix some key issues!", changes: [ { - title: "What's New?", - type: "added", - items: [ - "Keybinds will now show properly instead of `[object Undefined]`.", - "Update banners will now appear consistently when there are updates.", - "Translations should now actually load when a new locale is selected in Discord's settings." - ] - }, - { - title: "Translations", - type: "improved", - items: [ - "Added a new Vietnamese translation thanks to Minato Isuki.", - "Improved Italian translation thanks to TheItalianTranslator.", - "Improved Chinese (traditional) translation thanks to Frost_koi.", - "Removed several outdated keys and strings.", - "Added multiple translated strings to UI where they were hardcoded." - ] - }, - { - title: "Technical Changes", + title: "What's Fixed?", type: "fixed", items: [ - "The webpack hook now no longer prevents Discord modules from shadowing built-in functions. This was originally meant as a sanity check but now Discord actually does this intentionally which can lead to issues like the incorrectly displayed keybinds.", - "`BdApi.UI.showNotice` should work again in cases where it seemed not to unless you had addons with updates. This was a race condition versus the load order of class modules.", - "`BdApi.Net.fetch` now actually uses all the options passed to it, previously it failed to pass the options to the other process.", - "It also now supports all HTTP request types rather than just `POST`, `GET`, `DELETE`, and `PUT`." + "Spanish (LATAM) is now properly supported.", + "Future cases of unrecognized locales as well as locale fallback now works as intended and shouldn't cause loading issues.", + "Updated translations for Vietnamese locale.", + "Fixed an issue where certain actions (such as favoriting GIFs) caused unexpected lag.", + "Fixed some issues with general client lag." ] } ] From a94385f4f8ff94801e1167c9b9e3c86152d818e2 Mon Sep 17 00:00:00 2001 From: F04C Date: Thu, 22 Feb 2024 10:22:48 +1100 Subject: [PATCH 05/16] Update InviteActions in discordmodules.js (#1711) --- renderer/src/modules/discordmodules.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/src/modules/discordmodules.js b/renderer/src/modules/discordmodules.js index 8eea820..de9dfff 100644 --- a/renderer/src/modules/discordmodules.js +++ b/renderer/src/modules/discordmodules.js @@ -64,7 +64,7 @@ export default Utilities.memoizeObject({ /* Invite Store and Utils */ get InviteStore() {return WebpackModules.getByProps("getInvites");}, get InviteResolver() {return WebpackModules.getByProps("findInvite");}, - get InviteActions() {return WebpackModules.getByProps("acceptInvite");}, + get InviteActions() {return WebpackModules.getByProps("createInvite");}, /* Discord Objects & Utils */ get DiscordConstants() {return WebpackModules.getByProps("Permissions", "ActivityTypes", "StatusTypes");}, From 7c5b4ab3eaf7344e12fdb2bd033cfa743f0381f4 Mon Sep 17 00:00:00 2001 From: F04C Date: Wed, 21 Feb 2024 15:25:54 -0800 Subject: [PATCH 06/16] Auto focus search bar when plugins/themes tab is selected (#1705) #1213 --- renderer/src/ui/settings/components/search.jsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/renderer/src/ui/settings/components/search.jsx b/renderer/src/ui/settings/components/search.jsx index 1bd481c..26c8d4f 100644 --- a/renderer/src/ui/settings/components/search.jsx +++ b/renderer/src/ui/settings/components/search.jsx @@ -1,19 +1,26 @@ import React from "@modules/react"; import SearchIcon from "@ui/icons/search"; -const {useState, useCallback} = React; +const {useState, useEffect, useCallback, useRef} = React; export default function Search({onChange, className, onKeyDown, placeholder}) { + const input = useRef(null); const [value, setValue] = useState(""); + + // focus search bar on page select + useEffect(()=>{ + if (!input.current) return; + input.current.focus(); + }, []); + const change = useCallback((e) => { onChange?.(e); setValue(e.target.value); }, [onChange]); - return
- +
; From 7492e7eb1176beba5d65d7124ca6f940ff220864 Mon Sep 17 00:00:00 2001 From: F04C Date: Wed, 21 Feb 2024 18:17:51 -0500 Subject: [PATCH 07/16] Fix for upcoming electron update Fixes #1715 --- preload/src/api/electron.js | 1 - 1 file changed, 1 deletion(-) diff --git a/preload/src/api/electron.js b/preload/src/api/electron.js index 246535f..f8c9155 100644 --- a/preload/src/api/electron.js +++ b/preload/src/api/electron.js @@ -3,7 +3,6 @@ import {ipcRenderer as IPC, shell} from "electron"; export const ipcRenderer = { send: IPC.send.bind(IPC), sendToHost: IPC.sendToHost.bind(IPC), - sendTo: IPC.sendTo.bind(IPC), sendSync: IPC.sendSync.bind(IPC), invoke: IPC.invoke.bind(IPC), on: IPC.on.bind(IPC), From 5f732bb5bf0e781547c67082fd1ec7608083b886 Mon Sep 17 00:00:00 2001 From: F04C Date: Wed, 21 Feb 2024 19:05:28 -0500 Subject: [PATCH 08/16] Use openPath workaround on windows Fixes #1700 --- common/constants/ipcevents.js | 3 ++- injector/src/modules/betterdiscord.js | 4 +++- injector/src/modules/ipc.js | 9 ++++++++- injector/webpack.config.js | 3 ++- renderer/src/modules/addonmanager.js | 3 ++- renderer/src/modules/ipc.js | 4 ++++ renderer/src/ui/settings/addonlist.jsx | 5 ++--- 7 files changed, 23 insertions(+), 8 deletions(-) diff --git a/common/constants/ipcevents.js b/common/constants/ipcevents.js index 6aa2997..7b52f3c 100644 --- a/common/constants/ipcevents.js +++ b/common/constants/ipcevents.js @@ -16,4 +16,5 @@ export const WINDOW_SIZE = "bd-window-size"; export const DEVTOOLS_WARNING = "bd-remove-devtools-message"; export const OPEN_DIALOG = "bd-open-dialog"; export const REGISTER_PRELOAD = "bd-register-preload"; -export const GET_ACCENT_COLOR = "bd-get-accent-color"; \ No newline at end of file +export const GET_ACCENT_COLOR = "bd-get-accent-color"; +export const OPEN_PATH = "bd-open-path"; \ No newline at end of file diff --git a/injector/src/modules/betterdiscord.js b/injector/src/modules/betterdiscord.js index cd455ee..d9481ac 100644 --- a/injector/src/modules/betterdiscord.js +++ b/injector/src/modules/betterdiscord.js @@ -1,6 +1,7 @@ import fs from "fs"; import path from "path"; import electron from "electron"; +import {spawn} from "child_process"; import ReactDevTools from "./reactdevtools"; import * as IPCEvents from "common/constants/ipcevents"; @@ -97,7 +98,8 @@ export default class BetterDiscord { electron.app.exit(); } if (result.response === 1) { - electron.shell.openPath(path.join(dataPath, "plugins")); + if (process.platform === "win32") spawn("explorer.exe", [path.join(dataPath, "plugins")]); + else electron.shell.openPath(path.join(dataPath, "plugins")); } }); hasCrashed = false; diff --git a/injector/src/modules/ipc.js b/injector/src/modules/ipc.js index 0de96f9..1d2f1a4 100644 --- a/injector/src/modules/ipc.js +++ b/injector/src/modules/ipc.js @@ -1,4 +1,5 @@ -import {ipcMain as ipc, BrowserWindow, app, dialog, systemPreferences} from "electron"; +import {spawn} from "child_process"; +import {ipcMain as ipc, BrowserWindow, app, dialog, systemPreferences, shell} from "electron"; import * as IPCEvents from "common/constants/ipcevents"; @@ -32,6 +33,11 @@ const getPath = (event, pathReq) => { event.returnValue = returnPath; }; +const openPath = (event, path) => { + if (process.platform === "win32") spawn("explorer.exe", [path]); + else shell.openPath(path); +}; + const relaunch = () => { app.quit(); app.relaunch(); @@ -140,6 +146,7 @@ export default class IPCMain { static registerEvents() { try { ipc.on(IPCEvents.GET_PATH, getPath); + ipc.on(IPCEvents.OPEN_PATH, openPath); ipc.on(IPCEvents.RELAUNCH, relaunch); ipc.on(IPCEvents.OPEN_DEVTOOLS, openDevTools); ipc.on(IPCEvents.CLOSE_DEVTOOLS, closeDevTools); diff --git a/injector/webpack.config.js b/injector/webpack.config.js index 5a0f185..38b5e4f 100644 --- a/injector/webpack.config.js +++ b/injector/webpack.config.js @@ -19,7 +19,8 @@ module.exports = (env, argv) => ({ rimraf: `require("rimraf")`, yauzl: `require("yauzl")`, mkdirp: `require("mkdirp")`, - module: `require("module")` + module: `require("module")`, + child_process: `require("child_process")`, }, resolve: { extensions: [".js"], diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js index 365445b..a88a526 100644 --- a/renderer/src/modules/addonmanager.js +++ b/renderer/src/modules/addonmanager.js @@ -11,13 +11,14 @@ import Events from "./emitter"; import DataStore from "./datastore"; import React from "./react"; import Strings from "./strings"; +import ipc from "./ipc"; import AddonEditor from "@ui/misc/addoneditor"; import FloatingWindows from "@ui/floatingwindows"; import Toasts from "@ui/toasts"; -const openItem = shell.openItem || shell.openPath; +const openItem = ipc.openPath; const splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/; const escapedAtRegex = /^\\@/; diff --git a/renderer/src/modules/ipc.js b/renderer/src/modules/ipc.js index bb937b0..2f72d1d 100644 --- a/renderer/src/modules/ipc.js +++ b/renderer/src/modules/ipc.js @@ -60,4 +60,8 @@ export default new class IPCRenderer { getSystemAccentColor() { return ipc.invoke(IPCEvents.GET_ACCENT_COLOR); } + + openPath(path) { + return ipc.send(IPCEvents.OPEN_PATH, path); + } }; \ No newline at end of file diff --git a/renderer/src/ui/settings/addonlist.jsx b/renderer/src/ui/settings/addonlist.jsx index 3d1cfec..3b28d42 100644 --- a/renderer/src/ui/settings/addonlist.jsx +++ b/renderer/src/ui/settings/addonlist.jsx @@ -3,6 +3,7 @@ import Strings from "@modules/strings"; import Events from "@modules/emitter"; import DataStore from "@modules/datastore"; import DiscordModules from "@modules/discordmodules"; +import ipc from "@modules/ipc"; import SettingsTitle from "./title"; import AddonCard from "./addoncard"; @@ -37,9 +38,7 @@ const buildDirectionOptions = () => [ function openFolder(folder) { - const shell = require("electron").shell; - const open = shell.openItem || shell.openPath; - open(folder); + ipc.openPath(folder); } function blankslate(type, onClick) { From d56ba2441503d8a80833f2006eeef457cb79a0fd Mon Sep 17 00:00:00 2001 From: F04C Date: Wed, 21 Feb 2024 19:49:24 -0500 Subject: [PATCH 09/16] Add onClose for modals --- renderer/src/modules/api/ui.js | 1 + renderer/src/ui/modals.js | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/renderer/src/modules/api/ui.js b/renderer/src/modules/api/ui.js index 1533269..b4bac7b 100644 --- a/renderer/src/modules/api/ui.js +++ b/renderer/src/modules/api/ui.js @@ -50,6 +50,7 @@ const UI = { * @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 {callable} [options.onClose=NOOP] Callback to occur when exiting the modal * @returns {string} The key used for this modal. */ showConfirmationModal(title, content, options = {}) { diff --git a/renderer/src/ui/modals.js b/renderer/src/ui/modals.js index 157b2f6..d5e5a09 100644 --- a/renderer/src/ui/modals.js +++ b/renderer/src/ui/modals.js @@ -157,6 +157,7 @@ export default class Modals { * @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 {callable} [options.onClose=NOOP] - callback to occur when exiting the modal * @param {string} [options.key] - key used to identify the modal. If not provided, one is generated and returned * @returns {string} - the key used for this modal */ @@ -168,7 +169,7 @@ export default class Modals { if (content instanceof FormattableString) content = content.toString(); const emptyFunction = () => {}; - const {onConfirm = emptyFunction, onCancel = emptyFunction, confirmText = Strings.Modals.okay, cancelText = Strings.Modals.cancel, danger = false, key = undefined} = options; + const {onClose = emptyFunction, onConfirm = emptyFunction, onCancel = emptyFunction, confirmText = Strings.Modals.okay, cancelText = Strings.Modals.cancel, danger = false, key = undefined} = options; if (!this.ModalActions || !this.ConfirmationModal || !this.Markdown) { return this.default(title, content, [ @@ -181,6 +182,7 @@ export default class Modals { content = content.map(c => typeof(c) === "string" ? React.createElement(Markdown, null, c) : c); const modalKey = ModalActions.openModal(props => { + console.log(props); return React.createElement(ErrorBoundary, { onError: () => { setTimeout(() => { @@ -197,7 +199,8 @@ export default class Modals { confirmText: confirmText, cancelText: cancelText, onConfirm: onConfirm, - onCancel: onCancel + onCancel: onCancel, + onCloseCallback: () => {if (props?.transitionState === 1) onClose?.()} }, props), React.createElement(ErrorBoundary, {}, content))); }, {modalKey: key}); return modalKey; From 54100bfe33066e077a285ee6495b5fc9c9c25ecf Mon Sep 17 00:00:00 2001 From: F04C Date: Wed, 21 Feb 2024 19:58:40 -0500 Subject: [PATCH 10/16] Prompt to delete large debug logs Fixes #1648 --- assets/locales/en-us.json | 3 ++- renderer/src/builtins/developer/debuglogs.js | 23 +++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/assets/locales/en-us.json b/assets/locales/en-us.json index a2d5cd9..deacfe1 100644 --- a/assets/locales/en-us.json +++ b/assets/locales/en-us.json @@ -212,7 +212,8 @@ "restartLater": "Restart Later", "additionalInfo": "Additional Info", "restartPrompt": "In order to take effect, Discord needs to be restarted. Do you want to restart now?", - "changelog": "Changelog" + "changelog": "Changelog", + "debuglog": "Your debug log file has exceeded 100MB, would you like to clear the log?" }, "ReactDevTools": { "notFound": "Extension Not Found", diff --git a/renderer/src/builtins/developer/debuglogs.js b/renderer/src/builtins/developer/debuglogs.js index b1b0ea0..8a7c68f 100644 --- a/renderer/src/builtins/developer/debuglogs.js +++ b/renderer/src/builtins/developer/debuglogs.js @@ -4,6 +4,8 @@ import path from "path"; import Builtin from "@structs/builtin"; import DataStore from "@modules/datastore"; +import Modals from "@ui/modals"; + const timestamp = () => new Date().toISOString().replace("T", " ").replace("Z", ""); const levels = ["log", "info", "warn", "error", "debug"]; @@ -28,8 +30,9 @@ export default new class DebugLogs extends Builtin { get category() {return "developer";} get id() {return "debugLogs";} - enabled() { + async enabled() { this.logFile = path.join(DataStore.dataFolder, "debug.log"); + await this.checkFilesize(); this.stream = fs.createWriteStream(this.logFile, {flags: "a"}); this.stream.write(`\n\n================= Starting Debug Log (${timestamp()}) =================\n`); for (const level of levels) { @@ -62,4 +65,22 @@ export default new class DebugLogs extends Builtin { } return sanitized.join(" "); } + + async checkFilesize() { + try { + const stats = fs.statSync(this.logFile); + const mb = stats.size / (1024 * 1024); + if (mb < 100) return; // Under 100MB, all good + return new Promise(resolve => Modals.showConfirmationModal(Strings.Modals.additionalInfo, Strings.Modals.debuglog, { + confirmText: Strings.Modals.okay, + cancelText: Strings.Modals.cancel, + danger: true, + onConfirm: () => fs.rmSync(this.logFile), + onClose: resolve + })); + } + catch (e) { + this.error(e); + } + } }; \ No newline at end of file From d505a29df03d287cc0ab7a00b8c69d587ed5cea8 Mon Sep 17 00:00:00 2001 From: F04C Date: Wed, 21 Feb 2024 23:10:24 -0500 Subject: [PATCH 11/16] Allow clearing search field --- renderer/src/styles/search.css | 14 ++++++++++++++ renderer/src/ui/settings/components/search.jsx | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/renderer/src/styles/search.css b/renderer/src/styles/search.css index 3354f08..6c510b4 100644 --- a/renderer/src/styles/search.css +++ b/renderer/src/styles/search.css @@ -24,4 +24,18 @@ .bd-search-wrapper > svg { margin-right: 2px; fill: var(--interactive-normal); +} + +.bd-search-wrapper > .bd-button { + margin-right: 2px; + background: none; + padding: 0; +} + +.bd-search-wrapper > .bd-button > svg .fill { + fill: var(--interactive-normal); +} + +.bd-search-wrapper > .bd-button:hover > svg .fill { + fill: var(--interactive-hover); } \ No newline at end of file diff --git a/renderer/src/ui/settings/components/search.jsx b/renderer/src/ui/settings/components/search.jsx index 26c8d4f..1f8b504 100644 --- a/renderer/src/ui/settings/components/search.jsx +++ b/renderer/src/ui/settings/components/search.jsx @@ -1,4 +1,5 @@ import React from "@modules/react"; +import Close from "@ui/icons/close"; import SearchIcon from "@ui/icons/search"; const {useState, useEffect, useCallback, useRef} = React; @@ -19,9 +20,15 @@ export default function Search({onChange, className, onKeyDown, placeholder}) { setValue(e.target.value); }, [onChange]); + const reset = useCallback(() => { + onChange?.({target: {value: ""}}); + setValue(""); + }, [onChange]); + return
- + {!value && } + {value && }
; } \ No newline at end of file From d4341c71821fd33c48c1ebf2bd1d0c2ff84ec458 Mon Sep 17 00:00:00 2001 From: F04C Date: Wed, 21 Feb 2024 23:10:48 -0500 Subject: [PATCH 12/16] Add onClose to confirmation modal Fixes #1646 --- renderer/src/ui/modals.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/renderer/src/ui/modals.js b/renderer/src/ui/modals.js index d5e5a09..5f4aaeb 100644 --- a/renderer/src/ui/modals.js +++ b/renderer/src/ui/modals.js @@ -182,7 +182,6 @@ export default class Modals { content = content.map(c => typeof(c) === "string" ? React.createElement(Markdown, null, c) : c); const modalKey = ModalActions.openModal(props => { - console.log(props); return React.createElement(ErrorBoundary, { onError: () => { setTimeout(() => { @@ -200,7 +199,9 @@ export default class Modals { cancelText: cancelText, onConfirm: onConfirm, onCancel: onCancel, - onCloseCallback: () => {if (props?.transitionState === 1) onClose?.()} + onCloseCallback: () => { + if (props?.transitionState === 1) onClose?.(); + } }, props), React.createElement(ErrorBoundary, {}, content))); }, {modalKey: key}); return modalKey; From 4631ee145072cb9e5a020da230996817b42a8483 Mon Sep 17 00:00:00 2001 From: F04C Date: Thu, 22 Feb 2024 00:06:30 -0500 Subject: [PATCH 13/16] Revamp list ui + add enable/disable all Fixes #723 --- assets/locales/en-us.json | 6 ++- renderer/src/builtins/developer/debuglogs.js | 1 + renderer/src/modules/addonmanager.js | 37 +++++++++++++-- renderer/src/modules/pluginmanager.js | 3 ++ renderer/src/modules/thememanager.js | 2 + renderer/src/styles/ui/addonlist.css | 24 ++++++---- renderer/src/styles/ui/bdsettings.css | 2 + renderer/src/ui/icons/folder.jsx | 9 ++++ renderer/src/ui/settings/addoncard.jsx | 26 ++++++++-- renderer/src/ui/settings/addonlist.jsx | 47 ++++++++++++++++--- .../src/ui/settings/components/switch.jsx | 7 +-- renderer/src/ui/settings/title.jsx | 4 +- 12 files changed, 138 insertions(+), 30 deletions(-) create mode 100644 renderer/src/ui/icons/folder.jsx diff --git a/assets/locales/en-us.json b/assets/locales/en-us.json index deacfe1..fb2d4b1 100644 --- a/assets/locales/en-us.json +++ b/assets/locales/en-us.json @@ -185,7 +185,11 @@ "isEnabled": "Enabled", "wasLoaded": "{{name}} v{{version}} was loaded.", "listView": "List View", - "gridView": "Grid View" + "gridView": "Grid View", + "enableAll": "Enable All", + "disableAll": "Disable All", + "results": "{{count}} Results", + "enableAllWarning": "Enabling all {{type}} can cause temporary lag and unexpected errors.\n\n(Hold shift while clicking to skip this prompt!)" }, "CustomCSS": { "confirmationText": "You have unsaved changes to your Custom CSS. Closing this window will lose all those changes.", diff --git a/renderer/src/builtins/developer/debuglogs.js b/renderer/src/builtins/developer/debuglogs.js index 8a7c68f..cd1b8f3 100644 --- a/renderer/src/builtins/developer/debuglogs.js +++ b/renderer/src/builtins/developer/debuglogs.js @@ -3,6 +3,7 @@ import path from "path"; import Builtin from "@structs/builtin"; import DataStore from "@modules/datastore"; +import Strings from "@modules/strings"; import Modals from "@ui/modals"; diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js index a88a526..fec5711 100644 --- a/renderer/src/modules/addonmanager.js +++ b/renderer/src/modules/addonmanager.js @@ -1,6 +1,5 @@ import path from "path"; import fs from "fs"; -import {shell} from "electron"; import Logger from "@common/logger"; @@ -18,6 +17,8 @@ import FloatingWindows from "@ui/floatingwindows"; import Toasts from "@ui/toasts"; +// const SWITCH_ANIMATION_TIME = 250; + const openItem = ipc.openPath; const splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/; @@ -271,8 +272,21 @@ export default class AddonManager { if (!addon || addon.partial) return; if (this.state[addon.id]) return; this.state[addon.id] = true; - this.startAddon(addon); - this.saveState(); + this.emit("enabled", addon); + // setTimeout(() => { + this.startAddon(addon); + this.saveState(); + // }, SWITCH_ANIMATION_TIME); + } + + enableAllAddons() { + const originalSetting = Settings.get("settings", "general", "showToasts", false); + Settings.set("settings", "general", "showToasts", false); + for (let a = 0; a < this.addonList.length; a++) { + this.enableAddon(this.addonList[a]); + } + Settings.set("settings", "general", "showToasts", originalSetting); + this.emit("batch"); } disableAddon(idOrAddon) { @@ -280,8 +294,21 @@ export default class AddonManager { if (!addon || addon.partial) return; if (!this.state[addon.id]) return; this.state[addon.id] = false; - this.stopAddon(addon); - this.saveState(); + this.emit("disabled", addon); + // setTimeout(() => { + this.stopAddon(addon); + this.saveState(); + // }, SWITCH_ANIMATION_TIME); + } + + disableAllAddons() { + const originalSetting = Settings.get("settings", "general", "showToasts", false); + Settings.set("settings", "general", "showToasts", false); + for (let a = 0; a < this.addonList.length; a++) { + this.disableAddon(this.addonList[a]); + } + Settings.set("settings", "general", "showToasts", originalSetting); + this.emit("batch"); } toggleAddon(id) { diff --git a/renderer/src/modules/pluginmanager.js b/renderer/src/modules/pluginmanager.js index 28c2a77..07e3796 100644 --- a/renderer/src/modules/pluginmanager.js +++ b/renderer/src/modules/pluginmanager.js @@ -57,6 +57,8 @@ export default new class PluginManager extends AddonManager { saveAddon: this.saveAddon.bind(this), editAddon: this.editAddon.bind(this), deleteAddon: this.deleteAddon.bind(this), + enableAll: this.enableAllAddons.bind(this), + disableAll: this.disableAllAddons.bind(this), prefix: this.prefix }) }); @@ -151,6 +153,7 @@ export default new class PluginManager extends AddonManager { } catch (err) { this.state[addon.id] = false; + this.emit("disabled", addon); Toasts.error(Strings.Addons.couldNotStart.format({name: addon.name, version: addon.version})); Logger.stacktrace(this.name, `${addon.name} v${addon.version} could not be started.`, err); return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "start()"}), {message: err.message, stack: err.stack}, this.prefix); diff --git a/renderer/src/modules/thememanager.js b/renderer/src/modules/thememanager.js index 008ed66..c623747 100644 --- a/renderer/src/modules/thememanager.js +++ b/renderer/src/modules/thememanager.js @@ -35,6 +35,8 @@ export default new class ThemeManager extends AddonManager { saveAddon: this.saveAddon.bind(this), editAddon: this.editAddon.bind(this), deleteAddon: this.deleteAddon.bind(this), + enableAll: this.enableAllAddons.bind(this), + disableAll: this.disableAllAddons.bind(this), prefix: this.prefix }) }); diff --git a/renderer/src/styles/ui/addonlist.css b/renderer/src/styles/ui/addonlist.css index 2ad67c2..fa74266 100644 --- a/renderer/src/styles/ui/addonlist.css +++ b/renderer/src/styles/ui/addonlist.css @@ -212,7 +212,7 @@ flex-wrap: wrap; } -.bd-addon-controls .bd-search { +.bd-settings-title .bd-search { font-size: 13px; margin: 0; width: 200px; @@ -261,35 +261,43 @@ margin-left: 10px; } -.bd-addon-views .bd-view-button { +.bd-addon-controls .bd-button { background-color: transparent; padding: 3px 4px; } -.bd-addon-views .bd-view-button svg { +.bd-addon-controls .bd-button svg { fill: var(--interactive-normal); } -.bd-addon-views .bd-view-button.selected svg { +.bd-addon-controls .bd-button.selected svg { fill: #FFFFFF; } -.bd-addon-views .bd-view-button:hover { +.bd-addon-controls .bd-button:hover { background-color: var(--background-modifier-selected); } -.bd-addon-views .bd-view-button:active { +.bd-addon-controls .bd-button:active { background-color: var(--background-modifier-accent); } -.bd-addon-views .bd-view-button.selected { +.bd-addon-controls .bd-button.selected { background-color: #3E82E5; } -.bd-addon-views .bd-view-button + .bd-view-button { +.bd-addon-controls .bd-button + .bd-button { margin-left: 5px; } +.bd-controls-basic .bd-button:active svg { + fill: #FFFFFF; +} + +.bd-controls-basic .bd-button:active { + background-color: #3E82E5; +} + .bd-addon-list .bd-footer .bd-links, .bd-addon-list .bd-footer .bd-links a, .bd-addon-list .bd-footer .bd-addon-button { diff --git a/renderer/src/styles/ui/bdsettings.css b/renderer/src/styles/ui/bdsettings.css index abc9c35..74ffce7 100644 --- a/renderer/src/styles/ui/bdsettings.css +++ b/renderer/src/styles/ui/bdsettings.css @@ -165,6 +165,8 @@ } .bd-settings-title { + display: flex; + justify-content: space-between; color: var(--header-primary, #FFFFFF); font-weight: 600; cursor: default; diff --git a/renderer/src/ui/icons/folder.jsx b/renderer/src/ui/icons/folder.jsx new file mode 100644 index 0000000..32d56a8 --- /dev/null +++ b/renderer/src/ui/icons/folder.jsx @@ -0,0 +1,9 @@ +import React from "@modules/react"; + +export default function FullScreen(props) { + const size = props.size || "20px"; + return + + + ; +} diff --git a/renderer/src/ui/settings/addoncard.jsx b/renderer/src/ui/settings/addoncard.jsx index ba6d743..6cdd7a1 100644 --- a/renderer/src/ui/settings/addoncard.jsx +++ b/renderer/src/ui/settings/addoncard.jsx @@ -3,6 +3,7 @@ import Logger from "@common/logger"; import SimpleMarkdown from "@structs/markdown"; import React from "@modules/react"; +import Events from "@modules/emitter"; import Strings from "@modules/strings"; import WebpackModules from "@modules/webpackmodules"; import DiscordModules from "@modules/discordmodules"; @@ -25,7 +26,7 @@ import ExtIcon from "@ui/icons/extension"; import ErrorIcon from "@ui/icons/error"; import ThemeIcon from "@ui/icons/theme"; -const {useState, useCallback, useMemo} = React; +const {useState, useCallback, useMemo, useEffect} = React; const LinkIcons = { @@ -88,12 +89,27 @@ function buildLink(type, url) { return makeButton(Strings.Addons[type], link); } -export default function AddonCard({addon, type, disabled, enabled: initialValue, onChange: parentChange, hasSettings, editAddon, deleteAddon, getSettingsPanel}) { +export default function AddonCard({addon, prefix, type, disabled, enabled: initialValue, onChange: parentChange, hasSettings, editAddon, deleteAddon, getSettingsPanel}) { const [isEnabled, setEnabled] = useState(initialValue); + + useEffect(() => { + const onEnabled = updated => { + if (addon.id === updated.id) setEnabled(true); + }; + const onDisabled = updated => { + if (addon.id === updated.id) setEnabled(false); + }; + Events.on(`${prefix}-enabled`, onEnabled); + Events.on(`${prefix}-disabled`, onDisabled); + return () => { + Events.off(`${prefix}-enabled`, onEnabled); + Events.off(`${prefix}-disabled`, onDisabled); + }; + }, [prefix, addon]); + const onChange = useCallback(() => { - setEnabled(!isEnabled); if (parentChange) parentChange(addon.id); - }, [addon.id, parentChange, isEnabled]); + }, [addon.id, parentChange]); const showSettings = useCallback(() => { if (!hasSettings || !isEnabled) return; @@ -154,7 +170,7 @@ export default function AddonCard({addon, type, disabled, enabled: initialValue,
{type === "plugin" ? : }
{title}
- +
{disabled &&
{`An error was encountered while trying to load this ${type}.`}
} diff --git a/renderer/src/ui/settings/addonlist.jsx b/renderer/src/ui/settings/addonlist.jsx index 3b28d42..505c64b 100644 --- a/renderer/src/ui/settings/addonlist.jsx +++ b/renderer/src/ui/settings/addonlist.jsx @@ -15,6 +15,9 @@ import ErrorBoundary from "@ui/errorboundary"; import ListIcon from "@ui/icons/list"; import GridIcon from "@ui/icons/grid"; +import FolderIcon from "@ui/icons/folder"; +import CheckIcon from "@ui/icons/check"; +import CloseIcon from "@ui/icons/close"; import NoResults from "@ui/blankslates/noresults"; import EmptyImage from "@ui/blankslates/emptyimage"; @@ -48,6 +51,12 @@ function blankslate(type, onClick) { ; } +function makeBasicButton(title, children, action) { + return + {(props) => } + ; +} + function makeControlButton(title, children, action, selected = false) { return {(props) => { @@ -81,8 +90,28 @@ function confirmDelete(addon) { }); } +/** + * @param {function} action + * @param {string} type + * @returns + */ +function confirmEnable(action, type) { + /** + * @param {MouseEvent} event + */ + return function(event) { + if (event.shiftKey) return action(); + Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.enableAllWarning.format({type: type.toLocaleLowerCase()}), { + confirmText: Strings.Modals.okay, + cancelText: Strings.Modals.cancel, + danger: true, + onConfirm: action, + }); + }; +} -export default function AddonList({prefix, type, title, folder, addonList, addonState, onChange, reload, editAddon, deleteAddon}) { + +export default function AddonList({prefix, type, title, folder, addonList, addonState, onChange, reload, editAddon, deleteAddon, enableAll, disableAll}) { const [query, setQuery] = useState(""); const [sort, setSort] = useState(getState.bind(null, type, "sort", "name")); const [ascending, setAscending] = useState(getState.bind(null, type, "ascending", true)); @@ -125,7 +154,6 @@ export default function AddonList({prefix, type, title, folder, addonList, addon 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"; @@ -154,18 +182,25 @@ export default function AddonList({prefix, type, title, folder, addonList, addon return sorted.map(addon => { const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function"; const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance); - return triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />; + return triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />; }); - }, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps + }, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, prefix, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps const hasAddonsInstalled = addonList.length !== 0; const isSearching = !!query; const hasResults = renderedCards.length !== 0; return [ - , + + + ,
- + {/* */} +
+ {makeBasicButton(Strings.Addons.openFolder.format({type: title}), , openFolder.bind(null, folder))} + {makeBasicButton(Strings.Addons.enableAll, , confirmEnable(enableAll, title))} + {makeBasicButton(Strings.Addons.disableAll, , disableAll)} +
diff --git a/renderer/src/ui/settings/components/switch.jsx b/renderer/src/ui/settings/components/switch.jsx index d72099a..398a6ae 100644 --- a/renderer/src/ui/settings/components/switch.jsx +++ b/renderer/src/ui/settings/components/switch.jsx @@ -3,17 +3,18 @@ import React from "@modules/react"; const {useState, useCallback} = React; -export default function Switch({id, checked: initialValue, disabled, onChange}) { +export default function Switch({id, checked: initialValue, disabled, onChange, internalState = true}) { const [checked, setChecked] = useState(initialValue); const change = useCallback(() => { onChange?.(!checked); setChecked(!checked); }, [checked, onChange]); + const isChecked = internalState ? checked : initialValue; const enabledClass = disabled ? " bd-switch-disabled" : ""; - const checkedClass = checked ? " bd-switch-checked" : ""; + const checkedClass = isChecked ? " bd-switch-checked" : ""; return
- +
diff --git a/renderer/src/ui/settings/title.jsx b/renderer/src/ui/settings/title.jsx index e42dba8..50f4618 100644 --- a/renderer/src/ui/settings/title.jsx +++ b/renderer/src/ui/settings/title.jsx @@ -6,7 +6,7 @@ const {useCallback} = React; const basicClass = "bd-settings-title"; const groupClass = "bd-settings-title bd-settings-group-title"; -export default function SettingsTitle({isGroup, className, button, onClick, text, otherChildren}) { +export default function SettingsTitle({isGroup, className, button, onClick, text, children}) { const click = useCallback((event) => { event.stopPropagation(); event.preventDefault(); @@ -19,7 +19,7 @@ export default function SettingsTitle({isGroup, className, button, onClick, text return

{onClick?.();}}> {text} {button && } - {otherChildren} + {children}

; } \ No newline at end of file From dcb29f056424363283fdd9a70ec9123d9399abf8 Mon Sep 17 00:00:00 2001 From: F04C Date: Thu, 22 Feb 2024 00:55:46 -0500 Subject: [PATCH 14/16] Fix linting issues --- preload/src/api/fetch.js | 12 ++++++------ preload/src/api/https.js | 3 ++- renderer/src/modules/api/contextmenu.js | 7 +++++-- renderer/src/modules/api/fetch.js | 6 +++--- renderer/src/modules/api/index.js | 2 +- renderer/src/modules/api/webpack.js | 9 ++++----- renderer/src/modules/webpackmodules.js | 6 ++++-- 7 files changed, 25 insertions(+), 20 deletions(-) diff --git a/preload/src/api/fetch.js b/preload/src/api/fetch.js index 098f46f..321c812 100644 --- a/preload/src/api/fetch.js +++ b/preload/src/api/fetch.js @@ -16,12 +16,12 @@ const redirectCodes = new Set([301, 302, 307, 308]); */ /** - * @param {string} url - * @param {FetchOptions} options + * @param {string} requestedUrl + * @param {FetchOptions} fetchOptions */ -export function nativeFetch(url, options) { +export function nativeFetch(requestedUrl, fetchOptions) { let state = "PENDING"; - const data = {content: [], headers: null, statusCode: null, url: url, statusText: "", redirected: false}; + const data = {content: [], headers: null, statusCode: null, url: requestedUrl, statusText: "", redirected: false}; const listeners = new Set(); const errors = new Set(); @@ -121,11 +121,11 @@ export function nativeFetch(url, options) { * reference to the object below so they have no way of * listening to the error through onError. */ - const parsed = new URL(url); + const parsed = new URL(requestedUrl); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { throw new Error(`Unsupported protocol: ${parsed.protocol}`); } - execute(parsed, options); + execute(parsed, fetchOptions); return { onComplete(listener) { diff --git a/preload/src/api/https.js b/preload/src/api/https.js index e2e6ae0..9f84bcc 100644 --- a/preload/src/api/https.js +++ b/preload/src/api/https.js @@ -39,7 +39,8 @@ const makeRequest = (url, options, callback, setReq) => { // Make sure to close the socket. try {req.write(options.formData);} finally {req.end();} - } else { + } + else { req.end(); } diff --git a/renderer/src/modules/api/contextmenu.js b/renderer/src/modules/api/contextmenu.js index 2af0149..ac14efd 100644 --- a/renderer/src/modules/api/contextmenu.js +++ b/renderer/src/modules/api/contextmenu.js @@ -35,7 +35,8 @@ const ContextMenuActions = (() => { } startupComplete &&= typeof(out.closeContextMenu) === "function" && typeof(out.openContextMenu) === "function"; - } catch (error) { + } + catch (error) { startupComplete = false; Logger.stacktrace("ContextMenu~Components", "Fatal startup error:", error); @@ -222,6 +223,7 @@ class ContextMenu { // This is done to make sure the UI actually displays the on/off correctly if (type === "toggle") { + // eslint-disable-next-line react-hooks/rules-of-hooks const [active, doToggle] = React.useState(props.checked || false); const originalAction = props.action; props.checked = active; @@ -330,7 +332,8 @@ Object.freeze(ContextMenu.prototype); try { MenuPatcher.initialize(); -} catch (error) { +} +catch (error) { Logger.error("ContextMenu~Patcher", "Fatal error:", error); } diff --git a/renderer/src/modules/api/fetch.js b/renderer/src/modules/api/fetch.js index 5a20e8c..fa82a02 100644 --- a/renderer/src/modules/api/fetch.js +++ b/renderer/src/modules/api/fetch.js @@ -80,13 +80,13 @@ export default function fetch(url, options = {}) { ctx.onComplete(() => { try { - const data = ctx.readData(); + const resultData = ctx.readData(); const req = new FetchResponse({ method: options.method ?? "GET", - status: data.statusCode, + status: resultData.statusCode, ...options, - ...data + ...resultData }); resolve(req); diff --git a/renderer/src/modules/api/index.js b/renderer/src/modules/api/index.js index 37cb7fc..421c5a9 100644 --- a/renderer/src/modules/api/index.js +++ b/renderer/src/modules/api/index.js @@ -58,7 +58,7 @@ export default class BdApi { get ContextMenu() {return ContextMenuAPI;} Components = { get Tooltip() {return DiscordModules.Tooltip;} - } + }; Net = {fetch}; } diff --git a/renderer/src/modules/api/webpack.js b/renderer/src/modules/api/webpack.js index c6f2788..2c45443 100644 --- a/renderer/src/modules/api/webpack.js +++ b/renderer/src/modules/api/webpack.js @@ -4,11 +4,10 @@ import WebpackModules, {Filters} from "@modules/webpackmodules"; const getOptions = (args, defaultOptions = {}) => { - if (args.length > 1 && - typeof(args[args.length - 1]) === "object" && - !Array.isArray(args[args.length - 1]) && - args[args.length - 1] !== null - ) { + if (args.length > 1 + && typeof(args[args.length - 1]) === "object" // eslint-disable-line operator-linebreak + && !Array.isArray(args[args.length - 1]) // eslint-disable-line operator-linebreak + && args[args.length - 1] !== null) { // eslint-disable-line operator-linebreak Object.assign(defaultOptions, args.pop()); } diff --git a/renderer/src/modules/webpackmodules.js b/renderer/src/modules/webpackmodules.js index da64664..40cb965 100644 --- a/renderer/src/modules/webpackmodules.js +++ b/renderer/src/modules/webpackmodules.js @@ -190,7 +190,8 @@ export default class WebpackModules { if (!modules.hasOwnProperty(index)) continue; let module = null; - try {module = modules[index];} catch {continue;} + try {module = modules[index];} + catch {continue;} const {exports} = module; if (!exports || exports === window || exports === document.documentElement || exports[Symbol.toStringTag] === "DOMTokenList") continue; @@ -199,7 +200,8 @@ export default class WebpackModules { for (const key in exports) { let foundModule = null; let wrappedExport = null; - try {wrappedExport = exports[key];} catch {continue;} + try {wrappedExport = exports[key];} + catch {continue;} if (!wrappedExport) continue; if (wrappedFilter(wrappedExport, module, index)) foundModule = wrappedExport; From d952a69c80b16c3baf48143177f95bf179246365 Mon Sep 17 00:00:00 2001 From: F04C Date: Thu, 22 Feb 2024 01:33:23 -0500 Subject: [PATCH 15/16] Add built-in settings context menu Fixes #476 --- assets/locales/en-us.json | 4 + renderer/src/builtins/builtins.js | 1 + renderer/src/builtins/general/contextmenu.js | 95 ++++++++++++++++++++ renderer/src/data/settings.js | 3 +- 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 renderer/src/builtins/general/contextmenu.js diff --git a/assets/locales/en-us.json b/assets/locales/en-us.json index fb2d4b1..e54cb86 100644 --- a/assets/locales/en-us.json +++ b/assets/locales/en-us.json @@ -21,6 +21,10 @@ "mediaKeys": { "name": "Disable Media Keys", "note": "Prevents Discord from hijacking your media keys after playing a video." + }, + "bdContextMenu": { + "name": "Settings Context Menu", + "note": "Adds a BetterDiscord subsection to the settings context menu." } }, "window": { diff --git a/renderer/src/builtins/builtins.js b/renderer/src/builtins/builtins.js index 7e49967..bcbebef 100644 --- a/renderer/src/builtins/builtins.js +++ b/renderer/src/builtins/builtins.js @@ -4,6 +4,7 @@ export {default as CustomCSS} from "./customcss"; export {default as VoiceDisconnect} from "./general/voicedisconnect"; export {default as MediaKeys} from "./general/mediakeys"; +export {default as BDContextMenu} from "./general/contextmenu"; // export {default as EmoteModule} from "./emotes/emotes"; // export {default as EmoteMenu} from "./emotes/emotemenu"; diff --git a/renderer/src/builtins/general/contextmenu.js b/renderer/src/builtins/general/contextmenu.js new file mode 100644 index 0000000..0fa3eff --- /dev/null +++ b/renderer/src/builtins/general/contextmenu.js @@ -0,0 +1,95 @@ +import Builtin from "@structs/builtin"; + +import Strings from "@modules/strings"; +import Settings from "@modules/settingsmanager"; +import Webpack from "@modules/webpackmodules"; + +import ContextMenuPatcher from "@modules/api/contextmenu"; +import pluginManager from "@modules/pluginmanager"; +import themeManager from "@modules/thememanager"; + + +const ContextMenu = new ContextMenuPatcher(); +const UserSettingsWindow = Webpack.getByProps("open", "updateAccount"); + +export default new class BDContextMenu extends Builtin { + get name() {return "BDContextMenu";} + get category() {return "general";} + get id() {return "bdContextMenu";} + + constructor() { + super(...arguments); + this.callback = this.callback.bind(this); + } + + enabled() { + this.patch = ContextMenu.patch("user-settings-cog", this.callback); + } + + disabled() { + this.patch?.(); + } + + callback(retVal) { + const items = Settings.collections.map(c => this.buildCollectionMenu(c)); + items.push({label: Strings.panels.updates, action: () => {this.openCategory("updates");}}); + if (Settings.get("settings", "customcss", "customcss")) items.push({label: Strings.panels.customcss, action: () => {this.openCategory("customcss");}}); + items.push(this.buildAddonMenu(Strings.panels.plugins, pluginManager)); + items.push(this.buildAddonMenu(Strings.panels.themes, themeManager)); + retVal?.props?.children?.props?.children?.[0].push(ContextMenu.buildItem({type: "separator"})); + retVal?.props?.children?.props?.children?.[0].push(ContextMenu.buildItem({type: "submenu", label: "BetterDiscord", items: items})); + } + + buildCollectionMenu(collection) { + return { + type: "submenu", + label: collection.name, + action: () => {this.openCategory(collection.name);}, + items: collection.settings.map(category => { + return { + type: "submenu", + label: category.name, + action: () => {this.openCategory(collection.name);}, + items: category.settings.filter(s => s.type === "switch" && !s.hidden && s.id !== this.id).map(setting => { + return { + type: "toggle", + label: setting.name, + disabled: setting.disabled, + active: Settings.get(collection.id, category.id, setting.id), + action: () => Settings.set(collection.id, category.id, setting.id, !Settings.get(collection.id, category.id, setting.id)) + }; + }) + }; + }) + }; + } + + /** + * + * @param {string} label + * @param {import("../../modules/addonmanager").default} manager + * @returns + */ + buildAddonMenu(label, manager) { + const names = manager.addonList.map(a => a.name || a.getName()).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + return { + type: "submenu", + label: label, + action: () => {this.openCategory(label.toLowerCase());}, + items: names.map(name => { + return { + type: "toggle", + label: name, + disabled: manager.getAddon(name)?.partial ?? false, + active: manager.isEnabled(name), + action: () => {manager.toggleAddon(name);} + }; + }) + }; + } + + async openCategory(id) { + ContextMenu.close(); + UserSettingsWindow?.open?.(id); + } +}; \ No newline at end of file diff --git a/renderer/src/data/settings.js b/renderer/src/data/settings.js index f2eea77..96a1dfd 100644 --- a/renderer/src/data/settings.js +++ b/renderer/src/data/settings.js @@ -6,7 +6,8 @@ export default [ settings: [ {type: "switch", id: "voiceDisconnect", value: false}, {type: "switch", id: "showToasts", value: true}, - {type: "switch", id: "mediaKeys", value: false} + {type: "switch", id: "mediaKeys", value: false}, + {type: "switch", id: "bdContextMenu", value: true} ] }, { From a80576f231803a7b58a2ccc37f91d4794460a2f3 Mon Sep 17 00:00:00 2001 From: F04C Date: Thu, 22 Feb 2024 07:55:22 +0100 Subject: [PATCH 16/16] Fixes Sources interceptor and names Patcher function (#1647) * Make patches easily spotted as opposed to 'anonymous' * reset source back to original method instead of getting the interceptor back in `BdApi.Webpack.modules` * fixes d5ce64 --- renderer/src/modules/patcher.js | 2 +- renderer/src/modules/webpackmodules.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/renderer/src/modules/patcher.js b/renderer/src/modules/patcher.js index 274ea2f..cb48e64 100644 --- a/renderer/src/modules/patcher.js +++ b/renderer/src/modules/patcher.js @@ -50,7 +50,7 @@ } static makeOverride(patch) { - return function () { + return function BDPatcher() { let returnValue; if (!patch.children || !patch.children.length) return patch.originalFunction.apply(this, arguments); for (const superPatch of patch.children.filter(c => c.type === "before")) { diff --git a/renderer/src/modules/webpackmodules.js b/renderer/src/modules/webpackmodules.js index 40cb965..1595752 100644 --- a/renderer/src/modules/webpackmodules.js +++ b/renderer/src/modules/webpackmodules.js @@ -524,6 +524,9 @@ export default class WebpackModules { catch (error) { Logger.stacktrace("WebpackModules", "Could not patch pushed module", error); } + finally{ + require.m[moduleId] = originalModule; + } }; Object.assign(modules[moduleId], originalModule, {