diff --git a/src/ZenThemesImporter.mjs b/src/ZenThemesImporter.mjs new file mode 100644 index 0000000..b23c1e6 --- /dev/null +++ b/src/ZenThemesImporter.mjs @@ -0,0 +1,133 @@ + +const kZenStylesheetThemeHeader = ` +/* Zen Themes - Generated by ZenThemeImporter. + * DO NOT EDIT THIS FILE DIRECTLY! + * Your changes will be overwritten. + * Instead, go to the preferences and edit the themes there. + */`; +const kenStylesheetFooter = ` +/* End of Zen Themes */ +`; +var gZenStylesheetManager = { + async writeStylesheet(path, themes) { + let content = kZenStylesheetThemeHeader; + for (let theme of themes) { + content += this.getThemeCSS(theme); + } + content += kenStylesheetFooter; + let buffer = new TextEncoder().encode(content); + await IOUtils.write(path, buffer); + }, + + getThemeCSS(theme) { + let css = "\n"; + if (theme._readmeURL) { + css += `/* Name: ${theme.name} */\n`; + css += `/* Description: ${theme.description} */\n`; + css += `/* Author: @${theme.author} */\n`; + css += `/* Readme: ${theme.readme} */\n`; + } + css += `@import url("${theme._chromeURL}");\n`; + return css; + } +}; + +var gZenThemeImporter = new class { + constructor() { + console.info("ZenThemeImporter: Initiating Zen theme importer"); + try { + this.insertStylesheet(); + console.info("ZenThemeImporter: Zen theme imported"); + } catch (e) { + console.error("ZenThemeImporter: Error importing Zen theme: ", e); + } + Services.prefs.addObserver("zen.themes.updated-value-observer", this.updateStylesheet.bind(this), false); + } + + get sss() { + if (!this._sss) { + this._sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService); + } + return this._sss; + } + + get styleSheetPath() { + return PathUtils.join( + PathUtils.profileDir, + "chrome", + "zen-themes.css" + ); + } + + get themesRootPath() { + return PathUtils.join( + PathUtils.profileDir, + "chrome", + "zen-themes" + ); + } + + get themesDataFile() { + return PathUtils.join( + PathUtils.profileDir, + "zen-themes.json" + ); + } + + getThemeFolder(theme) { + return PathUtils.join(this.themesRootPath, theme.id); + } + + async getThemes() { + if (!this._themes) { + if (!(await IOUtils.exists(this.themesDataFile))) { + await IOUtils.writeJSON(this.themesDataFile, {}); + } + this._themes = await IOUtils.readJSON(this.themesDataFile); + } + return this._themes; + } + + rebuildThemeStylesheet() { + this._themes = null; + this.updateStylesheet(); + } + + get styleSheetURI() { + if (!this._styleSheetURI) { + this._styleSheetURI = Services.io.newFileURI(new FileUtils.File(this.styleSheetPath)); + } + return this._styleSheetURI; + } + + getStylesheetURIForTheme(theme) { + return Services.io.newFileURI(new FileUtils.File(PathUtils.join(this.getThemeFolder(theme), "chrome.css"))); + } + + insertStylesheet() { + if (IOUtils.exists(this.styleSheetPath)) { + this.sss.loadAndRegisterSheet(this.styleSheetURI, this.sss.USER_SHEET); + } + } + + removeStylesheet() { + this.sss.unregisterSheet(this.styleSheetURI, this.sss.USER_SHEET); + } + + async updateStylesheet() { + this.removeStylesheet(); + await this.writeStylesheet(); + this.insertStylesheet(); + } + + async writeStylesheet() { + const themes = [] + this._themes = null; + for (let theme of Object.values(await this.getThemes())) { + theme._chromeURL = this.getStylesheetURIForTheme(theme).spec; + console.info("ZenThemeImporter: Writing theme: ", theme._chromeURL); + themes.push(theme); + } + gZenStylesheetManager.writeStylesheet(this.styleSheetPath, themes); + } +}; diff --git a/src/actors/ZenThemeMarketplaceChild.sys.mjs b/src/actors/ZenThemeMarketplaceChild.sys.mjs index edb96ad..f4c48f5 100644 --- a/src/actors/ZenThemeMarketplaceChild.sys.mjs +++ b/src/actors/ZenThemeMarketplaceChild.sys.mjs @@ -19,17 +19,59 @@ export class ZenThemeMarketplaceChild extends JSWindowActorChild { }, 0); } + get actionButton() { + return this.contentWindow.document.getElementById("install-theme"); + } + + get actionButtonUnnstall() { + return this.contentWindow.document.getElementById("install-theme-uninstall"); + } + + async receiveMessage(message) { + switch (message.name) { + case "ZenThemeMarketplace:ThemeChanged": { + const themeId = message.data.themeId; + const actionButton = this.actionButton; + const actionButtonInstalled = this.actionButtonUnnstall; + if (actionButton && actionButtonInstalled) { + actionButton.disabled = false; + actionButtonInstalled.disabled = false; + if (await this.isThemeInstalled(themeId)) { + actionButton.classList.add("hidden"); + actionButtonInstalled.classList.remove("hidden"); + } else { + actionButton.classList.remove("hidden"); + actionButtonInstalled.classList.add("hidden"); + } + } + break; + } + } + } + async addIntallButtons() { - - const actionButton = this.contentWindow.document.getElementById("install-theme"); + const actionButton = this.actionButton; + const actionButtonUnnstall = this.actionButtonUnnstall; const errorMessage = this.contentWindow.document.getElementById("install-theme-error"); - if (actionButton) { - console.info("ZenThemeMarketplaceChild: Initiating theme marketplace"); + if (!actionButton || !actionButtonUnnstall) { + return; } errorMessage.classList.add("hidden"); - actionButton.classList.remove("hidden"); + + const themeId = actionButton.getAttribute("zen-theme-id"); + if (await this.isThemeInstalled(themeId)) { + actionButtonUnnstall.classList.remove("hidden"); + } else { + actionButton.classList.remove("hidden"); + } + actionButton.addEventListener("click", this.installTheme.bind(this)); + actionButtonUnnstall.addEventListener("click", this.uninstallTheme.bind(this)); + } + + async isThemeInstalled(themeId) { + return await this.sendQuery("ZenThemeMarketplace:IsThemeInstalled", { themeId }); } addTheme(theme) { @@ -54,8 +96,17 @@ export class ZenThemeMarketplaceChild extends JSWindowActorChild { return null; } + async uninstallTheme(event) { + const button = event.target; + button.disabled = true; + const themeId = button.getAttribute("zen-theme-id"); + console.info("ZTM: Uninstalling theme with id: ", themeId); + this.sendAsyncMessage("ZenThemeMarketplace:UninstallTheme", { themeId }); + } + async installTheme(event) { const button = event.target; + button.disabled = true; const themeId = button.getAttribute("zen-theme-id"); console.info("ZTM: Installing theme with id: ", themeId); diff --git a/src/actors/ZenThemeMarketplaceParent.sys.mjs b/src/actors/ZenThemeMarketplaceParent.sys.mjs index d9421bd..1958c0b 100644 --- a/src/actors/ZenThemeMarketplaceParent.sys.mjs +++ b/src/actors/ZenThemeMarketplaceParent.sys.mjs @@ -12,11 +12,31 @@ export class ZenThemeMarketplaceParent extends JSWindowActorParent { const themes = await this.getThemes(); themes[theme.id] = theme; this.updateThemes(themes); + this.updateChildProcesses(theme.id); break; } + case "ZenThemeMarketplace:UninstallTheme": { + console.info("ZenThemeMarketplaceParent: Uninstalling theme"); + const themeId = message.data.themeId; + const themes = await this.getThemes(); + delete themes[themeId]; + this.removeTheme(themeId); + this.updateThemes(themes); + this.updateChildProcesses(themeId); + break; + } + case "ZenThemeMarketplace:IsThemeInstalled": { + const themeId = message.data.themeId; + const themes = await this.getThemes(); + return themes[themeId] ? true : false; + } } } + async updateChildProcesses(themeId) { + this.sendAsyncMessage("ZenThemeMarketplace:ThemeChanged", { themeId }); + } + async getThemes() { if (!this._themes) { if (!(await IOUtils.exists(this.themesDataFile))) { @@ -68,14 +88,19 @@ export class ZenThemeMarketplaceParent extends JSWindowActorParent { ); } + triggerThemeUpdate() { + const pref = "zen.themes.updated-value-observer"; + Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref)); + } + async installTheme(theme) { await this.downloadThemeFileContents(theme); - this.sendAsyncMessage("ZenThemeMarketplace:ThemeInstalled", { theme }); } async checkForThemeChanges() { const themes = await this.getThemes(); const themeIds = Object.keys(themes); + let changed = false; for (const themeId of themeIds) { const theme = themes[themeId]; if (!theme) { @@ -83,9 +108,19 @@ export class ZenThemeMarketplaceParent extends JSWindowActorParent { } const themePath = PathUtils.join(this.themesRootPath, themeId); if (!(await IOUtils.exists(themePath))) { - console.info("ZenThemeMarketplaceParent: Installing theme ", themeId); - this.installTheme(theme); + await this.installTheme(theme); + changed = true; } } + if (changed) { + this.triggerThemeUpdate(); + } + } + + async removeTheme(themeId) { + const themePath = PathUtils.join(this.themesRootPath, themeId); + console.info("ZenThemeMarketplaceParent: Removing theme ", themePath); + await IOUtils.remove(themePath, { recursive: true, ignoreAbsent: true }); + this.triggerThemeUpdate(); } }; \ No newline at end of file