Merge branch 'main' into development

This commit is contained in:
Zerebos 2024-02-22 02:07:00 -05:00
commit afa61df8d4
42 changed files with 590 additions and 130 deletions

View File

@ -21,6 +21,10 @@
"mediaKeys": { "mediaKeys": {
"name": "Disable Media Keys", "name": "Disable Media Keys",
"note": "Prevents Discord from hijacking your media keys after playing a video." "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": { "window": {
@ -188,7 +192,11 @@
"isEnabled": "Enabled", "isEnabled": "Enabled",
"wasLoaded": "{{name}} v{{version}} was loaded.", "wasLoaded": "{{name}} v{{version}} was loaded.",
"listView": "List View", "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": { "CustomCSS": {
"confirmationText": "You have unsaved changes to your Custom CSS. Closing this window will lose all those changes.", "confirmationText": "You have unsaved changes to your Custom CSS. Closing this window will lose all those changes.",
@ -215,7 +223,8 @@
"restartLater": "Restart Later", "restartLater": "Restart Later",
"additionalInfo": "Additional Info", "additionalInfo": "Additional Info",
"restartPrompt": "In order to take effect, Discord needs to be restarted. Do you want to restart now?", "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": { "ReactDevTools": {
"notFound": "Extension Not Found", "notFound": "Extension Not Found",

189
assets/locales/es-419.json Normal file
View File

@ -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?"
}
}

View File

@ -21,6 +21,7 @@ module.exports = {
"ru": require("./ru.json"), // Russian "ru": require("./ru.json"), // Russian
"sk": require("./sk.json"), // Slovak "sk": require("./sk.json"), // Slovak
"es-ES": require("./es-es.json"), // Spanish (Spain) "es-ES": require("./es-es.json"), // Spanish (Spain)
"es-419": require("./es-419.json"), // Spanish (LATAM)
"sv-SE": require("./sv-se.json"), // Swedish "sv-SE": require("./sv-se.json"), // Swedish
"tr": require("./tr.json"), // Turkish "tr": require("./tr.json"), // Turkish
"bg": require("./bg.json"), // Bulgarian "bg": require("./bg.json"), // Bulgarian

View File

@ -1,6 +1,6 @@
{ {
"Panels": { "Panels": {
"plugins": "Tiện Ích", "plugins": "Tiện ích",
"themes": "Chủ đề", "themes": "Chủ đề",
"customcss": "CSS Tùy Chỉnh", "customcss": "CSS Tùy Chỉnh",
"updates": "Cập nhật" "updates": "Cập nhật"
@ -11,8 +11,8 @@
"general": { "general": {
"name": "Chung", "name": "Chung",
"voiceDisconnect": { "voiceDisconnect": {
"name": "Ngắt Kết Nối Voice", "name": "Ngắt Kết Nối Kênh Đàm Thoại",
"note": "Ngắt kết nối kênh voice khi đóng Discord" "note": "Ngắt kết nối kênh đàm thoại khi đóng Discord"
}, },
"showToasts": { "showToasts": {
"name": "HIển Thị Thông Báo", "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", "note": "Nơi trình chỉnh sửa tiện ích và chủ đề hiển thị khi sửa",
"options": { "options": {
"detached": "Cửa Sổ Tách Rời", "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": { "inspectElement": {
"name": "Phím tắt Kiểm tra Thành Phần", "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": { "devToolsWarning": {
"name": "Tắt Cảnh Báo DevTools", "name": "Tắt Cảnh Báo DevTools",
@ -105,7 +105,7 @@
}, },
"devTools": { "devTools": {
"name": "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": { "editor": {
@ -115,8 +115,8 @@
"note": "Hiển thị số dòng ở cạnh của trình chỉnh sửa" "note": "Hiển thị số dòng ở cạnh của trình chỉnh sửa"
}, },
"fontSize": { "fontSize": {
"name": "Kích Cỡ Chữ", "name": "Kích Thước Phông Chữ",
"note": "Kích cỡ chữ (pt) được sử dụng trong trình chỉnh sửa" "note": "Kích thước phông chữ (pt) được sử dụng trong trình chỉnh sửa"
}, },
"minimap": { "minimap": {
"name": "Minimap", "name": "Minimap",
@ -131,12 +131,12 @@
"note": "Hiển thị những đề xuất tự hoàn thành khi bạn gõ" "note": "Hiển thị những đề xuất tự hoàn thành khi bạn gõ"
}, },
"renderWhitespace": { "renderWhitespace": {
"name": "Hiển Thị Khoảng Cách Trắng", "name": "Hiển Thị Khoảng Trắng",
"note": "Khi nào khoảng cách trắng sẽ được hiển thị trong trình chỉnh sửa", "note": "Khi nào khoảng trắng sẽ được hiển thị trong trình chỉnh sửa",
"options": { "options": {
"all": "Luôn luôn", "all": "Luôn luôn",
"none": "Không bao giờ", "none": "Không bao giờ",
"selection": "Lựa chọn" "selection": "Tùy chọn"
} }
} }
} }
@ -145,7 +145,7 @@
"Addons": { "Addons": {
"title": "{{name}} v{{version}} bởi {{author}}", "title": "{{name}} v{{version}} bởi {{author}}",
"byline": "bởi {{author}}", "byline": "bởi {{author}}",
"openFolder": "Mở Thư Mục {{type}}", "openFolder": "Mở thư mục {{type}}",
"reload": "Tải lại", "reload": "Tải lại",
"addonSettings": "Cài đặt", "addonSettings": "Cài đặt",
"website": "Trang web", "website": "Trang web",
@ -157,7 +157,7 @@
"author": "Tác giả", "author": "Tác giả",
"version": "Phiên bản", "version": "Phiên bản",
"added": "Ngày thêm", "added": "Ngày thêm",
"modified": "Ngày chỉnh sửa", "modified": "Ngày sửa đổi",
"search": "Tìm kiếm {{type}}", "search": "Tìm kiếm {{type}}",
"editAddon": "Chỉnh sửa", "editAddon": "Chỉnh sửa",
"deleteAddon": "Xóa", "deleteAddon": "Xóa",
@ -167,23 +167,23 @@
"disabled": "{{name}} đã được vô hiệu hóa.", "disabled": "{{name}} đã được vô hiệu hóa.",
"couldNotEnable": "{{name}} không thể được kích hoạt.", "couldNotEnable": "{{name}} không thể được kích hoạt.",
"couldNotDisable": "{{name}} không thể được vô hiệu hóa.", "couldNotDisable": "{{name}} không thể được vô hiệu hóa.",
"couldNotStart": "{{name}} không thể được bắt đầu.", "couldNotStart": "{{name}} không thể bắt đầu.",
"couldNotStop": "{{name}} không thể được dừng lại.", "couldNotStop": "{{name}} không thể dừng lại.",
"settingsError": "Không thể mở cài đặt cho {{name}}", "settingsError": "Không thể mở cài đặt cho {{name}}",
"methodError": "{{method}} không thể được kích hoạt.", "methodError": "{{method}} không thể được kích hoạt.",
"unknownAuthor": "Tác giả không xác định", "unknownAuthor": "Tác giả không xác định",
"noDescription": "Không có miêu tả.", "noDescription": "Không có miêu tả.",
"alreadyExists": "Đã có {{type}} với tên {{name}} rồi!", "alreadyExists": "Đã có {{type}} với tên {{name}} rồi!",
"alreadWatching": "Đã đang xem tiện ích.", "alreadWatching": "Đã đang xem tiện ích.",
"metaError": "META không thể được phân tích.", "metaError": "Không thể phân tích META.",
"missingNameData": "META đang thiếu tên dữ liệu.", "missingNameData": "Thiếu dữ liệu tên META.",
"metaNotFound": "Không thể tìm thấy 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.", "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}}!", "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}}.", "blankSlateMessage": "Lấy một số ở [trang web]({{link}}) và thêm vào thư mục {{type}}.",
"isEnabled": "Đã kích hoạt", "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", "listView": "Dạng Danh Sách",
"gridView": "Dạng Ô" "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.", "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", "update": "Cập nhật",
"save": "Lưu", "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", "openDetached": "Cửa Sổ Riêng",
"settings": "Cài đặt Trình Chỉnh Sửa", "settings": "Cài đặt Trình Chỉnh Sửa",
"editorTitle": "Trình Chỉnh Sửa CSS Tùy Chỉnh" "editorTitle": "Trình Chỉnh Sửa CSS Tùy Chỉnh"
@ -225,7 +225,7 @@
"descending": "Dưới lên trên" "descending": "Dưới lên trên"
}, },
"WindowPrefs": { "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ờ?" "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": { "Notices": {
@ -233,7 +233,7 @@
}, },
"Updater": { "Updater": {
"updateFailed": "Cập Nhật Thất Bại!", "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!", "updateSuccessful": "Cập Nhật Thành Công!",
"updateAvailable": "BetterDiscord có cập nhật mới (v{{version}})", "updateAvailable": "BetterDiscord có cập nhật mới (v{{version}})",
"addonUpdatesAvailable": "BetterDiscord đã phát hiện {{count}} cho {{type}} của bạn!", "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ứ!", "updateAll": "Cập Nhật Mọi Thứ!",
"noUpdatesAvailable": "Không có cập nhật mới.", "noUpdatesAvailable": "Không có cập nhật mới.",
"versionAvailable": "Phiên bản {{version}} đã có sẵn!", "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!" "updateButton": "Cập nhật!"
} }
} }

View File

@ -16,4 +16,5 @@ export const WINDOW_SIZE = "bd-window-size";
export const DEVTOOLS_WARNING = "bd-remove-devtools-message"; export const DEVTOOLS_WARNING = "bd-remove-devtools-message";
export const OPEN_DIALOG = "bd-open-dialog"; export const OPEN_DIALOG = "bd-open-dialog";
export const REGISTER_PRELOAD = "bd-register-preload"; export const REGISTER_PRELOAD = "bd-register-preload";
export const GET_ACCENT_COLOR = "bd-get-accent-color"; export const GET_ACCENT_COLOR = "bd-get-accent-color";
export const OPEN_PATH = "bd-open-path";

View File

@ -1,6 +1,7 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import electron from "electron"; import electron from "electron";
import {spawn} from "child_process";
import ReactDevTools from "./reactdevtools"; import ReactDevTools from "./reactdevtools";
import * as IPCEvents from "common/constants/ipcevents"; import * as IPCEvents from "common/constants/ipcevents";
@ -97,7 +98,8 @@ export default class BetterDiscord {
electron.app.exit(); electron.app.exit();
} }
if (result.response === 1) { 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; hasCrashed = false;

View File

@ -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"; import * as IPCEvents from "common/constants/ipcevents";
@ -32,6 +33,11 @@ const getPath = (event, pathReq) => {
event.returnValue = returnPath; event.returnValue = returnPath;
}; };
const openPath = (event, path) => {
if (process.platform === "win32") spawn("explorer.exe", [path]);
else shell.openPath(path);
};
const relaunch = () => { const relaunch = () => {
app.quit(); app.quit();
app.relaunch(); app.relaunch();
@ -140,6 +146,7 @@ export default class IPCMain {
static registerEvents() { static registerEvents() {
try { try {
ipc.on(IPCEvents.GET_PATH, getPath); ipc.on(IPCEvents.GET_PATH, getPath);
ipc.on(IPCEvents.OPEN_PATH, openPath);
ipc.on(IPCEvents.RELAUNCH, relaunch); ipc.on(IPCEvents.RELAUNCH, relaunch);
ipc.on(IPCEvents.OPEN_DEVTOOLS, openDevTools); ipc.on(IPCEvents.OPEN_DEVTOOLS, openDevTools);
ipc.on(IPCEvents.CLOSE_DEVTOOLS, closeDevTools); ipc.on(IPCEvents.CLOSE_DEVTOOLS, closeDevTools);

View File

@ -19,7 +19,8 @@ module.exports = (env, argv) => ({
rimraf: `require("rimraf")`, rimraf: `require("rimraf")`,
yauzl: `require("yauzl")`, yauzl: `require("yauzl")`,
mkdirp: `require("mkdirp")`, mkdirp: `require("mkdirp")`,
module: `require("module")` module: `require("module")`,
child_process: `require("child_process")`,
}, },
resolve: { resolve: {
extensions: [".js"], extensions: [".js"],

View File

@ -1,6 +1,6 @@
{ {
"name": "betterdiscord", "name": "betterdiscord",
"version": "1.9.6", "version": "1.9.7",
"description": "Enhances Discord by adding functionality and themes.", "description": "Enhances Discord by adding functionality and themes.",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {

View File

@ -3,7 +3,6 @@ import {ipcRenderer as IPC, shell} from "electron";
export const ipcRenderer = { export const ipcRenderer = {
send: IPC.send.bind(IPC), send: IPC.send.bind(IPC),
sendToHost: IPC.sendToHost.bind(IPC), sendToHost: IPC.sendToHost.bind(IPC),
sendTo: IPC.sendTo.bind(IPC),
sendSync: IPC.sendSync.bind(IPC), sendSync: IPC.sendSync.bind(IPC),
invoke: IPC.invoke.bind(IPC), invoke: IPC.invoke.bind(IPC),
on: IPC.on.bind(IPC), on: IPC.on.bind(IPC),

View File

@ -16,12 +16,12 @@ const redirectCodes = new Set([301, 302, 307, 308]);
*/ */
/** /**
* @param {string} url * @param {string} requestedUrl
* @param {FetchOptions} options * @param {FetchOptions} fetchOptions
*/ */
export function nativeFetch(url, options) { export function nativeFetch(requestedUrl, fetchOptions) {
let state = "PENDING"; 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 listeners = new Set();
const errors = 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 * reference to the object below so they have no way of
* listening to the error through onError. * listening to the error through onError.
*/ */
const parsed = new URL(url); const parsed = new URL(requestedUrl);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error(`Unsupported protocol: ${parsed.protocol}`); throw new Error(`Unsupported protocol: ${parsed.protocol}`);
} }
execute(parsed, options); execute(parsed, fetchOptions);
return { return {
onComplete(listener) { onComplete(listener) {

View File

@ -39,7 +39,8 @@ const makeRequest = (url, options, callback, setReq) => {
// Make sure to close the socket. // Make sure to close the socket.
try {req.write(options.formData);} try {req.write(options.formData);}
finally {req.end();} finally {req.end();}
} else { }
else {
req.end(); req.end();
} }

View File

@ -4,6 +4,7 @@ export {default as CustomCSS} from "./customcss";
export {default as VoiceDisconnect} from "./general/voicedisconnect"; export {default as VoiceDisconnect} from "./general/voicedisconnect";
export {default as MediaKeys} from "./general/mediakeys"; export {default as MediaKeys} from "./general/mediakeys";
export {default as BDContextMenu} from "./general/contextmenu";
// export {default as EmoteModule} from "./emotes/emotes"; // export {default as EmoteModule} from "./emotes/emotes";
// export {default as EmoteMenu} from "./emotes/emotemenu"; // export {default as EmoteMenu} from "./emotes/emotemenu";

View File

@ -3,6 +3,9 @@ import path from "path";
import Builtin from "@structs/builtin"; import Builtin from "@structs/builtin";
import DataStore from "@modules/datastore"; import DataStore from "@modules/datastore";
import Strings from "@modules/strings";
import Modals from "@ui/modals";
const timestamp = () => new Date().toISOString().replace("T", " ").replace("Z", ""); 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 category() {return "developer";}
get id() {return "debugLogs";} get id() {return "debugLogs";}
enabled() { async enabled() {
this.logFile = path.join(DataStore.dataFolder, "debug.log"); this.logFile = path.join(DataStore.dataFolder, "debug.log");
await this.checkFilesize();
this.stream = fs.createWriteStream(this.logFile, {flags: "a"}); this.stream = fs.createWriteStream(this.logFile, {flags: "a"});
this.stream.write(`\n\n================= Starting Debug Log (${timestamp()}) =================\n`); this.stream.write(`\n\n================= Starting Debug Log (${timestamp()}) =================\n`);
for (const level of levels) { for (const level of levels) {
@ -62,4 +66,22 @@ export default new class DebugLogs extends Builtin {
} }
return sanitized.join(" "); 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);
}
}
}; };

View File

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

View File

@ -1,35 +1,16 @@
// fixed, improved, added, progress // fixed, improved, added, progress
export default { 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: [ changes: [
{ {
title: "What's New?", title: "What's Fixed?",
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",
type: "fixed", type: "fixed",
items: [ 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.", "Spanish (LATAM) is now properly supported.",
"`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.", "Future cases of unrecognized locales as well as locale fallback now works as intended and shouldn't cause loading issues.",
"`BdApi.Net.fetch` now actually uses all the options passed to it, previously it failed to pass the options to the other process.", "Updated translations for Vietnamese locale.",
"It also now supports all HTTP request types rather than just `POST`, `GET`, `DELETE`, and `PUT`." "Fixed an issue where certain actions (such as favoriting GIFs) caused unexpected lag.",
"Fixed some issues with general client lag."
] ]
} }
] ]

View File

@ -6,7 +6,8 @@ export default [
settings: [ settings: [
{type: "switch", id: "voiceDisconnect", value: false}, {type: "switch", id: "voiceDisconnect", value: false},
{type: "switch", id: "showToasts", value: true}, {type: "switch", id: "showToasts", value: true},
{type: "switch", id: "mediaKeys", value: false} {type: "switch", id: "mediaKeys", value: false},
{type: "switch", id: "bdContextMenu", value: true}
] ]
}, },
{ {

View File

@ -1,6 +1,5 @@
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import {shell} from "electron";
import Logger from "@common/logger"; import Logger from "@common/logger";
@ -11,13 +10,16 @@ import Events from "./emitter";
import DataStore from "./datastore"; import DataStore from "./datastore";
import React from "./react"; import React from "./react";
import Strings from "./strings"; import Strings from "./strings";
import ipc from "./ipc";
import AddonEditor from "@ui/misc/addoneditor"; import AddonEditor from "@ui/misc/addoneditor";
import FloatingWindows from "@ui/floatingwindows"; import FloatingWindows from "@ui/floatingwindows";
import Toasts from "@ui/toasts"; 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 splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/;
const escapedAtRegex = /^\\@/; const escapedAtRegex = /^\\@/;
@ -270,8 +272,21 @@ export default class AddonManager {
if (!addon || addon.partial) return; if (!addon || addon.partial) return;
if (this.state[addon.id]) return; if (this.state[addon.id]) return;
this.state[addon.id] = true; this.state[addon.id] = true;
this.startAddon(addon); this.emit("enabled", addon);
this.saveState(); // 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) { disableAddon(idOrAddon) {
@ -279,8 +294,21 @@ export default class AddonManager {
if (!addon || addon.partial) return; if (!addon || addon.partial) return;
if (!this.state[addon.id]) return; if (!this.state[addon.id]) return;
this.state[addon.id] = false; this.state[addon.id] = false;
this.stopAddon(addon); this.emit("disabled", addon);
this.saveState(); // 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) { toggleAddon(id) {

View File

@ -35,7 +35,8 @@ const ContextMenuActions = (() => {
} }
startupComplete &&= typeof(out.closeContextMenu) === "function" && typeof(out.openContextMenu) === "function"; startupComplete &&= typeof(out.closeContextMenu) === "function" && typeof(out.openContextMenu) === "function";
} catch (error) { }
catch (error) {
startupComplete = false; startupComplete = false;
Logger.stacktrace("ContextMenu~Components", "Fatal startup error:", error); 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 // This is done to make sure the UI actually displays the on/off correctly
if (type === "toggle") { if (type === "toggle") {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [active, doToggle] = React.useState(props.checked || false); const [active, doToggle] = React.useState(props.checked || false);
const originalAction = props.action; const originalAction = props.action;
props.checked = active; props.checked = active;
@ -330,7 +332,8 @@ Object.freeze(ContextMenu.prototype);
try { try {
MenuPatcher.initialize(); MenuPatcher.initialize();
} catch (error) { }
catch (error) {
Logger.error("ContextMenu~Patcher", "Fatal error:", error); Logger.error("ContextMenu~Patcher", "Fatal error:", error);
} }

View File

@ -80,13 +80,13 @@ export default function fetch(url, options = {}) {
ctx.onComplete(() => { ctx.onComplete(() => {
try { try {
const data = ctx.readData(); const resultData = ctx.readData();
const req = new FetchResponse({ const req = new FetchResponse({
method: options.method ?? "GET", method: options.method ?? "GET",
status: data.statusCode, status: resultData.statusCode,
...options, ...options,
...data ...resultData
}); });
resolve(req); resolve(req);

View File

@ -58,7 +58,7 @@ export default class BdApi {
get ContextMenu() {return ContextMenuAPI;} get ContextMenu() {return ContextMenuAPI;}
Components = { Components = {
get Tooltip() {return DiscordModules.Tooltip;} get Tooltip() {return DiscordModules.Tooltip;}
} };
Net = {fetch}; Net = {fetch};
} }

View File

@ -50,6 +50,7 @@ const UI = {
* @param {string} [options.cancelText=Cancel] Text for the cancel button * @param {string} [options.cancelText=Cancel] Text for the cancel button
* @param {callable} [options.onConfirm=NOOP] Callback to occur when clicking the submit button * @param {callable} [options.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.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. * @returns {string} The key used for this modal.
*/ */
showConfirmationModal(title, content, options = {}) { showConfirmationModal(title, content, options = {}) {

View File

@ -4,11 +4,10 @@ import WebpackModules, {Filters} from "@modules/webpackmodules";
const getOptions = (args, defaultOptions = {}) => { const getOptions = (args, defaultOptions = {}) => {
if (args.length > 1 && if (args.length > 1
typeof(args[args.length - 1]) === "object" && && typeof(args[args.length - 1]) === "object" // eslint-disable-line operator-linebreak
!Array.isArray(args[args.length - 1]) && && !Array.isArray(args[args.length - 1]) // eslint-disable-line operator-linebreak
args[args.length - 1] !== null && args[args.length - 1] !== null) { // eslint-disable-line operator-linebreak
) {
Object.assign(defaultOptions, args.pop()); Object.assign(defaultOptions, args.pop());
} }

View File

@ -15,7 +15,7 @@ export default Utilities.memoizeObject({
get ChannelActions() {return WebpackModules.getByProps("selectChannel");}, get ChannelActions() {return WebpackModules.getByProps("selectChannel");},
get LocaleStore() {return WebpackModules.getByProps("locale", "initialize");}, get LocaleStore() {return WebpackModules.getByProps("locale", "initialize");},
get UserStore() {return WebpackModules.getByProps("getCurrentUser", "getUser");}, 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 SimpleMarkdown() {return WebpackModules.getByProps("parseBlock", "parseInline", "defaultOutput");},
get Strings() {return WebpackModules.getByProps("Messages").Messages;}, get Strings() {return WebpackModules.getByProps("Messages").Messages;},
get Dispatcher() {return WebpackModules.getByProps("dispatch", "subscribe", "register");}, get Dispatcher() {return WebpackModules.getByProps("dispatch", "subscribe", "register");},

View File

@ -60,4 +60,8 @@ export default new class IPCRenderer {
getSystemAccentColor() { getSystemAccentColor() {
return ipc.invoke(IPCEvents.GET_ACCENT_COLOR); return ipc.invoke(IPCEvents.GET_ACCENT_COLOR);
} }
openPath(path) {
return ipc.send(IPCEvents.OPEN_PATH, path);
}
}; };

View File

@ -11,7 +11,6 @@ export default new class LocaleManager {
get defaultLocale() {return "en-US";} get defaultLocale() {return "en-US";}
constructor() { constructor() {
this.locale = "";
this.strings = Utilities.extend({}, Locales[this.defaultLocale]); this.strings = Utilities.extend({}, Locales[this.defaultLocale]);
} }
@ -21,16 +20,13 @@ export default new class LocaleManager {
} }
setLocale() { setLocale() {
let newStrings; // Reset to the default locale in case a language is incomplete
if (this.discordLocale != this.defaultLocale) { Utilities.extend(this.strings, Locales[this.defaultLocale]);
newStrings = Locales[this.discordLocale];
if (!newStrings) return this.setLocale(this.defaultLocale); // Get the strings of the new language and extend if a translation exists
} const newStrings = Locales[this.discordLocale];
else { if (newStrings) Utilities.extendTruthy(this.strings, newStrings);
newStrings = Locales[this.defaultLocale];
}
this.locale = this.discordLocale;
Utilities.extendTruthy(this.strings, newStrings);
Events.emit("strings-updated"); Events.emit("strings-updated");
} }
}; };

View File

@ -50,7 +50,7 @@
} }
static makeOverride(patch) { static makeOverride(patch) {
return function () { return function BDPatcher() {
let returnValue; let returnValue;
if (!patch.children || !patch.children.length) return patch.originalFunction.apply(this, arguments); if (!patch.children || !patch.children.length) return patch.originalFunction.apply(this, arguments);
for (const superPatch of patch.children.filter(c => c.type === "before")) { for (const superPatch of patch.children.filter(c => c.type === "before")) {

View File

@ -57,6 +57,8 @@ export default new class PluginManager extends AddonManager {
saveAddon: this.saveAddon.bind(this), saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this), editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this), deleteAddon: this.deleteAddon.bind(this),
enableAll: this.enableAllAddons.bind(this),
disableAll: this.disableAllAddons.bind(this),
prefix: this.prefix prefix: this.prefix
}) })
}); });
@ -151,6 +153,7 @@ export default new class PluginManager extends AddonManager {
} }
catch (err) { catch (err) {
this.state[addon.id] = false; this.state[addon.id] = false;
this.emit("disabled", addon);
Toasts.error(Strings.Addons.couldNotStart.format({name: addon.name, version: addon.version})); 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); 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); return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "start()"}), {message: err.message, stack: err.stack}, this.prefix);

View File

@ -35,6 +35,8 @@ export default new class ThemeManager extends AddonManager {
saveAddon: this.saveAddon.bind(this), saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this), editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this), deleteAddon: this.deleteAddon.bind(this),
enableAll: this.enableAllAddons.bind(this),
disableAll: this.disableAllAddons.bind(this),
prefix: this.prefix prefix: this.prefix
}) })
}); });

View File

@ -190,7 +190,8 @@ export default class WebpackModules {
if (!modules.hasOwnProperty(index)) continue; if (!modules.hasOwnProperty(index)) continue;
let module = null; let module = null;
try {module = modules[index];} catch {continue;} try {module = modules[index];}
catch {continue;}
const {exports} = module; const {exports} = module;
if (!exports || exports === window || exports === document.documentElement || exports[Symbol.toStringTag] === "DOMTokenList") continue; 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) { for (const key in exports) {
let foundModule = null; let foundModule = null;
let wrappedExport = null; let wrappedExport = null;
try {wrappedExport = exports[key];} catch {continue;} try {wrappedExport = exports[key];}
catch {continue;}
if (!wrappedExport) continue; if (!wrappedExport) continue;
if (wrappedFilter(wrappedExport, module, index)) foundModule = wrappedExport; if (wrappedFilter(wrappedExport, module, index)) foundModule = wrappedExport;
@ -522,6 +524,9 @@ export default class WebpackModules {
catch (error) { catch (error) {
Logger.stacktrace("WebpackModules", "Could not patch pushed module", error); Logger.stacktrace("WebpackModules", "Could not patch pushed module", error);
} }
finally{
require.m[moduleId] = originalModule;
}
}; };
Object.assign(modules[moduleId], originalModule, { Object.assign(modules[moduleId], originalModule, {

View File

@ -22,6 +22,12 @@ originalFs.writeFile = (path, data, options) => fs.writeFile(path, data, Object.
export const createRequire = function (path) { export const createRequire = function (path) {
return mod => { 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)) { if (deprecated.has(mod)) {
Logger.warn("Remote~Require", `The "${mod}" module is marked as deprecated. ${deprecated.get(mod)}`); Logger.warn("Remote~Require", `The "${mod}" module is marked as deprecated. ${deprecated.get(mod)}`);
} }

View File

@ -24,4 +24,18 @@
.bd-search-wrapper > svg { .bd-search-wrapper > svg {
margin-right: 2px; margin-right: 2px;
fill: var(--interactive-normal); 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);
} }

View File

@ -212,7 +212,7 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.bd-addon-controls .bd-search { .bd-settings-title .bd-search {
font-size: 13px; font-size: 13px;
margin: 0; margin: 0;
width: 200px; width: 200px;
@ -261,35 +261,43 @@
margin-left: 10px; margin-left: 10px;
} }
.bd-addon-views .bd-view-button { .bd-addon-controls .bd-button {
background-color: transparent; background-color: transparent;
padding: 3px 4px; padding: 3px 4px;
} }
.bd-addon-views .bd-view-button svg { .bd-addon-controls .bd-button svg {
fill: var(--interactive-normal); fill: var(--interactive-normal);
} }
.bd-addon-views .bd-view-button.selected svg { .bd-addon-controls .bd-button.selected svg {
fill: #FFFFFF; fill: #FFFFFF;
} }
.bd-addon-views .bd-view-button:hover { .bd-addon-controls .bd-button:hover {
background-color: var(--background-modifier-selected); 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); background-color: var(--background-modifier-accent);
} }
.bd-addon-views .bd-view-button.selected { .bd-addon-controls .bd-button.selected {
background-color: #3E82E5; background-color: #3E82E5;
} }
.bd-addon-views .bd-view-button + .bd-view-button { .bd-addon-controls .bd-button + .bd-button {
margin-left: 5px; 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,
.bd-addon-list .bd-footer .bd-links a, .bd-addon-list .bd-footer .bd-links a,
.bd-addon-list .bd-footer .bd-addon-button { .bd-addon-list .bd-footer .bd-addon-button {

View File

@ -165,6 +165,8 @@
} }
.bd-settings-title { .bd-settings-title {
display: flex;
justify-content: space-between;
color: var(--header-primary, #FFFFFF); color: var(--header-primary, #FFFFFF);
display: flex; display: flex;
font-weight: 600; font-weight: 600;

View File

@ -0,0 +1,9 @@
import React from "@modules/react";
export default function FullScreen(props) {
const size = props.size || "20px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
</svg>;
}

View File

@ -158,6 +158,7 @@ export default class Modals {
* @param {string} [options.cancelText=Cancel] - text for the cancel button * @param {string} [options.cancelText=Cancel] - text for the cancel button
* @param {callable} [options.onConfirm=NOOP] - callback to occur when clicking the submit button * @param {callable} [options.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.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 * @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 * @returns {string} - the key used for this modal
*/ */
@ -167,7 +168,7 @@ export default class Modals {
if (content instanceof FormattableString) content = content.toString(); if (content instanceof FormattableString) content = content.toString();
const emptyFunction = () => {}; 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) { if (!this.ModalActions) {
return this.default(title, content, [ return this.default(title, content, [
@ -196,7 +197,10 @@ export default class Modals {
confirmText: confirmText, confirmText: confirmText,
cancelText: cancelText, cancelText: cancelText,
onConfirm: onConfirm, onConfirm: onConfirm,
onCancel: onCancel onCancel: onCancel,
onCloseCallback: () => {
if (props?.transitionState === 1) onClose?.();
}
}, props), React.createElement(ErrorBoundary, {}, content))); }, props), React.createElement(ErrorBoundary, {}, content)));
}, {modalKey: key}); }, {modalKey: key});
return modalKey; return modalKey;

View File

@ -3,6 +3,7 @@ import Logger from "@common/logger";
import SimpleMarkdown from "@structs/markdown"; import SimpleMarkdown from "@structs/markdown";
import React from "@modules/react"; import React from "@modules/react";
import Events from "@modules/emitter";
import Strings from "@modules/strings"; import Strings from "@modules/strings";
import WebpackModules from "@modules/webpackmodules"; import WebpackModules from "@modules/webpackmodules";
import DiscordModules from "@modules/discordmodules"; import DiscordModules from "@modules/discordmodules";
@ -25,7 +26,7 @@ import ExtIcon from "@ui/icons/extension";
import ErrorIcon from "@ui/icons/error"; import ErrorIcon from "@ui/icons/error";
import ThemeIcon from "@ui/icons/theme"; import ThemeIcon from "@ui/icons/theme";
const {useState, useCallback, useMemo} = React; const {useState, useCallback, useMemo, useEffect} = React;
const LinkIcons = { const LinkIcons = {
@ -88,12 +89,27 @@ function buildLink(type, url) {
return makeButton(Strings.Addons[type], link); 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); 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(() => { const onChange = useCallback(() => {
setEnabled(!isEnabled);
if (parentChange) parentChange(addon.id); if (parentChange) parentChange(addon.id);
}, [addon.id, parentChange, isEnabled]); }, [addon.id, parentChange]);
const showSettings = useCallback(() => { const showSettings = useCallback(() => {
if (!hasSettings || !isEnabled) return; if (!hasSettings || !isEnabled) return;
@ -154,7 +170,7 @@ export default function AddonCard({addon, type, disabled, enabled: initialValue,
<div className="bd-addon-header"> <div className="bd-addon-header">
{type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />} {type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />}
<div className="bd-title">{title}</div> <div className="bd-title">{title}</div>
<Switch disabled={disabled} checked={isEnabled} onChange={onChange} /> <Switch internalState={false} disabled={disabled} checked={isEnabled} onChange={onChange} />
</div> </div>
<div className="bd-description-wrap"> <div className="bd-description-wrap">
{disabled && <div className="banner banner-danger"><ErrorIcon className="bd-icon" />{`An error was encountered while trying to load this ${type}.`}</div>} {disabled && <div className="banner banner-danger"><ErrorIcon className="bd-icon" />{`An error was encountered while trying to load this ${type}.`}</div>}

View File

@ -3,6 +3,7 @@ import Strings from "@modules/strings";
import Events from "@modules/emitter"; import Events from "@modules/emitter";
import DataStore from "@modules/datastore"; import DataStore from "@modules/datastore";
import DiscordModules from "@modules/discordmodules"; import DiscordModules from "@modules/discordmodules";
import ipc from "@modules/ipc";
import Button from "../base/button"; import Button from "../base/button";
import SettingsTitle from "./title"; import SettingsTitle from "./title";
@ -15,6 +16,9 @@ import ErrorBoundary from "@ui/errorboundary";
import ListIcon from "@ui/icons/list"; import ListIcon from "@ui/icons/list";
import GridIcon from "@ui/icons/grid"; 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 NoResults from "@ui/blankslates/noresults";
import EmptyImage from "@ui/blankslates/emptyimage"; import EmptyImage from "@ui/blankslates/emptyimage";
@ -38,9 +42,7 @@ const buildDirectionOptions = () => [
function openFolder(folder) { function openFolder(folder) {
const shell = require("electron").shell; ipc.openPath(folder);
const open = shell.openItem || shell.openPath;
open(folder);
} }
function blankslate(type, onClick) { function blankslate(type, onClick) {
@ -50,6 +52,12 @@ function blankslate(type, onClick) {
</EmptyImage>; </EmptyImage>;
} }
function makeBasicButton(title, children, action) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => <button {...props} className="bd-button" onClick={action}>{children}</button>}
</DiscordModules.Tooltip>;
}
function makeControlButton(title, children, action, selected = false) { function makeControlButton(title, children, action, selected = false) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}> return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => { {(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 [query, setQuery] = useState("");
const [sort, setSort] = useState(getState.bind(null, type, "sort", "name")); const [sort, setSort] = useState(getState.bind(null, type, "sort", "name"));
const [ascending, setAscending] = useState(getState.bind(null, type, "ascending", true)); 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); if (deleteAddon) deleteAddon(addon);
}, [addonList, deleteAddon]); }, [addonList, deleteAddon]);
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: openFolder.bind(null, folder)} : null;
const renderedCards = useMemo(() => { const renderedCards = useMemo(() => {
let sorted = addonList.sort((a, b) => { let sorted = addonList.sort((a, b) => {
const sortByEnabled = sort === "isEnabled"; const sortByEnabled = sort === "isEnabled";
@ -156,18 +183,25 @@ export default function AddonList({prefix, type, title, folder, addonList, addon
return sorted.map(addon => { return sorted.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function"; const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance); const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>; return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} prefix={prefix} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
}); });
}, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps }, [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 hasAddonsInstalled = addonList.length !== 0;
const isSearching = !!query; const isSearching = !!query;
const hasResults = renderedCards.length !== 0; const hasResults = renderedCards.length !== 0;
return [ return [
<SettingsTitle key="title" text={title} button={button} />, <SettingsTitle key="title" text={isSearching ? `${title} - ${Strings.Addons.results.format({count: `${renderedCards.length}`})}` : title}>
<Search onChange={search} placeholder={`${Strings.Addons.search.format({type: `${renderedCards.length} ${title}`})}...`} />
</SettingsTitle>,
<div className={"bd-controls bd-addon-controls"}> <div className={"bd-controls bd-addon-controls"}>
<Search onChange={search} placeholder={`${Strings.Addons.search.format({type: title})}...`} /> {/* <Search onChange={search} placeholder={`${Strings.Addons.search.format({type: title})}...`} /> */}
<div className="bd-controls-basic">
{makeBasicButton(Strings.Addons.openFolder.format({type: title}), <FolderIcon />, openFolder.bind(null, folder))}
{makeBasicButton(Strings.Addons.enableAll, <CheckIcon size="20px" />, confirmEnable(enableAll, title))}
{makeBasicButton(Strings.Addons.disableAll, <CloseIcon size="20px" />, disableAll)}
</div>
<div className="bd-controls-advanced"> <div className="bd-controls-advanced">
<div className="bd-addon-dropdowns"> <div className="bd-addon-dropdowns">
<div className="bd-select-wrapper"> <div className="bd-select-wrapper">

View File

@ -1,20 +1,34 @@
import React from "@modules/react"; import React from "@modules/react";
import Close from "@ui/icons/close";
import SearchIcon from "@ui/icons/search"; import SearchIcon from "@ui/icons/search";
const {useState, useCallback} = React; const {useState, useEffect, useCallback, useRef} = React;
export default function Search({onChange, className, onKeyDown, placeholder}) { export default function Search({onChange, className, onKeyDown, placeholder}) {
const input = useRef(null);
const [value, setValue] = useState(""); const [value, setValue] = useState("");
// focus search bar on page select
useEffect(()=>{
if (!input.current) return;
input.current.focus();
}, []);
const change = useCallback((e) => { const change = useCallback((e) => {
onChange?.(e); onChange?.(e);
setValue(e.target.value); setValue(e.target.value);
}, [onChange]); }, [onChange]);
const reset = useCallback(() => {
onChange?.({target: {value: ""}});
setValue("");
}, [onChange]);
return <div className={"bd-search-wrapper" + (className ? ` ${className}` : "")}> return <div className={"bd-search-wrapper" + (className ? ` ${className}` : "")}>
<input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-search" placeholder={placeholder} maxLength="50" value={value} /> <input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-search" placeholder={placeholder} maxLength="50" value={value} ref={input}/>
<SearchIcon /> {!value && <SearchIcon />}
{value && <button className="bd-button" onClick={reset}><Close size="16px" /></button>}
</div>; </div>;
} }

View File

@ -3,17 +3,18 @@ import React from "@modules/react";
const {useState, useCallback} = 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 [checked, setChecked] = useState(initialValue);
const change = useCallback(() => { const change = useCallback(() => {
onChange?.(!checked); onChange?.(!checked);
setChecked(!checked); setChecked(!checked);
}, [checked, onChange]); }, [checked, onChange]);
const isChecked = internalState ? checked : initialValue;
const enabledClass = disabled ? " bd-switch-disabled" : ""; const enabledClass = disabled ? " bd-switch-disabled" : "";
const checkedClass = checked ? " bd-switch-checked" : ""; const checkedClass = isChecked ? " bd-switch-checked" : "";
return <div className={`bd-switch` + enabledClass + checkedClass}> return <div className={`bd-switch` + enabledClass + checkedClass}>
<input id={id} type="checkbox" disabled={disabled} checked={checked} onChange={change} /> <input id={id} type="checkbox" disabled={disabled} checked={isChecked} onChange={change} />
<div className="bd-switch-body"> <div className="bd-switch-body">
<svg className="bd-switch-slider" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet"> <svg className="bd-switch-slider" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet">
<rect className="bd-switch-handle" fill="white" x="4" y="0" height="20" width="20" rx="10"></rect> <rect className="bd-switch-handle" fill="white" x="4" y="0" height="20" width="20" rx="10"></rect>

View File

@ -8,7 +8,7 @@ const {useCallback} = React;
const basicClass = "bd-settings-title"; const basicClass = "bd-settings-title";
const groupClass = "bd-settings-title bd-settings-group-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) => { const click = useCallback((event) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
@ -21,7 +21,7 @@ export default function SettingsTitle({isGroup, className, button, onClick, text
return <h2 className={titleClass} onClick={() => {onClick?.();}}> return <h2 className={titleClass} onClick={() => {onClick?.();}}>
{text} {text}
{button && <Button className="bd-button-title" onClick={click} size={Button.Sizes.NONE}>{button.title}</Button>} {button && <Button className="bd-button-title" onClick={click} size={Button.Sizes.NONE}>{button.title}</Button>}
{otherChildren} {children}
</h2>; </h2>;
} }

View File

@ -52,6 +52,7 @@ const editorMap = {
"ru": "ru.json", // Russian "ru": "ru.json", // Russian
"sk": "sk.json", // Slovak "sk": "sk.json", // Slovak
"es": "es-es.json", // Spanish (Spain) "es": "es-es.json", // Spanish (Spain)
"es-419": "es-419.json", // Spanish (LATAM)
"sv": "sv-se.json", // Swedish "sv": "sv-se.json", // Swedish
"tr": "tr.json", // Turkish "tr": "tr.json", // Turkish
"bg": "bg.json", // Bulgarian "bg": "bg.json", // Bulgarian