refactor(mods): rework ZenMods module (#8618)

Co-authored-by: mr. m <mr.m@tuta.com>
Co-authored-by: mr. m <91018726+mauro-balades@users.noreply.github.com>
This commit is contained in:
Bryan Galdámez 2025-05-27 04:02:59 -06:00 committed by GitHub
parent 316ff45859
commit 797152da89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1129 additions and 1107 deletions

2
l10n

@ -1 +1 @@
Subproject commit 644474b8c92e306288d835698eb6714081a650d8
Subproject commit 9b1df3a65db379b357ac864f84ad08313b3d4c94

View file

@ -22,13 +22,17 @@ pref('zen.mediacontrols.enabled', true);
// Exposure:
pref('zen.haptic-feedback.enabled', true);
pref('zen.mods.auto-update-days', 12); // In days
#ifdef MOZILLA_OFFICIAL
pref('zen.mods.auto-update', true);
pref('zen.rice.api.url', 'https://share.zen-browser.app', locked);
pref('zen.injections.match-urls', 'https://zen-browser.app/*,https://share.zen-browser.app/*', locked);
#else
pref('zen.mods.auto-update', false);
pref('zen.rice.api.url', "http://localhost", locked);
pref('zen.injections.match-urls', 'http://localhost/*', locked);
#endif
pref('zen.rice.share.notice.accepted', false);
#ifdef XP_MACOSX

View file

@ -31,8 +31,7 @@
# Scripts used all over the browser
<script src="chrome://browser/content/ZenUIManager.mjs"></script>
<script src="chrome://browser/content/zen-components/ZenFolders.mjs"></script>
<script src="chrome://browser/content/zen-components/ZenThemesCommon.mjs"></script>
<script src="chrome://browser/content/zen-components/ZenThemesImporter.mjs"></script>
<script src="chrome://browser/content/zen-components/ZenMods.mjs"></script>
<script src="chrome://browser/content/zen-components/ZenCompactMode.mjs"></script>
<script src="chrome://browser/content/zen-components/ZenPinnedTabsStorage.mjs"></script>
<script src="chrome://browser/content/zen-components/ZenWorkspacesStorage.mjs"></script>

View file

@ -35,10 +35,7 @@
content/browser/zen-components/ZenViewSplitter.mjs (../../zen/split-view/ZenViewSplitter.mjs)
content/browser/zen-styles/zen-decks.css (../../zen/split-view/zen-decks.css)
content/browser/zen-components/ZenThemesCommon.mjs (../../zen/mods/ZenThemesCommon.mjs)
content/browser/zen-components/ZenThemesImporter.mjs (../../zen/mods/ZenThemesImporter.mjs)
content/browser/zen-components/actors/ZenThemeMarketplaceParent.sys.mjs (../../zen/mods/actors/ZenThemeMarketplaceParent.sys.mjs)
content/browser/zen-components/actors/ZenThemeMarketplaceChild.sys.mjs (../../zen/mods/actors/ZenThemeMarketplaceChild.sys.mjs)
content/browser/zen-components/ZenMods.mjs (../../zen/mods/ZenMods.mjs)
content/browser/zen-components/ZenWorkspaceIcons.mjs (../../zen/workspaces/ZenWorkspaceIcons.mjs)
content/browser/zen-components/ZenWorkspace.mjs (../../zen/workspaces/ZenWorkspace.mjs)
@ -58,8 +55,6 @@
content/browser/zen-components/ZenGlanceManager.mjs (../../zen/glance/ZenGlanceManager.mjs)
content/browser/zen-styles/zen-glance.css (../../zen/glance/zen-glance.css)
content/browser/zen-components/actors/ZenGlanceChild.sys.mjs (../../zen/glance/actors/ZenGlanceChild.sys.mjs)
content/browser/zen-components/actors/ZenGlanceParent.sys.mjs (../../zen/glance/actors/ZenGlanceParent.sys.mjs)
content/browser/zen-components/ZenFolders.mjs (../../zen/folders/ZenFolders.mjs)
content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css)

View file

@ -14,30 +14,37 @@ var gZenMarketplaceManager = {
return;
}
if (!window.gZenMods) {
window.gZenMods = ZenMultiWindowFeature.currentBrowser.gZenMods;
}
header.appendChild(this._initDisableAll());
this._initImportExport();
this.__hasInitializedEvents = true;
await this._buildThemesList();
await this._buildModsList();
Services.prefs.addObserver(this.updatePref, this);
Services.prefs.addObserver(gZenMods.updatePref, this);
const checkForUpdateClick = (event) => {
if (event.target === checkForUpdates) {
event.preventDefault();
this._checkForThemeUpdates(event);
}
};
checkForUpdates.addEventListener('click', checkForUpdateClick);
document.addEventListener('ZenThemeMarketplace:CheckForUpdatesFinished', (event) => {
document.addEventListener('ZenModsMarketplace:CheckForUpdatesFinished', (event) => {
checkForUpdates.disabled = false;
const updates = event.detail.updates;
const success = document.getElementById('zenThemeMarketplaceUpdatesSuccess');
const error = document.getElementById('zenThemeMarketplaceUpdatesFailure');
if (updates) {
success.hidden = false;
error.hidden = true;
@ -48,13 +55,16 @@ var gZenMarketplaceManager = {
});
window.addEventListener('unload', () => {
Services.prefs.removeObserver(this.updatePref, this);
Services.prefs.removeObserver(gZenMods.updatePref, this);
this.__hasInitializedEvents = false;
document.removeEventListener('ZenThemeMarketplace:CheckForUpdatesFinished', this);
document.removeEventListener('ZenCheckForThemeUpdates', this);
document.removeEventListener('ZenModsMarketplace:CheckForUpdatesFinished', this);
document.removeEventListener('ZenCheckForModUpdates', this);
checkForUpdates.removeEventListener('click', checkForUpdateClick);
this.themesList.innerHTML = '';
this._doNotRebuildThemesList = false;
this.modsList.innerHTML = '';
this._doNotRebuildModsList = false;
});
},
@ -63,36 +73,32 @@ var gZenMarketplaceManager = {
const exportButton = document.getElementById('zenThemeMarketplaceExport');
if (importButton) {
importButton.addEventListener('click', async () => {
await this._importThemes();
});
importButton.addEventListener('click', this._importThemes.bind(this));
}
if (exportButton) {
exportButton.addEventListener('click', async () => {
await this._exportThemes();
});
exportButton.addEventListener('click', this._exportThemes.bind(this));
}
},
_initDisableAll() {
const areThemesDisabled = Services.prefs.getBoolPref('zen.themes.disable-all', false);
const browser = ZenThemesCommon.currentBrowser;
const areModsDisabled = Services.prefs.getBoolPref('zen.themes.disable-all', false);
const browser = ZenMultiWindowFeature.currentBrowser;
const mozToggle = document.createElement('moz-toggle');
mozToggle.className =
'zenThemeMarketplaceItemPreferenceToggle zenThemeMarketplaceDisableAllToggle';
mozToggle.pressed = !areThemesDisabled;
mozToggle.pressed = !areModsDisabled;
browser.document.l10n.setAttributes(
mozToggle,
`zen-theme-disable-all-${!areThemesDisabled ? 'enabled' : 'disabled'}`
`zen-theme-disable-all-${!areModsDisabled ? 'enabled' : 'disabled'}`
);
mozToggle.addEventListener('toggle', async (event) => {
const { pressed = false } = event.target || {};
this.themesList.style.display = pressed ? '' : 'none';
this.modsList.style.display = pressed ? '' : 'none';
Services.prefs.setBoolPref('zen.themes.disable-all', !pressed);
browser.document.l10n.setAttributes(
mozToggle,
@ -100,90 +106,65 @@ var gZenMarketplaceManager = {
);
});
if (areThemesDisabled) {
this.themesList.style.display = 'none';
if (areModsDisabled) {
this.modsList.style.display = 'none';
}
return mozToggle;
},
async observe() {
await this._buildThemesList();
await this._buildModsList();
},
_checkForThemeUpdates(event) {
// Send a message to the child to check for theme updates.
event.target.disabled = true;
// send an event that will be listened by the child process.
document.dispatchEvent(new CustomEvent('ZenCheckForThemeUpdates'));
document.dispatchEvent(new CustomEvent('ZenCheckForModUpdates'));
},
get updatePref() {
return 'zen.themes.updated-value-observer';
},
triggerThemeUpdate() {
Services.prefs.setBoolPref(this.updatePref, !Services.prefs.getBoolPref(this.updatePref));
},
get themesList() {
if (!this._themesList) {
this._themesList = document.getElementById('zenThemeMarketplaceList');
get modsList() {
if (!this._modsList) {
this._modsList = document.getElementById('zenThemeMarketplaceList');
}
return this._themesList;
return this._modsList;
},
async removeTheme(themeId) {
const themePath = ZenThemesCommon.getThemeFolder(themeId);
async removeMod(modId) {
await gZenMods.removeMod(modId);
console.info(`[ZenThemeMarketplaceParent:settings]: Removing theme ${themePath}`);
await IOUtils.remove(themePath, { recursive: true, ignoreAbsent: true });
const themes = await ZenThemesCommon.getThemes();
delete themes[themeId];
await IOUtils.writeJSON(ZenThemesCommon.themesDataFile, themes);
this.triggerThemeUpdate();
gZenMods.triggerModsUpdate();
},
async disableTheme(themeId) {
const themes = await ZenThemesCommon.getThemes();
const theme = themes[themeId];
async disableMod(modId) {
await gZenMods.disableMod(modId);
console.log(`[ZenThemeMarketplaceParent:settings]: Disabling theme ${theme.name}`);
theme.enabled = false;
await IOUtils.writeJSON(ZenThemesCommon.themesDataFile, themes);
this._doNotRebuildThemesList = true;
this.triggerThemeUpdate();
this._doNotRebuildModsList = true;
gZenMods.triggerModsUpdate();
},
async enableTheme(themeId) {
const themes = await ZenThemesCommon.getThemes();
const theme = themes[themeId];
async enableMod(modId) {
await gZenMods.enableMod(modId);
console.log(`[ZenThemeMarketplaceParent:settings]: Enabling theme ${theme.name}`);
theme.enabled = true;
await IOUtils.writeJSON(ZenThemesCommon.themesDataFile, themes);
this._doNotRebuildThemesList = true;
this.triggerThemeUpdate();
this._doNotRebuildModsList = true;
gZenMods.triggerModsUpdate();
},
_triggerBuildUpdateWithoutRebuild() {
this._doNotRebuildThemesList = true;
this.triggerThemeUpdate();
this._doNotRebuildModsList = true;
gZenMods.triggerModsUpdate();
},
async _importThemes() {
const errorBox = document.getElementById('zenThemeMarketplaceImportFailure');
const successBox = document.getElementById('zenThemeMarketplaceImportSuccess');
successBox.hidden = true;
errorBox.hidden = true;
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.style.display = 'none';
@ -191,37 +172,52 @@ var gZenMarketplaceManager = {
input.setAttribute('accept', '.json');
let timeout;
const filePromise = new Promise((resolve) => {
input.addEventListener('change', (event) => {
if (timeout) clearTimeout(timeout);
if (timeout) {
clearTimeout(timeout);
}
const file = event.target.files[0];
resolve(file);
});
timeout = setTimeout(() => {
console.warn('[ZenThemeMarketplaceParent:settings]: Import timeout reached, aborting.');
console.warn('[ZenSettings:ZenMods]: Import timeout reached, aborting.');
resolve(null);
}, 60000);
});
input.addEventListener('cancel', () => {
console.warn('[ZenSettings:ZenMods]: Import cancelled by user.');
clearTimeout(timeout);
});
input.click();
try {
const file = await filePromise;
if (!file) {
return;
}
const content = await file.text();
const themes = JSON.parse(content);
for (const theme of Object.values(themes)) {
theme.themeId = theme.id;
window.ZenInstallTheme(theme);
const mods = JSON.parse(content);
for (const mod of Object.values(mods)) {
mod.modId = mod.id;
window.ZenInstallMod(mod);
}
} catch (error) {
console.error('[ZenThemeMarketplaceParent:settings]: Error while importing themes:', error);
console.error('[ZenSettings:ZenMods]: Error while importing mods:', error);
errorBox.hidden = false;
} finally {
if (input) input.remove();
}
if (input) {
input.remove();
}
},
@ -232,51 +228,54 @@ var gZenMarketplaceManager = {
successBox.hidden = true;
errorBox.hidden = true;
let a, url;
let temporalAnchor, temporalUrl;
try {
const themes = await ZenThemesCommon.getThemes();
const themesJson = JSON.stringify(themes, null, 2);
const blob = new Blob([themesJson], { type: 'application/json' });
url = URL.createObjectURL(blob);
// Creating a link to download the JSON file
a = document.createElement('a');
a.href = url;
a.download = 'zen-themes-export.json';
const mods = await gZenMods.getMods();
const modsJson = JSON.stringify(mods, null, 2);
const blob = new Blob([modsJson], { type: 'application/json' });
temporalUrl = URL.createObjectURL(blob);
// Creating a link to download the JSON file
temporalAnchor = document.createElement('a');
temporalAnchor.href = temporalUrl;
temporalAnchor.download = 'zen-mods-export.json';
document.body.appendChild(temporalAnchor);
temporalAnchor.click();
temporalAnchor.remove();
document.body.appendChild(a);
a.click();
a.remove();
successBox.hidden = false;
} catch (error) {
console.error('[ZenThemeMarketplaceParent:settings]: Error while exporting themes:', error);
console.error('[ZenSettings:ZenMods]: Error while exporting mods:', error);
errorBox.hidden = false;
} finally {
if (a) {
a.remove();
}
if (url) {
URL.revokeObjectURL(url);
if (temporalAnchor) {
temporalAnchor.remove();
}
if (temporalUrl) {
URL.revokeObjectURL(temporalUrl);
}
},
async _buildThemesList() {
if (!this.themesList) {
async _buildModsList() {
if (!this.modsList) {
return;
}
if (this._doNotRebuildThemesList) {
this._doNotRebuildThemesList = false;
if (this._doNotRebuildModsList) {
this._doNotRebuildModsList = false;
return;
}
const themes = await ZenThemesCommon.getThemes();
const mods = await gZenMods.getMods();
const browser = ZenMultiWindowFeature.currentBrowser;
const themeList = document.createElement('div');
const modList = document.createElement('div');
for (const theme of Object.values(themes).sort((a, b) => a.name.localeCompare(b.name))) {
const sanitizedName = `theme-${theme.name?.replaceAll(/\s/g, '-')?.replaceAll(/[^A-Za-z_-]+/g, '')}`;
const isThemeEnabled = theme.enabled === undefined || theme.enabled;
for (const mod of Object.values(mods).sort((a, b) => a.name.localeCompare(b.name))) {
const sanitizedName = gZenMods.sanitizeModName(mod.name);
const isModEnabled = mod.enabled === undefined || mod.enabled;
const fragment = window.MozXULElement.parseXULToFragment(`
<vbox class="zenThemeMarketplaceItem">
<vbox class="zenThemeMarketplaceItemContent">
@ -286,14 +285,14 @@ var gZenMarketplaceManager = {
<description class="description-deemphasized zenThemeMarketplaceItemDescription"></description>
</vbox>
<hbox class="zenThemeMarketplaceItemActions">
${theme.preferences ? `<button id="zenThemeMarketplaceItemConfigureButton-${sanitizedName}" class="zenThemeMarketplaceItemConfigureButton" hidden="true"></button>` : ''}
${theme.homepage ? `<button id="zenThemeMarketplaceItemHomePageLink-${sanitizedName}" class="zenThemeMarketplaceItemHomepageButton" zen-theme-id="${theme.id}"></button>` : ''}
<button class="zenThemeMarketplaceItemUninstallButton" data-l10n-id="zen-theme-marketplace-remove-button" zen-theme-id="${theme.id}"></button>
${mod.preferences ? `<button id="zenThemeMarketplaceItemConfigureButton-${sanitizedName}" class="zenThemeMarketplaceItemConfigureButton" hidden="true"></button>` : ''}
${mod.homepage ? `<button id="zenThemeMarketplaceItemHomePageLink-${sanitizedName}" class="zenThemeMarketplaceItemHomepageButton" zen-mod-id="${mod.id}"></button>` : ''}
<button class="zenThemeMarketplaceItemUninstallButton" data-l10n-id="zen-theme-marketplace-remove-button" zen-mod-id="${mod.id}"></button>
</hbox>
</vbox>
`);
const themeName = `${theme.name} (v${theme.version || '1.0.0'})`;
const modName = `${mod.name} (v${mod.version ?? '1.0.0'})`;
const base = fragment.querySelector('.zenThemeMarketplaceItem');
const baseHeader = fragment.querySelector('#zenThemeMarketplaceItemContentHeader');
@ -308,7 +307,7 @@ var gZenMarketplaceManager = {
mainDialogDiv.className = 'zenThemeMarketplaceItemPreferenceDialog';
headerDiv.className = 'zenThemeMarketplaceItemPreferenceDialogTopBar';
headerTitle.textContent = themeName;
headerTitle.textContent = modName;
browser.document.l10n.setAttributes(headerTitle, 'zen-theme-marketplace-theme-header-title', {
name: sanitizedName,
});
@ -319,10 +318,10 @@ var gZenMarketplaceManager = {
contentDiv.className = 'zenThemeMarketplaceItemPreferenceDialogContent';
mozToggle.className = 'zenThemeMarketplaceItemPreferenceToggle';
mozToggle.pressed = isThemeEnabled;
mozToggle.pressed = isModEnabled;
browser.document.l10n.setAttributes(
mozToggle,
`zen-theme-marketplace-toggle-${isThemeEnabled ? 'enabled' : 'disabled'}-button`
`zen-theme-marketplace-toggle-${isModEnabled ? 'enabled' : 'disabled'}-button`
);
baseHeader.appendChild(mozToggle);
@ -340,34 +339,34 @@ var gZenMarketplaceManager = {
});
mozToggle.addEventListener('toggle', async (event) => {
const themeId = event.target
const modId = event.target
.closest('.zenThemeMarketplaceItem')
.querySelector('.zenThemeMarketplaceItemUninstallButton')
.getAttribute('zen-theme-id');
.getAttribute('zen-mod-id');
event.target.setAttribute('disabled', true);
if (!event.target.hasAttribute('pressed')) {
await this.disableTheme(themeId);
await this.disableMod(modId);
browser.document.l10n.setAttributes(
mozToggle,
'zen-theme-marketplace-toggle-disabled-button'
);
if (theme.preferences) {
if (mod.preferences) {
document
.getElementById(`zenThemeMarketplaceItemConfigureButton-${sanitizedName}`)
.setAttribute('hidden', true);
}
} else {
await this.enableTheme(themeId);
await this.enableMod(modId);
browser.document.l10n.setAttributes(
mozToggle,
'zen-theme-marketplace-toggle-enabled-button'
);
if (theme.preferences) {
if (mod.preferences) {
document
.getElementById(`zenThemeMarketplaceItemConfigureButton-${sanitizedName}`)
.removeAttribute('hidden');
@ -379,8 +378,8 @@ var gZenMarketplaceManager = {
}, 400);
});
fragment.querySelector('.zenThemeMarketplaceItemTitle').textContent = themeName;
fragment.querySelector('.zenThemeMarketplaceItemDescription').textContent = theme.description;
fragment.querySelector('.zenThemeMarketplaceItemTitle').textContent = modName;
fragment.querySelector('.zenThemeMarketplaceItemDescription').textContent = mod.description;
fragment
.querySelector('.zenThemeMarketplaceItemUninstallButton')
.addEventListener('click', async (event) => {
@ -392,34 +391,34 @@ var gZenMarketplaceManager = {
return;
}
await this.removeTheme(event.target.getAttribute('zen-theme-id'));
await this.removeMod(event.target.getAttribute('zen-mod-id'));
});
if (theme.homepage) {
if (mod.homepage) {
const homepageButton = fragment.querySelector('.zenThemeMarketplaceItemHomepageButton');
homepageButton.addEventListener('click', () => {
// open the homepage url in a new tab
const url = theme.homepage;
const url = mod.homepage;
window.open(url, '_blank');
});
}
if (theme.preferences) {
if (mod.preferences) {
fragment
.querySelector('.zenThemeMarketplaceItemConfigureButton')
.addEventListener('click', () => {
dialog.showModal();
});
if (isThemeEnabled) {
if (isModEnabled) {
fragment
.querySelector('.zenThemeMarketplaceItemConfigureButton')
.removeAttribute('hidden');
}
}
const preferences = await ZenThemesCommon.getThemePreferences(theme);
const preferences = await gZenMods.getModPreferences(mod);
if (preferences.length > 0) {
const preferencesWrapper = document.createXULElement('vbox');
@ -471,7 +470,7 @@ var gZenMarketplaceManager = {
if (!['string', 'number'].includes(valueType)) {
console.log(
`[ZenThemeMarketplaceParent:settings]: Warning, invalid data type received (${valueType}), skipping.`
`[ZenSettings:ZenMods]: Warning, invalid data type received (${valueType}), skipping.`
);
continue;
}
@ -583,7 +582,7 @@ var gZenMarketplaceManager = {
input.addEventListener(
'change',
ZenThemesCommon.debounce((event) => {
gZenMods.debounce((event) => {
const value = event.target.value;
Services.prefs.setStringPref(property, value);
@ -617,18 +616,18 @@ var gZenMarketplaceManager = {
default:
console.log(
`[ZenThemeMarketplaceParent:settings]: Warning, unknown preference type received (${type}), skipping.`
`[ZenSettings:ZenMods]: Warning, unknown preference type received (${type}), skipping.`
);
continue;
}
}
contentDiv.appendChild(preferencesWrapper);
}
themeList.appendChild(fragment);
modList.appendChild(fragment);
}
this.themesList.replaceChildren(...themeList.children);
themeList.remove();
this.modsList.replaceChildren(...modList.children);
modList.remove();
},
};
@ -636,6 +635,19 @@ const kZenExtendedSidebar = 'zen.view.sidebar-expanded';
const kZenSingleToolbar = 'zen.view.use-single-toolbar';
var gZenLooksAndFeel = {
kZenColors: [
'#aac7ff',
'#74d7cb',
'#a0d490',
'#dec663',
'#ffb787',
'#dec1b1',
'#ffb1c0',
'#ddbfc3',
'#f6b0ea',
'#d4bbff',
],
init() {
if (this.__hasInitialized) return;
this.__hasInitialized = true;
@ -737,7 +749,8 @@ var gZenLooksAndFeel = {
_initializeColorPicker(accentColor) {
let elem = document.getElementById('zenLooksAndFeelColorOptions');
elem.innerHTML = '';
for (let color of ZenThemesCommon.kZenColors) {
for (let color of this.kZenColors) {
let colorElemParen = document.createElement('div');
let colorElem = document.createElement('div');
colorElemParen.classList.add('zenLooksAndFeelColorOptionParen');
@ -761,7 +774,7 @@ var gZenLooksAndFeel = {
},
_getInitialAccentColor() {
return Services.prefs.getStringPref('zen.theme.accent-color', ZenThemesCommon.kZenColors[0]);
return Services.prefs.getStringPref('zen.theme.accent-color', this.kZenColors[0]);
},
};
@ -1224,4 +1237,9 @@ Preferences.addAll([
type: 'bool',
default: true,
},
{
id: 'zen.mods.auto-update',
type: 'bool',
default: true,
},
]);

View file

@ -1,5 +1,4 @@
<script src="chrome://browser/content/zen-components/ZenCommonUtils.mjs" defer=""/>
<script src="chrome://browser/content/zen-components/ZenThemesCommon.mjs" defer=""/>
<html:template id="template-paneZenMarketplace">
<hbox id="ZenMarketplaceCategory"
class="subcategory"
@ -21,6 +20,10 @@
<button id="zenThemeMarketplaceCheckForUpdates" data-l10n-id="zen-theme-marketplace-check-for-updates-button" />
</hbox>
<checkbox id="zenWorkspacesForceContainerTabsToWorkspace"
data-l10n-id="zen-themes-auto-update"
preference="zen.mods.auto-update"/>
<description class="description-deemphasized" data-l10n-id="zen-theme-marketplace-updates-success" hidden="true" id="zenThemeMarketplaceUpdatesSuccess" />
<description class="description-deemphasized" data-l10n-id="zen-theme-marketplace-updates-failure" hidden="true" id="zenThemeMarketplaceUpdatesFailure" />

View file

@ -5,16 +5,28 @@
var gZenActorsManager = {
_actors: new Set(),
_lazy: {},
addJSWindowActor(...args) {
if (this._actors.has(args[0])) {
init() {
ChromeUtils.defineESModuleGetters(this._lazy, {
ActorManagerParent: 'resource://gre/modules/ActorManagerParent.sys.mjs',
});
},
addJSWindowActor(name, data) {
if (!this._lazy.ActorManagerParent) {
this.init();
}
if (this._actors.has(name)) {
// Actor already registered, nothing to do
return;
}
const decl = {};
decl[name] = data;
try {
ChromeUtils.registerWindowActor(...args);
this._actors.add(args[0]);
this._lazy.ActorManagerParent.addJSWindowActors(decl);
this._actors.add(name);
} catch (e) {
console.warn(`Failed to register JSWindowActor: ${e}`);
}

View file

@ -742,13 +742,12 @@
window.gZenGlanceManager = new ZenGlanceManager();
function registerWindowActors() {
if (Services.prefs.getBoolPref('zen.glance.enabled', true)) {
gZenActorsManager.addJSWindowActor('ZenGlance', {
parent: {
esModuleURI: 'chrome://browser/content/zen-components/actors/ZenGlanceParent.sys.mjs',
esModuleURI: 'resource:///actors/ZenGlanceParent.sys.mjs',
},
child: {
esModuleURI: 'chrome://browser/content/zen-components/actors/ZenGlanceChild.sys.mjs',
esModuleURI: 'resource:///actors/ZenGlanceChild.sys.mjs',
events: {
DOMContentLoaded: {},
keydown: {
@ -758,9 +757,9 @@
},
allFrames: true,
matches: ['*://*/*'],
enablePreference: 'zen.glance.enabled',
});
}
}
registerWindowActors();
}

722
src/zen/mods/ZenMods.mjs Normal file
View file

@ -0,0 +1,722 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
class ZenMods extends ZenPreloadedFeature {
// private properties start
#kZenStylesheetModHeader = '/* Zen Mods - Generated by ZenMods.';
#kZenStylesheetModHeaderBody = `* DO NOT EDIT THIS FILE DIRECTLY!
* Your changes will be overwritten.
* Instead, go to the preferences and edit the mods there.
*/
`;
#kZenStylesheetModFooter = `
/* End of Zen Mods */
`;
#getCurrentDateTime = () =>
new Intl.DateTimeFormat('en-US', {
dateStyle: 'full',
timeStyle: 'full',
}).format(new Date().getTime());
constructor() {
console.log('[ZenMods]: Initializing ZenMods module');
super();
}
// Stylesheet service
#sss = null;
get #stylesheetService() {
if (!this.#sss) {
this.#sss = Cc['@mozilla.org/content/style-sheet-service;1'].getService(
Ci.nsIStyleSheetService
);
}
return this.#sss;
}
#ssu = null;
get #styleSheetUri() {
if (!this.#ssu) {
this.#ssu = Services.io.newFileURI(new FileUtils.File(this.#styleSheetPath));
}
return this.#ssu;
}
get #styleSheetPath() {
return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes.css');
}
async #handleDisableMods() {
if (Services.prefs.getBoolPref('zen.themes.disable-all', false)) {
console.log('[ZenMods]: Disabling mods module.');
await this.#removeStylesheet();
} else {
console.log('[ZenMods]: Enabling mods module.');
await this.#rebuildModsStylesheet();
}
}
#getStylesheetURIForMod(mod) {
return Services.io.newFileURI(
new FileUtils.File(PathUtils.join(this.getModFolder(mod.id), 'chrome.css'))
);
}
async #insertStylesheet() {
if (await IOUtils.exists(this.#styleSheetPath)) {
await this.#stylesheetService.loadAndRegisterSheet(
this.#styleSheetUri,
this.#stylesheetService.AGENT_SHEET
);
}
if (
!this.#stylesheetService.sheetRegistered(
this.#styleSheetUri,
this.#stylesheetService.AGENT_SHEET
)
) {
console.error(`[ZenMods]: Failed to register stylesheet at ${this.#styleSheetUri.spec}.`);
}
}
async #removeStylesheet() {
await this.#stylesheetService.unregisterSheet(
this.#styleSheetUri,
this.#stylesheetService.AGENT_SHEET
);
const rv = this.#stylesheetService.sheetRegistered(
this.#styleSheetUri,
this.#stylesheetService.AGENT_SHEET
);
await IOUtils.remove(this.#styleSheetPath, { ignoreAbsent: true });
if (rv || (await IOUtils.exists(this.#styleSheetPath))) {
console.error(`[ZenMods]: Failed to unregister stylesheet at ${this.#styleSheetUri.spec}.`);
}
}
async #rebuildModsStylesheet() {
await this.#removeStylesheet();
const mods = await this.#getEnabledMods();
await this.#writeStylesheet(mods);
const modsWithPreferences = await Promise.all(
mods.map(async (mod) => {
const preferences = await this.getModPreferences(mod);
return {
name: mod.name,
enabled: mod.enabled,
preferences,
};
})
);
this.#setDefaults(modsWithPreferences);
this.#writeToDom(modsWithPreferences);
await this.#insertStylesheet();
}
async #getEnabledMods() {
const modsObject = await this.getMods();
const mods = Object.values(modsObject).filter(
(mod) => mod.enabled === undefined || mod.enabled
);
const modList = mods.map(({ name }) => name).join(', ');
const message =
modList !== ''
? `[ZenMods]: Loading enabled Zen mods: ${modList}.`
: '[ZenMods]: No enabled Zen mods.';
console.log(message);
return mods;
}
#setDefaults(modsWithPreferences) {
for (const { preferences, enabled } of modsWithPreferences) {
if (enabled !== undefined && !enabled) {
continue;
}
for (const { type, property, defaultValue } of preferences) {
if (defaultValue === undefined) {
continue;
}
if (type === 'checkbox') {
const value = Services.prefs.getBoolPref(property, false);
if (typeof defaultValue !== 'boolean') {
console.warn(
'[ZenMods]: Warning, invalid data type received for expected type boolean, skipping.'
);
continue;
}
if (!value) {
Services.prefs.setBoolPref(property, defaultValue);
}
} else {
const value = Services.prefs.getStringPref(property, 'zen-property-no-saved');
if (typeof defaultValue !== 'string' && typeof defaultValue !== 'number') {
console.warn(
`[ZenMods]: Warning, invalid data type received (${typeof defaultValue}), skipping.`
);
continue;
}
if (value === 'zen-property-no-saved') {
Services.prefs.setStringPref(property, defaultValue.toString());
}
}
}
}
}
#writeToDom(modsWithPreferences) {
for (const browser of ZenMultiWindowFeature.browsers) {
for (const { enabled, preferences, name } of modsWithPreferences) {
const sanitizedName = this.sanitizeModName(name);
if (enabled !== undefined && !enabled) {
const element = browser.document.getElementById(sanitizedName);
if (element) {
element.remove();
}
for (const { property } of preferences.filter(({ type }) => type !== 'checkbox')) {
const sanitizedProperty = property?.replaceAll(/\./g, '-');
browser.document.querySelector(':root').style.removeProperty(`--${sanitizedProperty}`);
}
continue;
}
for (const { property, type } of preferences) {
const value = Services.prefs.getStringPref(property, '');
const sanitizedProperty = property?.replaceAll(/\./g, '-');
switch (type) {
case 'dropdown': {
if (value !== '') {
let element = browser.document.getElementById(sanitizedName);
if (!element) {
element = browser.document.createElement('div');
element.style.display = 'none';
element.setAttribute('id', sanitizedName);
browser.document.body.appendChild(element);
}
element.setAttribute(sanitizedProperty, value);
}
break;
}
case 'string': {
if (value === '') {
browser.document
.querySelector(':root')
.style.removeProperty(`--${sanitizedProperty}`);
} else {
browser.document
.querySelector(':root')
.style.setProperty(`--${sanitizedProperty}`, value);
}
break;
}
default: {
}
}
}
}
}
}
async #writeStylesheet(modList = []) {
const mods = [];
for (let mod of modList) {
mod._chromeURL = this.#getStylesheetURIForMod(mod).spec;
mods.push(mod);
}
let content = this.#kZenStylesheetModHeader;
content += `\n* FILE GENERATED AT: ${this.#getCurrentDateTime()}\n`;
content += this.#kZenStylesheetModHeaderBody;
for (let mod of mods) {
if (mod.enabled !== undefined && !mod.enabled) {
continue;
}
content += `\n/* Name: ${mod.name} */\n`;
content += `/* Description: ${mod.description} */\n`;
content += `/* Author: @${mod.author} */\n`;
if (mod._readmeURL) {
content += `/* Readme: ${mod.readme} */\n`;
}
content += `@import url("${mod._chromeURL}");\n`;
}
content += this.#kZenStylesheetModFooter;
const buffer = new TextEncoder().encode(content);
await IOUtils.write(this.#styleSheetPath, buffer);
}
#compareVersions(version1, version2) {
let result = false;
if (typeof version1 !== 'object') {
version1 = version1.toString().split('.');
}
if (typeof version2 !== 'object') {
version2 = version2.toString().split('.');
}
for (let i = 0; i < Math.max(version1.length, version2.length); i++) {
if (version1[i] == undefined) {
version1[i] = 0;
}
if (version2[i] == undefined) {
version2[i] = 0;
}
if (Number(version1[i]) < Number(version2[i])) {
result = true;
break;
}
if (version1[i] != version2[i]) {
break;
}
}
return result;
}
#composeModApiUrl(modId) {
// keeping theme here as it would require changes to CI to change the name
return `https://zen-browser.github.io/theme-store/themes/${modId}/theme.json`;
}
async #downloadUrlToFile(url, path, isStyleSheet = false, maxRetries = 3, retryDelayMs = 500) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status} for url: ${url}`);
}
const data = await response.text();
let content = data;
if (isStyleSheet) {
content = '@-moz-document url-prefix("chrome:") {\n';
for (const line of data.split('\n')) {
content += ` ${line}\n`;
}
content += '}';
}
// convert the data into a Uint8Array
const buffer = new TextEncoder().encode(content);
await IOUtils.write(path, buffer);
return; // to exit the loop
} catch (e) {
attempt++;
if (attempt >= maxRetries) {
console.error('[ZenMods]: Error downloading file after retries', url, e);
} else {
console.warn(
`[ZenMods]: Download failed (attempt ${attempt} of ${maxRetries}), retrying in ${retryDelayMs}ms...`,
url,
e
);
await new Promise((res) => setTimeout(res, retryDelayMs));
}
}
}
}
// private properties end
// public properties start
throttle(mainFunction, delay) {
let timerFlag = null;
return (...args) => {
if (timerFlag === null) {
mainFunction(...args);
timerFlag = setTimeout(() => {
timerFlag = null;
}, delay);
}
};
}
debounce(mainFunction, wait) {
let timerFlag;
return (...args) => {
clearTimeout(timerFlag);
timerFlag = setTimeout(() => {
mainFunction(...args);
}, wait);
};
}
sanitizeModName(name) {
// Do not change to "mod-" for backwards compatibility
return `theme-${name?.replaceAll(/\s/g, '-')?.replaceAll(/[^A-Za-z_-]+/g, '')}`;
}
get updatePref() {
return 'zen.themes.updated-value-observer';
}
get modsRootPath() {
return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes');
}
get modsDataFile() {
return PathUtils.join(PathUtils.profileDir, 'zen-themes.json');
}
getModFolder(modId) {
return PathUtils.join(this.modsRootPath, modId);
}
async getMods() {
if (!(await IOUtils.exists(this.modsDataFile))) {
await IOUtils.writeJSON(this.modsDataFile, {});
return {};
}
let mods = {};
try {
mods = await IOUtils.readJSON(this.modsDataFile);
if (mods === null || typeof mods !== 'object') {
throw new Error('Mods data file is invalid');
}
} catch {
// If we have a corrupted file, reset it
await IOUtils.writeJSON(this.modsDataFile, {});
Services.wm
.getMostRecentWindow('navigator:browser')
.gZenUIManager.showToast('zen-themes-corrupted', {
timeout: 8000,
});
}
return mods;
}
async getModPreferences(mod) {
const modPath = PathUtils.join(this.modsRootPath, mod.id, 'preferences.json');
if (!(await IOUtils.exists(modPath)) || !mod.preferences) {
return [];
}
try {
const preferences = await IOUtils.readJSON(modPath);
return preferences.filter(({ disabledOn = [] }) => {
return !disabledOn.includes(gZenOperatingSystemCommonUtils.currentOperatingSystem);
});
} catch (e) {
console.error(`[ZenMods]: Error reading mod preferences for ${mod.name}:`, e);
return [];
}
}
async init() {
try {
await SessionStore.promiseInitialized;
if (
Services.prefs.getBoolPref('zen.themes.disable-all', false) ||
Services.appinfo.inSafeMode
) {
console.log('[ZenMods]: Mods disabled by user or in safe mode.');
return;
}
await this.getMods(); // Check for any errors in the themes data file
const mods = await this.#getEnabledMods();
const modsWithPreferences = await Promise.all(
mods.map(async (mod) => {
const preferences = await this.getModPreferences(mod);
return {
name: mod.name,
enabled: mod.enabled,
preferences,
};
})
);
this.#writeToDom(modsWithPreferences);
await this.#insertStylesheet();
this.#setNewMilestoneIfNeeded();
if (this.#shouldAutoUpdate()) {
requestIdleCallback(
() => {
if (!window.closed) {
requestAnimationFrame(() => {
this.checkForModsUpdates();
});
}
},
{ timeout: 1000 }
);
}
} catch (e) {
console.error('[ZenMods]: Error loading Zen Mods:', e);
}
Services.prefs.addObserver(this.updatePref, this.#rebuildModsStylesheet.bind(this), false);
Services.prefs.addObserver('zen.themes.disable-all', this.#handleDisableMods.bind(this), false);
}
#setNewMilestoneIfNeeded() {
const previousMilestone = Services.prefs.getStringPref('zen.mods.milestone', '');
if (previousMilestone != Services.appinfo.version) {
Services.prefs.setStringPref('zen.mods.milestone', Services.appinfo.version);
Services.prefs.clearUserPref('zen.mods.last-update');
}
}
#shouldAutoUpdate() {
const daysBeforeUpdate = Services.prefs.getIntPref('zen.mods.auto-update-days');
const lastUpdatedSec = Services.prefs.getIntPref('zen.mods.last-update', -1);
const nowSec = Math.floor(Date.now() / 1000);
const daysSinceUpdate = (nowSec - lastUpdatedSec) / (60 * 60 * 24);
return (
(Services.prefs.getBoolPref('zen.mods.auto-update', true) &&
daysSinceUpdate >= daysBeforeUpdate) ||
lastUpdatedSec < 0
);
}
async checkForModsUpdates() {
const mods = await this.getMods();
const updates = await Promise.all(
Object.values(mods).map(async (currentMod) => {
try {
const possibleNewModVersion = await this.requestMod(currentMod.id);
if (!possibleNewModVersion) {
return null;
}
if (
!this.#compareVersions(possibleNewModVersion.version, currentMod.version ?? '0.0.0') &&
possibleNewModVersion.version != currentMod.version
) {
console.log(
`[ZenMods]: Mod update found for mod ${currentMod.name} (${currentMod.id}), current: ${currentMod.version}, new: ${possibleNewModVersion.version}`
);
possibleNewModVersion.enabled = currentMod.enabled;
await this.removeMod(currentMod.id, false);
mods[currentMod.id] = possibleNewModVersion;
return possibleNewModVersion;
}
return null;
} catch (e) {
console.error('[ZenMods]: Error checking for mod updates', e);
return null;
}
})
);
await this.updateMods(mods);
Services.prefs.setIntPref('zen.mods.last-update', Math.floor(Date.now() / 1000));
return updates.filter((update) => {
return update !== null;
});
}
async removeMod(modId, triggerUpdate = true) {
const modPath = this.getModFolder(modId);
console.log(`[ZenMods]: Removing mod ${modPath}`);
await IOUtils.remove(modPath, { recursive: true, ignoreAbsent: true });
const mods = await this.getMods();
delete mods[modId];
await IOUtils.writeJSON(this.modsDataFile, mods);
if (triggerUpdate) {
this.triggerModsUpdate();
}
}
async enableMod(modId) {
const mods = await this.getMods();
const mod = mods[modId];
console.log(`[ZenMods]: Enabling mod ${mod.name}`);
mod.enabled = true;
await IOUtils.writeJSON(this.modsDataFile, mods);
}
async disableMod(modId) {
const mods = await this.getMods();
const mod = mods[modId];
console.log(`[ZenMods]: Disabling mod ${mod.name}`);
mod.enabled = false;
await IOUtils.writeJSON(this.modsDataFile, mods);
}
async updateMods(mods = undefined) {
if (!mods) {
mods = await this.getMods();
}
await IOUtils.writeJSON(this.modsDataFile, mods);
await this.checkForModChanges();
}
triggerModsUpdate() {
Services.prefs.setBoolPref(this.updatePref, !Services.prefs.getBoolPref(this.updatePref));
}
async installMod(mod) {
try {
const modPath = PathUtils.join(this.modsRootPath, mod.id);
await IOUtils.makeDirectory(modPath, { ignoreExisting: true });
await this.#downloadUrlToFile(mod.style, PathUtils.join(modPath, 'chrome.css'), true);
await this.#downloadUrlToFile(mod.readme, PathUtils.join(modPath, 'readme.md'));
if (mod.preferences) {
await this.#downloadUrlToFile(mod.preferences, PathUtils.join(modPath, 'preferences.json'));
}
} catch (e) {
console.error('[ZenMods]: Error installing mod', mod.id, e);
}
}
async checkForModChanges() {
const mods = await this.getMods();
for (const [modId, mod] of Object.entries(mods)) {
try {
if (!mod) {
continue;
}
if (!(await IOUtils.exists(this.getModFolder(modId)))) {
await this.installMod(mod);
}
} catch (e) {
console.error('[ZenMods]: Error checking for mod changes', e);
}
}
this.triggerModsUpdate();
}
async requestMod(modId) {
const url = this.#composeModApiUrl(modId);
console.debug(`[ZenMods]: Fetching mod ${modId} info from ${url}`);
const data = await fetch(url, {
mode: 'no-cors',
});
if (data.ok) {
try {
const obj = await data.json();
return obj;
} catch (e) {
console.error(`[ZenMods]: Error parsing mod ${modId} info:`, e);
}
} else {
console.error(`[ZenMods]: Error fetching mod ${modId} info:`, data.status);
}
return null;
}
async isModInstalled(modId) {
const mods = await this.getMods();
return Boolean(mods?.[modId]);
}
// public properties end
}
window.gZenMods = new ZenMods();
gZenActorsManager.addJSWindowActor('ZenModsMarketplace', {
parent: {
esModuleURI: 'resource:///actors/ZenModsMarketplaceParent.sys.mjs',
},
child: {
esModuleURI: 'resource:///actors/ZenModsMarketplaceChild.sys.mjs',
events: {
DOMContentLoaded: {},
},
},
matches: [
...Services.prefs.getStringPref('zen.injections.match-urls').split(','),
'about:preferences',
],
});

View file

@ -1,138 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
var ZenThemesCommon = {
kZenColors: [
'#aac7ff',
'#74d7cb',
'#a0d490',
'#dec663',
'#ffb787',
'#dec1b1',
'#ffb1c0',
'#ddbfc3',
'#f6b0ea',
'#d4bbff',
],
get browsers() {
return Services.wm.getEnumerator('navigator:browser');
},
get currentBrowser() {
return Services.wm.getMostRecentWindow('navigator:browser');
},
get themesRootPath() {
return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes');
},
get themesDataFile() {
return PathUtils.join(PathUtils.profileDir, 'zen-themes.json');
},
getThemeFolder(themeId) {
return PathUtils.join(this.themesRootPath, themeId);
},
async getThemes() {
if (!(await IOUtils.exists(this.themesDataFile))) {
await IOUtils.writeJSON(this.themesDataFile, {});
}
let themes = {};
try {
themes = await IOUtils.readJSON(this.themesDataFile);
if (themes === null || typeof themes !== 'object') {
throw new Error('Themes data file is null');
}
} catch {
// If we have a corrupted file, reset it
await IOUtils.writeJSON(this.themesDataFile, {});
Services.wm
.getMostRecentWindow('navigator:browser')
.gZenUIManager.showToast('zen-themes-corrupted', {
timeout: 8000,
});
}
return themes;
},
async getThemePreferences(theme) {
const themePath = PathUtils.join(this.themesRootPath, theme.id, 'preferences.json');
if (!(await IOUtils.exists(themePath)) || !theme.preferences) {
return [];
}
try {
const preferences = await IOUtils.readJSON(themePath);
// compat mode for old preferences, all of them can only be checkboxes
if (typeof preferences === 'object' && !Array.isArray(preferences)) {
console.warn(
`[ZenThemes]: Warning, ${theme.name} uses legacy preferences, please migrate them to the new preferences style, as legacy preferences might be removed at a future release. More information at: https://docs.zen-browser.app/themes-store/themes-marketplace-preferences`
);
const newThemePreferences = [];
for (let [entry, label] of Object.entries(preferences)) {
const [, negation = '', os = '', property] =
/(!?)(?:(macos|windows|linux):)?([A-Za-z0-9-_.]+)/g.exec(entry);
const isNegation = negation === '!';
if (
(isNegation && os === gZenOperatingSystemCommonUtils.currentOperatingSystem) ||
(os !== '' &&
os !== gZenOperatingSystemCommonUtils.currentOperatingSystem &&
!isNegation)
) {
continue;
}
newThemePreferences.push({
property,
label,
type: 'checkbox',
disabledOn: os !== '' ? [os] : [],
});
}
return newThemePreferences;
}
return preferences.filter(
({ disabledOn = [] }) =>
!disabledOn.includes(gZenOperatingSystemCommonUtils.currentOperatingSystem)
);
} catch (e) {
console.error(`[ZenThemes]: Error reading preferences for ${theme.name}:`, e);
return [];
}
},
throttle(mainFunction, delay) {
let timerFlag = null;
return (...args) => {
if (timerFlag === null) {
mainFunction(...args);
timerFlag = setTimeout(() => {
timerFlag = null;
}, delay);
}
};
},
debounce(mainFunction, wait) {
let timerFlag;
return (...args) => {
clearTimeout(timerFlag);
timerFlag = setTimeout(() => {
mainFunction(...args);
}, wait);
};
},
};

View file

@ -1,338 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
const kZenStylesheetThemeHeader = '/* Zen Themes - Generated by ZenThemesImporter.';
const kZenStylesheetThemeHeaderBody = `* 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 */
`;
const getCurrentDateTime = () =>
new Intl.DateTimeFormat('en-US', {
dateStyle: 'full',
timeStyle: 'full',
}).format(new Date().getTime());
var gZenStylesheetManager = {
async writeStylesheet(path, themes) {
let content = kZenStylesheetThemeHeader;
content += `\n* FILE GENERATED AT: ${getCurrentDateTime()}\n`;
content += kZenStylesheetThemeHeaderBody;
for (let theme of themes) {
if (theme.enabled !== undefined && !theme.enabled) {
continue;
}
content += this.getThemeCSS(theme);
}
content += kenStylesheetFooter;
const buffer = new TextEncoder().encode(content);
await IOUtils.write(path, buffer);
},
getThemeCSS(theme) {
let css = '\n';
css += `/* Name: ${theme.name} */\n`;
css += `/* Description: ${theme.description} */\n`;
css += `/* Author: @${theme.author} */\n`;
if (theme._readmeURL) {
css += `/* Readme: ${theme.readme} */\n`;
}
css += `@import url("${theme._chromeURL}");\n`;
return css;
},
};
var gZenThemesImporter = new (class {
constructor() {
try {
window.SessionStore.promiseInitialized.then(async () => {
if (
Services.prefs.getBoolPref('zen.themes.disable-all', false) ||
Services.appinfo.inSafeMode
) {
console.log('[ZenThemesImporter]: Disabling all themes.');
return;
}
await ZenThemesCommon.getThemes(); // Check for any errors in the themes data file
const themes = await this.getEnabledThemes();
const themesWithPreferences = await Promise.all(
themes.map(async (theme) => {
const preferences = await ZenThemesCommon.getThemePreferences(theme);
return {
name: theme.name,
enabled: theme.enabled,
preferences,
};
})
);
this.writeToDom(themesWithPreferences);
await this.insertStylesheet();
});
console.info('[ZenThemesImporter]: Zen Themes imported');
} catch (e) {
console.error('[ZenThemesImporter]: Error importing Zen Themes: ', e);
}
Services.prefs.addObserver(
'zen.themes.updated-value-observer',
this.rebuildThemeStylesheet.bind(this),
false
);
Services.prefs.addObserver(
'zen.themes.disable-all',
this.handleDisableThemes.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');
}
async handleDisableThemes() {
if (Services.prefs.getBoolPref('zen.themes.disable-all', false)) {
console.log('[ZenThemesImporter]: Disabling themes module.');
await this.removeStylesheet();
} else {
console.log('[ZenThemesImporter]: Enabling themes module.');
await this.rebuildThemeStylesheet();
}
}
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(ZenThemesCommon.getThemeFolder(theme.id), 'chrome.css'))
);
}
async insertStylesheet() {
if (await IOUtils.exists(this.styleSheetPath)) {
await this.sss.loadAndRegisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET);
}
if (this.sss.sheetRegistered(this.styleSheetURI, this.sss.AGENT_SHEET)) {
console.debug('[ZenThemesImporter]: Sheet successfully registered');
}
}
async removeStylesheet() {
await this.sss.unregisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET);
const rv = this.sss.sheetRegistered(this.styleSheetURI, this.sss.AGENT_SHEET);
await IOUtils.remove(this.styleSheetPath, { ignoreAbsent: true });
if (!rv && !(await IOUtils.exists(this.styleSheetPath))) {
console.debug('[ZenThemesImporter]: Sheet successfully unregistered');
}
}
async rebuildThemeStylesheet() {
await this.removeStylesheet();
const themes = await this.getEnabledThemes();
await this.writeStylesheet(themes);
const themesWithPreferences = await Promise.all(
themes.map(async (theme) => {
const preferences = await ZenThemesCommon.getThemePreferences(theme);
return {
name: theme.name,
enabled: theme.enabled,
preferences,
};
})
);
this.setDefaults(themesWithPreferences);
this.writeToDom(themesWithPreferences);
await this.insertStylesheet();
}
async getEnabledThemes() {
const themeObject = await ZenThemesCommon.getThemes();
const themes = Object.values(themeObject).filter(
(theme) => theme.enabled === undefined || theme.enabled
);
const themeList = themes.map(({ name }) => name).join(', ');
const message =
themeList !== ''
? `[ZenThemesImporter]: Loading enabled Zen themes: ${themeList}.`
: '[ZenThemesImporter]: No enabled Zen themes.';
console.log(message);
return themes;
}
setDefaults(themesWithPreferences) {
for (const { preferences, enabled } of themesWithPreferences) {
if (enabled !== undefined && !enabled) {
continue;
}
for (const { type, property, defaultValue } of preferences) {
if (defaultValue === undefined) {
continue;
}
if (type === 'checkbox') {
const value = Services.prefs.getBoolPref(property, false);
if (typeof defaultValue !== 'boolean') {
console.log(
`[ZenThemesImporter]: Warning, invalid data type received for expected type boolean, skipping.`
);
continue;
}
if (!value) {
Services.prefs.setBoolPref(property, defaultValue);
}
} else {
const value = Services.prefs.getStringPref(property, 'zen-property-no-saved');
if (typeof defaultValue !== 'string' && typeof defaultValue !== 'number') {
console.log(
`[ZenThemesImporter]: Warning, invalid data type received (${typeof defaultValue}), skipping.`
);
continue;
}
if (value === 'zen-property-no-saved') {
Services.prefs.setStringPref(property, defaultValue.toString());
}
}
}
}
}
writeToDom(themesWithPreferences) {
for (const browser of ZenMultiWindowFeature.browsers) {
for (const { enabled, preferences, name } of themesWithPreferences) {
const sanitizedName = `theme-${name?.replaceAll(/\s/g, '-')?.replaceAll(/[^A-Za-z_-]+/g, '')}`;
if (enabled !== undefined && !enabled) {
const element = browser.document.getElementById(sanitizedName);
if (element) {
element.remove();
}
for (const { property } of preferences.filter(({ type }) => type !== 'checkbox')) {
const sanitizedProperty = property?.replaceAll(/\./g, '-');
browser.document.querySelector(':root').style.removeProperty(`--${sanitizedProperty}`);
}
continue;
}
for (const { property, type } of preferences) {
const value = Services.prefs.getStringPref(property, '');
const sanitizedProperty = property?.replaceAll(/\./g, '-');
switch (type) {
case 'dropdown': {
if (value !== '') {
let element = browser.document.getElementById(sanitizedName);
if (!element) {
element = browser.document.createElement('div');
element.style.display = 'none';
element.setAttribute('id', sanitizedName);
browser.document.body.appendChild(element);
}
element.setAttribute(sanitizedProperty, value);
}
break;
}
case 'string': {
if (value === '') {
browser.document
.querySelector(':root')
.style.removeProperty(`--${sanitizedProperty}`);
} else {
browser.document
.querySelector(':root')
.style.setProperty(`--${sanitizedProperty}`, value);
}
break;
}
default: {
}
}
}
}
}
}
async writeStylesheet(themeList = []) {
const themes = [];
for (let theme of themeList) {
theme._chromeURL = this.getStylesheetURIForTheme(theme).spec;
themes.push(theme);
}
await gZenStylesheetManager.writeStylesheet(this.styleSheetPath, themes);
}
})();
gZenActorsManager.addJSWindowActor('ZenThemeMarketplace', {
parent: {
esModuleURI: 'chrome://browser/content/zen-components/actors/ZenThemeMarketplaceParent.sys.mjs',
},
child: {
esModuleURI: 'chrome://browser/content/zen-components/actors/ZenThemeMarketplaceChild.sys.mjs',
events: {
DOMContentLoaded: {},
},
},
matches: [
...Services.prefs.getStringPref('zen.injections.match-urls').split(','),
'about:preferences',
],
});

View file

@ -0,0 +1,141 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
export class ZenModsMarketplaceChild extends JSWindowActorChild {
constructor() {
super();
}
handleEvent(event) {
if (event.type === 'DOMContentLoaded') {
const verifier = this.contentWindow.document.querySelector(
'meta[name="zen-content-verified"]'
);
if (verifier) {
verifier.setAttribute('content', 'verified');
}
this.initiateModsMarketplace();
this.contentWindow.document.addEventListener(
'ZenCheckForModUpdates',
this.checkForModUpdates.bind(this)
);
}
}
// This function will be called from about:preferences
checkForModUpdates(event) {
event.preventDefault();
this.sendAsyncMessage('ZenModsMarketplace:CheckForUpdates');
}
initiateModsMarketplace() {
this.contentWindow.setTimeout(() => {
this.addButtons();
this.injectMarketplaceAPI();
}, 0);
}
get actionButton() {
return this.contentWindow.document.getElementById('install-theme');
}
get actionButtonUninstall() {
return this.contentWindow.document.getElementById('install-theme-uninstall');
}
async isThemeInstalled(themeId) {
return await this.sendQuery('ZenModsMarketplace:IsModInstalled', { themeId });
}
async receiveMessage(message) {
switch (message.name) {
case 'ZenModsMarketplace:ModChanged': {
const modId = message.data.modId;
const actionButton = this.actionButton;
const actionButtonInstalled = this.actionButtonUninstall;
if (actionButton && actionButtonInstalled) {
actionButton.disabled = false;
actionButtonInstalled.disabled = false;
if (await this.isThemeInstalled(modId)) {
actionButton.classList.add('hidden');
actionButtonInstalled.classList.remove('hidden');
} else {
actionButton.classList.remove('hidden');
actionButtonInstalled.classList.add('hidden');
}
}
break;
}
case 'ZenModsMarketplace:CheckForUpdatesFinished': {
const updates = message.data.updates;
this.contentWindow.document.dispatchEvent(
new CustomEvent('ZenModsMarketplace:CheckForUpdatesFinished', { detail: { updates } })
);
break;
}
}
}
injectMarketplaceAPI() {
Cu.exportFunction(this.handleModInstallationEvent.bind(this), this.contentWindow, {
defineAs: 'ZenInstallMod',
});
}
async addButtons() {
const actionButton = this.actionButton;
const actionButtonUninstall = this.actionButtonUninstall;
const errorMessage = this.contentWindow.document.getElementById('install-theme-error');
if (!actionButton || !actionButtonUninstall) {
return;
}
errorMessage.classList.add('hidden');
const themeId = actionButton.getAttribute('zen-theme-id');
if (await this.isThemeInstalled(themeId)) {
actionButtonUninstall.classList.remove('hidden');
} else {
actionButton.classList.remove('hidden');
}
actionButton.addEventListener('click', this.handleModInstallationEvent.bind(this));
actionButtonUninstall.addEventListener('click', this.handleModUninstallEvent.bind(this));
}
async handleModUninstallEvent(event) {
const button = event.target;
button.disabled = true;
const modId = button.getAttribute('zen-theme-id');
this.sendAsyncMessage('ZenModsMarketplace:UninstallMod', { modId });
}
async handleModInstallationEvent(event) {
// Object can be an event or a theme id
let modId;
if (event.target) {
const button = event.target;
button.disabled = true;
modId = button.getAttribute('zen-theme-id');
} else {
modId = event.themeId;
}
this.sendAsyncMessage('ZenModsMarketplace:InstallMod', { modId });
}
}

View file

@ -0,0 +1,65 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
export class ZenModsMarketplaceParent extends JSWindowActorParent {
constructor() {
super();
}
get modsManager() {
return this.browsingContext.topChromeWindow.gZenMods;
}
async receiveMessage(message) {
switch (message.name) {
case 'ZenModsMarketplace:InstallMod': {
const modId = message.data.modId;
const mod = await this.modsManager.requestMod(modId);
console.log(`[ZenModsMarketplaceParent]: Installing mod ${mod.id}`);
mod.enabled = true;
const mods = await this.modsManager.getMods();
mods[mod.id] = mod;
await this.modsManager.updateMods(mods);
await this.updateChildProcesses(mod.id);
break;
}
case 'ZenModsMarketplace:UninstallMod': {
const modId = message.data.modId;
console.log(`[ZenModsMarketplaceParent]: Uninstalling mod ${modId}`);
const mods = await this.modsManager.getMods();
delete mods[modId];
await this.modsManager.removeMod(modId);
await this.modsManager.updateMods(mods);
await this.updateChildProcesses(modId);
break;
}
case 'ZenModsMarketplace:CheckForUpdates': {
const updates = await this.modsManager.checkForModsUpdates();
this.sendAsyncMessage('ZenModsMarketplace:CheckForUpdatesFinished', { updates });
break;
}
case 'ZenModsMarketplace:IsModInstalled': {
const themeId = message.data.themeId;
const themes = await this.modsManager.getMods();
return Boolean(themes?.[themeId]);
}
}
}
async updateChildProcesses(modId) {
this.sendAsyncMessage('ZenModsMarketplace:ModChanged', { modId });
}
}

View file

@ -1,200 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
export class ZenThemeMarketplaceChild extends JSWindowActorChild {
constructor() {
super();
}
handleEvent(event) {
switch (event.type) {
case 'DOMContentLoaded':
this.initalizeZenAPI(event);
break;
default:
}
}
initalizeZenAPI(event) {
const verifier = this.contentWindow.document.querySelector('meta[name="zen-content-verified"]');
if (verifier) {
verifier.setAttribute('content', 'verified');
}
const possibleRicePage = this.collectRiceMetadata();
if (possibleRicePage?.id) {
this.sendAsyncMessage('ZenThemeMarketplace:RicePage', possibleRicePage);
return;
}
this.initiateThemeMarketplace();
this.contentWindow.document.addEventListener(
'ZenCheckForThemeUpdates',
this.checkForThemeUpdates.bind(this)
);
}
collectRiceMetadata() {
const meta = this.contentWindow.document.querySelector('meta[name="zen-rice-data"]');
if (meta) {
return {
id: meta.getAttribute('data-id'),
name: meta.getAttribute('data-name'),
author: meta.getAttribute('data-author'),
};
}
return null;
}
// This function will be called from about:preferences
checkForThemeUpdates(event) {
event.preventDefault();
this.sendAsyncMessage('ZenThemeMarketplace:CheckForUpdates');
}
initiateThemeMarketplace() {
this.contentWindow.setTimeout(() => {
this.addInstallButtons();
this.injectMarketplaceAPI();
}, 0);
}
get actionButton() {
return this.contentWindow.document.getElementById('install-theme');
}
get actionButtonUninstall() {
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.actionButtonUninstall;
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;
}
case 'ZenThemeMarketplace:CheckForUpdatesFinished': {
const updates = message.data.updates;
this.contentWindow.document.dispatchEvent(
new CustomEvent('ZenThemeMarketplace:CheckForUpdatesFinished', { detail: { updates } })
);
break;
}
case 'ZenThemeMarketplace:GetThemeInfo': {
const themeId = message.data.themeId;
const theme = await this.getThemeInfo(themeId);
return theme;
}
}
}
injectMarketplaceAPI() {
Cu.exportFunction(this.installTheme.bind(this), this.contentWindow, {
defineAs: 'ZenInstallTheme',
});
}
async addInstallButtons() {
const actionButton = this.actionButton;
const actionButtonUninstall = this.actionButtonUninstall;
const errorMessage = this.contentWindow.document.getElementById('install-theme-error');
if (!actionButton || !actionButtonUninstall) {
return;
}
errorMessage.classList.add('hidden');
const themeId = actionButton.getAttribute('zen-theme-id');
if (await this.isThemeInstalled(themeId)) {
actionButtonUninstall.classList.remove('hidden');
} else {
actionButton.classList.remove('hidden');
}
actionButton.addEventListener('click', this.installTheme.bind(this));
actionButtonUninstall.addEventListener('click', this.uninstallTheme.bind(this));
}
async isThemeInstalled(themeId) {
return await this.sendQuery('ZenThemeMarketplace:IsThemeInstalled', { themeId });
}
addTheme(theme) {
this.sendAsyncMessage('ZenThemeMarketplace:InstallTheme', { theme });
}
getThemeAPIUrl(themeId) {
return `https://zen-browser.github.io/theme-store/themes/${themeId}/theme.json`;
}
async getThemeInfo(themeId) {
const url = this.getThemeAPIUrl(themeId);
const data = await fetch(url, {
mode: 'no-cors',
});
if (data.ok) {
try {
const obj = await data.json();
return obj;
} catch (e) {
console.error('ZenThemeMarketplace: Error parsing theme info: ', e);
}
} else {
console.error('ZenThemeMarketplace: Error fetching theme info: ', data.status);
}
return null;
}
async uninstallTheme(event) {
const button = event.target;
button.disabled = true;
const themeId = button.getAttribute('zen-theme-id');
this.sendAsyncMessage('ZenThemeMarketplace:UninstallTheme', { themeId });
}
async installTheme(object) {
// Object can be an event or a theme id
let themeId;
if (object.target) {
const button = object.target;
button.disabled = true;
themeId = button.getAttribute('zen-theme-id');
} else {
themeId = object.themeId;
}
console.info('ZenThemeMarketplace: Installing theme with id: ', themeId);
const theme = await this.getThemeInfo(themeId);
if (!theme) {
console.error('ZenThemeMarketplace: Error fetching theme info');
return;
}
this.addTheme(theme);
}
}

View file

@ -1,259 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
export class ZenThemeMarketplaceParent extends JSWindowActorParent {
constructor() {
super();
}
async receiveMessage(message) {
switch (message.name) {
case 'ZenThemeMarketplace:InstallTheme': {
console.info('ZenThemeMarketplaceParent: Updating themes');
const theme = message.data.theme;
theme.enabled = true;
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 Boolean(themes?.[themeId]);
}
case 'ZenThemeMarketplace:CheckForUpdates': {
this.checkForThemeUpdates();
break;
}
case 'ZenThemeMarketplace:RicePage': {
this.openRicePage(this.browsingContext.topChromeWindow, message.data);
break;
}
}
}
openRicePage(window, data) {
window.gZenThemePicker.riceManager.openRicePage(data);
}
compareVersions(version1, version2) {
let result = false;
if (typeof version1 !== 'object') {
version1 = version1.toString().split('.');
}
if (typeof version2 !== 'object') {
version2 = version2.toString().split('.');
}
for (let i = 0; i < Math.max(version1.length, version2.length); i++) {
if (version1[i] == undefined) {
version1[i] = 0;
}
if (version2[i] == undefined) {
version2[i] = 0;
}
if (Number(version1[i]) < Number(version2[i])) {
result = true;
break;
}
if (version1[i] != version2[i]) {
break;
}
}
return result;
}
async checkForThemeUpdates() {
console.info('ZenThemeMarketplaceParent: Checking for theme updates');
let updates = [];
const themes = await this.getThemes();
for (const theme of Object.values(themes)) {
try {
const themeInfo = await this.sendQuery('ZenThemeMarketplace:GetThemeInfo', {
themeId: theme.id,
});
if (!themeInfo) {
continue;
}
if (
!this.compareVersions(themeInfo.version, theme.version || '0.0.0') &&
themeInfo.version != theme.version
) {
console.info(
'ZenThemeMarketplaceParent: Theme update found',
theme.id,
theme.version,
themeInfo.version
);
themeInfo.enabled = theme.enabled;
updates.push(themeInfo);
await this.removeTheme(theme.id, false);
themes[themeInfo.id] = themeInfo;
}
} catch (e) {
console.error('ZenThemeMarketplaceParent: Error checking for theme updates', e);
}
}
await this.updateThemes(themes);
this.sendAsyncMessage('ZenThemeMarketplace:CheckForUpdatesFinished', { updates });
}
async updateChildProcesses(themeId) {
this.sendAsyncMessage('ZenThemeMarketplace:ThemeChanged', { themeId });
}
async getThemes() {
return await IOUtils.readJSON(this.themesDataFile);
}
async updateThemes(themes = undefined) {
if (!themes) {
themes = await this.getThemes();
}
await IOUtils.writeJSON(this.themesDataFile, themes);
await this.checkForThemeChanges();
}
getStyleSheetFullContent(style = '') {
let stylesheet = '@-moz-document url-prefix("chrome:") {\n';
for (const line of style.split('\n')) {
stylesheet += ` ${line}\n`;
}
stylesheet += '}';
return stylesheet;
}
async downloadUrlToFile(url, path, isStyleSheet = false, maxRetries = 3, retryDelayMs = 500) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`ZenThemeMarketplaceParent: HTTP error! status: ${response.status} for url: ${url}`
);
}
const data = await response.text();
const content = isStyleSheet ? this.getStyleSheetFullContent(data) : data;
// convert the data into a Uint8Array
const buffer = new TextEncoder().encode(content);
await IOUtils.write(path, buffer);
return;
} catch (e) {
attempt++;
if (attempt >= maxRetries) {
console.error('ZenThemeMarketplaceParent: Error downloading file after retries', url, e);
} else {
console.warn(
`ZenThemeMarketplaceParent: Download failed (attempt ${attempt} of ${maxRetries}), retrying in ${retryDelayMs}ms...`,
url,
e
);
await new Promise((res) => setTimeout(res, retryDelayMs));
}
}
}
}
async downloadThemeFileContents(theme) {
try {
const themePath = PathUtils.join(this.themesRootPath, theme.id);
await IOUtils.makeDirectory(themePath, { ignoreExisting: true });
await this.downloadUrlToFile(theme.style, PathUtils.join(themePath, 'chrome.css'), true);
await this.downloadUrlToFile(theme.readme, PathUtils.join(themePath, 'readme.md'));
if (theme.preferences) {
await this.downloadUrlToFile(
theme.preferences,
PathUtils.join(themePath, 'preferences.json')
);
}
} catch (e) {
console.log('ZenThemeMarketplaceParent: Error downloading theme file contents', theme.id, e);
}
}
get themesRootPath() {
return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes');
}
get themesDataFile() {
return PathUtils.join(PathUtils.profileDir, 'zen-themes.json');
}
triggerThemeUpdate() {
const pref = 'zen.themes.updated-value-observer';
Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref));
}
async installTheme(theme) {
try {
await this.downloadThemeFileContents(theme);
} catch (e) {
console.error('ZenThemeMarketplaceParent: Error installing theme', theme.id, e);
}
}
async checkForThemeChanges() {
const themes = await this.getThemes();
const themeIds = Object.keys(themes);
for (const themeId of themeIds) {
try {
const theme = themes[themeId];
if (!theme) {
continue;
}
const themePath = PathUtils.join(this.themesRootPath, themeId);
if (!(await IOUtils.exists(themePath))) {
await this.installTheme(theme);
}
} catch (e) {
console.error('ZenThemeMarketplaceParent: Error checking for theme changes', e);
}
}
this.triggerThemeUpdate();
}
async removeTheme(themeId, triggerUpdate = true) {
const themePath = PathUtils.join(this.themesRootPath, themeId);
await IOUtils.remove(themePath, { recursive: true, ignoreAbsent: true });
if (triggerUpdate) {
this.triggerThemeUpdate();
}
}
}

View file

@ -3,8 +3,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
FINAL_TARGET_FILES.actors += [
"actors/ZenThemeMarketplaceChild.sys.mjs",
"actors/ZenThemeMarketplaceParent.sys.mjs",
"actors/ZenModsMarketplaceChild.sys.mjs",
"actors/ZenModsMarketplaceParent.sys.mjs",
]