mirror of
https://github.com/zen-browser/desktop.git
synced 2025-07-07 16:05:31 +02:00
1184 lines
39 KiB
JavaScript
1184 lines
39 KiB
JavaScript
// 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 gZenMarketplaceManager = {
|
|
async init() {
|
|
const checkForUpdates = document.getElementById('zenThemeMarketplaceCheckForUpdates');
|
|
const header = document.getElementById('zenMarketplaceHeader');
|
|
|
|
if (!checkForUpdates || !header) {
|
|
return; // We haven't entered the settings page yet.
|
|
}
|
|
|
|
if (this.__hasInitializedEvents) {
|
|
return;
|
|
}
|
|
|
|
if (!window.gZenMods) {
|
|
window.gZenMods = ZenMultiWindowFeature.currentBrowser.gZenMods;
|
|
}
|
|
|
|
header.appendChild(this._initDisableAll());
|
|
|
|
this._initImportExport();
|
|
|
|
this.__hasInitializedEvents = true;
|
|
|
|
await this._buildModsList();
|
|
|
|
Services.prefs.addObserver(gZenMods.updatePref, this);
|
|
|
|
const checkForUpdateClick = (event) => {
|
|
if (event.target === checkForUpdates) {
|
|
event.preventDefault();
|
|
|
|
this._checkForThemeUpdates(event);
|
|
}
|
|
};
|
|
|
|
checkForUpdates.addEventListener('click', checkForUpdateClick);
|
|
|
|
document.addEventListener('ZenModsMarketplace:CheckForUpdatesFinished', (event) => {
|
|
checkForUpdates.disabled = false;
|
|
|
|
const updates = event.detail.updates;
|
|
const success = document.getElementById('zenThemeMarketplaceUpdatesSuccess');
|
|
const error = document.getElementById('zenThemeMarketplaceUpdatesFailure');
|
|
|
|
if (updates) {
|
|
success.hidden = false;
|
|
error.hidden = true;
|
|
} else {
|
|
success.hidden = true;
|
|
error.hidden = false;
|
|
}
|
|
});
|
|
|
|
window.addEventListener('unload', () => {
|
|
Services.prefs.removeObserver(gZenMods.updatePref, this);
|
|
this.__hasInitializedEvents = false;
|
|
|
|
document.removeEventListener('ZenModsMarketplace:CheckForUpdatesFinished', this);
|
|
document.removeEventListener('ZenCheckForModUpdates', this);
|
|
|
|
checkForUpdates.removeEventListener('click', checkForUpdateClick);
|
|
|
|
this.modsList.innerHTML = '';
|
|
this._doNotRebuildModsList = false;
|
|
});
|
|
},
|
|
|
|
_initImportExport() {
|
|
const importButton = document.getElementById('zenThemeMarketplaceImport');
|
|
const exportButton = document.getElementById('zenThemeMarketplaceExport');
|
|
|
|
if (importButton) {
|
|
importButton.addEventListener('click', this._importThemes.bind(this));
|
|
}
|
|
|
|
if (exportButton) {
|
|
exportButton.addEventListener('click', this._exportThemes.bind(this));
|
|
}
|
|
},
|
|
|
|
_initDisableAll() {
|
|
const areModsDisabled = Services.prefs.getBoolPref('zen.themes.disable-all', false);
|
|
const browser = ZenMultiWindowFeature.currentBrowser;
|
|
const mozToggle = document.createElement('moz-toggle');
|
|
|
|
mozToggle.className =
|
|
'zenThemeMarketplaceItemPreferenceToggle zenThemeMarketplaceDisableAllToggle';
|
|
mozToggle.pressed = !areModsDisabled;
|
|
|
|
browser.document.l10n.setAttributes(
|
|
mozToggle,
|
|
`zen-theme-disable-all-${!areModsDisabled ? 'enabled' : 'disabled'}`
|
|
);
|
|
|
|
mozToggle.addEventListener('toggle', async (event) => {
|
|
const { pressed = false } = event.target || {};
|
|
|
|
this.modsList.style.display = pressed ? '' : 'none';
|
|
Services.prefs.setBoolPref('zen.themes.disable-all', !pressed);
|
|
browser.document.l10n.setAttributes(
|
|
mozToggle,
|
|
`zen-theme-disable-all-${pressed ? 'enabled' : 'disabled'}`
|
|
);
|
|
});
|
|
|
|
if (areModsDisabled) {
|
|
this.modsList.style.display = 'none';
|
|
}
|
|
|
|
return mozToggle;
|
|
},
|
|
|
|
async observe() {
|
|
await this._buildModsList();
|
|
},
|
|
|
|
_checkForThemeUpdates(event) {
|
|
// Send a message to the child to check for theme updates.
|
|
event.target.disabled = true;
|
|
// send an event that will be listened by the child process.
|
|
document.dispatchEvent(new CustomEvent('ZenCheckForModUpdates'));
|
|
},
|
|
|
|
get modsList() {
|
|
if (!this._modsList) {
|
|
this._modsList = document.getElementById('zenThemeMarketplaceList');
|
|
}
|
|
return this._modsList;
|
|
},
|
|
|
|
_triggerBuildUpdateWithoutRebuild() {
|
|
this._doNotRebuildModsList = true;
|
|
gZenMods.triggerModsUpdate();
|
|
},
|
|
|
|
async removeMod(modId) {
|
|
await gZenMods.removeMod(modId);
|
|
|
|
gZenMods.triggerModsUpdate();
|
|
},
|
|
|
|
async disableMod(modId) {
|
|
await gZenMods.disableMod(modId);
|
|
|
|
this._triggerBuildUpdateWithoutRebuild();
|
|
},
|
|
|
|
async enableMod(modId) {
|
|
await gZenMods.enableMod(modId);
|
|
|
|
this._triggerBuildUpdateWithoutRebuild();
|
|
},
|
|
|
|
async _importThemes() {
|
|
const errorBox = document.getElementById('zenThemeMarketplaceImportFailure');
|
|
const successBox = document.getElementById('zenThemeMarketplaceImportSuccess');
|
|
|
|
successBox.hidden = true;
|
|
errorBox.hidden = true;
|
|
|
|
const input = document.createElement('input');
|
|
|
|
input.type = 'file';
|
|
input.accept = '.json';
|
|
input.style.display = 'none';
|
|
input.setAttribute('moz-accept', '.json');
|
|
input.setAttribute('accept', '.json');
|
|
|
|
let timeout;
|
|
|
|
const filePromise = new Promise((resolve) => {
|
|
input.addEventListener('change', (event) => {
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
|
|
const file = event.target.files[0];
|
|
resolve(file);
|
|
});
|
|
|
|
timeout = setTimeout(() => {
|
|
console.warn('[ZenSettings:ZenMods]: Import timeout reached, aborting.');
|
|
resolve(null);
|
|
}, 60000);
|
|
});
|
|
|
|
input.addEventListener('cancel', () => {
|
|
console.warn('[ZenSettings:ZenMods]: Import cancelled by user.');
|
|
clearTimeout(timeout);
|
|
});
|
|
|
|
input.click();
|
|
|
|
try {
|
|
const file = await filePromise;
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
const content = await file.text();
|
|
|
|
const mods = JSON.parse(content);
|
|
|
|
for (const mod of Object.values(mods)) {
|
|
mod.modId = mod.id;
|
|
await window.ZenInstallMod(mod);
|
|
}
|
|
} catch (error) {
|
|
console.error('[ZenSettings:ZenMods]: Error while importing mods:', error);
|
|
errorBox.hidden = false;
|
|
}
|
|
|
|
if (input) {
|
|
input.remove();
|
|
}
|
|
},
|
|
|
|
async _exportThemes() {
|
|
const errorBox = document.getElementById('zenThemeMarketplaceExportFailure');
|
|
const successBox = document.getElementById('zenThemeMarketplaceExportSuccess');
|
|
|
|
successBox.hidden = true;
|
|
errorBox.hidden = true;
|
|
|
|
let temporalAnchor, temporalUrl;
|
|
try {
|
|
const mods = await gZenMods.getMods();
|
|
const modsJson = JSON.stringify(mods, null, 2);
|
|
const blob = new Blob([modsJson], { type: 'application/json' });
|
|
|
|
temporalUrl = URL.createObjectURL(blob);
|
|
// Creating a link to download the JSON file
|
|
temporalAnchor = document.createElement('a');
|
|
temporalAnchor.href = temporalUrl;
|
|
temporalAnchor.download = 'zen-mods-export.json';
|
|
|
|
document.body.appendChild(temporalAnchor);
|
|
temporalAnchor.click();
|
|
temporalAnchor.remove();
|
|
|
|
successBox.hidden = false;
|
|
} catch (error) {
|
|
console.error('[ZenSettings:ZenMods]: Error while exporting mods:', error);
|
|
errorBox.hidden = false;
|
|
}
|
|
|
|
if (temporalAnchor) {
|
|
temporalAnchor.remove();
|
|
}
|
|
|
|
if (temporalUrl) {
|
|
URL.revokeObjectURL(temporalUrl);
|
|
}
|
|
},
|
|
|
|
async _buildModsList() {
|
|
if (!this.modsList) {
|
|
return;
|
|
}
|
|
|
|
if (this._doNotRebuildModsList) {
|
|
this._doNotRebuildModsList = false;
|
|
return;
|
|
}
|
|
|
|
const mods = await gZenMods.getMods();
|
|
const browser = ZenMultiWindowFeature.currentBrowser;
|
|
const modList = document.createElement('div');
|
|
|
|
for (const mod of Object.values(mods).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
const sanitizedName = gZenMods.sanitizeModName(mod.name);
|
|
const isModEnabled = mod.enabled === undefined || mod.enabled;
|
|
const fragment = window.MozXULElement.parseXULToFragment(`
|
|
<vbox class="zenThemeMarketplaceItem">
|
|
<vbox class="zenThemeMarketplaceItemContent">
|
|
<hbox flex="1" id="zenThemeMarketplaceItemContentHeader">
|
|
<label><h3 class="zenThemeMarketplaceItemTitle"></h3></label>
|
|
</hbox>
|
|
<description class="description-deemphasized zenThemeMarketplaceItemDescription"></description>
|
|
</vbox>
|
|
<hbox class="zenThemeMarketplaceItemActions">
|
|
${mod.preferences ? `<button id="zenThemeMarketplaceItemConfigureButton-${sanitizedName}" class="zenThemeMarketplaceItemConfigureButton" hidden="true"></button>` : ''}
|
|
${mod.homepage ? `<button id="zenThemeMarketplaceItemHomePageLink-${sanitizedName}" class="zenThemeMarketplaceItemHomepageButton" zen-mod-id="${mod.id}"></button>` : ''}
|
|
<button class="zenThemeMarketplaceItemUninstallButton" data-l10n-id="zen-theme-marketplace-remove-button" zen-mod-id="${mod.id}"></button>
|
|
</hbox>
|
|
</vbox>
|
|
`);
|
|
|
|
const modName = `${mod.name} (v${mod.version ?? '1.0.0'})`;
|
|
|
|
const base = fragment.querySelector('.zenThemeMarketplaceItem');
|
|
const baseHeader = fragment.querySelector('#zenThemeMarketplaceItemContentHeader');
|
|
|
|
const dialog = document.createElement('dialog');
|
|
const mainDialogDiv = document.createElement('div');
|
|
const headerDiv = document.createElement('div');
|
|
const headerTitle = document.createElement('h3');
|
|
const closeButton = document.createElement('button');
|
|
const contentDiv = document.createElement('div');
|
|
const mozToggle = document.createElement('moz-toggle');
|
|
|
|
mainDialogDiv.className = 'zenThemeMarketplaceItemPreferenceDialog';
|
|
headerDiv.className = 'zenThemeMarketplaceItemPreferenceDialogTopBar';
|
|
headerTitle.textContent = modName;
|
|
browser.document.l10n.setAttributes(headerTitle, 'zen-theme-marketplace-theme-header-title', {
|
|
name: sanitizedName,
|
|
});
|
|
headerTitle.className = 'zenThemeMarketplaceItemTitle';
|
|
closeButton.id = `${sanitizedName}-modal-close`;
|
|
browser.document.l10n.setAttributes(closeButton, 'zen-theme-marketplace-close-modal');
|
|
contentDiv.id = `${sanitizedName}-preferences-content`;
|
|
contentDiv.className = 'zenThemeMarketplaceItemPreferenceDialogContent';
|
|
mozToggle.className = 'zenThemeMarketplaceItemPreferenceToggle';
|
|
|
|
mozToggle.pressed = isModEnabled;
|
|
browser.document.l10n.setAttributes(
|
|
mozToggle,
|
|
`zen-theme-marketplace-toggle-${isModEnabled ? 'enabled' : 'disabled'}-button`
|
|
);
|
|
|
|
baseHeader.appendChild(mozToggle);
|
|
|
|
headerDiv.appendChild(headerTitle);
|
|
headerDiv.appendChild(closeButton);
|
|
|
|
mainDialogDiv.appendChild(headerDiv);
|
|
mainDialogDiv.appendChild(contentDiv);
|
|
dialog.appendChild(mainDialogDiv);
|
|
base.appendChild(dialog);
|
|
|
|
closeButton.addEventListener('click', () => {
|
|
dialog.close();
|
|
});
|
|
|
|
mozToggle.addEventListener('toggle', async (event) => {
|
|
const modId = event.target
|
|
.closest('.zenThemeMarketplaceItem')
|
|
.querySelector('.zenThemeMarketplaceItemUninstallButton')
|
|
.getAttribute('zen-mod-id');
|
|
event.target.setAttribute('disabled', true);
|
|
|
|
if (!event.target.hasAttribute('pressed')) {
|
|
await this.disableMod(modId);
|
|
|
|
browser.document.l10n.setAttributes(
|
|
mozToggle,
|
|
'zen-theme-marketplace-toggle-disabled-button'
|
|
);
|
|
|
|
if (mod.preferences) {
|
|
document
|
|
.getElementById(`zenThemeMarketplaceItemConfigureButton-${sanitizedName}`)
|
|
.setAttribute('hidden', true);
|
|
}
|
|
} else {
|
|
await this.enableMod(modId);
|
|
|
|
browser.document.l10n.setAttributes(
|
|
mozToggle,
|
|
'zen-theme-marketplace-toggle-enabled-button'
|
|
);
|
|
|
|
if (mod.preferences) {
|
|
document
|
|
.getElementById(`zenThemeMarketplaceItemConfigureButton-${sanitizedName}`)
|
|
.removeAttribute('hidden');
|
|
}
|
|
}
|
|
setTimeout(() => {
|
|
// We use a timeout to make sure the theme list has been updated before re-enabling the button.
|
|
event.target.removeAttribute('disabled');
|
|
}, 400);
|
|
});
|
|
|
|
fragment.querySelector('.zenThemeMarketplaceItemTitle').textContent = modName;
|
|
fragment.querySelector('.zenThemeMarketplaceItemDescription').textContent = mod.description;
|
|
fragment
|
|
.querySelector('.zenThemeMarketplaceItemUninstallButton')
|
|
.addEventListener('click', async (event) => {
|
|
const [msg] = await document.l10n.formatValues([
|
|
{ id: 'zen-theme-marketplace-remove-confirmation' },
|
|
]);
|
|
|
|
if (!confirm(msg)) {
|
|
return;
|
|
}
|
|
|
|
await this.removeMod(event.target.getAttribute('zen-mod-id'));
|
|
});
|
|
|
|
if (mod.homepage) {
|
|
const homepageButton = fragment.querySelector('.zenThemeMarketplaceItemHomepageButton');
|
|
homepageButton.addEventListener('click', () => {
|
|
// open the homepage url in a new tab
|
|
const url = mod.homepage;
|
|
|
|
window.open(url, '_blank');
|
|
});
|
|
}
|
|
|
|
if (mod.preferences) {
|
|
fragment
|
|
.querySelector('.zenThemeMarketplaceItemConfigureButton')
|
|
.addEventListener('click', () => {
|
|
dialog.showModal();
|
|
});
|
|
|
|
if (isModEnabled) {
|
|
fragment
|
|
.querySelector('.zenThemeMarketplaceItemConfigureButton')
|
|
.removeAttribute('hidden');
|
|
}
|
|
}
|
|
|
|
const preferences = await gZenMods.getModPreferences(mod);
|
|
|
|
if (preferences.length > 0) {
|
|
const preferencesWrapper = document.createXULElement('vbox');
|
|
|
|
preferencesWrapper.setAttribute('flex', '1');
|
|
|
|
for (const entry of preferences) {
|
|
const { property, label, type, placeholder, defaultValue } = entry;
|
|
|
|
switch (type) {
|
|
case 'dropdown': {
|
|
const { options } = entry;
|
|
|
|
const container = document.createXULElement('hbox');
|
|
container.classList.add('zenThemeMarketplaceItemPreference');
|
|
container.setAttribute('align', 'center');
|
|
container.setAttribute('role', 'group');
|
|
|
|
const menulist = document.createXULElement('menulist');
|
|
const menupopup = document.createXULElement('menupopup');
|
|
|
|
menulist.setAttribute('sizetopopup', 'none');
|
|
menulist.setAttribute('id', property + '-popup-menulist');
|
|
|
|
const savedValue = Services.prefs.getStringPref(property, defaultValue ?? 'none');
|
|
|
|
menulist.setAttribute('value', savedValue);
|
|
menulist.setAttribute('tooltiptext', property);
|
|
|
|
const defaultItem = document.createXULElement('menuitem');
|
|
|
|
defaultItem.setAttribute('value', 'none');
|
|
|
|
if (placeholder) {
|
|
defaultItem.setAttribute('label', placeholder || '-');
|
|
} else {
|
|
browser.document.l10n.setAttributes(
|
|
defaultItem,
|
|
'zen-theme-marketplace-dropdown-default-label'
|
|
);
|
|
}
|
|
|
|
menupopup.appendChild(defaultItem);
|
|
|
|
for (const option of options) {
|
|
const { label, value } = option;
|
|
|
|
const valueType = typeof value;
|
|
|
|
if (!['string', 'number'].includes(valueType)) {
|
|
console.log(
|
|
`[ZenSettings:ZenMods]: Warning, invalid data type received (${valueType}), skipping.`
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const menuitem = document.createXULElement('menuitem');
|
|
|
|
menuitem.setAttribute('value', value.toString());
|
|
menuitem.setAttribute('label', label);
|
|
|
|
menupopup.appendChild(menuitem);
|
|
}
|
|
|
|
menulist.appendChild(menupopup);
|
|
|
|
menulist.addEventListener('command', () => {
|
|
const value = menulist.selectedItem.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(property?.replaceAll(/\./g, '-'), value);
|
|
|
|
Services.prefs.setStringPref(property, value === 'none' ? '' : value);
|
|
this._triggerBuildUpdateWithoutRebuild();
|
|
});
|
|
|
|
const nameLabel = document.createXULElement('label');
|
|
nameLabel.setAttribute('flex', '1');
|
|
nameLabel.setAttribute('class', 'zenThemeMarketplaceItemPreferenceLabel');
|
|
nameLabel.setAttribute('value', label);
|
|
nameLabel.setAttribute('tooltiptext', property);
|
|
|
|
container.appendChild(nameLabel);
|
|
container.appendChild(menulist);
|
|
container.setAttribute('aria-labelledby', label);
|
|
|
|
preferencesWrapper.appendChild(container);
|
|
break;
|
|
}
|
|
|
|
case 'checkbox': {
|
|
const checkbox = window.MozXULElement.parseXULToFragment(`
|
|
<hbox class="zenThemeMarketplaceItemPreference">
|
|
<checkbox class="zenThemeMarketplaceItemPreferenceCheckbox"></checkbox>
|
|
</hbox>
|
|
`);
|
|
|
|
const checkboxElement = checkbox.querySelector(
|
|
'.zenThemeMarketplaceItemPreferenceCheckbox'
|
|
);
|
|
checkboxElement.setAttribute('label', label);
|
|
checkboxElement.setAttribute('tooltiptext', property);
|
|
checkboxElement.setAttribute('zen-pref', property);
|
|
|
|
// Checkbox only works with "true" and "false" values, it's not like HTML checkboxes.
|
|
if (Services.prefs.getBoolPref(property, defaultValue ?? false)) {
|
|
checkboxElement.setAttribute('checked', 'true');
|
|
}
|
|
|
|
checkboxElement.addEventListener('click', (event) => {
|
|
const target = event.target.closest('.zenThemeMarketplaceItemPreferenceCheckbox');
|
|
const key = target.getAttribute('zen-pref');
|
|
const checked = target.hasAttribute('checked');
|
|
|
|
if (!checked) {
|
|
target.removeAttribute('checked');
|
|
} else {
|
|
target.setAttribute('checked', 'true');
|
|
}
|
|
|
|
Services.prefs.setBoolPref(key, !checked);
|
|
});
|
|
|
|
preferencesWrapper.appendChild(checkbox);
|
|
break;
|
|
}
|
|
|
|
case 'string': {
|
|
const container = document.createXULElement('hbox');
|
|
container.classList.add('zenThemeMarketplaceItemPreference');
|
|
container.setAttribute('align', 'center');
|
|
container.setAttribute('role', 'group');
|
|
|
|
const savedValue = Services.prefs.getStringPref(property, defaultValue ?? '');
|
|
const sanitizedProperty = property?.replaceAll(/\./g, '-');
|
|
|
|
const input = document.createElement('input');
|
|
input.setAttribute('flex', '1');
|
|
input.setAttribute('type', 'text');
|
|
input.id = `${sanitizedProperty}-input`;
|
|
input.value = savedValue;
|
|
|
|
if (placeholder) {
|
|
input.setAttribute('placeholder', placeholder || '-');
|
|
} else {
|
|
browser.document.l10n.setAttributes(
|
|
input,
|
|
'zen-theme-marketplace-input-default-placeholder'
|
|
);
|
|
}
|
|
|
|
input.addEventListener(
|
|
'change',
|
|
gZenMods.debounce((event) => {
|
|
const value = event.target.value;
|
|
|
|
Services.prefs.setStringPref(property, value);
|
|
this._triggerBuildUpdateWithoutRebuild();
|
|
|
|
if (value === '') {
|
|
browser.document
|
|
.querySelector(':root')
|
|
.style.removeProperty(`--${sanitizedProperty}`);
|
|
} else {
|
|
browser.document
|
|
.querySelector(':root')
|
|
.style.setProperty(`--${sanitizedProperty}`, value);
|
|
}
|
|
}, 500)
|
|
);
|
|
|
|
const nameLabel = document.createXULElement('label');
|
|
nameLabel.setAttribute('flex', '1');
|
|
nameLabel.setAttribute('class', 'zenThemeMarketplaceItemPreferenceLabel');
|
|
nameLabel.setAttribute('value', label);
|
|
nameLabel.setAttribute('tooltiptext', property);
|
|
|
|
container.appendChild(nameLabel);
|
|
container.appendChild(input);
|
|
container.setAttribute('aria-labelledby', label);
|
|
|
|
preferencesWrapper.appendChild(container);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
console.log(
|
|
`[ZenSettings:ZenMods]: Warning, unknown preference type received (${type}), skipping.`
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
contentDiv.appendChild(preferencesWrapper);
|
|
}
|
|
modList.appendChild(fragment);
|
|
}
|
|
|
|
this.modsList.replaceChildren(...modList.children);
|
|
modList.remove();
|
|
},
|
|
};
|
|
|
|
const kZenExtendedSidebar = 'zen.view.sidebar-expanded';
|
|
const kZenSingleToolbar = 'zen.view.use-single-toolbar';
|
|
|
|
var gZenLooksAndFeel = {
|
|
init() {
|
|
if (this.__hasInitialized) return;
|
|
this.__hasInitialized = true;
|
|
gZenMarketplaceManager.init();
|
|
for (const pref of [kZenExtendedSidebar, kZenSingleToolbar]) {
|
|
Services.prefs.addObserver(pref, this);
|
|
}
|
|
window.addEventListener('unload', () => {
|
|
for (const pref of [kZenExtendedSidebar, kZenSingleToolbar]) {
|
|
Services.prefs.removeObserver(pref, this);
|
|
}
|
|
});
|
|
this.setCompactModeStyle();
|
|
|
|
this.applySidebarLayout();
|
|
},
|
|
|
|
observe(subject, topic, data) {
|
|
this.applySidebarLayout();
|
|
},
|
|
|
|
applySidebarLayout() {
|
|
const isSingleToolbar = Services.prefs.getBoolPref(kZenSingleToolbar);
|
|
const isExtendedSidebar = Services.prefs.getBoolPref(kZenExtendedSidebar);
|
|
for (const layout of document.getElementById('zenLayoutList').children) {
|
|
layout.classList.remove('selected');
|
|
if (layout.getAttribute('layout') == 'single' && isSingleToolbar) {
|
|
layout.classList.add('selected');
|
|
} else if (
|
|
layout.getAttribute('layout') == 'multiple' &&
|
|
!isSingleToolbar &&
|
|
isExtendedSidebar
|
|
) {
|
|
layout.classList.add('selected');
|
|
} else if (layout.getAttribute('layout') == 'collapsed' && !isExtendedSidebar) {
|
|
layout.classList.add('selected');
|
|
}
|
|
}
|
|
if (this.__hasInitializedLayout) return;
|
|
this.__hasInitializedLayout = true;
|
|
for (const layout of document.getElementById('zenLayoutList').children) {
|
|
layout.addEventListener('click', () => {
|
|
if (layout.hasAttribute('disabled')) {
|
|
return;
|
|
}
|
|
|
|
for (const el of document.getElementById('zenLayoutList').children) {
|
|
el.classList.remove('selected');
|
|
}
|
|
|
|
layout.classList.add('selected');
|
|
|
|
Services.prefs.setBoolPref(
|
|
kZenExtendedSidebar,
|
|
layout.getAttribute('layout') != 'collapsed'
|
|
);
|
|
Services.prefs.setBoolPref(kZenSingleToolbar, layout.getAttribute('layout') == 'single');
|
|
});
|
|
}
|
|
},
|
|
|
|
setCompactModeStyle() {
|
|
const chooser = document.getElementById('zen-compact-mode-styles-form');
|
|
const radios = [...chooser.querySelectorAll('input')];
|
|
|
|
let value = '';
|
|
if (
|
|
Services.prefs.getBoolPref('zen.view.compact.hide-tabbar', false) &&
|
|
Services.prefs.getBoolPref('zen.view.compact.hide-toolbar', false)
|
|
) {
|
|
value = 'both';
|
|
} else {
|
|
value = Services.prefs.getBoolPref('zen.view.compact.hide-tabbar') ? 'left' : 'top';
|
|
}
|
|
chooser.querySelector(`[value='${value}']`).checked = true;
|
|
for (let radio of radios) {
|
|
radio.addEventListener('change', (e) => {
|
|
let value = e.target.value;
|
|
switch (value) {
|
|
case 'left':
|
|
Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', true);
|
|
Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', false);
|
|
break;
|
|
case 'top':
|
|
Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', false);
|
|
Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', true);
|
|
break;
|
|
default:
|
|
Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', true);
|
|
Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', true);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
},
|
|
};
|
|
|
|
var gZenWorkspacesSettings = {
|
|
init() {
|
|
var tabsUnloaderPrefListener = {
|
|
async observe(subject, topic, data) {
|
|
let buttonIndex = await confirmRestartPrompt(true, 1, true, true);
|
|
if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) {
|
|
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
|
|
}
|
|
},
|
|
};
|
|
Services.prefs.addObserver('zen.tab-unloader.enabled', tabsUnloaderPrefListener);
|
|
Services.prefs.addObserver('zen.glance.enabled', tabsUnloaderPrefListener); // We can use the same listener for both prefs
|
|
Services.prefs.addObserver('zen.workspaces.separate-essentials', tabsUnloaderPrefListener);
|
|
Services.prefs.addObserver('zen.glance.activation-method', tabsUnloaderPrefListener);
|
|
window.addEventListener('unload', () => {
|
|
Services.prefs.removeObserver('zen.tab-unloader.enabled', tabsUnloaderPrefListener);
|
|
Services.prefs.removeObserver('zen.glance.enabled', tabsUnloaderPrefListener);
|
|
Services.prefs.removeObserver('zen.glance.activation-method', tabsUnloaderPrefListener);
|
|
Services.prefs.removeObserver('zen.workspaces.separate-essentials', tabsUnloaderPrefListener);
|
|
});
|
|
},
|
|
};
|
|
|
|
const ZEN_CKS_CLASS_BASE = 'zenCKSOption';
|
|
const ZEN_CKS_INPUT_FIELD_CLASS = `${ZEN_CKS_CLASS_BASE}-input`;
|
|
const ZEN_CKS_LABEL_CLASS = `${ZEN_CKS_CLASS_BASE}-label`;
|
|
const ZEN_CKS_WRAPPER_ID = `${ZEN_CKS_CLASS_BASE}-wrapper`;
|
|
const ZEN_CKS_GROUP_PREFIX = `${ZEN_CKS_CLASS_BASE}-group`;
|
|
const KEYBIND_ATTRIBUTE_KEY = 'key';
|
|
|
|
var zenMissingKeyboardShortcutL10n = {
|
|
key_quickRestart: 'zen-key-quick-restart',
|
|
key_delete: 'zen-key-delete',
|
|
goBackKb: 'zen-key-go-back',
|
|
goForwardKb: 'zen-key-go-forward',
|
|
key_enterFullScreen: 'zen-key-enter-full-screen',
|
|
key_exitFullScreen: 'zen-key-exit-full-screen',
|
|
key_aboutProcesses: 'zen-key-about-processes',
|
|
key_stop: 'zen-key-stop',
|
|
key_sanitize: 'zen-key-sanitize',
|
|
key_wrCaptureCmd: 'zen-key-wr-capture-cmd',
|
|
key_wrToggleCaptureSequenceCmd: 'zen-key-wr-toggle-capture-sequence-cmd',
|
|
key_undoCloseWindow: 'zen-key-undo-close-window',
|
|
|
|
key_selectTab1: 'zen-key-select-tab-1',
|
|
key_selectTab2: 'zen-key-select-tab-2',
|
|
key_selectTab3: 'zen-key-select-tab-3',
|
|
key_selectTab4: 'zen-key-select-tab-4',
|
|
key_selectTab5: 'zen-key-select-tab-5',
|
|
key_selectTab6: 'zen-key-select-tab-6',
|
|
key_selectTab7: 'zen-key-select-tab-7',
|
|
key_selectTab8: 'zen-key-select-tab-8',
|
|
key_selectLastTab: 'zen-key-select-tab-last',
|
|
|
|
key_showAllTabs: 'zen-key-show-all-tabs',
|
|
key_gotoHistory: 'zen-key-goto-history',
|
|
|
|
goHome: 'zen-key-go-home',
|
|
key_redo: 'zen-key-redo',
|
|
|
|
key_inspectorMac: 'zen-key-inspector-mac',
|
|
|
|
// Devtools
|
|
key_toggleToolbox: 'zen-devtools-toggle-shortcut',
|
|
key_browserToolbox: 'zen-devtools-toggle-browser-toolbox-shortcut',
|
|
key_browserConsole: 'zen-devtools-toggle-browser-console-shortcut',
|
|
key_responsiveDesignMode: 'zen-devtools-toggle-responsive-design-mode-shortcut',
|
|
key_inspector: 'zen-devtools-toggle-inspector-shortcut',
|
|
key_webconsole: 'zen-devtools-toggle-web-console-shortcut',
|
|
key_jsdebugger: 'zen-devtools-toggle-js-debugger-shortcut',
|
|
key_netmonitor: 'zen-devtools-toggle-net-monitor-shortcut',
|
|
key_styleeditor: 'zen-devtools-toggle-style-editor-shortcut',
|
|
key_performance: 'zen-devtools-toggle-performance-shortcut',
|
|
key_storage: 'zen-devtools-toggle-storage-shortcut',
|
|
key_dom: 'zen-devtools-toggle-dom-shortcut',
|
|
key_accessibility: 'zen-devtools-toggle-accessibility-shortcut',
|
|
};
|
|
|
|
var zenIgnoreKeyboardShortcutL10n = [
|
|
'zen-full-zoom-reduce-shortcut-alt-b',
|
|
'zen-full-zoom-reduce-shortcut-alt-a',
|
|
];
|
|
|
|
var gZenCKSSettings = {
|
|
async init() {
|
|
await this._initializeCKS();
|
|
if (this.__hasInitialized) return;
|
|
this.__hasInitialized = true;
|
|
this._currentActionID = null;
|
|
this._initializeEvents();
|
|
window.addEventListener('unload', () => {
|
|
this.__hasInitialized = false;
|
|
document.getElementById(ZEN_CKS_WRAPPER_ID).innerHTML = '';
|
|
});
|
|
},
|
|
|
|
_initializeEvents() {
|
|
const resetAllListener = this.resetAllShortcuts.bind(this);
|
|
const handleKeyDown = this._handleKeyDown.bind(this);
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
const button = document.getElementById('zenCKSResetButton');
|
|
button.addEventListener('click', resetAllListener);
|
|
window.addEventListener('unload', () => {
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
button.removeEventListener('click', resetAllListener);
|
|
});
|
|
},
|
|
|
|
async resetAllShortcuts() {
|
|
let buttonIndex = await confirmRestartPrompt(true, 1, true, false);
|
|
if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) {
|
|
await gZenKeyboardShortcutsManager.resetAllShortcuts();
|
|
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
|
|
}
|
|
},
|
|
|
|
async _initializeCKS() {
|
|
let wrapper = document.getElementById(ZEN_CKS_WRAPPER_ID);
|
|
wrapper.innerHTML = '';
|
|
|
|
let shortcuts = await gZenKeyboardShortcutsManager.getModifiableShortcuts();
|
|
|
|
if (!shortcuts) {
|
|
throw Error('No shortcuts defined!');
|
|
}
|
|
|
|
// Generate section per each group
|
|
for (let group of VALID_SHORTCUT_GROUPS) {
|
|
let groupClass = `${ZEN_CKS_GROUP_PREFIX}-${group}`;
|
|
if (!wrapper.querySelector(`[data-group="${groupClass}"]`)) {
|
|
let groupElem = document.createElement('h2');
|
|
groupElem.setAttribute('data-group', groupClass);
|
|
document.l10n.setAttributes(groupElem, groupClass);
|
|
wrapper.appendChild(groupElem);
|
|
}
|
|
}
|
|
|
|
for (let shortcut of shortcuts) {
|
|
const keyID = shortcut.getID();
|
|
const action = shortcut.getAction();
|
|
const l10nID = shortcut.getL10NID();
|
|
const group = shortcut.getGroup();
|
|
const keyInString = shortcut.toUserString();
|
|
|
|
const labelValue = zenMissingKeyboardShortcutL10n[keyID] ?? l10nID;
|
|
|
|
if (zenIgnoreKeyboardShortcutL10n.includes(labelValue)) {
|
|
continue;
|
|
}
|
|
|
|
let fragment = window.MozXULElement.parseXULToFragment(`
|
|
<hbox class="${ZEN_CKS_CLASS_BASE}">
|
|
<label class="${ZEN_CKS_LABEL_CLASS}" for="${ZEN_CKS_CLASS_BASE}-${keyID}"></label>
|
|
<vbox flex="1">
|
|
<html:input readonly="1" class="${ZEN_CKS_INPUT_FIELD_CLASS}" id="${ZEN_CKS_INPUT_FIELD_CLASS}-${keyID}" />
|
|
</vbox>
|
|
</hbox>
|
|
`);
|
|
|
|
const label = fragment.querySelector(`.${ZEN_CKS_LABEL_CLASS}`);
|
|
if (!labelValue) {
|
|
label.textContent = action; // Just in case
|
|
} else {
|
|
document.l10n.setAttributes(label, labelValue);
|
|
}
|
|
|
|
let input = fragment.querySelector(`.${ZEN_CKS_INPUT_FIELD_CLASS}`);
|
|
if (keyInString && !shortcut.isEmpty()) {
|
|
input.value = keyInString;
|
|
} else {
|
|
this._resetShortcut(input);
|
|
}
|
|
|
|
input.setAttribute(KEYBIND_ATTRIBUTE_KEY, keyID);
|
|
input.setAttribute('data-group', group);
|
|
input.setAttribute('data-id', keyID);
|
|
|
|
input.addEventListener('focus', (event) => {
|
|
const value = event.target.getAttribute(KEYBIND_ATTRIBUTE_KEY);
|
|
this._currentActionID = event.target.getAttribute('data-id');
|
|
event.target.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
|
|
this._hasSafed = true;
|
|
});
|
|
|
|
input.addEventListener('editDone', (event) => {
|
|
const target = event.target;
|
|
target.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
|
|
});
|
|
|
|
input.addEventListener('blur', (event) => {
|
|
const target = event.target;
|
|
target.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
|
|
if (!this._hasSafed) {
|
|
target.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-unsafed`);
|
|
if (!target.nextElementSibling) {
|
|
target.after(
|
|
window.MozXULElement.parseXULToFragment(`
|
|
<label class="${ZEN_CKS_CLASS_BASE}-unsafed" data-l10n-id="zen-key-unsaved"></label>
|
|
`)
|
|
);
|
|
target.value = 'Not set';
|
|
}
|
|
} else {
|
|
target.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-unsafed`);
|
|
const sibling = target.nextElementSibling;
|
|
if (sibling && sibling.classList.contains(`${ZEN_CKS_CLASS_BASE}-unsafed`)) {
|
|
sibling.remove();
|
|
}
|
|
}
|
|
if (target.classList.contains(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`)) {
|
|
target.label = 'Not set';
|
|
}
|
|
});
|
|
|
|
const groupElem = wrapper.querySelector(`[data-group="${ZEN_CKS_GROUP_PREFIX}-${group}"]`);
|
|
groupElem.after(fragment);
|
|
}
|
|
},
|
|
|
|
async _resetShortcut(input) {
|
|
input.value = 'Not set';
|
|
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-invalid`);
|
|
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
|
|
input.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`);
|
|
|
|
if (this._currentActionID) {
|
|
this._editDone();
|
|
await gZenKeyboardShortcutsManager.setShortcut(this._currentActionID, null, null);
|
|
}
|
|
},
|
|
|
|
_editDone(shortcut, modifiers) {
|
|
// Check if we have a valid key
|
|
if (!shortcut || !modifiers) {
|
|
return;
|
|
}
|
|
gZenKeyboardShortcutsManager.setShortcut(this._currentActionID, shortcut, modifiers);
|
|
this._currentActionID = null;
|
|
},
|
|
|
|
//TODO Check for duplicates
|
|
async _handleKeyDown(event) {
|
|
if (!this._currentActionID || document.hidden) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
let input = document.querySelector(
|
|
`.${ZEN_CKS_INPUT_FIELD_CLASS}[${KEYBIND_ATTRIBUTE_KEY}="${this._currentActionID}"]`
|
|
);
|
|
const modifiers = new nsKeyShortcutModifiers(
|
|
event.ctrlKey,
|
|
event.altKey,
|
|
event.shiftKey,
|
|
event.metaKey,
|
|
false
|
|
);
|
|
const modifiersActive = modifiers.areAnyActive();
|
|
|
|
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`);
|
|
|
|
// First, try to read the *physical* key via event.code.
|
|
// If event.code is like "KeyS", "KeyA", ..., strip off "Key" → "S".
|
|
// Otherwise, fall back to event.key (e.g. "F5", "Enter", etc.).
|
|
let shortcut;
|
|
if (event.code && event.code.startsWith('Key')) shortcut = event.code.slice(3);
|
|
else shortcut = event.key;
|
|
|
|
shortcut = shortcut.replace(/Ctrl|Control|Shift|Alt|Option|Cmd|Meta/, ''); // Remove all modifiers
|
|
|
|
if (shortcut == 'Tab' && !modifiersActive) {
|
|
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`);
|
|
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
|
|
this._latestValidKey = null;
|
|
return;
|
|
} else if (shortcut == 'Escape' && !modifiersActive) {
|
|
const hasConflicts = gZenKeyboardShortcutsManager.checkForConflicts(
|
|
this._latestValidKey ? this._latestValidKey : shortcut,
|
|
this._latestModifier ? this._latestModifier : modifiers,
|
|
this._currentActionID
|
|
);
|
|
|
|
if (!this._latestValidKey && !this._latestModifier) {
|
|
} else if (!this._latestValidKey || hasConflicts) {
|
|
if (!input.classList.contains(`${ZEN_CKS_INPUT_FIELD_CLASS}-invalid`)) {
|
|
input.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-invalid`);
|
|
}
|
|
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-unsafed`);
|
|
if (hasConflicts && !input.nextElementSibling) {
|
|
input.after(
|
|
window.MozXULElement.parseXULToFragment(`
|
|
<label class="${ZEN_CKS_CLASS_BASE}-conflict" data-l10n-id="zen-key-conflict"></label>
|
|
`)
|
|
);
|
|
}
|
|
} else {
|
|
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
|
|
|
|
this._editDone(this._latestValidKey, this._latestModifier);
|
|
if (this.name == 'Not set') {
|
|
input.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`);
|
|
}
|
|
this._latestValidKey = null;
|
|
this._latestModifier = null;
|
|
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-invalid`);
|
|
input.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-valid`);
|
|
setTimeout(() => {
|
|
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-valid`);
|
|
}, 1000);
|
|
const sibling = input.nextElementSibling;
|
|
if (sibling && sibling.classList.contains(`${ZEN_CKS_CLASS_BASE}-conflict`)) {
|
|
sibling.remove();
|
|
}
|
|
}
|
|
this._hasSafed = true;
|
|
input.blur();
|
|
this._currentActionID = null;
|
|
return;
|
|
} else if (shortcut == 'Backspace' && !modifiersActive) {
|
|
this._resetShortcut(input);
|
|
this._latestValidKey = null;
|
|
this._latestModifier = null;
|
|
this._hasSafed = true;
|
|
const sibling = input.nextElementSibling;
|
|
if (sibling && sibling.classList.contains(`${ZEN_CKS_CLASS_BASE}-conflict`)) {
|
|
sibling.remove();
|
|
}
|
|
return;
|
|
}
|
|
|
|
this._latestModifier = modifiers;
|
|
this._hasSafed = false;
|
|
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-invalid`);
|
|
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`);
|
|
input.value = modifiers.toUserString() + shortcut;
|
|
this._latestValidKey = shortcut;
|
|
},
|
|
};
|
|
|
|
Preferences.addAll([
|
|
{
|
|
id: 'zen.view.compact.hide-toolbar',
|
|
type: 'bool',
|
|
default: false,
|
|
},
|
|
{
|
|
id: 'zen.view.compact.toolbar-flash-popup',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'zen.workspaces.hide-default-container-indicator',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'zen.tab-unloader.enabled',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'zen.tab-unloader.timeout-minutes',
|
|
type: 'int',
|
|
default: 10,
|
|
},
|
|
{
|
|
id: 'zen.pinned-tab-manager.restore-pinned-tabs-to-pinned-url',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'zen.pinned-tab-manager.close-shortcut-behavior',
|
|
type: 'string',
|
|
default: 'switch',
|
|
},
|
|
{
|
|
id: 'zen.workspaces.force-container-workspace',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'zen.workspaces.open-new-tab-if-last-unpinned-tab-is-closed',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'zen.tabs.show-newtab-under',
|
|
type: 'bool',
|
|
default: false,
|
|
},
|
|
{
|
|
id: 'zen.glance.activation-method',
|
|
type: 'string',
|
|
default: 'ctrl',
|
|
},
|
|
{
|
|
id: 'zen.glance.enabled',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'zen.view.compact.color-toolbar',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'zen.urlbar.behavior',
|
|
type: 'string',
|
|
default: 'float',
|
|
},
|
|
{
|
|
id: 'zen.view.compact.color-sidebar',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'zen.workspaces.separate-essentials',
|
|
type: 'bool',
|
|
default: false,
|
|
},
|
|
{
|
|
id: 'zen.tabs.show-newtab-vertical',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'zen.view.show-newtab-button-border-top',
|
|
type: 'bool',
|
|
default: false,
|
|
},
|
|
{
|
|
id: 'zen.view.show-newtab-button-top',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'media.videocontrols.picture-in-picture.enabled',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
{
|
|
id: 'zen.workspaces.continue-where-left-off',
|
|
type: 'bool',
|
|
default: false,
|
|
},
|
|
{
|
|
id: 'zen.mods.auto-update',
|
|
type: 'bool',
|
|
default: true,
|
|
},
|
|
]);
|