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": {
"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",

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

View File

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

View File

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

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

View File

@ -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"],

View File

@ -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": {

View File

@ -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),

View File

@ -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) {

View File

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

View File

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

View File

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

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

View File

@ -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}
]
},
{

View File

@ -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) {

View File

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

View File

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

View File

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

View File

@ -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 = {}) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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")) {

View File

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

View File

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

View File

@ -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, {

View File

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

View File

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

View File

@ -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 {

View File

@ -165,6 +165,8 @@
}
.bd-settings-title {
display: flex;
justify-content: space-between;
color: var(--header-primary, #FFFFFF);
display: flex;
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 {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;

View File

@ -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,
<div className="bd-addon-header">
{type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />}
<div className="bd-title">{title}</div>
<Switch disabled={disabled} checked={isEnabled} onChange={onChange} />
<Switch internalState={false} disabled={disabled} checked={isEnabled} onChange={onChange} />
</div>
<div className="bd-description-wrap">
{disabled && <div className="banner banner-danger"><ErrorIcon className="bd-icon" />{`An error was encountered while trying to load this ${type}.`}</div>}

View File

@ -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) {
</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) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(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 <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 isSearching = !!query;
const hasResults = renderedCards.length !== 0;
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"}>
<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-addon-dropdowns">
<div className="bd-select-wrapper">

View File

@ -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 <div className={"bd-search-wrapper" + (className ? ` ${className}` : "")}>
<input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-search" placeholder={placeholder} maxLength="50" value={value} />
<SearchIcon />
<input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-search" placeholder={placeholder} maxLength="50" value={value} ref={input}/>
{!value && <SearchIcon />}
{value && <button className="bd-button" onClick={reset}><Close size="16px" /></button>}
</div>;
}

View File

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

View File

@ -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 <h2 className={titleClass} onClick={() => {onClick?.();}}>
{text}
{button && <Button className="bd-button-title" onClick={click} size={Button.Sizes.NONE}>{button.title}</Button>}
{otherChildren}
{children}
</h2>;
}

View File

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