diff --git a/assets/locales/en-us.json b/assets/locales/en-us.json index b5d5cc6e..a358436c 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": { @@ -188,7 +192,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.", @@ -215,7 +223,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/assets/locales/es-419.json b/assets/locales/es-419.json new file mode 100644 index 00000000..bcc90ba7 --- /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 01a80003..87a94043 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 a47052e5..7ef5bc33 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/common/constants/ipcevents.js b/common/constants/ipcevents.js index 6aa2997b..7b52f3c4 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 cd455ee9..d9481ac3 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 0de96f9a..1d2f1a46 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 5a0f1854..38b5e4fe 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/package.json b/package.json index 15286d8e..a4829186 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/preload/src/api/electron.js b/preload/src/api/electron.js index 246535f9..f8c91558 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), diff --git a/preload/src/api/fetch.js b/preload/src/api/fetch.js index 098f46f1..321c812b 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 e2e6ae0a..9f84bccc 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/builtins/builtins.js b/renderer/src/builtins/builtins.js index 7e499675..bcbebef5 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/developer/debuglogs.js b/renderer/src/builtins/developer/debuglogs.js index b1b0ea0f..cd1b8f3b 100644 --- a/renderer/src/builtins/developer/debuglogs.js +++ b/renderer/src/builtins/developer/debuglogs.js @@ -3,6 +3,9 @@ import path from "path"; import Builtin from "@structs/builtin"; import DataStore from "@modules/datastore"; +import Strings from "@modules/strings"; + +import Modals from "@ui/modals"; const timestamp = () => new Date().toISOString().replace("T", " ").replace("Z", ""); @@ -28,8 +31,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 +66,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 diff --git a/renderer/src/builtins/general/contextmenu.js b/renderer/src/builtins/general/contextmenu.js new file mode 100644 index 00000000..0fa3efff --- /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/changelog.js b/renderer/src/data/changelog.js index d8964c21..7d8489d6 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." ] } ] diff --git a/renderer/src/data/settings.js b/renderer/src/data/settings.js index f2eea77e..96a1dfdc 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} ] }, { diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js index 365445b2..fec5711b 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"; @@ -11,13 +10,16 @@ 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 SWITCH_ANIMATION_TIME = 250; + +const openItem = ipc.openPath; const splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/; const escapedAtRegex = /^\\@/; @@ -270,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) { @@ -279,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/api/contextmenu.js b/renderer/src/modules/api/contextmenu.js index 2af01498..ac14efdf 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 5a20e8c4..fa82a020 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 37cb7fc3..421c5a95 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/ui.js b/renderer/src/modules/api/ui.js index 15332691..b4bac7b8 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/modules/api/webpack.js b/renderer/src/modules/api/webpack.js index c6f27887..2c45443c 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/discordmodules.js b/renderer/src/modules/discordmodules.js index 003ec9d8..a19c3391 100644 --- a/renderer/src/modules/discordmodules.js +++ b/renderer/src/modules/discordmodules.js @@ -15,7 +15,7 @@ export default Utilities.memoizeObject({ get ChannelActions() {return WebpackModules.getByProps("selectChannel");}, get LocaleStore() {return WebpackModules.getByProps("locale", "initialize");}, get UserStore() {return WebpackModules.getByProps("getCurrentUser", "getUser");}, - get InviteActions() {return WebpackModules.getByProps("acceptInvite");}, + get InviteActions() {return WebpackModules.getByProps("createInvite");}, get SimpleMarkdown() {return WebpackModules.getByProps("parseBlock", "parseInline", "defaultOutput");}, get Strings() {return WebpackModules.getByProps("Messages").Messages;}, get Dispatcher() {return WebpackModules.getByProps("dispatch", "subscribe", "register");}, diff --git a/renderer/src/modules/ipc.js b/renderer/src/modules/ipc.js index bb937b05..2f72d1df 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/modules/localemanager.js b/renderer/src/modules/localemanager.js index dcbd7a80..09f10c44 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 diff --git a/renderer/src/modules/patcher.js b/renderer/src/modules/patcher.js index 274ea2fd..cb48e645 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/pluginmanager.js b/renderer/src/modules/pluginmanager.js index 28c2a77f..07e37968 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 008ed669..c6237472 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/modules/webpackmodules.js b/renderer/src/modules/webpackmodules.js index da646646..1595752c 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; @@ -522,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, { diff --git a/renderer/src/polyfill/index.js b/renderer/src/polyfill/index.js index 07b710d6..de3839d8 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)}`); } diff --git a/renderer/src/styles/search.css b/renderer/src/styles/search.css index 3354f08d..6c510b4f 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/styles/ui/addonlist.css b/renderer/src/styles/ui/addonlist.css index 2ad67c29..fa74266d 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 0603206c..7662523c 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); display: flex; font-weight: 600; diff --git a/renderer/src/ui/icons/folder.jsx b/renderer/src/ui/icons/folder.jsx new file mode 100644 index 00000000..32d56a8d --- /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/modals.js b/renderer/src/ui/modals.js index 4655a509..78d6660b 100644 --- a/renderer/src/ui/modals.js +++ b/renderer/src/ui/modals.js @@ -158,6 +158,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 */ @@ -167,7 +168,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) { return this.default(title, content, [ @@ -196,7 +197,10 @@ 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; diff --git a/renderer/src/ui/settings/addoncard.jsx b/renderer/src/ui/settings/addoncard.jsx index 20690984..47181206 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 10ff7fe8..e066d663 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 Button from "../base/button"; import SettingsTitle from "./title"; @@ -15,6 +16,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"; @@ -38,9 +42,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) { @@ -50,6 +52,12 @@ function blankslate(type, onClick) { ; } +function makeBasicButton(title, children, action) { + return + {(props) => } + ; +} + function makeControlButton(title, children, action, selected = false) { return {(props) => { @@ -83,8 +91,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)); @@ -127,7 +155,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"; @@ -156,18 +183,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/search.jsx b/renderer/src/ui/settings/components/search.jsx index 1bd481c8..1f8b504f 100644 --- a/renderer/src/ui/settings/components/search.jsx +++ b/renderer/src/ui/settings/components/search.jsx @@ -1,20 +1,34 @@ import React from "@modules/react"; +import Close from "@ui/icons/close"; 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]); + const reset = useCallback(() => { + onChange?.({target: {value: ""}}); + setValue(""); + }, [onChange]); return
- - + + {!value && } + {value && }
; } \ No newline at end of file diff --git a/renderer/src/ui/settings/components/switch.jsx b/renderer/src/ui/settings/components/switch.jsx index d72099a0..398a6ae9 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 5b8f4c03..553638cd 100644 --- a/renderer/src/ui/settings/title.jsx +++ b/renderer/src/ui/settings/title.jsx @@ -8,7 +8,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(); @@ -21,7 +21,7 @@ export default function SettingsTitle({isGroup, className, button, onClick, text return

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

; } \ No newline at end of file diff --git a/scripts/translations.js b/scripts/translations.js index 6a0a3453..439c08d7 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