// ==UserScript== // @name Themes - Bonk.io // @description Recolors elements in Bonk.io to customizable colors, and allows toggling your theme with a hotkey // @author Excigma // @namespace https://greasyfork.org/users/416480 // @license GPL-3.0 // @version 0.1.7 // @match https://bonk.io/* // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant unsafeWindow // @run-at document-start // @downloadURL none // ==/UserScript== (() => { const configuration_metadata_map = { hotkey: { default_dark: "d", default_light: "d", description: "Hotkey used to toggle the theme on and off\n(Ctrl + Alt + )", datatype: "char" }, colored_text: { default_dark: "#40c99e", default_light: "#032a71", description: "Text color of custom games with friends online in the room, and 'Added by' in picks", datatype: "color" }, primary_text: { default_dark: "#f8fafd", default_light: "#000000", description: "Primary text color", datatype: "color" }, secondary_text: { default_dark: "#bebebe", default_light: "#505050", description: "Secondary text color\nNote: Some of the texts are lightened by 'brightness_lighter' above\nthis will have no affect on such texts (Eg: Chat usernames)", datatype: "color" }, window_color: { default_dark: "#2b6351", default_light: "#009688", description: "Color of window titles\n(Eg: Custom games, Leave Game, Chat, Level Select, ...)", datatype: "color" }, window_text: { default_dark: "#f8fafd", default_light: "#ffffff", description: "Color of text in window titles\n(Eg: Custom games, Leave Game, Chat, Level Select, ...)", datatype: "color" }, page_background: { default_dark: "#111111", default_light: "#1a2733", description: "Used for the main page's background", datatype: "color" }, primary_background: { default_dark: "#222222", default_light: "#e2e2e2", description: "Used as the main background color of windows\n(Eg: Chat background, Leave lobby background, Login page)", datatype: "color" }, secondary_background: { default_dark: "#333333", default_light: "#f3f3f3", description: "Secondary background color for things inside windows\n(Eg: Auto login, Joining room status text, Map Editor inputs, ...)", datatype: "color" }, behind_lobby_background: { default_dark: "#131313", default_light: "#1a2733", description: "Color for the Lobby's background\n(Eg: Behind the Lobby and Map Editor)", datatype: "color" }, red_text: { default_dark: "#e14747", default_light: "#cc3333", description: "Used for player nerf indicator in the lobby", datatype: "color" }, blue_text: { default_dark: "#179be8", default_light: "#0955c7", description: "Used for [Load] when a map is suggested, and Copy link is clicked", datatype: "color" }, green_text: { default_dark: "#17e88b", default_light: "#155824", description: "Used for player buff and in chat friend requests", datatype: "color" }, purple_text: { default_dark: "#8f68e8", default_light: "#6033cc", description: "Used for output of /curate", datatype: "color" }, magenta_text: { default_dark: "#d23cfb", default_light: "#800d6e", description: "Used for when [Load] is clicked, or when you are now the host of the game", datatype: "color" }, button_color: { default_dark: "#4f382f", default_light: "#795548", description: "Button/dropdown color", datatype: "color" }, hover_button_color: { default_dark: "#3a2a24", default_light: "#7f5d51", description: "Button/dropdown hover color", datatype: "color" }, active_button_color: { default_dark: "#362620", default_light: "#4b252b", description: "Button/dropdown click color", datatype: "color" }, disabled_button_color: { default_dark: "#444444", default_light: "#777777", description: "Button/dropdown disabled color", datatype: "color" }, button_text: { default_dark: "#f8fafd", default_light: "#ffffff", description: "Button/dropdown text color", datatype: "color" }, xp_bar_fill: { default_dark: "#473aaf", default_light: "#473aaf", description: "XP bar fill color at the top of your screen whilst in-game", datatype: "color" }, top_bar_opacity: { default_dark: "0.8", default_light: "0.8", description: "Opacity of the 'top bar' that contains your:\nSkin, Username, Level, Volume Settings, ...", datatype: "percentage" }, top_bar_color: { default_dark: "#333333", default_light: "#273749", description: "Color of the 'top bar'\nMake sure this value is dark, the icon colors are white and can't really be easily changed", datatype: "color" }, top_bar_hover_color: { default_dark: "#222222", default_light: "#2d435a", description: "Color of buttons in the 'top bar' then hovered", datatype: "color" }, top_bar_text: { default_dark: "#bebebe", default_light: "#bebebe", description: "Color of text in the 'top bar'", datatype: "color" }, football_background: { default_dark: "#161616", default_light: "#5a7f64", description: "Color for the Football gamemode's background\n(Note: Requires restarting the football to apply)\nWarning: Unstable, experimental", datatype: "color" }, border_color: { default_dark: "#333333", default_light: "#a5acb0", description: "Color used for subtile borders\n(Eg. Settings border, line separating chat and messages in lobby)", datatype: "color" }, mini_menu_color: { default_dark: "#191919", default_light: "#1e2833", description: "Used for Tooltips and 'Filter by' in Custom Games", datatype: "color" }, mini_menu_text: { default_dark: "#bebebe", default_light: "#ffffff", description: "Used for Tooltips and 'Filter by' in Custom Games", datatype: "color" }, scrollbar_background: { default_dark: "#191919", default_light: "#dddddd", description: "Used for the scrollbar's background (Chromium only)", datatype: "color" }, scrollbar_thumb: { default_dark: "#555555", default_light: "#aaaaaa", description: "Used for the scrollbar's thumb - which is the thing you drag (Chromium only)", datatype: "color" }, list_headers: { default_dark: "#2b3839", default_light: "#a8bcc0", description: "Used for the colors of lists\n(Eg: Friends list and the sections in the Map Editor, ...)", datatype: "color" }, table_color: { default_dark: "#444444", default_light: "#c1cdd2", description: "Stripe color for use in tables\n(Eg: Custom games, Friends list, Map Editor, Skin Editor, ...)", datatype: "color" }, table_alt_color: { default_dark: "#333333", default_light: "#d2dbde", description: "Alternative stripe color used in tables", datatype: "color" }, table_hover_color: { default_dark: "#222222", default_light: "#aac5d7", description: "Color used when a row in a table is hovered", datatype: "color" }, table_active_color: { default_dark: "#111111", default_light: "#9cc8d6", description: "Color used when a row in a table is selected", datatype: "color" }, brightness_lighter: { default_dark: "2.5", default_light: "1", description: "Used to lighten text that have bad contrast with dark colors\nNote: Change to 1 if making a light theme\n(0.5 = 50%, 1.5 = 150% brightness)", datatype: "brightness" }, brightness_darker: { default_dark: "0.5", default_light: "1", description: "Used to darken images that that are too bright\nNote: Change to 1 if making a light theme\n(0.5 = 50%, 1.5 = 150% brightness)", datatype: "brightness" }, description_invert: { default_dark: "1", default_light: "0", description: "How much to invert description at the bottom of the page by\n(Value from 0 to 1, Don't use 0.5)", datatype: "percentage" }, }; const datatype_metadata_map = { "color": { value: "value", attributes: { type: "color" } }, "percentage": { value: "value", attributes: { type: "number", max: 1, min: 0, step: 0.01 } }, "brightness": { value: "value", attributes: { type: "number", max: 10, min: 0, step: 0.05 } }, "char": { value: "value", attributes: { type: "text" , maxLength: 1 } }, }; const default_configuration = {}; for (const [key, value] of Object.entries(configuration_metadata_map)) default_configuration[key] = value.default_dark; // If running inside maingameframe, inject our injectors if (unsafeWindow.parent !== unsafeWindow) { unsafeWindow.bonk_theme = { on: true, get_football_background: () => parseInt(getComputedStyle(document.documentElement) .getPropertyValue("--bonk_theme_football_background").trim().slice(1), 16), get_hotkey: () => getComputedStyle(document.documentElement) .getPropertyValue("--bonk_theme_hotkey").trim() || default_configuration.hotkey }; // Injector for football mode, if it doesn't work, ...well football will be bright if (!unsafeWindow.bonkCodeInjectors) unsafeWindow.bonkCodeInjectors = []; unsafeWindow.bonkCodeInjectors.push(bonkCode => { try { // Default football background color, not used elsewhere (yet?) const FOOTBALL_BACKGROUND_COLOR = "0x5a7f64"; let newBonkCode = bonkCode; newBonkCode = newBonkCode .replace( FOOTBALL_BACKGROUND_COLOR, `(window.bonk_theme.on ? window.bonk_theme.get_football_background() : ${FOOTBALL_BACKGROUND_COLOR})` ); if (bonkCode === newBonkCode) throw "[Themes] Injection failed!"; console.log("Themes injector run"); return newBonkCode; } catch (er) { // Silently ignore errors, only football's background will be affected console.log("[Themes] Failed to inject"); console.error(er); return bonkCode; } }); } document.addEventListener("DOMContentLoaded", () => { document.body.classList.add("themed-colors"); // Get the currently applied theme const get_stored_theme = () => { // Get the stored theme, if it doesn't exist, it will be null let stored_theme = localStorage.getItem("bonk_theme"); try { stored_theme = JSON.parse(stored_theme); } catch (er) { // If the stored theme is somehow corrupted - use blank theme console.log("[Themes] Failed to load theme from localstorage"); console.log(`[Themes] ${stored_theme}`); console.error(er); alert("Failed to load theme from localstorage"); stored_theme = {}; } // Merge the stored theme with the default (if null, it will overwrite) return {...default_configuration, ...stored_theme}; }; // Get the stored theme when the page loads const configuration = get_stored_theme(); // Check if the script is running inside maingameframe or not if (unsafeWindow.parent === unsafeWindow) { // Outer bonk.io website // Hotkey to turn the script on and off const maingameframe = document.getElementById("maingameframe"); document.addEventListener("keydown", evt => { if (evt.key?.toLowerCase() === maingameframe.contentWindow.bonk_theme.get_hotkey() && evt.ctrlKey && evt.altKey) { document.body.classList.toggle("themed-colors"); maingameframe.contentDocument.body.classList.toggle("themed-colors"); maingameframe.contentWindow.bonk_theme.on = !maingameframe.contentWindow.bonk_theme.on; } }); // Flag to check whether the theme container already exists let theme_container_created = false; // Create the menu only when someone clicks on the thing in the menu // Show the theme container when you click the menu command thing // eslint-disable-next-line no-undef GM_registerMenuCommand("Edit theme", () => { // Creating a new theme_container, don't make new ones after this if (theme_container_created) return document.getElementById("theme_container").style.display = "block"; theme_container_created = true; // Create the theme menu. This is really ugly but there's like no other way idk. const theme_container = document.createElement("div"); const theme_container_title = document.createElement("p"); const configuration_container = document.createElement("div"); const configuration_container_label = document.createElement("label"); const configuration_list = document.createElement("div"); const configuration_json = document.createElement("textarea"); const configuration_json_label = document.createElement("label"); const theme_cancel = document.createElement("div"); const theme_save = document.createElement("div"); const theme_reset_dark = document.createElement("div"); const theme_reset_light = document.createElement("div"); // Add the IDs so we can style them with CSS theme_container_title.id = "theme_container_title"; theme_container.id = "theme_container"; configuration_container_label.id = "configuration_container_label"; configuration_list.id = "configuration_list"; configuration_json.id = "configuration_json"; theme_cancel.id = "theme_cancel"; theme_save.id = "theme_save"; theme_reset_dark.class = "theme_reset"; theme_reset_light.class = "theme_reset"; // Add the classes theme_container_title.classList.add("classicTopBar"); theme_cancel.classList.add("brownButton", "brownButton_classic", "buttonShadow"); theme_save.classList.add("brownButton", "brownButton_classic", "buttonShadow"); theme_reset_dark.classList.add("brownButton", "brownButton_classic", "buttonShadow"); theme_reset_light.classList.add("brownButton", "brownButton_classic", "buttonShadow"); // Add text to buttons and titles theme_container_title.textContent = "Theme Editor"; theme_cancel.textContent = "CANCEL"; theme_save.textContent = "SAVE"; theme_reset_dark.textContent = "RESET THEME TO DARK DEFAULT"; theme_reset_light.textContent = "RESET THEME TO LIGHT DEFAULT"; configuration_container_label.textContent = "Hover over settings for more info"; configuration_json_label.textContent = "Share, Backup or Import theme data - Copy or paste themes from/into the textbox below"; configuration_json_label.for = "configuration_json"; configuration_json.value = JSON.stringify(configuration); theme_container.appendChild(theme_container_title); configuration_list.appendChild(configuration_container_label); configuration_list.appendChild(document.createElement("br")); // Populate the themes list with the theme that is currently loaded for (const [key, value] of Object.entries(configuration)) { // Check if the setting exists if (!configuration_metadata_map[key]) continue; const datatype_metadata = datatype_metadata_map[configuration_metadata_map[key].datatype]; const configuration_container = document.createElement("div"); const configuration_name = document.createElement("p"); const configuration_value = document.createElement("input"); configuration_container.classList.add("configuration_container"); configuration_name.classList.add("configuration_name"); configuration_value.classList.add("configuration_value"); configuration_container.dataset["configuration_name"] = key; configuration_container.title = configuration_metadata_map[key].description ?? "No description"; configuration_name.textContent = key.replace(/_/g, " "); for (const [key, value] of Object.entries(datatype_metadata.attributes)) configuration_value[key] = value; configuration_value[datatype_metadata.value] = value; // Using "oninput" instead of addEventListener so we can call it somewhere else configuration_value.oninput = () => { if (!datatype_metadata.ignore_change) { // Update the CSS variable on both bonk.io and maingameframe document.documentElement.style .setProperty(`--bonk_theme_${key}`, configuration_value.value); maingameframe.contentDocument.documentElement.style .setProperty(`--bonk_theme_${key}`, configuration_value.value); // Store it in the current configuration (so it can be saved later) configuration[key] = configuration_value[datatype_metadata.value]; } // Update the data at the bottom configuration_json.value = JSON.stringify(configuration); }; // Add the current config to the list of settings configuration_container.appendChild(configuration_name); configuration_container.appendChild(configuration_value); configuration_list.appendChild(configuration_container); } // Add the Save and Cancel buttons configuration_container.appendChild(theme_cancel); configuration_container.appendChild(theme_save); configuration_list.appendChild(document.createElement("br")); // this configuration_list.appendChild(configuration_container); configuration_list.appendChild(document.createElement("br")); // is configuration_list.appendChild(configuration_json_label); configuration_list.appendChild(document.createElement("br")); // so configuration_list.appendChild(configuration_json); configuration_list.appendChild(document.createElement("br")); // so configuration_list.appendChild(theme_reset_dark); configuration_list.appendChild(document.createElement("br")); // bad configuration_list.appendChild(theme_reset_light); theme_container.appendChild(configuration_list); // When save is clicked, try save theme to localstorage theme_save.addEventListener("click", () => { try { localStorage.setItem("bonk_theme", JSON.stringify(configuration)); theme_container.style.display = "none"; } catch (er) { console.log("[Themes] Failed to save theme to localstorage"); console.log(`[Themes] ${JSON.stringify(configuration)}`); console.error(er); alert("Failed to save theme to localstorage"); } }); // Reset the state of configuration from localstorage theme_cancel.addEventListener("click", () => { configuration_json.value = JSON.stringify(get_stored_theme()); configuration_json.oninput(); theme_container.style.display = "none"; }); // Get the currently saved theme, and overwrite everything with it theme_reset_dark.addEventListener("click", () => { configuration_json.value = JSON.stringify(default_configuration); configuration_json.oninput(); }); theme_reset_light.addEventListener("click", () => { // Light theme defaults are not calculated by default, so we need to calculate it here now const default_light_configuration = {}; for (const [key, value] of Object.entries(configuration_metadata_map)) default_light_configuration[key] = value.default_light; configuration_json.value = JSON.stringify(default_light_configuration); configuration_json.oninput(); }); configuration_json.oninput = () => { try { const new_theme = JSON.parse(configuration_json.value); // Update the configurations const configuration_list_children = Array.from(configuration_list.children); for (const configuration_container of configuration_list_children) { const key = configuration_container.dataset["configuration_name"]; const value = new_theme[key]; if (!value) continue; const configuration_value = configuration_container.querySelector("input"); const datatype_meta = datatype_metadata_map[configuration_metadata_map[key].datatype]; configuration_value[datatype_meta.value] = value; configuration_value.oninput(); } // If success, remove the border configuration_json.style.setProperty("border", "none"); } catch (er) { console.log("[Themes] Failed to load theme from