├── 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 |
7 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------