Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 64 additions & 10 deletions js/__tests__/themebox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@
*/

global._ = jest.fn(str => str);

// Mock window.platformColor
window.platformColor = {
header: "#4DA6FF",
selectorSelected: "#1A8CFF"
};

// Mock document elements
document.body.innerHTML = `
<meta name="theme-color" content="#4DA6FF">
<canvas id="canvas"></canvas>
<div id="themeSelectIcon"></div>
<div id="light"><i class="material-icons">brightness_7</i></div>
<div id="dark"><i class="material-icons">brightness_4</i></div>
<div id="palette"><div></div></div>
`;

const ThemeBox = require("../themebox");

describe("ThemeBox", () => {
Expand All @@ -29,7 +46,9 @@ describe("ThemeBox", () => {
storage: {
themePreference: "light"
},
textMsg: jest.fn()
textMsg: jest.fn(),
refreshCanvas: jest.fn(),
palettes: {}
};

jest.spyOn(global.Storage.prototype, "getItem").mockImplementation(key => {
Expand All @@ -42,6 +61,9 @@ describe("ThemeBox", () => {
writable: true
});

// Reset body classes
document.body.classList.remove("light", "dark");

themeBox = new ThemeBox(mockActivity);
});

Expand All @@ -53,7 +75,7 @@ describe("ThemeBox", () => {
expect(themeBox._theme).toBe("light");
});

test("light_onclick() sets theme to light and updates preference", () => {
test("light_onclick() sets theme to light", () => {
themeBox.light_onclick();
expect(themeBox._theme).toBe("light");
expect(localStorage.getItem).toHaveBeenCalledWith("themePreference");
Expand All @@ -62,22 +84,54 @@ describe("ThemeBox", () => {
);
});

test("dark_onclick() sets theme to dark and updates preference", () => {
test("dark_onclick() sets theme to dark and applies instantly", () => {
themeBox.dark_onclick();
expect(themeBox._theme).toBe("dark");
expect(mockActivity.storage.themePreference).toBe("dark");
expect(window.location.reload).toHaveBeenCalled();
// Should NOT reload - instant theme switch
expect(window.location.reload).not.toHaveBeenCalled();
// Should show theme switched message
expect(mockActivity.textMsg).toHaveBeenCalledWith("Theme switched to dark mode.", 2000);
});

test("setPreference() updates theme and reloads if different", () => {
localStorage.getItem.mockReturnValue("dark"); // Correctly mocked now
themeBox.light_onclick();
expect(mockActivity.storage.themePreference).toBe("light");
expect(window.location.reload).toHaveBeenCalled();
test("setPreference() applies theme instantly without reload", () => {
localStorage.getItem.mockReturnValue("light");
themeBox._theme = "dark";
themeBox.setPreference();
expect(mockActivity.storage.themePreference).toBe("dark");
// Should NOT reload - instant theme switch
expect(window.location.reload).not.toHaveBeenCalled();
// Body should have dark class
expect(document.body.classList.contains("dark")).toBe(true);
expect(document.body.classList.contains("light")).toBe(false);
});

test("setPreference() does not reload if theme is unchanged", () => {
test("setPreference() does not change if theme is unchanged", () => {
themeBox.light_onclick();
expect(window.location.reload).not.toHaveBeenCalled();
expect(mockActivity.textMsg).toHaveBeenCalledWith(
"Music Blocks is already set to this theme."
);
});

test("applyThemeInstantly() updates body classes correctly", () => {
themeBox._theme = "dark";
themeBox.applyThemeInstantly();
expect(document.body.classList.contains("dark")).toBe(true);
expect(document.body.classList.contains("light")).toBe(false);
});

test("applyThemeInstantly() updates canvas background for dark mode", () => {
themeBox._theme = "dark";
themeBox.applyThemeInstantly();
const canvas = document.getElementById("canvas");
expect(canvas.style.backgroundColor).toBe("rgb(28, 28, 28)");
});

test("applyThemeInstantly() updates canvas background for light mode", () => {
themeBox._theme = "light";
themeBox.applyThemeInstantly();
const canvas = document.getElementById("canvas");
expect(canvas.style.backgroundColor).toBe("rgb(255, 255, 255)");
});
});
225 changes: 213 additions & 12 deletions js/themebox.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,98 @@
//A dropdown for selecting theme

/*
global _
global _, platformColor
*/

/* exported ThemeBox */
/* exported ThemeBox, themeConfigs */

const themeConfigs = {
dark: {
textColor: "#E2E2E2",
blockText: "#E2E2E2",
dialogueBox: "#1C1C1C",
strokeColor: "#E2E2E2",
fillColor: "#F9F9F9",
blueButton: "#0066FF",
blueButtonHover: "#023a76",
cancelButton: "#f1f1f1",
cancelButtonHover: "#afafaf",
hoverColor: "#808080",
widgetButton: "#225A91",
widgetButtonSelect: "#979797",
widgetBackground: "#454545",
disconnected: "#5C5C5C",
header: "#1E88E5",
aux: "#1976D2",
sub: "#64B5F6",
rule: "#303030",
ruleColor: "#303030",
trashColor: "#757575",
trashBorder: "#424242",
trashActive: "#E53935",
background: "#303030",
paletteSelected: "#1E1E1E",
paletteBackground: "#1C1C1C",
paletteLabelBackground: "#022363",
paletteLabelSelected: "#01143b",
paletteText: "#BDBDBD",
rulerHighlight: "#FFEB3B",
selectorBackground: "#64B5F6",
selectorSelected: "#1E88E5",
labelColor: "#BDBDBD",
rhythmcellcolor: "#303030",
stopIconcolor: "#D50000",
hitAreaGraphicsBeginFill: "#121212",
orange: "#FB8C00"
},
light: {
textColor: "black",
blockText: "#282828",
dialogueBox: "#fff",
strokeColor: "#E2E2E2",
fillColor: "#F9F9F9",
blueButton: "#0066FF",
blueButtonHover: "#023a76",
cancelButton: "#f1f1f1",
cancelButtonHover: "#afafaf",
hoverColor: "#E0E0E0",
widgetBackground: "#ccc",
widgetButton: "#8cc6ff",
widgetButtonSelect: "#C8C8C8",
disconnected: "#C4C4C4",
header: "#4DA6FF",
aux: "#1A8CFF",
sub: "#8CC6FF",
rule: "#E2E2E2",
ruleColor: "#E2E2E2",
trashColor: "#C0C0C0",
trashBorder: "#808080",
trashActive: "#FF0000",
background: "#F9F9F9",
paletteSelected: "#F3F3F3",
paletteBackground: "#FFFFFF",
paletteLabelBackground: "#8CC6FF",
paletteLabelSelected: "#1A8CFF",
paletteText: "#666666",
rulerHighlight: "#FFBF00",
selectorBackground: "#8CC6FF",
selectorSelected: "#1A8CFF",
labelColor: "#a0a0a0",
rhythmcellcolor: "#c8c8c8",
stopIconcolor: "#ea174c",
hitAreaGraphicsBeginFill: "#FFF",
orange: "#e37a00"
}
};

class ThemeBox {
/**
* @constructor
*/
constructor(activity) {
this.activity = activity;
this._theme = activity.storage.themePreference;
this._theme = activity.storage.themePreference || "light";
this._themes = ["light", "dark"];
}

/**
Expand All @@ -44,32 +124,153 @@ class ThemeBox {
this.setPreference();
}

// /**
// * @public
// * @returns {void}
// */
// custom_onclick() {
// this._theme = "custom";
// this.setPreference();
// }
/**
* Apply theme instantly without page reload
* @private
* @returns {void}
*/
applyThemeInstantly() {
const body = document.body;
// Update body classes
this._themes.forEach(theme => {
if (theme === this._theme) {
body.classList.add(theme);
} else {
body.classList.remove(theme);
}
});

// Update platformColor globally
if (themeConfigs[this._theme]) {
Object.assign(window.platformColor, themeConfigs[this._theme]);
}

// Update theme-color meta tag
const themeColorMeta = document.querySelector("meta[name=theme-color]");
if (themeColorMeta && window.platformColor) {
themeColorMeta.content = window.platformColor.header;
}

// Update canvas background using theme config
const canvas = document.getElementById("canvas");
if (canvas) {
canvas.style.backgroundColor = window.platformColor.background;
}

// Update the turtles background color (this redraws the canvas background)
if (this.activity.turtles) {
// Unlock to allow makeBackground to redraw
this.activity.turtles._locked = false;
this.activity.turtles._backgroundColor = window.platformColor.background;
this.activity.turtles.makeBackground();
this.activity.refreshCanvas();
}

// Update toolbar icon for current theme
this.updateThemeIcon();

// Refresh UI components that depend on platformColor
this.refreshUIComponents();

// Notify user
this.activity.textMsg(_("Theme switched to " + this._theme + " mode."), 2000);
}

/**
* Update theme selector icon to reflect current theme
* @private
* @returns {void}
*/
updateThemeIcon() {
const themeSelectIcon = document.getElementById("themeSelectIcon");
if (themeSelectIcon) {
const currentThemeElement = document.getElementById(this._theme);
if (currentThemeElement) {
themeSelectIcon.innerHTML = currentThemeElement.innerHTML;
}
}
}

/**
* Refresh UI components that depend on theme colors
* @private
* @returns {void}
*/
refreshUIComponents() {
// Refresh palette if it exists
if (this.activity.palettes) {
try {
// Update palette selector border color
const paletteElement = document.getElementById("palette");
if (paletteElement && paletteElement.childNodes[0]) {
paletteElement.childNodes[0].style.border = `1px solid ${window.platformColor.selectorSelected}`;
}
} catch (e) {
console.debug("Could not refresh palette:", e);
}
}

// Refresh floating windows
const floatingWindows = document.querySelectorAll("#floatingWindows > .windowFrame");
floatingWindows.forEach(win => {
if (this._theme === "dark") {
win.style.backgroundColor = "#454545";
win.style.borderColor = "#000000";
} else {
win.style.backgroundColor = "";
win.style.borderColor = "";
}
});

// Refresh the activity canvas if available
if (this.activity.refreshCanvas) {
this.activity.refreshCanvas();
}

// Update planet iframe theme if it exists
const planetIframe = document.getElementById("planet-iframe");
if (planetIframe && planetIframe.contentDocument) {
try {
const planetBody = planetIframe.contentDocument.body;
if (planetBody) {
this._themes.forEach(theme => {
if (theme === this._theme) {
planetBody.classList.add(theme);
} else {
planetBody.classList.remove(theme);
}
});
}
} catch (e) {
// Cross-origin restriction may prevent this
console.debug("Could not update planet iframe theme:", e);
}
}
}

/**
* @public
* @returns {void}
*/
reload() {
// Keep for backward compatibility, but prefer instant switching
window.location.reload();
}

setPreference() {
if (localStorage.getItem("themePreference") === this._theme) {
this.activity.textMsg(_("Music Blocks is already set to this theme."));
} else {
// Save preference to localStorage
this.activity.storage.themePreference = this._theme;
this.reload();
localStorage.setItem("themePreference", this._theme);

// Apply theme instantly instead of reloading
this.applyThemeInstantly();
}
}
}

if (typeof module !== "undefined" && module.exports) {
module.exports = ThemeBox;
}
Loading