├── image.png ├── preferences.json ├── theme.json ├── LICENSE ├── README.md ├── userChrome.css └── advanced-tab-groups.uc.js /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vertex-Mods/Advanced-Tab-Groups/HEAD/image.png -------------------------------------------------------------------------------- /preferences.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "property": "browser.tabs.groups.enabled", 4 | "type": "checkbox", 5 | "label": "Enable Tab Groups in the Zen Browser.", 6 | "default": "true", 7 | "force": "true" 8 | }, 9 | { 10 | "type": "separator", 11 | "label": "Other Styles" 12 | }, 13 | { 14 | "property": "browser.tabs.groups.arc-style", 15 | "type": "checkbox", 16 | "label": "Apply Arc-like group styling" 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "advanced-tab-groups", 3 | "name": "Advanced Tab Groups (BETA)", 4 | "description": "Tab Groups for Zen Browser", 5 | "js": true, 6 | "homepage": "https://github.com/Vertex-Mods/Advanced-Tab-Groups", 7 | "preferences": "https://raw.githubusercontent.com/Vertex-Mods/Advanced-Tab-Groups/main/preferences.json", 8 | "style": "https://raw.githubusercontent.com/Vertex-Mods/Advanced-Tab-Groups/main/userChrome.css", 9 | "readme": "https://raw.githubusercontent.com/Vertex-Mods/Advanced-Tab-Groups/main/README.md", 10 | "image": "https://raw.githubusercontent.com/Vertex-Mods/Advanced-Tab-Groups/main/image.png", 11 | "author": "Vertex", 12 | "version": "3.2.1b", 13 | "tags": [ 14 | "Zen Browser", 15 | "Vertical Tabs", 16 | "Tab Groups", 17 | "Tab Folders", 18 | "12th" 19 | ], 20 | "createdAt": "2025-05-13", 21 | "updatedAt": "2025-09-24", 22 | "fork": ["zen"] 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Anoms12 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advanced Tab Groups 2 | 3 | Tab groups for Zen Browser 4 | 5 |

6 | image 7 | Advanced Tab Groups Video 8 |

9 | 10 | ## To-Do 11 | - [ ] https://github.com/12th-devs/Advanced-Tab-Groups/issues/111 12 | - [x] Add Arc & Edge Styles 13 | - [x] Allow folder to group conversion (@JustaDumbPrsn) Thank you Bibek! 14 | - [ ] Fix odd scrolling glitches 15 | - [x] Add the font fade overflow effect 16 | - [x] Fix renaming glitches 17 | - [x] Add icon support 18 | - [x] Improve animations 19 | - [ ] Add the stashing feature (soon™) 20 | 21 | These are in no specific order and just an idea of what is coming for the `v3.x.x` release line. I may also add star features because I wanna overtake Nebula 22 | 23 | --- 24 | ## Acknowledgements 25 | Thanks to [BibekBhusal0](https://github.com/BibekBhusal0) for fixing various issues! 26 | 27 | Thanks to [Wacky-Wombat](https://github.com/Wacky-Wombat) for themes and feedback! 28 | 29 | Thank you to everyone for motivating me to keep this project going! Could not do it without you! 30 | 31 | Made For Zen Badge 32 | -------------------------------------------------------------------------------- /userChrome.css: -------------------------------------------------------------------------------- 1 | /* ==== Tab groups ==== */ 2 | /* https://github.com/Anoms12/Advanced-Tab-Groups */ 3 | /* ====== v3.2.1b ====== */ 4 | tab-group:not([split-view-group]) { 5 | --advanced-tab-color: var(--tab-group-color); 6 | --tab-group-color-pale: transparent !important; 7 | --tab-group-stroke: light-dark( 8 | color-mix(in srgb, var(--zen-primary-color) 60%, black), 9 | color-mix(in srgb, var(--zen-colors-primary) 20%, var(--toolbox-textcolor))); 10 | 11 | 12 | .tab-group-label-container { 13 | height: 36px !important; 14 | margin: 0 !important; 15 | padding: 0 10px 0 0 !important; 16 | border-radius: var(--border-radius-medium) !important; 17 | display: flex !important; 18 | flex-direction: row !important; 19 | align-items: center !important; 20 | justify-content: flex-start !important; 21 | 22 | .tab-close-button { 23 | display: none !important; 24 | } 25 | 26 | &:hover { 27 | background-color: var(--tab-hover-background-color) !important; 28 | 29 | @media (-moz-bool-pref: "browser.tabs.groups.stash-icon") { 30 | .group-stash-button { 31 | -moz-window-dragging: no-drag !important; 32 | visibility: visible !important; 33 | list-style-image: url("chrome://browser/skin/taskbar-tabs-add-tab.svg"); 34 | /*list-style-image: url("chrome://browser/skin/taskbar-tabs-move-tab.svg");*/ 35 | width: 16px; 36 | height: 16px; 37 | margin-inline: auto 0; 38 | -moz-context-properties: fill, fill-opacity; 39 | fill: currentColor; 40 | scale: 0.85; 41 | } 42 | } 43 | 44 | .tab-close-button { 45 | -moz-window-dragging: no-drag !important; 46 | visibility: visible !important; 47 | display: flex !important; 48 | } 49 | } 50 | 51 | .tab-group-icon-container { 52 | margin: 0 6px 0 10px; 53 | } 54 | 55 | .tab-group-icon { 56 | list-style-image: none !important; 57 | display: flex !important; 58 | justify-content: center !important; 59 | align-items: center !important; 60 | width: 16px; 61 | height: 16px; 62 | border-radius: 4px; 63 | position: relative; 64 | fill: var(--tab-group-stroke) !important; 65 | background: var(--advanced-tab-color); 66 | &[zen-emoji-open='true']::before { 67 | border: 1px dashed light-dark(rgba(0, 0, 0, .5), rgba(255, 255, 255, .5)); 68 | border-radius: 6px; 69 | width: 18px; 70 | height: 18px; 71 | content: ''; 72 | position: absolute; 73 | pointer-events: none; 74 | } 75 | .group-icon { 76 | width: 16px; 77 | height: 16px; 78 | display: block; 79 | } 80 | } 81 | 82 | .tab-group-label { 83 | direction: ltr; 84 | mask-image: linear-gradient(to left, transparent, black 1em); 85 | margin-right: auto !important; 86 | width: 100%; 87 | height: 100% !important; 88 | padding: 0 4px 0 4px !important; 89 | background: transparent !important; 90 | color: var(--tab-selected-textcolor) !important; 91 | 92 | &:-moz-window-inactive { 93 | color: var(--toolbox-textcolor-inactive) !important; 94 | } 95 | 96 | font-weight: 400 !important; 97 | text-align: start !important; 98 | display: flex; 99 | flex-direction: row; 100 | align-items: center; 101 | gap: 5px; 102 | } 103 | } 104 | 105 | .tab-group-container { 106 | max-height: 100% !important; 107 | position: relative; 108 | margin-inline-start: 12px !important; 109 | 110 | &::after { 111 | content: ""; 112 | position: absolute; 113 | left: -2px !important; 114 | top: 0; 115 | background: var(--advanced-tab-color); 116 | height: 100%; 117 | width: 2px; 118 | pointer-events: none !important; 119 | } 120 | 121 | tab { 122 | max-height: 40px; 123 | opacity: 1; 124 | overflow: hidden; 125 | transition: scale 0.07s ease, var(--zen-tabbox-element-indent-transition), 126 | min-width 100ms ease-out, max-width 100ms ease-out, max-height 0.1s ease, 127 | opacity 0.1s ease !important; 128 | } 129 | } 130 | 131 | &[collapsed] { 132 | .tab-group-container { 133 | tab:not([selected]) { 134 | max-height: 0 !important; 135 | opacity: 0 !important; 136 | } 137 | } 138 | } 139 | } 140 | 141 | /* Arc-like group styling */ 142 | @media (-moz-bool-pref: "browser.tabs.groups.arc-style") { 143 | /* Hide the color context menu options */ 144 | #advanced-tab-groups-context-menu { 145 | .change-group-color { 146 | display: none !important; 147 | } 148 | } 149 | 150 | 151 | tab-group:not([split-view-group]) { 152 | .tab-group-label-container { 153 | height: 30px !important; 154 | margin-block-start: 5px !important; 155 | &:hover { 156 | background-color: transparent !important; 157 | } 158 | 159 | .tab-group-icon-container { 160 | margin: 0 0 0 10px !important; 161 | display: none !important; 162 | .tab-group-icon { 163 | background: none !important; 164 | } 165 | &:has(.group-icon) { 166 | display: flex !important; 167 | } 168 | &:has(.tab-group-icon[zen-emoji-open='true']) { 169 | display: flex !important; 170 | } 171 | & .group-icon { 172 | width: 14px !important; 173 | height: 14px !important; 174 | } 175 | } 176 | 177 | .tab-group-label { 178 | font-weight: 600 !important; 179 | padding-inline: 0 var(--space-medium) !important; 180 | margin-left: 10px !important; 181 | 182 | #tabbrowser-tabs[orient="vertical"] & { 183 | #tabbrowser-tabs[expanded] & { 184 | margin-inline-end: var(--space-medium); 185 | } 186 | } 187 | } 188 | 189 | .tab-close-button { 190 | display: none !important; 191 | } 192 | } 193 | 194 | .tab-group-container { 195 | margin-inline-start: 0 !important; 196 | &::after { 197 | content: none; 198 | } 199 | } 200 | 201 | &[collapsed] { 202 | .tab-group-container { 203 | tab:not([selected]) { 204 | max-height: 40px !important; 205 | opacity: 100% !important; 206 | } 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /advanced-tab-groups.uc.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Advanced Tab Groups 3 | // @ignorecache 4 | // ==/UserScript== 5 | /* ==== Tab groups ==== */ 6 | /* https://github.com/Anoms12/Advanced-Tab-Groups */ 7 | /* ====== v3.2.1b ====== */ 8 | 9 | class AdvancedTabGroups { 10 | constructor() { 11 | this.init(); 12 | } 13 | 14 | init() { 15 | console.log("[AdvancedTabGroups] Initializing..."); 16 | 17 | // Clear any stored color picker data to prevent persistence issues 18 | this.clearStoredColorData(); 19 | 20 | // Load saved tab group colors 21 | this.loadSavedColors(); 22 | 23 | // Load saved tab group icons 24 | this.loadGroupIcons(); 25 | 26 | this.setupStash(); 27 | 28 | // Set up observer for all tab groups 29 | this.setupObserver(); 30 | 31 | // Add folder context menu item 32 | this.addFolderContextMenuItems(); 33 | 34 | // Remove built-in tab group editor menus if they exist 35 | this.removeBuiltinTabGroupMenu(); 36 | 37 | // Process existing groups 38 | this.processExistingGroups(); 39 | 40 | // Also try again after a delay to catch any missed groups 41 | setTimeout(() => this.processExistingGroups(), 1000); 42 | 43 | // Set up periodic saving of colors (every 30 seconds) 44 | setInterval(() => { 45 | this.saveTabGroupColors(); 46 | }, 30000); 47 | 48 | // Listen for tab group creation events from the platform component 49 | document.addEventListener( 50 | "TabGroupCreate", 51 | this.onTabGroupCreate.bind(this) 52 | ); 53 | } 54 | 55 | setupObserver() { 56 | const observer = new MutationObserver((mutations) => { 57 | mutations.forEach((mutation) => { 58 | if (mutation.type === "childList" && mutation.addedNodes.length > 0) { 59 | mutation.addedNodes.forEach((node) => { 60 | if (node.nodeType === Node.ELEMENT_NODE) { 61 | // Proactively remove Firefox built-in tab group editor menu if it appears 62 | if ( 63 | node.id === "tab-group-editor" || 64 | node.nodeName?.toLowerCase() === "tabgroup-meu" || 65 | node.querySelector?.("#tab-group-editor, tabgroup-meu") 66 | ) { 67 | this.removeBuiltinTabGroupMenu(node); 68 | } 69 | // Check if the added node is a tab-group 70 | if (node.tagName === "tab-group") { 71 | // Skip split-view-groups 72 | if (!node.hasAttribute("split-view-group")) { 73 | this.processGroup(node); 74 | } 75 | } 76 | // Check if any children are tab-groups 77 | const childGroups = node.querySelectorAll?.("tab-group") || []; 78 | childGroups.forEach((group) => { 79 | // Skip split-view-groups 80 | if (!group.hasAttribute("split-view-group")) { 81 | this.processGroup(group); 82 | } 83 | }); 84 | } 85 | }); 86 | } 87 | }); 88 | }); 89 | 90 | observer.observe(document.body, { 91 | childList: true, 92 | subtree: true, 93 | }); 94 | 95 | console.log("[AdvancedTabGroups] Observer set up"); 96 | } 97 | 98 | // Remove Firefox's built-in tab group editor menu elements if present 99 | removeBuiltinTabGroupMenu(root = document) { 100 | try { 101 | const list = root.querySelectorAll 102 | ? root.querySelectorAll("#tab-group-editor, tabgroup-meu") 103 | : []; 104 | list.forEach((el) => { 105 | console.log( 106 | "[AdvancedTabGroups] Removing built-in tab group menu:", 107 | el.id || el.nodeName 108 | ); 109 | el.remove(); 110 | }); 111 | // Fallback direct id lookup 112 | const byId = root.getElementById 113 | ? root.getElementById("tab-group-editor") 114 | : null; 115 | if (byId) { 116 | console.log( 117 | "[AdvancedTabGroups] Removing built-in tab group menu by id fallback" 118 | ); 119 | byId.remove(); 120 | } 121 | } catch (e) { 122 | console.error( 123 | "[AdvancedTabGroups] Error removing built-in tab group menu:", 124 | e 125 | ); 126 | } 127 | } 128 | 129 | processExistingGroups() { 130 | const groups = document.querySelectorAll("tab-group"); 131 | console.log( 132 | "[AdvancedTabGroups] Processing existing groups:", 133 | groups.length 134 | ); 135 | 136 | groups.forEach((group) => { 137 | // Skip split-view-groups 138 | if (!group.hasAttribute("split-view-group")) { 139 | this.processGroup(group); 140 | } 141 | }); 142 | } 143 | 144 | // Track currently edited group for rename 145 | _editingGroup = null; 146 | _groupEdited = null; 147 | 148 | renameGroupKeydown(event) { 149 | event.stopPropagation(); 150 | if (event.key === "Enter") { 151 | let label = this._groupEdited; 152 | let input = document.getElementById("tab-label-input"); 153 | let newName = input.value.trim(); 154 | document.documentElement.removeAttribute("zen-renaming-group"); 155 | input.remove(); 156 | if (label && newName) { 157 | const group = label.closest("tab-group"); 158 | if (group && newName !== group.label) { 159 | group.label = newName; 160 | } 161 | } 162 | label.classList.remove("tab-group-label-editing"); 163 | label.style.display = ""; 164 | this._groupEdited = null; 165 | } else if (event.key === "Escape") { 166 | event.target.blur(); 167 | } 168 | } 169 | 170 | renameGroupStart(group, selectAll = true) { 171 | if (this._groupEdited) return; 172 | const labelElement = group.querySelector(".tab-group-label"); 173 | if (!labelElement) return; 174 | this._groupEdited = labelElement; 175 | document.documentElement.setAttribute("zen-renaming-group", "true"); 176 | labelElement.classList.add("tab-group-label-editing"); 177 | labelElement.style.display = "none"; 178 | const input = document.createElement("input"); 179 | input.id = "tab-label-input"; 180 | input.className = "tab-group-label"; 181 | input.type = "text"; 182 | input.value = group.label || labelElement.textContent || ""; 183 | input.setAttribute("autocomplete", "off"); 184 | input.style.caretColor = "auto"; 185 | labelElement.after(input); 186 | // Focus after insertion 187 | input.focus(); 188 | if (selectAll) { 189 | // Select all text for manual rename 190 | input.select(); 191 | } else { 192 | // Place cursor at end for auto-rename on new groups 193 | try { 194 | const len = input.value.length; 195 | input.setSelectionRange(len, len); 196 | } catch (_) {} 197 | } 198 | input.addEventListener("keydown", this.renameGroupKeydown.bind(this)); 199 | input.addEventListener("blur", this.renameGroupHalt.bind(this)); 200 | } 201 | 202 | renameGroupHalt(event) { 203 | if (document.activeElement === event.target || !this._groupEdited) { 204 | return; 205 | } 206 | document.documentElement.removeAttribute("zen-renaming-group"); 207 | let input = document.getElementById("tab-label-input"); 208 | if (input) input.remove(); 209 | this._groupEdited.classList.remove("tab-group-label-editing"); 210 | this._groupEdited.style.display = ""; 211 | this._groupEdited = null; 212 | } 213 | 214 | processGroup(group) { 215 | // Skip if already processed, if it's a folder, or if it's a split-view-group 216 | if ( 217 | group.hasAttribute("data-close-button-added") || 218 | group.classList.contains("zen-folder") || 219 | group.hasAttribute("zen-folder") || 220 | group.hasAttribute("split-view-group") 221 | ) { 222 | return; 223 | } 224 | 225 | console.log("[AdvancedTabGroups] Processing group:", group.id); 226 | 227 | const labelContainer = group.querySelector(".tab-group-label-container"); 228 | if (!labelContainer) { 229 | console.log( 230 | "[AdvancedTabGroups] No label container found for group:", 231 | group.id 232 | ); 233 | return; 234 | } 235 | 236 | // Check if close button already exists 237 | if (labelContainer.querySelector(".tab-close-button")) { 238 | console.log( 239 | "[AdvancedTabGroups] Close button already exists for group:", 240 | group.id 241 | ); 242 | return; 243 | } 244 | 245 | // Create and inject the icon container and close button together for readability 246 | const groupDomFrag = window.MozXULElement.parseXULToFragment(` 247 |
248 |
249 | 250 |
251 | 252 | 253 | `); 254 | const iconContainer = groupDomFrag.children[0]; 255 | const stashButton = groupDomFrag.children[1]; 256 | const closeButton = groupDomFrag.children[2]; 257 | 258 | // Insert the icon container at the beginning of the label container 259 | labelContainer.insertBefore(iconContainer, labelContainer.firstChild); 260 | // Add the stash button to the label container 261 | labelContainer.appendChild(stashButton); 262 | // Add the close button to the label container 263 | labelContainer.appendChild(closeButton); 264 | 265 | console.log( 266 | "[AdvancedTabGroups] Icon container and close button injected for group:", 267 | group.id 268 | ); 269 | 270 | // Add click event listener 271 | closeButton.addEventListener("click", (event) => { 272 | event.stopPropagation(); 273 | event.preventDefault(); 274 | console.log( 275 | "[AdvancedTabGroups] Close button clicked for group:", 276 | group.id 277 | ); 278 | 279 | try { 280 | // Remove the group's saved color and icon before removing the group 281 | this.removeSavedColor(group.id); 282 | this.removeSavedIcon(group.id); 283 | 284 | gBrowser.removeTabGroup(group); 285 | console.log( 286 | "[AdvancedTabGroups] Successfully removed tab group:", 287 | group.id 288 | ); 289 | } catch (error) { 290 | console.error("[AdvancedTabGroups] Error removing tab group:", error); 291 | } 292 | }); 293 | 294 | // Remove editor mode class if present (prevent editor mode on new group) 295 | group.classList.remove("tab-group-editor-mode-create"); 296 | 297 | // If the group is new (no label or default label), start renaming and set color 298 | if ( 299 | !group.label || 300 | group.label === "" || 301 | ("defaultGroupName" in group && group.label === group.defaultGroupName) 302 | ) { 303 | // Start renaming 304 | this.renameGroupStart(group, false); // Don't select all for new groups 305 | // Set color to average favicon color 306 | if (typeof group._useFaviconColor === "function") { 307 | group._useFaviconColor(); 308 | } 309 | } 310 | 311 | // Add context menu to the group 312 | this.addContextMenu(group); 313 | 314 | console.log( 315 | "[AdvancedTabGroups] Close button, rename functionality, and context menu added to group:", 316 | group.id 317 | ); 318 | } 319 | 320 | // Ensure a single, shared context menu exists and is wired up 321 | ensureSharedContextMenu() { 322 | if (this._sharedContextMenu) return this._sharedContextMenu; 323 | 324 | const contextMenuFrag = window.MozXULElement.parseXULToFragment(` 325 | 326 | 327 | 328 | 330 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 340 | 341 | `); 342 | 343 | const contextMenu = contextMenuFrag.firstElementChild; 344 | document.body.appendChild(contextMenu); 345 | 346 | // Track which group is targeted while the popup is open 347 | this._contextMenuCurrentGroup = null; 348 | 349 | const setGroupColorItem = contextMenu.querySelector(".set-group-color"); 350 | const useFaviconColorItem = contextMenu.querySelector(".use-favicon-color"); 351 | const renameGroupItem = contextMenu.querySelector(".rename-group"); 352 | const changeGroupIconItem = contextMenu.querySelector(".change-group-icon"); 353 | const ungroupTabsItem = contextMenu.querySelector(".ungroup-tabs"); 354 | const convertToFolderItem = contextMenu.querySelector( 355 | ".convert-group-to-folder" 356 | ); 357 | 358 | if (setGroupColorItem) { 359 | setGroupColorItem.addEventListener("command", () => { 360 | const group = this._contextMenuCurrentGroup; 361 | if (group && typeof group._setGroupColor === "function") { 362 | group._setGroupColor(); 363 | } 364 | }); 365 | } 366 | 367 | if (useFaviconColorItem) { 368 | useFaviconColorItem.addEventListener("command", () => { 369 | const group = this._contextMenuCurrentGroup; 370 | if (group && typeof group._useFaviconColor === "function") { 371 | group._useFaviconColor(); 372 | } 373 | }); 374 | } 375 | 376 | if (renameGroupItem) { 377 | renameGroupItem.addEventListener("command", () => { 378 | const group = this._contextMenuCurrentGroup; 379 | if (group) this.renameGroupStart(group); 380 | }); 381 | } 382 | 383 | if (changeGroupIconItem) { 384 | changeGroupIconItem.addEventListener("command", () => { 385 | const group = this._contextMenuCurrentGroup; 386 | if (group) this.changeGroupIcon(group); 387 | }); 388 | } 389 | 390 | if (ungroupTabsItem) { 391 | ungroupTabsItem.addEventListener("command", () => { 392 | const group = this._contextMenuCurrentGroup; 393 | if (group && typeof group.ungroupTabs === "function") { 394 | try { 395 | group.ungroupTabs(); 396 | } catch (error) { 397 | console.error("[AdvancedTabGroups] Error ungrouping tabs:", error); 398 | } 399 | } 400 | }); 401 | } 402 | 403 | if (convertToFolderItem) { 404 | convertToFolderItem.addEventListener("command", () => { 405 | const group = this._contextMenuCurrentGroup; 406 | if (group) this.convertGroupToFolder(group); 407 | }); 408 | } 409 | 410 | // Clear the current group when the menu closes (ready to be reused) 411 | contextMenu.addEventListener("popuphidden", () => { 412 | this._contextMenuCurrentGroup = null; 413 | }); 414 | 415 | this._sharedContextMenu = contextMenu; 416 | return this._sharedContextMenu; 417 | } 418 | 419 | addFolderContextMenuItems() { 420 | // Use a timeout to ensure the menu exists, as it's created by another component 421 | setTimeout(() => { 422 | const folderMenu = document.getElementById("zenFolderActions"); 423 | if (!folderMenu || folderMenu.querySelector("#convert-folder-to-group")) { 424 | return; // Already exists or menu not found 425 | } 426 | 427 | const menuFragment = window.MozXULElement.parseXULToFragment(` 428 | 429 | `); 430 | 431 | const convertToSpaceItem = folderMenu.querySelector( 432 | "#context_zenFolderToSpace" 433 | ); 434 | if (convertToSpaceItem) { 435 | convertToSpaceItem.after(menuFragment); 436 | } else { 437 | // Fallback if the reference item isn't found 438 | folderMenu.appendChild(menuFragment); 439 | } 440 | 441 | folderMenu.addEventListener("command", (event) => { 442 | if (event.target.id === "convert-folder-to-group") { 443 | const triggerNode = folderMenu.triggerNode; 444 | if (!triggerNode) { 445 | console.error( 446 | "[AdvancedTabGroups] Could not find trigger node for folder context menu." 447 | ); 448 | return; 449 | } 450 | const folder = triggerNode.closest("zen-folder"); 451 | if (folder) { 452 | this.convertFolderToGroup(folder); 453 | } else { 454 | console.error( 455 | "[AdvancedTabGroups] Could not find folder from trigger node:", 456 | triggerNode 457 | ); 458 | } 459 | } 460 | }); 461 | console.log( 462 | "[AdvancedTabGroups] Added 'Convert Folder to Group' to context menu." 463 | ); 464 | }, 1500); 465 | } 466 | 467 | // Handle platform-dispatched creation event for groups 468 | onTabGroupCreate(event) { 469 | try { 470 | const target = event.target; 471 | const group = target?.closest 472 | ? target.closest("tab-group") || 473 | (target.tagName === "tab-group" ? target : null) 474 | : null; 475 | if (!group) return; 476 | 477 | // Skip split-view-groups 478 | if (group.hasAttribute("split-view-group")) { 479 | return; 480 | } 481 | 482 | // Remove built-in menu that may be created alongside new groups 483 | this.removeBuiltinTabGroupMenu(); 484 | 485 | // Ensure group gets processed (buttons/context menu) if not already 486 | if (!group.hasAttribute("data-close-button-added")) { 487 | this.processGroup(group); 488 | } 489 | 490 | // Auto-start rename and apply favicon color when newly created 491 | if ( 492 | !group.label || 493 | group.label === "" || 494 | ("defaultGroupName" in group && group.label === group.defaultGroupName) 495 | ) { 496 | if (!this._groupEdited) { 497 | this.renameGroupStart(group, false); // Don't select all for new groups 498 | } 499 | if (typeof group._useFaviconColor === "function") { 500 | setTimeout(() => group._useFaviconColor(), 300); 501 | } 502 | } 503 | } catch (e) { 504 | console.error("[AdvancedTabGroups] Error handling TabGroupCreate:", e); 505 | } 506 | } 507 | 508 | addContextMenu(group) { 509 | // Prevent duplicate listener wiring per group 510 | if (group._contextMenuAdded) return; 511 | group._contextMenuAdded = true; 512 | 513 | // Create shared menu once 514 | const sharedMenu = this.ensureSharedContextMenu(); 515 | 516 | // Attach context menu only to the label container 517 | const labelContainer = group.querySelector(".tab-group-label-container"); 518 | if (labelContainer) { 519 | // Strip default context attribute to prevent built-in menu 520 | labelContainer.removeAttribute("context"); 521 | labelContainer.addEventListener("contextmenu", (event) => { 522 | event.preventDefault(); 523 | event.stopPropagation(); 524 | this._contextMenuCurrentGroup = group; 525 | sharedMenu.openPopupAtScreen(event.screenX, event.screenY, false); 526 | }); 527 | } 528 | 529 | // Also strip any context attribute from the group itself 530 | group.removeAttribute("context"); 531 | 532 | // Add methods to the group for context menu actions (used by commands) 533 | group._renameGroupFromContextMenu = () => { 534 | this.renameGroupStart(group); 535 | }; 536 | 537 | group._closeGroupFromContextMenu = () => { 538 | try { 539 | // Remove the group's saved color and icon before removing the group 540 | this.removeSavedColor(group.id); 541 | this.removeSavedIcon(group.id); 542 | 543 | gBrowser.removeTabGroup(group); 544 | console.log( 545 | "[AdvancedTabGroups] Group closed via context menu:", 546 | group.id 547 | ); 548 | } catch (error) { 549 | console.error( 550 | "[AdvancedTabGroups] Error closing group via context menu:", 551 | error 552 | ); 553 | } 554 | }; 555 | 556 | group._collapseGroupFromContextMenu = () => { 557 | if (group.hasAttribute("collapsed")) { 558 | group.removeAttribute("collapsed"); 559 | console.log( 560 | "[AdvancedTabGroups] Group expanded via context menu:", 561 | group.id 562 | ); 563 | } else { 564 | group.setAttribute("collapsed", "true"); 565 | console.log( 566 | "[AdvancedTabGroups] Group collapsed via context menu:", 567 | group.id 568 | ); 569 | } 570 | }; 571 | 572 | group._expandGroupFromContextMenu = () => { 573 | group.removeAttribute("collapsed"); 574 | console.log( 575 | "[AdvancedTabGroups] Group expanded via context menu:", 576 | group.id 577 | ); 578 | }; 579 | 580 | group._setGroupColor = () => { 581 | console.log( 582 | "[AdvancedTabGroups] Set Group Color clicked for group:", 583 | group.id 584 | ); 585 | 586 | // Check if the gradient picker is available 587 | if (window.nsZenThemePicker && window.gZenThemePicker) { 588 | // Store reference to current group for color application 589 | window.gZenThemePicker._currentTabGroup = group; 590 | 591 | // Try to find and click an existing button that opens the gradient picker 592 | try { 593 | // Look for the existing button that opens the gradient picker 594 | const existingButton = document.getElementById( 595 | "zenToolbarThemePicker" 596 | ); 597 | if (existingButton) { 598 | console.log( 599 | "[AdvancedTabGroups] Found existing gradient picker button, clicking it" 600 | ); 601 | 602 | // Store the current group reference in multiple places to ensure persistence 603 | window.gZenThemePicker._currentTabGroup = group; 604 | window.gZenThemePicker._tabGroupForColorPicker = group; // Backup reference 605 | 606 | // Store original methods for restoration 607 | const originalUpdateMethod = 608 | window.gZenThemePicker.updateCurrentWorkspace; 609 | const originalOnWorkspaceChange = 610 | window.gZenThemePicker.onWorkspaceChange; 611 | const originalGetGradient = window.gZenThemePicker.getGradient; 612 | 613 | // Capture AdvancedTabGroups instance for callbacks inside overrides 614 | const atg = this; 615 | 616 | // Override the updateCurrentWorkspace method to prevent browser background changes 617 | window.gZenThemePicker.updateCurrentWorkspace = async function ( 618 | skipSave = true 619 | ) { 620 | // Check both references to ensure we don't lose the tab group 621 | const currentTabGroup = 622 | this._currentTabGroup || this._tabGroupForColorPicker; 623 | console.log( 624 | "[AdvancedTabGroups] updateCurrentWorkspace called, currentTabGroup:", 625 | currentTabGroup 626 | ); 627 | 628 | // Only block browser changes if we're setting tab group colors 629 | if (currentTabGroup) { 630 | console.log( 631 | "[AdvancedTabGroups] Blocking browser background change, applying to tab group instead" 632 | ); 633 | // Don't call the original method - this prevents browser background changes 634 | // Instead, just apply the color to our tab group 635 | try { 636 | // Get the current dots and their colors 637 | const dots = this.panel.querySelectorAll( 638 | ".zen-theme-picker-dot" 639 | ); 640 | const colors = Array.from(dots) 641 | .map((dot) => { 642 | if (!dot || !dot.style) { 643 | return null; 644 | } 645 | 646 | const colorValue = dot.style.getPropertyValue( 647 | "--zen-theme-picker-dot-color" 648 | ); 649 | if (!colorValue || colorValue === "undefined") { 650 | return null; 651 | } 652 | 653 | const isPrimary = dot.classList.contains("primary"); 654 | const type = dot.getAttribute("data-type"); 655 | 656 | // Handle both RGB and hex colors 657 | let rgb; 658 | if (colorValue.startsWith("rgb")) { 659 | rgb = colorValue.match(/\d+/g)?.map(Number) || [ 660 | 0, 0, 0, 661 | ]; 662 | } else if (colorValue.startsWith("#")) { 663 | // Convert hex to RGB 664 | const hex = colorValue.replace("#", ""); 665 | rgb = [ 666 | parseInt(hex.substr(0, 2), 16), 667 | parseInt(hex.substr(2, 2), 16), 668 | parseInt(hex.substr(4, 2), 16), 669 | ]; 670 | } else { 671 | rgb = [0, 0, 0]; 672 | } 673 | 674 | return { 675 | c: rgb, 676 | isPrimary: isPrimary, 677 | type: type, 678 | }; 679 | }) 680 | .filter(Boolean); 681 | 682 | if (colors.length > 0) { 683 | const gradient = this.getGradient(colors); 684 | console.log( 685 | "[AdvancedTabGroups] Generated gradient:", 686 | gradient, 687 | "from colors:", 688 | colors 689 | ); 690 | 691 | // Set the --tab-group-color CSS variable on the group 692 | currentTabGroup.style.setProperty( 693 | "--tab-group-color", 694 | gradient 695 | ); 696 | 697 | // For simplicity, set the inverted color to the same value 698 | // This simplifies the UI while maintaining the variable structure 699 | currentTabGroup.style.setProperty( 700 | "--tab-group-color-invert", 701 | gradient 702 | ); 703 | 704 | console.log( 705 | "[AdvancedTabGroups] Applied color to group:", 706 | currentTabGroup.id, 707 | "Color:", 708 | gradient 709 | ); 710 | 711 | // Save the color to persistent storage (use plugin instance, not theme picker) 712 | atg.saveTabGroupColors(); 713 | } 714 | } catch (error) { 715 | console.error( 716 | "[AdvancedTabGroups] Error applying color to group:", 717 | error 718 | ); 719 | } 720 | 721 | // Don't call the original method - this prevents browser background changes 722 | return; 723 | } else { 724 | console.log( 725 | "[AdvancedTabGroups] No tab group selected, allowing normal browser background changes" 726 | ); 727 | // If no tab group is selected, allow normal browser background changes 728 | return await originalUpdateMethod.call(this, skipSave); 729 | } 730 | }; 731 | 732 | // Also override the onWorkspaceChange method to prevent browser theme changes 733 | window.gZenThemePicker.onWorkspaceChange = async function ( 734 | workspace, 735 | skipUpdate = false, 736 | theme = null 737 | ) { 738 | // Check both references to ensure we don't lose the tab group 739 | const currentTabGroup = 740 | this._currentTabGroup || this._tabGroupForColorPicker; 741 | console.log( 742 | "[AdvancedTabGroups] onWorkspaceChange called, currentTabGroup:", 743 | currentTabGroup 744 | ); 745 | 746 | // Only block browser theme changes if we're setting tab group colors 747 | if (currentTabGroup) { 748 | console.log( 749 | "[AdvancedTabGroups] Blocking browser theme change" 750 | ); 751 | // Don't call the original method - this prevents browser theme changes 752 | return; 753 | } else { 754 | console.log( 755 | "[AdvancedTabGroups] No tab group selected, allowing normal browser theme changes" 756 | ); 757 | // If no tab group is selected, allow normal browser theme changes 758 | return await originalOnWorkspaceChange.call( 759 | this, 760 | workspace, 761 | skipUpdate, 762 | theme 763 | ); 764 | } 765 | }; 766 | 767 | // Now click the button to open the picker 768 | existingButton.click(); 769 | 770 | // Set up a listener for when the panel closes to apply the final color and cleanup 771 | const panel = window.gZenThemePicker.panel; 772 | const handlePanelClose = () => { 773 | try { 774 | console.log( 775 | "[AdvancedTabGroups] Panel closed, applying final color and cleaning up" 776 | ); 777 | 778 | // Get the final color from the dots using the same logic 779 | const dots = window.gZenThemePicker.panel.querySelectorAll( 780 | ".zen-theme-picker-dot" 781 | ); 782 | const colors = Array.from(dots) 783 | .map((dot) => { 784 | if (!dot || !dot.style) { 785 | return null; 786 | } 787 | 788 | const colorValue = dot.style.getPropertyValue( 789 | "--zen-theme-picker-dot-color" 790 | ); 791 | if (!colorValue || colorValue === "undefined") { 792 | return null; 793 | } 794 | 795 | const isPrimary = dot.classList.contains("primary"); 796 | const type = dot.getAttribute("data-type"); 797 | 798 | // Handle both RGB and hex colors 799 | let rgb; 800 | if (colorValue.startsWith("rgb")) { 801 | rgb = colorValue.match(/\d+/g)?.map(Number) || [0, 0, 0]; 802 | } else if (colorValue.startsWith("#")) { 803 | // Convert hex to RGB 804 | const hex = colorValue.replace("#", ""); 805 | rgb = [ 806 | parseInt(hex.substr(0, 2), 16), 807 | parseInt(hex.substr(2, 2), 16), 808 | parseInt(hex.substr(4, 2), 16), 809 | ]; 810 | } else { 811 | rgb = [0, 0, 0]; 812 | } 813 | 814 | return { 815 | c: rgb, 816 | isPrimary: isPrimary, 817 | type: type, 818 | }; 819 | }) 820 | .filter(Boolean); 821 | 822 | if (colors.length > 0) { 823 | // Check both references to ensure we don't lose the tab group 824 | const currentTabGroup = 825 | window.gZenThemePicker._currentTabGroup || 826 | window.gZenThemePicker._tabGroupForColorPicker; 827 | 828 | if (currentTabGroup) { 829 | const gradient = window.gZenThemePicker.getGradient(colors); 830 | console.log( 831 | "[AdvancedTabGroups] Final gradient generated:", 832 | gradient, 833 | "from colors:", 834 | colors 835 | ); 836 | 837 | // Set the --tab-group-color CSS variable on the group 838 | currentTabGroup.style.setProperty( 839 | "--tab-group-color", 840 | gradient 841 | ); 842 | 843 | // For simplicity, set the inverted color to the same value 844 | currentTabGroup.style.setProperty( 845 | "--tab-group-color-invert", 846 | gradient 847 | ); 848 | 849 | console.log( 850 | "[AdvancedTabGroups] Final color applied to group:", 851 | currentTabGroup.id, 852 | "Color:", 853 | gradient 854 | ); 855 | 856 | // Save the color to persistent storage (use plugin instance) 857 | atg.saveTabGroupColors(); 858 | } 859 | } 860 | 861 | // CRITICAL: Clean up all references and restore original methods 862 | delete window.gZenThemePicker._currentTabGroup; 863 | delete window.gZenThemePicker._tabGroupForColorPicker; 864 | window.gZenThemePicker.updateCurrentWorkspace = 865 | originalUpdateMethod; 866 | window.gZenThemePicker.onWorkspaceChange = 867 | originalOnWorkspaceChange; 868 | 869 | // Remove the event listener 870 | panel.removeEventListener("popuphidden", handlePanelClose); 871 | 872 | // Clear any stored color data to prevent persistence 873 | if (window.gZenThemePicker.dots) { 874 | window.gZenThemePicker.dots.forEach((dot) => { 875 | if (dot.element && dot.element.style) { 876 | dot.element.style.removeProperty( 877 | "--zen-theme-picker-dot-color" 878 | ); 879 | } 880 | }); 881 | } 882 | 883 | console.log( 884 | "[AdvancedTabGroups] Cleanup completed, color picker restored to normal operation" 885 | ); 886 | } catch (error) { 887 | console.error( 888 | "[AdvancedTabGroups] Error during cleanup:", 889 | error 890 | ); 891 | // Ensure cleanup happens even if there's an error 892 | try { 893 | delete window.gZenThemePicker._currentTabGroup; 894 | delete window.gZenThemePicker._tabGroupForColorPicker; 895 | window.gZenThemePicker.updateCurrentWorkspace = 896 | originalUpdateMethod; 897 | window.gZenThemePicker.onWorkspaceChange = 898 | originalOnWorkspaceChange; 899 | panel.removeEventListener("popuphidden", handlePanelClose); 900 | } catch (cleanupError) { 901 | console.error( 902 | "[AdvancedTabGroups] Error during error cleanup:", 903 | cleanupError 904 | ); 905 | } 906 | } 907 | }; 908 | 909 | panel.addEventListener("popuphidden", handlePanelClose); 910 | } else { 911 | // Fallback: try to open the panel directly with proper sizing 912 | if (window.gZenThemePicker && window.gZenThemePicker.panel) { 913 | const panel = window.gZenThemePicker.panel; 914 | 915 | // Force the panel to show its content 916 | panel.style.width = "400px"; 917 | panel.style.height = "600px"; 918 | panel.style.minWidth = "400px"; 919 | panel.style.minHeight = "600px"; 920 | 921 | // Trigger initialization events 922 | panel.dispatchEvent(new Event("popupshowing")); 923 | 924 | // Open at a reasonable position 925 | const rect = group.getBoundingClientRect(); 926 | panel.openPopupAtScreen(rect.left, rect.bottom + 10, false); 927 | } else { 928 | console.error( 929 | "[AdvancedTabGroups] Gradient picker not available" 930 | ); 931 | } 932 | } 933 | } catch (error) { 934 | console.error( 935 | "[AdvancedTabGroups] Error opening gradient picker:", 936 | error 937 | ); 938 | // Last resort: try to open the panel directly 939 | try { 940 | if (window.gZenThemePicker && window.gZenThemePicker.panel) { 941 | const panel = window.gZenThemePicker.panel; 942 | panel.style.width = "400px"; 943 | panel.style.height = "600px"; 944 | panel.openPopupAtScreen(0, 0, false); 945 | } 946 | } catch (fallbackError) { 947 | console.error( 948 | "[AdvancedTabGroups] Fallback panel opening also failed:", 949 | fallbackError 950 | ); 951 | } 952 | } 953 | } else { 954 | console.warn("[AdvancedTabGroups] Gradient picker not available"); 955 | } 956 | }; 957 | 958 | group._changeGroupIcon = () => { 959 | this.changeGroupIcon(group); 960 | }; 961 | 962 | group._useFaviconColor = () => { 963 | console.log( 964 | "[AdvancedTabGroups] Use Average Favicon Color clicked for group:", 965 | group.id 966 | ); 967 | 968 | try { 969 | // Get all favicon images directly from the group 970 | const favicons = group.querySelectorAll(".tab-icon-image"); 971 | if (favicons.length === 0) { 972 | console.log("[AdvancedTabGroups] No favicons found in group"); 973 | return; 974 | } 975 | 976 | console.log( 977 | "[AdvancedTabGroups] Found", 978 | favicons.length, 979 | "favicons in group" 980 | ); 981 | 982 | // Extract colors from favicons 983 | const colors = []; 984 | let processedCount = 0; 985 | const totalFavicons = favicons.length; 986 | 987 | favicons.forEach((favicon, index) => { 988 | if (favicon && favicon.src) { 989 | console.log( 990 | "[AdvancedTabGroups] Processing favicon", 991 | index + 1, 992 | "of", 993 | totalFavicons, 994 | ":", 995 | favicon.src 996 | ); 997 | 998 | // Create a canvas to analyze the favicon 999 | const canvas = document.createElement("canvas"); 1000 | const ctx = canvas.getContext("2d"); 1001 | const img = new Image(); 1002 | 1003 | img.onload = () => { 1004 | try { 1005 | canvas.width = img.width; 1006 | canvas.height = img.height; 1007 | ctx.drawImage(img, 0, 0); 1008 | 1009 | const imageData = ctx.getImageData( 1010 | 0, 1011 | 0, 1012 | canvas.width, 1013 | canvas.height 1014 | ); 1015 | const data = imageData.data; 1016 | 1017 | // Sample pixels and extract colors 1018 | let r = 0, 1019 | g = 0, 1020 | b = 0, 1021 | count = 0; 1022 | for (let i = 0; i < data.length; i += 4) { 1023 | // Skip transparent pixels and very dark pixels 1024 | if ( 1025 | data[i + 3] > 128 && 1026 | data[i] + data[i + 1] + data[i + 2] > 30 1027 | ) { 1028 | r += data[i]; 1029 | g += data[i + 1]; 1030 | b += data[i + 2]; 1031 | count++; 1032 | } 1033 | } 1034 | 1035 | if (count > 0) { 1036 | const avgColor = [ 1037 | Math.round(r / count), 1038 | Math.round(g / count), 1039 | Math.round(b / count), 1040 | ]; 1041 | colors.push(avgColor); 1042 | console.log( 1043 | "[AdvancedTabGroups] Extracted color from favicon", 1044 | index + 1, 1045 | ":", 1046 | avgColor 1047 | ); 1048 | } else { 1049 | console.log( 1050 | "[AdvancedTabGroups] No valid pixels found in favicon", 1051 | index + 1 1052 | ); 1053 | } 1054 | 1055 | processedCount++; 1056 | 1057 | // If this is the last favicon processed, calculate average and apply 1058 | if (processedCount === totalFavicons) { 1059 | if (colors.length > 0) { 1060 | const finalColor = this._calculateAverageColor(colors); 1061 | const colorString = `rgb(${finalColor[0]}, ${finalColor[1]}, ${finalColor[2]})`; 1062 | 1063 | // Set the --tab-group-color CSS variable 1064 | group.style.setProperty("--tab-group-color", colorString); 1065 | group.style.setProperty( 1066 | "--tab-group-color-invert", 1067 | colorString 1068 | ); 1069 | console.log( 1070 | "[AdvancedTabGroups] Applied average favicon color to group:", 1071 | group.id, 1072 | "Color:", 1073 | colorString, 1074 | "from", 1075 | colors.length, 1076 | "favicons" 1077 | ); 1078 | 1079 | // Save the color to persistent storage 1080 | this.saveTabGroupColors(); 1081 | } else { 1082 | console.log( 1083 | "[AdvancedTabGroups] No valid colors extracted from any favicons" 1084 | ); 1085 | } 1086 | } 1087 | } catch (error) { 1088 | console.error( 1089 | "[AdvancedTabGroups] Error processing favicon", 1090 | index + 1, 1091 | ":", 1092 | error 1093 | ); 1094 | processedCount++; 1095 | 1096 | // Still check if we're done processing 1097 | if (processedCount === totalFavicons && colors.length > 0) { 1098 | const finalColor = this._calculateAverageColor(colors); 1099 | const colorString = `rgb(${finalColor[0]}, ${finalColor[1]}, ${finalColor[2]})`; 1100 | 1101 | group.style.setProperty("--tab-group-color", colorString); 1102 | group.style.setProperty( 1103 | "--tab-group-color-invert", 1104 | colorString 1105 | ); 1106 | console.log( 1107 | "[AdvancedTabGroups] Applied average favicon color to group:", 1108 | group.id, 1109 | "Color:", 1110 | colorString, 1111 | "from", 1112 | colors.length, 1113 | "favicons (some failed)" 1114 | ); 1115 | 1116 | this.saveTabGroupColors(); 1117 | } 1118 | } 1119 | }; 1120 | 1121 | img.onerror = () => { 1122 | console.log( 1123 | "[AdvancedTabGroups] Failed to load favicon", 1124 | index + 1, 1125 | ":", 1126 | favicon.src 1127 | ); 1128 | processedCount++; 1129 | 1130 | // Check if we're done processing 1131 | if (processedCount === totalFavicons && colors.length > 0) { 1132 | const finalColor = this._calculateAverageColor(colors); 1133 | const colorString = `rgb(${finalColor[0]}, ${finalColor[1]}, ${finalColor[2]})`; 1134 | 1135 | group.style.setProperty("--tab-group-color", colorString); 1136 | group.style.setProperty( 1137 | "--tab-group-color-invert", 1138 | colorString 1139 | ); 1140 | console.log( 1141 | "[AdvancedTabGroups] Applied average favicon color to group:", 1142 | group.id, 1143 | "Color:", 1144 | colorString, 1145 | "from", 1146 | colors.length, 1147 | "favicons (some failed to load)" 1148 | ); 1149 | 1150 | this.saveTabGroupColors(); 1151 | } 1152 | }; 1153 | 1154 | img.src = favicon.src; 1155 | } else { 1156 | console.log("[AdvancedTabGroups] Favicon", index + 1, "has no src"); 1157 | processedCount++; 1158 | 1159 | // Check if we're done processing 1160 | if (processedCount === totalFavicons && colors.length > 0) { 1161 | const finalColor = this._calculateAverageColor(colors); 1162 | const colorString = `rgb(${finalColor[0]}, ${finalColor[1]}, ${finalColor[2]})`; 1163 | 1164 | group.style.setProperty("--tab-group-color", colorString); 1165 | group.style.setProperty("--tab-group-color-invert", colorString); 1166 | console.log( 1167 | "[AdvancedTabGroups] Applied average favicon color to group:", 1168 | group.id, 1169 | "Color:", 1170 | colorString, 1171 | "from", 1172 | colors.length, 1173 | "favicons (some had no src)" 1174 | ); 1175 | 1176 | this.saveTabGroupColors(); 1177 | } 1178 | } 1179 | }); 1180 | 1181 | if (favicons.length === 0) { 1182 | console.log("[AdvancedTabGroups] No favicons found in group"); 1183 | } 1184 | } catch (error) { 1185 | console.error( 1186 | "[AdvancedTabGroups] Error extracting favicon colors:", 1187 | error 1188 | ); 1189 | } 1190 | }; 1191 | } 1192 | 1193 | // New method to convert group to folder 1194 | convertGroupToFolder(group) { 1195 | console.log("[AdvancedTabGroups] Converting group to folder:", group.id); 1196 | 1197 | try { 1198 | // Check if Zen folders functionality is available 1199 | if (!window.gZenFolders) { 1200 | console.error( 1201 | "[AdvancedTabGroups] Zen folders functionality not available" 1202 | ); 1203 | return; 1204 | } 1205 | 1206 | // Get all tabs in the group 1207 | const tabs = Array.from(group.tabs); 1208 | if (tabs.length === 0) { 1209 | console.log("[AdvancedTabGroups] No tabs found in group to convert"); 1210 | return; 1211 | } 1212 | 1213 | console.log( 1214 | "[AdvancedTabGroups] Found", 1215 | tabs.length, 1216 | "tabs to convert to folder" 1217 | ); 1218 | 1219 | // Get the group name for the new folder 1220 | const groupName = group.label || "New Folder"; 1221 | 1222 | // Create a new folder using Zen folders functionality 1223 | const newFolder = window.gZenFolders.createFolder(tabs, { 1224 | label: groupName, 1225 | renameFolder: false, // Don't prompt for rename since we're using the group name 1226 | workspaceId: 1227 | group.getAttribute("zen-workspace-id") || 1228 | window.gZenWorkspaces?.activeWorkspace, 1229 | }); 1230 | 1231 | if (newFolder) { 1232 | console.log( 1233 | "[AdvancedTabGroups] Successfully created folder:", 1234 | newFolder.id 1235 | ); 1236 | 1237 | // Remove the original group 1238 | try { 1239 | gBrowser.removeTabGroup(group); 1240 | console.log( 1241 | "[AdvancedTabGroups] Successfully removed original group:", 1242 | group.id 1243 | ); 1244 | } catch (error) { 1245 | console.error( 1246 | "[AdvancedTabGroups] Error removing original group:", 1247 | error 1248 | ); 1249 | } 1250 | 1251 | // Remove the saved color and icon for the original group 1252 | this.removeSavedColor(group.id); 1253 | this.removeSavedIcon(group.id); 1254 | 1255 | console.log( 1256 | "[AdvancedTabGroups] Group successfully converted to folder" 1257 | ); 1258 | } else { 1259 | console.error("[AdvancedTabGroups] Failed to create folder"); 1260 | } 1261 | } catch (error) { 1262 | console.error( 1263 | "[AdvancedTabGroups] Error converting group to folder:", 1264 | error 1265 | ); 1266 | } 1267 | } 1268 | 1269 | convertFolderToGroup(folder) { 1270 | console.log("[AdvancedTabGroups] Converting folder to group:", folder.id); 1271 | try { 1272 | const tabsToGroup = folder.allItemsRecursive.filter( 1273 | (item) => gBrowser.isTab(item) && !item.hasAttribute("zen-empty-tab") 1274 | ); 1275 | 1276 | const folderName = folder.label || "New Group"; 1277 | 1278 | if (tabsToGroup.length === 0) { 1279 | console.log( 1280 | "[AdvancedTabGroups] No tabs in folder, removing empty folder." 1281 | ); 1282 | if ( 1283 | folder && 1284 | folder.isConnected && 1285 | typeof folder.delete === "function" 1286 | ) { 1287 | folder.delete(); 1288 | } 1289 | return; 1290 | } 1291 | 1292 | // Unpin all tabs before attempting to group them 1293 | tabsToGroup.forEach((tab) => { 1294 | if (tab.pinned) { 1295 | gBrowser.unpinTab(tab); 1296 | } 1297 | }); 1298 | 1299 | // Use a brief timeout to allow the UI to process the unpinning before creating the group. 1300 | setTimeout(() => { 1301 | try { 1302 | const newGroup = document.createXULElement("tab-group"); 1303 | newGroup.id = `${Date.now()}-${Math.round(Math.random() * 100)}`; 1304 | newGroup.label = folderName; 1305 | 1306 | const unpinnedTabsContainer = 1307 | gZenWorkspaces.activeWorkspaceStrip || 1308 | gBrowser.tabContainer.querySelector("tabs"); 1309 | unpinnedTabsContainer.prepend(newGroup); 1310 | 1311 | newGroup.addTabs(tabsToGroup); 1312 | 1313 | if ( 1314 | folder && 1315 | folder.isConnected && 1316 | typeof folder.delete === "function" 1317 | ) { 1318 | folder.delete(); 1319 | } 1320 | 1321 | this.processGroup(newGroup); 1322 | 1323 | console.log( 1324 | "[AdvancedTabGroups] Folder successfully converted to group:", 1325 | newGroup.id 1326 | ); 1327 | } catch (groupingError) { 1328 | console.error( 1329 | "[AdvancedTabGroups] Error during manual group creation:", 1330 | groupingError 1331 | ); 1332 | } 1333 | }, 200); 1334 | } catch (error) { 1335 | console.error( 1336 | "[AdvancedTabGroups] Error converting folder to group:", 1337 | error 1338 | ); 1339 | } 1340 | } 1341 | 1342 | // Change group icon using the Zen emoji picker (SVG icons only) 1343 | async changeGroupIcon(group) { 1344 | console.log( 1345 | "[AdvancedTabGroups] Change Group Icon clicked for group:", 1346 | group.id 1347 | ); 1348 | 1349 | try { 1350 | // Check if the Zen emoji picker is available 1351 | if (!window.gZenEmojiPicker) { 1352 | console.error("[AdvancedTabGroups] Zen emoji picker not available"); 1353 | return; 1354 | } 1355 | 1356 | // Find the icon container in the group 1357 | const iconContainer = group.querySelector(".tab-group-icon-container"); 1358 | if (!iconContainer) { 1359 | console.error( 1360 | "[AdvancedTabGroups] Icon container not found for group:", 1361 | group.id 1362 | ); 1363 | return; 1364 | } 1365 | 1366 | // Find the icon element (create if it doesn't exist) 1367 | let iconElement = iconContainer.querySelector(".tab-group-icon"); 1368 | if (!iconElement) { 1369 | iconElement = document.createElement("div"); 1370 | iconElement.className = "tab-group-icon"; 1371 | iconContainer.appendChild(iconElement); 1372 | } 1373 | 1374 | // Open the emoji picker with SVG icons only 1375 | const selectedIcon = await window.gZenEmojiPicker.open(iconElement, { 1376 | onlySvgIcons: true, 1377 | }); 1378 | 1379 | if (selectedIcon) { 1380 | console.log("[AdvancedTabGroups] Selected icon:", selectedIcon); 1381 | 1382 | // Clear any existing icon content 1383 | iconElement.innerHTML = ""; 1384 | 1385 | // Create an image element for the SVG icon using parsed XUL 1386 | const imgFrag = window.MozXULElement.parseXULToFragment(` 1387 | Group Icon 1388 | `); 1389 | iconElement.appendChild(imgFrag.firstElementChild); 1390 | 1391 | // Save the icon to persistent storage 1392 | this.saveGroupIcon(group.id, selectedIcon); 1393 | 1394 | console.log("[AdvancedTabGroups] Icon applied to group:", group.id); 1395 | } else if (selectedIcon === null) { 1396 | console.log("[AdvancedTabGroups] Icon removal requested"); 1397 | 1398 | // Clear the icon content 1399 | iconElement.innerHTML = ""; 1400 | 1401 | // Remove the icon from persistent storage 1402 | this.removeSavedIcon(group.id); 1403 | 1404 | console.log("[AdvancedTabGroups] Icon removed from group:", group.id); 1405 | } else { 1406 | console.log("[AdvancedTabGroups] No icon selected"); 1407 | } 1408 | } catch (error) { 1409 | console.error("[AdvancedTabGroups] Error changing group icon:", error); 1410 | } 1411 | } 1412 | 1413 | // Helper method to calculate average color 1414 | _calculateAverageColor(colors) { 1415 | if (colors.length === 0) return [0, 0, 0]; 1416 | 1417 | const total = colors.reduce( 1418 | (acc, color) => { 1419 | acc[0] += color[0]; 1420 | acc[1] += color[1]; 1421 | acc[2] += color[2]; 1422 | return acc; 1423 | }, 1424 | [0, 0, 0] 1425 | ); 1426 | 1427 | return [ 1428 | Math.round(total[0] / colors.length), 1429 | Math.round(total[1] / colors.length), 1430 | Math.round(total[2] / colors.length), 1431 | ]; 1432 | } 1433 | 1434 | // Helper method to determine contrast color (black or white) for a given background color 1435 | _getContrastColor(backgroundColor) { 1436 | try { 1437 | // Parse the background color to get RGB values 1438 | let r, g, b; 1439 | 1440 | if (backgroundColor.startsWith("rgb")) { 1441 | // Handle rgb(r, g, b) format 1442 | const match = backgroundColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); 1443 | if (match) { 1444 | r = parseInt(match[1]); 1445 | g = parseInt(match[2]); 1446 | b = parseInt(match[3]); 1447 | } 1448 | } else if (backgroundColor.startsWith("#")) { 1449 | // Handle hex format 1450 | const hex = backgroundColor.replace("#", ""); 1451 | r = parseInt(hex.substr(0, 2), 16); 1452 | g = parseInt(hex.substr(2, 2), 16); 1453 | b = parseInt(hex.substr(4, 2), 16); 1454 | } else if (backgroundColor.startsWith("linear-gradient")) { 1455 | // For gradients, extract the first color 1456 | const match = backgroundColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); 1457 | if (match) { 1458 | r = parseInt(match[1]); 1459 | g = parseInt(match[2]); 1460 | b = parseInt(match[3]); 1461 | } 1462 | } 1463 | 1464 | if (r !== undefined && g !== undefined && b !== undefined) { 1465 | // Calculate relative luminance using the sRGB formula 1466 | const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; 1467 | 1468 | // Return 'white' for dark backgrounds, 'black' for light backgrounds 1469 | return luminance > 0.5 ? "black" : "white"; 1470 | } 1471 | } catch (error) { 1472 | console.error( 1473 | "[AdvancedTabGroups] Error calculating contrast color:", 1474 | error 1475 | ); 1476 | } 1477 | 1478 | // Default to black if we can't parse the color 1479 | return "black"; 1480 | } 1481 | 1482 | // Clear any stored color picker data to prevent persistence issues 1483 | clearStoredColorData() { 1484 | try { 1485 | if (window.gZenThemePicker) { 1486 | // Clear any stored tab group references 1487 | delete window.gZenThemePicker._currentTabGroup; 1488 | delete window.gZenThemePicker._tabGroupForColorPicker; 1489 | 1490 | // Clear any stored color data on dots 1491 | if (window.gZenThemePicker.dots) { 1492 | window.gZenThemePicker.dots.forEach((dot) => { 1493 | if (dot.element && dot.element.style) { 1494 | dot.element.style.removeProperty("--zen-theme-picker-dot-color"); 1495 | } 1496 | }); 1497 | } 1498 | 1499 | // Clear any stored color data in the panel 1500 | if (window.gZenThemePicker.panel) { 1501 | const dots = window.gZenThemePicker.panel.querySelectorAll( 1502 | ".zen-theme-picker-dot" 1503 | ); 1504 | dots.forEach((dot) => { 1505 | if (dot.style) { 1506 | dot.style.removeProperty("--zen-theme-picker-dot-color"); 1507 | } 1508 | }); 1509 | } 1510 | 1511 | // Reset the color picker to its default state 1512 | this.resetColorPickerToDefault(); 1513 | 1514 | console.log("[AdvancedTabGroups] Stored color data cleared"); 1515 | } 1516 | } catch (error) { 1517 | console.error( 1518 | "[AdvancedTabGroups] Error clearing stored color data:", 1519 | error 1520 | ); 1521 | } 1522 | } 1523 | 1524 | // Reset the color picker to its default state 1525 | resetColorPickerToDefault() { 1526 | try { 1527 | if (window.gZenThemePicker) { 1528 | // Reset any workspace or theme data that might be cached 1529 | if (window.gZenThemePicker.currentWorkspace) { 1530 | // Force a refresh of the current workspace 1531 | window.gZenThemePicker.currentWorkspace = null; 1532 | } 1533 | 1534 | // Clear any cached theme data 1535 | if (window.gZenThemePicker.currentTheme) { 1536 | window.gZenThemePicker.currentTheme = null; 1537 | } 1538 | 1539 | // Reset the dots to their default state 1540 | if (window.gZenThemePicker.dots) { 1541 | window.gZenThemePicker.dots.forEach((dot) => { 1542 | if (dot.element && dot.element.style) { 1543 | // Remove any custom color properties 1544 | dot.element.style.removeProperty("--zen-theme-picker-dot-color"); 1545 | dot.element.style.removeProperty("background-color"); 1546 | dot.element.style.removeProperty("border-color"); 1547 | } 1548 | }); 1549 | } 1550 | 1551 | console.log("[AdvancedTabGroups] Color picker reset to default state"); 1552 | } 1553 | } catch (error) { 1554 | console.error("[AdvancedTabGroups] Error resetting color picker:", error); 1555 | } 1556 | } 1557 | 1558 | // Save tab group colors to persistent storage 1559 | async saveTabGroupColors() { 1560 | try { 1561 | if (typeof UC_API !== "undefined" && UC_API.FileSystem) { 1562 | const colors = {}; 1563 | 1564 | // Get all tab groups and their colors (excluding split-view-groups) 1565 | const groups = document.querySelectorAll("tab-group"); 1566 | groups.forEach((group) => { 1567 | if (group.id && !group.hasAttribute("split-view-group")) { 1568 | const color = group.style.getPropertyValue("--tab-group-color"); 1569 | if (color && color !== "") { 1570 | colors[group.id] = color; 1571 | } 1572 | } 1573 | }); 1574 | 1575 | // Save to file 1576 | const jsonData = JSON.stringify(colors, null, 2); 1577 | await UC_API.FileSystem.writeFile("tab_group_colors.json", jsonData); 1578 | console.log("[AdvancedTabGroups] Tab group colors saved:", colors); 1579 | } else { 1580 | console.warn( 1581 | "[AdvancedTabGroups] UC_API.FileSystem not available, using localStorage fallback" 1582 | ); 1583 | // Fallback to localStorage if UC_API is not available 1584 | const colors = {}; 1585 | const groups = document.querySelectorAll("tab-group"); 1586 | groups.forEach((group) => { 1587 | if (group.id && !group.hasAttribute("split-view-group")) { 1588 | const color = group.style.getPropertyValue("--tab-group-color"); 1589 | if (color && color !== "") { 1590 | colors[group.id] = color; 1591 | } 1592 | } 1593 | }); 1594 | localStorage.setItem( 1595 | "advancedTabGroups_colors", 1596 | JSON.stringify(colors) 1597 | ); 1598 | console.log( 1599 | "[AdvancedTabGroups] Tab group colors saved to localStorage:", 1600 | colors 1601 | ); 1602 | } 1603 | } catch (error) { 1604 | console.error( 1605 | "[AdvancedTabGroups] Error saving tab group colors:", 1606 | error 1607 | ); 1608 | } 1609 | } 1610 | 1611 | // Load saved tab group colors from persistent storage 1612 | async loadSavedColors() { 1613 | try { 1614 | let colors = {}; 1615 | 1616 | if (typeof UC_API !== "undefined" && UC_API.FileSystem) { 1617 | try { 1618 | // Try to read from file 1619 | const fsResult = await UC_API.FileSystem.readFile( 1620 | "tab_group_colors.json" 1621 | ); 1622 | if (fsResult.isContent()) { 1623 | colors = JSON.parse(fsResult.content()); 1624 | console.log("[AdvancedTabGroups] Loaded colors from file:", colors); 1625 | } 1626 | } catch (fileError) { 1627 | console.log( 1628 | "[AdvancedTabGroups] No saved color file found, starting fresh" 1629 | ); 1630 | } 1631 | } else { 1632 | // Fallback to localStorage 1633 | const savedColors = localStorage.getItem("advancedTabGroups_colors"); 1634 | if (savedColors) { 1635 | colors = JSON.parse(savedColors); 1636 | console.log( 1637 | "[AdvancedTabGroups] Loaded colors from localStorage:", 1638 | colors 1639 | ); 1640 | } 1641 | } 1642 | 1643 | // Apply colors to existing groups 1644 | if (Object.keys(colors).length > 0) { 1645 | setTimeout(() => { 1646 | this.applySavedColors(colors); 1647 | }, 500); // Small delay to ensure groups are fully loaded 1648 | } 1649 | } catch (error) { 1650 | console.error("[AdvancedTabGroups] Error loading saved colors:", error); 1651 | } 1652 | } 1653 | 1654 | // Apply saved colors to tab groups 1655 | applySavedColors(colors) { 1656 | try { 1657 | Object.entries(colors).forEach(([groupId, color]) => { 1658 | const group = document.getElementById(groupId); 1659 | if (group && !group.hasAttribute("split-view-group")) { 1660 | group.style.setProperty("--tab-group-color", color); 1661 | group.style.setProperty("--tab-group-color-invert", color); 1662 | console.log( 1663 | "[AdvancedTabGroups] Applied saved color to group:", 1664 | groupId, 1665 | color 1666 | ); 1667 | } 1668 | }); 1669 | } catch (error) { 1670 | console.error("[AdvancedTabGroups] Error applying saved colors:", error); 1671 | } 1672 | } 1673 | 1674 | // Remove saved color for a specific tab group 1675 | async removeSavedColor(groupId) { 1676 | try { 1677 | if (typeof UC_API !== "undefined" && UC_API.FileSystem) { 1678 | try { 1679 | // Read current colors 1680 | const fsResult = await UC_API.FileSystem.readFile( 1681 | "tab_group_colors.json" 1682 | ); 1683 | if (fsResult.isContent()) { 1684 | const colors = JSON.parse(fsResult.content()); 1685 | delete colors[groupId]; 1686 | 1687 | // Save updated colors 1688 | const jsonData = JSON.stringify(colors, null, 2); 1689 | await UC_API.FileSystem.writeFile( 1690 | "tab_group_colors.json", 1691 | jsonData 1692 | ); 1693 | console.log( 1694 | "[AdvancedTabGroups] Removed saved color for group:", 1695 | groupId 1696 | ); 1697 | } 1698 | } catch (fileError) { 1699 | console.log( 1700 | "[AdvancedTabGroups] No saved color file found to remove from" 1701 | ); 1702 | } 1703 | } else { 1704 | // Fallback to localStorage 1705 | const savedColors = localStorage.getItem("advancedTabGroups_colors"); 1706 | if (savedColors) { 1707 | const colors = JSON.parse(savedColors); 1708 | delete colors[groupId]; 1709 | localStorage.setItem( 1710 | "advancedTabGroups_colors", 1711 | JSON.stringify(colors) 1712 | ); 1713 | console.log( 1714 | "[AdvancedTabGroups] Removed saved color for group:", 1715 | groupId 1716 | ); 1717 | } 1718 | } 1719 | } catch (error) { 1720 | console.error("[AdvancedTabGroups] Error removing saved color:", error); 1721 | } 1722 | } 1723 | 1724 | // Save group icon to persistent storage 1725 | async saveGroupIcon(groupId, iconUrl) { 1726 | try { 1727 | if (typeof UC_API !== "undefined" && UC_API.FileSystem) { 1728 | // Read current icons 1729 | let icons = {}; 1730 | try { 1731 | const fsResult = await UC_API.FileSystem.readFile( 1732 | "tab_group_icons.json" 1733 | ); 1734 | if (fsResult.isContent()) { 1735 | icons = JSON.parse(fsResult.content()); 1736 | } 1737 | } catch (fileError) { 1738 | console.log( 1739 | "[AdvancedTabGroups] No saved icon file found, creating new one" 1740 | ); 1741 | } 1742 | 1743 | // Update with new icon 1744 | icons[groupId] = iconUrl; 1745 | 1746 | // Save to file 1747 | const jsonData = JSON.stringify(icons, null, 2); 1748 | await UC_API.FileSystem.writeFile("tab_group_icons.json", jsonData); 1749 | console.log("[AdvancedTabGroups] Group icon saved:", groupId, iconUrl); 1750 | } else { 1751 | // Fallback to localStorage 1752 | const savedIcons = localStorage.getItem("advancedTabGroups_icons"); 1753 | let icons = savedIcons ? JSON.parse(savedIcons) : {}; 1754 | icons[groupId] = iconUrl; 1755 | localStorage.setItem("advancedTabGroups_icons", JSON.stringify(icons)); 1756 | console.log( 1757 | "[AdvancedTabGroups] Group icon saved to localStorage:", 1758 | groupId, 1759 | iconUrl 1760 | ); 1761 | } 1762 | } catch (error) { 1763 | console.error("[AdvancedTabGroups] Error saving group icon:", error); 1764 | } 1765 | } 1766 | 1767 | // Load saved group icons from persistent storage 1768 | async loadGroupIcons() { 1769 | try { 1770 | let icons = {}; 1771 | 1772 | if (typeof UC_API !== "undefined" && UC_API.FileSystem) { 1773 | try { 1774 | const fsResult = await UC_API.FileSystem.readFile( 1775 | "tab_group_icons.json" 1776 | ); 1777 | if (fsResult.isContent()) { 1778 | icons = JSON.parse(fsResult.content()); 1779 | console.log("[AdvancedTabGroups] Loaded icons from file:", icons); 1780 | } 1781 | } catch (fileError) { 1782 | console.log("[AdvancedTabGroups] No saved icon file found"); 1783 | } 1784 | } else { 1785 | // Fallback to localStorage 1786 | const savedIcons = localStorage.getItem("advancedTabGroups_icons"); 1787 | if (savedIcons) { 1788 | icons = JSON.parse(savedIcons); 1789 | console.log( 1790 | "[AdvancedTabGroups] Loaded icons from localStorage:", 1791 | icons 1792 | ); 1793 | } 1794 | } 1795 | 1796 | // Apply icons to existing groups 1797 | if (Object.keys(icons).length > 0) { 1798 | setTimeout(() => { 1799 | this.applySavedIcons(icons); 1800 | }, 500); // Small delay to ensure groups are fully loaded 1801 | } 1802 | } catch (error) { 1803 | console.error("[AdvancedTabGroups] Error loading saved icons:", error); 1804 | } 1805 | } 1806 | 1807 | // Apply saved icons to tab groups 1808 | applySavedIcons(icons) { 1809 | try { 1810 | Object.entries(icons).forEach(([groupId, iconUrl]) => { 1811 | const group = document.getElementById(groupId); 1812 | if (group && !group.hasAttribute("split-view-group")) { 1813 | const iconContainer = group.querySelector( 1814 | ".tab-group-icon-container" 1815 | ); 1816 | if (iconContainer) { 1817 | let iconElement = iconContainer.querySelector(".tab-group-icon"); 1818 | if (!iconElement) { 1819 | iconElement = document.createElement("div"); 1820 | iconElement.className = "tab-group-icon"; 1821 | iconContainer.appendChild(iconElement); 1822 | } 1823 | 1824 | // Clear any existing content and add the icon 1825 | iconElement.innerHTML = ""; 1826 | const imgFrag = window.MozXULElement.parseXULToFragment(` 1827 | Group Icon 1828 | `); 1829 | iconElement.appendChild(imgFrag.firstElementChild); 1830 | 1831 | console.log( 1832 | "[AdvancedTabGroups] Applied saved icon to group:", 1833 | groupId, 1834 | iconUrl 1835 | ); 1836 | } 1837 | } 1838 | }); 1839 | } catch (error) { 1840 | console.error("[AdvancedTabGroups] Error applying saved icons:", error); 1841 | } 1842 | } 1843 | 1844 | // Remove saved icon for a specific tab group 1845 | async removeSavedIcon(groupId) { 1846 | try { 1847 | if (typeof UC_API !== "undefined" && UC_API.FileSystem) { 1848 | try { 1849 | // Read current icons 1850 | const fsResult = await UC_API.FileSystem.readFile( 1851 | "tab_group_icons.json" 1852 | ); 1853 | if (fsResult.isContent()) { 1854 | const icons = JSON.parse(fsResult.content()); 1855 | delete icons[groupId]; 1856 | 1857 | // Save updated icons 1858 | const jsonData = JSON.stringify(icons, null, 2); 1859 | await UC_API.FileSystem.writeFile("tab_group_icons.json", jsonData); 1860 | console.log( 1861 | "[AdvancedTabGroups] Removed saved icon for group:", 1862 | groupId 1863 | ); 1864 | } 1865 | } catch (fileError) { 1866 | console.log( 1867 | "[AdvancedTabGroups] No saved icon file found to remove from" 1868 | ); 1869 | } 1870 | } else { 1871 | // Fallback to localStorage 1872 | const savedIcons = localStorage.getItem("advancedTabGroups_icons"); 1873 | if (savedIcons) { 1874 | const icons = JSON.parse(savedIcons); 1875 | delete icons[groupId]; 1876 | localStorage.setItem( 1877 | "advancedTabGroups_icons", 1878 | JSON.stringify(icons) 1879 | ); 1880 | console.log( 1881 | "[AdvancedTabGroups] Removed saved icon for group:", 1882 | groupId 1883 | ); 1884 | } 1885 | } 1886 | } catch (error) { 1887 | console.error("[AdvancedTabGroups] Error removing saved icon:", error); 1888 | } 1889 | } 1890 | setupStash() { 1891 | const createMenu = document.getElementById("zenCreateNewPopup"); 1892 | 1893 | const stashButton = window.MozXULElement.parseXULToFragment(` 1894 | 1895 | 1896 | 1897 | 1898 | `); 1899 | createMenu.appendChild(stashButton); 1900 | console.log(createMenu); 1901 | 1902 | stashButton.addEventListener("command", () => {}); 1903 | } 1904 | } 1905 | 1906 | // Initialize when the page loads 1907 | (function () { 1908 | if (!globalThis.advancedTabGroups) { 1909 | window.addEventListener( 1910 | "load", 1911 | () => { 1912 | console.log("[AdvancedTabGroups] Page loaded, initializing"); 1913 | globalThis.advancedTabGroups = new AdvancedTabGroups(); 1914 | }, 1915 | { once: true } 1916 | ); 1917 | 1918 | // Clean up when the page is about to unload 1919 | window.addEventListener("beforeunload", () => { 1920 | if (globalThis.advancedTabGroups) { 1921 | globalThis.advancedTabGroups.clearStoredColorData(); 1922 | globalThis.advancedTabGroups.saveTabGroupColors(); 1923 | console.log( 1924 | "[AdvancedTabGroups] Cleanup and save completed before page unload" 1925 | ); 1926 | } 1927 | }); 1928 | 1929 | // Hide tab group menu items for folders in tab context menu 1930 | const tabContextMenu = document.getElementById("tabContextMenu"); 1931 | if (tabContextMenu) { 1932 | tabContextMenu.addEventListener("popupshowing", () => { 1933 | // selecting folders to hide 1934 | const foldersToHide = Array.from( 1935 | gBrowser.tabContainer.querySelectorAll("zen-folder") 1936 | ).map((f) => f.id); 1937 | 1938 | // finding menu items with tab group id 1939 | const groupMenuItems = document.querySelectorAll( 1940 | "#context_moveTabToGroupPopupMenu menuitem[tab-group-id]" 1941 | ); 1942 | 1943 | // Iterate over each item and hide one present in folderstohide array. 1944 | for (const menuItem of groupMenuItems) { 1945 | const tabGroupId = menuItem.getAttribute("tab-group-id"); 1946 | 1947 | if (foldersToHide.includes(tabGroupId)) { 1948 | menuItem.hidden = true; 1949 | } 1950 | } 1951 | }); 1952 | } 1953 | // ^ 1954 | // | 1955 | // Thx to Bibek for this snippet! bibekbhusal on Discord. 1956 | } 1957 | })(); 1958 | --------------------------------------------------------------------------------