diff --git a/src/ZenSidebarManager.mjs b/src/ZenSidebarManager.mjs index 3851e4e..fbb2320 100644 --- a/src/ZenSidebarManager.mjs +++ b/src/ZenSidebarManager.mjs @@ -79,7 +79,7 @@ var gZenBrowserManagerSidebar = { // relative to avoid the top margin // 20px is the padding let parentRelativeHeight = - parent.getBoundingClientRect().height - parent.getBoundingClientRect().top + sidebar.getBoundingClientRect().top - 20; + parent.getBoundingClientRect().height - parent.getBoundingClientRect().top + sidebar.getBoundingClientRect().top; let minHeight = parseInt(computedStyle.getPropertyValue('min-height').replace('px', '')); if (!this._isDragging) { // Prevent multiple resizes @@ -304,7 +304,6 @@ var gZenBrowserManagerSidebar = { let data = this.sidebarData; let newPos = []; for (let element of this.__dragingElement.parentNode.children) { - console.log(element); let panelId = element.getAttribute('zen-sidebar-id'); newPos.push(panelId); } diff --git a/src/ZenThemesCommon.mjs b/src/ZenThemesCommon.mjs new file mode 100644 index 0000000..b59ef92 --- /dev/null +++ b/src/ZenThemesCommon.mjs @@ -0,0 +1,98 @@ +var ZenThemesCommon = { + kZenOSToSmallName: { + WINNT: 'windows', + Darwin: 'macos', + Linux: 'linux', + }, + + kZenColors: ['#aac7ff', '#74d7cb', '#a0d490', '#dec663', '#ffb787', '#dec1b1', '#ffb1c0', '#ddbfc3', '#f6b0ea', '#d4bbff'], + + get currentOperatingSystem() { + let os = Services.appinfo.OS; + return this.kZenOSToSmallName[os]; + }, + + 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 (!this.themes) { + if (!(await IOUtils.exists(this.themesDataFile))) { + await IOUtils.writeJSON(this.themesDataFile, {}); + } + + this.themes = await IOUtils.readJSON(this.themesDataFile); + } + return this.themes; + }, + + async getThemePreferences(theme) { + const themePath = PathUtils.join(this.themesRootPath, theme.id, 'preferences.json'); + if (!(await IOUtils.exists(themePath)) || !theme.preferences) { + return []; + } + + 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-z0-9-_.]+)/g.exec(entry); + const isNegation = negation === '!'; + + if ( + (isNegation && os === this.currentOperatingSystem) || + (os !== '' && os !== this.currentOperatingSystem && !isNegation) + ) { + continue; + } + + newThemePreferences.push({ + property, + label, + type: 'checkbox', + disabledOn: os !== '' ? [os] : [], + }); + } + + return newThemePreferences; + } + + return preferences.filter(({ disabledOn = [] }) => !disabledOn.includes(this.currentOperatingSystem)); + }, + + throttle(mainFunction, delay) { + let timerFlag = null; + + return (...args) => { + if (timerFlag === null) { + mainFunction(...args); + timerFlag = setTimeout(() => { + timerFlag = null; + }, delay); + } + }; + }, +}; diff --git a/src/ZenThemesImporter.mjs b/src/ZenThemesImporter.mjs index 257e2b1..7302f34 100644 --- a/src/ZenThemesImporter.mjs +++ b/src/ZenThemesImporter.mjs @@ -8,14 +8,22 @@ const kZenStylesheetThemeHeader = ` const kenStylesheetFooter = ` /* End of Zen Themes */ `; + var gZenStylesheetManager = { async writeStylesheet(path, themes) { let content = kZenStylesheetThemeHeader; + for (let theme of themes) { + if (theme.enabled !== undefined && !theme.enabled) { + continue; + } content += this.getThemeCSS(theme); } + content += kenStylesheetFooter; - let buffer = new TextEncoder().encode(content); + + const buffer = new TextEncoder().encode(content); + await IOUtils.write(path, buffer); }, @@ -36,8 +44,22 @@ var gZenThemeImporter = new (class { constructor() { console.info('ZenThemeImporter: Initiating Zen theme importer'); try { - window.SessionStore.promiseInitialized.then(() => { + window.SessionStore.promiseInitialized.then(async () => { this.insertStylesheet(); + + const themesWithPreferences = await Promise.all( + Object.values(await ZenThemesCommon.getThemes()).map(async (theme) => { + const preferences = await ZenThemesCommon.getThemePreferences(theme); + + return { + name: theme.name, + enabled: theme.enabled, + preferences, + }; + }) + ); + + this.writeToDom(themesWithPreferences); }); console.info('ZenThemeImporter: Zen theme imported'); } catch (e) { @@ -57,31 +79,9 @@ var gZenThemeImporter = new (class { return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes.css'); } - get themesRootPath() { - return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes'); - } - - get themesDataFile() { - return PathUtils.join(PathUtils.profileDir, 'zen-themes.json'); - } - - getThemeFolder(theme) { - return PathUtils.join(this.themesRootPath, theme.id); - } - - async getThemes() { - if (!this._themes) { - if (!(await IOUtils.exists(this.themesDataFile))) { - await IOUtils.writeJSON(this.themesDataFile, {}); - } - this._themes = await IOUtils.readJSON(this.themesDataFile); - } - return this._themes; - } - - rebuildThemeStylesheet() { - this._themes = null; - this.updateStylesheet(); + async rebuildThemeStylesheet() { + ZenThemesCommon.themes = null; + await this.updateStylesheet(); } get styleSheetURI() { @@ -92,32 +92,155 @@ var gZenThemeImporter = new (class { } getStylesheetURIForTheme(theme) { - return Services.io.newFileURI(new FileUtils.File(PathUtils.join(this.getThemeFolder(theme), 'chrome.css'))); + return Services.io.newFileURI(new FileUtils.File(PathUtils.join(ZenThemesCommon.getThemeFolder(theme.id), 'chrome.css'))); } - insertStylesheet() { - if (IOUtils.exists(this.styleSheetPath)) { - this.sss.loadAndRegisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET); + async insertStylesheet() { + if (await IOUtils.exists(this.styleSheetPath)) { + await this.sss.loadAndRegisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET); } } - removeStylesheet() { - this.sss.unregisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET); + async removeStylesheet() { + await this.sss.unregisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET); } async updateStylesheet() { - this.removeStylesheet(); - await this.writeStylesheet(); - this.insertStylesheet(); + await this.removeStylesheet(); + + const themes = Object.values(await ZenThemesCommon.getThemes()); + 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 writeStylesheet() { + setDefaults(themesWithPreferences) { + for (const { preferences, enabled } of themesWithPreferences) { + if (enabled !== undefined && !enabled) { + continue; + } + + for (const { type, property, defaultValue } of preferences) { + if (defaultValue === undefined) { + continue; + } + + switch (type) { + case '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); + } + break; + } + + default: { + 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) { + const browser = ZenThemesCommon.currentBrowser; + + for (const { enabled, preferences, name } of themesWithPreferences) { + const sanitizedName = `theme-${name?.replaceAll(/\s/g, '-')?.replaceAll(/[^A-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 = []; - this._themes = null; - for (let theme of Object.values(await this.getThemes())) { + ZenThemesCommon.themes = null; + + for (let theme of themeList) { theme._chromeURL = this.getStylesheetURIForTheme(theme).spec; themes.push(theme); } + await gZenStylesheetManager.writeStylesheet(this.styleSheetPath, themes); } })(); diff --git a/src/ZenViewSplitter.mjs b/src/ZenViewSplitter.mjs index 4361fc5..1bac78d 100644 --- a/src/ZenViewSplitter.mjs +++ b/src/ZenViewSplitter.mjs @@ -536,8 +536,9 @@ var gZenViewSplitter = new (class { if (this.currentView < 0) return; const currentTab = window.gBrowser.selectedTab; const tabs = this._data[this.currentView].tabs; - for (const tab of tabs) { - this.handleTabClose({ target: tab, forUnsplit: true }); + // note: This MUST be an index loop, as we are removing tabs from the array + for (let i = tabs.length - 1; i >= 0; i--) { + this.handleTabClose({ target: tabs[i], forUnsplit: true }); } window.gBrowser.selectedTab = currentTab; this.updateSplitViewButton(true); @@ -562,7 +563,7 @@ var gZenViewSplitter = new (class { this.unsplitCurrentView(); return; } - const tabs = gBrowser.tabs; + const tabs = gBrowser.visibleTabs; if (tabs.length < 2) { return; } diff --git a/src/ZenWorkspaces.mjs b/src/ZenWorkspaces.mjs index 2c91f9f..7281e9d 100644 --- a/src/ZenWorkspaces.mjs +++ b/src/ZenWorkspaces.mjs @@ -1,11 +1,12 @@ + var ZenWorkspaces = { - async init() { - let docElement = document.documentElement; - if ( - docElement.getAttribute('chromehidden').includes('toolbar') || - docElement.getAttribute('chromehidden').includes('menubar') || - docElement.hasAttribute('privatebrowsingmode') - ) { + /** + * Stores workspace IDs and their last selected tabs. + */ + _lastSelectedWorkspaceTabs: {}, + + init() { + if (!this.shouldHaveWorkspaces) { console.warn('ZenWorkspaces: !!! ZenWorkspaces is disabled in hidden windows !!!'); return; // We are in a hidden window, don't initialize ZenWorkspaces } @@ -16,8 +17,29 @@ var ZenWorkspaces = { }); }, + get shouldShowIconStrip() { + delete this.shouldShowIconStrip; + this.shouldShowIconStrip = Services.prefs.getBoolPref('zen.workspaces.show-icon-strip', true); + return this.shouldShowIconStrip; + }, + + get shouldHaveWorkspaces() { + delete this.shouldHaveWorkspaces; + let docElement = document.documentElement; + this.shouldHaveWorkspaces = !(docElement.hasAttribute('privatebrowsingmode') + || docElement.getAttribute('chromehidden').includes('toolbar') + || docElement.getAttribute('chromehidden').includes('menubar')); + return this.shouldHaveWorkspaces; + }, + get workspaceEnabled() { - return Services.prefs.getBoolPref('zen.workspaces.enabled', false); + delete this.workspaceEnabled; + this.workspaceEnabled = Services.prefs.getBoolPref('zen.workspaces.enabled', false) && this.shouldHaveWorkspaces; + return this.workspaceEnabled; + }, + + getActiveWorkspaceFromCache() { + return this._workspaceCache.workspaces.find((workspace) => workspace.used); }, // Wrorkspaces saving/loading @@ -47,15 +69,25 @@ var ZenWorkspaces = { } }, + async onWorkspacesIconStripChanged() { + this.shouldShowIconStrip = Services.prefs.getBoolPref('zen.workspaces.show-icon-strip', true); + await this._expandWorkspacesStrip(); + }, + async initializeWorkspaces() { Services.prefs.addObserver('zen.workspaces.enabled', this.onWorkspacesEnabledChanged.bind(this)); - this.initializeWorkspacesButton(); + Services.prefs.addObserver('zen.workspaces.show-icon-strip', this.onWorkspacesIconStripChanged.bind(this)); let file = new FileUtils.File(this._storeFile); if (!file.exists()) { await IOUtils.writeJSON(this._storeFile, {}); } + await this.initializeWorkspacesButton(); if (this.workspaceEnabled) { + this._initializeWorkspaceCreationIcons(); + this._initializeWorkspaceEditIcons(); + this._initializeWorkspaceTabContextMenus(); window.addEventListener('TabClose', this.handleTabClose.bind(this)); + window.addEventListener('TabBrowserInserted', this.onTabBrowserInserted.bind(this)); let workspaces = await this._workspaces(); if (workspaces.workspaces.length === 0) { await this.createAndSaveWorkspace('Default Workspace', true); @@ -73,9 +105,6 @@ var ZenWorkspaces = { } this.changeWorkspace(activeWorkspace, true); } - this._initializeWorkspaceCreationIcons(); - this._initializeWorkspaceEditIcons(); - this._initializeWorkspaceTabContextMenus(); } }, @@ -91,12 +120,12 @@ var ZenWorkspaces = { if (tabs.length === 1) { this._createNewTabForWorkspace({ uuid: workspaceID }); // We still need to close other tabs in the workspace - this.changeWorkspace({ uuid: workspaceID }); + this.changeWorkspace({ uuid: workspaceID }, true); } } }, - _kIcons: ['🏠', '📄', '💹', '💼', '📧', '✅', '👥'], + _kIcons: JSON.parse(Services.prefs.getStringPref("zen.workspaces.icons")).map((icon) => icon), _initializeWorkspaceCreationIcons() { let container = document.getElementById('PanelUI-zen-workspaces-create-icons-container'); @@ -160,6 +189,7 @@ var ZenWorkspaces = { console.info('ZenWorkspaces: Removing workspace', windowID); await this.changeWorkspace(json.workspaces.find((workspace) => workspace.uuid !== windowID)); this._deleteAllTabsInWorkspace(windowID); + delete this._lastSelectedWorkspaceTabs[windowID]; json.workspaces = json.workspaces.filter((workspace) => workspace.uuid !== windowID); await this.unsafeSaveWorkspaces(json); await this._propagateWorkspaceData(); @@ -219,20 +249,41 @@ var ZenWorkspaces = { return workspace.name[0].toUpperCase(); }, - async _propagateWorkspaceData() { + async _propagateWorkspaceData({ + ignoreStrip = false + } = {}) { let currentContainer = document.getElementById('PanelUI-zen-workspaces-current-info'); let workspaceList = document.getElementById('PanelUI-zen-workspaces-list'); + if (!ignoreStrip) { + await this._expandWorkspacesStrip(); + } const createWorkspaceElement = (workspace) => { let element = document.createXULElement('toolbarbutton'); element.className = 'subviewbutton'; element.setAttribute('tooltiptext', workspace.name); element.setAttribute('zen-workspace-id', workspace.uuid); - //element.setAttribute("context", "zenWorkspaceActionsMenu"); + if (workspace.used) { + element.setAttribute('active', 'true'); + } + if (workspace.default) { + element.setAttribute('default', 'true'); + } + const containerGroup = ContextualIdentityService.getPublicIdentities().find( + (container) => container.userContextId === workspace.containerTabId + ); + if (containerGroup) { + element.classList.add('identity-color-' + containerGroup.color); + element.setAttribute('data-usercontextid', containerGroup.userContextId); + } let childs = window.MozXULElement.parseXULToFragment(`
-