mirror of
https://github.com/zen-browser/desktop.git
synced 2025-10-05 17:36:34 +00:00
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:
2
l10n
2
l10n
Submodule l10n updated: 644474b8c9...9b1df3a65d
@@ -22,13 +22,17 @@ pref('zen.mediacontrols.enabled', true);
|
|||||||
// Exposure:
|
// Exposure:
|
||||||
pref('zen.haptic-feedback.enabled', true);
|
pref('zen.haptic-feedback.enabled', true);
|
||||||
|
|
||||||
|
pref('zen.mods.auto-update-days', 12); // In days
|
||||||
#ifdef MOZILLA_OFFICIAL
|
#ifdef MOZILLA_OFFICIAL
|
||||||
|
pref('zen.mods.auto-update', true);
|
||||||
pref('zen.rice.api.url', 'https://share.zen-browser.app', locked);
|
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);
|
pref('zen.injections.match-urls', 'https://zen-browser.app/*,https://share.zen-browser.app/*', locked);
|
||||||
#else
|
#else
|
||||||
|
pref('zen.mods.auto-update', false);
|
||||||
pref('zen.rice.api.url', "http://localhost", locked);
|
pref('zen.rice.api.url', "http://localhost", locked);
|
||||||
pref('zen.injections.match-urls', 'http://localhost/*', locked);
|
pref('zen.injections.match-urls', 'http://localhost/*', locked);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
pref('zen.rice.share.notice.accepted', false);
|
pref('zen.rice.share.notice.accepted', false);
|
||||||
|
|
||||||
#ifdef XP_MACOSX
|
#ifdef XP_MACOSX
|
||||||
|
@@ -31,8 +31,7 @@
|
|||||||
# Scripts used all over the browser
|
# 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/ZenFolders.mjs"></script>
|
||||||
<script src="chrome://browser/content/zen-components/ZenThemesCommon.mjs"></script>
|
<script src="chrome://browser/content/zen-components/ZenMods.mjs"></script>
|
||||||
<script src="chrome://browser/content/zen-components/ZenThemesImporter.mjs"></script>
|
|
||||||
<script src="chrome://browser/content/zen-components/ZenCompactMode.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/ZenPinnedTabsStorage.mjs"></script>
|
||||||
<script src="chrome://browser/content/zen-components/ZenWorkspacesStorage.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-components/ZenViewSplitter.mjs (../../zen/split-view/ZenViewSplitter.mjs)
|
||||||
content/browser/zen-styles/zen-decks.css (../../zen/split-view/zen-decks.css)
|
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/ZenMods.mjs (../../zen/mods/ZenMods.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/ZenWorkspaceIcons.mjs (../../zen/workspaces/ZenWorkspaceIcons.mjs)
|
content/browser/zen-components/ZenWorkspaceIcons.mjs (../../zen/workspaces/ZenWorkspaceIcons.mjs)
|
||||||
content/browser/zen-components/ZenWorkspace.mjs (../../zen/workspaces/ZenWorkspace.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-components/ZenGlanceManager.mjs (../../zen/glance/ZenGlanceManager.mjs)
|
||||||
content/browser/zen-styles/zen-glance.css (../../zen/glance/zen-glance.css)
|
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-components/ZenFolders.mjs (../../zen/folders/ZenFolders.mjs)
|
||||||
content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css)
|
content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css)
|
||||||
|
@@ -14,30 +14,37 @@ var gZenMarketplaceManager = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!window.gZenMods) {
|
||||||
|
window.gZenMods = ZenMultiWindowFeature.currentBrowser.gZenMods;
|
||||||
|
}
|
||||||
|
|
||||||
header.appendChild(this._initDisableAll());
|
header.appendChild(this._initDisableAll());
|
||||||
|
|
||||||
this._initImportExport();
|
this._initImportExport();
|
||||||
|
|
||||||
this.__hasInitializedEvents = true;
|
this.__hasInitializedEvents = true;
|
||||||
|
|
||||||
await this._buildThemesList();
|
await this._buildModsList();
|
||||||
|
|
||||||
Services.prefs.addObserver(this.updatePref, this);
|
Services.prefs.addObserver(gZenMods.updatePref, this);
|
||||||
|
|
||||||
const checkForUpdateClick = (event) => {
|
const checkForUpdateClick = (event) => {
|
||||||
if (event.target === checkForUpdates) {
|
if (event.target === checkForUpdates) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
this._checkForThemeUpdates(event);
|
this._checkForThemeUpdates(event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkForUpdates.addEventListener('click', checkForUpdateClick);
|
checkForUpdates.addEventListener('click', checkForUpdateClick);
|
||||||
|
|
||||||
document.addEventListener('ZenThemeMarketplace:CheckForUpdatesFinished', (event) => {
|
document.addEventListener('ZenModsMarketplace:CheckForUpdatesFinished', (event) => {
|
||||||
checkForUpdates.disabled = false;
|
checkForUpdates.disabled = false;
|
||||||
|
|
||||||
const updates = event.detail.updates;
|
const updates = event.detail.updates;
|
||||||
const success = document.getElementById('zenThemeMarketplaceUpdatesSuccess');
|
const success = document.getElementById('zenThemeMarketplaceUpdatesSuccess');
|
||||||
const error = document.getElementById('zenThemeMarketplaceUpdatesFailure');
|
const error = document.getElementById('zenThemeMarketplaceUpdatesFailure');
|
||||||
|
|
||||||
if (updates) {
|
if (updates) {
|
||||||
success.hidden = false;
|
success.hidden = false;
|
||||||
error.hidden = true;
|
error.hidden = true;
|
||||||
@@ -48,13 +55,16 @@ var gZenMarketplaceManager = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('unload', () => {
|
window.addEventListener('unload', () => {
|
||||||
Services.prefs.removeObserver(this.updatePref, this);
|
Services.prefs.removeObserver(gZenMods.updatePref, this);
|
||||||
this.__hasInitializedEvents = false;
|
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);
|
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');
|
const exportButton = document.getElementById('zenThemeMarketplaceExport');
|
||||||
|
|
||||||
if (importButton) {
|
if (importButton) {
|
||||||
importButton.addEventListener('click', async () => {
|
importButton.addEventListener('click', this._importThemes.bind(this));
|
||||||
await this._importThemes();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exportButton) {
|
if (exportButton) {
|
||||||
exportButton.addEventListener('click', async () => {
|
exportButton.addEventListener('click', this._exportThemes.bind(this));
|
||||||
await this._exportThemes();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_initDisableAll() {
|
_initDisableAll() {
|
||||||
const areThemesDisabled = Services.prefs.getBoolPref('zen.themes.disable-all', false);
|
const areModsDisabled = Services.prefs.getBoolPref('zen.themes.disable-all', false);
|
||||||
const browser = ZenThemesCommon.currentBrowser;
|
const browser = ZenMultiWindowFeature.currentBrowser;
|
||||||
const mozToggle = document.createElement('moz-toggle');
|
const mozToggle = document.createElement('moz-toggle');
|
||||||
|
|
||||||
mozToggle.className =
|
mozToggle.className =
|
||||||
'zenThemeMarketplaceItemPreferenceToggle zenThemeMarketplaceDisableAllToggle';
|
'zenThemeMarketplaceItemPreferenceToggle zenThemeMarketplaceDisableAllToggle';
|
||||||
mozToggle.pressed = !areThemesDisabled;
|
mozToggle.pressed = !areModsDisabled;
|
||||||
|
|
||||||
browser.document.l10n.setAttributes(
|
browser.document.l10n.setAttributes(
|
||||||
mozToggle,
|
mozToggle,
|
||||||
`zen-theme-disable-all-${!areThemesDisabled ? 'enabled' : 'disabled'}`
|
`zen-theme-disable-all-${!areModsDisabled ? 'enabled' : 'disabled'}`
|
||||||
);
|
);
|
||||||
|
|
||||||
mozToggle.addEventListener('toggle', async (event) => {
|
mozToggle.addEventListener('toggle', async (event) => {
|
||||||
const { pressed = false } = event.target || {};
|
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);
|
Services.prefs.setBoolPref('zen.themes.disable-all', !pressed);
|
||||||
browser.document.l10n.setAttributes(
|
browser.document.l10n.setAttributes(
|
||||||
mozToggle,
|
mozToggle,
|
||||||
@@ -100,90 +106,65 @@ var gZenMarketplaceManager = {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (areThemesDisabled) {
|
if (areModsDisabled) {
|
||||||
this.themesList.style.display = 'none';
|
this.modsList.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
return mozToggle;
|
return mozToggle;
|
||||||
},
|
},
|
||||||
|
|
||||||
async observe() {
|
async observe() {
|
||||||
await this._buildThemesList();
|
await this._buildModsList();
|
||||||
},
|
},
|
||||||
|
|
||||||
_checkForThemeUpdates(event) {
|
_checkForThemeUpdates(event) {
|
||||||
// Send a message to the child to check for theme updates.
|
// Send a message to the child to check for theme updates.
|
||||||
event.target.disabled = true;
|
event.target.disabled = true;
|
||||||
// send an event that will be listened by the child process.
|
// send an event that will be listened by the child process.
|
||||||
document.dispatchEvent(new CustomEvent('ZenCheckForThemeUpdates'));
|
document.dispatchEvent(new CustomEvent('ZenCheckForModUpdates'));
|
||||||
},
|
},
|
||||||
|
|
||||||
get updatePref() {
|
get modsList() {
|
||||||
return 'zen.themes.updated-value-observer';
|
if (!this._modsList) {
|
||||||
},
|
this._modsList = document.getElementById('zenThemeMarketplaceList');
|
||||||
|
|
||||||
triggerThemeUpdate() {
|
|
||||||
Services.prefs.setBoolPref(this.updatePref, !Services.prefs.getBoolPref(this.updatePref));
|
|
||||||
},
|
|
||||||
|
|
||||||
get themesList() {
|
|
||||||
if (!this._themesList) {
|
|
||||||
this._themesList = document.getElementById('zenThemeMarketplaceList');
|
|
||||||
}
|
}
|
||||||
return this._themesList;
|
return this._modsList;
|
||||||
},
|
},
|
||||||
|
|
||||||
async removeTheme(themeId) {
|
async removeMod(modId) {
|
||||||
const themePath = ZenThemesCommon.getThemeFolder(themeId);
|
await gZenMods.removeMod(modId);
|
||||||
|
|
||||||
console.info(`[ZenThemeMarketplaceParent:settings]: Removing theme ${themePath}`);
|
gZenMods.triggerModsUpdate();
|
||||||
|
|
||||||
await IOUtils.remove(themePath, { recursive: true, ignoreAbsent: true });
|
|
||||||
|
|
||||||
const themes = await ZenThemesCommon.getThemes();
|
|
||||||
delete themes[themeId];
|
|
||||||
await IOUtils.writeJSON(ZenThemesCommon.themesDataFile, themes);
|
|
||||||
|
|
||||||
this.triggerThemeUpdate();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async disableTheme(themeId) {
|
async disableMod(modId) {
|
||||||
const themes = await ZenThemesCommon.getThemes();
|
await gZenMods.disableMod(modId);
|
||||||
const theme = themes[themeId];
|
|
||||||
|
|
||||||
console.log(`[ZenThemeMarketplaceParent:settings]: Disabling theme ${theme.name}`);
|
this._doNotRebuildModsList = true;
|
||||||
|
gZenMods.triggerModsUpdate();
|
||||||
theme.enabled = false;
|
|
||||||
|
|
||||||
await IOUtils.writeJSON(ZenThemesCommon.themesDataFile, themes);
|
|
||||||
this._doNotRebuildThemesList = true;
|
|
||||||
this.triggerThemeUpdate();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async enableTheme(themeId) {
|
async enableMod(modId) {
|
||||||
const themes = await ZenThemesCommon.getThemes();
|
await gZenMods.enableMod(modId);
|
||||||
const theme = themes[themeId];
|
|
||||||
|
|
||||||
console.log(`[ZenThemeMarketplaceParent:settings]: Enabling theme ${theme.name}`);
|
this._doNotRebuildModsList = true;
|
||||||
|
gZenMods.triggerModsUpdate();
|
||||||
theme.enabled = true;
|
|
||||||
|
|
||||||
await IOUtils.writeJSON(ZenThemesCommon.themesDataFile, themes);
|
|
||||||
this._doNotRebuildThemesList = true;
|
|
||||||
this.triggerThemeUpdate();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_triggerBuildUpdateWithoutRebuild() {
|
_triggerBuildUpdateWithoutRebuild() {
|
||||||
this._doNotRebuildThemesList = true;
|
this._doNotRebuildModsList = true;
|
||||||
this.triggerThemeUpdate();
|
gZenMods.triggerModsUpdate();
|
||||||
},
|
},
|
||||||
|
|
||||||
async _importThemes() {
|
async _importThemes() {
|
||||||
const errorBox = document.getElementById('zenThemeMarketplaceImportFailure');
|
const errorBox = document.getElementById('zenThemeMarketplaceImportFailure');
|
||||||
const successBox = document.getElementById('zenThemeMarketplaceImportSuccess');
|
const successBox = document.getElementById('zenThemeMarketplaceImportSuccess');
|
||||||
|
|
||||||
successBox.hidden = true;
|
successBox.hidden = true;
|
||||||
errorBox.hidden = true;
|
errorBox.hidden = true;
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
|
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.accept = '.json';
|
input.accept = '.json';
|
||||||
input.style.display = 'none';
|
input.style.display = 'none';
|
||||||
@@ -191,37 +172,52 @@ var gZenMarketplaceManager = {
|
|||||||
input.setAttribute('accept', '.json');
|
input.setAttribute('accept', '.json');
|
||||||
|
|
||||||
let timeout;
|
let timeout;
|
||||||
|
|
||||||
const filePromise = new Promise((resolve) => {
|
const filePromise = new Promise((resolve) => {
|
||||||
input.addEventListener('change', (event) => {
|
input.addEventListener('change', (event) => {
|
||||||
if (timeout) clearTimeout(timeout);
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
resolve(file);
|
resolve(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
console.warn('[ZenThemeMarketplaceParent:settings]: Import timeout reached, aborting.');
|
console.warn('[ZenSettings:ZenMods]: Import timeout reached, aborting.');
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}, 60000);
|
}, 60000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
input.addEventListener('cancel', () => {
|
||||||
|
console.warn('[ZenSettings:ZenMods]: Import cancelled by user.');
|
||||||
|
clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
input.click();
|
input.click();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = await filePromise;
|
const file = await filePromise;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await file.text();
|
const content = await file.text();
|
||||||
|
|
||||||
const themes = JSON.parse(content);
|
const mods = JSON.parse(content);
|
||||||
for (const theme of Object.values(themes)) {
|
|
||||||
theme.themeId = theme.id;
|
for (const mod of Object.values(mods)) {
|
||||||
window.ZenInstallTheme(theme);
|
mod.modId = mod.id;
|
||||||
|
window.ZenInstallMod(mod);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ZenThemeMarketplaceParent:settings]: Error while importing themes:', error);
|
console.error('[ZenSettings:ZenMods]: Error while importing mods:', error);
|
||||||
errorBox.hidden = false;
|
errorBox.hidden = false;
|
||||||
} finally {
|
}
|
||||||
if (input) input.remove();
|
|
||||||
|
if (input) {
|
||||||
|
input.remove();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -232,51 +228,54 @@ var gZenMarketplaceManager = {
|
|||||||
successBox.hidden = true;
|
successBox.hidden = true;
|
||||||
errorBox.hidden = true;
|
errorBox.hidden = true;
|
||||||
|
|
||||||
let a, url;
|
let temporalAnchor, temporalUrl;
|
||||||
try {
|
try {
|
||||||
const themes = await ZenThemesCommon.getThemes();
|
const mods = await gZenMods.getMods();
|
||||||
const themesJson = JSON.stringify(themes, null, 2);
|
const modsJson = JSON.stringify(mods, null, 2);
|
||||||
const blob = new Blob([themesJson], { type: 'application/json' });
|
const blob = new Blob([modsJson], { type: 'application/json' });
|
||||||
url = URL.createObjectURL(blob);
|
|
||||||
// Creating a link to download the JSON file
|
temporalUrl = URL.createObjectURL(blob);
|
||||||
a = document.createElement('a');
|
// Creating a link to download the JSON file
|
||||||
a.href = url;
|
temporalAnchor = document.createElement('a');
|
||||||
a.download = 'zen-themes-export.json';
|
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;
|
successBox.hidden = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ZenThemeMarketplaceParent:settings]: Error while exporting themes:', error);
|
console.error('[ZenSettings:ZenMods]: Error while exporting mods:', error);
|
||||||
errorBox.hidden = false;
|
errorBox.hidden = false;
|
||||||
} finally {
|
|
||||||
if (a) {
|
|
||||||
a.remove();
|
|
||||||
}
|
}
|
||||||
if (url) {
|
|
||||||
URL.revokeObjectURL(url);
|
if (temporalAnchor) {
|
||||||
|
temporalAnchor.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (temporalUrl) {
|
||||||
|
URL.revokeObjectURL(temporalUrl);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async _buildThemesList() {
|
async _buildModsList() {
|
||||||
if (!this.themesList) {
|
if (!this.modsList) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._doNotRebuildThemesList) {
|
if (this._doNotRebuildModsList) {
|
||||||
this._doNotRebuildThemesList = false;
|
this._doNotRebuildModsList = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const themes = await ZenThemesCommon.getThemes();
|
const mods = await gZenMods.getMods();
|
||||||
const browser = ZenMultiWindowFeature.currentBrowser;
|
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))) {
|
for (const mod of Object.values(mods).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||||
const sanitizedName = `theme-${theme.name?.replaceAll(/\s/g, '-')?.replaceAll(/[^A-Za-z_-]+/g, '')}`;
|
const sanitizedName = gZenMods.sanitizeModName(mod.name);
|
||||||
const isThemeEnabled = theme.enabled === undefined || theme.enabled;
|
const isModEnabled = mod.enabled === undefined || mod.enabled;
|
||||||
const fragment = window.MozXULElement.parseXULToFragment(`
|
const fragment = window.MozXULElement.parseXULToFragment(`
|
||||||
<vbox class="zenThemeMarketplaceItem">
|
<vbox class="zenThemeMarketplaceItem">
|
||||||
<vbox class="zenThemeMarketplaceItemContent">
|
<vbox class="zenThemeMarketplaceItemContent">
|
||||||
@@ -286,14 +285,14 @@ var gZenMarketplaceManager = {
|
|||||||
<description class="description-deemphasized zenThemeMarketplaceItemDescription"></description>
|
<description class="description-deemphasized zenThemeMarketplaceItemDescription"></description>
|
||||||
</vbox>
|
</vbox>
|
||||||
<hbox class="zenThemeMarketplaceItemActions">
|
<hbox class="zenThemeMarketplaceItemActions">
|
||||||
${theme.preferences ? `<button id="zenThemeMarketplaceItemConfigureButton-${sanitizedName}" class="zenThemeMarketplaceItemConfigureButton" hidden="true"></button>` : ''}
|
${mod.preferences ? `<button id="zenThemeMarketplaceItemConfigureButton-${sanitizedName}" class="zenThemeMarketplaceItemConfigureButton" hidden="true"></button>` : ''}
|
||||||
${theme.homepage ? `<button id="zenThemeMarketplaceItemHomePageLink-${sanitizedName}" class="zenThemeMarketplaceItemHomepageButton" zen-theme-id="${theme.id}"></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-theme-id="${theme.id}"></button>
|
<button class="zenThemeMarketplaceItemUninstallButton" data-l10n-id="zen-theme-marketplace-remove-button" zen-mod-id="${mod.id}"></button>
|
||||||
</hbox>
|
</hbox>
|
||||||
</vbox>
|
</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 base = fragment.querySelector('.zenThemeMarketplaceItem');
|
||||||
const baseHeader = fragment.querySelector('#zenThemeMarketplaceItemContentHeader');
|
const baseHeader = fragment.querySelector('#zenThemeMarketplaceItemContentHeader');
|
||||||
@@ -308,7 +307,7 @@ var gZenMarketplaceManager = {
|
|||||||
|
|
||||||
mainDialogDiv.className = 'zenThemeMarketplaceItemPreferenceDialog';
|
mainDialogDiv.className = 'zenThemeMarketplaceItemPreferenceDialog';
|
||||||
headerDiv.className = 'zenThemeMarketplaceItemPreferenceDialogTopBar';
|
headerDiv.className = 'zenThemeMarketplaceItemPreferenceDialogTopBar';
|
||||||
headerTitle.textContent = themeName;
|
headerTitle.textContent = modName;
|
||||||
browser.document.l10n.setAttributes(headerTitle, 'zen-theme-marketplace-theme-header-title', {
|
browser.document.l10n.setAttributes(headerTitle, 'zen-theme-marketplace-theme-header-title', {
|
||||||
name: sanitizedName,
|
name: sanitizedName,
|
||||||
});
|
});
|
||||||
@@ -319,10 +318,10 @@ var gZenMarketplaceManager = {
|
|||||||
contentDiv.className = 'zenThemeMarketplaceItemPreferenceDialogContent';
|
contentDiv.className = 'zenThemeMarketplaceItemPreferenceDialogContent';
|
||||||
mozToggle.className = 'zenThemeMarketplaceItemPreferenceToggle';
|
mozToggle.className = 'zenThemeMarketplaceItemPreferenceToggle';
|
||||||
|
|
||||||
mozToggle.pressed = isThemeEnabled;
|
mozToggle.pressed = isModEnabled;
|
||||||
browser.document.l10n.setAttributes(
|
browser.document.l10n.setAttributes(
|
||||||
mozToggle,
|
mozToggle,
|
||||||
`zen-theme-marketplace-toggle-${isThemeEnabled ? 'enabled' : 'disabled'}-button`
|
`zen-theme-marketplace-toggle-${isModEnabled ? 'enabled' : 'disabled'}-button`
|
||||||
);
|
);
|
||||||
|
|
||||||
baseHeader.appendChild(mozToggle);
|
baseHeader.appendChild(mozToggle);
|
||||||
@@ -340,34 +339,34 @@ var gZenMarketplaceManager = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mozToggle.addEventListener('toggle', async (event) => {
|
mozToggle.addEventListener('toggle', async (event) => {
|
||||||
const themeId = event.target
|
const modId = event.target
|
||||||
.closest('.zenThemeMarketplaceItem')
|
.closest('.zenThemeMarketplaceItem')
|
||||||
.querySelector('.zenThemeMarketplaceItemUninstallButton')
|
.querySelector('.zenThemeMarketplaceItemUninstallButton')
|
||||||
.getAttribute('zen-theme-id');
|
.getAttribute('zen-mod-id');
|
||||||
event.target.setAttribute('disabled', true);
|
event.target.setAttribute('disabled', true);
|
||||||
|
|
||||||
if (!event.target.hasAttribute('pressed')) {
|
if (!event.target.hasAttribute('pressed')) {
|
||||||
await this.disableTheme(themeId);
|
await this.disableMod(modId);
|
||||||
|
|
||||||
browser.document.l10n.setAttributes(
|
browser.document.l10n.setAttributes(
|
||||||
mozToggle,
|
mozToggle,
|
||||||
'zen-theme-marketplace-toggle-disabled-button'
|
'zen-theme-marketplace-toggle-disabled-button'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (theme.preferences) {
|
if (mod.preferences) {
|
||||||
document
|
document
|
||||||
.getElementById(`zenThemeMarketplaceItemConfigureButton-${sanitizedName}`)
|
.getElementById(`zenThemeMarketplaceItemConfigureButton-${sanitizedName}`)
|
||||||
.setAttribute('hidden', true);
|
.setAttribute('hidden', true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.enableTheme(themeId);
|
await this.enableMod(modId);
|
||||||
|
|
||||||
browser.document.l10n.setAttributes(
|
browser.document.l10n.setAttributes(
|
||||||
mozToggle,
|
mozToggle,
|
||||||
'zen-theme-marketplace-toggle-enabled-button'
|
'zen-theme-marketplace-toggle-enabled-button'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (theme.preferences) {
|
if (mod.preferences) {
|
||||||
document
|
document
|
||||||
.getElementById(`zenThemeMarketplaceItemConfigureButton-${sanitizedName}`)
|
.getElementById(`zenThemeMarketplaceItemConfigureButton-${sanitizedName}`)
|
||||||
.removeAttribute('hidden');
|
.removeAttribute('hidden');
|
||||||
@@ -379,8 +378,8 @@ var gZenMarketplaceManager = {
|
|||||||
}, 400);
|
}, 400);
|
||||||
});
|
});
|
||||||
|
|
||||||
fragment.querySelector('.zenThemeMarketplaceItemTitle').textContent = themeName;
|
fragment.querySelector('.zenThemeMarketplaceItemTitle').textContent = modName;
|
||||||
fragment.querySelector('.zenThemeMarketplaceItemDescription').textContent = theme.description;
|
fragment.querySelector('.zenThemeMarketplaceItemDescription').textContent = mod.description;
|
||||||
fragment
|
fragment
|
||||||
.querySelector('.zenThemeMarketplaceItemUninstallButton')
|
.querySelector('.zenThemeMarketplaceItemUninstallButton')
|
||||||
.addEventListener('click', async (event) => {
|
.addEventListener('click', async (event) => {
|
||||||
@@ -392,34 +391,34 @@ var gZenMarketplaceManager = {
|
|||||||
return;
|
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');
|
const homepageButton = fragment.querySelector('.zenThemeMarketplaceItemHomepageButton');
|
||||||
homepageButton.addEventListener('click', () => {
|
homepageButton.addEventListener('click', () => {
|
||||||
// open the homepage url in a new tab
|
// open the homepage url in a new tab
|
||||||
const url = theme.homepage;
|
const url = mod.homepage;
|
||||||
|
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme.preferences) {
|
if (mod.preferences) {
|
||||||
fragment
|
fragment
|
||||||
.querySelector('.zenThemeMarketplaceItemConfigureButton')
|
.querySelector('.zenThemeMarketplaceItemConfigureButton')
|
||||||
.addEventListener('click', () => {
|
.addEventListener('click', () => {
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isThemeEnabled) {
|
if (isModEnabled) {
|
||||||
fragment
|
fragment
|
||||||
.querySelector('.zenThemeMarketplaceItemConfigureButton')
|
.querySelector('.zenThemeMarketplaceItemConfigureButton')
|
||||||
.removeAttribute('hidden');
|
.removeAttribute('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferences = await ZenThemesCommon.getThemePreferences(theme);
|
const preferences = await gZenMods.getModPreferences(mod);
|
||||||
|
|
||||||
if (preferences.length > 0) {
|
if (preferences.length > 0) {
|
||||||
const preferencesWrapper = document.createXULElement('vbox');
|
const preferencesWrapper = document.createXULElement('vbox');
|
||||||
@@ -471,7 +470,7 @@ var gZenMarketplaceManager = {
|
|||||||
|
|
||||||
if (!['string', 'number'].includes(valueType)) {
|
if (!['string', 'number'].includes(valueType)) {
|
||||||
console.log(
|
console.log(
|
||||||
`[ZenThemeMarketplaceParent:settings]: Warning, invalid data type received (${valueType}), skipping.`
|
`[ZenSettings:ZenMods]: Warning, invalid data type received (${valueType}), skipping.`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -583,7 +582,7 @@ var gZenMarketplaceManager = {
|
|||||||
|
|
||||||
input.addEventListener(
|
input.addEventListener(
|
||||||
'change',
|
'change',
|
||||||
ZenThemesCommon.debounce((event) => {
|
gZenMods.debounce((event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
Services.prefs.setStringPref(property, value);
|
Services.prefs.setStringPref(property, value);
|
||||||
@@ -617,18 +616,18 @@ var gZenMarketplaceManager = {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
console.log(
|
console.log(
|
||||||
`[ZenThemeMarketplaceParent:settings]: Warning, unknown preference type received (${type}), skipping.`
|
`[ZenSettings:ZenMods]: Warning, unknown preference type received (${type}), skipping.`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentDiv.appendChild(preferencesWrapper);
|
contentDiv.appendChild(preferencesWrapper);
|
||||||
}
|
}
|
||||||
themeList.appendChild(fragment);
|
modList.appendChild(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.themesList.replaceChildren(...themeList.children);
|
this.modsList.replaceChildren(...modList.children);
|
||||||
themeList.remove();
|
modList.remove();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -636,6 +635,19 @@ const kZenExtendedSidebar = 'zen.view.sidebar-expanded';
|
|||||||
const kZenSingleToolbar = 'zen.view.use-single-toolbar';
|
const kZenSingleToolbar = 'zen.view.use-single-toolbar';
|
||||||
|
|
||||||
var gZenLooksAndFeel = {
|
var gZenLooksAndFeel = {
|
||||||
|
kZenColors: [
|
||||||
|
'#aac7ff',
|
||||||
|
'#74d7cb',
|
||||||
|
'#a0d490',
|
||||||
|
'#dec663',
|
||||||
|
'#ffb787',
|
||||||
|
'#dec1b1',
|
||||||
|
'#ffb1c0',
|
||||||
|
'#ddbfc3',
|
||||||
|
'#f6b0ea',
|
||||||
|
'#d4bbff',
|
||||||
|
],
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (this.__hasInitialized) return;
|
if (this.__hasInitialized) return;
|
||||||
this.__hasInitialized = true;
|
this.__hasInitialized = true;
|
||||||
@@ -737,7 +749,8 @@ var gZenLooksAndFeel = {
|
|||||||
_initializeColorPicker(accentColor) {
|
_initializeColorPicker(accentColor) {
|
||||||
let elem = document.getElementById('zenLooksAndFeelColorOptions');
|
let elem = document.getElementById('zenLooksAndFeelColorOptions');
|
||||||
elem.innerHTML = '';
|
elem.innerHTML = '';
|
||||||
for (let color of ZenThemesCommon.kZenColors) {
|
|
||||||
|
for (let color of this.kZenColors) {
|
||||||
let colorElemParen = document.createElement('div');
|
let colorElemParen = document.createElement('div');
|
||||||
let colorElem = document.createElement('div');
|
let colorElem = document.createElement('div');
|
||||||
colorElemParen.classList.add('zenLooksAndFeelColorOptionParen');
|
colorElemParen.classList.add('zenLooksAndFeelColorOptionParen');
|
||||||
@@ -761,7 +774,7 @@ var gZenLooksAndFeel = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_getInitialAccentColor() {
|
_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',
|
type: 'bool',
|
||||||
default: true,
|
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/ZenCommonUtils.mjs" defer=""/>
|
||||||
<script src="chrome://browser/content/zen-components/ZenThemesCommon.mjs" defer=""/>
|
|
||||||
<html:template id="template-paneZenMarketplace">
|
<html:template id="template-paneZenMarketplace">
|
||||||
<hbox id="ZenMarketplaceCategory"
|
<hbox id="ZenMarketplaceCategory"
|
||||||
class="subcategory"
|
class="subcategory"
|
||||||
@@ -21,6 +20,10 @@
|
|||||||
<button id="zenThemeMarketplaceCheckForUpdates" data-l10n-id="zen-theme-marketplace-check-for-updates-button" />
|
<button id="zenThemeMarketplaceCheckForUpdates" data-l10n-id="zen-theme-marketplace-check-for-updates-button" />
|
||||||
</hbox>
|
</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-success" hidden="true" id="zenThemeMarketplaceUpdatesSuccess" />
|
||||||
<description class="description-deemphasized" data-l10n-id="zen-theme-marketplace-updates-failure" hidden="true" id="zenThemeMarketplaceUpdatesFailure" />
|
<description class="description-deemphasized" data-l10n-id="zen-theme-marketplace-updates-failure" hidden="true" id="zenThemeMarketplaceUpdatesFailure" />
|
||||||
|
|
||||||
|
@@ -5,16 +5,28 @@
|
|||||||
|
|
||||||
var gZenActorsManager = {
|
var gZenActorsManager = {
|
||||||
_actors: new Set(),
|
_actors: new Set(),
|
||||||
|
_lazy: {},
|
||||||
|
|
||||||
addJSWindowActor(...args) {
|
init() {
|
||||||
if (this._actors.has(args[0])) {
|
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
|
// Actor already registered, nothing to do
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decl = {};
|
||||||
|
decl[name] = data;
|
||||||
try {
|
try {
|
||||||
ChromeUtils.registerWindowActor(...args);
|
this._lazy.ActorManagerParent.addJSWindowActors(decl);
|
||||||
this._actors.add(args[0]);
|
this._actors.add(name);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Failed to register JSWindowActor: ${e}`);
|
console.warn(`Failed to register JSWindowActor: ${e}`);
|
||||||
}
|
}
|
||||||
|
@@ -742,13 +742,12 @@
|
|||||||
window.gZenGlanceManager = new ZenGlanceManager();
|
window.gZenGlanceManager = new ZenGlanceManager();
|
||||||
|
|
||||||
function registerWindowActors() {
|
function registerWindowActors() {
|
||||||
if (Services.prefs.getBoolPref('zen.glance.enabled', true)) {
|
|
||||||
gZenActorsManager.addJSWindowActor('ZenGlance', {
|
gZenActorsManager.addJSWindowActor('ZenGlance', {
|
||||||
parent: {
|
parent: {
|
||||||
esModuleURI: 'chrome://browser/content/zen-components/actors/ZenGlanceParent.sys.mjs',
|
esModuleURI: 'resource:///actors/ZenGlanceParent.sys.mjs',
|
||||||
},
|
},
|
||||||
child: {
|
child: {
|
||||||
esModuleURI: 'chrome://browser/content/zen-components/actors/ZenGlanceChild.sys.mjs',
|
esModuleURI: 'resource:///actors/ZenGlanceChild.sys.mjs',
|
||||||
events: {
|
events: {
|
||||||
DOMContentLoaded: {},
|
DOMContentLoaded: {},
|
||||||
keydown: {
|
keydown: {
|
||||||
@@ -758,9 +757,9 @@
|
|||||||
},
|
},
|
||||||
allFrames: true,
|
allFrames: true,
|
||||||
matches: ['*://*/*'],
|
matches: ['*://*/*'],
|
||||||
|
enablePreference: 'zen.glance.enabled',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
registerWindowActors();
|
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
|
# 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/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
|
||||||
FINAL_TARGET_FILES.actors += [
|
FINAL_TARGET_FILES.actors += [
|
||||||
"actors/ZenThemeMarketplaceChild.sys.mjs",
|
"actors/ZenModsMarketplaceChild.sys.mjs",
|
||||||
"actors/ZenThemeMarketplaceParent.sys.mjs",
|
"actors/ZenModsMarketplaceParent.sys.mjs",
|
||||||
]
|
]
|
||||||
|
Reference in New Issue
Block a user