forked from ZenBrowserMirrors/zen-desktop
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:
parent
316ff45859
commit
797152da89
16 changed files with 1129 additions and 1107 deletions
2
l10n
2
l10n
|
@ -1 +1 @@
|
|||
Subproject commit 644474b8c92e306288d835698eb6714081a650d8
|
||||
Subproject commit 9b1df3a65db379b357ac864f84ad08313b3d4c94
|
|
@ -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
|
||||
|
|
|
@ -29,10 +29,9 @@
|
|||
</linkset>
|
||||
|
||||
# Scripts used all over the browser
|
||||
<script src="chrome://browser/content/ZenUIManager.mjs"></script>
|
||||
<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>
|
||||
|
|
|
@ -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)
|
||||
|
@ -75,7 +70,7 @@
|
|||
content/browser/zen-styles/zen-download-box-animation.css (../../zen/downloads/zen-download-box-animation.css)
|
||||
|
||||
|
||||
# Images
|
||||
# Images
|
||||
content/browser/zen-images/gradient.png (../../zen/images/gradient.png)
|
||||
content/browser/zen-images/brand-header.svg (../../zen/images/brand-header.svg)
|
||||
content/browser/zen-images/layouts/collapsed.png (../../zen/images/layouts/collapsed.png)
|
||||
|
@ -87,7 +82,7 @@
|
|||
content/browser/zen-images/downloads/download.svg (../../zen/images/downloads/download.svg)
|
||||
content/browser/zen-images/downloads/archive.svg (../../zen/images/downloads/archive.svg)
|
||||
|
||||
# Fonts
|
||||
# Fonts
|
||||
content/browser/zen-fonts/JunicodeVF-Italic.woff2 (../../zen/fonts/JunicodeVF-Italic.woff2)
|
||||
content/browser/zen-fonts/JunicodeVF-Roman.woff2 (../../zen/fonts/JunicodeVF-Roman.woff2)
|
||||
|
||||
|
@ -105,4 +100,4 @@
|
|||
content/browser/zen-images/favicons/slack.ico (../../zen/images/favicons/slack.ico)
|
||||
content/browser/zen-images/favicons/reddit.ico (../../zen/images/favicons/reddit.ico)
|
||||
content/browser/zen-images/favicons/x.ico (../../zen/images/favicons/x.ico)
|
||||
content/browser/zen-images/favicons/trello.ico (../../zen/images/favicons/trello.ico)
|
||||
content/browser/zen-images/favicons/trello.ico (../../zen/images/favicons/trello.ico)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -742,24 +742,23 @@
|
|||
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',
|
||||
},
|
||||
child: {
|
||||
esModuleURI: 'chrome://browser/content/zen-components/actors/ZenGlanceChild.sys.mjs',
|
||||
events: {
|
||||
DOMContentLoaded: {},
|
||||
keydown: {
|
||||
capture: true,
|
||||
},
|
||||
gZenActorsManager.addJSWindowActor('ZenGlance', {
|
||||
parent: {
|
||||
esModuleURI: 'resource:///actors/ZenGlanceParent.sys.mjs',
|
||||
},
|
||||
child: {
|
||||
esModuleURI: 'resource:///actors/ZenGlanceChild.sys.mjs',
|
||||
events: {
|
||||
DOMContentLoaded: {},
|
||||
keydown: {
|
||||
capture: true,
|
||||
},
|
||||
},
|
||||
allFrames: true,
|
||||
matches: ['*://*/*'],
|
||||
});
|
||||
}
|
||||
},
|
||||
allFrames: true,
|
||||
matches: ['*://*/*'],
|
||||
enablePreference: 'zen.glance.enabled',
|
||||
});
|
||||
}
|
||||
|
||||
registerWindowActors();
|
||||
|
|
722
src/zen/mods/ZenMods.mjs
Normal file
722
src/zen/mods/ZenMods.mjs
Normal 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',
|
||||
],
|
||||
});
|
|
@ -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);
|
||||
};
|
||||
},
|
||||
};
|
|
@ -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',
|
||||
],
|
||||
});
|
141
src/zen/mods/actors/ZenModsMarketplaceChild.sys.mjs
Normal file
141
src/zen/mods/actors/ZenModsMarketplaceChild.sys.mjs
Normal 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 });
|
||||
}
|
||||
}
|
65
src/zen/mods/actors/ZenModsMarketplaceParent.sys.mjs
Normal file
65
src/zen/mods/actors/ZenModsMarketplaceParent.sys.mjs
Normal 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 });
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue