feat(sidebar): part 1 - tree vertical tabs v1.1

This commit is contained in:
Alex Kontos
2025-08-05 10:28:43 +01:00
parent 5f4596402e
commit 8678621548
301 changed files with 97795 additions and 2 deletions

View File

@@ -929,7 +929,7 @@ set_config("MOZ_SYSTEM_ZLIB", True, when="--with-system-zlib")
option(
env="USE_LIBZ_RS",
default=milestone.is_early_beta_or_earlier,
default=True,
help="Use libz-rs-sys instead of zlib",
when=toolkit & ~with_system_zlib_option,
)

View File

@@ -18,6 +18,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
TabGrouping: "resource:///modules/TabGrouping.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
UICustomizations: "resource:///modules/UICustomizations.sys.mjs",
SidebarPreferencesHandler: "resource:///modules/SidebarPreferencesHandler.sys.mjs",
});
const WATERFOX_CUSTOMIZATIONS_PREF =
@@ -64,6 +65,9 @@ export const WaterfoxGlue = {
this.startupManifest = await this.getChromeManifest("startup");
this.privateManifest = await this.getChromeManifest("private");
// Initialize sidebar prefs handler early so it can observe pane loads
lazy.SidebarPreferencesHandler.init();
// Observe chrome-document-loaded topic to detect window open
Services.obs.addObserver(this, "chrome-document-loaded");
// Observe main-pane-loaded topic to detect about:preferences open

View File

@@ -0,0 +1,98 @@
import os
import io
import argparse
import json
skipFiles = [
"manifest.json.template",
"test",
"moz.build",
]
def getFullFileList(outputLoc, dirName):
result = {dirName: []}
for entry in os.listdir(outputLoc):
if entry in skipFiles:
continue
if os.path.isdir(os.path.join(outputLoc, entry)):
result.update(getFullFileList(os.path.join(outputLoc, entry), os.path.join(dirName, entry)))
elif dirName:
result[dirName].append(os.path.join(dirName, entry))
else:
result[dirName].append(entry)
return result
def rewriteMozBuild(outputLoc, fileList, extension_id):
mozBuildFile = os.path.join(outputLoc, "moz.build")
print("Rewriting %s" % mozBuildFile)
with io.open(mozBuildFile, "w", encoding="UTF-8") as buildFile:
insertion_text = ''
for dir in sorted(fileList.keys()):
if not fileList[dir]:
continue
if not dir:
mozBuildPathName = ""
else:
mozBuildPathName = '["' + '"]["'.join(dir.split(os.sep)) + '"]'
sorted_files = sorted(fileList[dir], key=lambda s: s.lower()) # Sort the files
insertion_text += \
"FINAL_TARGET_FILES.features['%s']%s += [\n" % (extension_id, mozBuildPathName) + \
" '" + \
"',\n '".join(sorted_files) + "'\n]\n\n" # Use the sorted files
new_contents = "# AUTOMATIC INSERTION START\n" + insertion_text + "# AUTOMATIC INSERTION END\n"
buildFile.write(new_contents)
def replace_locale_strings_in_manifest(manifest_path, locale_path):
# Load the manifest file
with open(manifest_path, 'r') as f:
manifest_data = json.load(f)
# Load the messages file
with open(locale_path, 'r') as f:
messages_data = json.load(f)
# Recursive function to replace placeholders in nested objects
def replace_placeholders(obj):
if isinstance(obj, dict):
for key in obj:
obj[key] = replace_placeholders(obj[key])
elif isinstance(obj, str) and obj.startswith('__MSG_') and obj.endswith('__'):
# Extract the message key from the placeholder
message_key = obj[6:-2]
# Replace the placeholder with the message from the messages file
if message_key in messages_data:
return messages_data[message_key]['message']
return obj
# Replace placeholders in the manifest data
manifest_data = replace_placeholders(manifest_data)
# Write the modified manifest data back to the file
with open(manifest_path, 'w') as f:
json.dump(manifest_data, f, indent=2)
def main(directory, extension_id):
outputLoc = directory
fileList = getFullFileList(outputLoc, "")
rewriteMozBuild(outputLoc, fileList, extension_id)
# Call the function with the paths to the manifest.json and messages.json files
manifest_path = os.path.join(directory, 'manifest.json')
locale_path = os.path.join(directory, '_locales', 'en', 'messages.json')
if os.path.exists(manifest_path) and os.path.exists(locale_path):
replace_locale_strings_in_manifest(manifest_path, locale_path)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate moz.build file")
parser.add_argument("directory", help="The directory to list files for")
parser.add_argument("extension_id", help="The ID of the extension")
args = parser.parse_args()
main(args.directory, args.extension_id)

View File

@@ -12,6 +12,7 @@ DIRS += [
"preferences",
"privatetab",
"search",
"sidebar",
"statusbar",
"tabfeatures",
"tabgrouping",

View File

@@ -0,0 +1,617 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
});
const SIDEBAR_ADDON_ID = "sidebar@waterfox.net";
const SIDEBAR_ADDON_VERSION = "1.1.0";
const SIDEBAR_ADDON_URI = "resource://builtin-addons/sidebar/";
const SIDEBAR_COMPONENT_PREF = "browser.sidebar.enabled";
const CONTAINERS_CONTROLLER_PREF = "privacy.userContext.extension";
export const SidebarPreferencesHandler = {
_initialized: false,
_sidebarPrefObserver: null,
init() {
if (this._initialized) {
return;
}
this._initialized = true;
// Watch for about:preferences being loaded so we can patch the UI.
Services.obs.addObserver(this, "main-pane-loaded");
// Monitor the sidebar component pref and sync the built-in add-on state.
this._monitorSidebarPref();
},
uninit() {
if (!this._initialized) {
return;
}
this._initialized = false;
try {
Services.obs.removeObserver(this, "main-pane-loaded");
} catch (_) {}
if (this._sidebarPrefObserver) {
try {
Services.prefs.removeObserver(SIDEBAR_COMPONENT_PREF, this._sidebarPrefObserver);
} catch (_) {}
this._sidebarPrefObserver = null;
}
},
observe(subject, topic, _data) {
switch (topic) {
case "main-pane-loaded": {
// subject is the about:preferences content window
this._ensureTreeCategory(subject);
this._patchPreferences(subject);
break;
}
}
},
async _monitorSidebarPref() {
let addon = await lazy.AddonManager.getAddonByID(SIDEBAR_ADDON_ID);
// Ensure the built-in sidebar add-on is present (first run or after updates).
addon =
(await lazy.AddonManager.maybeInstallBuiltinAddon(
SIDEBAR_ADDON_ID,
SIDEBAR_ADDON_VERSION,
SIDEBAR_ADDON_URI
)) || addon;
const syncAddonStateWithPref = async () => {
try {
const disabled = Services.prefs.getBoolPref(SIDEBAR_COMPONENT_PREF, false);
if (disabled) {
if (addon?.isActive) {
await addon.disable({ allowSystemAddons: true });
}
} else {
if (addon && !addon.isActive) {
await addon.enable({ allowSystemAddons: true });
}
}
} catch (_) {
// Ignore transient failures.
}
};
// Initial sync and observer
await syncAddonStateWithPref();
this._sidebarPrefObserver = syncAddonStateWithPref;
Services.prefs.addObserver(SIDEBAR_COMPONENT_PREF, syncAddonStateWithPref);
},
_ensureTreeCategory(prefsWin) {
const doc = prefsWin.document;
const categories = doc.getElementById("categories");
if (!categories) {
return;
}
// Bail out if already present
if (doc.getElementById("category-tree")) {
return;
}
// Insert before search category (like previous overlay did)
const before = doc.getElementById("category-search");
const item = doc.createXULElement("richlistitem");
item.id = "category-tree";
item.className = "category";
item.setAttribute("data-l10n-id", "category-tree");
item.setAttribute("data-l10n-attrs", "tooltiptext");
item.setAttribute("value", "paneTree");
item.setAttribute("align", "center");
if (before && before.parentNode === categories) {
categories.insertBefore(item, before);
} else {
categories.appendChild(item);
}
const img = doc.createXULElement("image");
img.className = "category-icon";
const label = doc.createXULElement("label");
label.className = "category-name";
label.setAttribute("flex", "1");
doc.l10n.setAttributes(label, "pane-tree-title");
item.appendChild(img);
item.appendChild(label);
// Ensure the preferences framework knows about our pane so gotoPref works.
if (!prefsWin.gCategoryModules?.has("paneTree")) {
prefsWin.register_module("paneTree", {
init() {
// No-op: our pane is already injected and bound via data-category="paneTree".
},
});
console.log("[SidebarPrefs] paneTree registered with preferences");
}
// Wire category selection to navigate to our pane
categories.addEventListener("select", () => {
const sel = categories.selectedItem;
if (sel && sel.getAttribute("value") === "paneTree") {
prefsWin.gotoPref("paneTree");
}
}, { capture: true });
},
_patchPreferences(prefsWin) {
// Wait until the Containers UI has been constructed in about:preferences.
const startedAt = Date.now();
const timer = prefsWin.setInterval(() => {
if (Date.now() - startedAt > 5000) {
prefsWin.clearInterval(timer);
// Still attempt to inject pane even if containers box wasn't found
this._injectTreePreferences(prefsWin);
return;
}
const doc = prefsWin.document;
if (!doc || doc.readyState === "uninitialized") {
return;
}
const containersBox = doc.getElementById("browserContainersbox");
if (!containersBox) {
return;
}
prefsWin.clearInterval(timer);
this._hideContainersControlledBannerForBuiltin(prefsWin);
this._injectTreePreferences(prefsWin);
// Handle initial navigation: allow about:preferences#tree to go to our pane.
const hash = String(doc.location.hash || "").replace(/^#/, "");
if (hash === "tree" || hash === "paneTree") {
prefsWin.gotoPref("paneTree");
}
}, 100);
},
_hideContainersControlledBannerForBuiltin(prefsWin) {
try {
const controller = Services.prefs.getCharPref(CONTAINERS_CONTROLLER_PREF, "");
if (controller !== SIDEBAR_ADDON_ID) {
// Some other extension controls containers; do not modify UI.
return;
}
const doc = prefsWin.document;
const banner = doc.getElementById("browserContainersExtensionContent");
const checkbox = doc.getElementById("browserContainersCheckbox");
// Hide the “controlled by extension” info row.
if (banner) {
banner.hidden = true;
}
// Optional UX: Ensure the checkbox is enabled so it doesn't look disabled
// without an explanation banner.
if (checkbox) {
checkbox.disabled = false;
}
// Keep the state enforced if prefs code tries to flip it back.
const target = doc.getElementById("browserContainersbox") || doc;
const mo = new prefsWin.MutationObserver(() => {
const b = doc.getElementById("browserContainersExtensionContent");
const c = doc.getElementById("browserContainersCheckbox");
if (b && !b.hidden) {
b.hidden = true;
}
if (c && c.disabled) {
c.disabled = false;
}
});
mo.observe(target, { childList: true, subtree: true, attributes: true });
} catch (_) {
// Ignore unexpected contexts.
}
},
_injectTreePreferences(prefsWin) {
const doc = prefsWin.document;
// Avoid duplicating UI if called more than once
if (doc.getElementById("treePreferencesInjected")) {
return;
}
// Insert our pane elements into the main pane container. We rely on data-category="paneTree"
// so the preferences framework shows/hides it appropriately.
const mainPane = doc.getElementById("mainPrefPane");
if (!mainPane) {
return;
}
// Helper to create XUL nodes
const xul = (name, attrs = {}) => {
const el = doc.createXULElement(name);
for (const [k, v] of Object.entries(attrs)) {
if (v !== undefined && v !== null) {
el.setAttribute(k, v);
}
}
return el;
};
// Root container
const group = xul("groupbox", {
id: "treePreferencesInjected",
"data-category": "paneTree",
hidden: "true",
});
const label = xul("label");
const h2 = doc.createElementNS("http://www.w3.org/1999/xhtml", "h2");
label.appendChild(h2);
group.appendChild(label);
doc.l10n.setAttributes(h2, "tree-header");
// A small helper to add checkbox bound to a pref with l10n id.
const addCheckbox = (parent, id, pref, l10nId) => {
const box = xul("vbox", { id });
const cb = xul("checkbox", { id: `${id}-checkbox`, preference: pref });
doc.l10n.setAttributes(cb, l10nId);
box.appendChild(cb);
parent.appendChild(box);
};
// Appearance section
const appearance = xul("groupbox", {
id: "treeAppearanceGroup",
"data-category": "paneTree",
hidden: "true",
});
{
const l = xul("label");
const h = doc.createElementNS("http://www.w3.org/1999/xhtml", "h2");
l.appendChild(h);
doc.l10n.setAttributes(h, "tree-appearance-header");
appearance.appendChild(l);
addCheckbox(
appearance,
"tree_faviconizePinnedTabsBox",
"browser.sidebar.faviconizePinnedTabs",
"tree-faviconize-pinned-tabs"
);
}
group.appendChild(appearance);
// Auto-sticky section
const sticky = xul("groupbox", {
id: "treeAutoStickyGroup",
"data-category": "paneTree",
hidden: "true",
});
{
const l = xul("label");
const h = doc.createElementNS("http://www.w3.org/1999/xhtml", "h2");
l.appendChild(h);
doc.l10n.setAttributes(h, "tree-auto-sticky-header");
sticky.appendChild(l);
addCheckbox(
sticky,
"tree_stickyActiveTabBox",
"browser.sidebar.stickyActiveTab",
"tree-sticky-active-tab"
);
addCheckbox(
sticky,
"tree_stickySoundPlayingTabBox",
"browser.sidebar.stickySoundPlayingTab",
"tree-sticky-sound-playing-tab"
);
addCheckbox(
sticky,
"tree_stickySharingTabBox",
"browser.sidebar.stickySharingTab",
"tree-sticky-sharing-tab"
);
}
group.appendChild(sticky);
// Behavior section
const behavior = xul("groupbox", {
id: "treeBehaviorGroup",
"data-category": "paneTree",
hidden: "true",
});
{
const l = xul("label");
const h = doc.createElementNS("http://www.w3.org/1999/xhtml", "h2");
l.appendChild(h);
doc.l10n.setAttributes(h, "tree-behavior-header");
behavior.appendChild(l);
addCheckbox(
behavior,
"tree_autoCollapseExpandSubtreeOnAttachBox",
"browser.sidebar.autoCollapseExpandSubtreeOnAttach",
"tree-auto-collapse-expand-subtree-on-attach"
);
addCheckbox(
behavior,
"tree_autoCollapseExpandSubtreeOnSelecthBox",
"browser.sidebar.autoCollapseExpandSubtreeOnSelect",
"tree-auto-collapse-expand-subtree-on-select"
);
// Double-click behavior menulist
const dbl = xul("hbox", {
id: "tree_treeDoubleClickBehaviorBox",
align: "center",
});
const dblCaption = xul("label", {
id: "tree_treeDoubleClickBehaviorCaption",
control: "tree_treeDoubleClickBehavior",
});
doc.l10n.setAttributes(dblCaption, "tree-tree-double-click-behavior-caption");
dbl.appendChild(dblCaption);
const dblList = xul("menulist", {
id: "tree_treeDoubleClickBehavior",
preference: "browser.sidebar.treeDoubleClickBehavior",
});
const dblPopup = xul("menupopup", { class: "in-menulist" });
const dblOptions = [
["1", "tree-tree-double-click-behavior-toggle-collapsed"],
["4", "tree-tree-double-click-behavior-toggle-sticky"],
["3", "tree-tree-double-click-behavior-toggle-close"],
["0", "tree-tree-double-click-behavior-toggle-none"],
];
for (const [value, l10nId] of dblOptions) {
const item = xul("menuitem", { value });
doc.l10n.setAttributes(item, l10nId);
dblPopup.appendChild(item);
}
dblList.appendChild(dblPopup);
dbl.appendChild(dblList);
behavior.appendChild(dbl);
// Successor control menulist
const suc = xul("hbox", {
id: "tree_successorTabControlLevelBox",
align: "center",
});
const sucCaption = xul("label", {
id: "tree_successorTabControlLevelCaption",
control: "tree_successorTabControlLevel",
});
doc.l10n.setAttributes(sucCaption, "tree-successor-tab-control-level-caption");
suc.appendChild(sucCaption);
const sucList = xul("menulist", {
id: "tree_successorTabControlLevel",
preference: "browser.sidebar.successorTabControlLevel",
});
const sucPopup = xul("menupopup", { class: "in-menulist" });
const sucOptions = [
["2", "tree-successor-tab-control-level-in-tree"],
["1", "tree-successor-tab-control-level-simulate-default"],
["0", "tree-successor-tab-control-level-never"],
];
for (const [value, l10nId] of sucOptions) {
const item = xul("menuitem", { value });
doc.l10n.setAttributes(item, l10nId);
sucPopup.appendChild(item);
}
sucList.appendChild(sucPopup);
suc.appendChild(sucList);
behavior.appendChild(suc);
// Drop links behavior menulist
const drop = xul("hbox", {
id: "tree_dropLinksOnTabBehaviorBox",
align: "center",
});
const dropCaption = xul("label", {
id: "tree_dropLinksOnTabBehaviorCaption",
control: "tree_dropLinksOnTabBehavior",
});
doc.l10n.setAttributes(dropCaption, "tree-drop-links-on-tab-behavior-caption");
drop.appendChild(dropCaption);
const dropList = xul("menulist", {
id: "tree_dropLinksOnTabBehavior",
preference: "browser.sidebar.dropLinksOnTabBehavior",
});
const dropPopup = xul("menupopup", { class: "in-menulist" });
const dropOptions = [
["0", "tree-drop-links-on-tab-behavior-ask"],
["1", "tree-drop-links-on-tab-behavior-load"],
["2", "tree-drop-links-on-tab-behavior-newtab"],
];
for (const [value, l10nId] of dropOptions) {
const item = xul("menuitem", { value });
doc.l10n.setAttributes(item, l10nId);
dropPopup.appendChild(item);
}
dropList.appendChild(dropPopup);
drop.appendChild(dropList);
behavior.appendChild(drop);
}
group.appendChild(behavior);
// Auto-attach section
const autoAttach = xul("groupbox", {
id: "treeAutoAttachGroup",
"data-category": "paneTree",
hidden: "true",
});
{
const l = xul("label");
const h = doc.createElementNS("http://www.w3.org/1999/xhtml", "h2");
l.appendChild(h);
doc.l10n.setAttributes(h, "tree-auto-attach-header");
autoAttach.appendChild(l);
const addMenu = (rootId, captionL10n, pref, options) => {
const box = xul("vbox", { id: rootId });
const cap = xul("label", { id: `${rootId}Caption`, control: `${rootId}Menu` });
doc.l10n.setAttributes(cap, captionL10n);
box.appendChild(cap);
const hb = xul("hbox", { id: `${rootId}MenulistBox`, align: "center", class: "sub" });
const list = xul("menulist", { id: `${rootId.replace(/Box$/, "")}`, preference: pref });
const popup = xul("menupopup", { class: "in-menulist" });
for (const [value, l10nId] of options) {
const item = xul("menuitem", { value });
doc.l10n.setAttributes(item, l10nId);
popup.appendChild(item);
}
list.appendChild(popup);
hb.appendChild(list);
box.appendChild(hb);
autoAttach.appendChild(box);
};
addMenu(
"tree_autoAttachOnOpenedWithOwnerBox",
"tree-auto-attach-on-opened-with-owner-caption",
"browser.sidebar.autoAttachOnOpenedWithOwner",
[
["-1", "tree-auto-attach-no-control"],
["0", "tree-auto-attach-independent"],
["6", "tree-auto-attach-child-top"],
["7", "tree-auto-attach-child-end"],
["5", "tree-auto-attach-child-next-to-last-related-tab"],
["2", "tree-auto-attach-sibling"],
["3", "tree-auto-attach-next-sibling"],
]
);
addMenu(
"tree_insertNewTabFromPinnedTabAtBox",
"tree-insert-new-tab-from-pinned-tab-at-caption",
"browser.sidebar.insertNewTabFromPinnedTabAt",
[
["-1", "tree-insert-new-tab-from-pinned-tab-at-no-control"],
["3", "tree-insert-new-tab-from-pinned-tab-at-next-to-last-related-tab"],
["0", "tree-insert-new-tab-from-pinned-tab-at-top"],
["1", "tree-insert-new-tab-from-pinned-tab-at-end"],
]
);
addMenu(
"tree_autoAttachOnNewTabCommandBox",
"tree-auto-attach-on-new-tab-command-caption",
"browser.sidebar.autoAttachOnNewTabCommand",
[
["-1", "tree-auto-attach-no-control"],
["0", "tree-auto-attach-independent"],
["6", "tree-auto-attach-child-top"],
["7", "tree-auto-attach-child-end"],
["2", "tree-auto-attach-sibling"],
["3", "tree-auto-attach-next-sibling"],
]
);
addMenu(
"tree_autoAttachOnNewTabButtonMiddleClickBox",
"tree-auto-attach-on-new-tab-button-middle-click-caption",
"browser.sidebar.autoAttachOnNewTabButtonMiddleClick",
[
["-1", "tree-auto-attach-no-control"],
["0", "tree-auto-attach-independent"],
["6", "tree-auto-attach-child-top"],
["7", "tree-auto-attach-child-end"],
["2", "tree-auto-attach-sibling"],
["3", "tree-auto-attach-next-sibling"],
]
);
addMenu(
"tree_autoAttachOnDuplicatedBox",
"tree-auto-attach-on-duplicated-caption",
"browser.sidebar.autoAttachOnDuplicated",
[
["-1", "tree-auto-attach-no-control"],
["0", "tree-auto-attach-independent"],
["6", "tree-auto-attach-child-top"],
["7", "tree-auto-attach-child-end"],
["2", "tree-auto-attach-sibling"],
["3", "tree-auto-attach-next-sibling"],
]
);
addMenu(
"tree_autoAttachSameSiteOrphanBox",
"tree-auto-attach-same-site-orphan-caption",
"browser.sidebar.autoAttachSameSiteOrphan",
[
["-1", "tree-auto-attach-no-control"],
["0", "tree-auto-attach-independent"],
["6", "tree-auto-attach-child-top"],
["7", "tree-auto-attach-child-end"],
["2", "tree-auto-attach-sibling"],
["3", "tree-auto-attach-next-sibling"],
]
);
addMenu(
"tree_autoAttachOnOpenedFromExternalBox",
"tree-auto-attach-on-opened-from-external-caption",
"browser.sidebar.autoAttachOnOpenedFromExternal",
[
["-1", "tree-auto-attach-no-control"],
["0", "tree-auto-attach-independent"],
["6", "tree-auto-attach-child-top"],
["7", "tree-auto-attach-child-end"],
["2", "tree-auto-attach-sibling"],
["3", "tree-auto-attach-next-sibling"],
]
);
addMenu(
"tree_autoAttachOnAnyOtherTriggerBox",
"tree-auto-attach-on-any-other-trigger-caption",
"browser.sidebar.autoAttachOnAnyOtherTrigger",
[
["-1", "tree-auto-attach-no-control"],
["0", "tree-auto-attach-independent"],
["6", "tree-auto-attach-child-top"],
["7", "tree-auto-attach-child-end"],
["2", "tree-auto-attach-sibling"],
["3", "tree-auto-attach-next-sibling"],
]
);
}
group.appendChild(autoAttach);
// Insert into DOM as a top-level pane element
mainPane.appendChild(group);
// Register preferences with the Preferences binding so widgets reflect pref values
const toRegister = [
["browser.sidebar.faviconizePinnedTabs", "bool"],
["browser.sidebar.stickyActiveTab", "bool"],
["browser.sidebar.stickySoundPlayingTab", "bool"],
["browser.sidebar.stickySharingTab", "bool"],
["browser.sidebar.autoCollapseExpandSubtreeOnAttach", "bool"],
["browser.sidebar.autoCollapseExpandSubtreeOnSelect", "bool"],
["browser.sidebar.treeDoubleClickBehavior", "unichar"],
["browser.sidebar.successorTabControlLevel", "unichar"],
["browser.sidebar.dropLinksOnTabBehavior", "unichar"],
["browser.sidebar.autoAttachOnOpenedWithOwner", "unichar"],
["browser.sidebar.insertNewTabFromPinnedTabAt", "unichar"],
["browser.sidebar.autoAttachOnNewTabCommand", "unichar"],
["browser.sidebar.autoAttachOnNewTabButtonMiddleClick", "unichar"],
["browser.sidebar.autoAttachOnDuplicated", "unichar"],
["browser.sidebar.autoAttachSameSiteOrphan", "unichar"],
["browser.sidebar.autoAttachOnOpenedFromExternal", "unichar"],
["browser.sidebar.autoAttachOnAnyOtherTrigger", "unichar"],
];
for (const [id, type] of toRegister) {
prefsWin.Preferences.add({ id, type });
}
// Reveal sections for our dedicated category
group.hidden = false;
appearance.hidden = false;
sticky.hidden = false;
behavior.hidden = false;
autoAttach.hidden = false;
},
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
% resource builtin-addons %builtin-addons/ contentaccessible=yes
builtin-addons/sidebar/_locales/ (./_locales/**)
builtin-addons/sidebar/background/ (./background/**)
builtin-addons/sidebar/common/ (./common/**)
builtin-addons/sidebar/experiments/ (./experiments/**)
builtin-addons/sidebar/extlib/ (./extlib/**)
builtin-addons/sidebar/options/ (./options/**)
builtin-addons/sidebar/resources/ (./resources/**)
builtin-addons/sidebar/sidebar/ (./sidebar/**)
builtin-addons/sidebar/manifest.json (./manifest.json)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
configs,
} from '/common/common.js';
import * as Constants from '/common/constants.js';
import { Tab } from '/common/TreeItem.js';
configs.$addObserver(key => {
switch (key) {
case 'stickyActiveTab':
updateAutoStickyActive();
break;
case 'stickySoundPlayingTab':
updateAutoStickySoundPlaying();
break;
case 'stickySharingTab':
updateAutoStickySharing();
break;
default:
break;
}
});
configs.$loaded.then(() => {
updateAutoStickyActive();
updateAutoStickySoundPlaying();
updateAutoStickySharing();
});
function updateAutoStickyActive() {
if (configs.stickyActiveTab)
Tab.registerAutoStickyState(Constants.kTAB_STATE_ACTIVE);
else
Tab.unregisterAutoStickyState(Constants.kTAB_STATE_ACTIVE);
}
function updateAutoStickySoundPlaying() {
if (configs.stickySoundPlayingTab)
Tab.registerAutoStickyState(Constants.kTAB_STATE_SOUND_PLAYING);
else
Tab.unregisterAutoStickyState(Constants.kTAB_STATE_SOUND_PLAYING);
}
function updateAutoStickySharing() {
if (configs.stickySharingTab)
Tab.registerAutoStickyState([
Constants.kTAB_STATE_SHARING_CAMERA,
Constants.kTAB_STATE_SHARING_MICROPHONE,
Constants.kTAB_STATE_SHARING_SCREEN,
]);
else
Tab.unregisterAutoStickyState([
Constants.kTAB_STATE_SHARING_CAMERA,
Constants.kTAB_STATE_SHARING_MICROPHONE,
Constants.kTAB_STATE_SHARING_SCREEN,
]);
}

View File

@@ -0,0 +1,614 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import { SequenceMatcher } from '/extlib/diff.js';
import {
log as internalLogger,
dumpTab,
wait,
mapAndFilter,
configs
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as CacheStorage from '/common/cache-storage.js';
import * as Constants from '/common/constants.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TabsUpdate from '/common/tabs-update.js';
import * as UniqueId from '/common/unique-id.js';
import MetricsData from '/common/MetricsData.js';
import { Tab } from '/common/TreeItem.js';
import * as Tree from './tree.js';
function log(...args) {
internalLogger('background/background-cache', ...args);
}
const kCONTENTS_VERSION = 5;
let mActivated = false;
const mCaches = {};
export function activate() {
mActivated = true;
configs.$addObserver(onConfigChange);
if (!configs.persistCachedTree) {
// clear obsolete cache
browser.windows.getAll().then(windows => {
for (const win of windows) {
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_TABS).catch(ApiTabs.createErrorSuppressor());
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY).catch(ApiTabs.createErrorSuppressor());
}
});
}
}
// ===================================================================
// restoring tabs from cache
// ===================================================================
export async function restoreWindowFromEffectiveWindowCache(windowId, options = {}) {
MetricsData.add('restoreWindowFromEffectiveWindowCache: start');
log(`restoreWindowFromEffectiveWindowCache for ${windowId} start`);
const owner = options.owner || getWindowCacheOwner(windowId);
if (!owner) {
log(`restoreWindowFromEffectiveWindowCache for ${windowId} fail: no owner`);
return false;
}
cancelReservedCacheTree(windowId); // prevent to break cache before loading
const tabs = options.tabs || await browser.tabs.query({ windowId }).catch(ApiTabs.createErrorHandler());
if (configs.debug)
log(`restoreWindowFromEffectiveWindowCache for ${windowId} tabs: `, () => tabs.map(dumpTab));
const actualSignature = getWindowSignature(tabs);
let cache = options.caches?.get(`window-${owner.windowId}`) || await MetricsData.addAsync('restoreWindowFromEffectiveWindowCache: window cache', getWindowCache(owner, Constants.kWINDOW_STATE_CACHED_TABS));
if (!cache) {
log(`restoreWindowFromEffectiveWindowCache for ${windowId} fail: no cache`);
return false;
}
const promisedPermanentStates = Promise.all(tabs.map(tab => Tab.get(tab.id).$TST.getPermanentStates())); // don't await at here for better performance
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: start');
let cachedSignature = cache?.signature;
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: got from the owner `, {
owner, cachedSignature, cache
});
const signatureGeneratedFromCache = getWindowSignature(cache.tabs).join('\n');
if (cache &&
cache.tabs &&
cachedSignature &&
cachedSignature.join('\n') != signatureGeneratedFromCache) {
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: cache is broken.`, {
cachedSignature: cachedSignature.join('\n'),
signatureGeneratedFromCache
});
cache = cachedSignature = null;
TabsInternalOperation.clearCache(owner);
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: signature failed.');
}
else {
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: signature passed.');
}
if (options.ignorePinnedTabs &&
cache &&
cache.tabs &&
cachedSignature) {
cache.tabs = trimTabsCache(cache.tabs, cache.pinnedTabsCount);
cachedSignature = trimSignature(cachedSignature, cache.pinnedTabsCount);
}
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: matching actual signature of got cache');
const signatureMatchResult = matcheSignatures({
actual: actualSignature,
cached: cachedSignature
});
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: verify cache`, {
cache, actualSignature, cachedSignature,
...signatureMatchResult,
});
if (!cache ||
cache.version != kCONTENTS_VERSION ||
!signatureMatchResult.matched) {
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: no effective cache`);
TabsInternalOperation.clearCache(owner);
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: actual signature failed.');
return false;
}
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: actual signature passed.');
cache.offset = signatureMatchResult.offset;
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: restore from cache`);
const permanentStates = await MetricsData.addAsync('restoreWindowFromEffectiveWindowCache: permanentStatus', promisedPermanentStates); // await at here for better performance
const restored = await MetricsData.addAsync('restoreWindowFromEffectiveWindowCache: restoreTabsFromCache', restoreTabsFromCache(windowId, { cache, tabs, permanentStates }));
if (restored) {
MetricsData.add(`restoreWindowFromEffectiveWindowCache: window ${windowId} succeeded`);
// Now we reload the sidebar if it is opened, because it is the easiest way
// to synchronize state of tabs completely.
log('reload sidebar for a tree restored from cache');
browser.runtime.sendMessage({
type: Constants.kCOMMAND_RELOAD,
windowId,
}).catch(ApiTabs.createErrorSuppressor());
}
else {
MetricsData.add(`restoreWindowFromEffectiveWindowCache: window ${windowId} failed`);
}
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: restored = ${restored}`);
return restored;
}
function getWindowSignature(tabs) {
return tabs.map(tab => `${tab.cookieStoreId},${tab.incognito},${tab.pinned},${tab.url}`);
}
function trimSignature(signature, ignoreCount) {
if (!ignoreCount || ignoreCount < 0)
return signature;
return signature.slice(ignoreCount);
}
function trimTabsCache(cache, ignoreCount) {
if (!ignoreCount || ignoreCount < 0)
return cache;
return cache.slice(ignoreCount);
}
function matcheSignatures(signatures) {
const operations = (new SequenceMatcher(signatures.cached, signatures.actual)).operations();
log('matcheSignatures: operations ', operations);
let matched = false;
let offset = 0;
for (const operation of operations) {
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
if (tag == 'equal' &&
fromEnd - fromStart == signatures.cached.length) {
matched = true;
break;
}
offset += toEnd - toStart;
}
log('matcheSignatures: ', { matched, offset });
return { matched, offset };
}
async function restoreTabsFromCache(windowId, params = {}) {
if (!params.cache ||
params.cache.version != kCONTENTS_VERSION)
return false;
return (await restoreTabsFromCacheInternal({
windowId,
tabs: params.tabs,
permanentStates: params.permanentStates,
offset: params.cache.offset || 0,
cache: params.cache.tabs
})).length > 0;
}
async function restoreTabsFromCacheInternal(params) {
MetricsData.add('restoreTabsFromCacheInternal: start');
log(`restoreTabsFromCacheInternal: restore tabs for ${params.windowId} from cache`);
const offset = params.offset || 0;
const win = TabsStore.windows.get(params.windowId);
const tabs = params.tabs.slice(offset).map(tab => Tab.get(tab.id));
if (offset > 0 &&
tabs.length <= offset) {
log('restoreTabsFromCacheInternal: missing window');
return [];
}
log(`restoreTabsFromCacheInternal: there is ${win.tabs.size} tabs`);
if (params.cache.length != tabs.length) {
log('restoreTabsFromCacheInternal: Mismatched number of restored tabs?');
return [];
}
try {
await MetricsData.addAsync('rebuildAll: fixupTabsRestoredFromCache', fixupTabsRestoredFromCache(tabs, params.permanentStates, params.cache));
}
catch(e) {
log(String(e), e.stack);
throw e;
}
log('restoreTabsFromCacheInternal: done');
if (configs.debug)
Tab.dumpAll();
return tabs;
}
async function fixupTabsRestoredFromCache(tabs, permanentStates, cachedTabs) {
MetricsData.add('fixupTabsRestoredFromCache: start');
if (tabs.length != cachedTabs.length)
throw new Error(`fixupTabsRestoredFromCache: Mismatched number of tabs restored from cache, tabs=${tabs.length}, cachedTabs=${cachedTabs.length}`);
log('fixupTabsRestoredFromCache start ', () => ({ tabs: tabs.map(dumpTab), cachedTabs }));
const idMap = new Map();
let remappedCount = 0;
// step 1: build a map from old id to new id
tabs = tabs.map((tab, index) => {
const cachedTab = cachedTabs[index];
const oldId = cachedTab.id;
tab = Tab.get(tab.id);
log(`fixupTabsRestoredFromCache: remap ${oldId} => ${tab.id}`);
idMap.set(oldId, tab);
if (oldId != tab.id)
remappedCount++;
return tab;
});
if (remappedCount && remappedCount < tabs.length)
throw new Error(`fixupTabsRestoredFromCache: not a window restoration, only ${remappedCount} tab(s) are restored (maybe restoration of closed tabs)`);
MetricsData.add('fixupTabsRestoredFromCache: step 1 done.');
// step 2: restore information of tabs
// Do this from bottom to top, to reduce post operations for modified trees.
// (Attaching a tab to an existing tree will trigger "update" task for
// existing ancestors, but attaching existing subtree to a solo tab won't
// trigger such tasks.)
// See also: https://github.com/piroor/treestyletab/issues/2278#issuecomment-519387792
for (let i = tabs.length - 1; i > -1; i--) {
fixupTabRestoredFromCache(tabs[i], permanentStates[i], cachedTabs[i], idMap);
}
// step 3: restore collapsed/expanded state of tabs and finalize the
// restoration process
// Do this from top to bottom, because a tab can be placed under an
// expanded parent but the parent can be placed under a collapsed parent.
for (const tab of tabs) {
fixupTabRestoredFromCachePostProcess(tab);
}
MetricsData.add('fixupTabsRestoredFromCache: step 2 done.');
}
function fixupTabRestoredFromCache(tab, permanentStates, cachedTab, idMap) {
tab.$TST.clear();
const tabStates = new Set([...cachedTab.$TST.states, ...permanentStates]);
for (const state of Constants.kTAB_TEMPORARY_STATES) {
tabStates.delete(state);
}
tab.$TST.states = tabStates;
tab.$TST.attributes = cachedTab.$TST.attributes;
log('fixupTabRestoredFromCache children: ', cachedTab.$TST.childIds);
const childIds = mapAndFilter(cachedTab.$TST.childIds, oldId => {
const tab = idMap.get(oldId);
return tab?.id || undefined;
});
tab.$TST.children = childIds;
if (childIds.length > 0)
tab.$TST.setAttribute(Constants.kCHILDREN, `|${childIds.join('|')}|`);
else
tab.$TST.removeAttribute(Constants.kCHILDREN);
log('fixupTabRestoredFromCache children: => ', tab.$TST.childIds);
log('fixupTabRestoredFromCache parent: ', cachedTab.$TST.parentId);
const parentTab = idMap.get(cachedTab.$TST.parentId) || null;
tab.$TST.parent = parentTab;
if (parentTab)
tab.$TST.setAttribute(Constants.kPARENT, parentTab.id);
else
tab.$TST.removeAttribute(Constants.kPARENT);
log('fixupTabRestoredFromCache parent: => ', tab.$TST.parentId);
if (tab.discarded) {
tab.$TST.addState(Constants.kTAB_STATE_PENDING);
}
tab.$TST.temporaryMetadata.set('treeStructureAlreadyRestoredFromSessionData', true);
}
function fixupTabRestoredFromCachePostProcess(tab) {
const parentTab = tab.$TST.parent;
if (parentTab &&
(parentTab.$TST.collapsed ||
parentTab.$TST.subtreeCollapsed)) {
tab.$TST.addState(Constants.kTAB_STATE_COLLAPSED);
tab.$TST.addState(Constants.kTAB_STATE_COLLAPSED_DONE);
}
else {
tab.$TST.removeState(Constants.kTAB_STATE_COLLAPSED);
tab.$TST.removeState(Constants.kTAB_STATE_COLLAPSED_DONE);
}
TabsStore.updateIndexesForTab(tab);
TabsUpdate.updateTab(tab, tab, { forceApply: true, onlyApply: true });
}
// ===================================================================
// updating cache
// ===================================================================
async function updateWindowCache(owner, key, value) {
if (!owner)
return;
if (configs.persistCachedTree) {
try {
if (value)
await CacheStorage.setValue({
windowId: owner.windowId,
key,
value,
store: CacheStorage.BACKGROUND,
});
else
await CacheStorage.deleteValue({
windowId: owner.windowId,
key,
store: CacheStorage.BACKGROUND,
});
return;
}
catch(error) {
console.log(`BackgroundCache.updateWindowCache for ${owner.windowId}/${key} failed: `, error.message, error.stack, error);
}
}
const storageKey = `backgroundCache-${await UniqueId.ensureWindowId(owner.windowId)}-${key}`;
if (value)
mCaches[storageKey] = value;
else
delete mCaches[storageKey];
}
export function markWindowCacheDirtyFromTab(tab, akey) {
const win = TabsStore.windows.get(tab.windowId);
if (!win) // the window may be closed
return;
if (win.markWindowCacheDirtyFromTabTimeout)
clearTimeout(win.markWindowCacheDirtyFromTabTimeout);
win.markWindowCacheDirtyFromTabTimeout = setTimeout(() => {
win.markWindowCacheDirtyFromTabTimeout = null;
updateWindowCache(win.lastWindowCacheOwner, akey, true);
}, 100);
}
async function getWindowCache(owner, key) {
if (configs.persistCachedTree) {
try {
const value = await CacheStorage.getValue({
windowId: owner.windowId,
key,
store: CacheStorage.BACKGROUND,
});
return value;
}
catch(error) {
console.log(`BackgroundCache.getWindowCache for ${owner.windowId}/${key} failed: `, error.message, error.stack, error);
}
}
const storageKey = `backgroundCache-${await UniqueId.ensureWindowId(owner.windowId)}-${key}`;
return mCaches[storageKey];
}
function getWindowCacheOwner(windowId) {
const tab = Tab.getFirstTab(windowId);
if (!tab)
return null;
return {
id: tab.id,
windowId: tab.windowId
};
}
export async function reserveToCacheTree(windowId, trigger) {
if (!mActivated ||
!configs.useCachedTree)
return;
const win = TabsStore.windows.get(windowId);
if (!win)
return;
// If there is any opening (but not resolved its unique id yet) tab,
// we are possibly restoring tabs. To avoid cache breakage before
// restoration, we must wait until we know whether there is any other
// restoring tab or not.
if (Tab.needToWaitTracked(windowId))
await Tab.waitUntilTrackedAll(windowId);
if (win.promisedAllTabsRestored) // not restored yet
return;
if (!trigger && configs.debug)
trigger = new Error().stack;
log('reserveToCacheTree for window ', windowId, trigger);
TabsInternalOperation.clearCache(win.lastWindowCacheOwner);
if (trigger)
reserveToCacheTree.triggers.add(trigger);
if (win.waitingToCacheTree)
clearTimeout(win.waitingToCacheTree);
win.waitingToCacheTree = setTimeout(() => {
const triggers = [...reserveToCacheTree.triggers];
reserveToCacheTree.triggers.clear();
cacheTree(windowId, triggers);
}, 500);
}
reserveToCacheTree.triggers = new Set();
function cancelReservedCacheTree(windowId) {
const win = TabsStore.windows.get(windowId);
if (win?.waitingToCacheTree) {
clearTimeout(win.waitingToCacheTree);
delete win.waitingToCacheTree;
}
}
async function cacheTree(windowId, triggers) {
if (Tab.needToWaitTracked(windowId))
await Tab.waitUntilTrackedAll(windowId);
const win = TabsStore.windows.get(windowId);
if (!win ||
!configs.useCachedTree)
return;
const signature = getWindowSignature(Tab.getAllTabs(windowId));
if (win.promisedAllTabsRestored) // not restored yet
return;
//log('save cache for ', windowId);
win.lastWindowCacheOwner = getWindowCacheOwner(windowId);
if (!win.lastWindowCacheOwner)
return;
const firstTab = Tab.getFirstTab(windowId);
if (firstTab.incognito) { // never save cache for incognito windows
updateWindowCache(win.lastWindowCacheOwner, Constants.kWINDOW_STATE_CACHED_TABS, null);
return;
}
log('cacheTree for window ', windowId, triggers/*{ stack: configs.debug && new Error().stack }*/);
updateWindowCache(win.lastWindowCacheOwner, Constants.kWINDOW_STATE_CACHED_TABS, {
version: kCONTENTS_VERSION,
tabs: TabsStore.windows.get(windowId).export(true).tabs,
pinnedTabsCount: Tab.getPinnedTabs(windowId).length,
signature
});
}
// update cache on events
Tab.onCreated.addListener((tab, _info = {}) => {
if (!tab.$TST.previousTab) { // it is a new cache owner
const win = TabsStore.windows.get(tab.windowId);
if (win.lastWindowCacheOwner)
TabsInternalOperation.clearCache(win.lastWindowCacheOwner);
}
reserveToCacheTree(tab.windowId, 'tab created');
});
// Tree restoration for "Restore Previous Session"
Tab.onWindowRestoring.addListener(async ({ windowId, restoredCount }) => {
if (!configs.useCachedTree)
return;
log('Tabs.onWindowRestoring ', { windowId, restoredCount });
if (restoredCount == 1) {
log('Tabs.onWindowRestoring: single tab restored');
return;
}
log('Tabs.onWindowRestoring: continue ', windowId);
MetricsData.add('Tabs.onWindowRestoring restore start');
const tabs = await browser.tabs.query({ windowId }).catch(ApiTabs.createErrorHandler());
try {
await restoreWindowFromEffectiveWindowCache(windowId, {
ignorePinnedTabs: true,
owner: tabs[tabs.length - 1],
tabs
});
MetricsData.add('Tabs.onWindowRestoring restore end');
}
catch(e) {
log('Tabs.onWindowRestoring: FATAL ERROR while restoring tree from cache', String(e), e.stack);
}
});
Tab.onRemoved.addListener((tab, info) => {
if (!tab.$TST.previousTab) // the tab was the cache owner
TabsInternalOperation.clearCache(tab);
wait(0).then(() => {
// "Restore Previous Session" closes some tabs at first, so we should not clear the old cache yet.
// See also: https://dxr.mozilla.org/mozilla-central/rev/5be384bcf00191f97d32b4ac3ecd1b85ec7b18e1/browser/components/sessionstore/SessionStore.jsm#3053
reserveToCacheTree(info.windowId, 'tab removed');
});
});
Tab.onMoved.addListener((tab, info) => {
if (info.fromIndex == 0) // the tab is not the cache owner anymore
TabsInternalOperation.clearCache(tab);
reserveToCacheTree(info.windowId, 'tab moved');
});
Tab.onUpdated.addListener((tab, info) => {
markWindowCacheDirtyFromTab(tab, Constants.kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY);
if ('url' in info)
reserveToCacheTree(tab.windowId, 'tab updated');
});
Tab.onStateChanged.addListener((tab, state, _has) => {
if (state == Constants.kTAB_STATE_STICKY)
markWindowCacheDirtyFromTab(tab, Constants.kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY);
});
Tree.onSubtreeCollapsedStateChanging.addListener(tab => {
reserveToCacheTree(tab.windowId, 'subtree collapsed/expanded');
});
Tree.onAttached.addListener((tab, _info) => {
wait(0).then(() => {
// "Restore Previous Session" closes some tabs at first and it causes tree changes, so we should not clear the old cache yet.
// See also: https://dxr.mozilla.org/mozilla-central/rev/5be384bcf00191f97d32b4ac3ecd1b85ec7b18e1/browser/components/sessionstore/SessionStore.jsm#3053
reserveToCacheTree(tab.windowId, 'tab attached to tree');
});
});
Tree.onDetached.addListener((tab, _info) => {
TabsInternalOperation.clearCache(tab);
wait(0).then(() => {
// "Restore Previous Session" closes some tabs at first and it causes tree changes, so we should not clear the old cache yet.
// See also: https://dxr.mozilla.org/mozilla-central/rev/5be384bcf00191f97d32b4ac3ecd1b85ec7b18e1/browser/components/sessionstore/SessionStore.jsm#3053
reserveToCacheTree(tab.windowId, 'tab detached from tree');
});
});
Tab.onPinned.addListener(tab => {
reserveToCacheTree(tab.windowId, 'tab pinned');
});
Tab.onUnpinned.addListener(tab => {
if (tab.$TST.previousTab) // the tab was the cache owner
TabsInternalOperation.clearCache(tab);
reserveToCacheTree(tab.windowId, 'tab unpinned');
});
Tab.onShown.addListener(tab => {
reserveToCacheTree(tab.windowId, 'tab shown');
});
Tab.onHidden.addListener(tab => {
reserveToCacheTree(tab.windowId, 'tab hidden');
});
browser.windows.onRemoved.addListener(async windowId => {
try {
CacheStorage.clearForWindow(windowId);
}
catch(_error) {
}
const storageKeyPart = `Cache-${await UniqueId.ensureWindowId(windowId)}-`;
for (const key in mCaches) {
if (key.includes(storageKeyPart))
delete mCaches[key];
}
});
function onConfigChange(key) {
switch (key) {
case 'useCachedTree':
case 'persistCachedTree':
browser.windows.getAll({
populate: true,
windowTypes: ['normal']
}).then(windows => {
for (const win of windows) {
const owner = win.tabs[win.tabs.length - 1];
if (configs[key]) {
reserveToCacheTree(win.id, 'config change');
}
else {
TabsInternalOperation.clearCache(owner);
location.reload();
}
}
}).catch(ApiTabs.createErrorSuppressor());
break;
}
}

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html>
<head>
<meta charset="UTF-8">
<script type="module" src="./index-ws.js"></script>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,928 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
log as internalLogger,
wait,
configs,
sanitizeForHTMLText,
waitUntilStartupOperationsUnblocked,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as ContextualIdentities from '/common/contextual-identities.js';
import * as Dialog from '/common/dialog.js';
import * as Permissions from '/common/permissions.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as Sync from '/common/sync.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TabsUpdate from '/common/tabs-update.js';
import * as TSTAPI from '/common/tst-api.js';
import * as UniqueId from '/common/unique-id.js';
import '/common/bookmark.js'; // we need to load this once in the background page to register the global listener
import MetricsData from '/common/MetricsData.js';
import { Tab, TabGroup } from '/common/TreeItem.js';
import Window from '/common/Window.js';
import * as ApiTabsListener from './api-tabs-listener.js';
import * as BackgroundCache from './background-cache.js';
import * as Commands from './commands.js';
import * as ContextMenu from './context-menu.js';
import * as Migration from './migration.js';
import * as NativeTabGroups from './native-tab-groups.js';
import * as TabContextMenu from './tab-context-menu.js';
import * as Tree from './tree.js';
import * as TreeStructure from './tree-structure.js';
import './browser-action-menu.js';
import './duplicated-tab-detection.js';
import './successor-tab.js';
function log(...args) {
internalLogger('background/background', ...args);
}
// This needs to be large enough for bulk updates on multiple tabs.
const DELAY_TO_PROCESS_RESERVED_UPDATE_TASKS = 250;
export const onInit = new EventListenerManager();
export const onBuilt = new EventListenerManager();
export const onReady = new EventListenerManager();
export const onDestroy = new EventListenerManager();
export const onTreeCompletelyAttached = new EventListenerManager();
export const instanceId = `${Date.now()}-${parseInt(Math.random() * 65000)}`;
const mDarkModeMatchMedia = window.matchMedia('(prefers-color-scheme: dark)');
let mInitialized = false;
const mPreloadedCaches = new Map();
async function getAllWindows() {
const [windows, tabGroups] = await Promise.all([
browser.windows.getAll({
populate: true,
// We need to track all type windows because
// popup windows can be destination of tabs.move().
// See also: https://github.com/piroor/treestyletab/issues/3311
windowTypes: ['normal', 'panel', 'popup'],
}).catch(ApiTabs.createErrorHandler()),
browser.tabGroups.query({}),
]);
const groupsByWindow = new Map();
for (const group of tabGroups) {
const groupsInWindow = groupsByWindow.get(group.windowId) || [];
groupsInWindow.push(group);
groupsByWindow.set(group.windowId, groupsInWindow)
}
for (const win of windows) {
win.tabGroups = groupsByWindow.get(win.id) || [];
}
return windows;
}
log('init: Start queuing of messages notified via WE APIs');
ApiTabsListener.init();
const promisedRestored = waitUntilCompletelyRestored(); // this must be called synchronosly
export async function init() {
log('init: start');
MetricsData.add('init: start');
window.addEventListener('pagehide', destroy, { once: true });
onInit.dispatch();
SidebarConnection.init();
// Read caches from existing tabs at first, for better performance.
// Those promises will be resolved while waiting for waitUntilCompletelyRestored().
getAllWindows()
.then(windows => {
for (const win of windows) {
browser.sessions.getWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_TABS)
.catch(ApiTabs.createErrorSuppressor())
.then(cache => mPreloadedCaches.set(`window-${win.id}`, cache));
const tab = win.tabs[0];
browser.sessions.getTabValue(tab.id, Constants.kWINDOW_STATE_CACHED_TABS)
.catch(ApiTabs.createErrorSuppressor())
.then(cache => mPreloadedCaches.set(`tab-${tab.id}`, cache));
}
});
let promisedWindows;
log('init: Getting existing windows and tabs');
await MetricsData.addAsync('init: waiting for waitUntilCompletelyRestored, ContextualIdentities.init and configs.$loaded', Promise.all([
promisedRestored.then(() => {
// don't wait at here for better performance
promisedWindows = getAllWindows();
}),
ContextualIdentities.init(),
configs.$loaded.then(waitUntilStartupOperationsUnblocked),
]));
MetricsData.add('init: prepare');
EventListenerManager.debug = configs.debug;
Migration.migrateConfigs();
Migration.migrateBookmarkUrls();
configs.grantedRemovingTabIds = []; // clear!
MetricsData.add('init: Migration.migrateConfigs');
updatePanelUrl();
const windows = await MetricsData.addAsync('init: getting all tabs across windows', promisedWindows); // wait at here for better performance
const restoredFromCache = await MetricsData.addAsync('init: rebuildAll', rebuildAll(windows));
mPreloadedCaches.clear();
await MetricsData.addAsync('init: TreeStructure.loadTreeStructure', TreeStructure.loadTreeStructure(windows, restoredFromCache));
log('init: Start to process messages including queued ones');
ApiTabsListener.start();
Migration.tryNotifyNewFeatures();
ContextualIdentities.startObserve();
onBuilt.dispatch(); // after this line, this master process may receive "kCOMMAND_PING_TO_BACKGROUND" requests from sidebars.
MetricsData.add('init: started listening');
TabContextMenu.init();
ContextMenu.init().then(() => updateIconForBrowserTheme());
MetricsData.add('init: started initializing of context menu');
Permissions.clearRequest();
for (const windowId of restoredFromCache.keys()) {
if (!restoredFromCache[windowId])
BackgroundCache.reserveToCacheTree(windowId, 'initialize');
TabsUpdate.completeLoadingTabs(windowId);
}
for (const tab of Tab.getAllTabs(null, { iterator: true })) {
updateSubtreeCollapsed(tab);
}
for (const tab of Tab.getActiveTabs()) {
for (const ancestor of tab.$TST.ancestors) {
Tree.collapseExpandTabAndSubtree(ancestor, {
collapsed: false,
justNow: true
});
}
}
// we don't need to await that for the initialization of TST itself.
MetricsData.addAsync('init: initializing API for other addons', TSTAPI.initAsBackend());
mInitialized = true;
UniqueId.readyToDetectDuplicatedTab();
Tab.broadcastState.enabled = true;
onReady.dispatch();
BackgroundCache.activate();
TreeStructure.startTracking();
Sync.init();
await NativeTabGroups.startToMaintainTree();
await MetricsData.addAsync('init: exporting tabs to sidebars', notifyReadyToSidebars());
log(`Startup metrics for ${TabsStore.tabs.size} tabs: `, MetricsData.toString());
}
async function notifyReadyToSidebars() {
log('notifyReadyToSidebars: start');
const promisedResults = [];
for (const win of TabsStore.windows.values()) {
// Send PING to all windows whether they are detected as opened or not, because
// the connection may be established before this background page starts listening
// of messages from sidebar pages.
// See also: https://github.com/piroor/treestyletab/issues/2200
TabsUpdate.completeLoadingTabs(win.id); // failsafe
log(`notifyReadyToSidebars: to ${win.id}`);
promisedResults.push(browser.runtime.sendMessage({
type: Constants.kCOMMAND_NOTIFY_BACKGROUND_READY,
windowId: win.id,
exported: win.export(true), // send tabs together to optimizie further initialization tasks in the sidebar
}).catch(ApiTabs.createErrorSuppressor()));
}
return Promise.all(promisedResults);
}
async function updatePanelUrl(theme) {
const url = new URL(Constants.kSHORTHAND_URIS.tabbar);
url.searchParams.set('style', configs.style);
url.searchParams.set('reloadMaskImage', !!configs.enableWorkaroundForBug1763420_reloadMaskImage);
if (!theme)
theme = await browser.theme.getCurrent();
if (browser.sidebarAction)
browser.sidebarAction.setPanel({ panel: url.href });
/*
const url = new URL(Constants.kSHORTHAND_URIS.tabbar);
url.searchParams.set('style', configs.style);
if (browser.sidebarAction)
browser.sidebarAction.setPanel({ panel: url.href });
*/
}
async function waitUntilCompletelyRestored() {
log('waitUntilCompletelyRestored');
const initialTabs = await browser.tabs.query({});
await Promise.all([
MetricsData.addAsync('waitUntilCompletelyRestored: existing tabs ', Promise.all(
initialTabs.map(tab => waitUntilPersistentIdBecomeAvailable(tab.id).catch(_error => {}))
)),
MetricsData.addAsync('waitUntilCompletelyRestored: opening tabs ', new Promise((resolve, _reject) => {
let promises = [];
let timeout;
let resolver;
let onNewTabRestored = async (tab, _info = {}) => {
clearTimeout(timeout);
log('new restored tab is detected.');
promises.push(waitUntilPersistentIdBecomeAvailable(tab.id).catch(_error => {}));
// Read caches from restored tabs while waiting, for better performance.
browser.sessions.getWindowValue(tab.windowId, Constants.kWINDOW_STATE_CACHED_TABS)
.catch(ApiTabs.createErrorSuppressor())
.then(cache => mPreloadedCaches.set(`window-${tab.windowId}`, cache));
browser.sessions.getTabValue(tab.id, Constants.kWINDOW_STATE_CACHED_TABS)
.catch(ApiTabs.createErrorSuppressor())
.then(cache => mPreloadedCaches.set(`tab-${tab.id}`, cache));
//uniqueId = uniqueId?.id || '?'; // not used
timeout = setTimeout(resolver, 100);
};
browser.tabs.onCreated.addListener(onNewTabRestored);
resolver = (async () => {
log(`timeout: all ${promises.length} tabs are restored. `, promises);
browser.tabs.onCreated.removeListener(onNewTabRestored);
timeout = resolver = onNewTabRestored = undefined;
await Promise.all(promises);
promises = undefined;
resolve();
});
timeout = setTimeout(resolver, 500);
})),
]);
}
async function waitUntilPersistentIdBecomeAvailable(tabId, retryCount = 0) {
if (retryCount > 10) {
console.log(`could not get persistent ID for ${tabId}`);
return false;
}
const uniqueId = await browser.sessions.getTabValue(tabId, Constants.kPERSISTENT_ID);
if (!uniqueId)
return wait(100).then(() => waitUntilPersistentIdBecomeAvailable(tabId, retryCount + 1));
return true;
}
function destroy() {
browser.runtime.sendMessage({
type: TSTAPI.kUNREGISTER_SELF
}).catch(ApiTabs.createErrorSuppressor());
// This API doesn't work as expected because it is not notified to
// other addons actually when browser.runtime.sendMessage() is called
// on pagehide or something unloading event.
TSTAPI.broadcastMessage({
type: TSTAPI.kNOTIFY_SHUTDOWN
}).catch(ApiTabs.createErrorSuppressor());
onDestroy.dispatch();
ApiTabsListener.destroy();
ContextualIdentities.endObserve();
}
async function rebuildAll(windows) {
if (!windows)
windows = await getAllWindows();
const restoredFromCache = new Map();
await Promise.all(windows.map(async win => {
await MetricsData.addAsync(`rebuildAll: tabs in window ${win.id}`, async () => {
let trackedWindow = TabsStore.windows.get(win.id);
if (!trackedWindow)
trackedWindow = Window.init(win.id, win.tabGroups.map(TabGroup.init));
for (const tab of win.tabs) {
Tab.track(tab);
Tab.init(tab, { existing: true });
tryStartHandleAccelKeyOnTab(tab);
}
try {
if (configs.useCachedTree) {
log(`trying to restore window ${win.id} from cache`);
const restored = await MetricsData.addAsync(`rebuildAll: restore tabs in window ${win.id} from cache`, BackgroundCache.restoreWindowFromEffectiveWindowCache(win.id, {
owner: win.tabs[win.tabs.length - 1],
tabs: win.tabs,
caches: mPreloadedCaches
}));
restoredFromCache.set(win.id, restored);
log(`window ${win.id}: restored from cache?: `, restored);
if (restored)
return;
}
}
catch(e) {
log(`failed to restore tabs for ${win.id} from cache `, e);
}
try {
log(`build tabs for ${win.id} from scratch`);
Window.init(win.id, win.tabGroups.map(TabGroup.init));
const promises = [];
for (let tab of win.tabs) {
tab = Tab.get(tab.id);
tab.$TST.clear(); // clear dirty restored states
promises.push(
tab.$TST.getPermanentStates()
.then(states => {
tab.$TST.states = new Set(states);
tab.$TST.addState(Constants.kTAB_STATE_PENDING);
})
.catch(console.error)
.then(() => {
TabsUpdate.updateTab(tab, tab, { forceApply: true });
})
);
tryStartHandleAccelKeyOnTab(tab);
}
await Promise.all(promises);
}
catch(e) {
log(`failed to build tabs for ${win.id}`, e);
}
restoredFromCache.set(win.id, false);
});
for (const tab of Tab.getGroupTabs(win.id, { iterator: true })) {
if (!tab.discarded)
tab.$TST.temporaryMetadata.set('shouldReloadOnSelect', true);
}
}));
return restoredFromCache;
}
export async function reload(options = {}) {
mPreloadedCaches.clear();
for (const win of TabsStore.windows.values()) {
win.clear();
}
TabsStore.clear();
const windows = await getAllWindows();
await MetricsData.addAsync('reload: rebuildAll', rebuildAll(windows));
await MetricsData.addAsync('reload: TreeStructure.loadTreeStructure', TreeStructure.loadTreeStructure(windows));
if (!options.all)
return;
for (const win of TabsStore.windows.values()) {
if (!SidebarConnection.isOpen(win.id))
continue;
log('reload all sidebars: ', new Error().stack);
browser.runtime.sendMessage({
type: Constants.kCOMMAND_RELOAD
}).catch(ApiTabs.createErrorSuppressor());
}
}
export async function tryStartHandleAccelKeyOnTab(tab) {
if (!TabsStore.ensureLivingItem(tab))
return;
const granted = await Permissions.isGranted(Permissions.ALL_URLS);
if (!granted ||
/^(about|chrome|resource):/.test(tab.url))
return;
try {
//log(`tryStartHandleAccelKeyOnTab: initialize tab ${tab.id}`);
if (browser.scripting) // Manifest V3
browser.scripting.executeScript({
target: {
tabId: tab.id,
allFrames: true,
},
files: ['/common/handle-accel-key.js'],
}).catch(ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError));
else
browser.tabs.executeScript(tab.id, {
file: '/common/handle-accel-key.js',
allFrames: true,
matchAboutBlank: true,
runAt: 'document_start'
}).catch(ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError));
}
catch(error) {
console.log(error);
}
}
export function reserveToUpdateInsertionPosition(tabOrTabs) {
const tabs = Array.isArray(tabOrTabs) ? tabOrTabs : [tabOrTabs] ;
for (const tab of tabs) {
if (!TabsStore.ensureLivingItem(tab))
continue;
const reserved = reserveToUpdateInsertionPosition.reserved.get(tab.windowId) || {
timer: null,
tabs: new Set()
};
if (reserved.timer)
clearTimeout(reserved.timer);
reserved.tabs.add(tab);
reserved.timer = setTimeout(() => {
reserveToUpdateInsertionPosition.reserved.delete(tab.windowId);
for (const tab of reserved.tabs) {
if (!tab.$TST)
continue;
updateInsertionPosition(tab);
}
}, DELAY_TO_PROCESS_RESERVED_UPDATE_TASKS);
reserveToUpdateInsertionPosition.reserved.set(tab.windowId, reserved);
}
}
reserveToUpdateInsertionPosition.reserved = new Map();
async function updateInsertionPosition(tab) {
if (!TabsStore.ensureLivingItem(tab))
return;
const prev = tab.hidden ? tab.$TST.unsafePreviousTab : tab.$TST.previousTab;
if (prev)
browser.sessions.setTabValue(
tab.id,
Constants.kPERSISTENT_INSERT_AFTER,
prev.$TST.uniqueId.id
).catch(ApiTabs.createErrorSuppressor(
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
));
else
browser.sessions.removeTabValue(
tab.id,
Constants.kPERSISTENT_INSERT_AFTER
).catch(ApiTabs.createErrorSuppressor(
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
));
// This code should be removed after legacy data are cleared enough, maybe after Firefox 128 is released.
browser.sessions.removeTabValue(
tab.id,
Constants.kPERSISTENT_INSERT_AFTER_LEGACY
).catch(ApiTabs.createErrorSuppressor(
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
));
const next = tab.hidden ? tab.$TST.unsafeNextTab : tab.$TST.nextTab;
if (next)
browser.sessions.setTabValue(
tab.id,
Constants.kPERSISTENT_INSERT_BEFORE,
next.$TST.uniqueId.id
).catch(ApiTabs.createErrorSuppressor(
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
));
else
browser.sessions.removeTabValue(
tab.id,
Constants.kPERSISTENT_INSERT_BEFORE
).catch(ApiTabs.createErrorSuppressor(
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
));
}
export function reserveToUpdateAncestors(tabOrTabs) {
const tabs = Array.isArray(tabOrTabs) ? tabOrTabs : [tabOrTabs] ;
for (const tab of tabs) {
if (!TabsStore.ensureLivingItem(tab))
continue;
const reserved = reserveToUpdateAncestors.reserved.get(tab.windowId) || {
timer: null,
tabs: new Set()
};
if (reserved.timer)
clearTimeout(reserved.timer);
reserved.tabs.add(tab);
reserved.timer = setTimeout(() => {
reserveToUpdateAncestors.reserved.delete(tab.windowId);
for (const tab of reserved.tabs) {
if (!tab.$TST)
continue;
updateAncestors(tab);
}
}, DELAY_TO_PROCESS_RESERVED_UPDATE_TASKS);
reserveToUpdateAncestors.reserved.set(tab.windowId, reserved);
}
}
reserveToUpdateAncestors.reserved = new Map();
async function updateAncestors(tab) {
if (!TabsStore.ensureLivingItem(tab))
return;
const ancestors = tab.$TST.ancestors.map(ancestor => ancestor.$TST.uniqueId.id);
log(`updateAncestors: save persistent ancestors for ${tab.id}: `, ancestors);
browser.sessions.setTabValue(
tab.id,
Constants.kPERSISTENT_ANCESTORS,
ancestors
).catch(ApiTabs.createErrorSuppressor(
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
));
}
export function reserveToUpdateChildren(tabOrTabs) {
const tabs = Array.isArray(tabOrTabs) ? tabOrTabs : [tabOrTabs] ;
for (const tab of tabs) {
if (!TabsStore.ensureLivingItem(tab))
continue;
const reserved = reserveToUpdateChildren.reserved.get(tab.windowId) || {
timer: null,
tabs: new Set()
};
if (reserved.timer)
clearTimeout(reserved.timer);
reserved.tabs.add(tab);
reserved.timer = setTimeout(() => {
reserveToUpdateChildren.reserved.delete(tab.windowId);
for (const tab of reserved.tabs) {
if (!tab.$TST)
continue;
updateChildren(tab);
}
}, DELAY_TO_PROCESS_RESERVED_UPDATE_TASKS);
reserveToUpdateChildren.reserved.set(tab.windowId, reserved);
}
}
reserveToUpdateChildren.reserved = new Map();
async function updateChildren(tab) {
if (!TabsStore.ensureLivingItem(tab))
return;
const children = tab.$TST.children.map(child => child.$TST.uniqueId.id);
log(`updateChildren: save persistent children for ${tab.id}: `, children);
browser.sessions.setTabValue(
tab.id,
Constants.kPERSISTENT_CHILDREN,
children
).catch(ApiTabs.createErrorSuppressor(
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
));
}
function reserveToUpdateSubtreeCollapsed(tab) {
if (!mInitialized ||
!TabsStore.ensureLivingItem(tab))
return;
const reserved = reserveToUpdateSubtreeCollapsed.reserved.get(tab.windowId) || {
timer: null,
tabs: new Set()
};
if (reserved.timer)
clearTimeout(reserved.timer);
reserved.tabs.add(tab);
reserved.timer = setTimeout(() => {
reserveToUpdateSubtreeCollapsed.reserved.delete(tab.windowId);
for (const tab of reserved.tabs) {
if (!tab.$TST)
continue;
updateSubtreeCollapsed(tab);
}
}, DELAY_TO_PROCESS_RESERVED_UPDATE_TASKS);
reserveToUpdateSubtreeCollapsed.reserved.set(tab.windowId, reserved);
}
reserveToUpdateSubtreeCollapsed.reserved = new Map();
async function updateSubtreeCollapsed(tab) {
if (!TabsStore.ensureLivingItem(tab))
return;
tab.$TST.toggleState(Constants.kTAB_STATE_SUBTREE_COLLAPSED, tab.$TST.subtreeCollapsed, { permanently: true });
}
export async function confirmToCloseTabs(tabs, { windowId, configKey, messageKey, titleKey, minConfirmCount } = {}) {
if (!windowId)
windowId = tabs[0].windowId;
const grantedIds = new Set(configs.grantedRemovingTabIds);
let count = 0;
const tabIds = [];
tabs = tabs.map(tab => Tab.get(tab?.id)).filter(tab => {
if (tab && !grantedIds.has(tab.id)) {
count++;
tabIds.push(tab.id);
return true;
}
return false;
});
if (!configKey)
configKey = 'warnOnCloseTabs';
const shouldConfirm = configs[configKey];
const deltaFromLastConfirmation = Date.now() - configs.lastConfirmedToCloseTabs;
log('confirmToCloseTabs ', { tabIds, count, windowId, configKey, grantedIds, shouldConfirm, deltaFromLastConfirmation, minConfirmCount });
if (count <= (typeof minConfirmCount == 'number' ? minConfirmCount : 1) ||
!shouldConfirm ||
deltaFromLastConfirmation < 500) {
log('confirmToCloseTabs: skip confirmation and treated as granted');
return true;
}
const win = await browser.windows.get(windowId);
const listing = configs.warnOnCloseTabsWithListing ?
Dialog.tabsToHTMLList(tabs, {
maxHeight: Math.round(win.height * 0.8),
maxWidth: Math.round(win.width * 0.75)
}) :
'';
const result = await Dialog.show(win, {
content: `
<div>${sanitizeForHTMLText(browser.i18n.getMessage(messageKey || 'warnOnCloseTabs_message', [count]))}</div>${listing}
`.trim(),
buttons: [
browser.i18n.getMessage('warnOnCloseTabs_close'),
browser.i18n.getMessage('warnOnCloseTabs_cancel')
],
checkMessage: browser.i18n.getMessage('warnOnCloseTabs_warnAgain'),
checked: true,
modal: !configs.debug, // for popup
type: 'common-dialog', // for popup
url: ((await Permissions.isGranted(Permissions.ALL_URLS)) ? null : '/resources/blank.html'), // for popup
title: browser.i18n.getMessage(titleKey || 'warnOnCloseTabs_title'), // for popup
onShownInPopup(container) {
setTimeout(() => { // because window.requestAnimationFrame is decelerate for an invisible document.
// this need to be done on the next tick, to use the height of
// the box for calculation of dialog size
const style = container.querySelector('ul').style;
style.height = '0px'; // this makes the box shrinkable
style.maxHeight = 'none';
style.minHeight = '0px';
}, 0);
}
});
log('confirmToCloseTabs: result = ', result);
switch (result.buttonIndex) {
case 0:
if (!result.checked)
configs[configKey] = false;
configs.grantedRemovingTabIds = Array.from(new Set((configs.grantedRemovingTabIds || []).concat(tabIds)));
log('confirmToCloseTabs: granted ', configs.grantedRemovingTabIds);
reserveToClearGrantedRemovingTabs();
return true;
default:
return false;
}
}
Commands.onTabsClosing.addListener((tabIds, options = {}) => {
return confirmToCloseTabs(tabIds.map(Tab.get), options);
});
function reserveToClearGrantedRemovingTabs() {
const lastGranted = configs.grantedRemovingTabIds.join(',');
setTimeout(() => {
if (configs.grantedRemovingTabIds.join(',') == lastGranted)
configs.grantedRemovingTabIds = [];
}, 1000);
}
Tab.onCreated.addListener((tab, info = {}) => {
if (!info.duplicated)
return;
// Duplicated tab has its own tree structure information inherited
// from the original tab, but they must be cleared.
reserveToUpdateAncestors(tab);
reserveToUpdateChildren(tab);
reserveToUpdateInsertionPosition([
tab,
tab.hidden ? tab.$TST.unsafePreviousTab : tab.$TST.previousTab,
tab.hidden ? tab.$TST.unsafeNextTab : tab.$TST.nextTab
]);
});
Tab.onUpdated.addListener((tab, changeInfo) => {
if (!mInitialized)
return;
// Loading of "about:(unknown type)" won't report new URL via tabs.onUpdated,
// so we need to see the complete tab object.
const status = changeInfo.status || tab?.status;
const url = changeInfo.url ? changeInfo.url :
status == 'complete' && tab ? tab.url : '';
if (tab &&
Constants.kSHORTHAND_ABOUT_URI.test(url)) {
const shorthand = RegExp.$1;
const oldUrl = tab.url;
wait(100).then(() => { // redirect with delay to avoid infinite loop of recursive redirections.
if (tab.url != oldUrl)
return;
browser.tabs.update(tab.id, {
url: url.replace(Constants.kSHORTHAND_ABOUT_URI, Constants.kSHORTHAND_URIS[shorthand] || 'about:blank')
}).catch(ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError));
if (shorthand == 'group')
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
});
}
if (changeInfo.status || changeInfo.url)
tryStartHandleAccelKeyOnTab(tab);
});
Tab.onShown.addListener(tab => {
if (!mInitialized)
return;
if (configs.fixupTreeOnTabVisibilityChanged) {
reserveToUpdateAncestors(tab);
reserveToUpdateChildren(tab);
}
reserveToUpdateInsertionPosition([
tab,
tab.hidden ? tab.$TST.unsafePreviousTab : tab.$TST.previousTab,
tab.hidden ? tab.$TST.unsafeNextTab : tab.$TST.nextTab
]);
});
Tab.onMutedStateChanged.addListener((root, toBeMuted) => {
if (!mInitialized)
return;
// Spread muted state of a parent tab to its collapsed descendants
if (!root.$TST.subtreeCollapsed ||
// We don't need to spread muted state to descendants of multiselected
// tabs here, because tabs.update() was called with all multiselected tabs.
root.$TST.multiselected ||
// We should not spread muted state to descendants of collapsed tab
// recursively, because they were already controlled from a visible
// ancestor.
root.$TST.collapsed)
return;
const tabs = root.$TST.descendants;
for (const tab of tabs) {
const playing = tab.$TST.soundPlaying;
const muted = tab.$TST.muted;
log(`tab ${tab.id}: playing=${playing}, muted=${muted}`);
if (configs.spreadMutedStateOnlyToSoundPlayingTabs &&
!playing &&
playing != toBeMuted)
continue;
log(` => set muted=${toBeMuted}`);
browser.tabs.update(tab.id, {
muted: toBeMuted
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
const add = [];
const remove = [];
if (toBeMuted) {
add.push(Constants.kTAB_STATE_MUTED);
tab.$TST.addState(Constants.kTAB_STATE_MUTED);
}
else {
remove.push(Constants.kTAB_STATE_MUTED);
tab.$TST.removeState(Constants.kTAB_STATE_MUTED);
}
if (tab.audible && !toBeMuted) {
add.push(Constants.kTAB_STATE_SOUND_PLAYING);
tab.$TST.addState(Constants.kTAB_STATE_SOUND_PLAYING);
}
else {
remove.push(Constants.kTAB_STATE_SOUND_PLAYING);
tab.$TST.removeState(Constants.kTAB_STATE_SOUND_PLAYING);
}
// tabs.onUpdated is too slow, so users will be confused
// from still-not-updated tabs (in other words, they tabs
// are unresponsive for quick-clicks).
Tab.broadcastState(tab, { add, remove });
}
});
Tab.onTabInternallyMoved.addListener((tab, info = {}) => {
reserveToUpdateInsertionPosition([
tab,
tab.hidden ? tab.$TST.unsafePreviousTab : tab.$TST.previousTab,
tab.hidden ? tab.$TST.unsafeNextTab : tab.$TST.nextTab,
info.oldPreviousTab,
info.oldNextTab
]);
});
Tab.onMoved.addListener((tab, moveInfo) => {
if (moveInfo.movedInBulk)
return;
reserveToUpdateInsertionPosition([
tab,
moveInfo.oldPreviousTab,
moveInfo.oldNextTab,
tab.hidden ? tab.$TST.unsafePreviousTab : tab.$TST.previousTab,
tab.hidden ? tab.$TST.unsafeNextTab : tab.$TST.nextTab
]);
});
Tree.onAttached.addListener(async (tab, attachInfo) => {
await tab.$TST.opened;
if (!TabsStore.ensureLivingItem(tab) || // not removed while waiting
tab.$TST.parent != attachInfo.parent) // not detached while waiting
return;
if (attachInfo.newlyAttached)
reserveToUpdateAncestors([tab].concat(tab.$TST.descendants));
reserveToUpdateChildren(tab.$TST.parent);
reserveToUpdateInsertionPosition([
tab,
tab.$TST.nextTab,
tab.$TST.previousTab
]);
});
Tree.onDetached.addListener((tab, detachInfo) => {
reserveToUpdateAncestors([tab].concat(tab.$TST.descendants));
reserveToUpdateChildren(detachInfo.oldParentTab);
});
Tree.onSubtreeCollapsedStateChanging.addListener((tab, _info) => { reserveToUpdateSubtreeCollapsed(tab); });
const BASE_ICONS = {
'16': '/resources/16x16.svg',
'20': '/resources/20x20.svg',
'24': '/resources/24x24.svg',
'32': '/resources/32x32.svg',
};
async function updateIconForBrowserTheme(theme) {
// generate icons with theme specific color
const toolbarIcons = {};
const menuIcons = {};
const sidebarIcons = {};
if (!theme) {
const win = await browser.windows.getLastFocused();
theme = await browser.theme.getCurrent(win.id);
}
log('updateIconForBrowserTheme: ', theme);
if (theme.colors) {
const toolbarIconColor = theme.colors.icons || theme.colors.toolbar_text || theme.colors.tab_text || theme.colors.tab_background_text || theme.colors.bookmark_text || theme.colors.textcolor;
const menuIconColor = theme.colors.popup_text || toolbarIconColor;
const sidebarIconColor = theme.colors.sidebar_text || toolbarIconColor;
log(' => ', { toolbarIconColor, menuIconColor, sidebarIconColor }, theme.colors);
await Promise.all(Array.from(Object.entries(BASE_ICONS), async ([size, url]) => {
const response = await fetch(url);
const body = await response.text();
const toolbarIconSource = body.replace(/transparent\s*\/\*\s*TO BE REPLACED WITH THEME COLOR\s*\*\//g, toolbarIconColor);
toolbarIcons[size] = `data:image/svg+xml,${escape(toolbarIconSource)}#toolbar-theme`;
const menuIconSource = body.replace(/transparent\s*\/\*\s*TO BE REPLACED WITH THEME COLOR\s*\*\//g, menuIconColor);
menuIcons[size] = `data:image/svg+xml,${escape(menuIconSource)}#default-theme`;
const sidebarIconSource = body.replace(/transparent\s*\/\*\s*TO BE REPLACED WITH THEME COLOR\s*\*\//g, sidebarIconColor);
sidebarIcons[size] = `data:image/svg+xml,${escape(sidebarIconSource)}#default-theme`;
}));
}
else {
for (const [size, url] of Object.entries(BASE_ICONS)) {
toolbarIcons[size] = `${url}#toolbar`;
menuIcons[size] = sidebarIcons[size] = `${url}#default`;
}
}
log('updateIconForBrowserTheme: applying icons: ', {
toolbarIcons,
menuIcons,
sidebarIcons,
});
await Promise.all([
...ContextMenu.getItemIdsWithIcon().map(id => browser.menus.update(id, { icons: menuIcons })),
browser.menus.refresh().catch(ApiTabs.createErrorSuppressor()),
browser.action?.setIcon({ path: toolbarIcons }), // Manifest v2
browser.browserAction?.setIcon({ path: toolbarIcons }), // Manifest v3
browser.sidebarAction?.setIcon({ path: sidebarIcons }),
]);
}
browser.theme.onUpdated.addListener(updateInfo => {
updateIconForBrowserTheme(updateInfo.theme);
});
mDarkModeMatchMedia.addListener(async _event => {
updateIconForBrowserTheme();
});
configs.$addObserver(key => {
switch (key) {
case 'style':
updatePanelUrl();
break;
case 'debug':
EventListenerManager.debug = configs.debug;
break;
case 'testKey': // for tests/utils.js
browser.runtime.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TEST_KEY_CHANGED,
value: configs.testKey
});
break;
}
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,764 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
configs,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Bookmark from '/common/bookmark.js';
import * as Sync from '/common/sync.js';
import * as TSTAPI from '/common/tst-api.js';
import { Tab } from '/common/TreeItem.js';
import * as Commands from './commands.js';
import * as TabContextMenu from './tab-context-menu.js';
function log(...args) {
internalLogger('background/context-menu', ...args);
}
export async function init() {
return Promise.all([
addTabItems(),
addBookmarkItems(),
]);
}
const SAFE_CREATE_PROPERTIES = [
'checked',
'contexts',
'documentUrlPatterns',
'enabled',
'icons',
'id',
'parentId',
'title',
'type',
'viewTypes',
'visible'
];
function getSafeCreateParams(params) {
const safeParams = {};
for (const property of SAFE_CREATE_PROPERTIES) {
if (property in params)
safeParams[property] = params[property];
}
return safeParams;
}
const manifest = browser.runtime.getManifest();
const kROOT_TAB_ITEM = 'ws';
const kROOT_BOOKMARK_ITEM = 'ws-bookmark';
const mTabItemsById = {
'reloadTree': {
title: browser.i18n.getMessage('context_reloadTree_label'),
titleMultiselected: browser.i18n.getMessage('context_reloadTree_label_multiselected')
},
'reloadDescendants': {
title: browser.i18n.getMessage('context_reloadDescendants_label'),
titleMultiselected: browser.i18n.getMessage('context_reloadDescendants_label_multiselected')
},
// This item won't be handled by the onClicked handler, so you may need to handle it with something experiments API.
'unblockAutoplayTree': {
title: browser.i18n.getMessage('context_unblockAutoplayTree_label'),
titleMultiselected: browser.i18n.getMessage('context_unblockAutoplayTree_label_multiselected'),
requireAutoplayBlockedTab: true,
},
// This item won't be handled by the onClicked handler, so you may need to handle it with something experiments API.
'unblockAutoplayDescendants': {
title: browser.i18n.getMessage('context_unblockAutoplayDescendants_label'),
titleMultiselected: browser.i18n.getMessage('context_unblockAutoplayDescendants_label_multiselected'),
requireAutoplayBlockedDescendant: true,
},
'toggleMuteTree': {
titleMuteTree: browser.i18n.getMessage('context_toggleMuteTree_label_mute'),
titleMultiselectedMuteTree: browser.i18n.getMessage('context_toggleMuteTree_label_multiselected_mute'),
titleUnmuteTree: browser.i18n.getMessage('context_toggleMuteTree_label_unmute'),
titleMultiselectedUnmuteTree: browser.i18n.getMessage('context_toggleMuteTree_label_multiselected_unmute')
},
'toggleMuteDescendants': {
titleMuteDescendant: browser.i18n.getMessage('context_toggleMuteDescendants_label_mute'),
titleMultiselectedMuteDescendant: browser.i18n.getMessage('context_toggleMuteDescendants_label_multiselected_mute'),
titleUnmuteDescendant: browser.i18n.getMessage('context_toggleMuteDescendants_label_unmute'),
titleMultiselectedUnmuteDescendant: browser.i18n.getMessage('context_toggleMuteDescendants_label_multiselected_unmute')
},
'separatorAfterReload': {
type: 'separator'
},
'closeTree': {
title: browser.i18n.getMessage('context_closeTree_label'),
titleMultiselected: browser.i18n.getMessage('context_closeTree_label_multiselected')
},
'closeDescendants': {
title: browser.i18n.getMessage('context_closeDescendants_label'),
titleMultiselected: browser.i18n.getMessage('context_closeDescendants_label_multiselected'),
requireTree: true,
},
'closeOthers': {
title: browser.i18n.getMessage('context_closeOthers_label'),
titleMultiselected: browser.i18n.getMessage('context_closeOthers_label_multiselected')
},
'separatorAfterClose': {
type: 'separator'
},
'toggleSticky': {
titleStick: browser.i18n.getMessage('context_toggleSticky_label_stick'),
titleMultiselectedStick: browser.i18n.getMessage('context_toggleSticky_label_multiselected_stick'),
titleUnstick: browser.i18n.getMessage('context_toggleSticky_label_unstick'),
titleMultiselectedUnstick: browser.i18n.getMessage('context_toggleSticky_label_multiselected_unstick'),
requireNormal: true,
},
'separatorAfterToggleSticky': {
type: 'separator'
},
'collapseTree': {
title: browser.i18n.getMessage('context_collapseTree_label'),
titleMultiselected: browser.i18n.getMessage('context_collapseTree_label_multiselected'),
requireTree: true,
},
'collapseTreeRecursively': {
title: browser.i18n.getMessage('context_collapseTreeRecursively_label'),
titleMultiselected: browser.i18n.getMessage('context_collapseTreeRecursively_label_multiselected'),
requireTree: true,
},
'collapseAll': {
title: browser.i18n.getMessage('context_collapseAll_label'),
hideOnMultiselected: true
},
'expandTree': {
title: browser.i18n.getMessage('context_expandTree_label'),
titleMultiselected: browser.i18n.getMessage('context_expandTree_label_multiselected'),
requireTree: true,
},
'expandTreeRecursively': {
title: browser.i18n.getMessage('context_expandTreeRecursively_label'),
titleMultiselected: browser.i18n.getMessage('context_expandTreeRecursively_label_multiselected'),
requireTree: true,
},
'expandAll': {
title: browser.i18n.getMessage('context_expandAll_label'),
hideOnMultiselected: true
},
'separatorAfterCollapseExpand': {
type: 'separator'
},
'bookmarkTree': {
title: browser.i18n.getMessage('context_bookmarkTree_label'),
titleMultiselected: browser.i18n.getMessage('context_bookmarkTree_label_multiselected')
},
'sendTreeToDevice': {
title: browser.i18n.getMessage('context_sendTreeToDevice_label'),
titleMultiselected: browser.i18n.getMessage('context_sendTreeToDevice_label_multiselected')
},
'separatorAfterBookmark': {
type: 'separator'
},
'collapsed': {
title: browser.i18n.getMessage('context_collapsed_label'),
requireTree: true,
type: 'checkbox'
},
'pinnedTab': {
title: browser.i18n.getMessage('context_pinnedTab_label'),
type: 'radio'
},
'unpinnedTab': {
title: browser.i18n.getMessage('context_unpinnedTab_label'),
type: 'radio'
}
};
const mTabItems = [];
const mGroupedTabItems = [];
const mGroupedTabItemsById = {};
for (const id of Object.keys(mTabItemsById)) {
const item = mTabItemsById[id];
item.id = id;
item.configKey = `context_${id}`;
item.checked = false; // initialize as unchecked
item.enabled = true;
// Access key is not supported by WE API.
// See also: https://bugzilla.mozilla.org/show_bug.cgi?id=1320462
item.titleWithoutAccesskey = item.title?.replace(/\(&[a-z]\)|&([a-z])/i, '$1');
item.titleMultiselectedWithoutAccesskey = item.titleMultiselected?.replace(/\(&[a-z]\)|&([a-z])/i, '$1');
item.type = item.type || 'normal';
item.contexts = ['tab'];
item.lastVisible = item.visible = false;
item.lastTitle = item.title;
mTabItems.push(item);
const groupedItem = {
...item,
id: `grouped:${id}`,
parentId: kROOT_TAB_ITEM
};
mGroupedTabItems.push(groupedItem);
mGroupedTabItemsById[groupedItem.id] = groupedItem;
}
const mTabSeparator = {
id: `separatprBefore${kROOT_TAB_ITEM}`,
type: 'separator',
contexts: ['tab'],
viewTypes: ['sidebar'],
documentUrlPatterns: [`moz-extension://${location.host}/*`],
visible: false,
lastVisible: false
};
const mTabRootItem = {
id: kROOT_TAB_ITEM,
type: 'normal',
contexts: ['tab'],
title: browser.i18n.getMessage('context_menu_label'),
icons: manifest.icons,
visible: false,
lastVisible: false
};
const mAllTabItems = [
mTabSeparator,
mTabRootItem,
...mTabItems,
...mGroupedTabItems
];
function addTabItems() {
const promises = [];
if (addTabItems.done) {
for (const item of mAllTabItems) {
promises.push(browser.menus.remove(item.id));
}
}
for (const item of mAllTabItems) {
const params = getSafeCreateParams(item);
promises.push(browser.menus.create(params));
if (item.id == mTabSeparator.id ||
addTabItems.done)
continue;
promises.push(TabContextMenu.onMessageExternal({
type: TSTAPI.kCONTEXT_MENU_CREATE,
params
}, browser.runtime));
}
addTabItems.done = true;
return Promise.all(promises);
}
addTabItems.done = false;
const mBookmarkItemsById = {
openAllBookmarksWithStructure: {
title: browser.i18n.getMessage('context_openAllBookmarksWithStructure_label')
},
openAllBookmarksWithStructureRecursively: {
title: browser.i18n.getMessage('context_openAllBookmarksWithStructureRecursively_label')
}
};
const mBookmarkItems = [];
const mGroupedBookmarkItems = []
for (const id of Object.keys(mBookmarkItemsById)) {
const item = mBookmarkItemsById[id];
item.id = id;
item.contexts = ['bookmark'];
item.configKey = `context_${id}`;
const groupedItem = {
...item,
id: `grouped:${id}`,
parentId: kROOT_BOOKMARK_ITEM,
ungroupedItem: item
};
item.icons = manifest.icons;
mBookmarkItems.push(item);
mBookmarkItemsById[groupedItem.id] = groupedItem;
mGroupedBookmarkItems.push(groupedItem);
}
const mBookmarkSeparator = {
id: `separatprBefore${kROOT_BOOKMARK_ITEM}`,
type: 'separator',
contexts: ['bookmark'],
viewTypes: ['sidebar'],
documentUrlPatterns: [`moz-extension://${location.host}/*`],
visible: false,
lastVisible: false
};
const mBookmarkRootItem = {
id: kROOT_BOOKMARK_ITEM,
type: 'normal',
contexts: ['bookmark'],
title: manifest.name,
icons: manifest.icons,
visible: false,
lastVisible: false
};
const mAllBookmarkItems = [
mBookmarkSeparator,
mBookmarkRootItem,
...mBookmarkItems,
...mGroupedBookmarkItems
];
function addBookmarkItems() {
const promises = [];
if (addBookmarkItems.done) {
for (const item of mAllBookmarkItems) {
promises.push(browser.menus.remove(item.id));
}
}
for (const item of mAllBookmarkItems) {
promises.push(browser.menus.create(getSafeCreateParams(item)));
}
addBookmarkItems.done = true;
return Promise.all(promises);
}
addBookmarkItems.done = false;
// Re-register items to put them after
// top level items added by other addons.
TabContextMenu.onTopLevelItemAdded.addListener(reserveToRefreshItems);
function reserveToRefreshItems() {
if (reserveToRefreshItems.invoked)
return;
reserveToRefreshItems.invoked = true;
setTimeout(() => { // because window.requestAnimationFrame is decelerate for an invisible document.
reserveToRefreshItems.invoked = false;
addTabItems();
addBookmarkItems();
}, 0);
}
function updateItem(id, params) {
log('updateItem ', id, params);
browser.menus.update(id, params).catch(ApiTabs.createErrorSuppressor());
TabContextMenu.onMessageExternal({
type: TSTAPI.kCONTEXT_MENU_UPDATE,
params: [id, params]
}, browser.runtime);
}
function updateItemsVisibility(items, { forceVisible = null, multiselected = false, hasUnmutedTab = false, hasUnmutedDescendant = false, hasAutoplayBlockedTab = false, hasAutoplayBlockedDescendant = false, sticky = false, hidden = false } = {}) {
log('updateItemsVisibility ', items, { forceVisible, multiselected, hasUnmutedTab, hasUnmutedDescendant, hasAutoplayBlockedTab, hasAutoplayBlockedDescendant, sticky });
let updated = false;
let visibleItemsCount = 0;
let visibleNormalItemsCount = 0;
let lastSeparator;
for (const item of items) {
if (item.type == 'separator') {
if (lastSeparator) {
if (lastSeparator.lastVisible) {
updateItem(lastSeparator.id, { visible: false });
lastSeparator.lastVisible = false;
updated = true;
}
}
lastSeparator = item;
}
else {
const title = Commands.getMenuItemTitle(item, { multiselected, hasUnmutedTab, hasUnmutedDescendant, sticky });
let visible = !hidden && (!(item.configKey in configs) || configs[item.configKey] || false);
log('checking ', item.id, {
config: visible,
multiselected: item.hideOnMultiselected && multiselected,
lastVisible: item.lastVisible,
forceVisible,
});
if (forceVisible !== null && forceVisible !== undefined)
visible = forceVisible;
if ((item.hideOnMultiselected && multiselected) ||
(item.requireAutoplayBlockedTab && !hasAutoplayBlockedTab) ||
(item.requireAutoplayBlockedDescendant && !hasAutoplayBlockedDescendant))
visible = false;
if (visible) {
if (lastSeparator) {
updateItem(lastSeparator.id, { visible: visibleNormalItemsCount > 0 });
lastSeparator.lastVisible = true;
lastSeparator = null;
updated = true;
visibleNormalItemsCount = 0;
}
visibleNormalItemsCount++;
visibleItemsCount++;
}
const updatedParams = {};
if (visible !== item.lastVisible) {
updatedParams.visible = visible;
item.lastVisible = visible;
}
if (title !== item.lastTitle) {
updatedParams.title = title;
item.lastTitle = title;
}
if (Object.keys(updatedParams).length == 0)
continue;
updateItem(item.id, updatedParams);
updated = true;
}
}
if (lastSeparator?.lastVisible) {
updateItem(lastSeparator.id, { visible: false });
lastSeparator.lastVisible = false;
updated = true;
}
return { updated, visibleItemsCount };
}
async function updateItems({ multiselected, hasUnmutedTab, hasUnmutedDescendant, hasAutoplayBlockedTab, hasAutoplayBlockedDescendant, sticky, hidden } = {}) {
let updated = false;
const groupedItems = updateItemsVisibility(mGroupedTabItems, { multiselected, hasUnmutedTab, hasUnmutedDescendant, hasAutoplayBlockedTab, hasAutoplayBlockedDescendant, sticky, hidden });
if (groupedItems.updated)
updated = true;
const separatorVisible = !hidden && configs.emulateDefaultContextMenu && groupedItems.visibleItemsCount > 0;
if (separatorVisible != mTabSeparator.lastVisible) {
updateItem(mTabSeparator.id, { visible: separatorVisible });
mTabSeparator.lastVisible = separatorVisible;
updated = true;
}
const grouped = !hidden && configs.emulateDefaultContextMenu && groupedItems.visibleItemsCount > 1;
if (grouped != mTabRootItem.lastVisible) {
updateItem(mTabRootItem.id, { visible: grouped });
mTabRootItem.lastVisible = grouped;
updated = true;
}
const topLevelItems = updateItemsVisibility(mTabItems, { forceVisible: grouped ? false : null, multiselected, hasUnmutedTab, hasUnmutedDescendant, hasAutoplayBlockedTab, hasAutoplayBlockedDescendant, sticky, hidden });
if (topLevelItems.updated)
updated = true;
if (mGroupedTabItemsById['grouped:sendTreeToDevice'].lastVisible &&
await TabContextMenu.updateSendToDeviceItems('grouped:sendTreeToDevice', {
manage: navigator.userAgent.includes('Fennec'), // see also https://github.com/piroor/treestyletab/issues/3174
}))
updated = true;
return updated;
}
export function onClick(info, tab) {
if (info.bookmarkId)
return onBookmarkItemClick(info);
else
return onTabItemClick(info, tab);
}
browser.menus.onClicked.addListener(onClick);
function onTabItemClick(info, tab) {
// Extra context menu commands won't be available on the blank area of the tab bar.
if (!tab)
return;
log('context menu item clicked: ', info, tab);
const contextTab = Tab.get(tab.id);
const contextTabs = contextTab.$TST.multiselected ? Tab.getSelectedTabs(contextTab.windowId) : [contextTab];
const itemId = info.menuItemId.replace(/^(?:grouped:|context_topLevel_)/, '');
if (mTabItemsById[itemId] &&
mTabItemsById[itemId].type == 'checkbox')
mTabItemsById[itemId].checked = !mTabItemsById[itemId].checked;
const inverted = info.button == 1;
switch (itemId) {
case 'reloadTree':
if (inverted)
Commands.reloadDescendants(contextTabs);
else
Commands.reloadTree(contextTabs);
break;
case 'reloadDescendants':
if (inverted)
Commands.reloadTree(contextTabs);
else
Commands.reloadDescendants(contextTabs);
break;
case 'toggleMuteTree':
if (inverted)
Commands.toggleMuteDescendants(contextTabs);
else
Commands.toggleMuteTree(contextTabs);
break;
case 'toggleMuteDescendants':
if (inverted)
Commands.toggleMuteTree(contextTabs);
else
Commands.toggleMuteDescendants(contextTabs);
break;
case 'closeTree':
if (inverted)
Commands.closeDescendants(contextTabs);
else
Commands.closeTree(contextTabs);
break;
case 'closeDescendants':
if (inverted)
Commands.closeTree(contextTabs);
else
Commands.closeDescendants(contextTabs);
break;
case 'closeOthers':
Commands.closeOthers(contextTabs);
break;
case 'toggleSticky':
Commands.toggleSticky(contextTabs, !contextTab.$TST.sticky);
break;
case 'collapseTree':
Commands.collapseTree(contextTabs, { recursively: inverted });
break;
case 'collapseTreeRecursively':
Commands.collapseTree(contextTabs, { recursively: !inverted });
break;
case 'collapseAll':
Commands.collapseAll(contextTab.windowId);
break;
case 'expandTree':
Commands.expandTree(contextTabs, { recursively: inverted });
break;
case 'expandTreeRecursively':
Commands.expandTree(contextTabs, { recursively: !inverted });
break;
case 'expandAll':
Commands.expandAll(contextTab.windowId);
break;
case 'bookmarkTree':
Commands.bookmarkTree(contextTabs);
break;
case 'sendTreeToDevice:all':
Sync.sendTabsToAllDevices(contextTabs, { recursively: true });
break;
case 'collapsed':
if (info.wasChecked)
Commands.expandTree(contextTab);
else
Commands.collapseTree(contextTab);
break;
case 'pinnedTab': {
const tabs = Tab.getPinnedTabs(contextTab.windowId);
if (tabs.length > 0)
browser.tabs.update(tabs[0].id, { active: true })
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
}; break;
case 'unpinnedTab': {
const tabs = Tab.getUnpinnedTabs(tab.windowId);
if (tabs.length > 0)
browser.tabs.update(tabs[0].id, { active: true })
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
}; break;
default: {
const sendToDeviceMatch = info.menuItemId.match(/^sendTreeToDevice:device:(.+)$/);
if (contextTab &&
sendToDeviceMatch)
Sync.sendTabsToDevice(
contextTabs,
{ to: sendToDeviceMatch[1],
recursively: true }
);
}; break;
}
}
TabContextMenu.onTSTItemClick.addListener(onTabItemClick);
async function onBookmarkItemClick(info) {
switch (info.menuItemId.replace(/^grouped:/, '')) {
case 'openAllBookmarksWithStructure':
Commands.openAllBookmarksWithStructure(info.bookmarkId, { recursively: false });
break;
case 'openAllBookmarksWithStructureRecursively':
Commands.openAllBookmarksWithStructure(info.bookmarkId, { recursively: true });
break;
}
}
async function onShown(info, tab) {
if (info.contexts.includes('tab'))
await onTabContextMenuShown(info, tab);
else if (info.contexts.includes('bookmark'))
onBookmarkContextMenuShown(info);
}
browser.menus.onShown.addListener(onShown);
let mLastContextTabId = null;
async function onTabContextMenuShown(info, tab) {
const contextTabId = tab?.id;
mLastContextTabId = contextTabId;
tab = tab && Tab.get(contextTabId);
const multiselected = tab?.$TST.multiselected;
const contextTabs = multiselected ? Tab.getSelectedTabs(tab.windowId) : tab ? [tab] : [];
const hasChild = contextTabs.length > 0 && contextTabs.some(tab => tab.$TST.hasChild);
const subtreeCollapsed = contextTabs.length > 0 && contextTabs.some(tab => tab.$TST.subtreeCollapsed);
const grouped = contextTabs.length > 0 && contextTabs.some(tab => tab.$TST.isGroupTab);
const { hasUnmutedTab, hasUnmutedDescendant } = Commands.getUnmutedState(contextTabs);
const { hasAutoplayBlockedTab, hasAutoplayBlockedDescendant } = Commands.getAutoplayBlockedState(contextTabs);
let updated = await updateItems({
multiselected,
hasUnmutedTab,
hasUnmutedDescendant,
hasAutoplayBlockedTab,
hasAutoplayBlockedDescendant,
sticky: tab?.$TST.sticky,
hidden: !configs.showTreeCommandsInTabsContextMenuGlobally && info.viewType != 'sidebar',
});
if (mLastContextTabId != contextTabId)
return; // Skip further operations if the menu was already reopened on a different context tab.
for (const item of mTabItems) {
let newVisible;
let newEnabled;
if (item.id == 'sendTreeToDevice' &&
item.visible) {
newVisible = contextTabs.filter(Sync.isSendableTab).length > 0;
newEnabled = (
hasChild &&
Sync.getOtherDevices().length > 0
);
}
else if (item.requireTree) {
newEnabled = hasChild;
switch (item.id) {
case 'collapseTree':
if (subtreeCollapsed)
newEnabled = false;
break;
case 'expandTree':
if (!subtreeCollapsed)
newEnabled = false;
break;
}
}
else if (item.requireMultiselected) {
newEnabled = multiselected;
}
else if (item.requireGrouped) {
newEnabled = grouped;
}
else if (item.requireNormal) {
newEnabled = tab?.pinned;
}
else {
continue;
}
if ((newVisible === undefined ||
newVisible == !!item.visible) &&
(newEnabled === undefined ||
newEnabled == !!item.enabled))
continue;
const params = {};
if (newVisible !== undefined &&
newVisible != !!item.visible)
params.visible = item.visible = !!newVisible;
if (newEnabled !== undefined &&
newEnabled != !!item.enabled)
params.enabled = item.enabled = !!newEnabled;
updateItem(item.id, params);
updateItem(`grouped:${item.id}`, params);
updated = true;
}
{
const canExpand = hasChild && subtreeCollapsed;
mTabItemsById.collapsed.checked = canExpand;
const params = {
checked: canExpand
};
updateItem('collapsed', params);
updateItem(`grouped:collapsed`, params);
updated = true;
}
if (updated)
browser.menus.refresh().catch(ApiTabs.createErrorSuppressor());
}
TabContextMenu.onTSTTabContextMenuShown.addListener(onTabContextMenuShown);
let mLastContextItemId = null;
async function onBookmarkContextMenuShown(info) {
const contextItemId = info.bookmarkId;
mLastContextItemId = contextItemId;
let isFolder = true;
if (info.bookmarkId) {
const item = await Bookmark.getItemById(info.bookmarkId);
if (mLastContextItemId != contextItemId)
return; // Skip further operations if the menu was already reopened on a different context item.
isFolder = (
item.type == 'folder' ||
(item.type == 'bookmark' &&
/^place:parent=([^&]+)$/.test(item.url))
);
}
let visibleItemCount = 0;
mBookmarkItemsById.openAllBookmarksWithStructure.visible = !!(
isFolder &&
configs[mBookmarkItemsById.openAllBookmarksWithStructure.configKey] &&
++visibleItemCount
);
mBookmarkItemsById.openAllBookmarksWithStructureRecursively.visible = !!(
isFolder &&
configs[mBookmarkItemsById.openAllBookmarksWithStructureRecursively.configKey] &&
++visibleItemCount
);
for (const item of mGroupedBookmarkItems) {
item.visible = !!(
visibleItemCount > 1 &&
item.ungroupedItem.visible
);
if (item.visible)
item.ungroupedItem.visible = false;
}
for (const item of [...mBookmarkItems, ...mGroupedBookmarkItems]) {
browser.menus.update(item.id, {
visible: !!item.visible,
});
}
browser.menus.update(mBookmarkSeparator.id, {
visible: visibleItemCount > 0
});
browser.menus.update(mBookmarkRootItem.id, {
visible: visibleItemCount > 1
});
browser.menus.refresh().catch(ApiTabs.createErrorSuppressor());
}
export function getItemIdsWithIcon() {
return [
kROOT_TAB_ITEM,
kROOT_BOOKMARK_ITEM,
...Object.keys(mBookmarkItemsById),
];
}

View File

@@ -0,0 +1,61 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
configs,
} from '/common/common.js';
import * as Constants from '/common/constants.js';
import { Tab } from '/common/TreeItem.js';
async function doTest() {
const win = await browser.windows.getLastFocused({ populate: true });
const tab = win.tabs.find(tab => tab.active);
const maxTry = 10;
const promises = [];
const duplicatedTabIds = [];
for (let i = 0; i < maxTry; i++) {
promises.push(browser.tabs.duplicate(tab.id).then(async duplicated => {
await Tab.waitUntilTracked(duplicated.id);
const tracked = Tab.get(duplicated.id);
const uniqueId = await tracked.$TST.promisedUniqueId;
duplicatedTabIds.push(duplicated.id);
return uniqueId?.duplicated;
}));
}
const successCount = (await Promise.all(promises)).filter(result => !!result).length;
await browser.tabs.remove(duplicatedTabIds);
return successCount / maxTry;
}
async function autoDetectSuitableDelay() {
configs.delayForDuplicatedTabDetection = 0;
let successRate = await doTest();
if (successRate == 1)
return;
configs.delayForDuplicatedTabDetection = 10;
while (successRate < 1) {
configs.delayForDuplicatedTabDetection = Math.round(configs.delayForDuplicatedTabDetection * (1 / Math.max(successRate, 0.5)));
successRate = await doTest();
}
}
browser.runtime.onMessage.addListener((message, _sender) => {
if (!message || !message.type)
return;
switch (message.type) {
case Constants.kCOMMAND_TEST_DUPLICATED_TAB_DETECTION:
return doTest();
case Constants.kCOMMAND_AUTODETECT_DUPLICATED_TAB_DETECTION_DELAY:
autoDetectSuitableDelay();
return;
}
});

View File

@@ -0,0 +1,150 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
configs,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import { Tab } from '/common/TreeItem.js';
import * as Background from './background.js';
function log(...args) {
if (configs.debug)
console.log(...args);
}
function uniqTabsAndDescendantsSet(tabs) {
if (!Array.isArray(tabs))
tabs = [tabs];
return Array.from(new Set(tabs.map(tab => [tab].concat(tab.$TST.descendants)).flat())).sort(Tab.compare);
}
function unblockAutoplayTree(tabs) {
const tabsToUpdate = [];
let shouldUnblockAutoplay = false;
for (const tab of uniqTabsAndDescendantsSet(tabs)) {
if (!shouldUnblockAutoplay && tab.$TST.autoplayBlocked)
shouldUnblockAutoplay = true;
tabsToUpdate.push(tab);
}
browser.waterfoxBridge.unblockAutoplay(tabsToUpdate.map(tab => tab.id));
}
function unblockAutoplayDescendants(rootTabs) {
const rootTabsSet = new Set(rootTabs);
const tabsToUpdate = [];
let shouldUnblockAutoplay = false;
for (const tab of uniqTabsAndDescendantsSet(rootTabs)) {
if (rootTabsSet.has(tab))
continue;
if (!shouldUnblockAutoplay && tab.$TST.autoplayBlocked)
shouldUnblockAutoplay = true;
tabsToUpdate.push(tab);
}
browser.waterfoxBridge.unblockAutoplay(tabsToUpdate.map(tab => tab.id));
}
browser.menus.onClicked.addListener((info, contextTab) => {
contextTab = contextTab && Tab.get(contextTab.id);
const contextTabs = contextTab.$TST.multiselected ? Tab.getSelectedTabs(contextTab.windowId) : [contextTab];
const inverted = info.button == 1;
switch (info.menuItemId.replace(/^grouped:/, '')) {
case 'context_unblockAutoplay':
browser.waterfoxBridge.unblockAutoplay(contextTabs.map(tab => tab.id));
break;
case 'context_topLevel_unblockAutoplayTree':
case 'unblockAutoplayTree':
if (inverted)
unblockAutoplayDescendants(contextTabs);
else
unblockAutoplayTree(contextTabs);
break;
case 'context_topLevel_unblockAutoplayDescendants':
case 'unblockAutoplayDescendants':
if (inverted)
unblockAutoplayTree(contextTabs);
else
unblockAutoplayDescendants(contextTabs);
break;
}
});
SidebarConnection.onMessage.addListener(async (windowId, message) => {
switch (message.type) {
case Constants.kCOMMAND_UNBLOCK_AUTOPLAY_FROM_SOUND_BUTTON: {
await Tab.waitUntilTracked(message.tabId);
const root = Tab.get(message.tabId);
log('unblock autoplay from sound button: ', message, root);
if (!root)
break;
const multiselected = root.$TST.multiselected;
const tabs = multiselected ?
Tab.getSelectedTabs(root.windowId, { iterator: true }) :
[root] ;
if (!multiselected &&
root.$TST.subtreeCollapsed) {
const tabsInTree = [root, ...root.$TST.descendants];
const toBeUpdatedTabs = tabsInTree.filter(tab => tab.$TST.autoplayBlocked);
log(' toBeUpdatedTabs: ', toBeUpdatedTabs);
browser.waterfoxBridge.unblockAutoplay(toBeUpdatedTabs.map(tab => tab.id));
}
else {
log(' tabs: ', tabs);
browser.waterfoxBridge.unblockAutoplay(tabs.map(tab => tab.id));
}
}; break;
}
});
browser.commands.onCommand.addListener(async command => {
let activeTabs = await browser.tabs.query({
active: true,
currentWindow: true,
}).catch(ApiTabs.createErrorHandler());
if (activeTabs.length == 0)
activeTabs = await browser.tabs.query({
currentWindow: true,
}).catch(ApiTabs.createErrorHandler());
const activeTab = Tab.get(activeTabs[0].id);
const selectedTabs = activeTab.$TST.multiselected ? Tab.getSelectedTabs(activeTab.windowId) : [activeTab];
switch (command) {
case 'unblockAutoplayTree':
unblockAutoplayTree(selectedTabs);
return;
case 'unblockAutoplayDescendants':
unblockAutoplayDescendants(selectedTabs);
return;
}
});
Background.onReady.addListener(() => {
browser.waterfoxBridge.listAutoplayBlockedTabs().then(tabs => {
for (const tab of tabs) {
Tab.get(tab.id)?.$TST.addState(Constants.kTAB_STATE_AUTOPLAY_BLOCKED);
}
});
});
browser.waterfoxBridge.onAutoplayBlocked.addListener(tab => {
Tab.get(tab.id)?.$TST.addState(Constants.kTAB_STATE_AUTOPLAY_BLOCKED);
});
browser.waterfoxBridge.onAutoplayUnblocked.addListener(tab => {
Tab.get(tab.id)?.$TST.removeState(Constants.kTAB_STATE_AUTOPLAY_BLOCKED);
});

View File

@@ -0,0 +1,32 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
configs,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as TabsStore from '/common/tabs-store.js';
import * as Commands from './commands.js';
browser.waterfoxBridge.onMenuCommand.addListener(async info => {
switch (info.itemId) {
case 'tabs-sidebar-newTab': {
const behavior = info.button == 1 ?
configs.autoAttachOnNewTabButtonMiddleClick :
(info.ctrlKey || info.metaKey) ?
configs.autoAttachOnNewTabButtonAccelClick :
configs.autoAttachOnNewTabCommand;
const win = await browser.windows.getLastFocused({ populate: true }).catch(ApiTabs.createErrorHandler());
const activeTab = TabsStore.activeTabInWindow.get(win.id);
Commands.openNewTabAs({
baseTab: activeTab,
as: behavior,
});
}; break;
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,296 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
dumpTab,
configs,
wait,
isMacOS,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import * as TSTAPI from '/common/tst-api.js';
import { Tab } from '/common/TreeItem.js';
import * as Commands from './commands.js';
import * as NativeTabGroups from './native-tab-groups.js';
import * as TabsGroup from './tabs-group.js';
import * as Tree from './tree.js';
import * as TreeStructure from './tree-structure.js';
function log(...args) {
internalLogger('background/handle-moved-tabs', ...args);
}
function logApiTabs(...args) {
internalLogger('common/api-tabs', ...args);
}
let mMaybeTabMovingByShortcut = false;
Tab.onCreated.addListener((tab, info = {}) => {
if (!info.mayBeReplacedWithContainer &&
(info.duplicated ||
info.restored ||
info.skipFixupTree ||
// do nothing for already attached tabs
(tab.openerTabId &&
tab.$TST.parent == Tab.get(tab.openerTabId)))) {
log('skip to fixup tree for replaced/duplicated/restored tab ', tab, info);
return;
}
// if the tab is opened inside existing tree by someone, we must fixup the tree.
if (!(info.positionedBySelf ||
info.movedBySelfWhileCreation) &&
(tab.$TST.nearestCompletelyOpenedNormalFollowingTab ||
tab.$TST.nearestCompletelyOpenedNormalPrecedingTab ||
(info.treeForActionDetection?.target &&
(info.treeForActionDetection.target.next ||
info.treeForActionDetection.target.previous)))) {
tryFixupTreeForInsertedTab(tab, {
toIndex: tab.index,
fromIndex: Tab.getLastTab(tab.windowId).index,
treeForActionDetection: info.treeForActionDetection,
isTabCreating: true
});
}
else {
log('no need to fixup tree for newly created tab ', tab, info);
}
});
Tab.onMoving.addListener((tab, moveInfo) => {
// avoid TabMove produced by browser.tabs.insertRelatedAfterCurrent=true or something.
const win = TabsStore.windows.get(tab.windowId);
const isNewlyOpenedTab = win.openingTabs.has(tab.id);
const positionControlled = configs.insertNewChildAt != Constants.kINSERT_NO_CONTROL;
if (!isNewlyOpenedTab ||
!positionControlled ||
moveInfo.byInternalOperation ||
moveInfo.alreadyMoved ||
!moveInfo.movedInBulk)
return true;
// if there is no valid opener, it can be a restored initial tab in a restored window
// and can be just moved as a part of window restoration process.
if (!tab.$TST.openerTab)
return true;
log('onTabMove for new child tab: move back '+moveInfo.toIndex+' => '+moveInfo.fromIndex);
moveBack(tab, moveInfo);
return false;
});
async function tryFixupTreeForInsertedTab(tab, moveInfo = {}) {
const internalGroupMoveCount = NativeTabGroups.internallyMovingNativeTabGroups.get(tab.groupId);
if (internalGroupMoveCount) {
log('ignore internal move of tab groups ', internalGroupMoveCount);
return;
}
const parentTabOperationBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE,
...moveInfo,
});
log('tryFixupTreeForInsertedTab ', {
tab: tab.id,
parentTabOperationBehavior,
moveInfo,
childIds: tab.$TST.childIds,
parentId: tab.$TST.parentId,
});
if (!moveInfo.isTabCreating &&
parentTabOperationBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE) {
if (tab.$TST.hasChild)
tab.$TST.temporaryMetadata.set('childIdsBeforeMoved', tab.$TST.childIds.slice(0));
tab.$TST.temporaryMetadata.set('parentIdBeforeMoved', tab.$TST.parentId);
const replacedGroupTab = (parentTabOperationBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB) ?
await TabsGroup.tryReplaceTabWithGroup(tab, { insertBefore: tab.$TST.firstChild }) :
null;
if (!replacedGroupTab && tab.$TST.hasChild) {
if (tab.$TST.isGroupTab)
await TabsGroup.clearTemporaryState(tab);
await Tree.detachAllChildren(tab, {
behavior: parentTabOperationBehavior,
nearestFollowingRootTab: tab.$TST.firstChild.$TST.nearestFollowingRootTab,
broadcast: true
});
}
if (tab.$TST.parentId)
await Tree.detachTab(tab, {
broadcast: true
});
// Pinned tab is moved at first, so Tab.onPinned handler cannot know tree information
// before the pinned tab was moved. Thus we cache tree information for the handler.
wait(100).then(() => {
tab.$TST.temporaryMetadata.delete('childIdsBeforeMoved');
tab.$TST.temporaryMetadata.delete('parentIdBeforeMoved');
});
}
log('The tab can be placed inside existing tab unexpectedly, so now we are trying to fixup tree.');
const action = Tree.detectTabActionFromNewPosition(tab, {
isMovingByShortcut: mMaybeTabMovingByShortcut,
...moveInfo,
});
log(' => action: ', action);
if (!action.action) {
log('no action');
return;
}
// When multiple tabs are moved at once by outside of TST (e.g. moving of multiselected tabs)
// Tree.detectTabActionFromNewPosition() may be called for other tabs asynchronously
// before this operation finishes. Thus we need to memorize the calculated "parent"
// and Tree.detectTabActionFromNewPosition() will use it.
if (action.parent)
tab.$TST.temporaryMetadata.set('goingToBeAttachedTo', action.parent);
// notify event to helper addons with action and allow or deny
const cache = {};
const allowed = await TSTAPI.tryOperationAllowed(
TSTAPI.kNOTIFY_TRY_FIXUP_TREE_ON_TAB_MOVED,
{
tab,
fromIndex: moveInfo.fromIndex,
toIndex: moveInfo.toIndex,
action: action.action,
parent: action.parent,
insertBefore: action.insertBefore,
insertAfter: action.insertAfter,
},
{ tabProperties: ['tab', 'parent', 'insertBefore', 'insertAfter'], cache }
);
TSTAPI.clearCache(cache);
if (!allowed) {
log('no action - canceled by a helper addon');
}
else {
log('action: ', action);
switch (action.action) {
case 'invalid':
moveBack(tab, moveInfo);
break;
default:
log('tryFixupTreeForInsertedTab: apply action for unattached tab: ', tab, action);
await action.apply();
break;
}
}
if (tab.$TST.temporaryMetadata.get('goingToBeAttachedTo') == action.parent)
tab.$TST.temporaryMetadata.delete('goingToBeAttachedTo');
}
function reserveToEnsureRootTabVisible(tab) {
reserveToEnsureRootTabVisible.tabIds.add(tab.id);
if (reserveToEnsureRootTabVisible.reserved)
clearTimeout(reserveToEnsureRootTabVisible.reserved);
reserveToEnsureRootTabVisible.reserved = setTimeout(() => {
delete reserveToEnsureRootTabVisible.reserved;
const tabs = Array.from(reserveToEnsureRootTabVisible.tabIds, Tab.get);
reserveToEnsureRootTabVisible.tabIds.clear();
for (const tab of tabs) {
if (!tab.$TST ||
tab.$TST.parent ||
!tab.$TST.collapsed)
continue;
Tree.collapseExpandTabAndSubtree(tab, {
collapsed: false,
broadcast: true
});
}
}, 150);
}
reserveToEnsureRootTabVisible.tabIds = new Set();
Tab.onMoved.addListener((tab, moveInfo = {}) => {
if (moveInfo.byInternalOperation ||
!moveInfo.movedInBulk ||
tab.$TST.duplicating) {
log('internal move');
tab.$TST.nativeTabGroup?.$TST.reindex();
}
else {
log('process moved tab');
tryFixupTreeForInsertedTab(tab, moveInfo).then(() => {
tab.$TST.nativeTabGroup?.$TST.reindex();
});
}
reserveToEnsureRootTabVisible(tab);
});
function onMessage(message, _sender) {
if (!message ||
typeof message.type != 'string')
return;
//log('onMessage: ', message, sender);
switch (message.type) {
case Constants.kNOTIFY_TAB_MOUSEDOWN:
mMaybeTabMovingByShortcut = false;
break;
case Constants.kCOMMAND_NOTIFY_MAY_START_TAB_SWITCH:
if (message.modifier != (configs.accelKey || (isMacOS() ? 'meta' : 'control')))
return;
mMaybeTabMovingByShortcut = true;
break;
case Constants.kCOMMAND_NOTIFY_MAY_END_TAB_SWITCH:
if (message.modifier != (configs.accelKey || (isMacOS() ? 'meta' : 'control')))
return;
mMaybeTabMovingByShortcut = false;
break;
}
}
browser.runtime.onMessage.addListener(onMessage);
Commands.onMoveUp.addListener(async tab => {
await tryFixupTreeForInsertedTab(tab, {
toIndex: tab.index,
fromIndex: tab.index + 1,
});
});
Commands.onMoveDown.addListener(async tab => {
await tryFixupTreeForInsertedTab(tab, {
toIndex: tab.index,
fromIndex: tab.index - 1,
});
});
TreeStructure.onTabAttachedFromRestoredInfo.addListener((tab, moveInfo) => { tryFixupTreeForInsertedTab(tab, moveInfo); });
function moveBack(tab, moveInfo) {
log('Move back tab from unexpected move: ', dumpTab(tab), moveInfo);
const id = tab.id;
const win = TabsStore.windows.get(tab.windowId);
const index = moveInfo.fromIndex;
win.internalMovingTabs.set(id, index);
logApiTabs(`handle-moved-tabs:moveBack: browser.tabs.move() `, tab.id, {
windowId: moveInfo.windowId,
index: moveInfo.fromIndex
});
// Because we need to use the raw "fromIndex" directly,
// we cannot use TabsMove.moveTabInternallyBefore/After here.
return browser.tabs.move(tab.id, {
windowId: moveInfo.windowId,
index: moveInfo.fromIndex
}).catch(ApiTabs.createErrorHandler(e => {
if (win.internalMovingTabs.get(id) == index)
win.internalMovingTabs.delete(id);
ApiTabs.handleMissingTabError(e);
}));
}

View File

@@ -0,0 +1,598 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
dumpTab,
configs,
isFirefoxViewTab,
} from '/common/common.js';
import * as Constants from '/common/constants.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import * as TSTAPI from '/common/tst-api.js';
import { Tab } from '/common/TreeItem.js';
import * as TabsMove from './tabs-move.js';
import * as TabsOpen from './tabs-open.js';
import * as Tree from './tree.js';
function log(...args) {
internalLogger('background/handle-new-tabs', ...args);
}
Tab.onBeforeCreate.addListener(async (tab, info) => {
const activeTab = info.activeTab || Tab.getActiveTab(tab.windowId);
// Special case, when all these conditions are true:
// 1) A new blank tab is configured to be opened as a child of the active tab.
// 2) The active tab is pinned.
// 3) Tabs opened from a pinned parent are configured to be placed near the
// opener pinned tab.
// then we fakely attach the new blank tab to the active pinned tab.
// See also https://github.com/piroor/treestyletab/issues/3296
const shouldAttachToPinnedOpener = (
!tab.openerTabId &&
!tab.pinned &&
tab.$TST.isNewTabCommandTab &&
Constants.kCONTROLLED_NEWTAB_POSITION.has(configs.autoAttachOnNewTabCommand) &&
(
(activeTab?.pinned &&
Constants.kCONTROLLED_INSERTION_POSITION.has(configs.insertNewTabFromPinnedTabAt)) ||
(isFirefoxViewTab(activeTab) &&
Constants.kCONTROLLED_INSERTION_POSITION.has(configs.insertNewTabFromFirefoxViewAt))
)
);
if (shouldAttachToPinnedOpener)
tab.openerTabId = activeTab.id;
});
// this should return false if the tab is / may be moved while processing
Tab.onCreating.addListener((tab, info = {}) => {
if (info.duplicatedInternally)
return true;
log('Tabs.onCreating ', dumpTab(tab), tab.openerTabId, info);
const activeTab = info.activeTab || Tab.getActiveTab(tab.windowId);
const opener = tab.$TST.openerTab;
if (opener) {
tab.$TST.setAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID, opener.$TST.uniqueId.id);
if (!info.bypassTabControl)
TabsStore.addToBeGroupedTab(tab);
}
else {
let dontMove = false;
if (!info.maybeOrphan &&
!info.bypassTabControl &&
activeTab &&
!info.restored) {
let autoAttachBehavior = configs.autoAttachOnNewTabCommand;
if (tab.$TST.nextTab &&
activeTab == tab.$TST.previousTab) {
// New tab opened with browser.tabs.insertAfterCurrent=true may have
// next tab. In this case the tab is expected to be placed next to the
// active tab always, so we should change the behavior specially.
// See also:
// https://github.com/piroor/treestyletab/issues/2054
// https://github.com/piroor/treestyletab/issues/2194#issuecomment-505272940
dontMove = true;
switch (autoAttachBehavior) {
case Constants.kNEWTAB_OPEN_AS_ORPHAN:
case Constants.kNEWTAB_OPEN_AS_SIBLING:
case Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING:
if (activeTab.$TST.hasChild)
autoAttachBehavior = Constants.kNEWTAB_OPEN_AS_CHILD;
else
autoAttachBehavior = Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING;
break;
case Constants.kNEWTAB_OPEN_AS_CHILD:
default:
break;
}
}
if (tab.$TST.isNewTabCommandTab) {
if (!info.positionedBySelf) {
log('behave as a tab opened by new tab command');
return handleNewTabFromActiveTab(tab, {
activeTab,
autoAttachBehavior,
dontMove,
openedWithCookieStoreId: info.openedWithCookieStoreId,
inheritContextualIdentityMode: configs.inheritContextualIdentityToChildTabMode,
context: TSTAPI.kNEWTAB_CONTEXT_NEWTAB_COMMAND,
}).then(moved => !moved);
}
return false;
}
else if (activeTab != tab) {
tab.$TST.temporaryMetadata.set('possibleOpenerTab', activeTab.id);
}
if (!info.fromExternal)
tab.$TST.temporaryMetadata.set('isNewTab', true);
}
if (info.fromExternal &&
!info.bypassTabControl) {
log('behave as a tab opened from external application');
// we may need to reopen the tab with loaded URL
if (configs.inheritContextualIdentityToTabsFromExternalMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
tab.$TST.temporaryMetadata.set('fromExternal', true);
return notifyToTryHandleNewTab(tab, {
context: TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL,
activeTab,
}).then(allowed => {
if (!allowed) {
log(' => handling is canceled by someone');
return true;
}
return Tree.behaveAutoAttachedTab(tab, {
baseTab: activeTab,
behavior: configs.autoAttachOnOpenedFromExternal,
dontMove,
broadcast: true
}).then(moved => !moved);
});
}
log('behave as a tab opened with any URL ', tab.title, tab.url);
if (!info.restored &&
!info.positionedBySelf &&
!info.bypassTabControl &&
configs.autoAttachOnAnyOtherTrigger != Constants.kNEWTAB_DO_NOTHING) {
if (configs.inheritContextualIdentityToTabsFromAnyOtherTriggerMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
tab.$TST.temporaryMetadata.set('anyOtherTrigger', true);
log('controlled as a new tab from other unknown trigger');
return notifyToTryHandleNewTab(tab, {
context: TSTAPI.kNEWTAB_CONTEXT_UNKNOWN,
activeTab,
}).then(allowed => {
if (!allowed) {
log(' => handling is canceled by someone');
return true;
}
return Tree.behaveAutoAttachedTab(tab, {
baseTab: activeTab,
behavior: configs.autoAttachOnAnyOtherTrigger,
dontMove,
broadcast: true
}).then(moved => !moved);
});
}
if (info.positionedBySelf)
tab.$TST.temporaryMetadata.set('positionedBySelf', true);
return true;
}
log(`opener: ${dumpTab(opener)}, positionedBySelf = ${info.positionedBySelf}`);
if (!info.bypassTabControl &&
opener &&
(opener.pinned || isFirefoxViewTab(opener)) &&
opener.windowId == tab.windowId) {
return handleTabsFromPinnedOpener(tab, opener, { activeTab }).then(moved => !moved);
}
else if (!info.maybeOrphan || info.bypassTabControl) {
if (info.fromExternal &&
!info.bypassTabControl &&
configs.inheritContextualIdentityToTabsFromExternalMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
tab.$TST.temporaryMetadata.set('fromExternal', true);
return notifyToTryHandleNewTab(tab, {
context: info.fromExternal && !info.bypassTabControl ?
TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL :
info.duplicated ?
TSTAPI.kNEWTAB_CONTEXT_DUPLICATED :
TSTAPI.kNEWTAB_CONTEXT_WITH_OPENER,
activeTab,
openerTab: opener,
}).then(allowed => {
if (!allowed) {
log(' => handling is canceled by someone');
return true;
}
const behavior = info.fromExternal && !info.bypassTabControl ?
configs.autoAttachOnOpenedFromExternal :
info.duplicated ?
configs.autoAttachOnDuplicated :
configs.autoAttachOnOpenedWithOwner;
return Tree.behaveAutoAttachedTab(tab, {
baseTab: opener,
behavior,
dontMove: info.positionedBySelf || info.mayBeReplacedWithContainer,
broadcast: true
}).then(moved => !moved);
});
}
return true;
});
async function notifyToTryHandleNewTab(tab, { context, activeTab, openerTab } = {}) {
const cache = {};
const result = TSTAPI.tryOperationAllowed(
TSTAPI.kNOTIFY_TRY_HANDLE_NEWTAB,
{ tab,
activeTab,
openerTab,
context },
{ tabProperties: ['tab', 'activeTab', 'openerTab'], cache }
);
TSTAPI.clearCache(cache);
return result;
}
async function handleNewTabFromActiveTab(tab, { url, activeTab, autoAttachBehavior, dontMove, openedWithCookieStoreId, inheritContextualIdentityMode, context } = {}) {
log('handleNewTabFromActiveTab: activeTab = ', dumpTab(activeTab), { url, activeTab, autoAttachBehavior, dontMove, inheritContextualIdentityMode, context });
if (activeTab &&
activeTab.$TST.ancestors.includes(tab)) {
log(' => ignore restored ancestor tab');
return false;
}
const allowed = await notifyToTryHandleNewTab(tab, {
context,
activeTab,
});
if (!allowed) {
log(' => handling is canceled by someone');
return false;
}
const moved = await Tree.behaveAutoAttachedTab(tab, {
baseTab: activeTab,
behavior: autoAttachBehavior,
broadcast: true,
dontMove: dontMove || false
});
if (openedWithCookieStoreId) {
log('handleNewTabFromActiveTab: do not reopen tab opened with contextual identity explicitly');
return moved;
}
if (tab.cookieStoreId && tab.cookieStoreId != 'firefox-default') {
log('handleNewTabFromActiveTab: do not reopen tab opened with non-default contextual identity ', tab.cookieStoreId);
return moved;
}
const parent = tab.$TST.parent;
let cookieStoreId = null;
switch (inheritContextualIdentityMode) {
case Constants.kCONTEXTUAL_IDENTITY_FROM_PARENT:
if (parent)
cookieStoreId = parent.cookieStoreId;
break;
case Constants.kCONTEXTUAL_IDENTITY_FROM_LAST_ACTIVE:
cookieStoreId = activeTab.cookieStoreId
break;
default:
return moved;
}
if ((tab.cookieStoreId || 'firefox-default') == (cookieStoreId || 'firefox-default')) {
log('handleNewTabFromActiveTab: no need to reopen with inherited contextual identity ', cookieStoreId);
return moved;
}
if (!configs.inheritContextualIdentityToUnopenableURLTabs &&
!TabsOpen.isOpenable(url)) {
log('handleNewTabFromActiveTab: not openable URL, skip reopening ', cookieStoreId, url);
return moved;
}
log('handleNewTabFromActiveTab: reopen with inherited contextual identity ', cookieStoreId);
// We need to prevent grouping of this original tab and the reopened tab
// by the "multiple tab opened in XXX msec" feature.
const win = TabsStore.windows.get(tab.windowId);
win.openedNewTabs.delete(tab.id);
await TabsOpen.openURIInTab(url || null, {
windowId: activeTab.windowId,
parent,
insertBefore: tab,
active: tab.active,
cookieStoreId
});
TabsInternalOperation.removeTab(tab);
return moved;
}
async function handleTabsFromPinnedOpener(tab, opener, { activeTab } = {}) {
const allowed = await notifyToTryHandleNewTab(tab, {
context: TSTAPI.kNEWTAB_CONTEXT_FROM_PINNED,
activeTab,
openerTab: opener,
});
if (!allowed) {
log('handleTabsFromPinnedOpener: handling is canceled by someone');
return false;
}
const parent = Tab.getGroupTabForOpener(opener);
if (parent) {
log('handleTabsFromPinnedOpener: attach to corresponding group tab');
tab.$TST.setAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER, true);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
// it could be updated already...
const lastRelatedTab = opener.$TST.lastRelatedTabId == tab.id ?
opener.$TST.previousLastRelatedTab :
opener.$TST.lastRelatedTab;
// If there is already opened group tab, it is more natural that
// opened tabs are treated as a tab opened from unpinned tabs.
const insertAt = configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_NEXT_TO_LAST_RELATED_TAB ?
Constants.kINSERT_NEXT_TO_LAST_RELATED_TAB :
configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_TOP ?
Constants.kINSERT_TOP :
configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_END ?
Constants.kINSERT_END :
undefined;
return Tree.attachTabTo(tab, parent, {
lastRelatedTab,
insertAt,
forceExpand: true, // this is required to avoid the group tab itself is active from active tab in collapsed tree
broadcast: true
});
}
if ((configs.autoGroupNewTabsFromPinned ||
configs.autoGroupNewTabsFromFirefoxView) &&
tab.$TST.needToBeGroupedSiblings.length > 0) {
log('handleTabsFromPinnedOpener: controlled by auto-grouping');
return false;
}
switch (isFirefoxViewTab(opener) ? configs.insertNewTabFromFirefoxViewAt : configs.insertNewTabFromPinnedTabAt) {
case Constants.kINSERT_NEXT_TO_LAST_RELATED_TAB: {
// it could be updated already...
const lastRelatedTab = opener.$TST.lastRelatedTab != tab ?
opener.$TST.lastRelatedTab :
opener.$TST.previousLastRelatedTab;
if (lastRelatedTab) {
log(`handleTabsFromPinnedOpener: place after last related tab ${dumpTab(lastRelatedTab)}`);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabAfter(tab, lastRelatedTab.$TST.lastDescendant || lastRelatedTab, {
delayedMove: true,
broadcast: true
});
}
const lastPinnedTab = Tab.getLastPinnedTab(tab.windowId);
if (lastPinnedTab) {
log(`handleTabsFromPinnedOpener: place after last pinned tab ${dumpTab(lastPinnedTab)}`);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabAfter(tab, lastPinnedTab, {
delayedMove: true,
broadcast: true
});
}
const firstNormalTab = Tab.getFirstNormalTab(tab.windowId);
if (firstNormalTab) {
log(`handleTabsFromPinnedOpener: place before first unpinned tab ${dumpTab(firstNormalTab)}`);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabBefore(tab, firstNormalTab, {
delayedMove: true,
broadcast: true
});
}
};
case Constants.kINSERT_TOP: {
const lastPinnedTab = Tab.getLastPinnedTab(tab.windowId);
if (lastPinnedTab) {
log(`handleTabsFromPinnedOpener: opened from pinned opener: place after last pinned tab ${dumpTab(lastPinnedTab)}`);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabAfter(tab, lastPinnedTab, {
delayedMove: true,
broadcast: true
});
}
const firstNormalTab = Tab.getFirstNormalTab(tab.windowId);
if (firstNormalTab) {
log(`handleTabsFromPinnedOpener: opened from pinned opener: place before first pinned tab ${dumpTab(firstNormalTab)}`);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabBefore(tab, firstNormalTab, {
delayedMove: true,
broadcast: true
});
}
}; break;
case Constants.kINSERT_END: {
const lastTab = Tab.getLastTab(tab.windowId);
log('handleTabsFromPinnedOpener: opened from pinned opener: place after the last tab ', lastTab);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabAfter(tab, lastTab, {
delayedMove: true,
broadcast: true
});
};
}
return Promise.resolve(false);
}
Tab.onCreated.addListener((tab, info = {}) => {
if (!info.duplicated ||
info.bypassTabControl)
return;
const original = info.originalTab;
log('duplicated ', dumpTab(tab), dumpTab(original));
if (info.duplicatedInternally) {
log('duplicated by internal operation');
tab.$TST.addState(Constants.kTAB_STATE_DUPLICATING, { broadcast: true });
TabsStore.addDuplicatingTab(tab);
}
else {
// On old versions of Firefox, duplicated tabs had no openerTabId so they were
// not handled by Tab.onCreating listener. Today they are already handled before
// here, so this is just a failsafe (or for old versions of Firefox).
// See also: https://github.com/piroor/treestyletab/issues/2830#issuecomment-831414189
Tree.behaveAutoAttachedTab(tab, {
baseTab: original,
behavior: configs.autoAttachOnDuplicated,
dontMove: info.positionedBySelf || info.movedBySelfWhileCreation || info.mayBeReplacedWithContainer,
broadcast: true
});
}
});
Tab.onUpdated.addListener((tab, changeInfo) => {
if ('openerTabId' in changeInfo &&
configs.syncParentTabAndOpenerTab &&
!tab.$TST.updatingOpenerTabIds.includes(changeInfo.openerTabId) /* accept only changes from outside of TST */) {
Tab.waitUntilTrackedAll(tab.windowId).then(() => {
const parent = tab.$TST.openerTab;
if (!parent ||
parent.windowId != tab.windowId ||
parent == tab.$TST.parent)
return;
Tree.attachTabTo(tab, parent, {
insertAt: Constants.kINSERT_NEAREST,
forceExpand: tab.active,
broadcast: true
});
});
}
if (tab.$TST.temporaryMetadata.has('openedCompletely') &&
tab.windowId == tab.$windowIdOnCreated && // Don't treat tab as "opened from active tab" if it is moved across windows while loading
(changeInfo.url || changeInfo.status == 'complete') &&
(tab.$TST.temporaryMetadata.has('isNewTab') ||
tab.$TST.temporaryMetadata.has('fromExternal') ||
tab.$TST.temporaryMetadata.has('anyOtherTrigger'))) {
log('loaded tab ', dumpTab(tab), {
isNewTab: tab.$TST.temporaryMetadata.has('isNewTab'),
fromExternal: tab.$TST.temporaryMetadata.has('fromExternal'),
anyOtherTrigger: tab.$TST.temporaryMetadata.has('anyOtherTrigger'),
});
tab.$TST.temporaryMetadata.delete('isNewTab');
const possibleOpenerTab = Tab.get(tab.$TST.temporaryMetadata.get('possibleOpenerTab'));
tab.$TST.temporaryMetadata.delete('possibleOpenerTab');
log('possibleOpenerTab ', dumpTab(possibleOpenerTab));
if (tab.$TST.temporaryMetadata.has('fromExternal')) {
tab.$TST.temporaryMetadata.delete('fromExternal');
log('behave as a tab opened from external application (delayed)');
handleNewTabFromActiveTab(tab, {
url: tab.url,
activeTab: possibleOpenerTab,
autoAttachBehavior: configs.autoAttachOnOpenedFromExternal,
inheritContextualIdentityMode: configs.inheritContextualIdentityToTabsFromExternalMode,
context: TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL,
});
return;
}
if (tab.$TST.temporaryMetadata.has('anyOtherTrigger')) {
tab.$TST.temporaryMetadata.delete('anyOtherTrigger');
log('behave as a tab opened from any other trigger (delayed)');
handleNewTabFromActiveTab(tab, {
url: tab.url,
activeTab: possibleOpenerTab,
autoAttachBehavior: configs.autoAttachOnAnyOtherTrigger,
inheritContextualIdentityMode: configs.inheritContextualIdentityToTabsFromAnyOtherTriggerMode,
context: TSTAPI.kNEWTAB_CONTEXT_UNKNOWN,
});
return;
}
const win = TabsStore.windows.get(tab.windowId);
log('win.openedNewTabs ', win.openedNewTabs);
if (tab.$TST.parent ||
!possibleOpenerTab ||
win.openedNewTabs.has(tab.id) ||
tab.$TST.temporaryMetadata.has('openedWithOthers') ||
tab.$TST.temporaryMetadata.has('positionedBySelf')) {
log(' => no need to control ', {
parent: tab.$TST.parent,
possibleOpenerTab,
openedNewTab: win.openedNewTabs.has(tab.id),
openedWithOthers: tab.$TST.temporaryMetadata.has('openedWithOthers'),
positionedBySelf: tab.$TST.temporaryMetadata.has('positionedBySelf')
});
return;
}
if (tab.$TST.isNewTabCommandTab) {
log('behave as a tab opened by new tab command (delayed)');
tab.$TST.addState(Constants.kTAB_STATE_NEW_TAB_COMMAND_TAB);
handleNewTabFromActiveTab(tab, {
activeTab: possibleOpenerTab,
autoAttachBehavior: configs.autoAttachOnNewTabCommand,
inheritContextualIdentityMode: configs.inheritContextualIdentityToChildTabMode,
context: TSTAPI.kNEWTAB_CONTEXT_NEWTAB_COMMAND,
});
return;
}
const siteMatcher = /^\w+:\/\/([^\/]+)(?:$|\/.*$)/;
const openerTabSite = possibleOpenerTab.url.match(siteMatcher);
const newTabSite = tab.url.match(siteMatcher);
if (openerTabSite &&
newTabSite &&
tab.url != possibleOpenerTab.url && // It may be opened by "Duplciate Tab" or "Open in New Container Tab" if the URL is completely same.
openerTabSite[1] == newTabSite[1]) {
log('behave as a tab opened from same site (delayed)');
tab.$TST.addState(Constants.kTAB_STATE_OPENED_FOR_SAME_WEBSITE);
handleNewTabFromActiveTab(tab, {
url: tab.url,
activeTab: possibleOpenerTab,
autoAttachBehavior: configs.autoAttachSameSiteOrphan,
inheritContextualIdentityMode: configs.inheritContextualIdentityToSameSiteOrphanMode,
context: TSTAPI.kNEWTAB_CONTEXT_WEBSITE_SAME_TO_ACTIVE_TAB,
});
return;
}
log('checking special openers (delayed)', { opener: possibleOpenerTab.url, child: tab.url });
for (const rule of Constants.kAGGRESSIVE_OPENER_TAB_DETECTION_RULES_WITH_URL) {
if (rule.opener.test(possibleOpenerTab.url) &&
rule.child.test(tab.url)) {
log('behave as a tab opened from special opener (delayed)', { rule });
handleNewTabFromActiveTab(tab, {
url: tab.url,
activeTab: possibleOpenerTab,
autoAttachBehavior: configs.autoAttachOnOpenedWithOwner,
context: TSTAPI.kNEWTAB_CONTEXT_FROM_ABOUT_ADDONS,
});
return;
}
}
}
});
Tab.onAttached.addListener(async (tab, attachInfo = {}) => {
if (!attachInfo.windowId)
return;
const parentTabOperationBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE,
...attachInfo,
});
if (parentTabOperationBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
return;
log('Tabs.onAttached ', dumpTab(tab), attachInfo);
log('descendants of attached tab: ', () => attachInfo.descendants.map(dumpTab));
const movedTabs = await Tree.moveTabs(attachInfo.descendants, {
destinationWindowId: tab.windowId,
insertAfter: tab
});
log('moved descendants: ', () => movedTabs.map(dumpTab));
if (attachInfo.descendants.length == movedTabs.length) {
await Tree.applyTreeStructureToTabs(
[tab, ...movedTabs],
attachInfo.structure
);
}
else {
for (const movedTab of movedTabs) {
Tree.attachTabTo(movedTab, tab, {
broadcast: true,
dontMove: true
});
}
}
});

View File

@@ -0,0 +1,292 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
dumpTab,
wait,
mapAndFilterUniq,
countMatched,
configs
} from '/common/common.js';
import * as Constants from '/common/constants.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import { Tab } from '/common/TreeItem.js';
import * as Background from './background.js';
import * as TabsGroup from './tabs-group.js';
import * as Tree from './tree.js';
import * as Commands from './commands.js';
function log(...args) {
internalLogger('background/handle-removed-tabs', ...args);
}
Tab.onRemoving.addListener(async (tab, removeInfo = {}) => {
log('Tabs.onRemoving ', dumpTab(tab), removeInfo);
if (removeInfo.isWindowClosing)
return;
let closeParentBehavior;
let newParent;
const successor = tab.$TST.possibleSuccessorWithDifferentContainer;
if (successor) {
log('override closeParentBehaior with kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD for different container successor ', successor);
closeParentBehavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD;
// When a new tab is created with a different container and this tab
// is removed immediately before the new tab is completely handled,
// TST fails to detect the new tab as the successor of this tab. Thus,
// we treat the new tab as the successor - the first child of this
// (actually not attached to this tab yet).
if (successor && successor != tab.$TST.firstChild)
newParent = successor;
}
else {
closeParentBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
...removeInfo,
});
log('detected closeParentBehaior: ', closeParentBehavior);
}
if (closeParentBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE &&
tab.$TST.subtreeCollapsed)
Tree.collapseExpandSubtree(tab, {
collapsed: false,
justNow: true,
broadcast: false // because the tab is going to be closed, broadcasted Tree.collapseExpandSubtree can be ignored.
});
const postProcessParams = {
windowId: tab.windowId,
removedTab: tab.$TST.export(true),
structure: TreeBehavior.getTreeStructureFromTabs([tab, ...tab.$TST.descendants], {
full: true,
keepParentOfRootTabs: true
}),
insertBefore: tab, // not firstChild, because the "tab" is disappeared from tree.
parent: tab.$TST.parent,
newParent,
children: tab.$TST.children,
descendants: tab.$TST.descendants,
nearestFollowingRootTab: tab.$TST.nearestFollowingRootTab,
closeParentBehavior
};
if (tab.$TST.subtreeCollapsed) {
tryGrantCloseTab(tab, closeParentBehavior).then(async granted => {
if (!granted)
return;
log('Tabs.onRemoving: granted to close ', dumpTab(tab));
handleRemovingPostProcess(postProcessParams)
});
// First we always need to detach children from the closing parent.
// They will be processed again after confirmation.
Tree.detachAllChildren(tab, {
newParent,
behavior: Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN,
dontExpand: true,
dontUpdateIndent: true,
broadcast: true
});
}
else {
await handleRemovingPostProcess(postProcessParams)
}
const win = TabsStore.windows.get(tab.windowId);
if (!win.internalClosingTabs.has(tab.$TST.parentId))
Tree.detachTab(tab, {
dontUpdateIndent: true,
dontSyncParentToOpenerTab: true,
broadcast: true
});
});
async function handleRemovingPostProcess({ closeParentBehavior, windowId, parent, newParent, insertBefore, nearestFollowingRootTab, children, descendants, removedTab, structure } = {}) {
log('handleRemovingPostProcess ', { closeParentBehavior, windowId, parent, newParent, insertBefore, nearestFollowingRootTab, children, descendants, removedTab, structure });
if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
await closeChildTabs(descendants, {
triggerTab: removedTab,
originalStructure: structure
});
const replacedGroupTab = (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB) ?
await TabsGroup.tryReplaceTabWithGroup(null, { windowId, parent, children, insertBefore, newParent }) :
null;
if (!replacedGroupTab) {
await Tree.detachAllChildren(null, {
windowId,
parent,
newParent,
children,
descendants,
nearestFollowingRootTab,
behavior: closeParentBehavior,
broadcast: true
});
}
}
async function tryGrantCloseTab(tab, closeParentBehavior) {
log('tryGrantClose: ', { alreadyGranted: configs.grantedRemovingTabIds, closing: dumpTab(tab) });
const alreadyGranted = configs.grantedRemovingTabIds.includes(tab.id);
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);
if (!tab || alreadyGranted) {
log(' => no need to confirm');
return true;
}
const self = tryGrantCloseTab;
self.closingTabIds.push(tab.id);
if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE) {
self.closingDescendantTabIds = self.closingDescendantTabIds
.concat(TreeBehavior.getClosingTabsFromParent(tab).map(tab => tab.id));
self.closingDescendantTabIds = Array.from(new Set(self.closingDescendantTabIds));
}
if (self.promisedGrantedToCloseTabs) {
log(' => have promisedGrantedToCloseTabs');
return self.promisedGrantedToCloseTabs;
}
self.closingTabWasActive = self.closingTabWasActive || tab.active;
let shouldRestoreCount;
self.promisedGrantedToCloseTabs = wait(250).then(async () => {
log(' => confirmation with delay');
const closingTabIds = new Set(self.closingTabIds);
let allClosingTabs = new Set();
allClosingTabs.add(tab);
self.closingTabIds = Array.from(closingTabIds);
self.closingDescendantTabIds = mapAndFilterUniq(self.closingDescendantTabIds, id => {
if (closingTabIds.has(id))
return undefined;
const tab = Tab.get(id);
if (tab) // ignore already closed tabs
allClosingTabs.add(tab);
return id;
});
allClosingTabs = Array.from(allClosingTabs);
shouldRestoreCount = self.closingTabIds.length;
const restorableClosingTabsCount = countMatched(
allClosingTabs,
tab => tab.url != 'about:blank' &&
!tab.$TST.isNewTabCommandTab
);
log(' => restorableClosingTabsCount: ', restorableClosingTabsCount);
if (restorableClosingTabsCount > 0) {
log('tryGrantClose: show confirmation for ', allClosingTabs);
return Background.confirmToCloseTabs(allClosingTabs.slice(1).map(tab => tab.$TST.sanitized), {
windowId: tab.windowId,
messageKey: 'warnOnCloseTabs_fromOutside_message',
titleKey: 'warnOnCloseTabs_fromOutside_title',
minConfirmCount: 0
});
}
return true;
})
.then(async (granted) => {
log(' => granted: ', granted);
// remove the closed tab itself because it is already closed!
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);
if (granted)
return true;
log(`tryGrantClose: not granted, restore ${shouldRestoreCount} tabs`);
// this is required to wait until the closing tab is stored to the "recently closed" list
wait(0).then(async () => {
const restoredTabs = await Commands.restoreTabs(shouldRestoreCount);
log('tryGrantClose: restored ', restoredTabs);
});
return false;
});
const granted = await self.promisedGrantedToCloseTabs;
self.closingTabIds = [];
self.closingDescendantTabIds = [];
self.closingTabWasActive = false;
self.promisedGrantedToCloseTabs = null;
return granted;
}
tryGrantCloseTab.closingTabIds = [];
tryGrantCloseTab.closingDescendantTabIds = [];
tryGrantCloseTab.closingTabWasActive = false;
tryGrantCloseTab.promisedGrantedToCloseTabs = null;
async function closeChildTabs(tabs, { triggerTab, originalStructure } = {}) {
//if (!fireTabSubtreeClosingEvent(parent, tabs))
// return;
//markAsClosedSet([parent].concat(tabs));
// close bottom to top!
await TabsInternalOperation.removeTabs(tabs.slice(0).reverse(), { triggerTab, originalStructure });
//fireTabSubtreeClosedEvent(parent, tabs);
}
Tab.onRemoved.addListener((tab, info) => {
log('Tabs.onRemoved: removed ', dumpTab(tab));
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);
if (info.isWindowClosing)
return;
// The removing tab may be attached to another tab or
// other tabs may be attached to the removing tab.
// We need to detach such relations always on this timing.
if (info.oldChildren.length > 0) {
Tree.detachAllChildren(tab, {
children: info.oldChildren,
parent: info.oldParent,
behavior: Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD,
broadcast: true
});
}
if (info.oldParent) {
Tree.detachTab(tab, {
dontSyncParentToOpenerTab: true,
parent: info.oldParent,
broadcast: true
});
}
});
browser.windows.onRemoved.addListener(windowId => {
const win = TabsStore.windows.get(windowId);
if (!win)
return;
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => !win.tabs.has(id));
});
Tab.onDetached.addListener((tab, info = {}) => {
log('Tabs.onDetached ', dumpTab(tab));
let closeParentBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE,
...info
});
if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
closeParentBehavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD;
const dontSyncParentToOpenerTab = info.trigger == 'tabs.onDetached';
Tree.detachAllChildren(tab, {
dontSyncParentToOpenerTab,
behavior: closeParentBehavior,
broadcast: true
});
//reserveCloseRelatedTabs(toBeClosedTabs);
Tree.detachTab(tab, {
dontSyncParentToOpenerTab,
dontUpdateIndent: true,
broadcast: true
});
//restoreTabAttributes(tab, backupAttributes);
});

View File

@@ -0,0 +1,549 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
dumpTab,
configs,
sanitizeForHTMLText,
compareAsNumber,
isFirefoxViewTab,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Bookmark from '/common/bookmark.js';
import * as Constants from '/common/constants.js';
import * as Dialog from '/common/dialog.js';
import * as Permissions from '/common/permissions.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TSTAPI from '/common/tst-api.js';
import { Tab, TreeItem } from '/common/TreeItem.js';
import * as TabsGroup from './tabs-group.js';
import * as TabsOpen from './tabs-open.js';
import * as Tree from './tree.js';
function log(...args) {
internalLogger('background/handle-tab-bunches', ...args);
}
// ====================================================================
// Detection of a bunch of tabs opened at same time.
// Firefox's WebExtensions API doesn't provide ability to know which tabs
// are opened together by a single trigger. Thus TST tries to detect such
// "tab bunches" based on their opened timing.
// ====================================================================
Tab.onBeforeCreate.addListener(async (tab, info) => {
const win = TabsStore.windows.get(tab.windowId);
if (!win)
return;
const openerId = tab.openerTabId;
const openerTab = openerId && (await browser.tabs.get(openerId).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError)));
if ((openerTab &&
(openerTab.pinned || isFirefoxViewTab(openerTab)) &&
openerTab.windowId == tab.windowId) ||
(!openerTab &&
!info.maybeOrphan)) {
if (win.preventToDetectTabBunchesUntil > Date.now()) {
win.preventToDetectTabBunchesUntil += configs.tabBunchesDetectionTimeout;
}
else {
win.openedNewTabs.set(tab.id, {
id: tab.id,
windowId: tab.windowId,
indexOnCreated: tab.$indexOnCreated,
openerId: openerTab?.id,
openerIsPinned: openerTab?.pinned,
openerIsFirefoxView: isFirefoxViewTab(openerTab),
maybeFromBookmark: tab.$TST.maybeFromBookmark,
shouldNotGrouped: TSTAPI.isGroupingBlocked(),
});
}
}
if (win.delayedTabBunchesDetection)
clearTimeout(win.delayedTabBunchesDetection);
win.delayedTabBunchesDetection = setTimeout(
tryDetectTabBunches,
configs.tabBunchesDetectionTimeout,
win
);
});
const mPossibleTabBunchesToBeGrouped = [];
const mPossibleTabBunchesFromBookmarks = [];
async function tryDetectTabBunches(win) {
if (Tab.needToWaitTracked(win.id))
await Tab.waitUntilTrackedAll(win.id);
if (Tab.needToWaitMoved(win.id))
await Tab.waitUntilMovedAll(win.id);
let tabReferences = Array.from(win.openedNewTabs.values());
log('tryDetectTabBunches for ', tabReferences);
win.openedNewTabs.clear();
tabReferences = tabReferences.filter(tabReference => {
if (!tabReference.id)
return false;
const tab = Tab.get(tabReference.id);
if (!tab)
return false;
const uniqueId = tab?.$TST?.uniqueId;
return !uniqueId || (!uniqueId.duplicated && !uniqueId.restored);
});
if (tabReferences.length == 0) {
log(' => there is no possible bunches of tabs.');
return;
}
if (tabReferences.length > 1) {
await Promise.all(tabReferences.map(tabReference => {
const tab = Tab.get(tabReference.id);
tab.$TST.temporaryMetadata.set('openedWithOthers', true);
// We need to wait until all tabs are handlede completely.
// Otherwise `tab.$TST.needToBeGroupedSiblings` may contain unrelated tabs
// (tabs opened from any other parent tab) unexpectedly.
return tab.$TST.opened;
}));
}
if (areTabsFromOtherDeviceWithInsertAfterCurrent(tabReferences) &&
configs.fixupOrderOfTabsFromOtherDevice) {
const ids = tabReferences.map(tabReference => tabReference.id);
const index = tabReferences.map(tabReference => Tab.get(tabReference.id).index).sort(compareAsNumber)[0];
log(' => gather tabs from other device at ', ids, index);
await browser.tabs.move(ids, { index });
}
if (configs.autoGroupNewTabsFromPinned ||
configs.autoGroupNewTabsFromFirefoxView ||
configs.autoGroupNewTabsFromOthers) {
mPossibleTabBunchesToBeGrouped.push(tabReferences);
tryGroupTabBunches();
}
if (configs.autoGroupNewTabsFromBookmarks ||
configs.restoreTreeForTabsFromBookmarks) {
mPossibleTabBunchesFromBookmarks.push(tabReferences);
tryHandlTabBunchesFromBookmarks();
}
}
async function tryGroupTabBunches() {
if (tryGroupTabBunches.running)
return;
const tabReferences = mPossibleTabBunchesToBeGrouped.shift();
if (!tabReferences)
return;
log('tryGroupTabBunches for ', tabReferences);
tryGroupTabBunches.running = true;
try {
const fromPinned = [];
const fromOthers = [];
// extract only pure new tabs
for (const tabReference of tabReferences) {
if (tabReference.shouldNotGrouped)
continue;
const tab = Tab.get(tabReference.id);
if (!tab)
continue;
if (tabReference.openerTabId)
tab.openerTabId = parseInt(tabReference.openerTabId); // restore the opener information
const uniqueId = tab.$TST.uniqueId;
if (tab.$TST.isGroupTab || uniqueId.duplicated || uniqueId.restored)
continue;
if (tabReference.openerIsPinned ||
tabReference.openerIsFirefoxView) {
// We should check the "autoGroupNewTabsFromPinned" config here,
// because to-be-grouped tabs should be ignored by the handler for
// "autoAttachSameSiteOrphan" behavior.
if ((tab.$TST.hasPinnedOpener &&
configs.autoGroupNewTabsFromPinned) ||
(tab.$TST.hasFirefoxViewOpener &&
configs.autoGroupNewTabsFromFirefoxView))
fromPinned.push(tab);
}
else {
fromOthers.push(tab);
}
}
log(' => ', { fromPinned, fromOthers });
if (fromPinned.length > 0 &&
(configs.autoGroupNewTabsFromPinned ||
configs.autoGroupNewTabsFromFirefoxView)) {
const newRootTabs = Tab.collectRootTabs(TreeItem.sort(fromPinned));
if (newRootTabs.length > 0) {
await tryGroupTabBunchesFromPinnedOpener(newRootTabs);
}
}
// We can assume that new tabs from a bookmark folder and from other
// sources won't be mixed.
const openedFromBookmarkFolder = fromOthers.length > 0 && await detectBookmarkFolderFromTabs(fromOthers, tabReferences.length);
log(' => tryGroupTabBunches:openedFromBookmarkFolder: ', !!openedFromBookmarkFolder);
const newRootTabs = Tab.collectRootTabs(TreeItem.sort(openedFromBookmarkFolder ? openedFromBookmarkFolder.tabs : fromOthers));
log(' newRootTabs: ', newRootTabs);
if (newRootTabs.length > 1 &&
!openedFromBookmarkFolder && // we should ignore tabs from bookmark folder: they should be handled by tryHandlTabBunchesFromBookmarks
configs.autoGroupNewTabsFromOthers) {
const granted = await confirmToAutoGroupNewTabsFromOthers(fromOthers);
if (granted)
await TabsGroup.groupTabs(newRootTabs, {
...TabsGroup.temporaryStateParams(configs.groupTabTemporaryStateForNewTabsFromOthers),
broadcast: true
});
}
}
catch(error) {
log('Error on tryGroupTabBunches: ', String(error), error.stack);
}
finally {
tryGroupTabBunches.running = false;
if (mPossibleTabBunchesToBeGrouped.length > 0)
tryGroupTabBunches();
}
}
async function confirmToAutoGroupNewTabsFromOthers(tabs) {
if (tabs.length <= 1 ||
!configs.warnOnAutoGroupNewTabs)
return true;
const windowId = tabs[0].windowId;
const win = await browser.windows.get(windowId);
const listing = configs.warnOnAutoGroupNewTabsWithListing ?
Dialog.tabsToHTMLList(tabs, {
maxRows: configs.warnOnAutoGroupNewTabsWithListingMaxRows,
maxHeight: Math.round(win.height * 0.8),
maxWidth: Math.round(win.width * 0.75)
}) :
'';
const result = await Dialog.show(win, {
content: `
<div>${sanitizeForHTMLText(browser.i18n.getMessage('warnOnAutoGroupNewTabs_message', [tabs.length]))}</div>${listing}
`.trim(),
buttons: [
browser.i18n.getMessage('warnOnAutoGroupNewTabs_close'),
browser.i18n.getMessage('warnOnAutoGroupNewTabs_cancel')
],
checkMessage: browser.i18n.getMessage('warnOnAutoGroupNewTabs_warnAgain'),
checked: true,
modal: true, // for popup
type: 'common-dialog', // for popup
url: ((await Permissions.isGranted(Permissions.ALL_URLS)) ? null : '/resources/blank.html'), // for popup
title: browser.i18n.getMessage('warnOnAutoGroupNewTabs_title'), // for popup
onShownInPopup(container) {
setTimeout(() => { // because window.requestAnimationFrame is decelerate for an invisible document.
// this need to be done on the next tick, to use the height of the box for calculation of dialog size
const style = container.querySelector('ul').style;
style.height = '0px'; // this makes the box shrinkable
style.maxHeight = 'none';
style.minHeight = '0px';
}, 0);
}
});
switch (result.buttonIndex) {
case 0:
if (!result.checked)
configs.warnOnAutoGroupNewTabs = false;
return true;
case 1:
if (!result.checked) {
configs.warnOnAutoGroupNewTabs = false;
configs.autoGroupNewTabsFromOthers = false;
}
default:
return false;
}
}
async function tryGroupTabBunchesFromPinnedOpener(rootTabs) {
log(`tryGroupTabBunchesFromPinnedOpener: ${rootTabs.length} root tabs are opened from pinned tabs`);
// First, collect pinned opener tabs.
let pinnedOpeners = [];
const childrenOfPinnedTabs = {};
for (const tab of rootTabs) {
const opener = tab.$TST.openerTab;
if (!pinnedOpeners.includes(opener))
pinnedOpeners.push(opener);
}
log('pinnedOpeners ', () => pinnedOpeners.map(dumpTab));
// Second, collect tabs opened from pinned openers including existing tabs
// (which were left ungrouped in previous process).
const openerOf = {};
const allRootTabs = await Tab.getRootTabs(rootTabs[0].windowId)
for (const tab of allRootTabs) {
if (tab.$TST.getAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER))
continue;
if (rootTabs.includes(tab)) { // newly opened tab
const opener = tab.$TST.openerTab;
if (!opener)
continue;
openerOf[tab.id] = opener;
const tabs = childrenOfPinnedTabs[opener.id] || [];
childrenOfPinnedTabs[opener.id] = tabs.concat([tab]);
continue;
}
const opener = Tab.getByUniqueId(tab.$TST.getAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID));
if (!opener ||
!(opener.pinned || isFirefoxViewTab(opener)) ||
opener.windowId != tab.windowId)
continue;
// existing and not yet grouped tab
if (!pinnedOpeners.includes(opener))
pinnedOpeners.push(opener);
openerOf[tab.id] = opener;
const tabs = childrenOfPinnedTabs[opener.id] || [];
childrenOfPinnedTabs[opener.id] = tabs.concat([tab]);
}
// Ignore pinned openeres which has no child tab to be grouped.
pinnedOpeners = pinnedOpeners.filter(opener => {
return childrenOfPinnedTabs[opener.id].length > 1 || Tab.getGroupTabForOpener(opener);
});
log(' => ', () => pinnedOpeners.map(dumpTab));
// Move newly opened tabs to expected position before grouping!
// Note that we should refer "insertNewChildAt" instead of "insertNewTabFromPinnedTabAt" / "insertNewTabFromFirefoxViewAt"
// because these children are going to be controlled in a sub tree.
for (const tab of rootTabs.slice(0).sort((a, b) => a.id - b.id)/* process them in the order they were opened */) {
const opener = openerOf[tab.id];
const siblings = tab.$TST.needToBeGroupedSiblings;
if (!pinnedOpeners.includes(opener) ||
Tab.getGroupTabForOpener(opener) ||
siblings.length == 0 ||
tab.$TST.temporaryMetadata.has('alreadyMovedAsOpenedFromPinnedOpener'))
continue;
let refTabs = {};
try {
refTabs = Tree.getReferenceTabsForNewChild(tab, null, {
lastRelatedTab: opener.$TST.previousLastRelatedTab,
parent: siblings[0],
children: siblings,
descendants: siblings.map(sibling => [sibling, ...sibling.$TST.descendants]).flat()
});
}
catch(_error) {
// insertChildAt == "no control" case
}
if (refTabs.insertAfter) {
await Tree.moveTabSubtreeAfter(
tab,
refTabs.insertAfter,
{ broadcast: true }
);
log(`newly opened child ${tab.id} has been moved after ${refTabs.insertAfter?.id}`);
}
else if (refTabs.insertBefore) {
await Tree.moveTabSubtreeBefore(
tab,
refTabs.insertBefore,
{ broadcast: true }
);
log(`newly opened child ${tab.id} has been moved before ${refTabs.insertBefore?.id}`);
}
else {
continue;
}
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
}
// Finally, try to group opened tabs.
const newGroupTabs = new Map();
for (const opener of pinnedOpeners) {
const children = childrenOfPinnedTabs[opener.id].sort((a, b) => a.index - b.index);
let parent = Tab.getGroupTabForOpener(opener);
if (parent) {
for (const child of children) {
TabsStore.removeToBeGroupedTab(child);
}
continue;
}
log(`trying to group children of ${dumpTab(opener)}: `, () => children.map(dumpTab));
const uri = TabsGroup.makeGroupTabURI({
title: browser.i18n.getMessage('groupTab_fromPinnedTab_label', opener.title),
openerTabId: opener.$TST.uniqueId.id,
...TabsGroup.temporaryStateParams(isFirefoxViewTab(opener) ? configs.groupTabTemporaryStateForChildrenOfFirefoxView : configs.groupTabTemporaryStateForChildrenOfPinned)
});
parent = await TabsOpen.openURIInTab(uri, {
windowId: opener.windowId,
insertBefore: children[0],
cookieStoreId: opener.cookieStoreId,
inBackground: true
});
log('opened group tab: ', dumpTab(parent));
newGroupTabs.set(opener, true);
for (const child of children) {
// Prevent the tab to be grouped again after it is ungrouped manually.
child.$TST.setAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER, true);
TabsStore.removeToBeGroupedTab(child);
await Tree.attachTabTo(child, parent, {
forceExpand: true, // this is required to avoid the group tab itself is active from active tab in collapsed tree
dontMove: true,
broadcast: true
});
}
if (opener.active)
parent.$TST.addState(Constants.kTAB_STATE_BUNDLED_ACTIVE);
}
return true;
}
async function tryHandlTabBunchesFromBookmarks() {
if (tryHandlTabBunchesFromBookmarks.running)
return;
const tabReferences = mPossibleTabBunchesFromBookmarks.shift();
if (!tabReferences)
return;
log('tryHandlTabBunchesFromBookmarks for ', tabReferences);
tryHandlTabBunchesFromBookmarks.running = true;
try {
const tabs = [];
for (const tabReference of tabReferences) {
const tab = Tab.get(tabReference.id);
if (!tab)
continue;
if (tabReference.openerTabId)
tab.openerTabId = parseInt(tabReference.openerTabId); // restore the opener information
const uniqueId = tab.$TST.uniqueId;
if (uniqueId.duplicated || uniqueId.restored)
continue;
tabs.push(tab);
}
log(' => ', { tabs });
// We can assume that new tabs from a bookmark folder and from other
// sources won't be mixed.
const openedFromBookmarkFolder = tabs.length > 0 && await detectBookmarkFolderFromTabs(tabs, tabReferences.length);
log(' => tryHandlTabBunchesFromBookmarks:openedFromBookmarkFolder: ', openedFromBookmarkFolder);
if (openedFromBookmarkFolder) {
if (configs.restoreTreeForTabsFromBookmarks) {
log(' ==> trying to restore tree structure from bookmark information');
const structure = await Bookmark.getTreeStructureFromBookmarkFolder(openedFromBookmarkFolder.folder);
log(' ==> structure:', structure);
if (structure.length == openedFromBookmarkFolder.tabs.length) {
log(' ===> apply');
await Tree.applyTreeStructureToTabs(openedFromBookmarkFolder.tabs, structure);
}
}
const newRootTabs = Tab.collectRootTabs(TreeItem.sort(openedFromBookmarkFolder.tabs));
if (newRootTabs.length > 1 &&
configs.autoGroupNewTabsFromBookmarks &&
tabReferences.every(tabReference => !tabReference.shouldNotGrouped)) {
log(' => tryHandlTabBunchesFromBookmarks:group');
await TabsGroup.groupTabs(openedFromBookmarkFolder.tabs, {
...TabsGroup.temporaryStateParams(configs.groupTabTemporaryStateForNewTabsFromBookmarks),
broadcast: true
});
}
}
}
catch(error) {
log('Error on tryHandlTabBunchesFromBookmarks: ', String(error), error.stack);
}
finally {
tryHandlTabBunchesFromBookmarks.running = false;
if (mPossibleTabBunchesFromBookmarks.length > 0)
tryHandlTabBunchesFromBookmarks();
}
}
async function detectBookmarkFolderFromTabs(tabs, allNewTabsCount = tabs.length) {
log('detectBookmarkFolderFromTabs: ', tabs, allNewTabsCount);
return new Promise((resolve, _reject) => {
const maybeFromBookmarks = [];
let restCount = tabs.length;
for (const tab of tabs) {
tab.$TST.promisedPossibleOpenerBookmarks.then(async bookmarks => {
log(` bookmarks from tab ${tab.id}: `, bookmarks);
restCount--;
if (bookmarks.length > 0)
maybeFromBookmarks.push(tab);
const folder = await tryDetectMostTabsContainedBookmarkFolder(maybeFromBookmarks, allNewTabsCount);
if (folder) {
log('detectBookmarkFolderFromTabs: found folder => ', { folder, tabs });
resolve({
folder,
tabs: maybeFromBookmarks,
});
}
else if (restCount == 0) {
resolve(null);
}
});
}
});
}
async function tryDetectMostTabsContainedBookmarkFolder(bookmarkedTabs, allNewTabsCount = bookmarkedTabs.length) {
log('tryDetectMostTabsContainedBookmarkFolder ', { bookmarkedTabs, allNewTabsCount });
const parentIds = bookmarkedTabs.map(tab => tab.$TST.possibleOpenerBookmarks.map(bookmark => bookmark.parentId)).flat();
const counts = [];
const countById = {};
for (const id of parentIds) {
if (!(id in countById))
counts.push(countById[id] = { id, count: 0 });
countById[id].count++;
}
log(' counts: ', counts);
if (counts.length == 0)
return null;
const greatestCountParent = counts.sort((a, b) => b.count - a.count)[0];
const minCount = allNewTabsCount * configs.tabsFromSameFolderMinThresholdPercentage / 100;
log(' => ', { greatestCountParent, minCount });
if (greatestCountParent.count <= minCount)
return null;
const items = await browser.bookmarks.get(greatestCountParent.id);
return Array.isArray(items) ? items[0] : items;
}
// Detect tabs sent from other device with `browser.tabs.insertAfterCurrent`=true based on their index
// (Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1596787 )
// See also: https://github.com/piroor/treestyletab/issues/2419
function areTabsFromOtherDeviceWithInsertAfterCurrent(tabReferences) {
if (tabReferences.length == 0)
return false;
const activeTab = Tab.getActiveTab(tabReferences[0].windowId);
if (!activeTab)
return false;
const activeIndex = activeTab.index;
const followingTabsCount = Tab.getTabs(activeTab.windowId).filter(tab => tab.index > activeIndex).length;
const createdCount = tabReferences.length;
const expectedIndices = [activeIndex + 1];
const actualIndices = tabReferences.map(tabReference => tabReference.indexOnCreated);
const overTabsCount = Math.max(0, createdCount - followingTabsCount);
const shouldCountDown = Math.min(createdCount - 1, createdCount - Math.floor(overTabsCount / 2));
const shouldCountUp = createdCount - shouldCountDown - 1;
for (let i = 0; i < shouldCountUp; i++) {
expectedIndices.push(activeIndex + 2 + i + (createdCount - overTabsCount));
}
for (let i = shouldCountDown - 1; i > -1; i--) {
expectedIndices.push(activeIndex + 1 + i);
}
const received = actualIndices.join(',') == expectedIndices.join(',');
log('areTabsFromOtherDeviceWithInsertAfterCurrent:', received, { overTabsCount, shouldCountUp, shouldCountDown, size: expectedIndices.length, actualIndices, expectedIndices });
return received;
}

View File

@@ -0,0 +1,471 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
dumpTab,
wait,
configs,
isMacOS,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as Permissions from '/common/permissions.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TSTAPI from '/common/tst-api.js';
import { Tab } from '/common/TreeItem.js';
import Window from '/common/Window.js';
import * as Background from './background.js';
import * as Tree from './tree.js';
function log(...args) {
internalLogger('background/handle-tab-focus', ...args);
}
const PHASE_LOADING = 0;
const PHASE_BACKGROUND_INITIALIZED = 1;
const PHASE_BACKGROUND_BUILT = 2;
const PHASE_BACKGROUND_READY = 3;
let mInitializationPhase = PHASE_LOADING;
Background.onInit.addListener(() => {
mInitializationPhase = PHASE_BACKGROUND_INITIALIZED;
});
Background.onBuilt.addListener(() => {
mInitializationPhase = PHASE_BACKGROUND_BUILT;
});
Background.onReady.addListener(() => {
mInitializationPhase = PHASE_BACKGROUND_READY;
});
let mTabSwitchedByShortcut = false;
let mMaybeTabSwitchingByShortcut = false;
const mLastTabsCountInWindow = new Map();
Window.onInitialized.addListener(win => {
browser.tabs.query({
windowId: win.id,
active: true
})
.then(activeTabs => {
// There may be no active tab on a startup...
if (activeTabs.length > 0 &&
!win.lastActiveTab)
win.lastActiveTab = activeTabs[0].id;
});
});
browser.windows.onRemoved.addListener(windowId => {
mLastTabsCountInWindow.delete(windowId);
});
Tab.onActivating.addListener(async (tab, info = {}) => { // return false if the activation should be canceled
log('Tabs.onActivating ', { tab: dumpTab(tab), info, mMaybeTabSwitchingByShortcut });
if (mMaybeTabSwitchingByShortcut) {
const lastCount = mLastTabsCountInWindow.get(tab.windowId);
const count = Tab.getAllTabs(tab.windowId).length;
if (lastCount != count) {
log('tabs are created or removed: cancel tab switching');
mMaybeTabSwitchingByShortcut = false;
}
mLastTabsCountInWindow.set(tab.windowId, count);
}
if (tab.$TST.temporaryMetadata.has('shouldReloadOnSelect')) {
browser.tabs.reload(tab.id)
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
tab.$TST.temporaryMetadata.delete('shouldReloadOnSelect');
}
const win = TabsStore.windows.get(tab.windowId);
log(' lastActiveTab: ', win.lastActiveTab); // it may be blank on a startup
const lastActiveTab = Tab.get(win.lastActiveTab || info.previousTabId);
cancelDelayedExpand(lastActiveTab);
const shouldSkipCollapsed = (
!info.byInternalOperation &&
mMaybeTabSwitchingByShortcut &&
configs.skipCollapsedTabsForTabSwitchingShortcuts
);
mTabSwitchedByShortcut = mMaybeTabSwitchingByShortcut;
const focusDirection = !lastActiveTab ?
0 :
(!lastActiveTab.$TST.nearestVisiblePrecedingTab &&
!tab.$TST.nearestVisibleFollowingTab) ?
-1 :
(!lastActiveTab.$TST.nearestVisibleFollowingTab &&
!tab.$TST.nearestVisiblePrecedingTab) ?
1 :
(lastActiveTab.index > tab.index) ?
-1 :
1;
const cache = {};
if (tab.$TST.collapsed) {
if (!tab.$TST.parent) {
// This is invalid case, generally never should happen,
// but actually happen on some environment:
// https://github.com/piroor/treestyletab/issues/1717
// So, always expand orphan collapsed tab as a failsafe.
Tree.collapseExpandTab(tab, {
collapsed: false,
broadcast: true
});
await handleNewActiveTab(tab, info);
}
else if (!shouldSkipCollapsed) {
log('=> reaction for focus given from outside of TST');
let allowed = false;
if (configs.unfocusableCollapsedTab) {
log(' => apply unfocusableCollapsedTab');
allowed = await TSTAPI.tryOperationAllowed(
TSTAPI.kNOTIFY_TRY_EXPAND_TREE_FROM_FOCUSED_COLLAPSED_TAB,
{ tab,
focusDirection },
{ tabProperties: ['tab'], cache }
);
TSTAPI.clearCache(cache);
if (allowed) {
const toBeExpandedAncestors = [tab].concat(tab.$TST.ancestors) ;
for (const ancestor of toBeExpandedAncestors) {
Tree.collapseExpandSubtree(ancestor, {
collapsed: false,
broadcast: true
});
}
}
else {
log(' => canceled by someone.');
}
}
info.allowed = allowed;
await handleNewActiveTab(tab, info);
}
if (shouldSkipCollapsed) {
log('=> reaction for focusing collapsed descendant while Ctrl-Tab/Ctrl-Shift-Tab');
let successor = tab.$TST.nearestVisibleAncestorOrSelf;
if (!successor) // this seems invalid case...
return false;
log('successor = ', successor.id);
if (shouldSkipCollapsed &&
(win.lastActiveTab == successor.id ||
successor.$TST.descendants.some(tab => tab.id == win.lastActiveTab)) &&
focusDirection > 0) {
log('=> redirect successor (focus moved from the successor itself or its descendants)');
successor = successor.$TST.nearestVisibleFollowingTab;
if (successor &&
successor.discarded &&
configs.avoidDiscardedTabToBeActivatedIfPossible)
successor = successor.$TST.nearestLoadedTabInTree ||
successor.$TST.nearestLoadedTab ||
successor;
if (!successor)
successor = Tab.getFirstVisibleTab(tab.windowId);
log('=> ', successor.id);
}
else if (!mTabSwitchedByShortcut && // intentional focus to a discarded tabs by Ctrl-Tab/Ctrl-Shift-Tab is always allowed!
successor.discarded &&
configs.avoidDiscardedTabToBeActivatedIfPossible) {
log('=> redirect successor (successor is discarded)');
successor = successor.$TST.nearestLoadedTabInTree ||
successor.$TST.nearestLoadedTab ||
successor;
log('=> ', successor.id);
}
const allowed = await TSTAPI.tryOperationAllowed(
TSTAPI.kNOTIFY_TRY_REDIRECT_FOCUS_FROM_COLLAPSED_TAB,
{ tab,
focusDirection },
{ tabProperties: ['tab'], cache }
);
TSTAPI.clearCache(cache);
if (allowed) {
win.lastActiveTab = successor.id;
if (mMaybeTabSwitchingByShortcut)
setupDelayedExpand(successor);
TabsInternalOperation.activateTab(successor, { silently: true });
log('Tabs.onActivating: discarded? ', dumpTab(tab), tab?.discarded);
if (tab.discarded)
tab.$TST.temporaryMetadata.set('discardURLAfterCompletelyLoaded', tab.url);
return false;
}
else {
log(' => canceled by someone.');
}
}
}
else if (info.byActiveTabRemove &&
(!configs.autoCollapseExpandSubtreeOnSelect ||
configs.autoCollapseExpandSubtreeOnSelectExceptActiveTabRemove)) {
log('=> reaction for removing current tab');
win.lastActiveTab = tab.id;
tryHighlightBundledTab(tab, {
...info,
shouldSkipCollapsed
});
return true;
}
else if (tab.$TST.hasChild &&
tab.$TST.subtreeCollapsed &&
!shouldSkipCollapsed) {
log('=> reaction for newly active parent tab');
await handleNewActiveTab(tab, info);
}
tab.$TST.temporaryMetadata.delete('discardOnCompletelyLoaded');
win.lastActiveTab = tab.id;
if (mMaybeTabSwitchingByShortcut)
setupDelayedExpand(tab);
else
tryHighlightBundledTab(tab, {
...info,
shouldSkipCollapsed
});
return true;
});
async function handleNewActiveTab(tab, { allowed, silently } = {}) {
log('handleNewActiveTab: ', dumpTab(tab), { allowed, silently });
const shouldCollapseExpandNow = configs.autoCollapseExpandSubtreeOnSelect;
const canCollapseTree = shouldCollapseExpandNow;
const canExpandTree = shouldCollapseExpandNow && !silently;
if (canExpandTree &&
allowed !== false) {
const cache = {};
const allowed = await TSTAPI.tryOperationAllowed(
tab.active ?
TSTAPI.kNOTIFY_TRY_EXPAND_TREE_FROM_FOCUSED_PARENT :
TSTAPI.kNOTIFY_TRY_EXPAND_TREE_FROM_FOCUSED_BUNDLED_PARENT,
{ tab },
{ tabProperties: ['tab'], cache }
);
if (!allowed)
return;
if (canCollapseTree &&
configs.autoExpandIntelligently)
await Tree.collapseExpandTreesIntelligentlyFor(tab, {
broadcast: true
});
else
Tree.collapseExpandSubtree(tab, {
collapsed: false,
broadcast: true
});
}
}
async function tryHighlightBundledTab(tab, { shouldSkipCollapsed, allowed, silently } = {}) {
const bundledTab = tab.$TST.bundledTab;
const oldBundledTabs = TabsStore.bundledActiveTabsInWindow.get(tab.windowId);
log('tryHighlightBundledTab ', {
tab: tab.id,
bundledTab: bundledTab?.id,
oldBundledTabs,
shouldSkipCollapsed,
allowed,
silently,
});
for (const tab of oldBundledTabs.values()) {
if (tab == bundledTab)
continue;
tab.$TST.removeState(Constants.kTAB_STATE_BUNDLED_ACTIVE);
}
if (!bundledTab)
return;
bundledTab.$TST.addState(Constants.kTAB_STATE_BUNDLED_ACTIVE);
await wait(100);
if (!tab.active || // ignore tab already inactivated while waiting
tab.$TST.hasOtherHighlighted || // ignore manual highlighting
bundledTab.pinned ||
!configs.syncActiveStateToBundledTabs)
return;
if (bundledTab.$TST.hasChild &&
bundledTab.$TST.subtreeCollapsed &&
!shouldSkipCollapsed)
await handleNewActiveTab(bundledTab, { allowed, silently });
}
Tab.onUpdated.addListener((tab, changeInfo = {}) => {
if ('url' in changeInfo) {
if (tab.$TST.temporaryMetadata.has('discardURLAfterCompletelyLoaded') &&
tab.$TST.temporaryMetadata.get('discardURLAfterCompletelyLoaded') != changeInfo.url)
tab.$TST.temporaryMetadata.delete('discardURLAfterCompletelyLoaded');
}
});
Tab.onStateChanged.addListener(tab => {
if (!tab ||
tab.status != 'complete')
return;
if (typeof browser.tabs.discard == 'function') {
if (tab.url == tab.$TST.temporaryMetadata.get('discardURLAfterCompletelyLoaded') &&
configs.autoDiscardTabForUnexpectedFocus) {
log('Try to discard accidentally restored tab (on restored) ', dumpTab(tab));
wait(configs.autoDiscardTabForUnexpectedFocusDelay).then(() => {
if (!TabsStore.ensureLivingItem(tab) ||
tab.active)
return;
if (tab.status == 'complete')
browser.tabs.discard(tab.id)
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
else
tab.$TST.temporaryMetadata.set('discardOnCompletelyLoaded', true);
});
}
else if (tab.$TST.temporaryMetadata.has('discardOnCompletelyLoaded') && !tab.active) {
log('Discard accidentally restored tab (on complete) ', dumpTab(tab));
browser.tabs.discard(tab.id)
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
}
}
tab.$TST.temporaryMetadata.delete('discardURLAfterCompletelyLoaded');
tab.$TST.temporaryMetadata.delete('discardOnCompletelyLoaded');
});
async function setupDelayedExpand(tab) {
if (!tab)
return;
cancelDelayedExpand(tab);
TabsStore.removeToBeExpandedTab(tab);
const cache = {};
const [ctrlTabHandlingEnabled, allowedToExpandViaAPI] = await Promise.all([
Permissions.isGranted(Permissions.ALL_URLS),
TSTAPI.tryOperationAllowed(
TSTAPI.kNOTIFY_TRY_EXPAND_TREE_FROM_LONG_PRESS_CTRL_KEY,
{ tab },
{ tabProperties: ['tab'], cache }
),
]);
if (!configs.autoExpandOnTabSwitchingShortcuts ||
!tab.$TST.hasChild ||
!tab.$TST.subtreeCollapsed ||
!ctrlTabHandlingEnabled ||
!allowedToExpandViaAPI)
return;
TabsStore.addToBeExpandedTab(tab);
tab.$TST.temporaryMetadata.set('delayedExpand', setTimeout(async () => {
if (!tab.$TST.temporaryMetadata.has('delayedExpand')) { // already canceled
log('delayed expand is already canceled ', tab.id);
return;
}
log('delayed expand by long-press of ctrl key on ', tab.id);
TabsStore.removeToBeExpandedTab(tab);
await Tree.collapseExpandTreesIntelligentlyFor(tab, {
broadcast: true
});
}, configs.autoExpandOnTabSwitchingShortcutsDelay));
}
function cancelDelayedExpand(tab) {
if (!tab ||
!tab.$TST.temporaryMetadata.has('delayedExpand'))
return;
clearTimeout(tab.$TST.temporaryMetadata.get('delayedExpand'));
tab.$TST.temporaryMetadata.delete('delayedExpand');
TabsStore.removeToBeExpandedTab(tab);
}
function cancelAllDelayedExpand(windowId) {
for (const tab of TabsStore.toBeExpandedTabsInWindow.get(windowId)) {
cancelDelayedExpand(tab);
}
}
Tab.onCollapsedStateChanged.addListener((tab, info = {}) => {
tab.$TST.toggleState(Constants.kTAB_STATE_COLLAPSED_DONE, info.collapsed, { broadcast: false });
});
Background.onReady.addListener(() => {
for (const tab of Tab.getAllTabs(null, { iterator: true })) {
tab.$TST.removeState(Constants.kTAB_STATE_BUNDLED_ACTIVE);
}
for (const tab of Tab.getActiveTabs({ iterator: true })) {
tryHighlightBundledTab(tab);
}
});
browser.windows.onFocusChanged.addListener(() => {
mMaybeTabSwitchingByShortcut = false;
});
browser.runtime.onMessage.addListener(onMessage);
function onMessage(message, sender) {
if (mInitializationPhase < PHASE_BACKGROUND_BUILT ||
!message ||
typeof message.type != 'string')
return;
//log('onMessage: ', message, sender);
switch (message.type) {
case Constants.kNOTIFY_TAB_MOUSEDOWN:
mMaybeTabSwitchingByShortcut =
mTabSwitchedByShortcut = false;
break;
case Constants.kCOMMAND_NOTIFY_MAY_START_TAB_SWITCH: {
if (message.modifier != (configs.accelKey || (isMacOS() ? 'meta' : 'control')))
return;
log('kCOMMAND_NOTIFY_MAY_START_TAB_SWITCH ', message.modifier);
mMaybeTabSwitchingByShortcut = true;
if (sender.tab?.active) {
const win = TabsStore.windows.get(sender.tab.windowId);
win.lastActiveTab = sender.tab.id;
}
if (sender.tab)
mLastTabsCountInWindow.set(sender.tab.windowId, Tab.getAllTabs(sender.tab.windowId).length);
}; break;
case Constants.kCOMMAND_NOTIFY_MAY_END_TAB_SWITCH:
if (message.modifier != (configs.accelKey || (isMacOS() ? 'meta' : 'control')))
return;
log('kCOMMAND_NOTIFY_MAY_END_TAB_SWITCH ', message.modifier);
return (async () => {
if (mTabSwitchedByShortcut &&
configs.skipCollapsedTabsForTabSwitchingShortcuts &&
sender.tab) {
await Tab.waitUntilTracked(sender.tab.id);
let tab = Tab.get(sender.tab.id);
if (!tab) {
let tabs = await browser.tabs.query({ currentWindow: true, active: true }).catch(ApiTabs.createErrorHandler());
if (tabs.length == 0)
tabs = await browser.tabs.query({ currentWindow: true }).catch(ApiTabs.createErrorHandler());
await Tab.waitUntilTracked(tabs[0].id);
tab = Tab.get(tabs[0].id);
}
cancelAllDelayedExpand(tab.windowId);
const cache = {};
if (configs.autoCollapseExpandSubtreeOnSelect &&
tab &&
TabsStore.windows.get(tab.windowId).lastActiveTab == tab.id &&
(await TSTAPI.tryOperationAllowed(
TSTAPI.kNOTIFY_TRY_EXPAND_TREE_FROM_END_TAB_SWITCH,
{ tab },
{ tabProperties: ['tab'], cache }
))) {
Tree.collapseExpandSubtree(tab, {
collapsed: false,
broadcast: true
});
}
}
mMaybeTabSwitchingByShortcut =
mTabSwitchedByShortcut = false;
})();
}
}

View File

@@ -0,0 +1,38 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import { Tab } from '/common/TreeItem.js';
function log(...args) {
internalLogger('background/handle-tab-multiselect', ...args);
}
Tab.onUpdated.addListener((tab, info, options = {}) => {
if (!('highlighted' in info) ||
!tab.$TST.subtreeCollapsed ||
tab.$TST.collapsed ||
!tab.$TST.multiselected ||
!options.inheritHighlighted)
return;
const collapsedDescendants = tab.$TST.descendants;
log('inherit highlighted state from root visible tab: ', {
highlighted: info.highlighted,
collapsedDescendants
});
for (const descendant of collapsedDescendants) {
browser.tabs.update(descendant.id, {
highlighted: info.highlighted,
active: descendant.active
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
}
});

View File

@@ -0,0 +1,140 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
configs
} from '/common/common.js';
import * as Constants from '/common/constants.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import { Tab, TreeItem } from '/common/TreeItem.js';
import * as Background from './background.js';
import * as BackgroundCache from './background-cache.js';
import * as Tree from './tree.js';
import * as TreeStructure from './tree-structure.js';
function log(...args) {
internalLogger('background/handle-tree-changes', ...args);
}
let mInitialized = false;
function reserveDetachHiddenTab(tab) {
reserveDetachHiddenTab.tabs.add(tab);
if (reserveDetachHiddenTab.reserved)
clearTimeout(reserveDetachHiddenTab.reserved);
reserveDetachHiddenTab.reserved = setTimeout(async () => {
delete reserveDetachHiddenTab.reserved;
const tabs = new Set(TreeItem.sort(Array.from(reserveDetachHiddenTab.tabs)));
reserveDetachHiddenTab.tabs.clear();
log('try to detach hidden tabs: ', tabs);
for (const tab of tabs) {
if (!TabsStore.ensureLivingItem(tab))
continue;
for (const descendant of tab.$TST.descendants) {
if (descendant.hidden)
continue;
const nearestVisibleAncestor = descendant.$TST.ancestors.find(ancestor => !ancestor.hidden && !tabs.has(ancestor));
if (nearestVisibleAncestor &&
nearestVisibleAncestor == descendant.$TST.parent)
continue;
for (const ancestor of descendant.$TST.ancestors) {
if (!ancestor.hidden &&
!ancestor.$TST.collapsed)
break;
if (!ancestor.$TST.subtreeCollapsed)
continue;
await Tree.collapseExpandSubtree(ancestor, {
collapsed: false,
broadcast: true
});
}
if (nearestVisibleAncestor) {
log(` => reattach descendant ${descendant.id} to ${nearestVisibleAncestor.id}`);
await Tree.attachTabTo(descendant, nearestVisibleAncestor, {
dontMove: true,
broadcast: true
});
}
else {
log(` => detach descendant ${descendant.id}`);
await Tree.detachTab(descendant, {
broadcast: true
});
}
}
if (tab.$TST.hasParent &&
!tab.$TST.parent.hidden) {
log(` => detach hidden tab ${tab.id}`);
await Tree.detachTab(tab, {
broadcast: true
});
}
}
}, 100);
}
reserveDetachHiddenTab.tabs = new Set();
Tab.onHidden.addListener(tab => {
if (configs.fixupTreeOnTabVisibilityChanged)
reserveDetachHiddenTab(tab);
});
function reserveAttachShownTab(tab) {
tab.$TST.addState(Constants.kTAB_STATE_SHOWING);
reserveAttachShownTab.tabs.add(tab);
if (reserveAttachShownTab.reserved)
clearTimeout(reserveAttachShownTab.reserved);
reserveAttachShownTab.reserved = setTimeout(async () => {
delete reserveAttachShownTab.reserved;
const tabs = new Set(TreeItem.sort(Array.from(reserveAttachShownTab.tabs)));
reserveAttachShownTab.tabs.clear();
log('try to attach shown tabs: ', tabs);
for (const tab of tabs) {
if (!TabsStore.ensureLivingItem(tab) ||
tab.$TST.hasParent) {
tab.$TST.removeState(Constants.kTAB_STATE_SHOWING);
continue;
}
const referenceTabs = TreeBehavior.calculateReferenceItemsFromInsertionPosition(tab, {
context: Constants.kINSERTION_CONTEXT_SHOWN,
insertAfter: tab.$TST.nearestVisiblePrecedingTab,
// Instead of nearestFollowingForeignerTab, to avoid placing the tab
// after hidden tabs (too far from the target)
insertBefore: tab.$TST.unsafeNearestFollowingForeignerTab
});
if (referenceTabs.parent) {
log(` => attach shown tab ${tab.id} to ${referenceTabs.parent.id}`);
await Tree.attachTabTo(tab, referenceTabs.parent, {
insertBefore: referenceTabs.insertBefore,
insertAfter: referenceTabs.insertAfter,
broadcast: true
});
}
tab.$TST.removeState(Constants.kTAB_STATE_SHOWING);
}
}, 100);
}
reserveAttachShownTab.tabs = new Set();
Tab.onShown.addListener(tab => {
if (configs.fixupTreeOnTabVisibilityChanged)
reserveAttachShownTab(tab);
});
Background.onReady.addListener(() => {
mInitialized = true;
});
Tree.onSubtreeCollapsedStateChanged.addListener((tab, _info) => {
if (mInitialized)
TreeStructure.reserveToSaveTreeStructure(tab.windowId);
BackgroundCache.markWindowCacheDirtyFromTab(tab, Constants.kWINDOW_STATE_CACHED_SIDEBAR_COLLAPSED_DIRTY);
});

View File

@@ -0,0 +1,36 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import './index.js';
import {
configs,
} from '/common/common.js';
import '/common/sync-provider.js';
import './auto-sticky-tabs.js';
import './handle-autoplay-blocking.js';
import './handle-chrome-menu-commands.js';
import './prefs.js';
import './sharing-service.js';
import * as TabsOpen from './tabs-open.js';
browser.waterfoxBridge.initUI();
TabsOpen.onForbiddenURLRequested.addListener(url => {
browser.waterfoxBridge.reserveToLoadForbiddenURL(url);
});
browser.waterfoxBridge.onSidebarShown.addListener(windowId => {
configs.sidebarVirtuallyClosedWindows = configs.sidebarVirtuallyClosedWindows.filter(id => id != windowId);
});
browser.waterfoxBridge.onSidebarHidden.addListener(windowId => {
if (!configs.sidebarVirtuallyClosedWindows.includes(windowId))
configs.sidebarVirtuallyClosedWindows = [...configs.sidebarVirtuallyClosedWindows, windowId];
});

View File

@@ -0,0 +1,48 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log,
configs
} from '/common/common.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as TabsStore from '/common/tabs-store.js';
import MetricsData from '/common/MetricsData.js';
import { Tab } from '/common/TreeItem.js';
import * as Background from './background.js';
import './handle-misc.js';
import './handle-moved-tabs.js';
import './handle-new-tabs.js';
import './handle-removed-tabs.js';
import './handle-tab-bunches.js';
import './handle-tab-focus.js';
import './handle-tab-multiselect.js';
import './handle-tree-changes.js';
import './sync-background.js';
log.context = 'BG';
MetricsData.add('index: Loaded');
window.addEventListener('DOMContentLoaded', Background.init, { once: true });
window.dumpMetricsData = () => {
return MetricsData.toString();
};
window.dumpLogs = () => {
return log.logs.join('\n');
};
// for old debugging method
window.log = log;
window.gMetricsData = MetricsData;
window.Tab = Tab;
window.TabsStore = TabsStore;
window.SidebarConnection = SidebarConnection;
window.configs = configs;

View File

@@ -0,0 +1,505 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
configs,
saveUserStyleRules,
notify,
wait,
isLinux,
isMacOS,
} from '/common/common.js';
import * as Constants from '/common/constants.js';
import * as Permissions from '/common/permissions.js';
// eslint-disable-next-line no-unused-vars
function log(...args) {
internalLogger('background/migration', ...args);
}
const kCONFIGS_VERSION = 33;
const kFEATURES_VERSION = 9;
export function migrateConfigs() {
switch (configs.configsVersion) {
case 0:
case 1:
if (configs.startDragTimeout !== null)
configs.longPressDuration = configs.startDragTimeout;
if (configs.emulateDefaultContextMenu !== null)
configs.emulateDefaultContextMenu = configs.emulateDefaultContextMenu;
case 2:
if (configs.simulateSelectOwnerOnClose !== null &&
!configs.simulateSelectOwnerOnClose)
configs.successorTabControlLevel = Constants.kSUCCESSOR_TAB_CONTROL_NEVER;
case 3:
if (!(configs.tabDragBehavior & Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK))
configs.tabDragBehavior |= Constants.kDRAG_BEHAVIOR_TEAR_OFF;
if (!(configs.tabDragBehaviorShift & Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK))
configs.tabDragBehaviorShift |= Constants.kDRAG_BEHAVIOR_TEAR_OFF;
case 4:
if (configs.fakeContextMenu !== null)
configs.emulateDefaultContextMenu = configs.fakeContextMenu;
if (configs.context_closeTabOptions_closeTree !== null)
configs.context_topLevel_closeTree = configs.context_closeTabOptions_closeTree;
if (configs.context_closeTabOptions_closeDescendants !== null)
configs.context_topLevel_closeDescendants = configs.context_closeTabOptions_closeDescendants;
if (configs.context_closeTabOptions_closeOthers !== null)
configs.context_topLevel_closeOthers = configs.context_closeTabOptions_closeOthers;
case 5:
if (configs.scrollbarMode !== null) {
switch (configs.scrollbarMode < 0 ? (isMacOS() ? 3 : 1) : configs.scrollbarMode) {
case 0: // default, refular width
configs.userStyleRules += `
/* regular width scrollbar */
#tabbar { scrollbar-width: auto; }`;
break;
case 1: // narrow width
break;
case 2: // hide
configs.userStyleRules += `
/* hide scrollbar */
#tabbar { scrollbar-width: none; }
/* cancel spaces for macOS overlay scrollbar */
:root.platform-mac #tabbar:dir(rtl).overflow .tab:not(.pinned) {
padding-inline-start: 0;
}
:root.platform-mac #tabbar:dir(ltr).overflow .tab:not(.pinned) {
padding-inline-end: 0;
}`;
break;
case 3: // overlay (macOS)
break;
}
}
if (configs.sidebarScrollbarPosition !== null) {
switch (configs.sidebarScrollbarPosition) {
default:
case 0: // auto
case 1: // left
break;
break;
case 2: // right
configs.userStyleRules += `
/* put scrollbar rightside */
:root.left #tabbar { direction: ltr; }`;
break;
}
}
case 6:
if (configs.promoteFirstChildForClosedRoot != null &&
configs.promoteFirstChildForClosedRoot &&
configs.closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN)
configs.closeParentBehavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY;
if (configs.parentTabBehaviorForChanges !== null) {
switch (configs.parentTabBehaviorForChanges) {
case Constants.kPARENT_TAB_BEHAVIOR_ALWAYS:
configs.parentTabOperationBehaviorMode = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CONSISTENT;
break;
default:
case Constants.kPARENT_TAB_BEHAVIOR_ONLY_WHEN_VISIBLE:
configs.parentTabOperationBehaviorMode = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_PARALLEL;
break;
case Constants.kPARENT_TAB_BEHAVIOR_ONLY_ON_SIDEBAR:
configs.parentTabOperationBehaviorMode = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CUSTOM;
configs.closeParentBehavior_outsideSidebar_expanded = configs.closeParentBehavior_noSidebar_expanded = configs. closeParentBehavior;
break;
}
}
case 7:
if (configs.collapseExpandSubtreeByDblClick !== null &&
configs.collapseExpandSubtreeByDblClick)
configs.treeDoubleClickBehavior = Constants.kTREE_DOUBLE_CLICK_BEHAVIOR_TOGGLE_COLLAPSED;
case 8:
if (configs.autoExpandOnCollapsedChildActive !== null)
configs.unfocusableCollapsedTab = configs.autoExpandOnCollapsedChildActive;
case 9:
if (configs.simulateCloseTabByDblclick !== null &&
configs.simulateCloseTabByDblclick)
configs.treeDoubleClickBehavior = Constants.kTREE_DOUBLE_CLICK_BEHAVIOR_CLOSE;
case 10:
if (configs.style == 'plain-dark' ||
configs.style == 'metal')
configs.style = configs.$default.style;
case 11:
if (configs.userStyleRules) {
configs.userStyleRules0 = configs.userStyleRules;
configs.userStyleRules = '';
}
case 12:
try {
saveUserStyleRules(Array.from(new Uint8Array(8), (_, index) => {
const key = `userStyleRules${index}`;
if (key in configs) {
const chunk = configs[key];
configs[key] = '';
return chunk || '';
}
else {
return '';
}
}).join(''));
}
catch(error) {
console.error(error);
}
case 13:
if (configs.style == 'mixed' ||
configs.style == 'vertigo')
configs.style = 'photon';
case 14:
if (configs.inheritContextualIdentityToNewChildTab !== null)
configs.inheritContextualIdentityToChildTabMode = configs.inheritContextualIdentityToNewChildTab ? Constants.kCONTEXTUAL_IDENTITY_FROM_PARENT : Constants.kCONTEXTUAL_IDENTITY_DEFAULT;
if (configs.inheritContextualIdentityToSameSiteOrphan !== null)
configs.inheritContextualIdentityToSameSiteOrphanMode = configs.inheritContextualIdentityToSameSiteOrphan ? Constants.kCONTEXTUAL_IDENTITY_FROM_LAST_ACTIVE : Constants.kCONTEXTUAL_IDENTITY_DEFAULT;
if (configs.inheritContextualIdentityToTabsFromExternal !== null)
configs.inheritContextualIdentityToTabsFromExternalMode = configs.inheritContextualIdentityToTabsFromExternal ? Constants.kCONTEXTUAL_IDENTITY_FROM_PARENT : Constants.kCONTEXTUAL_IDENTITY_DEFAULT;
case 15:
if (configs.moveDroppedTabToNewWindowForUnhandledDragEvent !== null &&
!configs.moveDroppedTabToNewWindowForUnhandledDragEvent) {
if (configs.tabDragBehavior & Constants.kDRAG_BEHAVIOR_TEAR_OFF)
configs.tabDragBehavior = configs.tabDragBehavior ^ Constants.kDRAG_BEHAVIOR_TEAR_OFF;
else if (configs.tabDragBehavior & Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK)
configs.tabDragBehavior = configs.tabDragBehavior ^ Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK;
if (configs.tabDragBehaviorShift & Constants.kDRAG_BEHAVIOR_TEAR_OFF)
configs.tabDragBehaviorShift = configs.tabDragBehaviorShift ^ Constants.kDRAG_BEHAVIOR_TEAR_OFF;
else if (configs.tabDragBehaviorShift & Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK)
configs.tabDragBehaviorShift = configs.tabDragBehaviorShift ^ Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK;
}
case 16:
configs.maximumDelayForBug1561879 = Math.max(configs.$default.maximumDelayForBug1561879, configs.maximumDelayForBug1561879);
case 17:
configs.tabDragBehavior |= Constants.kDRAG_BEHAVIOR_MOVE;
configs.tabDragBehaviorShift |= Constants.kDRAG_BEHAVIOR_MOVE;
case 18:
if (configs.connectionTimeoutDelay == 5000)
configs.connectionTimeoutDelay = configs.$default.connectionTimeoutDelay;
case 19:
if (configs.suppressGapFromShownOrHiddenToolbar !== null) {
configs.suppressGapFromShownOrHiddenToolbarOnNewTab =
configs.suppressGapFromShownOrHiddenToolbarOnFullScreen = configs.suppressGapFromShownOrHiddenToolbar;
}
case 20:
if (configs.treatTreeAsExpandedOnClosedWithNoSidebar !== null) {
configs.treatTreeAsExpandedOnClosed_noSidebar = configs.treatTreeAsExpandedOnClosedWithNoSidebar;
}
case 21:
if (configs.style == 'plain')
configs.style = 'photon';
case 22:
case 23:
if (configs.closeParentBehaviorMode !== null) {
configs.parentTabOperationBehaviorMode = configs.closeParentBehaviorMode;
}
if (configs.closeParentBehavior !== null) {
configs.closeParentBehavior_insideSidebar_expanded =
configs.closeParentBehavior;
}
if (configs.closeParentBehavior_outsideSidebar !== null) {
configs.closeParentBehavior_outsideSidebar_expanded =
configs.moveParentBehavior_outsideSidebar_expanded =
configs.closeParentBehavior_outsideSidebar;
}
if (configs.closeParentBehavior_noSidebar !== null) {
configs.closeParentBehavior_noSidebar_expanded =
configs.closeParentBehavior_noSidebar_expanded =
configs.closeParentBehavior_noSidebar;
}
if (configs.treatTreeAsExpandedOnClosed_outsideSidebar === true) {
configs.closeParentBehavior_outsideSidebar_collapsed =
configs.moveParentBehavior_outsideSidebar_collapsed =
configs.moveParentBehavior_outsideSidebar_expanded;
}
if (configs.treatTreeAsExpandedOnClosed_noSidebar === false) {
configs.closeParentBehavior_noSidebar_collapsed =
configs.moveParentBehavior_noSidebar_collapsed =
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE;
}
if (configs.treatTreeAsExpandedOnClosed_outsideSidebar === true ||
configs.treatTreeAsExpandedOnClosed_noSidebar === false) {
configs.parentTabOperationBehaviorMode = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CUSTOM;
}
case 24:
if (configs.autoGroupNewTabsTimeout !== null)
configs.tabBunchesDetectionTimeout = configs.autoGroupNewTabsTimeout;
if (configs.autoGroupNewTabsDelayOnNewWindow !== null)
configs.tabBunchesDetectionDelayOnNewWindow = configs.autoGroupNewTabsDelayOnNewWindow;
case 25:
if (configs.autoHiddenScrollbarPlaceholderSize !== null)
configs.shiftTabsForScrollbarDistance = configs.autoHiddenScrollbarPlaceholderSize;
case 26:
if (!configs.guessNewOrphanTabAsOpenedByNewTabCommandUrl.includes('about:privatebrowsing'))
configs.guessNewOrphanTabAsOpenedByNewTabCommandUrl = `${configs.guessNewOrphanTabAsOpenedByNewTabCommandUrl.trim().replace(/\|$/, '')}|about:privatebrowsing`;
case 27:
if (configs.openAllBookmarksWithGroupAlways !== null)
configs.suppressGroupTabForStructuredTabsFromBookmarks = !configs.openAllBookmarksWithGroupAlways;
case 28:
if (configs.heartbeatInterval == 1000)
configs.heartbeatInterval = configs.$default.heartbeatInterval;
case 29:
const autoAttachKeys = [
'autoAttachOnOpenedWithOwner',
'autoAttachOnNewTabCommand',
'autoAttachOnContextNewTabCommand',
'autoAttachOnNewTabButtonMiddleClick',
'autoAttachOnNewTabButtonAccelClick',
'autoAttachOnDuplicated',
'autoAttachSameSiteOrphan',
'autoAttachOnOpenedFromExternal',
'autoAttachOnAnyOtherTrigger',
];
if (configs.insertNewChildAt != configs.$default.insertNewChildAt &&
(configs.insertNewChildAt == Constants.kINSERT_TOP ||
configs.insertNewChildAt == Constants.kINSERT_END)) {
for (const key of autoAttachKeys) {
if (configs[key] != configs.$default[key] &&
configs[key] != Constants.kNEWTAB_OPEN_AS_CHILD)
continue;
if (configs.insertNewChildAt == Constants.kINSERT_TOP)
configs[key] = Constants.kNEWTAB_OPEN_AS_CHILD_TOP;
else
configs[key] = Constants.kNEWTAB_OPEN_AS_CHILD_END;
}
}
else {
for (const key of autoAttachKeys) {
if (configs[key] == Constants.kNEWTAB_OPEN_AS_CHILD)
configs[key] = Constants.kNEWTAB_OPEN_AS_CHILD_TOP;
}
}
case 30:
browser.windows.getAll().then(windows => {
for (const win of windows) {
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_SIDEBAR);
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY);
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_SIDEBAR_COLLAPSED_DIRTY);
}
});
case 31:
if (configs.tabPreviewTooltipInSidebar !== null)
configs.tabPreviewTooltipRenderIn = configs.tabPreviewTooltipInSidebar ?
Constants.kIN_CONTENT_PANEL_RENDER_IN_ANYWHERE :
Constants.kIN_CONTENT_PANEL_RENDER_IN_CONTENT;
case 32:
if (configs.tabPreviewTooltipOffsetTop !== null)
configs.inContentUIOffsetTop = configs.tabPreviewTooltipOffsetTop;
}
configs.configsVersion = kCONFIGS_VERSION;
}
let mShouldShowInitialStartupPage = false;
export function tryNotifyNewFeatures() {
/*
let featuresVersionOffset = 0;
const browserInfo = await browser.runtime.getBrowserInfo().catch(ApiTabs.createErrorHandler());
// "search" permission becomes available!
if (parseInt(browserInfo.version.split('.')[0]) >= 63)
featuresVersionOffset++;
// "menus.overrideContext" permission becomes available!
if (parseInt(browserInfo.version.split('.')[0]) >= 64)
featuresVersionOffset++;
*/
const featuresVersion = kFEATURES_VERSION /*+ featuresVersionOffset*/;
const isInitialInstall = configs.notifiedFeaturesVersion == 0;
if (configs.notifiedFeaturesVersion >= featuresVersion)
return;
configs.notifiedFeaturesVersion = featuresVersion;
if (isInitialInstall &&
!configs.syncOtherDevicesDetected &&
Object.keys(configs.syncDevices).length > 1) {
configs.syncAvailableNotified = true;
configs.syncOtherDevicesDetected = true;
}
const typeSuffix = isInitialInstall ? 'installed' : 'updated';
const platformSuffix = isLinux() ? '_linux' : '';
const url = isInitialInstall ? Constants.kSHORTHAND_URIS.startup : browser.i18n.getMessage('message_startup_history_uri');
notify({
url,
title: browser.i18n.getMessage(`startup_notification_title_${typeSuffix}`),
message: browser.i18n.getMessage(`startup_notification_message_${typeSuffix}${platformSuffix}`),
timeout: 90 * 1000
});
if (isInitialInstall) {
mShouldShowInitialStartupPage = true;
browser.browserAction.setBadgeText({
text: '!',
});
}
}
export function isInitialStartup() {
return !!mShouldShowInitialStartupPage;
}
export function openInitialStartupPage() {
mShouldShowInitialStartupPage = false;
browser.browserAction.setBadgeText({
text: null,
});
browser.tabs.create({
url: Constants.kSHORTHAND_URIS.startup,
active: true,
});
}
// Auto-migration of bookmarked internal URLs
//
// Internal URLs like "moz-extension://(UUID)/..." are runtime environment
// dependent and unavailable when such bookmarks are loaded in different
// runtime environment, for example they are synchronized from other devices.
// Thus we should migrate such internal URLs to universal shorthand URIs like
// "ext+ws:(name)".
export async function migrateBookmarkUrls() {
const granted = await Permissions.isGranted(Permissions.BOOKMARKS);
if (!granted)
return;
startBookmarksUrlAutoMigration();
const urls = new Set(configs.migratedBookmarkUrls);
const migrations = [];
const updates = [];
for (const key in Constants.kSHORTHAND_URIS) {
const url = Constants.kSHORTHAND_URIS[key].split('?')[0];
if (urls.has(url))
continue;
const shorthand = `ext+ws:${key.toLowerCase()}`;
migrations.push(browser.bookmarks.search({ query: url })
.then(bookmarks => {
for (const bookmark of bookmarks) {
updates.push(browser.bookmarks.update(bookmark.id, {
url: bookmark.url.replace(url, shorthand)
}));
}
}));
urls.add(url);
}
if (migrations.length > 0)
await Promise.all(migrations);
if (updates.length > 0)
await Promise.all(updates);
if (urls.size > configs.migratedBookmarkUrls.length)
configs.migratedBookmarkUrls = Array.from(urls);
}
async function migrateBookmarkUrl(bookmark) {
for (const key in Constants.kSHORTHAND_URIS) {
const url = Constants.kSHORTHAND_URIS[key].split('?')[0];
if (!bookmark.url.startsWith(url))
continue;
const shorthand = `ext+ws:${key.toLowerCase()}`;
return browser.bookmarks.update(bookmark.id, {
url: bookmark.url.replace(url, shorthand)
});
}
}
let mObservingBookmarks = false;
let mBookmarksListenersRegistered = false;
function onBookmarkCreated(id, bookmark) {
if (!mObservingBookmarks)
return;
if (bookmark.url)
migrateBookmarkUrl(bookmark);
}
async function onBookmarkChanged(id, changeInfo) {
if (!mObservingBookmarks)
return;
if (changeInfo.url &&
changeInfo.url.startsWith(browser.runtime.getURL(''))) {
const bookmark = await browser.bookmarks.get(id);
if (Array.isArray(bookmark))
bookmark.forEach(migrateBookmarkUrl);
else
migrateBookmarkUrl(bookmark);
}
}
if (browser.bookmarks &&
browser.bookmarks.onCreated) {
browser.bookmarks.onCreated.addListener(onBookmarkCreated);
browser.bookmarks.onChanged.addListener(onBookmarkChanged);
mBookmarksListenersRegistered = true;
}
async function startBookmarksUrlAutoMigration() {
if (mObservingBookmarks)
return;
mObservingBookmarks = true;
if (mBookmarksListenersRegistered ||
!browser.bookmarks ||
!browser.bookmarks.onCreated)
return;
browser.bookmarks.onCreated.addListener(onBookmarkCreated);
browser.bookmarks.onChanged.addListener(onBookmarkChanged);
mBookmarksListenersRegistered = true;
}
configs.$loaded.then(() => {
configs.$addObserver(async key => {
// This may be triggered not only "reset all", but while importing of configs also.
// We should try initial migration after all configs are successfully imported.
await wait(100);
if (key == 'configsVersion' &&
configs.configsVersion == 0)
migrateConfigs();
});
});

View File

@@ -0,0 +1,439 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
wait,
configs,
shouldApplyAnimation,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import { Tab, TabGroup, TreeItem } from '/common/TreeItem.js';
import * as Tree from './tree.js';
function log(...args) {
internalLogger('background/native-tab-groups', ...args);
}
export const internallyMovingNativeTabGroups = new Map();
export async function addTabsToGroup(tabs, groupIdOrProperties) {
const initialGroupId = typeof groupIdOrProperties == 'number' ? groupIdOrProperties : null;
const groupId = await addTabsToGroupInternal(tabs, groupIdOrProperties);
const created = groupId != initialGroupId;
return { groupId, created };
}
async function addTabsToGroupInternal(tabs, groupIdOrProperties) {
let groupId = typeof groupIdOrProperties == 'number' ? groupIdOrProperties : null;
const tabsToGrouped = tabs.filter(tab => tab.groupId != groupId);
if (tabsToGrouped.length == 0) {
return groupId;
}
log('addTabsToGroupInternal ', tabsToGrouped, groupId, groupIdOrProperties);
const pinnedTabs = tabsToGrouped.filter(tab => tab.pinned);
if (pinnedTabs.length > 0) {
await Promise.all(
pinnedTabs.map(
tab => browser.tabs.update(tab.id, { pinned: false })
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError))
)
);
}
const windowId = tabsToGrouped[0].windowId;
const structure = TreeBehavior.getTreeStructureFromTabs(tabsToGrouped);
await Tree.detachTabsFromTree(tabsToGrouped, {
fromParent: true,
partial: true,
});
const { promisedGrouped, finish } = waitUntilGrouped(tabsToGrouped, {
groupId,
windowId,
});
log('addTabsToGroupInternal: group tabs!');
await browser.tabs.group({
groupId,
tabIds: tabsToGrouped.map(tab => tab.id),
...(groupId ? {} : {
createProperties: {
windowId, // We must specify the window ID explicitly, otherwise tabs moved across windows may be reverted and grouped in the old window!
},
})
});
const group = await promisedGrouped;
groupId = group.id;
log('addTabsToGroupInternal: => ', group);
for (const tab of tabsToGrouped) {
TabsStore.addNativelyGroupedTab(tab, group.windowId);
}
if (groupIdOrProperties &&
typeof groupIdOrProperties == 'object') {
log('addTabsToGroupInternal: applying group properties');
const updateProperties = {};
if ('title' in groupIdOrProperties) {
updateProperties.title = groupIdOrProperties.title;
}
if ('color' in groupIdOrProperties) {
updateProperties.color = groupIdOrProperties.color;
}
if ('collapsed' in groupIdOrProperties) {
updateProperties.collapsed = groupIdOrProperties.collapsed;
}
await browser.tabGroups.update(groupId, updateProperties);
}
finish();
await rejectGroupFromTree(group);
log('addTabsToGroupInternal: applying tree structure');
await Tree.applyTreeStructureToTabs(tabsToGrouped, structure, {
broadcast: true
});
return groupId;
}
export async function rejectGroupFromTree(group) {
if (!group) {
return;
}
group = TabGroup.get(group.id);
if (!group?.$TST) {
log('rejectGroupFromTree: failed to reject untracked group');
return;
}
const firstMember = group.$TST.firstMember;
const lastMember = group.$TST.lastMember;
const prevTab = firstMember?.$TST.previousTab;
const nextTab = lastMember?.$TST.nextTab;
const rootTab = prevTab?.$TST.rootTab;
if (!prevTab ||
!nextTab ||
prevTab.groupId != nextTab.groupId ||
prevTab.groupId != -1 ||
rootTab != nextTab.$TST.rootTab) {
log('rejectGroupFromTree: no need to reject from tree');
return;
}
log('rejectGroupFromTree ', group.id);
await Tree.detachTabsFromTree(group.$TST.members, {
fromParent: true,
partial: true,
});
// The group is in a middle of a tree. We need to move the new group away from the tree.
const lastDescendant = rootTab.$TST.lastDescendant;
if (firstMember.index - rootTab.index <= lastDescendant.index - lastMember.index) { // move above the tree
log('rejectGroupFromTree: move ', group.id, ' before ', rootTab.id);
await moveGroupBefore(group, rootTab);
}
else { // move below the tree
log('rejectGroupFromTree: move ', group.id, ' after ', lastDescendant.id);
await moveGroupAfter(group, lastDescendant);
}
}
function waitUntilGrouped(tabs, { groupId, windowId } = {}) {
const toBeGroupedIds = tabs.map(tab => tab.id);
const win = TabsStore.windows.get(windowId || tabs[0].windowId);
for (const tab of tabs) {
win.internallyMovingTabsForUpdatedNativeTabGroups.add(tab.id);
win.internalMovingTabs.set(tab.id, -1);
}
let onUpdated = null;
const { promisedMoved, finish: finishMoved } = waitUntilMoved(tabs, win.id)
const promisedGrouped = new Promise((resolve, _reject) => {
if (!groupId) {
const onGroupCreated = group => {
groupId = group.id;
browser.tabGroups.onCreated.removeListener(onGroupCreated);
};
browser.tabGroups.onCreated.addListener(onGroupCreated);
}
const toBeGroupedIdsSet = new Set(toBeGroupedIds);
onUpdated = (tabId, changeInfo, _tab) => {
if (changeInfo.groupId == groupId) {
toBeGroupedIdsSet.delete(tabId);
win.internallyMovingTabsForUpdatedNativeTabGroups.delete(tabId);
}
if (toBeGroupedIdsSet.size == 0) {
resolve(changeInfo.groupId);
}
};
browser.tabs.onUpdated.addListener(onUpdated, { properties: ['groupId'] });
});
const finish = () => {
if (finish.done) {
return;
}
browser.tabs.onUpdated.removeListener(onUpdated);
for (const tab of tabs) {
win.internalMovingTabs.delete(tab.id);
}
finish.done = true;
};
return {
promisedGrouped: Promise.all([
promisedGrouped,
Promise.race([
promisedMoved,
wait(configs.nativeTabGroupModificationDetectionTimeoutAfterTabMove),
]),
]).then(([groupId]) => {
finish();
finishMoved();
return TabGroup.get(groupId);
}),
finish,
};
}
export async function removeTabsFromGroup(tabs) {
const tabsToBeUngrouped = tabs.filter(tab => tab.groupId != -1);
if (tabsToBeUngrouped.length == 0) {
return;
}
const win = TabsStore.windows.get(tabs[0].windowId);
for (const tab of tabs) {
win.internallyMovingTabsForUpdatedNativeTabGroups.add(tab.id);
win.internalMovingTabs.set(tab.id, -1);
}
const toBeUngroupedIds = tabsToBeUngrouped.map(tab => tab.id);
let onUpdated = null;
await new Promise((resolve, _reject) => {
const toBeUngroupedIdsSet = new Set(toBeUngroupedIds);
onUpdated = (tabId, changeInfo, _tab) => {
if (changeInfo.groupId == -1) {
toBeUngroupedIdsSet.delete(tabId);
win.internallyMovingTabsForUpdatedNativeTabGroups.delete(tabId);
}
if (toBeUngroupedIdsSet.size == 0) {
resolve();
}
};
browser.tabs.onUpdated.addListener(onUpdated, { properties: ['groupId'] });
browser.tabs.ungroup(toBeUngroupedIds);
});
for (const tab of tabsToBeUngrouped) {
win.internalMovingTabs.delete(tab.id);
TabsStore.removeNativelyGroupedTab(tab, win.id);
}
browser.tabs.onUpdated.removeListener(onUpdated);
}
export async function matchTabsGrouped(tabs, groupIdOrCreateParams) {
if (groupIdOrCreateParams == -1) {
await removeTabsFromGroup(tabs);
}
else {
await addTabsToGroup(tabs, groupIdOrCreateParams);
}
}
export async function moveGroupToNewWindow({ groupId, windowId, duplicate, left, top }) {
log('moveGroupToNewWindow: ', groupId, windowId);
const group = TabGroup.get(groupId);
const members = group.$TST.members;
const movedTabs = await Tree.openNewWindowFromTabs(members, { duplicate, left, top });
await addTabsToGroupInternal(movedTabs, {
title: group.title,
color: group.color,
});
}
export async function moveGroupBefore(group, insertBefore) {
log('moveGroupBefore: ', group, insertBefore);
const beforeCount = internallyMovingNativeTabGroups.get(group.id) || 0;
internallyMovingNativeTabGroups.set(group.id, beforeCount + 1);
const { promisedMoved, finish } = waitUntilMoved(group, insertBefore.windowId);
if (insertBefore.type == TreeItem.TYPE_GROUP) {
insertBefore = insertBefore.$TST.firstMember;
}
const members = group.$TST.members;
const firstMember = group.$TST.firstMember;
const delta = insertBefore.windowId == group.windowId && insertBefore.index > firstMember.index ? members.length : 0;
const index = insertBefore.index - delta;
log('moveGroupBefore: move to ', index, { delta, insertBeforeIndex: insertBefore.index });
await browser.tabGroups.move(group.id, {
index,
windowId: insertBefore.windowId,
});
await Promise.race([
promisedMoved,
wait(configs.nativeTabGroupModificationDetectionTimeoutAfterTabMove).then(() => {
if (finish.done) {
return;
}
}),
]);
finish();
const afterCount = internallyMovingNativeTabGroups.get(group.id) || 0;
if (afterCount <= 1) {
internallyMovingNativeTabGroups.delete(group.id);
}
else {
internallyMovingNativeTabGroups.set(group.id, afterCount - 1);
}
log('moveGroupBefore: finish');
}
export async function moveGroupAfter(group, insertAfter) {
log('moveGroupAfter: ', group, insertAfter);
const beforeCount = internallyMovingNativeTabGroups.get(group.id) || 0;
internallyMovingNativeTabGroups.set(group.id, beforeCount + 1);
const { promisedMoved, finish } = waitUntilMoved(group, insertAfter.windowId);
if (insertAfter.type == TreeItem.TYPE_GROUP) {
if (insertAfter.collapsed) {
insertAfter = insertAfter.$TST.lastMember;
}
else {
return moveGroupBefore(group, insertAfter.$TST.firstMember);
}
}
const members = group.$TST.members;
const firstMember = group.$TST.firstMember;
const delta = insertAfter.windowId == group.windowId && insertAfter.index > firstMember.index ? members.length : 0;
const index = insertAfter.index + 1 - delta;
log('moveGroupAfter: move to ', index, { delta, insertAfterIndex: insertAfter.index });
await browser.tabGroups.move(group.id, {
index,
windowId: insertAfter.windowId,
});
await Promise.race([
promisedMoved,
wait(configs.nativeTabGroupModificationDetectionTimeoutAfterTabMove).then(() => {
if (finish.done) {
return;
}
}),
]);
finish();
const afterCount = internallyMovingNativeTabGroups.get(group.id) || 0;
if (afterCount <= 1) {
internallyMovingNativeTabGroups.delete(group.id);
}
else {
internallyMovingNativeTabGroups.set(group.id, afterCount - 1);
}
log('moveGroupAfter: finish');
}
export function waitUntilMoved(groupOrMembers, destinationWindowId) {
const members = Array.isArray(groupOrMembers) ?
groupOrMembers :
groupOrMembers.$TST.members;
const win = TabsStore.windows.get(destinationWindowId || members[0].windowId);
const toBeMovedTabs = new Set();
for (const tab of members) {
toBeMovedTabs.add(tab.id);
win.internalMovingTabs.set(tab.id, -1);
}
let onTabMoved;
const promisedMoved = new Promise((resolve, _reject) => {
onTabMoved = (tabId, _moveInfo) => {
if (toBeMovedTabs.has(tabId)) {
toBeMovedTabs.delete(tabId);
}
if (toBeMovedTabs.size == 0) {
log('waitUntilMoved: all members have been moved');
resolve();
}
};
browser.tabs.onMoved.addListener(onTabMoved);
});
const finish = () => {
if (finish.done) {
return;
}
browser.tabs.onMoved.removeListener(onTabMoved);
for (const tab of members) {
win.internalMovingTabs.delete(tab.id);
}
finish.done = true;
};
return {
promisedMoved: promisedMoved.then(finish),
finish,
};
}
function reserveToMaintainTreeForGroup(groupId, options = {}) {
let timer = reserveToMaintainTreeForGroup.delayed.get(groupId);
if (timer)
clearTimeout(timer);
if (options.justNow || !shouldApplyAnimation()) {
const group = TabGroup.get(groupId);
rejectGroupFromTree(group);
}
timer = setTimeout(() => {
reserveToMaintainTreeForGroup.delayed.delete(groupId);
const group = TabGroup.get(groupId);
rejectGroupFromTree(group);
}, configs.nativeTabGroupModificationDetectionTimeoutAfterTabMove);
reserveToMaintainTreeForGroup.delayed.set(groupId, timer);
}
reserveToMaintainTreeForGroup.delayed = new Map();
export async function startToMaintainTree() {
// fixup mismatched tree structure and tab groups constructed while TST is disabled
const groups = await browser.tabGroups.query({});
for (const group of groups) {
await rejectGroupFromTree(TabGroup.get(group.id));
}
// after all we start tracking of dynamic changes of tab groups
browser.tabGroups.onMoved.addListener(group => {
group = TabGroup.get(group.id);
if (!group) {
return;
}
log('detected tab group move: ', group);
const internalMoveCount = internallyMovingNativeTabGroups.get(group.id);
if (internalMoveCount) {
log(' => ignore internal move ', internalMoveCount);
return;
}
reserveToMaintainTreeForGroup(group.id);
});
Tab.onNativeGroupModified.addListener(tab => {
const win = TabsStore.windows.get(tab.windowId);
if (win.internallyMovingTabsForUpdatedNativeTabGroups.has(tab.id)) {
return;
}
reserveToMaintainTreeForGroup(tab.groupId);
});
}

View File

@@ -0,0 +1,100 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
configs,
obsoleteConfigs,
} from '/common/common.js';
import * as Constants from '/common/constants.js';
export const onChanged = new EventListenerManager();
if (Constants.IS_BACKGROUND) {
for (const [name, value] of Object.entries(configs.$default)) {
if (obsoleteConfigs.has(name))
continue;
switch (typeof value) {
case 'boolean':
browser.prefs.setDefaultBoolValue(name, value);
break;
case 'string':
browser.prefs.setDefaultStringValue(name, value);
break;
default:
browser.prefs.setDefaultStringValue(name, JSON.stringify(value));
break;
}
getPref(name).then(value => {
if (JSON.stringify(configs[name]) == JSON.stringify(value))
return;
configs[name] = value;
});
}
}
async function getPref(name) {
const defaultValue = configs.$default[name];
switch (typeof defaultValue) {
case 'boolean':
return browser.prefs.getBoolValue(name, defaultValue);
case 'string':
return browser.prefs.getStringValue(name, defaultValue);
default:
return browser.prefs.getStringValue(name, JSON.stringify(defaultValue)).then(value => JSON.parse(value));
}
}
const mNamesSyncToChrome = new Set();
const mNamesSyncFromChrome = new Set();
browser.prefs.onChanged.addListener(name => {
if (mNamesSyncToChrome.has(name) ||
mNamesSyncFromChrome.has(name))
return;
mNamesSyncFromChrome.add(name);
getPref(name)
.then(value => {
configs[name] = value;
window.requestAnimationFrame(() => {
mNamesSyncFromChrome.delete(name);
});
});
});
configs.$addObserver(async name => {
if (mNamesSyncToChrome.has(name) ||
mNamesSyncFromChrome.has(name))
return;
mNamesSyncToChrome.add(name);
const value = configs[name];
const defaultValue = configs.$default[name];
switch (typeof defaultValue) {
case 'boolean':
await browser.prefs.setBoolValue(name, value);
break;
case 'string':
await browser.prefs.setStringValue(name, value);
break;
default:
await browser.prefs.setStringValue(name, JSON.stringify(value));
break;
}
window.requestAnimationFrame(() => {
mNamesSyncToChrome.delete(name);
});
});

View File

@@ -0,0 +1,22 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import * as TabContextMenu from './tab-context-menu.js';
TabContextMenu.registerSharingService({
async listServices(tab) {
return browser.waterfoxBridge.listSharingServices(tab.id);
},
share(tab, shareName = null) {
return browser.waterfoxBridge.share(tab.id, shareName);
},
openPreferences() {
return browser.waterfoxBridge.openSharingPreferences();
},
});

View File

@@ -0,0 +1,467 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
dumpTab,
configs,
wait,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import { Tab } from '/common/TreeItem.js';
import * as Tree from './tree.js';
function log(...args) {
internalLogger('background/successor-tab', ...args);
}
const mTabsToBeUpdated = new Set();
const mInProgressUpdates = new Set();
const mPromisedUpdatedSuccessorTabId = new Map();
browser.tabs.onUpdated.addListener((tabId, updateInfo, _tab) => {
if (!('successorTabId' in updateInfo) ||
!mPromisedUpdatedSuccessorTabId.has(tabId))
return;
const promisedUpdate = mPromisedUpdatedSuccessorTabId.get(tabId);
mPromisedUpdatedSuccessorTabId.delete(tabId);
promisedUpdate.resolver(updateInfo.successorTabId);
}, {
// we cannot watch only the property...
// properties: ['successorTabId'],
});
TabsInternalOperation.onBeforeTabsRemove.addListener(async tabs => {
let activeTab = null;
const tabIds = tabs.map(tab => {
if (tab.active)
activeTab = tab;
return tab.id;
});
if (activeTab)
await updateInternal(activeTab.id, tabIds);
});
function setSuccessor(tabId, successorTabId = -1) {
const tab = Tab.get(tabId);
const successorTab = Tab.get(successorTabId);
if (configs.successorTabControlLevel == Constants.kSUCCESSOR_TAB_CONTROL_NEVER ||
!tab ||
!successorTab ||
tab.windowId != successorTab.windowId)
return;
const promisedUpdate = {};
promisedUpdate.promisedSuccessorTabId = new Promise((resolve, _reject) => {
promisedUpdate.resolver = resolve;
setTimeout(() => {
if (!mPromisedUpdatedSuccessorTabId.has(tabId))
return;
mPromisedUpdatedSuccessorTabId.delete(tabId);
resolve(null);
}, 2000);
});
mPromisedUpdatedSuccessorTabId.set(tabId, promisedUpdate);
const initialSuccessorTabId = tab.successorTabId;
browser.tabs.update(tabId, {
successorTabId
}).then(async () => {
// tabs.onUpdated listener won't be called sometimes, so this is a failsafe.
while (true) {
const promisedUpdate = mPromisedUpdatedSuccessorTabId.get(tabId);
if (!promisedUpdate)
break;
const tab = await browser.tabs.get(tabId);
if (tab.successorTabId == initialSuccessorTabId &&
tab.successorTabId != successorTabId) {
await wait(200);
continue;
}
mPromisedUpdatedSuccessorTabId.delete(tabId);
promisedUpdate.resolver(tab.successorTabId);
}
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, error => {
// ignore error for already closed tab
if (!error ||
!error.message ||
(error.message.indexOf('Invalid successorTabId') != 0 &&
// This error may happen at the time just after a tab is detached from its original window.
error.message.indexOf('Successor tab must be in the same window as the tab being updated') != 0))
throw error;
}));
}
function clearSuccessor(tabId) {
setSuccessor(tabId, -1);
}
function update(tabId) {
mTabsToBeUpdated.add(tabId);
if (mInProgressUpdates.size == 0) {
const waitingUpdate = new Promise((resolve, _reject) => {
const timer = setInterval(() => {
if (mInProgressUpdates.size > 1)
return;
clearInterval(timer);
resolve();
}, 100);
});
waitingUpdate.catch(_error => {}).then(() => mInProgressUpdates.delete(waitingUpdate));
mInProgressUpdates.add(waitingUpdate);
}
setTimeout(() => {
const ids = Array.from(mTabsToBeUpdated);
mTabsToBeUpdated.clear();
for (const id of ids) {
if (!id)
continue;
try {
const promisedUpdate = updateInternal(id);
const promisedUpdateWithTimeout = new Promise((resolve, reject) => {
promisedUpdate.then(resolve).catch(reject);
setTimeout(resolve, 1000);
});
mInProgressUpdates.add(promisedUpdateWithTimeout);
promisedUpdateWithTimeout.catch(_error => {}).then(() => mInProgressUpdates.delete(promisedUpdateWithTimeout));
}
catch(_error) {
}
}
}, 0);
}
async function updateInternal(tabId, excludeTabIds = []) {
// tabs.onActivated can be notified before the tab is completely tracked...
await Tab.waitUntilTracked(tabId);
const tab = Tab.get(tabId);
if (!tab)
return;
const promisedUpdate = mPromisedUpdatedSuccessorTabId.get(tabId);
await Promise.all([
tab.$TST.opened,
promisedUpdate?.promisedSuccessorTabId,
]);
const renewedTab = await browser.tabs.get(tabId).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
if (!renewedTab ||
!TabsStore.ensureLivingItem(tab))
return;
log('updateInternal: ', dumpTab(tab), {
tabSuccessorTabId: tab.successorTabId,
renewedSuccessorTabId: renewedTab.successorTabId,
lastSuccessorTabIdByOwner: tab.$TST.temporaryMetadata.get('lastSuccessorTabIdByOwner'),
lastSuccessorTabId: tab.$TST.temporaryMetadata.get('lastSuccessorTabId'),
});
if (tab.$TST.temporaryMetadata.has('lastSuccessorTabIdByOwner')) {
log('respect last successor by owner');
const successor = Tab.get(renewedTab.successorTabId);
if (successor) {
log(` ${dumpTab(tab)} is already prepared for "selectOwnerOnClose" behavior (successor=${renewedTab.successorTabId})`);
return;
}
log(` clear successor of ${dumpTab(tab)}`);
tab.$TST.temporaryMetadata.delete('lastSuccessorTabIdByOwner');
tab.$TST.temporaryMetadata.delete('lastSuccessorTabId');
clearSuccessor(tab.id);
}
const lastSuccessorTab = tab.$TST.temporaryMetadata.has('lastSuccessorTabId') && Tab.get(tab.$TST.temporaryMetadata.get('lastSuccessorTabId'));
if (!lastSuccessorTab) {
log(` ${dumpTab(tab)}'s successor is missing: it was already closed.`);
}
else {
log(` ${dumpTab(tab)} is under control: `, {
successorTabId: renewedTab.successorTabId,
lastSuccessorTabId: tab.$TST.temporaryMetadata.get('lastSuccessorTabId'),
});
if (renewedTab.successorTabId != -1 &&
renewedTab.successorTabId != tab.$TST.temporaryMetadata.get('lastSuccessorTabId')) {
log(` ${dumpTab(tab)}'s successor is modified by someone! Now it is out of control.`);
tab.$TST.temporaryMetadata.delete('lastSuccessorTabId');
return;
}
}
tab.$TST.temporaryMetadata.delete('lastSuccessorTabId');
if (configs.successorTabControlLevel == Constants.kSUCCESSOR_TAB_CONTROL_NEVER)
return;
let successor = null;
if (renewedTab.active) {
log('it is active, so reset successor');
const excludeTabIdsSet = new Set(excludeTabIds);
const findSuccessor = (...candidates) => {
for (const candidate of candidates) {
if (!excludeTabIdsSet.has(candidate?.id) &&
candidate)
return candidate;
}
return null;
};
if (configs.successorTabControlLevel == Constants.kSUCCESSOR_TAB_CONTROL_IN_TREE) {
const closeParentBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
parent: tab.$TST.parent,
windowId: tab.windowId,
});
const collapsedChildSuccessorAllowed = (
closeParentBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE &&
closeParentBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB
);
const firstChild = collapsedChildSuccessorAllowed ? tab.$TST.firstChild : tab.$TST.firstVisibleChild;
const nextVisibleSibling = tab.$TST.nextVisibleSiblingTab;
const nearestVisiblePreceding = tab.$TST.nearestVisiblePrecedingTab;
successor = findSuccessor(
firstChild,
nextVisibleSibling,
nearestVisiblePreceding
);
log(` possible successor: ${dumpTab(tab)}: `, successor, {
closeParentBehavior,
collapsedChildSuccessorAllowed,
parent: tab.$TST.parentId,
firstChild: firstChild?.id,
nextVisibleSibling: nextVisibleSibling?.id,
nearestVisiblePreceding: nearestVisiblePreceding?.id,
});
if (successor &&
successor.discarded &&
configs.avoidDiscardedTabToBeActivatedIfPossible) {
log(` ${dumpTab(successor)} is discarded.`);
successor = findSuccessor(
tab.$TST.nearestLoadedSiblingTab,
tab.$TST.nearestLoadedTabInTree,
tab.$TST.nearestLoadedTab,
successor
);
log(` => redirected successor is: ${dumpTab(successor)}`);
}
}
else {
successor = findSuccessor(
tab.$TST.nearestVisibleFollowingTab,
tab.$TST.nearestVisiblePrecedingTab
);
log(` possible successor: ${dumpTab(tab)}`);
if (successor &&
successor.discarded &&
configs.avoidDiscardedTabToBeActivatedIfPossible) {
log(` ${dumpTab(successor)} is discarded.`);
successor = findSuccessor(
tab.$TST.nearestLoadedTab,
successor
);
log(` => redirected successor is: ${dumpTab(successor)}`);
}
}
}
if (successor) {
log(` ${dumpTab(tab)} is under control: successor = ${successor.id}`);
setSuccessor(renewedTab.id, successor.id);
tab.$TST.temporaryMetadata.set('lastSuccessorTabId', successor.id);
}
else {
log(` ${dumpTab(tab)} is out of control.`, {
active: renewedTab.active,
});
clearSuccessor(renewedTab.id);
}
}
async function tryClearOwnerSuccessor(tab) {
if (!tab ||
!tab.$TST.temporaryMetadata.get('lastSuccessorTabIdByOwner'))
return;
tab.$TST.temporaryMetadata.delete('lastSuccessorTabIdByOwner');
const renewedTab = await browser.tabs.get(tab.id).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
if (!renewedTab ||
renewedTab.successorTabId != tab.$TST.temporaryMetadata.get('lastSuccessorTabId'))
return;
log(`${dumpTab(tab)} is unprepared for "selectOwnerOnClose" behavior`);
tab.$TST.temporaryMetadata.delete('lastSuccessorTabId');
clearSuccessor(tab.id);
}
Tab.onActivated.addListener(async (newActiveTab, info = {}) => {
update(newActiveTab.id);
if (info.previousTabId) {
const oldActiveTab = Tab.get(info.previousTabId);
if (oldActiveTab) {
await tryClearOwnerSuccessor(oldActiveTab);
const lastRelatedTab = oldActiveTab.$TST.lastRelatedTab;
const newRelatedTabsCount = oldActiveTab.$TST.newRelatedTabsCount;
if (lastRelatedTab) {
log(`clear lastRelatedTabs for the window ${info.windowId} by tabs.onActivated on ${newActiveTab.id}`);
TabsStore.windows.get(info.windowId).clearLastRelatedTabs();
if (lastRelatedTab.id != newActiveTab.id) {
log(`non last-related-tab is activated: cancel "back to owner" behavior for ${lastRelatedTab.id}`);
await tryClearOwnerSuccessor(lastRelatedTab);
}
}
if (newRelatedTabsCount > 1) {
log(`multiple related tabs were opened: cancel "back to owner" behavior for ${newActiveTab.id}`);
await tryClearOwnerSuccessor(newActiveTab);
}
}
update(info.previousTabId);
}
});
Tab.onCreating.addListener((tab, info = {}) => {
if (!info.activeTab)
return;
const shouldControlSuccesor = (
configs.successorTabControlLevel != Constants.kSUCCESSOR_TAB_CONTROL_NEVER &&
configs.simulateSelectOwnerOnClose
);
log(`Tab.onCreating: should control succesor of ${tab.id}: `, shouldControlSuccesor);
if (shouldControlSuccesor) {
// don't use await here, to prevent that other onCreating handlers are treated async.
tryClearOwnerSuccessor(info.activeTab).then(() => {
const ownerTabId = tab.openerTabId || tab.active ? info.activeTab.id : null
if (!ownerTabId)
return;
log(`${dumpTab(tab)} is prepared for "selectOwnerOnClose" behavior (successor=${ownerTabId})`);
setSuccessor(tab.id, ownerTabId);
tab.$TST.temporaryMetadata.set('lastSuccessorTabId', ownerTabId);
tab.$TST.temporaryMetadata.set('lastSuccessorTabIdByOwner', true);
if (!tab.openerTabId)
return;
const opener = Tab.get(tab.openerTabId);
const lastRelatedTab = opener?.$TST.lastRelatedTab;
log(`opener ${dumpTab(opener)}'s lastRelatedTab: ${dumpTab(lastRelatedTab)})`);
if (lastRelatedTab) {
log(' => clear successor');
tryClearOwnerSuccessor(lastRelatedTab);
}
opener.$TST.lastRelatedTab = tab;
});
}
else {
const opener = Tab.get(tab.openerTabId);
if (opener)
opener.$TST.lastRelatedTab = tab;
}
});
function updateActiveTab(windowId) {
const activeTab = Tab.getActiveTab(windowId);
if (activeTab)
update(activeTab.id);
}
Tab.onCreated.addListener((tab, _info = {}) => {
updateActiveTab(tab.windowId);
});
Tab.onRemoving.addListener((tab, removeInfo = {}) => {
if (removeInfo.isWindowClosing)
return;
const lastRelatedTab = tab.$TST.lastRelatedTab;
if (lastRelatedTab &&
!lastRelatedTab.active)
tryClearOwnerSuccessor(lastRelatedTab);
});
Tab.onRemoved.addListener((tab, info = {}) => {
updateActiveTab(info.windowId);
const win = TabsStore.windows.get(info.windowId);
if (!win)
return;
log(`clear lastRelatedTabs for ${info.windowId} by tabs.onRemoved`);
win.clearLastRelatedTabs();
});
Tab.onMoved.addListener((tab, info = {}) => {
updateActiveTab(tab.windowId);
if (!info.byInternalOperation) {
log(`clear lastRelatedTabs for ${tab.windowId} by tabs.onMoved`);
TabsStore.windows.get(info.windowId).clearLastRelatedTabs();
}
});
Tab.onAttached.addListener((_tab, info = {}) => {
updateActiveTab(info.newWindowId);
});
Tab.onDetached.addListener((_tab, info = {}) => {
updateActiveTab(info.oldWindowId);
const win = TabsStore.windows.get(info.oldWindowId);
if (win) {
log(`clear lastRelatedTabs for ${info.windowId} by tabs.onDetached`);
win.clearLastRelatedTabs();
}
});
Tab.onUpdated.addListener((tab, changeInfo = {}) => {
if (!('discarded' in changeInfo))
return;
updateActiveTab(tab.windowId);
});
Tab.onShown.addListener(tab => {
updateActiveTab(tab.windowId);
});
Tab.onHidden.addListener(tab => {
updateActiveTab(tab.windowId);
});
Tree.onAttached.addListener((child, { parent } = {}) => {
updateActiveTab(child.windowId);
const lastRelatedTabId = parent.$TST.lastRelatedTabId;
if (lastRelatedTabId &&
child.$TST.previousSiblingTab &&
lastRelatedTabId == child.$TST.previousSiblingTab.id)
parent.$TST.lastRelatedTab = child;
});
Tree.onDetached.addListener((child, _info = {}) => {
updateActiveTab(child.windowId);
});
Tree.onSubtreeCollapsedStateChanging.addListener((tab, _info = {}) => {
updateActiveTab(tab.windowId);
});
SidebarConnection.onConnected.addListener((windowId, _openCount) => {
updateActiveTab(windowId);
});
SidebarConnection.onDisconnected.addListener((windowId, _openCount) => {
updateActiveTab(windowId);
});
// for automated test
browser.runtime.onMessage.addListener((message, _sender) => {
if (!message || !message.type)
return;
switch (message.type) {
case Constants.kCOMMAND_WAIT_UNTIL_SUCCESSORS_UPDATED:
return Promise.all([...mInProgressUpdates]);
}
});

View File

@@ -0,0 +1,83 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
configs,
notify,
wait,
} from '/common/common.js';
import * as Constants from '/common/constants.js';
import * as ContextualIdentities from '/common/contextual-identities.js';
import * as Sync from '/common/sync.js';
import * as TabsStore from '/common/tabs-store.js';
import { Tab } from '/common/TreeItem.js';
import * as Tree from './tree.js';
Sync.onMessage.addListener(async message => {
const data = message.data;
if (data.type != Constants.kSYNC_DATA_TYPE_TABS ||
!Array.isArray(data.tabs))
return;
const multiple = data.tabs.length > 1 ? '_multiple' : '';
notify({
title: browser.i18n.getMessage(
`receiveTabs_notification_title${multiple}`,
[Sync.getDeviceName(message.from)]
),
message: browser.i18n.getMessage(
`receiveTabs_notification_message${multiple}`,
data.tabs.length > 1 ?
[data.tabs[0].url, data.tabs.length, data.tabs.length - 1] :
[data.tabs[0].url]
),
timeout: configs.syncReceivedTabsNotificationTimeout
});
const windowId = TabsStore.getCurrentWindowId() || (await browser.windows.getCurrent()).id;
const win = TabsStore.windows.get(windowId);
const initialIndex = win.tabs.size;
win.toBeOpenedOrphanTabs += data.tabs.length;
let index = 0;
const tabs = [];
for (const tab of data.tabs) {
const createParams = {
windowId,
url: tab.url,
index: initialIndex + index,
active: index == 0
};
if (tab.cookieStoreId &&
tab.cookieStoreId != 'firefox-default' &&
ContextualIdentities.get(tab.cookieStoreId))
createParams.cookieStoreId = tab.cookieStoreId;
let openedTab;
try {
openedTab = await browser.tabs.create(createParams);
}
catch(error) {
console.log(error);
}
if (!openedTab)
openedTab = await browser.tabs.create({
...createParams,
url: `about:blank?${tab.url}`
});
tabs.push(openedTab);
index++;
}
if (!Array.isArray(data.structure))
return;
await wait(100); // wait for all tabs are tracked
await Tree.applyTreeStructureToTabs(tabs.map(tab => Tab.get(tab.id)), data.structure, {
broadcast: true
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,662 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
configs,
wait,
countMatched,
dumpTab
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TabsUpdate from '/common/tabs-update.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import { Tab } from '/common/TreeItem.js';
import * as TabsMove from './tabs-move.js';
import * as TabsOpen from './tabs-open.js';
import * as Tree from './tree.js';
function log(...args) {
internalLogger('background/tabs-group', ...args);
}
export function makeGroupTabURI({ title, temporary, temporaryAggressive, openerTabId, aliasTabId, replacedParentCount } = {}) {
const url = new URL(Constants.kGROUP_TAB_URI);
if (title)
url.searchParams.set('title', title);
if (temporaryAggressive)
url.searchParams.set('temporaryAggressive', 'true');
else if (temporary)
url.searchParams.set('temporary', 'true');
if (openerTabId)
url.searchParams.set('openerTabId', openerTabId);
if (aliasTabId)
url.searchParams.set('aliasTabId', aliasTabId);
if (replacedParentCount)
url.searchParams.set('replacedParentCount', replacedParentCount);
return url.href;
}
export function temporaryStateParams(state) {
switch (state) {
case Constants.kGROUP_TAB_TEMPORARY_STATE_PASSIVE:
return {
temporary: true,
temporaryAggressive: false,
};
case Constants.kGROUP_TAB_TEMPORARY_STATE_AGGRESSIVE:
return {
temporary: false,
temporaryAggressive: true,
};
default:
break;
}
return {
temporary: false,
temporaryAggressive: false,
};
}
export async function groupTabs(tabs, { broadcast, parent, withDescendants, ...groupTabOptions } = {}) {
const rootTabs = Tab.collectRootTabs(tabs);
if (rootTabs.length <= 0)
return null;
log('groupTabs: ', () => tabs.map(dumpTab), { broadcast, parent, withDescendants });
const uri = makeGroupTabURI({
title: browser.i18n.getMessage('groupTab_label', rootTabs[0].title),
temporary: true,
...groupTabOptions
});
const groupTab = await TabsOpen.openURIInTab(uri, {
windowId: rootTabs[0].windowId,
parent: parent || rootTabs[0].$TST.parent,
insertBefore: rootTabs[0],
inBackground: true
});
if (!withDescendants) {
const structure = TreeBehavior.getTreeStructureFromTabs(tabs);
await Tree.detachTabsFromTree(tabs, {
broadcast: !!broadcast,
});
log('structure: ', structure);
await Tree.applyTreeStructureToTabs(tabs, structure, {
broadcast: !!broadcast,
});
}
await TabsMove.moveTabsAfter(tabs.slice(1), tabs[0], {
broadcast: !!broadcast
});
for (const tab of rootTabs) {
await Tree.attachTabTo(tab, groupTab, {
forceExpand: true, // this is required to avoid the group tab itself is active from active tab in collapsed tree
dontMove: true,
broadcast: !!broadcast,
});
}
return groupTab;
}
function reserveToCleanupNeedlessGroupTab(tabOrTabs) {
const tabs = Array.isArray(tabOrTabs) ? tabOrTabs : [tabOrTabs] ;
for (const tab of tabs) {
if (!TabsStore.ensureLivingItem(tab))
continue;
if (tab.$TST.temporaryMetadata.has('reservedCleanupNeedlessGroupTab'))
clearTimeout(tab.$TST.temporaryMetadata.get('reservedCleanupNeedlessGroupTab'));
tab.$TST.temporaryMetadata.set('reservedCleanupNeedlessGroupTab', setTimeout(() => {
if (!tab.$TST)
return;
tab.$TST.temporaryMetadata.delete('reservedCleanupNeedlessGroupTab');
cleanupNeedlssGroupTab(tab);
}, 100));
}
}
function cleanupNeedlssGroupTab(tabs) {
if (!Array.isArray(tabs))
tabs = [tabs];
log('trying to clanup needless temporary group tabs from ', () => tabs.map(dumpTab));
const tabsToBeRemoved = [];
for (const tab of tabs) {
if (tab.$TST.temporaryMetadata.has('movingAcrossWindows'))
continue;
if (tab.$TST.isTemporaryGroupTab) {
if (tab.$TST.childIds.length > 1)
break;
const lastChild = tab.$TST.firstChild;
if (lastChild &&
!lastChild.$TST.isTemporaryGroupTab &&
!lastChild.$TST.isTemporaryAggressiveGroupTab)
break;
}
else if (tab.$TST.isTemporaryAggressiveGroupTab) {
if (tab.$TST.childIds.length > 1)
break;
}
else {
break;
}
tabsToBeRemoved.push(tab);
}
log('=> to be removed: ', () => tabsToBeRemoved.map(dumpTab));
TabsInternalOperation.removeTabs(tabsToBeRemoved, { keepDescendants: true });
}
export async function tryReplaceTabWithGroup(tab, { windowId, parent, children, insertBefore, newParent } = {}) {
if (tab) {
windowId = tab.windowId;
parent = tab.$TST.parent;
children = tab.$TST.children;
insertBefore = insertBefore || tab.$TST.unsafeNextTab;
}
if (children.length <= 1 ||
countMatched(children,
tab => !tab.$TST.states.has(Constants.kTAB_STATE_TO_BE_REMOVED)) <= 1)
return null;
log('trying to replace the closing tab with a new group tab');
const firstChild = children[0];
const uri = makeGroupTabURI({
title: browser.i18n.getMessage('groupTab_label', firstChild.title),
...temporaryStateParams(configs.groupTabTemporaryStateForOrphanedTabs),
replacedParentCount: (tab?.$TST?.replacedParentGroupTabCount || 0) + 1,
});
const win = TabsStore.windows.get(windowId);
win.toBeOpenedTabsWithPositions++;
const groupTab = await TabsOpen.openURIInTab(uri, {
windowId,
insertBefore,
inBackground: true
});
log('group tab: ', dumpTab(groupTab));
if (!groupTab) // the window is closed!
return;
if (newParent || parent)
await Tree.attachTabTo(groupTab, newParent || parent, {
dontMove: true,
broadcast: true
});
for (const child of children) {
await Tree.attachTabTo(child, groupTab, {
dontMove: true,
broadcast: true
});
}
// This can be triggered on closing of multiple tabs,
// so we should cleanup it on such cases for safety.
// https://github.com/piroor/treestyletab/issues/2317
wait(1000).then(() => reserveToCleanupNeedlessGroupTab(groupTab));
return groupTab;
}
// ====================================================================
// init/update group tabs
// ====================================================================
/*
To prevent the tab is closed by Firefox, we need to inject scripts dynamically.
See also: https://github.com/piroor/treestyletab/issues/1670#issuecomment-350964087
*/
async function tryInitGroupTab(tab) {
if (!tab.$TST.isGroupTab &&
!tab.$TST.hasGroupTabURL)
return;
log('tryInitGroupTab ', tab);
const v3Options = {
target: { tabId: tab.id },
};
const v2Options = {
runAt: 'document_start',
matchAboutBlank: true
};
try {
const getPageState = function getPageState() {
return [window.prepared, document.documentElement.matches('.initialized')];
};
const [prepared, initialized, reloaded] = (browser.scripting ?
browser.scripting.executeScript({ // Manifest V3
...v3Options,
func: getPageState,
}).then(results => results && results[0] && results[0].result || []) :
browser.tabs.executeScript(tab.id, {
...v2Options,
code: `(${getPageState.toString()})()`,
}).then(results => results && results[0] || [])
).catch(error => {
if (ApiTabs.isMissingHostPermissionError(error) &&
tab.$TST.hasGroupTabURL) {
log(' tryInitGroupTab: failed to run script for restored/discarded tab, reload the tab for safety ', tab.id);
browser.tabs.reload(tab.id);
return [[false, false, true]];
}
return ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError)(error);
});
log(' tryInitGroupTab: groupt tab state ', tab.id, { prepared, initialized, reloaded });
if (reloaded) {
log(' => reloaded ', tab.id);
return;
}
if (prepared && initialized) {
log(' => already initialized ', tab.id);
return;
}
}
catch(error) {
log(' tryInitGroupTab: error while checking initialized: ', tab.id, error);
}
try {
const getTitleExistence = function getState() {
return !!document.querySelector('#title');
};
const titleElementExists = (browser.scripting ?
browser.scripting.executeScript({ // Manifest V3
...v3Options,
func: getTitleExistence,
}).then(results => results && results[0] && results[0].result) :
browser.tabs.executeScript(tab.id, {
...v2Options,
code: `(${getTitleExistence.toString()})()`,
}).then(results => results && results[0])
).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError));
if (!titleElementExists && tab.status == 'complete') { // we need to load resources/group-tab.html at first.
log(' => title element exists, load again ', tab.id);
return browser.tabs.update(tab.id, { url: tab.url }).catch(ApiTabs.createErrorSuppressor());
}
}
catch(error) {
log(' tryInitGroupTab error while checking title element: ', tab.id, error);
}
(browser.scripting ?
browser.scripting.executeScript({ // Manifest V3
...v3Options,
files: [
'/extlib/l10n-classic.js', // ES module does not supported as a content script...
'/resources/group-tab.js',
],
}) :
Promise.all([
browser.tabs.executeScript(tab.id, {
...v2Options,
//file: '/common/l10n.js'
file: '/extlib/l10n-classic.js', // ES module does not supported as a content script...
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError)),
browser.tabs.executeScript(tab.id, {
...v2Options,
file: '/resources/group-tab.js',
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError)),
])
).then(() => {
log('tryInitGroupTab completely initialized: ', tab.id);
});
if (tab.$TST.states.has(Constants.kTAB_STATE_UNREAD)) {
tab.$TST.removeState(Constants.kTAB_STATE_UNREAD, { permanently: true });
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TAB_UPDATED,
windowId: tab.windowId,
tabId: tab.id,
removedStates: [Constants.kTAB_STATE_UNREAD]
});
}
}
function reserveToUpdateRelatedGroupTabs(tab, changedInfo) {
const tabMetadata = tab.$TST.temporaryMetadata;
const updatingTabs = tabMetadata.get('reserveToUpdateRelatedGroupTabsUpdatingTabs') || new Set();
if (!tabMetadata.has('reserveToUpdateRelatedGroupTabsUpdatingTabs'))
tabMetadata.set('reserveToUpdateRelatedGroupTabsUpdatingTabs', updatingTabs);
const ancestorGroupTabs = [
tab,
tab.$TST.bundledTab,
...tab.$TST.ancestors,
...tab.$TST.ancestors.map(tab => tab.$TST.bundledTab),
].filter(tab => tab?.$TST.isGroupTab);
for (const updatingTab of ancestorGroupTabs) {
const updatingMetadata = updatingTab.$TST.temporaryMetadata;
const reservedChangedInfo = updatingMetadata.get('reservedUpdateRelatedGroupTabChangedInfo') || new Set();
for (const info of changedInfo) {
reservedChangedInfo.add(info);
}
if (updatingTabs.has(updatingTab.id))
continue;
updatingTabs.add(updatingTab.id);
const triggeredUpdates = updatingMetadata.get('reservedUpdateRelatedGroupTabTriggeredUpdates') || new Set();
triggeredUpdates.add(updatingTabs);
updatingMetadata.set('reservedUpdateRelatedGroupTabTriggeredUpdates', triggeredUpdates);
if (updatingMetadata.has('reservedUpdateRelatedGroupTab'))
clearTimeout(updatingMetadata.get('reservedUpdateRelatedGroupTab'));
updatingMetadata.set('reservedUpdateRelatedGroupTabChangedInfo', reservedChangedInfo);
updatingMetadata.set('reservedUpdateRelatedGroupTab', setTimeout(() => {
updatingMetadata.delete('reservedUpdateRelatedGroupTab');
if (updatingTab.$TST) {
try {
if (reservedChangedInfo.size > 0)
updateRelatedGroupTab(updatingTab, [...reservedChangedInfo]);
}
catch(_error) {
}
updatingMetadata.delete('reservedUpdateRelatedGroupTabChangedInfo');
}
setTimeout(() => {
const triggerUpdates = updatingMetadata.get('reservedUpdateRelatedGroupTabTriggeredUpdates')
updatingMetadata.delete('reservedUpdateRelatedGroupTabTriggeredUpdates');
if (!triggerUpdates)
return;
for (const updatingTabs of triggerUpdates) {
updatingTabs.delete(updatingTab.id);
}
}, 100)
}, 100));
}
}
async function updateRelatedGroupTab(groupTab, changedInfo = []) {
if (!TabsStore.ensureLivingItem(groupTab))
return;
await tryInitGroupTab(groupTab);
if (changedInfo.includes('tree')) {
try {
await browser.tabs.sendMessage(groupTab.id, {
type: 'ws:update-tree',
}).catch(error => {
if (ApiTabs.isMissingHostPermissionError(error))
throw error;
return ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError, ApiTabs.handleUnloadedError)(error);
});
}
catch(error) {
if (ApiTabs.isMissingHostPermissionError(error)) {
log(' updateRelatedGroupTab: failed to run script for restored/discarded tab, reload the tab for safety ', groupTab.id);
browser.tabs.reload(groupTab.id);
return;
}
}
}
const firstChild = groupTab.$TST.firstChild;
if (!firstChild) // the tab can be closed while waiting...
return;
if (changedInfo.includes('title')) {
let newTitle;
if (Constants.kGROUP_TAB_DEFAULT_TITLE_MATCHER.test(groupTab.title)) {
newTitle = browser.i18n.getMessage('groupTab_label', firstChild.title);
}
else if (Constants.kGROUP_TAB_FROM_PINNED_DEFAULT_TITLE_MATCHER.test(groupTab.title)) {
const opener = groupTab.$TST.openerTab;
if (opener) {
if (opener &&
opener.favIconUrl) {
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TAB_FAVICON_UPDATED,
windowId: groupTab.windowId,
tabId: groupTab.id,
favIconUrl: opener.favIconUrl
});
}
newTitle = browser.i18n.getMessage('groupTab_fromPinnedTab_label', opener.title);
}
}
if (newTitle && groupTab.title != newTitle) {
browser.tabs.sendMessage(groupTab.id, {
type: 'ws:update-title',
title: newTitle,
}).catch(ApiTabs.createErrorHandler(
ApiTabs.handleMissingTabError,
ApiTabs.handleMissingHostPermissionError,
_error => {
// failed to update the title by group tab itself, so we try to update it from outside
groupTab.title = newTitle;
TabsUpdate.updateTab(groupTab, { title: newTitle });
}
));
}
}
}
Tab.onRemoved.addListener((tab, _closeInfo = {}) => {
const ancestors = tab.$TST.ancestors;
wait(0).then(() => {
reserveToCleanupNeedlessGroupTab(ancestors);
});
});
Tab.onUpdated.addListener((tab, changeInfo) => {
if ('url' in changeInfo ||
'previousUrl' in changeInfo ||
'state' in changeInfo) {
const status = changeInfo.status || tab?.status;
const url = changeInfo.url ? changeInfo.url :
status == 'complete' && tab ? tab.url : '';
if (tab &&
status == 'complete') {
if (url.indexOf(Constants.kGROUP_TAB_URI) == 0) {
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
}
else if (!Constants.kSHORTHAND_ABOUT_URI.test(url)) {
tab.$TST.getPermanentStates().then(async (states) => {
if (url.indexOf(Constants.kGROUP_TAB_URI) == 0)
return;
// Detect group tab from different session - which can have different UUID for the URL.
const PREFIX_REMOVER = /^moz-extension:\/\/[^\/]+/;
const pathPart = url.replace(PREFIX_REMOVER, '');
if (states.includes(Constants.kTAB_STATE_GROUP_TAB) &&
pathPart.split('?')[0] == Constants.kGROUP_TAB_URI.replace(PREFIX_REMOVER, '')) {
const parameters = pathPart.replace(/^[^\?]+\?/, '');
const oldUrl = tab.url;
await wait(100); // for safety
if (tab.url != oldUrl)
return;
browser.tabs.update(tab.id, {
url: `${Constants.kGROUP_TAB_URI}?${parameters}`
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB);
}
else {
tab.$TST.removeState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
}
});
}
}
// restored tab can be replaced with blank tab. we need to restore it manually.
else if (changeInfo.url == 'about:blank' &&
changeInfo.previousUrl &&
changeInfo.previousUrl.indexOf(Constants.kGROUP_TAB_URI) == 0) {
const oldUrl = tab.url;
wait(100).then(() => { // redirect with delay to avoid infinite loop of recursive redirections.
if (tab.url != oldUrl)
return;
browser.tabs.update(tab.id, {
url: changeInfo.previousUrl
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
});
}
if (changeInfo.status ||
changeInfo.url ||
url.indexOf(Constants.kGROUP_TAB_URI) == 0)
tryInitGroupTab(tab);
}
if ('title' in changeInfo) {
const group = Tab.getGroupTabForOpener(tab);
if (group)
reserveToUpdateRelatedGroupTabs(group, ['title', 'tree']);
}
});
Tab.onGroupTabDetected.addListener(tab => {
tryInitGroupTab(tab);
});
Tab.onLabelUpdated.addListener(tab => {
reserveToUpdateRelatedGroupTabs(tab, ['title', 'tree']);
});
Tab.onActivating.addListener((tab, _info = {}) => {
tryInitGroupTab(tab);
});
// returns a boolean: need to reload or not.
export async function clearTemporaryState(tab) {
if (!tab.$TST.isTemporaryGroupTab &&
!tab.$TST.isTemporaryAggressiveGroupTab)
return;
const url = new URL(tab.url);
url.searchParams.delete('temporary');
url.searchParams.delete('temporaryAggressive');
await Promise.all([
browser.tabs.sendMessage(tab.id, {
type: 'ws:clear-temporary-state',
}).catch(ApiTabs.createErrorHandler()),
browser.tabs.executeScript(tab.id, { // failsafe
runAt: 'document_start',
code: `history.replaceState({}, document.title, ${JSON.stringify(url.href)});`,
}).catch(ApiTabs.createErrorHandler()),
]);
tab.url = url.href;
}
Tab.onPinned.addListener(async tab => {
log('handlePinnedParentTab ', tab);
await Tree.collapseExpandSubtree(tab, {
collapsed: false,
broadcast: true
});
log(' childIdsBeforeMoved: ', tab.$TST.temporaryMetadata.get('childIdsBeforeMoved'));
log(' parentIdBeforeMoved: ', tab.$TST.temporaryMetadata.get('parentIdBeforeMoved'));
const children = (
tab.$TST.temporaryMetadata.has('childIdsBeforeMoved') ?
tab.$TST.temporaryMetadata.get('childIdsBeforeMoved').map(id => Tab.get(id)) :
tab.$TST.children
).filter(tab => TabsStore.ensureLivingItem(tab));
const parent = TabsStore.ensureLivingItem(
tab.$TST.temporaryMetadata.has('parentIdBeforeMoved') ?
Tab.get(tab.$TST.temporaryMetadata.get('parentIdBeforeMoved')) :
tab.$TST.parent
);
let openedGroupTab;
const shouldGroupChildren = configs.autoGroupNewTabsFromPinned || tab.$TST.isGroupTab;
if (shouldGroupChildren) {
log(' => trying to group left tabs with a group: ', children);
openedGroupTab = await groupTabs(children, {
// If the tab is a group tab, the opened tab should be treated as an alias of the pinned group tab.
// Otherwise it should be treated just as a temporary group tab to group children.
title: tab.$TST.isGroupTab ? tab.title : browser.i18n.getMessage('groupTab_fromPinnedTab_label', tab.title),
temporary: !tab.$TST.isGroupTab,
openerTabId: tab.$TST.uniqueId.id,
parent,
withDescendants: true,
});
log(' openedGroupTab: ', openedGroupTab);
// Tree structure of left tabs can be modified by someone like tryFixupTreeForInsertedTab@handle-moved-tabs.js.
// On such cases we need to restore the original tree structure.
const modifiedChildren = children.filter(child => children.includes(child.$TST.parent));
log(' modifiedChildren: ', modifiedChildren);
if (modifiedChildren.length > 0) {
for (const child of modifiedChildren) {
await Tree.detachTab(child, {
broadcast: true,
});
await Tree.attachTabTo(child, openedGroupTab, {
dontMove: true,
broadcast: true,
});
}
}
}
else {
log(' => no need to group left tabs, just detaching');
await Tree.detachAllChildren(tab, {
behavior: TreeBehavior.getParentTabOperationBehavior(tab, {
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
preventEntireTreeBehavior: true,
}),
broadcast: true
});
}
await Tree.detachTab(tab, {
broadcast: true
});
// Such a group tab will be closed automatically when all children are detached.
// To prevent the auto close behavior, the tab type need to be turned to permanent.
await clearTemporaryState(tab);
if (tab.$TST.isGroupTab && openedGroupTab) {
const url = new URL(tab.url);
url.searchParams.set('aliasTabId', openedGroupTab.$TST.uniqueId.id);
await Promise.all([
browser.tabs.sendMessage(tab.id, {
type: 'ws:replace-state-url',
url: url.href,
}).catch(ApiTabs.createErrorHandler()),
browser.tabs.executeScript(tab.id, { // failsafe
runAt: 'document_start',
code: `history.replaceState({}, document.title, ${JSON.stringify(url.href)});`,
}).catch(ApiTabs.createErrorHandler()),
]);
await browser.tabs.sendMessage(tab.id, {
type: 'ws:update-tree',
url: url.href,
}).catch(ApiTabs.createErrorHandler());
tab.url = url.href;
}
});
Tree.onAttached.addListener((tab, _info = {}) => {
reserveToUpdateRelatedGroupTabs(tab, ['tree']);
});
Tree.onDetached.addListener((_tab, detachInfo) => {
if (!detachInfo.oldParentTab)
return;
if (detachInfo.oldParentTab.$TST.isGroupTab)
reserveToCleanupNeedlessGroupTab(detachInfo.oldParentTab);
reserveToUpdateRelatedGroupTabs(detachInfo.oldParentTab, ['tree']);
});
/*
Tree.onSubtreeCollapsedStateChanging.addListener((tab, _info) => {
reserveToUpdateRelatedGroupTabs(tab);
});
*/

View File

@@ -0,0 +1,465 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Tree Style Tab.
*
* The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
* Portions created by the Initial Developer are Copyright (C) 2011-2025
* the Initial Developer. All Rights Reserved.
*
* Contributor(s): YUKI "Piro" Hiroshi <piro.outsider.reflex@gmail.com>
* wanabe <https://github.com/wanabe>
* Tetsuharu OHZEKI <https://github.com/saneyuki>
* Xidorn Quan <https://github.com/upsuper> (Firefox 40+ support)
* lv7777 (https://github.com/lv7777)
*
* ***** END LICENSE BLOCK ******/
'use strict';
import {
log as internalLogger,
wait,
toLines,
configs
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import { SequenceMatcher } from '/extlib/diff.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as TabsStore from '/common/tabs-store.js';
import { Tab, TreeItem } from '/common/TreeItem.js';
function log(...args) {
internalLogger('background/tabs-move', ...args);
}
function logApiTabs(...args) {
internalLogger('common/api-tabs', ...args);
}
// ========================================================
// primitive methods for internal use
export async function moveTabsBefore(tabs, referenceTab, options = {}) {
log('moveTabsBefore: ', tabs, referenceTab, options);
if (!tabs.length ||
!TabsStore.ensureLivingItem(referenceTab))
return [];
if (referenceTab.$TST.isAllPlacedBeforeSelf(tabs)) {
log('moveTabsBefore:no need to move');
return [];
}
return moveTabsInternallyBefore(tabs, referenceTab, options);
}
export async function moveTabBefore(tab, referenceTab, options = {}) {
return moveTabsBefore([tab], referenceTab, options).then(moved => moved.length > 0);
}
async function moveTabsInternallyBefore(tabs, referenceTab, options = {}) {
if (!tabs.length ||
!TabsStore.ensureLivingItem(referenceTab))
return [];
const win = TabsStore.windows.get(tabs[0].windowId);
log('moveTabsInternallyBefore: ', tabs, `${referenceTab.id}(index=${referenceTab.index})`, options);
if (referenceTab.type == TreeItem.TYPE_GROUP) {
referenceTab = referenceTab.$TST?.firstMember;
if (!TabsStore.ensureLivingItem(referenceTab)) {
log('missing reference tab');
return [];
}
log(` => reference tab: ${referenceTab.id}(index=${referenceTab.index})`);
}
const precedingReferenceTab = referenceTab.$TST.previousTab;
if (referenceTab.pinned) {
// unpinned tab cannot be moved before any pinned tab
tabs = tabs.filter(tab => tab.pinned);
}
else if (precedingReferenceTab &&
!precedingReferenceTab.pinned) {
// pinned tab cannot be moved after any unpinned tab
tabs = tabs.filter(tab => !tab.pinned);
}
if (!tabs.length)
return [];
const movedTabs = [];
try {
/*
Tab elements are moved by tabs.onMoved automatically, but
the operation is asynchronous. To help synchronous operations
following to this operation, we need to move tabs immediately.
*/
const tabGroups = new Set();
for (const tab of tabs) {
const oldPreviousTab = tab.$TST.unsafePreviousTab;
const oldNextTab = tab.$TST.unsafeNextTab;
if (oldNextTab?.id == referenceTab.id) // no move case
continue;
const fromIndex = tab.index;
if (referenceTab.index > tab.index)
tab.index = referenceTab.index - 1;
else
tab.index = referenceTab.index;
tabGroups.add(tab.$TST.nativeTabGroup);
if (SidebarConnection.isInitialized()) { // only on the background page
win.internalMovingTabs.set(tab.id, tab.index);
win.alreadyMovedTabs.set(tab.id, tab.index);
}
tab.reindexedBy = `moveTabsInternallyBefore (${tab.index})`;
Tab.track(tab);
movedTabs.push(tab);
Tab.onTabInternallyMoved.dispatch(tab, {
nextTab: referenceTab,
oldPreviousTab,
oldNextTab,
broadcasted: !!options.broadcasted
});
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TAB_INTERNALLY_MOVED,
windowId: tab.windowId,
tabId: tab.id,
fromIndex,
toIndex: tab.index,
nextTabId: referenceTab?.id,
broadcasted: !!options.broadcasted
});
if (options.doNotOptimize) {
win.internalMovingTabs.set(tab.id, tab.index);
win.alreadyMovedTabs.set(tab.id, tab.index);
await browser.tabs.move(tab.id, { index: tab.index });
win.internalMovingTabs.delete(tab.id);
win.alreadyMovedTabs.delete(tab.id);
}
}
for (const group of tabGroups) {
group?.$TST.reindex();
}
if (movedTabs.length == 0) {
log(' => actually nothing moved');
}
else {
log(
'Tab nodes rearranged by moveTabsInternallyBefore:\n',
(!configs.debug ? '' :
() => toLines(Array.from(win.getOrderedTabs()),
tab => ` - ${tab.index}: ${tab.id}${tabs.includes(tab) ? '[MOVED]' : ''}`))
);
}
if (SidebarConnection.isInitialized()) { // only on the background page
if (options.delayedMove) { // Wait until opening animation is finished.
await wait(configs.newTabAnimationDuration);
}
if (!options.doNotOptimize) {
syncToNativeTabs(tabs);
}
}
}
catch(e) {
ApiTabs.handleMissingTabError(e);
log('moveTabsInternallyBefore failed: ', String(e));
}
return movedTabs;
}
export async function moveTabInternallyBefore(tab, referenceTab, options = {}) {
return moveTabsInternallyBefore([tab], referenceTab, options);
}
export async function moveTabsAfter(tabs, referenceTab, options = {}) {
log('moveTabsAfter: ', tabs, referenceTab, options);
if (!tabs.length ||
!TabsStore.ensureLivingItem(referenceTab))
return [];
if (referenceTab.$TST.isAllPlacedAfterSelf(tabs)) {
log('moveTabsAfter:no need to move');
return [];
}
return moveTabsInternallyAfter(tabs, referenceTab, options);
}
export async function moveTabAfter(tab, referenceTab, options = {}) {
return moveTabsAfter([tab], referenceTab, options).then(moved => moved.length > 0);
}
async function moveTabsInternallyAfter(tabs, referenceTab, options = {}) {
if (!tabs.length ||
!TabsStore.ensureLivingItem(referenceTab))
return [];
const win = TabsStore.windows.get(tabs[0].windowId);
log('moveTabsInternallyAfter: ', tabs, `${referenceTab.id}(index=${referenceTab.index})`, options);
if (referenceTab.type == TreeItem.TYPE_GROUP) {
if (!referenceTab.collapsed) {
log(' => move before the first member tab of the reference group');
return moveTabsInternallyBefore(tabs, referenceTab.$TST?.firstMember, options = {});
}
referenceTab = referenceTab.$TST?.lastMember;
if (!TabsStore.ensureLivingItem(referenceTab)) {
log('missing reference tab');
return [];
}
log(` => reference tab: ${referenceTab.id}(index=${referenceTab.index})`);
}
const followingReferenceTab = referenceTab.$TST.nextTab;
if (followingReferenceTab &&
followingReferenceTab.pinned) {
// unpinned tab cannot be moved before any pinned tab
tabs = tabs.filter(tab => tab.pinned);
}
else if (!referenceTab.pinned) {
// pinned tab cannot be moved after any unpinned tab
tabs = tabs.filter(tab => !tab.pinned);
}
if (!tabs.length)
return [];
const movedTabs = [];
try {
/*
Tab elements are moved by tabs.onMoved automatically, but
the operation is asynchronous. To help synchronous operations
following to this operation, we need to move tabs immediately.
*/
let nextTab = referenceTab.$TST.unsafeNextTab;
while (nextTab && tabs.find(tab => tab.id == nextTab.id)) {
nextTab = nextTab.$TST.unsafeNextTab;
}
const tabGroups = new Set();
for (const tab of tabs) {
const oldPreviousTab = tab.$TST.unsafePreviousTab;
const oldNextTab = tab.$TST.unsafeNextTab;
if ((!oldNextTab && !nextTab) ||
(oldNextTab && nextTab && oldNextTab.id == nextTab.id)) // no move case
continue;
const fromIndex = tab.index;
if (nextTab) {
if (nextTab.index > tab.index)
tab.index = nextTab.index - 1;
else
tab.index = nextTab.index;
}
else {
tab.index = win.tabs.size - 1
}
tabGroups.add(tab.$TST.nativeTabGroup);
if (SidebarConnection.isInitialized()) { // only on the background page
win.internalMovingTabs.set(tab.id, tab.index);
win.alreadyMovedTabs.set(tab.id, tab.index);
}
tab.reindexedBy = `moveTabsInternallyAfter (${tab.index})`;
Tab.track(tab);
movedTabs.push(tab);
Tab.onTabInternallyMoved.dispatch(tab, {
nextTab,
oldPreviousTab,
oldNextTab,
broadcasted: !!options.broadcasted
});
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TAB_INTERNALLY_MOVED,
windowId: tab.windowId,
tabId: tab.id,
fromIndex,
toIndex: tab.index,
nextTabId: nextTab?.id,
broadcasted: !!options.broadcasted
});
if (options.doNotOptimize) {
win.internalMovingTabs.set(tab.id, tab.index);
win.alreadyMovedTabs.set(tab.id, tab.index);
await browser.tabs.move(tab.id, { index: tab.index });
win.internalMovingTabs.delete(tab.id);
win.alreadyMovedTabs.delete(tab.id);
}
}
for (const group of tabGroups) {
group?.$TST.reindex();
}
if (movedTabs.length == 0) {
log(' => actually nothing moved');
}
else {
log(
'Tab nodes rearranged by moveTabsInternallyAfter:\n',
(!configs.debug ? '' :
() => toLines(Array.from(win.getOrderedTabs()),
tab => ` - ${tab.index}: ${tab.id}${tabs.includes(tab) ? '[MOVED]' : ''}`))
);
}
if (SidebarConnection.isInitialized()) { // only on the background page
if (options.delayedMove) { // Wait until opening animation is finished.
await wait(configs.newTabAnimationDuration);
}
if (!options.doNotOptimize) {
syncToNativeTabs(tabs);
}
}
}
catch(e) {
ApiTabs.handleMissingTabError(e);
log('moveTabsInternallyAfter failed: ', String(e));
}
return movedTabs;
}
export async function moveTabInternallyAfter(tab, referenceTab, options = {}) {
return moveTabsInternallyAfter([tab], referenceTab, options);
}
// ========================================================
// Synchronize order of tab elements to browser's tabs
const mPreviousSync = new Map();
const mDelayedSync = new Map();
const mDelayedSyncTimer = new Map();
export async function waitUntilSynchronized(windowId) {
const previous = mPreviousSync.get(windowId);
if (previous)
return previous.then(() => waitUntilSynchronized(windowId));
return Promise.resolve(mDelayedSync.get(windowId)).then(() => {
const previous = mPreviousSync.get(windowId);
if (previous)
return waitUntilSynchronized(windowId);
});
}
function syncToNativeTabs(tabs) {
const windowId = tabs[0].windowId;
//log(`syncToNativeTabs(${windowId})`);
if (mDelayedSyncTimer.has(windowId))
clearTimeout(mDelayedSyncTimer.get(windowId));
const delayedSync = new Promise((resolve, _reject) => {
mDelayedSyncTimer.set(windowId, setTimeout(() => {
mDelayedSync.delete(windowId);
let previousSync = mPreviousSync.get(windowId);
if (previousSync)
previousSync = previousSync.then(() => syncToNativeTabsInternal(windowId));
else
previousSync = syncToNativeTabsInternal(windowId);
previousSync = previousSync.then(resolve);
mPreviousSync.set(windowId, previousSync);
}, 250));
}).then(() => {
mPreviousSync.delete(windowId);
});
mDelayedSync.set(windowId, delayedSync);
return delayedSync;
}
async function syncToNativeTabsInternal(windowId) {
mDelayedSyncTimer.delete(windowId);
if (Tab.needToWaitTracked(windowId))
await Tab.waitUntilTrackedAll(windowId);
if (Tab.needToWaitMoved(windowId))
await Tab.waitUntilMovedAll(windowId);
const win = TabsStore.windows.get(windowId);
if (!win) // already destroyed
return;
// Tabs may be removed while waiting.
const internalOrder = TabsStore.windows.get(windowId).order;
const nativeTabsOrder = (await browser.tabs.query({ windowId }).catch(ApiTabs.createErrorHandler())).map(tab => tab.id);
log(`syncToNativeTabs(${windowId}): rearrange `, { internalOrder:internalOrder.join(','), nativeTabsOrder:nativeTabsOrder.join(',') });
log(`syncToNativeTabs(${windowId}): step1, internalOrder => nativeTabsOrder`);
let tabIdsForUpdatedIndices = Array.from(nativeTabsOrder);
const moveOperations = (new SequenceMatcher(nativeTabsOrder, internalOrder)).operations();
const movedTabs = new Set();
for (const operation of moveOperations) {
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
log(`syncToNativeTabs(${windowId}): operation `, { tag, fromStart, fromEnd, toStart, toEnd });
switch (tag) {
case 'equal':
case 'delete':
break;
case 'insert':
case 'replace':
let moveTabIds = internalOrder.slice(toStart, toEnd);
const referenceId = nativeTabsOrder[fromStart] || null;
let toIndex = -1;
let fromIndices = moveTabIds.map(id => tabIdsForUpdatedIndices.indexOf(id));
if (referenceId) {
toIndex = tabIdsForUpdatedIndices.indexOf(referenceId);
}
if (toIndex < 0)
toIndex = internalOrder.length;
// ignore already removed tabs!
moveTabIds = moveTabIds.filter((id, index) => fromIndices[index] > -1);
if (moveTabIds.length == 0)
continue;
fromIndices = fromIndices.filter(index => index > -1);
const fromIndex = fromIndices[0];
if (fromIndex < toIndex)
toIndex--;
log(`syncToNativeTabs(${windowId}): step1, move ${moveTabIds.join(',')} before ${referenceId} / from = ${fromIndex}, to = ${toIndex}`);
for (const movedId of moveTabIds) {
win.internalMovingTabs.set(movedId, -1);
win.alreadyMovedTabs.set(movedId, -1);
movedTabs.add(movedId);
}
logApiTabs(`tabs-move:syncToNativeTabs(${windowId}): step1, browser.tabs.move() `, moveTabIds, {
windowId,
index: toIndex
});
let reallyMovedTabIds = new Set();
try {
const reallyMovedTabs = await browser.tabs.move(moveTabIds, {
windowId,
index: toIndex
}).catch(ApiTabs.createErrorHandler(e => {
log(`syncToNativeTabs(${windowId}): step1, failed to move: `, String(e), e.stack);
throw e;
}));
reallyMovedTabIds = new Set(reallyMovedTabs.map(tab => tab.id));
}
catch(error) {
console.error(error);
}
for (const id of moveTabIds) {
if (reallyMovedTabIds.has(id))
continue;
log(`syncToNativeTabs(${windowId}): failed to move tab ${id}: maybe unplacable position (regular tabs in pinned tabs/pinned tabs in regular tabs), or any other reason`);
win.internalMovingTabs.delete(id);
win.alreadyMovedTabs.delete(id);
}
tabIdsForUpdatedIndices = tabIdsForUpdatedIndices.filter(id => !moveTabIds.includes(id));
tabIdsForUpdatedIndices.splice(toIndex, 0, ...moveTabIds);
break;
}
}
log(`syncToNativeTabs(${windowId}): step1, rearrange completed.`);
if (movedTabs.size > 0) {
// tabs.onMoved produced by this operation can break the order of tabs
// in the sidebar, so we need to synchronize complete order of tabs after
// all.
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_SYNC_TABS_ORDER,
windowId
});
// Multiple times asynchronous tab move is unstable, so we retry again
// for safety until all tabs are completely synchronized.
syncToNativeTabs([{ windowId }]);
}
}

View File

@@ -0,0 +1,342 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Tree Style Tab.
*
* The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
* Portions created by the Initial Developer are Copyright (C) 2011-2024
* the Initial Developer. All Rights Reserved.
*
* Contributor(s): YUKI "Piro" Hiroshi <piro.outsider.reflex@gmail.com>
* wanabe <https://github.com/wanabe>
* Tetsuharu OHZEKI <https://github.com/saneyuki>
* Xidorn Quan <https://github.com/upsuper> (Firefox 40+ support)
* lv7777 (https://github.com/lv7777)
*
* ***** END LICENSE BLOCK ******/
'use strict';
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
log as internalLogger,
configs,
tryRevokeObjectURL,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as TabsStore from '/common/tabs-store.js';
import { Tab } from '/common/TreeItem.js';
import * as TabsMove from './tabs-move.js';
import * as Tree from './tree.js';
export const onForbiddenURLRequested = new EventListenerManager();
function log(...args) {
internalLogger('background/tabs-open', ...args);
}
const SEARCH_PREFIX_MATCHER = /^(ext\+ws:search:|about:ws-search\?)/;
export async function loadURI(uri, options = {}) {
if (!options.windowId && !options.tab)
throw new Error('missing loading target window or tab');
try {
let tabId;
if (options.tab) {
tabId = options.tab.id;
}
else {
let tabs = await browser.tabs.query({
windowId: options.windowId,
active: true
}).catch(ApiTabs.createErrorHandler());
if (tabs.length == 0)
tabs = await browser.tabs.query({
windowId: options.windowId,
}).catch(ApiTabs.createErrorHandler());
tabId = tabs[0].id;
}
let searchQuery = null;
if (SEARCH_PREFIX_MATCHER.test(uri)) {
const query = uri.replace(SEARCH_PREFIX_MATCHER, '');
if (browser.search &&
typeof browser.search.search == 'function')
searchQuery = query;
else
uri = configs.defaultSearchEngine.replace(/%s/gi, query);
}
if (searchQuery) {
await browser.search.search({
query: searchQuery,
tabId
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
}
else {
await browser.tabs.update(tabId, {
url: sanitizeURL(uri),
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
}
}
catch(e) {
ApiTabs.handleMissingTabError(e);
}
}
export async function openNewTab(options = {}) {
const win = TabsStore.windows.get(options.windowId);
win.toBeOpenedNewTabCommandTab++;
return openURIInTab(options.url || options.uri || null, options);
}
export async function openURIInTab(uri, options = {}) {
const tabs = await openURIsInTabs([uri], options);
return tabs[0];
}
const FORBIDDEN_URL_MATCHER = /^(about|chrome|resource|file):/;
const ALLOWED_URL_MATCHER = /^about:blank(\?|$)/;
export async function openURIsInTabs(uris, { windowId, insertBefore, insertAfter, cookieStoreId, isOrphan, active, inBackground, discarded, opener, parent, fixPositions } = {}) {
log('openURIsInTabs: ', { uris, windowId, insertBefore, insertAfter, cookieStoreId, isOrphan, active, inBackground, discarded, opener, parent, fixPositions });
if (!windowId)
throw new Error('missing loading target window\n' + new Error().stack);
const tabs = [];
// Don't return the result of Tab.doAndGetNewTabs because their order can
// be inverted due to browser.tabs.insertAfterCurrent=true
const actuallyOpenedTabIds = new Set(await Tab.doAndGetNewTabs(async () => {
await Tab.waitUntilTrackedAll(windowId);
await TabsMove.waitUntilSynchronized(windowId);
const startIndex = Tab.calculateNewTabIndex({ insertAfter, insertBefore });
log('startIndex: ', startIndex);
const win = TabsStore.windows.get(windowId);
if (insertBefore ||
insertAfter ||
uris.some(uri => uri && typeof uri == 'object' && 'index' in uri))
win.toBeOpenedTabsWithPositions += uris.length;
if (cookieStoreId)
win.toBeOpenedTabsWithCookieStoreId += uris.length;
if (isOrphan)
win.toBeOpenedOrphanTabs += uris.length;
return Promise.all(uris.map(async (uri, index) => {
const params = {
windowId,
active: index == 0 && (active || (inBackground === false)),
};
if (uri && typeof uri == 'object') { // tabs.create() compatible
if ('active' in uri)
params.active = uri.active;
if ('cookieStoreId' in uri)
params.cookieStoreId = uri.cookieStoreId;
if ('discarded' in uri)
params.discarded = uri.discarded;
if ('index' in uri)
params.index = uri.index;
if ('openerTabId' in uri)
params.openerTabId = uri.openerTabId;
if ('openInReaderMode' in uri)
params.openInReaderMode = uri.openInReaderMode;
if ('pinned' in uri)
params.pinned = uri.pinned;
if ('selected' in uri)
params.active = uri.selected;
if ('title' in uri)
params.title = uri.title;
uri = uri.uri || uri.url;
}
let searchQuery = null;
if (uri) {
if (SEARCH_PREFIX_MATCHER.test(uri)) {
const query = uri.replace(SEARCH_PREFIX_MATCHER, '');
if (browser.search &&
typeof browser.search.search == 'function')
searchQuery = query;
else
params.url = configs.defaultSearchEngine.replace(/%s/gi, query);
}
else {
params.url = uri;
}
}
if (discarded &&
!params.active &&
!('discarded' in params))
params.discarded = true;
if (params.url == 'about:newtab')
delete params.url
if (params.url)
params.url = sanitizeURL(params.url);
if (!('url' in params /* about:newtab case */) ||
/^about:/.test(params.url))
params.discarded = false; // discarded tab cannot be opened with any about: URL
if (!params.discarded) // title cannot be set for non-discarded tabs
params.title = null;
if (opener && !params.openerTabId)
params.openerTabId = opener.id;
if (startIndex > -1 && !('index' in params))
params.index = startIndex + index;
if (cookieStoreId && !params.cookieStoreId)
params.cookieStoreId = cookieStoreId;
// Tabs opened with different container can take time to be tracked,
// then TabsStore.waitUntilTabsAreCreated() may be resolved before it is
// tracked like as "the tab is already closed". So we wait until the
// tab is correctly tracked.
const promisedNewTabTracked = new Promise((resolve, reject) => {
const listener = (tab) => {
Tab.onCreating.removeListener(listener);
browser.tabs.get(tab.id)
.then(resolve)
.catch(ApiTabs.createErrorSuppressor(reject));
};
Tab.onCreating.addListener(listener);
});
const createdTab = await browser.tabs.create(params).catch(ApiTabs.createErrorHandler());
await Promise.all([
promisedNewTabTracked, // TabsStore.waitUntilTabsAreCreated(createdTab.id),
searchQuery && browser.search.search({
query: searchQuery,
tabId: createdTab.id
}).catch(ApiTabs.createErrorHandler())
]);
const tab = Tab.get(createdTab.id);
log('created tab: ', tab);
if (!tab)
throw new Error('tab is already closed');
if (!opener &&
parent &&
!isOrphan)
await Tree.attachTabTo(tab, parent, {
insertBefore: insertBefore,
insertAfter: insertAfter,
forceExpand: params.active,
broadcast: true
});
else if (insertBefore)
await TabsMove.moveTabInternallyBefore(tab, insertBefore, {
broadcast: true
});
else if (insertAfter)
await TabsMove.moveTabInternallyAfter(tab, insertAfter, {
broadcast: true
});
log('tab is opened.');
await tab.$TST.opened;
tabs.push(tab);
tryRevokeObjectURL(tab.url);
return tab;
}));
}, windowId));
const openedTabs = tabs.filter(tab => actuallyOpenedTabIds.has(tab));
if (fixPositions &&
openedTabs.every((tab, index) => (index == 0) || (openedTabs[index-1].index - tab.index) == 1)) {
// tabs are opened with reversed order due to browser.tabs.insertAfterCurrent=true
let lastTab;
for (const tab of openedTabs.slice(0).reverse()) {
if (lastTab)
TabsMove.moveTabInternallyBefore(tab, lastTab);
lastTab = tab;
}
await TabsMove.waitUntilSynchronized(windowId);
}
return openedTabs;
}
function sanitizeURL(url) {
if (ALLOWED_URL_MATCHER.test(url))
return url;
// tabs.create() doesn't accept about:reader URLs so we fallback them to regular URLs.
if (/^about:reader\?/.test(url))
return (new URL(url)).searchParams.get('url') || 'about:blank';
if (FORBIDDEN_URL_MATCHER.test(url)) {
onForbiddenURLRequested.dispatch(url);
return `about:blank?forbidden-url=${url}`;
}
return url;
}
export function isOpenable(url) {
return !url || url == sanitizeURL(url);
}
function onMessage(message, openerTab) {
switch (message.type) {
case Constants.kCOMMAND_LOAD_URI:
loadURI(message.uri, {
tab: Tab.get(message.tabId)
});
break;
case Constants.kCOMMAND_OPEN_TAB:
if (!message.parentId && openerTab)
message.parentId = openerTab.id;
if (!message.windowId && openerTab)
message.windowId = openerTab.windowId;
Tab.waitUntilTracked([
message.parentId,
message.insertBeforeId,
message.insertAfterId
]).then(() => {
openURIsInTabs(message.uris || [message.uri], {
windowId: message.windowId,
parent: Tab.get(message.parentId),
insertBefore: Tab.get(message.insertBeforeId),
insertAfter: Tab.get(message.insertAfterId),
active: !!message.active,
discarded: message.discarded,
});
});
break;
case Constants.kCOMMAND_NEW_TABS:
Tab.waitUntilTracked([
message.openerId,
message.parentId,
message.insertBeforeId,
message.insertAfterId
]).then(() => {
log('new tabs requested: ', message);
openURIsInTabs(message.uris, {
windowId: message.windowId,
opener: Tab.get(message.openerId),
parent: Tab.get(message.parentId),
insertBefore: Tab.get(message.insertBeforeId),
insertAfter: Tab.get(message.insertAfterId),
active: message.active,
discarded: message.discarded,
});
});
break;
}
}
SidebarConnection.onMessage.addListener((windowId, message) => {
onMessage(message);
});
browser.runtime.onMessage.addListener((message, sender) => {
if (!message ||
typeof message.type != 'string' ||
message.type.indexOf('ws:') != 0)
return;
onMessage(message, sender.tab);
});

View File

@@ -0,0 +1,691 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
log as internalLogger,
dumpTab,
toLines,
configs,
wait,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import * as UserOperationBlocker from '/common/user-operation-blocker.js';
import MetricsData from '/common/MetricsData.js';
import { Tab } from '/common/TreeItem.js';
import * as Commands from './commands.js';
import * as TabsMove from './tabs-move.js';
import * as TabsOpen from './tabs-open.js';
import * as Tree from './tree.js';
function log(...args) {
internalLogger('background/tree-structure', ...args);
}
export const onTabAttachedFromRestoredInfo = new EventListenerManager();
let mRecentlyClosedTabs = [];
let mRecentlyClosedTabsTreeStructure = [];
export function startTracking() {
Tab.onCreated.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
Tab.onRemoved.addListener((tab, info) => {
if (!info.isWindowClosing)
reserveToSaveTreeStructure(tab.windowId);
});
Tab.onMoved.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
Tab.onUpdated.addListener((tab, info) => {
if ('openerTabId' in info)
reserveToSaveTreeStructure(tab.windowId);
});
Tree.onAttached.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
Tree.onDetached.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
Tree.onSubtreeCollapsedStateChanging.addListener(tab => { reserveToSaveTreeStructure(tab.windowId); });
}
export function reserveToSaveTreeStructure(windowId) {
const win = TabsStore.windows.get(windowId);
if (!win)
return;
if (win.waitingToSaveTreeStructure)
clearTimeout(win.waitingToSaveTreeStructure);
win.waitingToSaveTreeStructure = setTimeout(() => {
saveTreeStructure(windowId);
}, 150);
}
async function saveTreeStructure(windowId) {
const win = TabsStore.windows.get(windowId);
if (!win)
return;
const structure = TreeBehavior.getTreeStructureFromTabs(Tab.getAllTabs(windowId));
browser.sessions.setWindowValue(
windowId,
Constants.kWINDOW_STATE_TREE_STRUCTURE,
structure
).catch(ApiTabs.createErrorSuppressor());
}
export async function loadTreeStructure(windows, restoredFromCacheResults) {
log('loadTreeStructure');
MetricsData.add('loadTreeStructure: start');
return MetricsData.addAsync('loadTreeStructure: restoration for windows', Promise.all(windows.map(async win => {
if (restoredFromCacheResults &&
restoredFromCacheResults.get(win.id)) {
log(`skip tree structure restoration for window ${win.id} (restored from cache)`);
return;
}
const tabs = Tab.getAllTabs(win.id);
let windowStateCompletelyApplied = false;
try {
const structure = await browser.sessions.getWindowValue(win.id, Constants.kWINDOW_STATE_TREE_STRUCTURE).catch(ApiTabs.createErrorHandler());
let uniqueIds = tabs.map(tab => tab.$TST.uniqueId || '?');
MetricsData.add('loadTreeStructure: read stored data');
if (structure &&
structure.length > 0 &&
structure.length <= tabs.length) {
uniqueIds = uniqueIds.map(id => id.id);
let tabsOffset;
if (structure[0].id) {
const structureSignature = toLines(structure, item => item.id);
tabsOffset = uniqueIds.join('\n').indexOf(structureSignature);
windowStateCompletelyApplied = tabsOffset > -1;
}
else {
tabsOffset = 0;
windowStateCompletelyApplied = structure.length == tabs.length;
}
if (tabsOffset > -1) {
const structureRestoreTabs = tabs.slice(tabsOffset);
await Tree.applyTreeStructureToTabs(structureRestoreTabs, structure);
for (const tab of structureRestoreTabs) {
tab.$TST.temporaryMetadata.set('treeStructureAlreadyRestoredFromSessionData', true);
}
MetricsData.add('loadTreeStructure: Tree.applyTreeStructureToTabs');
}
else {
MetricsData.add('loadTreeStructure: mismatched signature');
}
}
else {
MetricsData.add('loadTreeStructure: no valid structure information');
}
}
catch(error) {
console.log(`TreeStructure.loadTreeStructure: Fatal error, ${error}`, error.stack);
MetricsData.add('loadTreeStructure: failed to apply tree structure');
}
if (!windowStateCompletelyApplied) {
log(`Tree information for the window ${win.id} is not same to actual state. Fallback to restoration from tab relations.`);
MetricsData.add('loadTreeStructure: fallback to reserveToAttachTabFromRestoredInfo');
const unattachedTabs = new Set(tabs);
for (const tab of tabs) {
reserveToAttachTabFromRestoredInfo(tab, {
keepCurrentTree: true,
canCollapse: true
}).then(attached => {
if (attached ||
tab.$TST.parent ||
tab.$TST.hasChild)
unattachedTabs.delete(tab);
});
}
await reserveToAttachTabFromRestoredInfo.promisedDone;
MetricsData.add('loadTreeStructure: attachTabFromRestoredInfo finish');
// unknown tabs may appear inside tree, so we need to fixup tree based on their position.
for (const tab of unattachedTabs) {
const action = Tree.detectTabActionFromNewPosition(tab, {
fromIndex: tabs.length - 1,
toIndex: tab.index,
isTabCreating: true,
});
switch (action.action) {
default:
break;
case 'attach':
case 'detach':
log('loadTreeStructure: apply action for unattached tab: ', tab, action);
await action.applyIfNeeded();
break;
}
}
MetricsData.add('loadTreeStructure: finish to fixup tree structure');
}
Tab.dumpAll();
})));
}
async function reserveToAttachTabFromRestoredInfo(tab, options = {}) {
if (reserveToAttachTabFromRestoredInfo.waiting)
clearTimeout(reserveToAttachTabFromRestoredInfo.waiting);
reserveToAttachTabFromRestoredInfo.tasks.push({ tab, options: options });
if (!reserveToAttachTabFromRestoredInfo.promisedDone) {
reserveToAttachTabFromRestoredInfo.promisedDone = new Promise((resolve, _reject) => {
reserveToAttachTabFromRestoredInfo.onDone = resolve;
});
}
reserveToAttachTabFromRestoredInfo.waiting = setTimeout(async () => {
reserveToAttachTabFromRestoredInfo.waiting = null;
const tasks = reserveToAttachTabFromRestoredInfo.tasks.slice(0);
reserveToAttachTabFromRestoredInfo.tasks = [];
const uniqueIds = tasks.map(task => task.tab.$TST.uniqueId);
const bulk = tasks.length > 1;
const attachedResults = await Promise.all(uniqueIds.map((uniqueId, index) => {
const task = tasks[index];
return attachTabFromRestoredInfo(task.tab, {
...task.options,
uniqueId,
bulk
}).catch(error => {
console.log(`TreeStructure.reserveToAttachTabFromRestoredInfo: Fatal error on processing task ${index}, ${error}`, error.stack);
return false;
});
}));
if (typeof reserveToAttachTabFromRestoredInfo.onDone == 'function')
reserveToAttachTabFromRestoredInfo.onDone(attachedResults.every(attached => !!attached));
delete reserveToAttachTabFromRestoredInfo.onDone;
delete reserveToAttachTabFromRestoredInfo.promisedDone;
Tab.dumpAll();
}, 100);
return reserveToAttachTabFromRestoredInfo.promisedDone;
}
reserveToAttachTabFromRestoredInfo.waiting = null;
reserveToAttachTabFromRestoredInfo.tasks = [];
reserveToAttachTabFromRestoredInfo.promisedDone = null;
async function attachTabFromRestoredInfo(tab, options = {}) {
log('attachTabFromRestoredInfo ', tab, options);
if (tab.$TST.temporaryMetadata.has('treeStructureAlreadyRestoredFromSessionData')) {
log(' => already restored ', tab.id);
return;
}
let uniqueId, insertBefore, insertAfter, insertAfterLegacy, ancestors, children, states, collapsed /* for backward compatibility */;
// eslint-disable-next-line prefer-const
[uniqueId, insertBefore, insertAfter, insertAfterLegacy, ancestors, children, states, collapsed] = await Promise.all([
options.uniqueId || tab.$TST.uniqueId || tab.$TST.promisedUniqueId,
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_INSERT_BEFORE).catch(ApiTabs.createErrorHandler()),
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_INSERT_AFTER).catch(ApiTabs.createErrorHandler()),
// This legacy should be removed after legacy data are cleared enough, maybe after Firefox 128 is released.
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_INSERT_AFTER_LEGACY).catch(ApiTabs.createErrorHandler()),
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_ANCESTORS).catch(ApiTabs.createErrorHandler()),
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_CHILDREN).catch(ApiTabs.createErrorHandler()),
tab.$TST.getPermanentStates(),
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_SUBTREE_COLLAPSED).catch(ApiTabs.createErrorHandler()) // for backward compatibility
]);
ancestors = ancestors || [];
children = children || [];
log(`persistent references for ${dumpTab(tab)} (${uniqueId.id}): `, {
insertBefore, insertAfter,
insertAfterLegacy,
ancestors: ancestors.join(', '),
children: children.join(', '),
states,
collapsed
});
if (collapsed && !states.includes(Constants.kTAB_STATE_SUBTREE_COLLAPSED)) {
// migration
states.push(Constants.kTAB_STATE_SUBTREE_COLLAPSED);
browser.sessions.removeTabValue(tab.id, Constants.kPERSISTENT_SUBTREE_COLLAPSED).catch(ApiTabs.createErrorSuppressor());
}
insertBefore = Tab.getByUniqueId(insertBefore);
insertAfter = Tab.getByUniqueId(insertAfter || insertAfterLegacy);
ancestors = ancestors.map(Tab.getByUniqueId);
children = children.map(Tab.getByUniqueId);
log(' => references: ', tab.id, () => ({
insertBefore: dumpTab(insertBefore),
insertAfter: dumpTab(insertAfter),
ancestors: ancestors.map(dumpTab).join(', '),
children: children.map(dumpTab).join(', ')
}));
if (configs.fixupTreeOnTabVisibilityChanged) {
ancestors = ancestors.filter(ancestor => ancestor && (ancestor.hidden == tab.hidden));
children = children.filter(child => child && (child.hidden == tab.hidden));
log(' ==> references: ', tab.id, () => ({
ancestors: ancestors.map(dumpTab).join(', '),
children: children.map(dumpTab).join(', ')
}));
}
// clear wrong positioning information
if (tab.pinned ||
insertBefore?.pinned)
insertBefore = null;
const nextOfInsertAfter = insertAfter?.$TST.nextTab;
if (nextOfInsertAfter &&
nextOfInsertAfter.pinned)
insertAfter = null;
let attached = false;
const active = tab.active;
const promises = [];
for (const ancestor of ancestors) {
if (!ancestor)
continue;
log(' attach to old ancestor: ', tab.id, { child: tab, parent: ancestor });
const promisedDone = Tree.attachTabTo(tab, ancestor, {
insertBefore,
insertAfter,
dontExpand: !active,
forceExpand: active,
broadcast: true
});
if (options.bulk)
promises.push(promisedDone);
else
await promisedDone;
attached = true;
break;
}
if (!attached) {
const opener = tab.$TST.openerTab;
if (opener &&
configs.syncParentTabAndOpenerTab) {
log(' attach to opener: ', tab.id, { child: tab, parent: opener });
const promisedDone = Tree.attachTabTo(tab, opener, {
dontExpand: !active,
forceExpand: active,
broadcast: true,
insertAt: Constants.kINSERT_NEAREST
});
if (options.bulk)
promises.push(promisedDone);
else
await promisedDone;
}
else if (!options.bulk &&
(tab.$TST.nearestCompletelyOpenedNormalFollowingTab ||
tab.$TST.nearestCompletelyOpenedNormalPrecedingTab)) {
log(' attach from position: ', tab.id);
onTabAttachedFromRestoredInfo.dispatch(tab, {
toIndex: tab.index,
fromIndex: Tab.getLastTab(tab.windowId).index
});
}
}
if (!options.keepCurrentTree &&
// the restored tab is a roo tab
ancestors.length == 0 &&
// but attached to any parent based on its restored position
tab.$TST.parent &&
// when not in-middle position of existing tree (safely detachable position)
!tab.$TST.nextSiblingTab) {
Tree.detachTab(tab, {
broadcast: true
});
}
if (options.children && !options.bulk) {
let firstInTree = tab.$TST.firstChild || tab;
let lastInTree = tab.$TST.lastDescendant || tab;
for (const child of children) {
if (!child)
continue;
await Tree.attachTabTo(child, tab, {
dontExpand: !child.active,
forceExpand: active,
insertAt: Constants.kINSERT_NEAREST,
dontMove: child.index >= firstInTree.index && child.index <= lastInTree.index + 1,
broadcast: true
});
if (child.index < firstInTree.index)
firstInTree = child;
else if (child.index > lastInTree.index)
lastInTree = child;
}
}
const subtreeCollapsed = states.includes(Constants.kTAB_STATE_SUBTREE_COLLAPSED);
log('restore subtree collapsed state: ', tab.id, { current: tab.$TST.subtreeCollapsed, expected: subtreeCollapsed, ...options });
if ((options.canCollapse || options.bulk) &&
tab.$TST.subtreeCollapsed != subtreeCollapsed) {
const promisedDone = Tree.collapseExpandSubtree(tab, {
broadcast: true,
collapsed: subtreeCollapsed,
justNow: true
});
promises.push(promisedDone);
}
const updateCollapsedState = () => {
const shouldBeCollapsed = tab.$TST.ancestors.some(ancestor => ancestor.$TST.collapsed || ancestor.$TST.subtreeCollapsed);
log('update collapsed state: ', tab.id, { current: tab.$TST.collapsed, expected: shouldBeCollapsed });
if ((options.canCollapse || options.bulk) &&
tab.$TST.collapsed != shouldBeCollapsed) {
Tree.collapseExpandTabAndSubtree(tab, {
broadcast: true,
collapsed: !tab.$TST.collapsed,
justNow: true
});
}
};
tab.$TST.temporaryMetadata.set('treeStructureAlreadyRestoredFromSessionData', true);
if (options.bulk)
await Promise.all(promises).then(updateCollapsedState);
else
updateCollapsedState();
return attached;
}
const mRestoringTabs = new Map();
const mMaxRestoringTabs = new Map();
const mRestoredTabIds = new Set();
const mProcessingTabRestorations = [];
Tab.onRestored.addListener(tab => {
log('onTabRestored ', dumpTab(tab));
mProcessingTabRestorations.push(async () => {
try {
const count = mRestoringTabs.get(tab.windowId) || 0;
if (count == 0) {
setTimeout(() => {
const count = mRestoringTabs.get(tab.windowId) || 0;
if (count > 0) {
UserOperationBlocker.blockIn(tab.windowId, { throbber: true });
UserOperationBlocker.setProgress(0, tab.windowId);
}
}, configs.delayToBlockUserOperationForTabsRestoration);
}
mRestoringTabs.set(tab.windowId, count + 1);
const maxCount = mMaxRestoringTabs.get(tab.windowId) || 0;
mMaxRestoringTabs.set(tab.windowId, Math.max(count, maxCount));
const uniqueId = await tab.$TST.promisedUniqueId;
mRestoredTabIds.add(uniqueId.id);
if (count == 0) {
// Force restore recycled active tab.
// See also: https://github.com/piroor/treestyletab/issues/2191#issuecomment-489271889
const activeTab = Tab.getActiveTab(tab.windowId);
const [uniqueId, restoredUniqueId] = await Promise.all([
activeTab.$TST.promisedUniqueId,
browser.sessions.getTabValue(activeTab.id, Constants.kPERSISTENT_ID).catch(ApiTabs.createErrorHandler())
]);
if (restoredUniqueId?.id != uniqueId.id) {
activeTab.$TST.updateUniqueId({ id: restoredUniqueId.id });
reserveToAttachTabFromRestoredInfo(activeTab, {
children: true
});
}
}
reserveToAttachTabFromRestoredInfo(tab, {
children: true
});
reserveToAttachTabFromRestoredInfo.promisedDone.then(() => {
Tree.fixupSubtreeCollapsedState(tab, {
justNow: true,
broadcast: true
});
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TAB_RESTORED,
tabId: tab.id,
windowId: tab.windowId
});
let count = mRestoringTabs.get(tab.windowId) || 0;
count--;
if (count == 0) {
mRestoringTabs.delete(tab.windowId);
mMaxRestoringTabs.delete(tab.windowId);
setTimeout(() => { // because window.requestAnimationFrame is decelerate for an invisible document.
// unblock in the next event loop, after other asynchronous operations are finished
UserOperationBlocker.unblockIn(tab.windowId, { throbber: true });
}, 0);
const countToBeRestored = mRecentlyClosedTabs.filter(tab => !mRestoredTabIds.has(tab.uniqueId));
log('countToBeRestored: ', countToBeRestored);
if (countToBeRestored > 0)
tryRestoreClosedSetFor(tab, countToBeRestored);
mRestoredTabIds.clear();
}
else {
mRestoringTabs.set(tab.windowId, count);
const maxCount = mMaxRestoringTabs.get(tab.windowId);
UserOperationBlocker.setProgress(Math.round(maxCount - count / maxCount * 100), tab.windowId);
}
});
}
catch(_e) {
}
mProcessingTabRestorations.shift();
if (mProcessingTabRestorations.length > 0)
mProcessingTabRestorations[0]();
});
if (mProcessingTabRestorations.length == 1)
mProcessingTabRestorations[0]();
});
// Implementation for the "Undo Close Tab*s*" feature
// https://github.com/piroor/treestyletab/issues/2627
const mPendingRecentlyClosedTabsInfo = {
tabs: [],
structure: []
};
Tab.onRemoved.addListener((_tab, _info) => {
const currentlyRestorable = mRecentlyClosedTabs.length > 1;
mRecentlyClosedTabs = [];
mRecentlyClosedTabsTreeStructure = [];
const newlyRestorable = mRecentlyClosedTabs.length > 1;
if (currentlyRestorable != newlyRestorable)
Tab.onChangeMultipleTabsRestorability.dispatch(newlyRestorable);
});
Tab.onMultipleTabsRemoving.addListener((tabs, { triggerTab, originalStructure } = {}) => {
if (triggerTab)
tabs = [triggerTab, ...tabs];
mPendingRecentlyClosedTabsInfo.tabs = tabs.map(tab => ({
originalId: tab.id,
uniqueId: tab.$TST.uniqueId.id,
windowId: tab.windowId,
title: tab.title,
url: tab.url,
cookieStoreId: tab.cookieStoreId
}));
mPendingRecentlyClosedTabsInfo.structure = originalStructure || TreeBehavior.getTreeStructureFromTabs(tabs, {
full: true,
keepParentOfRootTabs: true
});
log('mPendingRecentlyClosedTabsInfo.tabs = ', mPendingRecentlyClosedTabsInfo.tabs);
log('mPendingRecentlyClosedTabsInfo.structure = ', mPendingRecentlyClosedTabsInfo.structure);
});
Tab.onMultipleTabsRemoved.addListener((tabs, { triggerTab } = {}) => {
log('multiple tabs are removed');
const currentlyRestorable = mRecentlyClosedTabs.length > 1;
if (triggerTab)
tabs = [triggerTab, ...tabs];
const tabIds = new Set(tabs.map(tab => tab.id));
mRecentlyClosedTabs = mPendingRecentlyClosedTabsInfo.tabs.filter(info => tabIds.has(info.originalId));
mRecentlyClosedTabsTreeStructure = mPendingRecentlyClosedTabsInfo.structure.filter(structure => tabIds.has(structure.originalId));
log(' structure: ', mRecentlyClosedTabsTreeStructure);
const newlyRestorable = mRecentlyClosedTabs.length > 1;
if (currentlyRestorable != newlyRestorable)
Tab.onChangeMultipleTabsRestorability.dispatch(newlyRestorable);
mPendingRecentlyClosedTabsInfo.tabs = [];
mPendingRecentlyClosedTabsInfo.structure = [];
});
let mToBeActivatedRestoredTabId = null;
function onRestoredTabActivated(activeInfo) {
if (mToBeActivatedRestoredTabId &&
activeInfo.id != mToBeActivatedRestoredTabId) {
TabsInternalOperation.activateTab(mToBeActivatedRestoredTabId);
}
mToBeActivatedRestoredTabId = null;
}
browser.tabs.onActivated.addListener(onRestoredTabActivated);
async function tryRestoreClosedSetFor(tab, countToBeRestored) {
const lastRecentlyClosedTabs = mRecentlyClosedTabs;
const lastRecentlyClosedTabsTreeStructure = mRecentlyClosedTabsTreeStructure;
mRecentlyClosedTabs = [];
mRecentlyClosedTabsTreeStructure = [];
if (lastRecentlyClosedTabs.length > 1)
Tab.onChangeMultipleTabsRestorability.dispatch(false);
if (!configs.undoMultipleTabsClose)
return;
const alreadRestoredIndex = lastRecentlyClosedTabs.findIndex(info => info.uniqueId == tab.$TST.uniqueId.id && info.windowId == tab.windowId);
log('tryRestoreClosedSetFor ', tab, lastRecentlyClosedTabs, lastRecentlyClosedTabsTreeStructure);
if (alreadRestoredIndex < 0) {
log(' => not a member of restorable tab set.');
return;
}
const toBeRestoredTabsCount = Math.min(
typeof countToBeRestored == 'number' ? countToBeRestored : Number.MAX_SAFE_INTEGER,
lastRecentlyClosedTabs.filter(tabInfo => tabInfo.uniqueId != tab.$TST.uniqueId.id).length
);
if (toBeRestoredTabsCount == 0) {
log(' => no more tab to be restored.');
return;
}
const sessions = (await browser.sessions.getRecentlyClosed({
maxResults: browser.sessions.MAX_SESSION_RESULTS
}).catch(ApiTabs.createErrorHandler())).filter(session => session.tab);
const canRestoreWithSession = toBeRestoredTabsCount <= sessions.length;
let restoredTabs;
if (canRestoreWithSession) {
log(`tryRestoreClosedSetFor: restore ${toBeRestoredTabsCount} tabs with the sessions API`);
const unsortedRestoredTabs = await Commands.restoreTabs(toBeRestoredTabsCount);
unsortedRestoredTabs.push(tab);
// tabs can be restored in different order, then we need to rearrange them manually
const tabsByUniqueId = new Map();
for (const tab of unsortedRestoredTabs) {
tabsByUniqueId.set(tab.$TST.uniqueId.id, tab);
}
restoredTabs = await Promise.all(lastRecentlyClosedTabsTreeStructure.map(tabInfo => {
const restoredTab = tabsByUniqueId.get(tabInfo.id);
if (restoredTab)
return restoredTab;
log('tryRestoreClosedSetFor: recreate tab for ', tabInfo);
return TabsOpen.openURIInTab({
title: tabInfo.title,
url: tabInfo.url,
cookieStoreId: tabInfo.cookieStoreId
}, {
windowId: tab.windowId,
isOrphan: true,
inBackground: true,
discarded: true,
fixPositions: true
});
}));
let lastTab;
for (const tab of restoredTabs) {
if (lastTab && tab.$TST.previousTab != lastTab)
await TabsMove.moveTabAfter(
tab,
lastTab,
{ broadcast: true }
);
lastTab = tab;
}
}
else {
log('tryRestoreClosedSetFor: recreate tabs');
const tabOpenOptions = {
windowId: lastRecentlyClosedTabs[0].windowId,
isOrphan: true,
inBackground: true,
discarded: true,
fixPositions: true
};
const beforeTabs = await TabsOpen.openURIsInTabs(
lastRecentlyClosedTabs.slice(0, alreadRestoredIndex).map(info => ({
title: info.title,
url: info.url,
cookieStoreId: info.cookieStoreId
})),
tabOpenOptions
);
const afterTabs = await TabsOpen.openURIsInTabs(
lastRecentlyClosedTabs.slice(alreadRestoredIndex + 1).map(info => ({
title: info.title,
url: info.url,
cookieStoreId: info.cookieStoreId
})),
tabOpenOptions
);
// We need to move tabs after they are opened instead of
// specifying the "index" option for TabsOpen.openURIsInTabs(),
// because the given restored tab can be moved while this
// operation.
if (beforeTabs.length > 0)
await TabsMove.moveTabsBefore(
beforeTabs,
tab,
{ broadcast: true }
);
if (afterTabs.length > 0)
await TabsMove.moveTabsAfter(
afterTabs,
tab,
{ broadcast: true }
);
restoredTabs = [...beforeTabs, tab, ...afterTabs];
await TabsInternalOperation.activateTab(restoredTabs[0]);
}
const rootTabs = restoredTabs.filter((tab, index) => lastRecentlyClosedTabsTreeStructure[index].parent == TreeBehavior.STRUCTURE_KEEP_PARENT || lastRecentlyClosedTabsTreeStructure[index].parent == TreeBehavior.STRUCTURE_NO_PARENT);
log(`tryRestoreClosedSetFor: rootTabs, restoredTabs = `, rootTabs, restoredTabs);
for (const rootTab of rootTabs) {
const referenceTabs = TreeBehavior.calculateReferenceItemsFromInsertionPosition(rootTab, {
context: Constants.kINSERTION_CONTEXT_MOVED,
insertAfter: rootTab.$TST.previousTab,
insertBefore: restoredTabs[restoredTabs.length - 1].$TST.nextTab
});
log(`tryRestoreClosedSetFor: referenceTabs for ${rootTab.id} => `, referenceTabs);
if (referenceTabs.parent)
await Tree.attachTabTo(rootTab, referenceTabs.parent, {
dontExpand: true,
insertAfter: referenceTabs.insertAfter,
dontMove: true,
broadcast: true
});
}
await Tree.applyTreeStructureToTabs(
restoredTabs,
lastRecentlyClosedTabsTreeStructure
);
// Firefox itself activates the initially restored tab with delay,
// so we need to activate the first tab of the restored tabs again.
mToBeActivatedRestoredTabId = restoredTabs[0].id;
wait(100).then(() => onRestoredTabActivated({ id: -1 })); // failsafe
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
export default class MetricsData {
constructor(name) {
this.mItems = [];
this.mName = name || '';
const now = Date.now();
this.mInitialTime = now;
this.mLastTime = now;
this.mDeltaBetweenLastItem = 0;
}
add(label) {
const now = Date.now();
this.mItems.push({
label: label,
delta: now - this.mLastTime
});
this.mDeltaBetweenLastItem = now - this.mInitialTime;
this.mLastTime = now;
}
addAsync(label, asyncTask) {
const start = Date.now();
if (typeof asyncTask == 'function')
asyncTask = asyncTask();
return asyncTask.then(result => {
this.mItems.push({
label: `(async) ${label}`,
delta: Date.now() - start,
async: true
});
return result;
});
}
toString() {
const logs = this.mItems.map(item => `${item.delta || 0}: ${item.label}`);
return `${this.mName ? this.mName + ': ' : ''}total ${this.mDeltaBetweenLastItem} msec\n${logs.join('\n')}`;
}
static add(label) {
return MetricsData.$static.add(label);
}
static addAsync(label, asyncTask) {
return MetricsData.$static.addAsync(label, asyncTask);
}
static toString() {
return MetricsData.$static.toString();
}
}
MetricsData.$static = new MetricsData();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,303 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
log as internalLogger,
dumpTab,
configs
} from './common.js';
import * as TabsStore from './tabs-store.js';
function log(...args) {
internalLogger('common/Window', ...args);
}
export default class Window {
constructor(windowId, tabGroups) {
const alreadyTracked = TabsStore.windows.get(windowId);
if (alreadyTracked)
return alreadyTracked;
log(`window ${windowId} is newly tracked`);
this.id = windowId;
this.tabs = new Map();
this.tabGroups = new Map();
if (tabGroups) {
this.initTabGroups(tabGroups);
}
this.order = [];
this.containerElement = null;
this.containerClassList = null;
this.pinnedContainerElement = null;
this.internalMovingTabs = new Map();
this.alreadyMovedTabs = new Map();
this.internalClosingTabs = new Set();
this.keepDescendantsTabs = new Set();
this.highlightingTabs = new Set();
this.tabsToBeHighlightedAlone = new Set();
this.internallyMovingTabsForUpdatedNativeTabGroups = new Set();
this.subTreeMovingCount =
this.subTreeChildrenMovingCount =
this.doingIntelligentlyCollapseExpandCount =
this.duplicatingTabsCount = 0;
this.internallyFocusingTabs = new Set();
this.internallyFocusingByMouseTabs = new Set();
this.internallyFocusingSilentlyTabs = new Set();
this.preventToDetectTabBunchesUntil = Date.now() + configs.tabBunchesDetectionDelayOnNewWindow;
this.openingTabs = new Set();
this.openedNewTabs = new Map();
this.bypassTabControlCount = 0;
this.toBeOpenedNewTabCommandTab = 0;
this.toBeOpenedTabsWithPositions = 0;
this.toBeOpenedTabsWithCookieStoreId = 0;
this.toBeOpenedOrphanTabs = 0;
this.toBeAttachedTabs = new Set();
this.toBeDetachedTabs = new Set();
this.lastRelatedTabs = new Map();
this.previousLastRelatedTabs = new Map();
TabsStore.windows.set(windowId, this);
TabsStore.prepareIndexesForWindow(windowId);
// We should initialize private properties with blank value for better performance with a fixed shape.
this.delayedDestroy = null;
}
initTabGroups(tabGroups) {
log(`initializing tabGroups of window ${this.id}: `, tabGroups);
this.tabGroups = new Map((tabGroups || []).map(group => [group.id, group]));
}
destroy() {
for (const tab of this.tabs.values()) {
if (tab.$TST)
tab.$TST.destroy();
}
this.tabs.clear();
this.tabGroups.clear();
TabsStore.windows.delete(this.id);
TabsStore.unprepareIndexesForWindow(this.id);
if (this.containerElement) {
const element = this.containerElement;
if (element.parentNode && !element.hasChildNodes())
element.parentNode.removeChild(element);
}
if (this.pinnedContainerElement) {
const element = this.element;
if (element.parentNode && !element.hasChildNodes())
element.parentNode.removeChild(element);
}
this.unbindElements();
this.tabs = null;
this.tabGroups = null;
this.order = null;
this.id = null;
}
clear() {
this.tabs.clear();
this.order = [];
TabsStore.unprepareIndexesForWindow(this.id);
TabsStore.prepareIndexesForWindow(this.id);
this.clearLastRelatedTabs();
}
clearLastRelatedTabs() {
for (const openerId of this.lastRelatedTabs.keys()) {
const opener = this.tabs.get(openerId);
if (!opener)
continue;
opener.$TST.newRelatedTabsCount = 0;
}
this.lastRelatedTabs.clear();
this.previousLastRelatedTabs.clear();
}
bindContainerElement(element) {
this.containerElement = element;
this.containerClassList = element.classList;
}
bindPinnedContainerElement(element) {
this.pinnedContainerElement = element;
this.pinnedContainerClassList = element.classList;
}
unbindElements() {
this.containerElement = null;
this.containerClassList = null;
this.pinnedContainerElement = null;
this.pinnedContainerClassList = null;
}
getOrderedTabs(startId, endId, tabs) {
const orderedIds = this.sliceOrder(startId, endId);
tabs = tabs || this.tabs;
return (function*() {
for (const id of orderedIds) {
const tab = tabs.get(id);
if (tab)
yield tab;
}
}).call(this);
}
getReversedOrderedTabs(startId, endId, tabs) {
const orderedIds = this.sliceOrder(startId, endId, this.order.slice(0).reverse());
tabs = tabs || this.tabs;
return (function*() {
for (const id of orderedIds) {
const tab = tabs.get(id);
if (tab)
yield tab;
}
}).call(this);
}
sliceOrder(startId, endId, orderedIds) {
if (!orderedIds)
orderedIds = this.order;
if (startId) {
if (!this.tabs.has(startId))
return [];
orderedIds = orderedIds.slice(orderedIds.indexOf(startId));
}
if (endId) {
if (!this.tabs.has(endId))
return [];
orderedIds = orderedIds.slice(0, orderedIds.indexOf(endId) + 1);
}
return orderedIds;
}
trackTab(tab) {
const alreadyTracked = TabsStore.tabs.get(tab.id);
if (alreadyTracked)
tab = alreadyTracked;
if (this.delayedDestroy) {
clearTimeout(this.delayedDestroy);
this.delayedDestroy = null;
}
const order = this.order;
if (this.tabs.has(tab.id)) { // already tracked: update
const prevState = tab.reindexedBy;
const index = order.indexOf(tab.id);
order.splice(index, 1);
order.splice(tab.index, 0, tab.id);
for (let i = Math.min(index, tab.index), maxi = Math.min(Math.max(index, tab.index) + 1, order.length); i < maxi; i++) {
const tab = this.tabs.get(order[i]);
if (!tab)
throw new Error(`Unknown tab: ${i}/${order[i]} (${order.join(', ')})`);
tab.index = i;
tab.reindexedBy = `Window.property.trackTab/update (${tab.index})`;
tab.$TST.invalidateCache();
}
const parent = tab.$TST.parent;
if (parent) {
parent.$TST.sortAndInvalidateChildren();
parent.$TST.invalidateCachedAncestors();
}
log(`tab ${dumpTab(tab)} is re-tracked under the window ${this.id}: `, prevState, index, '=>', tab.reindexedBy, order.join(', '));
}
else { // not tracked yet: add
this.tabs.set(tab.id, tab);
order.splice(tab.index, 0, tab.id);
for (let i = tab.index + 1, maxi = order.length; i < maxi; i++) {
const tab = this.tabs.get(order[i]);
if (!tab)
throw new Error(`Unknown tab: ${i}/${order[i]} (${order.join(', ')})`);
tab.index = i;
tab.reindexedBy = `Window.property.trackTab/new (${tab.index})`;
tab.$TST.invalidateCache();
}
log(`tab ${dumpTab(tab)} is newly tracked under the window ${this.id}: `, order);
}
TabsStore.updateIndexesForTab(tab);
tab.$TST.invalidateCache();
return tab;
}
detachTab(tabId) {
const tab = TabsStore.tabs.get(tabId);
if (!tab)
return;
TabsStore.removeTabFromIndexes(tab);
tab.$TST.detach();
this.tabs.delete(tabId);
const order = this.order;
const index = order.indexOf(tab.id);
if (index < 0) // the tab is not tracked yet!
return;
order.splice(index, 1);
if (this.tabs.size == 0) {
if (!TabsStore.getCurrentWindowId()) { // only in the background page - the sidebar has no need to destroy itself manually.
// the last tab can be removed with browser.tabs.closeWindowWithLastTab=false,
// so we should not destroy the window immediately.
if (this.delayedDestroy)
clearTimeout(this.delayedDestroy);
this.delayedDestroy = setTimeout(() => {
if (this.tabs &&
this.tabs.size == 0)
this.destroy();
}, (configs.collapseDuration, 1000) * 5);
}
}
else {
for (let i = index, maxi = order.length; i < maxi; i++) {
this.tabs.get(order[i]).index = i;
}
}
return tab;
}
untrackTab(tabId) {
const tab = this.detachTab(tabId);
if (tab)
tab.$TST.destroy();
}
export(full) {
const tabs = [];
for (const tab of this.getOrderedTabs()) {
tabs.push(tab.$TST.export(full));
}
return {
tabs,
tabGroups: [...this.tabGroups.values()].map(group => group.$TST.sanitized),
};
}
}
Window.onInitialized = new EventListenerManager();
Window.init = (windowId, tabGroups) => {
const win = TabsStore.windows.get(windowId) || new Window(windowId, tabGroups);
if (tabGroups && tabGroups.size != win.tabGroups.size) {
win.initTabGroups(tabGroups);
}
Window.onInitialized.dispatch(win);
return win;
}

View File

@@ -0,0 +1,137 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
configs
} from './common.js';
function log(...args) {
internalLogger('common/api-tabs', ...args);
}
export async function getIndexes(...queriedTabIds) {
log('getIndexes ', queriedTabIds);
if (queriedTabIds.length == 0)
return [];
const indexes = await Promise.all(queriedTabIds.map((tabId) => {
return browser.tabs.get(tabId)
.catch(e => {
handleMissingTabError(e);
return null;
});
}));
return indexes.map(tab => tab ? tab.index : -1);
}
export function isMissingTabError(error) {
return (
error &&
error.message &&
error.message.includes('Invalid tab ID:')
);
}
export function handleMissingTabError(error) {
if (!isMissingTabError(error))
throw error;
// otherwise, this error is caused from a tab already closed.
// we just ignore it.
//console.log('Invalid Tab ID error on: ' + error.stack);
}
export function isUnloadedError(error) {
return (
error &&
error.message &&
error.message.includes('can\'t access dead object')
);
}
export function handleUnloadedError(error) {
if (!isUnloadedError(error))
throw error;
}
export function isMissingHostPermissionError(error) {
return (
error &&
error.message &&
error.message.includes('Missing host permission for the tab')
);
}
export function handleMissingHostPermissionError(error) {
if (!isMissingHostPermissionError(error))
throw error;
}
export function createErrorHandler(...handlers) {
const stack = configs.debug && new Error().stack;
return (error) => {
try {
if (handlers.length > 0) {
let unhandledCount = 0;
handlers.forEach(handler => {
try {
handler(error);
}
catch(_error) {
unhandledCount++;
}
});
if (unhandledCount == handlers.length) // not handled
throw error;
}
else {
throw error;
}
}
catch(newError){
if (!configs.debug)
throw newError;
if (error == newError)
console.log('Unhandled Error: ', error, stack);
else
console.log('Unhandled Error: ', error, newError, stack);
}
};
}
export function createErrorSuppressor(...handlers) {
const stack = configs.debug && new Error().stack;
return (error) => {
try {
if (handlers.length > 0) {
let unhandledCount = 0;
handlers.forEach(handler => {
try {
handler(error);
}
catch(_error) {
unhandledCount++;
}
});
if (unhandledCount == handlers.length) // not handled
throw error;
}
else {
throw error;
}
}
catch(newError){
if (error &&
error.message &&
error.message.indexOf('Could not establish connection. Receiving end does not exist.') == 0)
return;
if (!configs.debug)
return;
if (error == newError)
console.log('Unhandled Error: ', error, stack);
else
console.log('Unhandled Error: ', error, newError, stack);
}
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,201 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
configs
} from '/common/common.js';
import * as Color from './color.js';
import * as Constants from './constants.js';
export function generateThemeRules(theme) {
const rules = [];
const generateCustomRule = (theme, prefix = '') => {
for (const key of Object.keys(theme)) {
if (!theme[key])
continue;
const propertyKey = prefix ? `${prefix}-${key}` : key;
let value = theme[key];
switch (typeof theme[key]) {
case 'object':
generateCustomRule(value, propertyKey);
break;
case 'string':
if (/^[^:]+:\/\//.test(value))
value = `url(${JSON.stringify(value)})`;
rules.push(`--theme-${propertyKey}: ${value};`);
for (let alpha = 10; alpha < 100; alpha += 10) {
rules.push(`--theme-${propertyKey}-${alpha}: ${Color.overrideCSSAlpha(value, alpha / 100)};`);
}
break;
}
}
};
generateCustomRule(theme);
return rules.join('\n');
}
export async function generateThemeDeclarations(theme) {
if (!theme ||
!theme.colors) {
return `
:root {
/* https://searchfox.org/mozilla-central/rev/0c7c41109902cb8967ec3ef2c0ddb326701cfbee/browser/themes/windows/browser.css#15 */
/* https://searchfox.org/mozilla-central/rev/0c7c41109902cb8967ec3ef2c0ddb326701cfbee/browser/themes/linux/browser.css#20 */
--non-lwt-selected-tab-background-color-proton: rgba(255, 255, 255, 0.15);
}`;
}
const extraColors = [];
const themeFrameColor = theme.colors.frame || theme.colors.accentcolor /* old name */;
const inactiveTextColor = theme.colors.tab_background_text || theme.colors.textcolor /* old name */;
const activeTextColor = theme.colors.tab_text || theme.colors.bookmark_text || theme.colors.toolbar_text /* old name */ || inactiveTextColor;
let bgAlpha = 1;
let hasImage = false;
if (theme.images) {
const isRightside = configs.sidebarPosition == Constants.kTABBAR_POSITION_RIGHT;
const images = [];
const frameImage = theme.images.theme_frame || theme.images.headerURL /* old name */;
if (frameImage) {
hasImage = true;
// https://searchfox.org/mozilla-central/rev/532e4b94b9e807d157ba8e55034aef05c1196dc9/browser/themes/shared/tabs.inc.css#537
extraColors.push('--browser-bg-hover-for-header-image: rgba(0, 0, 0, 0.1);');
// https://searchfox.org/mozilla-central/rev/532e4b94b9e807d157ba8e55034aef05c1196dc9/browser/base/content/browser.css#20
extraColors.push('--browser-bg-active-for-header-image: rgba(255, 255, 255, 0.4)');
// https://searchfox.org/mozilla-central/rev/532e4b94b9e807d157ba8e55034aef05c1196dc9/toolkit/themes/windows/global/global.css#138
if (Color.isBrightColor(inactiveTextColor)) {
// for bright text
extraColors.push('--browser-textshadow-for-header-image: 1px 1px 1.5px black');
}
else {
// for dark text
extraColors.push('--browser-textshadow-for-header-image: 0 0 1.5px white');
}
images.push({
url: frameImage,
position: isRightside ? 'top right' : 'top left',
repeat: 'no-repeat',
});
}
const positions = theme.properties?.additional_backgrounds_alignment || [];
const repeats = theme.properties?.additional_backgrounds_tiling || [];
if (Array.isArray(theme.images.additional_backgrounds) &&
theme.images.additional_backgrounds.length > 0) {
const leftImageCount = positions.filter(position => position.includes('left')).length;
const rightImageCount = positions.filter(position => position.includes('right')).length;
const repeatableImageCount = repeats.filter(repeat => repeat != 'no-repeat').length;
for (let i = 0, maxi = theme.images.additional_backgrounds.length; i < maxi; i++) {
const image = theme.images.additional_backgrounds[i];
const position = positions.length > 0 && positions[Math.min(i, positions.length - 1)] || 'default';
const repeat = repeats.length > 0 && repeats[Math.min(i, repeats.length - 1)] || 'default';
if (repeatableImageCount > 0 &&
repeat.includes('no-repeat'))
continue;
if (position &&
position.includes('right') != isRightside &&
repeat == 'no-repeat' &&
leftImageCount > 0 &&
rightImageCount > 0)
continue;
images.push({
url: image,
position,
repeat,
size: repeat == 'reepat-y' ? 'auto' : 'auto 100%',
});
}
bgAlpha = 0.75;
hasImage = true;
}
await Promise.all(images.map(async image => {
if (image.size)
return;
const loader = new Image();
try {
const shouldRepeat = (
theme.properties &&
Array.isArray(theme.properties.additional_backgrounds_tiling) &&
theme.properties.additional_backgrounds_tiling.some(value => value == 'repeat' || value == 'repeat-y')
);
const shouldNoRepeat = (
!theme.properties ||
!Array.isArray(theme.properties.additional_backgrounds_tiling) ||
theme.properties.additional_backgrounds_tiling.some(value => value == 'no-repeat')
);
let maybeRepeatable = false;
if (!shouldRepeat && !shouldNoRepeat) {
await new Promise((resolve, reject) => {
loader.addEventListener('load', resolve);
loader.addEventListener('error', reject);
loader.src = image;
});
maybeRepeatable = (loader.width / Math.max(1, loader.height)) <= configs.unrepeatableBGImageAspectRatio;
}
if (shouldNoRepeat)
image.size = 'cover';
else if (shouldRepeat || maybeRepeatable)
image.size = 'auto';
}
catch(error) {
console.error(error);
}
}));
if (hasImage) {
extraColors.push('--browser-bg-images: ' + images.map(image => `url(${JSON.stringify(image.url)})`).join(','));
extraColors.push('--browser-bg-position: ' + images.map(image => image.position).join(','));
extraColors.push('--browser-bg-repeat: ' + images.map(image => image.repeat).join(','));
extraColors.push('--browser-bg-size: ' + images.map(image => image.size).join(','));
}
}
const themeBaseColor = Color.overrideCSSAlpha(themeFrameColor, bgAlpha);
let toolbarColor = Color.mixCSSColors(themeBaseColor, 'rgba(255, 255, 255, 0.4)', bgAlpha);
if (theme.colors.toolbar) {
if (hasImage) {
extraColors.push(`--browser-bg-for-header-image: ${theme.colors.toolbar};`);
toolbarColor = theme.colors.toolbar;
}
else {
toolbarColor = Color.mixCSSColors(themeBaseColor, theme.colors.toolbar);
}
extraColors.push(`--browser-toolbar: ${theme.colors.toolbar}`);
if (Color.isParsable(theme.colors.toolbar) &&
Color.isParsable(theme.colors.toolbar_text)) {
const halfTransparentTextColor = Color.mixCSSColors(theme.colors.toolbar_text, theme.colors.toolbar_text, 0.5);
extraColors.push(`--browser-toolbar_text-darker: ${Color.mixCSSColors(theme.colors.toolbar, halfTransparentTextColor)}`);
}
}
else if (hasImage) {
extraColors.push('--browser-bg-for-header-image: rgba(255, 255, 255, 0.25);');
}
if (theme.colors.tab_line)
extraColors.push(`--browser-tab-highlighter: ${theme.colors.tab_line}`);
if (theme.colors.tab_loading)
extraColors.push(`--browser-loading-indicator: ${theme.colors.tab_loading}`);
if (theme.colors.tab_selected)
extraColors.push(`--browser-selected-tab-bg: ${theme.colors.tab_selected}`);
extraColors.push(generateThemeRules(theme));
return `
:root {
--browser-background: ${themeFrameColor};
--browser-bg-base: ${toolbarColor};
--browser-bg-less-lighter: ${Color.mixCSSColors(toolbarColor, 'rgba(255, 255, 255, 0.05)', bgAlpha)};
--browser-bg-lighter: ${Color.mixCSSColors(toolbarColor, 'rgba(255, 255, 255, 0.1)', bgAlpha)};
--browser-bg-more-lighter: ${Color.mixCSSColors(toolbarColor, 'rgba(255, 255, 255, 0.25)', bgAlpha)};
--browser-bg-lightest: ${Color.mixCSSColors(toolbarColor, 'rgba(255, 255, 255, 0.4)', bgAlpha)};
--browser-bg-less-darker: ${Color.mixCSSColors(toolbarColor, 'rgba(0, 0, 0, 0.1)', bgAlpha)};
--browser-bg-darker: ${Color.mixCSSColors(toolbarColor, 'rgba(0, 0, 0, 0.25)', bgAlpha)};
--browser-bg-more-darker: ${Color.mixCSSColors(toolbarColor, 'rgba(0, 0, 0, 0.5)', bgAlpha)};
--browser-fg: ${inactiveTextColor};
--browser-fg-active: ${activeTextColor};
--browser-border: ${Color.overrideCSSAlpha(inactiveTextColor, 0.4)};
${extraColors.join(';\n')}
}
`;
}

View File

@@ -0,0 +1,318 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import * as UniqueId from '/common/unique-id.js';
import {
asyncRunWithTimeout,
} from '/common/common.js';
const DB_NAME = 'PermanentStorage';
const DB_VERSION = 3;
const EXPIRATION_TIME_IN_MSEC = 7 * 24 * 60 * 60 * 1000; // 7 days
const TIMEOUT_IN_MSEC = 1000 * 5; // 5 sec
export const BACKGROUND = 'backgroundCaches';
const SIDEBAR = 'sidebarCaches'; // obsolete, but left here to delete old storage
let mOpenedDB;
async function openDB() {
if (mOpenedDB)
return mOpenedDB;
return new Promise((resolve, _reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => {
// This can fail if this is in a private window.
// See: https://github.com/piroor/treestyletab/issues/3387
//reject(new Error('Failed to open database'));
resolve(null);
};
request.onsuccess = () => {
const db = request.result;
mOpenedDB = db;
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
const objectStores = db.objectStoreNames;
const needToUpgrade = event.oldVersion < DB_VERSION;
if (needToUpgrade) {
if (objectStores.contains(BACKGROUND))
db.deleteObjectStore(BACKGROUND);
if (objectStores.contains(SIDEBAR))
db.deleteObjectStore(SIDEBAR);
}
if (needToUpgrade ||
!objectStores.contains(BACKGROUND)) {
const backgroundCachesStore = db.createObjectStore(BACKGROUND, { keyPath: 'key', unique: true });
backgroundCachesStore.createIndex('windowId', 'windowId', { unique: false });
backgroundCachesStore.createIndex('timestamp', 'timestamp');
}
};
});
}
export async function setValue({ windowId, key, value } = {}) {
const [db, windowUniqueId] = await Promise.all([
openDB(),
UniqueId.ensureWindowId(windowId),
]);
if (!db)
return;
reserveToExpireOldEntries();
const store = BACKGROUND;
const cacheKey = `${windowUniqueId}-${key}`;
asyncRunWithTimeout({
task: () => new Promise((resolve, reject) => {
const timestamp = Date.now();
try {
const transaction = db.transaction([store], 'readwrite');
const cacheStore = transaction.objectStore(store);
const cacheRequest = cacheStore.put({
key: cacheKey,
windowId: windowUniqueId,
value,
timestamp,
});
transaction.oncomplete = () => {
//db.close();
windowId = undefined;
key = undefined;
value = undefined;
resolve();
};
cacheRequest.onerror = event => {
console.error(`Failed to store cache ${cacheKey} in the store ${store}`, event);
reject(event);
};
}
catch(error) {
console.error(`Failed to store cache ${cacheKey} in the store ${store}`, error);
reject(error);
}
}),
timeout: TIMEOUT_IN_MSEC,
onTimedOut() {
throw new Error(`CacheStorage.setValue for ${windowId}/key timed out`);
},
});
}
export async function deleteValue({ windowId, key } = {}) {
const [db, windowUniqueId] = await Promise.all([
openDB(),
UniqueId.ensureWindowId(windowId),
]);
if (!db)
return;
reserveToExpireOldEntries();
const store = BACKGROUND;
const cacheKey = `${windowUniqueId}-${key}`;
asyncRunWithTimeout({
task: () => new Promise((resolve, reject) => {
try {
const transaction = db.transaction([store], 'readwrite');
const cacheStore = transaction.objectStore(store);
const cacheRequest = cacheStore.delete(cacheKey);
transaction.oncomplete = () => {
//db.close();
windowId = undefined;
key = undefined;
resolve();
};
cacheRequest.onerror = event => {
console.error(`Failed to delete cache ${cacheKey} in the store ${store}`, event);
reject(event);
};
}
catch(error) {
console.error(`Failed to delete cache ${cacheKey} in the store ${store}`, error);
reject(error);
}
}),
timeout: TIMEOUT_IN_MSEC,
onTimedOut() {
throw new Error(`CacheStorage.deleteValue for {windowId}/key timed out`);
},
});
}
export async function getValue({ windowId, key } = {}) {
return asyncRunWithTimeout({
task: () => getValueInternal({ windowId, key }),
timeout: TIMEOUT_IN_MSEC,
onTimedOut() {
throw new Error(`CacheStorage.getValue for {windowId}/${key} timed out`);
},
});
}
async function getValueInternal({ windowId, key } = {}) {
return new Promise(async (resolve, _reject) => {
const [db, windowUniqueId] = await Promise.all([
openDB(),
UniqueId.ensureWindowId(windowId),
]);
if (!db) {
resolve(null);
return;
}
const store = BACKGROUND;
const cacheKey = `${windowUniqueId}-${key}`;
const timestamp = Date.now();
try {
const transaction = db.transaction([store], 'readwrite');
const cacheStore = transaction.objectStore(store);
const cacheRequest = cacheStore.get(cacheKey);
cacheRequest.onsuccess = () => {
const cache = cacheRequest.result;
if (!cache) {
resolve(null);
return;
}
// IndexedDB does not support partial update, so
// we need to put all properties not only timestamp.
cacheStore.put({
key: cacheKey,
windowId: windowUniqueId,
value: cache.value,
timestamp,
});
resolve(cache.value);
cache.key = undefined;
cache.windowId = undefined;
cache.value = undefined;
};
cacheRequest.onerror = event => {
console.error('Failed to get from cache:', event);
resolve(null);
};
transaction.oncomplete = () => {
//db.close();
windowId = undefined;
key = undefined;
};
}
catch(error) {
console.error('Failed to get from cache:', error);
resolve(null);
}
});
}
export async function clearForWindow(windowId) {
return asyncRunWithTimeout({
task: () => clearForWindowInternal(windowId),
timeout: TIMEOUT_IN_MSEC,
onTimedOut() {
throw new Error(`CacheStorage.clearForWindow for ${windowId} timed out`);
},
});
}
async function clearForWindowInternal(windowId) {
reserveToExpireOldEntries();
return new Promise(async (resolve, reject) => {
const [db, windowUniqueId] = await Promise.all([
openDB(),
UniqueId.ensureWindowId(windowId),
]);
if (!db) {
resolve(null);
return;
}
try {
const transaction = db.transaction([BACKGROUND], 'readwrite');
const backgroundCacheStore = transaction.objectStore(BACKGROUND);
const backgroundCacheIndex = backgroundCacheStore.index('windowId');
const backgroundCacheRequest = backgroundCacheIndex.openCursor(IDBKeyRange.only(windowUniqueId));
backgroundCacheRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor)
return;
const key = cursor.primaryKey;
cursor.continue();
backgroundCacheStore.delete(key);
};
transaction.oncomplete = () => {
//db.close();
resolve();
};
}
catch(error) {
console.error('Failed to clear caches:', error);
reject(error);
}
});
}
async function reserveToExpireOldEntries() {
if (reserveToExpireOldEntries.reservedExpiration)
clearTimeout(reserveToExpireOldEntries.reservedExpiration);
reserveToExpireOldEntries.reservedExpiration = setTimeout(() => {
reserveToExpireOldEntries.reservedExpiration = null;
expireOldEntries();
}, 500);
}
async function expireOldEntries() {
return new Promise(async (resolve, reject) => {
const db = await openDB();
if (!db) {
resolve();
return;
}
try {
const transaction = db.transaction([BACKGROUND], 'readwrite');
const backgroundCacheStore = transaction.objectStore(BACKGROUND);
const backgroundCacheIndex = backgroundCacheStore.index('timestamp');
const expirationTimestamp = Date.now() - EXPIRATION_TIME_IN_MSEC;
const backgroundCacheRequest = backgroundCacheIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
backgroundCacheRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor)
return;
const key = cursor.primaryKey;
cursor.continue();
backgroundCacheStore.delete(key);
};
transaction.oncomplete = () => {
//db.close();
resolve();
};
}
catch(error) {
console.error('Failed to expire old entries:', error);
reject(error);
}
});
}

View File

@@ -0,0 +1,199 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
export function mixCSSColors(base, over, alpha = 1) {
base = parseCSSColor(base);
over = parseCSSColor(over);
const mixed = mixColors(base, over);
return `rgba(${mixed.red}, ${mixed.green}, ${mixed.blue}, ${alpha})`;
}
export function overrideCSSAlpha(color, alpha = 1) {
const parsed = parseCSSColor(color);
let base = color;
let baseAlpha = parsed.alpha;
if (baseAlpha > 0) {
while (baseAlpha < 1) {
base = mixCSSColors(base, base);
baseAlpha *= 2;
}
}
return mixCSSColors(base, base, alpha);
}
function normalizeColorElement(value) {
return Math.max(0, Math.min(255, value.endsWith('%') ? (parseFloat(value) / 100 * 255) : parseFloat(value)));
}
function normalizeAlpha(value) {
if (!value)
return 1;
return Math.max(0, Math.min(1, value.endsWith('%') ? (parseFloat(value) / 100) : parseFloat(value)));
}
function normalizeAngle(value, unit) {
value = parseFloat(value);
switch ((unit || '').toLowerCase()) {
case 'rad':
value = value * (180 / Math.PI);
break;
case 'grad':
value = value * 0.9;
break;
case 'turn':
value = value * 360;
break;
default: // deg
break;
}
value = value % 360;
if (value < 0)
value += 360;
return value;
}
const HEX = '[0-9a-f]';
const DOUBLE_HEX = `${HEX}{2}`;
const FLOAT = '(?:\\.?[0-9]+|[0-9]+(?:\\.[0-9]+)?)';
const FLOAT_OR_PERCENTAGE = `${FLOAT}%?`;
const HEX_RRGGBBAA_MATCHER = new RegExp(`^#?(${DOUBLE_HEX})(${DOUBLE_HEX})(${DOUBLE_HEX})(${DOUBLE_HEX})?$`, 'i');
const HEX_RGBA_MATCHER = new RegExp(`^#?(${HEX})(${HEX})(${HEX})(${HEX})?$`, 'i');
const RGBA_MATCHER = new RegExp(`^rgba?\\(\\s*(${FLOAT_OR_PERCENTAGE})\\s*,?\\s*(${FLOAT_OR_PERCENTAGE})\\s*,?\\s*(${FLOAT_OR_PERCENTAGE})(?:\\s*[,/]?\\s*(${FLOAT_OR_PERCENTAGE})\\s*)?\\)$`, 'i');
const HSLA_MATCHER = new RegExp(`^hsla?\\(\\s*(${FLOAT})(deg|rad|grad|turn)?\\s*,?\\s*(${FLOAT_OR_PERCENTAGE})\\s*,?\\s*(${FLOAT_OR_PERCENTAGE})(?:\\s*[,/]?\\s*(${FLOAT_OR_PERCENTAGE})\\s*)?\\)$`, 'i');
export function isParsable(color) {
if (!color)
return false;
const stringifiedColor = String(color);
return HEX_RRGGBBAA_MATCHER.test(stringifiedColor) ||
HEX_RGBA_MATCHER.test(stringifiedColor) ||
RGBA_MATCHER.test(stringifiedColor) ||
HSLA_MATCHER.test(stringifiedColor);
}
export function parseCSSColor(color, baseColor) {
if (typeof color!= 'string')
return color;
let red, green, blue, alpha;
let parts = color.match(HEX_RRGGBBAA_MATCHER);
if (parts) {
red = parseInt(parts[1], 16);
green = parseInt(parts[2], 16);
blue = parseInt(parts[3], 16);
alpha = parts[4] ? parseInt(parts[4], 16) / 255 : 1 ;
}
if (!parts) {
// #RGB, #RGBA
parts = color.match(HEX_RGBA_MATCHER);
if (parts) {
red = Math.min(255, Math.round(255 * (parseInt(parts[1], 16) / 16)));
green = Math.min(255, Math.round(255 * (parseInt(parts[2], 16) / 16)));
blue = Math.min(255, Math.round(255 * (parseInt(parts[3], 16) / 16)));
alpha = parts[4] ? parseInt(parts[4], 16) / 16 : 1 ;
}
}
if (!parts) {
// rgb(), rgba()
parts = color.match(RGBA_MATCHER);
if (parts) {
red = normalizeColorElement(parts[1]);
green = normalizeColorElement(parts[2]);
blue = normalizeColorElement(parts[3]);
alpha = normalizeAlpha(parts[4]);
}
}
if (!parts) {
// hsl(), hsla()
parts = color.match(HSLA_MATCHER);
if (parts) {
const hue = normalizeAngle(parts[1], parts[2]);
const saturation = parseFloat(parts[3]);
const lightness = parseFloat(parts[4]);
let min, max;
if (lightness < 50) {
max = 2.55 * (lightness + (lightness * (saturation / 100)));
min = 2.55 * (lightness - (lightness * (saturation / 100)));
}
else {
max = 2.55 * (lightness + ((100 - lightness) * (saturation / 100)));
min = 2.55 * (lightness - ((100 - lightness) * (saturation / 100)));
}
if (hue < 60) {
red = max;
green = (hue / 60) * (max - min) + min;
blue = min;
}
else if (hue < 120) {
red = ((120 - hue) / 60) * (max - min) + min;
green = max;
blue = min;
}
else if (hue < 180) {
red = min;
green = max;
blue = ((hue - 120) / 60) * (max - min) + min;
}
else if (hue < 240) {
red = min;
green = ((240 - hue) / 60) * (max - min) + min;
blue = max;
}
else if (hue < 300) {
red = ((hue - 240) / 60) * (max - min) + min;
green = min;
blue = max;
}
else {
red = max;
green = min;
blue = ((360 - hue) / 60) * (max - min) + min;
}
alpha = normalizeAlpha(parts[5]);
}
}
if (!parts) {
switch(color.toLowerCase()) {
case 'transparent':
red = 0;
green = 0;
blue = 0;
alpha = 0;
break;
default:
return color;
}
}
const parsed = { red, green, blue, alpha };
if (alpha < 1 && baseColor)
return mixColors(parseCSSColor(baseColor), parsed);
return parsed;
}
function mixColors(base, over) {
const alpha = over.alpha;
const red = Math.min(255, Math.round((base.red * (1 - alpha)) + (over.red * alpha)));
const green = Math.min(255, Math.round((base.green * (1 - alpha)) + (over.green * alpha)));
const blue = Math.min(255, Math.round((base.blue * (1 - alpha)) + (over.blue * alpha)));
return { red, green, blue, alpha: 1 };
}
export function isBrightColor(color) {
color = parseCSSColor(color);
// https://searchfox.org/mozilla-central/rev/532e4b94b9e807d157ba8e55034aef05c1196dc9/browser/base/content/browser.js#8200
const luminance = (color.red * 0.2125) + (color.green * 0.7154) + (color.blue * 0.0721);
return luminance > 110;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,482 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
export const kCOMMAND_GET_INSTANCE_ID = 'ws:get-instance-id';
export const kCOMMAND_RELOAD = 'ws:reload';
export const kCOMMAND_PING_TO_BACKGROUND = 'ws:ping-to-background';
export const kCOMMAND_PING_TO_SIDEBAR = 'ws:ping-to-sidebar';
export const kCOMMAND_REQUEST_CONNECT_PREFIX = 'ws:request-connect-from:';
export const kCOMMAND_REQUEST_UNIQUE_ID = 'ws:request-unique-id';
export const kCOMMAND_GET_THEME_DECLARATIONS = 'ws:get-theme-declarations';
export const kCOMMAND_GET_CONTEXTUAL_IDENTITIES_COLOR_INFO = 'ws:get-contextual-identities-color-info';
export const kCOMMAND_GET_CONFIG_VALUE = 'ws:get-config-value';
export const kCOMMAND_SET_CONFIG_VALUE = 'ws:set-config-value';
export const kCOMMAND_GET_USER_STYLE_RULES = 'ws:get-user-style-rules';
export const kCOMMAND_PULL_TABS = 'ws:pull-tabs';
export const kCOMMAND_SYNC_TABS_ORDER = 'ws:sync-tabs-order';
export const kCOMMAND_PULL_TABS_ORDER = 'ws:pull-tabs-order';
export const kCOMMAND_PULL_TREE_STRUCTURE = 'ws:pull-tree-structure';
export const kCOMMAND_GET_RENDERED_TAB_IDS = 'ws:get-rendered-tab-ids';
export const kCOMMAND_ASK_TAB_IS_IN_VIEWPORT = 'ws:ask-tab-is-in-viewport';
export const kCOMMAND_LOAD_URI = 'ws:load-uri';
export const kCOMMAND_OPEN_TAB = 'ws:open-tab';
export const kCOMMAND_NEW_WINDOW_FROM_TABS = 'ws:open-new-window-from-tabs';
export const kCOMMAND_NEW_TABS = 'ws:open-new-tabs';
export const kCOMMAND_NEW_TAB_AS = 'ws:open-new-tab-as';
export const kCOMMAND_REMOVE_TABS_BY_MOUSE_OPERATION = 'ws:remove-tabs-by-mouse-operation';
export const kCOMMAND_REMOVE_TABS_INTERNALLY = 'ws:remove-tabs-internally';
export const kCOMMAND_UPDATE_LOADING_STATE = 'ws:update-loading-state';
export const kCOMMAND_CONFIRM_TO_CLOSE_TABS = 'ws:confirm-to-close-tabs';
export const kCOMMAND_SHOW_DIALOG = 'ws:show-dialog';
export const kCOMMAND_NOTIFY_BACKGROUND_READY = 'ws:notify-background-ready';
export const kCOMMAND_NOTIFY_CONNECTION_READY = 'ws:notify-connection-ready';
export const kCOMMAND_NOTIFY_SIDEBAR_CLOSED = 'ws:notify-sidebar-closed';
export const kCOMMAND_NOTIFY_TAB_CREATING = 'ws:notify-tab-creating';
export const kCOMMAND_NOTIFY_TAB_CREATED = 'ws:notify-tab-created';
export const kCOMMAND_NOTIFY_TAB_UPDATED = 'ws:notify-tab-updated';
export const kCOMMAND_NOTIFY_TAB_MOVED = 'ws:notify-tab-moved';
export const kCOMMAND_NOTIFY_TAB_INTERNALLY_MOVED = 'ws:notify-tab-internally-moved';
export const kCOMMAND_NOTIFY_TAB_REMOVING = 'ws:notify-tab-removing';
export const kCOMMAND_NOTIFY_TAB_REMOVED = 'ws:notify-tab-removed';
export const kCOMMAND_NOTIFY_TAB_ACTIVATING = 'ws:notify-tab-activating';
export const kCOMMAND_NOTIFY_TAB_ACTIVATED = 'ws:notify-tab-activated';
export const kCOMMAND_NOTIFY_MAY_START_TAB_SWITCH = 'ws:notify-may-start-tab-switch';
export const kCOMMAND_NOTIFY_MAY_END_TAB_SWITCH = 'ws:notify-may-end-tab-switch';
export const kCOMMAND_NOTIFY_TAB_ATTACHED_TO_WINDOW = 'ws:notify-tab-attached-to-window';
export const kCOMMAND_NOTIFY_TAB_DETACHED_FROM_WINDOW = 'ws:notify-tab-detached-from-window';
export const kCOMMAND_NOTIFY_PERMISSIONS_GRANTED = 'ws:notify-permissions-granted';
export const kCOMMAND_NOTIFY_TAB_PINNED = 'ws:notify-tab-pinned';
export const kCOMMAND_NOTIFY_TAB_UNPINNED = 'ws:notify-tab-unpinned';
export const kCOMMAND_NOTIFY_TAB_SHOWN = 'ws:notify-tab-shown';
export const kCOMMAND_NOTIFY_TAB_HIDDEN = 'ws:notify-tab-hidden';
export const kCOMMAND_NOTIFY_TAB_RESTORING = 'ws:notify-tab-restoring';
export const kCOMMAND_NOTIFY_TAB_RESTORED = 'ws:notify-tab-restored';
export const kCOMMAND_NOTIFY_TREE_ITEM_LABEL_UPDATED = 'ws:notify-tab-label-updated';
export const kCOMMAND_NOTIFY_TAB_FAVICON_UPDATED = 'ws:notify-tab-favicon-updated';
export const kCOMMAND_NOTIFY_TAB_SOUND_STATE_UPDATED = 'ws:notify-tab-sound-state-updated';
export const kCOMMAND_NOTIFY_TAB_SHARING_STATE_UPDATED = 'ws:notify-tab-sharing-state-updated';
export const kCOMMAND_NOTIFY_TABS_CLOSING = 'ws:notify-tabs-closing';
export const kCOMMAND_NOTIFY_HIGHLIGHTED_TABS_CHANGED = 'ws:notify-highlighted-tabs-changed';
export const kCOMMAND_NOTIFY_TABS_HIGHLIGHTING_IN_PROGRESS = 'ws:notify-tabs-highlighting-in-progress';
export const kCOMMAND_NOTIFY_TABS_HIGHLIGHTING_COMPLETE = 'ws:notify-tabs-highlighting-complete';
export const kCOMMAND_NOTIFY_GROUP_TAB_DETECTED = 'ws:notify-group-tab-detected';
export const kCOMMAND_NOTIFY_CHILDREN_CHANGED = 'ws:notify-children-changed';
export const kCOMMAND_NOTIFY_TAB_COLLAPSED_STATE_CHANGED = 'ws:notify-tab-collapsed-state-changed';
export const kCOMMAND_NOTIFY_SUBTREE_COLLAPSED_STATE_CHANGING = 'ws:notify-subtree-collapsed-state-changing';
export const kCOMMAND_NOTIFY_SUBTREE_COLLAPSED_STATE_CHANGED = 'ws:notify-subtree-collapsed-state-changed';
export const kCOMMAND_NOTIFY_TAB_GROUP_CREATED = 'ws:notify-tab-group-created';
export const kCOMMAND_NOTIFY_TAB_GROUP_UPDATED = 'ws:notify-tab-group-updated';
export const kCOMMAND_NOTIFY_TAB_GROUP_REMOVED = 'ws:notify-tab-group-removed';
export const kCOMMAND_SET_SUBTREE_COLLAPSED_STATE = 'ws:set-subtree-collapsed-state';
export const kCOMMAND_SET_SUBTREE_COLLAPSED_STATE_INTELLIGENTLY_FOR = 'ws:set-subtree-collapsed-state-intelligently-for';
export const kCOMMAND_TOGGLE_STICKY = 'ws:toggle-sticky';
export const kCOMMAND_NOTIFY_TAB_LEVEL_CHANGED = 'ws:notify-tab-level-changed';
export const kCOMMAND_NOTIFY_TAB_ATTACHED_COMPLETELY = 'ws:notify-tab-attached-completely';
export const kCOMMAND_BROADCAST_CURRENT_DRAG_DATA = 'ws:broadcast-current-drag-data';
export const kCOMMAND_SHOW_CONTAINER_SELECTOR = 'ws:show-container-selector';
export const kCOMMAND_SCROLL_TABBAR = 'ws:scroll-tabbar';
export const kCOMMAND_TOGGLE_SUBPANEL = 'ws:toggle-subpanel';
export const kCOMMAND_SWITCH_SUBPANEL = 'ws:switch-subpanel';
export const kCOMMAND_INCREASE_SUBPANEL = 'ws:increase-subpanel';
export const kCOMMAND_DECREASE_SUBPANEL = 'ws:decrease-subpanel';
export const kCOMMAND_REQUEST_QUERY_LOGS = 'ws:request-query-logs';
export const kCOMMAND_RESPONSE_QUERY_LOGS = 'ws:response-query-logs';
export const kCOMMAND_REQUEST_CONNECTION_MESSAGE_LOGS = 'ws:request-connection-message-logs';
export const kCOMMAND_RESPONSE_CONNECTION_MESSAGE_LOGS = 'ws:response-connection-message-logs';
export const kCOMMAND_NOTIFY_TEST_KEY_CHANGED = 'ws:notify-test-key-changed';
export const kCOMMAND_SIMULATE_SIDEBAR_MESSAGE = 'ws:simulate-sidebar-message';
export const kCOMMAND_GET_CONTEXT_MENU_ITEMS = 'ws:contextMenu-get-items';
export const kCOMMAND_NOTIFY_CONTEXT_MENU_UPDATED = 'ws:contextMenu-updated';
export const kCOMMAND_NOTIFY_CONTEXT_ITEM_CHECKED_STATUS_CHANGED = 'ws:contextMenu-item-checked-status-changed';
export const kCOMMAND_NOTIFY_CONTEXT_OVERRIDDEN = 'ws:notify-context-overridden';
export const kCOMMAND_AUTODETECT_DUPLICATED_TAB_DETECTION_DELAY = 'ws:autodetect-duplicated-tab-detection-delay';
export const kCOMMAND_TEST_DUPLICATED_TAB_DETECTION = 'ws:test-duplicated-tab-detection';
export const kCOMMAND_WAIT_UNTIL_SUCCESSORS_UPDATED = 'ws:wait-until-successors-updated';
export const kCOMMAND_GET_SIDEBAR_POSITION = 'ws:get-sidebar-position';
export const kCOMMAND_GET_ABOVE_TAB = 'ws:get-above-tab';
export const kCOMMAND_GET_BELOW_TAB = 'ws:get-below-tab';
export const kCOMMAND_GET_LEFT_TAB = 'ws:get-left-tab';
export const kCOMMAND_GET_RIGHT_TAB = 'ws:get-right-tab';
export const kCOMMAND_GET_BOUNDING_CLIENT_RECT = 'ws:get-bounding-client-rect';
export const kCOMMAND_SHOW_NATIVE_TAB_GROUP_MENU_PANEL = 'ws:show-native-tab-group-menu-panel';
export const kCOMMAND_UPDATE_NATIVE_TAB_GROUP = 'ws:update-native-tab-group';
export const kCOMMAND_INVOKE_NATIVE_TAB_GROUP_MENU_PANEL_COMMAND = 'ws:invoke-native-tab-group-menu-panel-command';
export const kCOMMAND_NEW_WINDOW_FROM_NATIVE_TAB_GROUP = 'ws:new-window-from-native-tab-group';
export const kCOMMAND_ACTIVATE_TAB = 'ws:activate-tab';
export const kCOMMAND_HIGHLIGHT_TABS = 'ws:highlight-tabs';
export const kCOMMAND_TOGGLE_MUTED_FROM_SOUND_BUTTON = 'ws:toggle-muted-from-sound-button';
export const kCOMMAND_UNBLOCK_AUTOPLAY_FROM_SOUND_BUTTON = 'ws:unblock-autoplay-from-sound-button';
export const kCOMMAND_PERFORM_TABS_DRAG_DROP = 'ws:perform-tabs-drag-drop';
export const kCOMMAND_BLOCK_USER_OPERATIONS = 'ws:block-user-operations';
export const kCOMMAND_UNBLOCK_USER_OPERATIONS = 'ws:unblock-user-operations';
export const kCOMMAND_PROGRESS_USER_OPERATIONS = 'ws:progress-user-operations';
export const kCOMMAND_BROADCAST_TAB_STATE = 'ws:broadcast-tab-state';
export const kCOMMAND_BROADCAST_TAB_TOOLTIP_TEXT = 'ws:broadcast-tab-tooltip-text';
export const kCOMMAND_BROADCAST_TAB_AUTO_STICKY_STATE = 'ws:broadcast-tab-auto-sticky-state';
export const kCOMMAND_BOOKMARK_TAB_WITH_DIALOG = 'ws:bookmark-tab-with-dialog';
export const kCOMMAND_BOOKMARK_TABS_WITH_DIALOG = 'ws:bookmark-tabs-with-dialog';
export const kNOTIFY_TAB_MOUSEDOWN = 'ws:tab-mousedown';
export const kNOTIFY_TAB_MOUSEDOWN_EXPIRED = 'ws:tab-mousedown-expired';
export const kNOTIFY_SIDEBAR_FOCUS = 'ws:sidebar-focus';
export const kNOTIFY_SIDEBAR_BLUR = 'ws:sidebar-blur';
export const kNOTIFY_CONFIRMATION_DIALOG_READY = 'ws:confirmation-dialog-ready';
export const kCONNECTION_HEARTBEAT = 'ws:connection-heartbeat';
export const kAPI_TAB_ID = 'data-tab-id';
export const kAPI_WINDOW_ID = 'data-window-id';
export const kAPI_NATIVE_TAB_GROUP_ID = 'data-native-tab-group-id';
export const kGROUP_ID = 'data-group-id';
export const kPARENT = 'data-parent-id';
export const kCHILDREN = 'data-child-ids';
export const kLEVEL = 'data-level';
export const kCLOSED_SET_ID = 'data-closed-set-id';
export const kCURRENT_URI = 'data-current-uri';
export const kCURRENT_FAVICON_URI = 'data-current-favicon-uri';
export const kCONTEXTUAL_IDENTITY_NAME = 'data-contextual-identity-name';
export const kMAX_TREE_LEVEL = 'data-max-tree-level';
export const kLABEL_OVERFLOW = 'data-label-overflow';
export const kPERSISTENT_ID = 'data-persistent-id';
export const kPERSISTENT_ANCESTORS = 'ancestors';
export const kPERSISTENT_CHILDREN = 'children';
export const kPERSISTENT_INSERT_BEFORE = 'insert-before';
export const kPERSISTENT_INSERT_AFTER = 'insert-after';
export const kPERSISTENT_INSERT_AFTER_LEGACY = 'isnert-after';
export const kPERSISTENT_STATES = 'special-tab-states';
export const kPERSISTENT_SUBTREE_COLLAPSED = 'subtree-collapsed'; // obsolete
export const kPERSISTENT_ORIGINAL_OPENER_TAB_ID = 'data-original-opener-tab-id';
export const kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER = 'data-already-grouped-for-pinned-opener';
export const kFAVICON_IMAGE = 'favicon-image';
export const kFAVICON_BUILTIN = 'favicon-builtin';
export const kFAVICON_DEFAULT = 'favicon-default'; // just for backward compatibility, and this should be removed from future versions
export const kFAVICON_SHARING_STATE = 'favicon-sharing-state';
export const kFAVICON_STICKY_STATE = 'favicon-sticky-state';
export const kBACKGROUND = 'background';
export const kTHROBBER = 'throbber';
export const kHIGHLIGHTER = 'highlighter';
export const kBURSTER = 'burster';
export const kNEWTAB_BUTTON = 'newtab-button';
export const kEXTRA_ITEMS_CONTAINER = 'extra-items-container';
export const kCONTEXTUAL_IDENTITY_MARKER = 'contextual-identity-marker';
export const kCONTEXTUAL_IDENTITY_SELECTOR = 'contextual-identities-selector';
export const kCONTEXTUAL_IDENTITY_SELECTOR_CONTEXT_MENU = 'contextual-identities-selector-context';
export const kNEWTAB_ACTION_SELECTOR = 'newtab-action-selector';
export const kTABBAR_SPACER = 'tabs-spacer';
export const kTAB_STATE_ACTIVE = 'active';
export const kTAB_STATE_PINNED = 'pinned';
export const kTAB_STATE_LAST_ROW = 'last-row';
export const kTAB_STATE_LAST_VISIBLE = 'last-visible';
export const kTAB_STATE_AUDIBLE = 'audible';
export const kTAB_STATE_SOUND_PLAYING = 'sound-playing';
export const kTAB_STATE_HAS_SOUND_PLAYING_MEMBER = 'has-sound-playing-member';
export const kTAB_STATE_MUTED = 'muted';
export const kTAB_STATE_HAS_MUTED_MEMBER = 'has-muted-member';
export const kTAB_STATE_AUTOPLAY_BLOCKED = 'autoplay-blocked';
export const kTAB_STATE_HAS_AUTOPLAY_BLOCKED_MEMBER = 'has-autoplay-blocked-member';
export const kTAB_STATE_PRIVATE_BROWSING = 'private-browsing';
export const kTAB_STATE_HIDDEN = 'hidden';
export const kTAB_STATE_SHARING_CAMERA = 'sharing-camera';
export const kTAB_STATE_SHARING_MICROPHONE = 'sharing-microphone';
export const kTAB_STATE_SHARING_SCREEN = 'sharing-screen';
export const kTAB_STATE_HAS_SHARING_CAMERA_MEMBER = 'has-sharing-camera-member';
export const kTAB_STATE_HAS_SHARING_MICROPHONE_MEMBER = 'has-sharing-microphone-member';
export const kTAB_STATE_HAS_SHARING_SCREEN_MEMBER = 'has-sharing-screen-member';
export const kTAB_STATE_ANIMATION_READY = 'animation-ready';
export const kTAB_STATE_NOT_ACTIVATED_SINCE_LOAD = 'not-activated-since-load';
export const kTAB_STATE_BURSTING = 'bursting';
export const kTAB_STATE_CREATING = 'creating';
export const kTAB_STATE_TO_BE_REMOVED = 'to-be-removed';
export const kTAB_STATE_REMOVING = 'removing';
export const kTAB_STATE_COLLAPSED = 'collapsed';
export const kTAB_STATE_COLLAPSED_DONE = 'collapsed-completely';
export const kTAB_STATE_COLLAPSING = 'collapsing';
export const kTAB_STATE_EXPANDING = 'expanding';
export const kTAB_STATE_MOVING = 'moving';
export const kTAB_STATE_SHOWING = 'showing';
export const kTAB_STATE_SUBTREE_COLLAPSED = 'subtree-collapsed';
export const kTAB_STATE_SUBTREE_EXPANDED_MANUALLY = 'subtree-expanded-manually';
export const kTAB_STATE_FAVICONIZED = 'faviconized';
export const kTAB_STATE_UNREAD = 'unread';
export const kTAB_STATE_PENDING = 'pending';
export const kTAB_STATE_HIGHLIGHTED = 'highlighted';
export const kTAB_STATE_BUNDLED_ACTIVE = 'bundled-active';
export const kTAB_STATE_SOME_DESCENDANTS_HIGHLIGHTED = 'some-descendants-highlighted';
export const kTAB_STATE_ALL_DESCENDANTS_HIGHLIGHTED = 'all-descendants-highlighted';
export const kTAB_STATE_ATTENTION = 'attention';
export const kTAB_STATE_DISCARDED = 'discarded';
export const kTAB_STATE_SELECTED = 'selected';
export const kTAB_STATE_DRAGGING = 'dragging';
export const kTAB_STATE_DUPLICATING = 'duplicating';
export const kTAB_STATE_DUPLICATED = 'duplicated';
export const kTAB_STATE_RESTORED = 'restored';
export const kTAB_STATE_FROM_EXTERNAL = 'from-external';
export const kTAB_STATE_FROM_FIREFOX_VIEW = 'from-firefox-view';
export const kTAB_STATE_THROBBER_UNSYNCHRONIZED = 'throbber-unsynchronized';
export const kTAB_STATE_GROUP_TAB = 'group-tab';
export const kTAB_STATE_NEW_TAB_COMMAND_TAB = 'newtab-command-tab';
export const kTAB_STATE_OPENED_FOR_SAME_WEBSITE = 'opened-for-same-website';
export const kTAB_STATE_STICKY = 'sticky';
export const kTAB_STATE_STUCK = 'stuck'; // virtual state
export const kTAB_INTERNAL_STATES = new Set([ // TST specific states
'tab',
kTAB_STATE_LAST_ROW,
kTAB_STATE_LAST_VISIBLE,
kTAB_STATE_ANIMATION_READY,
kTAB_STATE_COLLAPSED_DONE,
kTAB_STATE_CREATING,
kTAB_STATE_DUPLICATING,
kTAB_STATE_COLLAPSING,
kTAB_STATE_EXPANDING,
kTAB_STATE_MOVING,
kTAB_STATE_TO_BE_REMOVED,
kTAB_STATE_REMOVING,
kTAB_STATE_SHOWING,
kTAB_STATE_THROBBER_UNSYNCHRONIZED,
kTAB_STATE_NEW_TAB_COMMAND_TAB,
kTAB_STATE_DUPLICATED,
kTAB_STATE_RESTORED,
kTAB_STATE_FROM_EXTERNAL,
kTAB_STATE_FROM_FIREFOX_VIEW,
kTAB_STATE_OPENED_FOR_SAME_WEBSITE,
kTAB_STATE_STICKY,
kTAB_STATE_STUCK,
kTAB_STATE_PENDING,
]);
export const kTAB_TEMPORARY_STATES = new Set([ // states not trigger updating of cache
kTAB_STATE_CREATING,
kTAB_STATE_BURSTING,
kTAB_STATE_COLLAPSING,
kTAB_STATE_DUPLICATING,
kTAB_STATE_EXPANDING,
kTAB_STATE_MOVING,
kTAB_STATE_TO_BE_REMOVED,
kTAB_STATE_REMOVING,
kTAB_STATE_SHOWING,
kTAB_STATE_NEW_TAB_COMMAND_TAB,
kTAB_STATE_DUPLICATED,
kTAB_STATE_RESTORED,
kTAB_STATE_FROM_EXTERNAL,
kTAB_STATE_FROM_FIREFOX_VIEW,
kTAB_STATE_OPENED_FOR_SAME_WEBSITE,
kTAB_STATE_PENDING,
]);
export const kTAB_SAFE_STATES = new Set([ // exportable via API
kTAB_STATE_COLLAPSED,
kTAB_STATE_SUBTREE_COLLAPSED,
kTAB_STATE_GROUP_TAB,
kTAB_STATE_CREATING,
kTAB_STATE_NEW_TAB_COMMAND_TAB,
kTAB_STATE_DUPLICATED,
kTAB_STATE_RESTORED,
kTAB_STATE_FROM_EXTERNAL,
kTAB_STATE_FROM_FIREFOX_VIEW,
kTAB_STATE_OPENED_FOR_SAME_WEBSITE,
kTAB_STATE_STICKY,
kTAB_STATE_STUCK,
]);
export const kTAB_SAFE_STATES_ARRAY = Array.from(kTAB_SAFE_STATES);
export const kTABBAR_STATE_OVERFLOW = 'overflow';
export const kTABBAR_STATE_SCROLLED = 'scrolled';
export const kTABBAR_STATE_FULLY_SCROLLED = 'fully-scrolled';
export const kTABBAR_STATE_BLOCKING = 'blocking';
export const kTABBAR_STATE_BLOCKING_WITH_THROBBER = 'blocking-throbber';
export const kTABBAR_STATE_BLOCKING_WITH_SHADE = 'blocking-shade';
export const kTABBAR_STATE_HAVE_LOADING_TAB = 'have-loading-tab';
export const kTABBAR_STATE_HAVE_UNSYNCHRONIZED_THROBBER = 'have-unsynchronized-throbber';
export const kTABBAR_STATE_THROBBER_SYNCHRONIZING = 'throbber-synchronizing';
export const kTABBAR_STATE_CONTEXTUAL_IDENTITY_SELECTABLE = 'contextual-identity-selectable';
export const kTABBAR_STATE_NEWTAB_ACTION_SELECTABLE = 'newtab-action-selectable';
export const kTABBAR_STATE_MULTIPLE_HIGHLIGHTED = 'mutiple-highlighted';
export const kTABBAR_STATE_HAS_VISUAL_GAP = 'has-visual-gap';
export const kTABBAR_STATE_HOVER_ON_TOP_EDGE = 'hover-on-top-edge';
export const kTABBAR_STATE_SCROLLBAR_AUTOHIDE = 'scrollbar-autohide';
export const kTABBAR_STATE_FAVICONIZE_PINNED_TABS = 'faviconize-pinned-tabs';
export const kTABBAR_STATE_TAB_DRAGGING = 'tab-dragging';
export const kTABBAR_STATE_LINK_DRAGGING = 'link-dragging';
export const kWINDOW_STATE_TREE_STRUCTURE = 'tree-structure';
export const kWINDOW_STATE_SCROLL_POSITION = 'scroll-position';
export const kWINDOW_STATE_SUBPANEL_PROVIDER_ID = 'subpanel-provider-id';
export const kWINDOW_STATE_SUBPANEL_HEIGHT = 'subpanel-height';
export const kWINDOW_STATE_SUBPANEL_EFFECTIVE_HEIGHT = 'subpanel-effective-height';
export const kWINDOW_STATE_CACHED_TABS = 'cached-tabs';
export const kWINDOW_STATE_CACHED_SIDEBAR = 'cached-sidebar-contents';
export const kWINDOW_STATE_CACHED_SIDEBAR_CONTENTS = 'cached-sidebar-contents:contents';
export const kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY = 'cached-sidebar-contents:tabs-dirty';
export const kWINDOW_STATE_CACHED_SIDEBAR_COLLAPSED_DIRTY = 'cached-sidebar-contents:collapsed-dirty';
export const kCACHE_KEYS = [
kWINDOW_STATE_CACHED_TABS,
kWINDOW_STATE_CACHED_SIDEBAR,
kWINDOW_STATE_CACHED_SIDEBAR_CONTENTS,
kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY,
kWINDOW_STATE_CACHED_SIDEBAR_COLLAPSED_DIRTY,
];
export const kCOUNTER_ROLE_ALL_TABS = 1;
export const kCOUNTER_ROLE_CONTAINED_TABS = 2;
export const kTABBAR_POSITION_AUTO = 0;
export const kTABBAR_POSITION_LEFT = 1;
export const kTABBAR_POSITION_RIGHT = 2;
export const kACTION_MOVE = 1 << 0;
export const kACTION_ATTACH = 1 << 10;
export const kACTION_DETACH = 1 << 11;
export const kDROPLINK_ASK = 0;
export const kDROPLINK_LOAD = 1 << 0;
export const kDROPLINK_NEWTAB = 1 << 1;
export const kGROUP_BOOKMARK_ASK = 0;
export const kGROUP_BOOKMARK_SUBTREE = 1 << 0;
export const kGROUP_BOOKMARK_SEPARATE = 1 << 1;
export const kGROUP_BOOKMARK_FIXED = kGROUP_BOOKMARK_SUBTREE | kGROUP_BOOKMARK_SEPARATE;
export const kGROUP_BOOKMARK_USE_DUMMY = 1 << 8;
export const kGROUP_BOOKMARK_USE_DUMMY_FORCE = 1 << 10;
export const kGROUP_BOOKMARK_DONT_RESTORE_TREE_STRUCTURE = 1 << 9;
export const kGROUP_BOOKMARK_EXPAND_ALL_TREE = 1 << 11;
export const kGROUP_BOOKMARK_CANCEL = -1;
export const kGROUP_TAB_TEMPORARY_STATE_NOTHING = 0;
export const kGROUP_TAB_TEMPORARY_STATE_PASSIVE = 1;
export const kGROUP_TAB_TEMPORARY_STATE_AGGRESSIVE = 2;
export const kPARENT_TAB_OPERATION_BEHAVIOR_MODE_PARALLEL = 0;
export const kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CONSISTENT = 1;
export const kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CUSTOM = -1;
export const kPARENT_TAB_OPERATION_CONTEXT_CLOSE = 1;
export const kPARENT_TAB_OPERATION_CONTEXT_MOVE = 2;
export const kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE = 2;
export const kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD = 3;
export const kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN = 0;
export const kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY = 6;
export const kPARENT_TAB_OPERATION_BEHAVIOR_DETACH_ALL_CHILDREN = 1;
export const kPARENT_TAB_OPERATION_BEHAVIOR_SIMPLY_DETACH_ALL_CHILDREN = 4; // just for internal use
export const kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB = 5;
// just for migration from old versions
export const kPARENT_TAB_BEHAVIOR_ALWAYS = 0;
export const kPARENT_TAB_BEHAVIOR_ONLY_WHEN_VISIBLE = 1;
export const kPARENT_TAB_BEHAVIOR_ONLY_ON_SIDEBAR = 2;
export const kINSERT_NO_CONTROL = -1;
export const kINSERT_INHERIT = -2;
export const kINSERT_TOP = 0;
export const kINSERT_END = 1;
export const kINSERT_NEAREST = 2;
export const kINSERT_NEXT_TO_LAST_RELATED_TAB = 3;
export const kCONTROLLED_INSERTION_POSITION = new Set([
kINSERT_TOP,
kINSERT_NEAREST,
kINSERT_NEXT_TO_LAST_RELATED_TAB,
]);
export const kSUCCESSOR_TAB_CONTROL_NEVER = 0;
export const kSUCCESSOR_TAB_CONTROL_SIMULATE_DEFAULT = 1;
export const kSUCCESSOR_TAB_CONTROL_IN_TREE = 2;
export const kTREE_DOUBLE_CLICK_BEHAVIOR_NONE = 0;
export const kTREE_DOUBLE_CLICK_BEHAVIOR_TOGGLE_COLLAPSED = 1;
export const kTREE_DOUBLE_CLICK_BEHAVIOR_TOGGLE_STICKY = 4;
export const kTREE_DOUBLE_CLICK_BEHAVIOR_CLOSE = 3;
// 2 is a retired number for a removed feature
export const kDRAG_BEHAVIOR_NONE = 0;
export const kDRAG_BEHAVIOR_ENTIRE_TREE = 1 << 0;
export const kDRAG_BEHAVIOR_ALLOW_BOOKMARK = 1 << 1;
export const kDRAG_BEHAVIOR_TEAR_OFF = 1 << 2;
export const kDRAG_BEHAVIOR_MOVE = 1 << 3;
export const kNEWTAB_DO_NOTHING = -1;
export const kNEWTAB_OPEN_AS_ORPHAN = 0;
export const kNEWTAB_OPEN_AS_CHILD = 1; // no control about position
export const kNEWTAB_OPEN_AS_CHILD_NEXT_TO_LAST_RELATED_TAB = 5;
export const kNEWTAB_OPEN_AS_CHILD_TOP = 6;
export const kNEWTAB_OPEN_AS_CHILD_END = 7;
export const kNEWTAB_OPEN_AS_SIBLING = 2;
export const kNEWTAB_OPEN_AS_NEXT_SIBLING = 3;
export const kNEWTAB_OPEN_AS_NEXT_SIBLING_WITH_INHERITED_CONTAINER = 4;
export const kCONTROLLED_NEWTAB_POSITION = new Set([
kNEWTAB_OPEN_AS_CHILD,
kNEWTAB_OPEN_AS_CHILD_NEXT_TO_LAST_RELATED_TAB,
kNEWTAB_OPEN_AS_CHILD_TOP,
kNEWTAB_OPEN_AS_CHILD_END,
kNEWTAB_OPEN_AS_SIBLING,
kNEWTAB_OPEN_AS_NEXT_SIBLING,
kNEWTAB_OPEN_AS_NEXT_SIBLING_WITH_INHERITED_CONTAINER,
]);
export const kCONTEXTUAL_IDENTITY_DEFAULT = 0;
export const kCONTEXTUAL_IDENTITY_FROM_PARENT = 1;
export const kCONTEXTUAL_IDENTITY_FROM_LAST_ACTIVE = 2;
export const kSCROLL_TO_NEW_TAB_IGNORE = 0;
export const kSCROLL_TO_NEW_TAB_IF_POSSIBLE = 1;
export const kTABBAR_UPDATE_REASON_RESIZE = 1 << 0;
export const kTABBAR_UPDATE_REASON_COLLAPSE = 1 << 1;
export const kTABBAR_UPDATE_REASON_EXPAND = 1 << 2;
export const kTABBAR_UPDATE_REASON_ANIMATION_END = 1 << 3;
export const kTABBAR_UPDATE_REASON_TAB_OPEN = 1 << 4;
export const kTABBAR_UPDATE_REASON_TAB_CLOSE = 1 << 5;
export const kTABBAR_UPDATE_REASON_TAB_MOVE = 1 << 6;
export const kTABBAR_UPDATE_REASON_VIRTUAL_SCROLL_VIEWPORT_UPDATE = 1 << 7;
export const kDEFAULT_MIN_INDENT = 3;
export const kGROUP_TAB_URI = browser.runtime.getURL('resources/group-tab.html');
export const kGROUP_TAB_DEFAULT_TITLE_MATCHER = new RegExp(`^${browser.i18n.getMessage('groupTab_label', '.+')}$`);
export const kGROUP_TAB_FROM_PINNED_DEFAULT_TITLE_MATCHER = new RegExp(`^${browser.i18n.getMessage('groupTab_fromPinnedTab_label', '.+')}$`);
export const kSHORTHAND_CUSTOM_URI = /^ext\+ws:([^:?#]+)(?:[:?]([^#]*))?(#.*)?$/;
export const kSHORTHAND_ABOUT_URI = /^about:ws-([^?]+)/;
export const kSHORTHAND_URIS = {
tabbar: browser.runtime.getURL('sidebar/sidebar.html'),
group: kGROUP_TAB_URI,
options: browser.runtime.getURL('options/options.html?independent=true'),
startup: browser.runtime.getURL('resources/startup.html'),
testRunner: browser.runtime.getURL('tests/runner.html'),
'test-runner': browser.runtime.getURL('tests/runner.html')
};
export const kINSERTION_CONTEXT_MOVED = 1;
export const kINSERTION_CONTEXT_SHOWN = 2;
export const kINSERTION_CONTEXT_CREATED = 3;
export const kIN_CONTENT_PANEL_RENDER_NOWHERE = 0;
export const kIN_CONTENT_PANEL_RENDER_IN_SIDEBAR = 1 << 0;
export const kIN_CONTENT_PANEL_RENDER_IN_CONTENT = 1 << 1;
export const kIN_CONTENT_PANEL_RENDER_IN_ANYWHERE = kIN_CONTENT_PANEL_RENDER_IN_SIDEBAR | kIN_CONTENT_PANEL_RENDER_IN_CONTENT;
export const kAGGRESSIVE_OPENER_TAB_DETECTION_RULES_WITH_URL = [
{ opener: /^about:addons/,
child: /^https:\/\/addons.mozilla.org\/([^\/]+\/)?[^\/]+\/search\// }
];
export const kNOTIFICATION_DEFAULT_ICON = '/resources/64x64.svg#default-bright';
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/sync
// Use 6 * 1024 instead of 8 * 1024 (max of the quota) for safety.
// For example, 6 * 8 = 48KB is the max size of the user style rules.
export const kSYNC_STORAGE_SAFE_QUOTA = 6 * 1024;
export const kSYNC_DATA_TYPE_TABS = 'tabs';
export const IS_BACKGROUND = location.href.startsWith(browser.runtime.getURL('background/background.html'));
export const IS_SIDEBAR = location.href.startsWith(browser.runtime.getURL('sidebar/sidebar.html'));

View File

@@ -0,0 +1,218 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
configs,
log as internalLogger,
isWindows,
} from './common.js';
import * as ApiTabs from '/common/api-tabs.js';
// eslint-disable-next-line no-unused-vars
function log(...args) {
internalLogger('common/contextual-identities', ...args);
}
const mContextualIdentities = new Map();
let mIsObserving = false;
export function get(id) {
return mContextualIdentities.get(id);
}
export function getIdFromName(name) {
for (const identity of mContextualIdentities.values()) {
if (identity.name.toLowerCase() == name.toLowerCase())
return identity.cookieStoreId;
}
return null;
}
// Respect container type stored by Container Bookmarks
// https://addons.mozilla.org/firefox/addon/container-bookmarks/
export function getIdFromBookmark(bookmark) {
const containerMatcher = new RegExp(`#${configs.containerRedirectKey}-(.+)$`);
const matchedContainer = bookmark.url.match(containerMatcher);
if (!matchedContainer)
return {};
const idPart = matchedContainer[matchedContainer.length-1];
const url = bookmark.url.replace(containerMatcher, '');
// old method
const identity = mContextualIdentities.get(decodeURIComponent(idPart));
if (identity) {
return {
cookieStoreId: identity.cookieStoreId,
url,
};
}
for (const [cookieStoreId, identity] of mContextualIdentities.entries()) {
if (idPart != encodeURIComponent(identity.name.toLowerCase().replace(/\s/g, '-')))
continue;
return {
cookieStoreId,
url,
};
}
return {};
}
export function getColorInfo() {
const colors = {};
const customColors = [];
forEach(identity => {
if (!identity.colorCode)
return;
let colorValue = identity.colorCode;
if (identity.color) {
const customColor = `--contextual-identity-color-${identity.color}`;
customColors.push(`${customColor}: ${identity.colorCode};`);
colorValue = `var(${customColor})`;
}
colors[identity.cookieStoreId] = colorValue;
});
return {
colors,
colorDeclarations: customColors.length > 0 ? `:root { ${customColors.join('\n')} }` : ''
};
}
export function getCount() {
return mContextualIdentities.size;
}
export function forEach(callback) {
for (const identity of mContextualIdentities.values()) {
callback(identity);
}
}
if (browser.contextualIdentities) { // already granted
browser.contextualIdentities.onCreated.addListener(onContextualIdentityCreated);
browser.contextualIdentities.onRemoved.addListener(onContextualIdentityRemoved);
browser.contextualIdentities.onUpdated.addListener(onContextualIdentityUpdated);
mIsObserving = true;
}
export function startObserve() {
if (!browser.contextualIdentities ||
mIsObserving)
return;
mIsObserving = true;
browser.contextualIdentities.onCreated.addListener(onContextualIdentityCreated);
browser.contextualIdentities.onRemoved.addListener(onContextualIdentityRemoved);
browser.contextualIdentities.onUpdated.addListener(onContextualIdentityUpdated);
}
export function endObserve() {
if (!browser.contextualIdentities ||
!mIsObserving)
return;
browser.contextualIdentities.onCreated.removeListener(onContextualIdentityCreated);
browser.contextualIdentities.onRemoved.removeListener(onContextualIdentityRemoved);
browser.contextualIdentities.onUpdated.removeListener(onContextualIdentityUpdated);
}
export async function init() {
if (!browser.contextualIdentities)
return;
const identities = await browser.contextualIdentities.query({}).catch(ApiTabs.createErrorHandler());
for (const identity of identities) {
mContextualIdentities.set(identity.cookieStoreId, fixupIcon(identity));
}
}
function fixupIcon(identity) {
if (identity.icon && identity.color)
identity.iconUrl = `/resources/icons/contextual-identities/${identity.icon}.svg#${safeColor(identity.color)}`;
return identity;
}
const mDarkModeMedia = window.matchMedia('(prefers-color-scheme: dark)');
mDarkModeMedia.addListener(async _event => {
await init();
forEach(identity => onContextualIdentityUpdated({ contextualIdentity: identity }));
});
function safeColor(color) {
switch (color) {
case 'blue':
case 'turquoise':
case 'green':
case 'yellow':
case 'orange':
case 'red':
case 'pink':
case 'purple':
return color;
case 'toolbar':
default:
return !isWindows() && mDarkModeMedia.matches ? 'toolbar-dark' : 'toolbar-light';
}
}
export const onUpdated = new EventListenerManager();
function onContextualIdentityCreated(createdInfo) {
if (!mIsObserving)
return;
const identity = createdInfo.contextualIdentity;
mContextualIdentities.set(identity.cookieStoreId, fixupIcon(identity));
onUpdated.dispatch();
}
function onContextualIdentityRemoved(removedInfo) {
if (!mIsObserving)
return;
const identity = removedInfo.contextualIdentity;
delete mContextualIdentities.delete(identity.cookieStoreId);
onUpdated.dispatch();
}
function onContextualIdentityUpdated(updatedInfo) {
if (!mIsObserving)
return;
const identity = updatedInfo.contextualIdentity;
mContextualIdentities.set(identity.cookieStoreId, fixupIcon(identity));
onUpdated.dispatch();
}
export function generateMenuItems({ hasDefault } = {}) {
const fragment = document.createDocumentFragment();
if (hasDefault) {
const defaultCotnainerItem = document.createElement('li');
defaultCotnainerItem.dataset.value = 'firefox-default';
defaultCotnainerItem.textContent = browser.i18n.getMessage('tabbar_newTabWithContexualIdentity_default');
fragment.appendChild(defaultCotnainerItem);
const separator = document.createElement('li');
separator.classList.add('separator');
fragment.appendChild(separator);
}
forEach(identity => {
const item = document.createElement('li');
item.dataset.value = identity.cookieStoreId;
item.textContent = identity.name;
item.dataset.icon = identity.iconUrl;
fragment.appendChild(item);
});
return fragment;
}

View File

@@ -0,0 +1,142 @@
/*
Original: https://github.com/joakimbeng/split-css-selector
MIT License © Joakim Carlstein
*/
'use strict';
export function splitSelectors(selectors) {
if (isAtRule(selectors))
return [selectors];
return split(selectors, {
splitter: ',',
appendSplitter: false,
});
}
export function splitSelectorParts(selector) {
if (isAtRule(selector))
return [selector];
return split(selector, {
splitters: [
' ', '\t', '\n', // https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_combinator
'>', // https://developer.mozilla.org/en-US/docs/Web/CSS/Child_combinator
'+', // https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_combinator
'~', // https://developer.mozilla.org/en-US/docs/Web/CSS/General_sibling_combinator
// '||', // https://developer.mozilla.org/en-US/docs/Web/CSS/Column_combinator
],
appendSplitter: true,
}).filter(part => part !== '');
}
function split(input, { splitter, splitters, appendSplitter }) {
const splittersSet = splitters && new Set(splitters);
const splitted = [];
let parens = 0;
let angulars = 0;
let soFar = '';
let escaped = false;
let singleQuoted = false;
let doubleQuoted = false;
for (const char of input) {
if (escaped) {
soFar += char;
escaped = false;
continue;
}
if (char === '\\' && !escaped) {
soFar += char;
escaped = true;
continue;
}
if (char === "'") {
if (singleQuoted)
singleQuoted = false;
else if (!doubleQuoted)
singleQuoted = true;
}
else if (char === '"') {
if (doubleQuoted)
doubleQuoted = false;
else if (!singleQuoted)
doubleQuoted = true;
}
else if (char === '(') {
parens++;
}
else if (char === ')') {
parens--;
}
else if (char === '[') {
angulars++;
}
else if (char === ']') {
angulars--;
}
else if (splitter && char === splitter) {
if (!parens &&
!angulars &&
!singleQuoted &&
!doubleQuoted) {
splitted.push(soFar.trim());
soFar = '';
if (appendSplitter)
soFar += char;
continue;
}
}
else if (splittersSet?.has(char)) {
if (!parens &&
!angulars &&
!singleQuoted &&
!doubleQuoted) {
splitted.push(soFar);
soFar = '';
if (appendSplitter)
soFar += char;
continue;
}
}
soFar += char;
}
splitted.push(soFar.trim());
return splitted;
}
function isAtRule(selector) {
return selector.startsWith('@');
}
// https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
const PSEUDO_ELEMENTS = `
:after
::after
::backdrop
:before
::before
::cue
::cue-region
:first-letter
::first-letter
:first-line
::first-line
::file-selector-button
::grammer-error
::marker
::part
::placeholder
::selection
::slotted
::spelling-error
::target-text
`.trim().split('\n').map(item => item.trim());
const PSEUDO_ELEMENTS_MATCHER = new RegExp(`(${PSEUDO_ELEMENTS.join('|')})$`, 'i');
export function appendPart(baseSelector, appendant) {
if (PSEUDO_ELEMENTS_MATCHER.test(baseSelector))
return baseSelector.replace(PSEUDO_ELEMENTS_MATCHER, `${appendant}$1`);
return `${baseSelector}${appendant}`;
}

View File

@@ -0,0 +1,140 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import RichConfirm from '/extlib/RichConfirm.js';
import {
log as internalLogger,
configs,
isMacOS,
sanitizeForHTMLText,
} from '/common/common.js';
import * as ApiTabs from './api-tabs.js';
import * as Constants from './constants.js';
import * as Permissions from './permissions.js';
import * as SidebarConnection from './sidebar-connection.js';
import * as UserOperationBlocker from './user-operation-blocker.js';
import { Tab } from './TreeItem.js';
function log(...args) {
internalLogger('common/dialog', ...args);
}
export async function show(ownerWindow, dialogParams) {
let result;
let unblocked = false;
try {
if (configs.showDialogInSidebar &&
SidebarConnection.isOpen(ownerWindow.id)/* &&
SidebarConnection.hasFocus(ownerWindow.id)*/) {
UserOperationBlocker.blockIn(ownerWindow.id, { throbber: false });
result = await browser.runtime.sendMessage({
type: Constants.kCOMMAND_SHOW_DIALOG,
params: {
...dialogParams,
onShown: null,
onShownInTab: null,
onShownInPopup: null,
userOperationBlockerParams: { throbber: false },
},
windowId: ownerWindow.id
}).catch(ApiTabs.createErrorHandler());
}
else if (isMacOS() &&
ownerWindow.state == 'fullscreen') {
// on macOS, a popup window opened from a fullscreen browser window is always
// opened as a new fullscreen window, thus we need to fallback to a workaround.
log('showDialog: show in a temporary tab in ', ownerWindow.id);
UserOperationBlocker.blockIn(ownerWindow.id, { throbber: false, shade: true });
const tempTab = await browser.tabs.create({
windowId: ownerWindow.id,
url: ((await Permissions.isGranted(Permissions.ALL_URLS)) ? null : '/resources/blank.html'),
active: true
});
await Tab.waitUntilTracked(tempTab.id).then(() => {
Tab.get(tempTab.id).$TST.addState('hidden', { broadcast: true });
});
result = await RichConfirm.showInTab(tempTab.id, {
...dialogParams,
onShown: [
container => {
const style = container.closest('.rich-confirm-dialog').style;
style.maxWidth = `${Math.floor(window.innerWidth * 0.6)}px`;
style.marginInlineStart = style.marginInlineEnd = 'auto';
},
dialogParams.onShownInTab || dialogParams.onShown
],
onHidden(...params) {
UserOperationBlocker.unblockIn(ownerWindow.id, { throbber: false });
unblocked = true;
if (typeof dialogParams.onHidden == 'function')
dialogParams.onHidden(...params);
},
});
browser.tabs.remove(tempTab.id);
}
else {
log('showDialog: show in a popup window on ', ownerWindow.id);
UserOperationBlocker.blockIn(ownerWindow.id, { throbber: false });
result = await RichConfirm.showInPopup(ownerWindow.id, {
...dialogParams,
onShown: dialogParams.onShownInPopup || dialogParams.onShown,
onHidden(...params) {
UserOperationBlocker.unblockIn(ownerWindow.id, { throbber: false });
unblocked = true;
if (typeof dialogParams.onHidden == 'function')
dialogParams.onHidden(...params);
},
});
}
}
catch(_error) {
result = { buttonIndex: -1 };
}
finally {
if (!unblocked)
UserOperationBlocker.unblockIn(ownerWindow.id, { throbber: false });
}
return result;
}
export function tabsToHTMLList(tabs, { maxHeight, maxWidth }) {
const rootLevelOffset = tabs.map(tab => parseInt(tab.$TST.getAttribute(Constants.kLEVEL) || 0)).sort()[0];
return (
`<ul style="border: 1px inset;
display: flex;
flex-direction: column;
flex-grow: 1;
flex-shrink: 1;
margin-block: 0.5em;
margin-inline: 0;
min-height: 2em;
max-height: calc(${maxHeight}px - 12em /* title bar, message, checkbox, buttons, and margins */);
max-width: ${maxWidth}px;
overflow: auto;
padding-block: 0.5em;
padding-inline: 0.5em;">` +
tabs.map(tab => `<li style="align-items: center;
display: flex;
flex-direction: row;
padding-inline-start: calc((${tab.$TST.getAttribute(Constants.kLEVEL)} - ${rootLevelOffset}) * 0.25em);"
title="${sanitizeForHTMLText(tab.title)}"
><img style="display: flex;
max-height: 1em;
max-width: 1em;"
alt=""
src="${sanitizeForHTMLText(tab.favIconUrl || browser.runtime.getURL('resources/icons/defaultFavicon.svg'))}"
><span style="margin-inline-start: 0.25em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;"
>${sanitizeForHTMLText(tab.title)}</span></li>`).join('') +
`</ul>`
);
}

View File

@@ -0,0 +1,909 @@
/**
* Mainly ported from difflib.py, the standard diff library of Python.
* This code is distributed under the Python Software Foundation License.
* Contributor(s): Sutou Kouhei <kou@clear-code.com> (porting)
* YUKI "Piro" Hiroshi <yuki@clear-code.com>
* (encoded diff, DOM Updater)
* ------------------------------------------------------------------------
* Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009
* Python Software Foundation.
* All rights reserved.
*
* Copyright (c) 2000 BeOpen.com.
* All rights reserved.
*
* Copyright (c) 1995-2001 Corporation for National Research Initiatives.
* All rights reserved.
*
* Copyright (c) 1991-1995 Stichting Mathematisch Centrum.
* All rights reserved.
*/
export class SequenceMatcher {
constructor(from, to, junkPredicate) {
this.from = from;
this.to = to;
this.junkPredicate = junkPredicate;
this._updateToIndexes();
}
longestMatch(fromStart, fromEnd, toStart, toEnd) {
let bestInfo = this._findBestMatchPosition(fromStart, fromEnd,
toStart, toEnd);
const haveJunk = Object.keys(this.junks).length > 0;
if (haveJunk) {
const adjust = this._adjustBestInfoWithJunkPredicate;
const args = [fromStart, fromEnd, toStart, toEnd];
bestInfo = adjust.apply(this, [false, bestInfo].concat(args));
bestInfo = adjust.apply(this, [true, bestInfo].concat(args));
}
return bestInfo;
}
matches() {
if (!this._matches)
this._matches = this._computeMatches();
return this._matches;
}
blocks() {
if (!this._blocks)
this._blocks = this._computeBlocks();
return this._blocks;
}
operations() {
if (!this._operations)
this._operations = this._computeOperations();
return this._operations;
}
groupedOperations(contextSize) {
if (!contextSize)
contextSize = 3;
let operations = this.operations();
if (operations.length == 0)
operations = [['equal', 0, 0, 0, 0]];
operations = this._expandEdgeEqualOperations(operations, contextSize);
const groupWindow = contextSize * 2;
const groups = [];
let group = [];
for (const operation of operations) {
const tag = operation[0];
let fromStart = operation[1];
const fromEnd = operation[2];
let toStart = operation[3];
const toEnd = operation[4];
if (tag == 'equal' && fromEnd - fromStart > groupWindow) {
group.push([tag,
fromStart,
Math.min(fromEnd, fromStart + contextSize),
toStart,
Math.min(toEnd, toStart + contextSize)]);
groups.push(group);
group = [];
fromStart = Math.max(fromStart, fromEnd - contextSize);
toStart = Math.max(toStart, toEnd - contextSize);
}
group.push([tag, fromStart, fromEnd, toStart, toEnd]);
}
if (group.length > 0)
groups.push(group);
return groups;
}
ratio() {
if (!this._ratio)
this._ratio = this._computeRatio();
return this._ratio;
}
_updateToIndexes() {
this.toIndexes = {};
this.junks = {};
for (let i = 0, length = this.to.length; i < length; i++) {
const item = this.to[i];
if (!this.toIndexes[item])
this.toIndexes[item] = [];
this.toIndexes[item].push(i);
}
if (!this.junkPredicate)
return;
const toIndexesWithoutJunk = {};
for (const item in this.toIndexes) {
if (this.junkPredicate(item)) {
this.junks[item] = true;
} else {
toIndexesWithoutJunk[item] = this.toIndexes[item];
}
}
this.toIndexes = toIndexesWithoutJunk;
}
_findBestMatchPosition(fromStart, fromEnd, toStart, toEnd) {
let bestFrom = fromStart;
let bestTo = toStart;
let bestSize = 0;
let lastSizes = {};
let fromIndex;
for (fromIndex = fromStart; fromIndex <= fromEnd; fromIndex++) {
const sizes = {};
const toIndexes = this.toIndexes[this.from[fromIndex]] || [];
for (let i = 0, length = toIndexes.length; i < length; i++) {
const toIndex = toIndexes[i];
if (toIndex < toStart)
continue;
if (toIndex > toEnd)
break;
const size = sizes[toIndex] = (lastSizes[toIndex - 1] || 0) + 1;
if (size > bestSize) {
bestFrom = fromIndex - size + 1;
bestTo = toIndex - size + 1;
bestSize = size;
}
}
lastSizes = sizes;
}
return [bestFrom, bestTo, bestSize];
}
_adjustBestInfoWithJunkPredicate(shouldJunk, bestInfo,
fromStart, fromEnd,
toStart, toEnd) {
let [bestFrom, bestTo, bestSize] = bestInfo;
while (bestFrom > fromStart &&
bestTo > toStart &&
(shouldJunk ?
this.junks[this.to[bestTo - 1]] :
!this.junks[this.to[bestTo - 1]]) &&
this.from[bestFrom - 1] == this.to[bestTo - 1]) {
bestFrom -= 1;
bestTo -= 1;
bestSize += 1;
}
while (bestFrom + bestSize < fromEnd &&
bestTo + bestSize < toEnd &&
(shouldJunk ?
this.junks[this.to[bestTo + bestSize]] :
!this.junks[this.to[bestTo + bestSize]]) &&
this.from[bestFrom + bestSize] == this.to[bestTo + bestSize]) {
bestSize += 1;
}
return [bestFrom, bestTo, bestSize];
}
_computeMatches() {
const matches = [];
const queue = [[0, this.from.length, 0, this.to.length]];
while (queue.length > 0) {
const target = queue.pop();
const [fromStart, fromEnd, toStart, toEnd] = target;
const match = this.longestMatch(fromStart, fromEnd - 1, toStart, toEnd - 1);
const matchFromIndex = match[0];
const matchToIndex = match[1];
const size = match[2];
if (size > 0) {
if (fromStart < matchFromIndex && toStart < matchToIndex)
queue.push([fromStart, matchFromIndex, toStart, matchToIndex]);
matches.push(match);
if (matchFromIndex + size < fromEnd && matchToIndex + size < toEnd)
queue.push([matchFromIndex + size, fromEnd,
matchToIndex + size, toEnd]);
}
}
matches.sort((matchInfo1, matchInfo2) => {
const fromIndex1 = matchInfo1[0];
const fromIndex2 = matchInfo2[0];
return fromIndex1 - fromIndex2;
});
return matches;
}
_computeBlocks() {
const blocks = [];
let currentFromIndex = 0;
let currentToIndex = 0;
let currentSize = 0;
for (const match of this.matches()) {
const [fromIndex, toIndex, size] = match;
if (currentFromIndex + currentSize == fromIndex &&
currentToIndex + currentSize == toIndex) {
currentSize += size;
} else {
if (currentSize > 0)
blocks.push([currentFromIndex, currentToIndex, currentSize]);
currentFromIndex = fromIndex;
currentToIndex = toIndex;
currentSize = size;
}
}
if (currentSize > 0)
blocks.push([currentFromIndex, currentToIndex, currentSize]);
blocks.push([this.from.length, this.to.length, 0]);
return blocks;
}
_computeOperations() {
let fromIndex = 0;
let toIndex = 0;
const operations = [];
for (const block of this.blocks()) {
const [matchFromIndex, matchToIndex, size] = block;
const tag = this._determineTag(fromIndex, toIndex,
matchFromIndex, matchToIndex);
if (tag != 'equal')
operations.push([tag,
fromIndex, matchFromIndex,
toIndex, matchToIndex]);
fromIndex = matchFromIndex + size;
toIndex = matchToIndex + size;
if (size > 0)
operations.push(['equal',
matchFromIndex, fromIndex,
matchToIndex, toIndex]);
}
return operations;
}
_determineTag(fromIndex, toIndex,
matchFromIndex, matchToIndex) {
if (fromIndex < matchFromIndex && toIndex < matchToIndex) {
return 'replace';
} else if (fromIndex < matchFromIndex) {
return 'delete';
} else if (toIndex < matchToIndex) {
return 'insert';
} else {
return 'equal';
}
}
_expandEdgeEqualOperations(operations, contextSize) {
const expandedOperations = [];
for (let index = 0, length = operations.length; index < length; index++) {
const operation = operations[index];
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
if (tag == 'equal' && index == 0) {
expandedOperations.push([tag,
Math.max(fromStart, fromEnd - contextSize),
fromEnd,
Math.max(toStart, toEnd - contextSize),
toEnd]);
} else if (tag == 'equal' && index == length - 1) {
expandedOperations.push([tag,
fromStart,
Math.min(fromEnd, fromStart + contextSize),
toStart,
Math.min(toEnd, toStart + contextSize),
toEnd]);
} else {
expandedOperations.push(operation);
}
}
return expandedOperations;
}
_computeRatio() {
const length = this.from.length + this.to.length;
if (length == 0)
return 1.0;
let matches = 0;
for (const block of this.blocks()) {
const size = block[2];
matches += size;
}
return 2.0 * matches / length;
}
};
export class ReadableDiffer {
constructor(from, to) {
this.from = from;
this.to = to;
}
diff() {
let lines = [];
const matcher = new SequenceMatcher(this.from, this.to);
for (const operation of matcher.operations()) {
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
let target;
switch (tag) {
case 'replace':
target = this._diffLines(fromStart, fromEnd, toStart, toEnd);
lines = lines.concat(target);
break;
case 'delete':
target = this.from.slice(fromStart, fromEnd);
lines = lines.concat(this._tagDeleted(target));
break;
case 'insert':
target = this.to.slice(toStart, toEnd);
lines = lines.concat(this._tagInserted(target));
break;
case 'equal':
target = this.from.slice(fromStart, fromEnd);
lines = lines.concat(this._tagEqual(target));
break;
default:
throw 'unknown tag: ' + tag;
break;
}
}
return lines;
}
encodedDiff() {
let lines = [];
const matcher = new SequenceMatcher(this.from, this.to);
for (const operation of matcher.operations()) {
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
let target;
switch (tag) {
case 'replace':
target = this._diffLines(fromStart, fromEnd, toStart, toEnd, true);
lines = lines.concat(target);
break;
case 'delete':
target = this.from.slice(fromStart, fromEnd);
lines = lines.concat(this._tagDeleted(target, true));
break;
case 'insert':
target = this.to.slice(toStart, toEnd);
lines = lines.concat(this._tagInserted(target, true));
break;
case 'equal':
target = this.from.slice(fromStart, fromEnd);
lines = lines.concat(this._tagEqual(target, true));
break;
default:
throw new Error(`unknown tag: ${tag}`);
break;
}
}
const blocks = [];
let lastBlock = '';
let lastLineType = '';
for (const line of lines) {
const lineType = line.match(/^<span class="line ([^" ]+)/)[1];
if (lineType != lastLineType) {
blocks.push(lastBlock + (lastBlock ? '</span>' : '' ));
lastBlock = `<span class="block ${lineType}">`;
lastLineType = lineType;
}
lastBlock += line;
}
if (lastBlock)
blocks.push(`${lastBlock}</span>`);
return blocks.join('');
}
_tagLine(mark, contents) {
return contents.map(content => `${mark} ${content}`);
}
_encodedTagLine(encodedClass, contents) {
return contents.map(content => `<span class="line ${encodedClass}">${this._escapeForEncoded(content)}</span>`);
}
_escapeForEncoded(string) {
return string
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
_tagDeleted(contents, encoded) {
return encoded ?
this._encodedTagLine('deleted', contents) :
this._tagLine('-', contents);
}
_tagInserted(contents, encoded) {
return encoded ?
this._encodedTagLine('inserted', contents) :
this._tagLine('+', contents);
}
_tagEqual(contents, encoded) {
return encoded ?
this._encodedTagLine('equal', contents) :
this._tagLine(' ', contents);
}
_tagDifference(contents, encoded) {
return encoded ?
this._encodedTagLine('difference', contents) :
this._tagLine('?', contents);
}
_findDiffLineInfo(fromStart, fromEnd, toStart, toEnd) {
let bestRatio = 0.74;
let fromEqualIndex, toEqualIndex;
let fromBestIndex, toBestIndex;
for (let toIndex = toStart; toIndex < toEnd; toIndex++) {
for (let fromIndex = fromStart; fromIndex < fromEnd; fromIndex++) {
if (this.from[fromIndex] == this.to[toIndex]) {
if (fromEqualIndex === undefined)
fromEqualIndex = fromIndex;
if (toEqualIndex === undefined)
toEqualIndex = toIndex;
continue;
}
const matcher = new SequenceMatcher(this.from[fromIndex],
this.to[toIndex],
this._isSpaceCharacter);
if (matcher.ratio() > bestRatio) {
bestRatio = matcher.ratio();
fromBestIndex = fromIndex;
toBestIndex = toIndex;
}
}
}
return [bestRatio,
fromEqualIndex, toEqualIndex,
fromBestIndex, toBestIndex];
}
_diffLines(fromStart, fromEnd, toStart, toEnd, encoded) {
const cutOff = 0.75;
const info = this._findDiffLineInfo(fromStart, fromEnd, toStart, toEnd);
let bestRatio = info[0];
const fromEqualIndex = info[1];
const toEqualIndex = info[2];
let fromBestIndex = info[3];
let toBestIndex = info[4];
if (bestRatio < cutOff) {
if (fromEqualIndex === undefined) {
const taggedFrom = this._tagDeleted(this.from.slice(fromStart, fromEnd), encoded);
const taggedTo = this._tagInserted(this.to.slice(toStart, toEnd), encoded);
if (toEnd - toStart < fromEnd - fromStart)
return taggedTo.concat(taggedFrom);
else
return taggedFrom.concat(taggedTo);
}
fromBestIndex = fromEqualIndex;
toBestIndex = toEqualIndex;
bestRatio = 1.0;
}
return [].concat(
this.__diffLines(fromStart, fromBestIndex,
toStart, toBestIndex,
encoded),
(encoded ?
this._diffLineEncoded(this.from[fromBestIndex],
this.to[toBestIndex]) :
this._diffLine(this.from[fromBestIndex],
this.to[toBestIndex])
),
this.__diffLines(fromBestIndex + 1, fromEnd,
toBestIndex + 1, toEnd,
encoded)
);
}
__diffLines(fromStart, fromEnd, toStart, toEnd, encoded) {
if (fromStart < fromEnd) {
if (toStart < toEnd) {
return this._diffLines(fromStart, fromEnd, toStart, toEnd, encoded);
} else {
return this._tagDeleted(this.from.slice(fromStart, fromEnd), encoded);
}
} else {
return this._tagInserted(this.to.slice(toStart, toEnd), encoded);
}
}
_diffLineEncoded(fromLine, toLine) {
const fromChars = fromLine.split('');
const toChars = toLine.split('');
const matcher = new SequenceMatcher(fromLine, toLine,
this._isSpaceCharacter);
const phrases = [];
for (const operation of matcher.operations()) {
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
const fromPhrase = fromChars.slice(fromStart, fromEnd).join('');
const toPhrase = toChars.slice(toStart, toEnd).join('');
switch (tag) {
case 'replace':
case 'delete':
case 'insert':
case 'equal':
phrases.push({ tag : tag,
from : fromPhrase,
encodedFrom : this._escapeForEncoded(fromPhrase),
to : toPhrase,
encodedTo : this._escapeForEncoded(toPhrase), });
break;
default:
throw new Error(`unknown tag: ${tag}`);
}
}
const encodedPhrases = [];
let current;
let replaced = 0;
let inserted = 0;
let deleted = 0;
for (let i = 0, maxi = phrases.length; i < maxi; i++)
{
current = phrases[i];
switch (current.tag) {
case 'replace':
encodedPhrases.push('<span class="phrase replaced">');
encodedPhrases.push(this._encodedTagPhrase('deleted', current.encodedFrom));
encodedPhrases.push(this._encodedTagPhrase('inserted', current.encodedTo));
encodedPhrases.push('</span>');
replaced++;
break;
case 'delete':
encodedPhrases.push(this._encodedTagPhrase('deleted', current.encodedFrom));
deleted++;
break;
case 'insert':
encodedPhrases.push(this._encodedTagPhrase('inserted', current.encodedTo));
inserted++;
break;
case 'equal':
// \95ύX\93_\82̊Ԃɋ\B2\82܂ꂽ1\95\B6\8E\9A\82\BE\82\AF\82̖\B3\95ύX\95\94\95\AA\82\BE\82\AF\82͓\C1\95ʈ\B5\82\A2
if (
current.from.length == 1 &&
(i > 0 && phrases[i-1].tag != 'equal') &&
(i < maxi-1 && phrases[i+1].tag != 'equal')
) {
encodedPhrases.push('<span class="phrase equal">');
encodedPhrases.push(this._encodedTagPhrase('duplicated', current.encodedFrom));
encodedPhrases.push(this._encodedTagPhrase('duplicated', current.encodedTo));
encodedPhrases.push('</span>');
}
else {
encodedPhrases.push(current.encodedFrom);
}
break;
}
}
const extraClass = (replaced || (deleted && inserted)) ?
' includes-both-modification' :
'' ;
return [
`<span class="line replaced${extraClass}">${encodedPhrases.join('')}</span>`
];
}
_encodedTagPhrase(encodedClass, content) {
return `<span class="phrase ${encodedClass}">${content}</span>`;
}
_diffLine(fromLine, toLine) {
let fromTags = '';
let toTags = '';
const matcher = new SequenceMatcher(fromLine, toLine,
this._isSpaceCharacter);
for (const operation of matcher.operations()) {
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
const fromLength = fromEnd - fromStart;
const toLength = toEnd - toStart;
switch (tag) {
case 'replace':
fromTags += this._repeat('^', fromLength);
toTags += this._repeat('^', toLength);
break;
case 'delete':
fromTags += this._repeat('-', fromLength);
break;
case 'insert':
toTags += this._repeat('+', toLength);
break;
case 'equal':
fromTags += this._repeat(' ', fromLength);
toTags += this._repeat(' ', toLength);
break;
default:
throw new Error(`unknown tag: ${tag}`);
break;
}
}
return this._formatDiffPoint(fromLine, toLine, fromTags, toTags);
}
_formatDiffPoint(fromLine, toLine, fromTags, toTags) {
let common;
let result;
common = Math.min(this._nLeadingCharacters(fromLine, '\t'),
this._nLeadingCharacters(toLine, '\t'));
common = Math.min(common,
this._nLeadingCharacters(fromTags.slice(0, common),
' '));
fromTags = fromTags.slice(common).replace(/\s*$/, '');
toTags = toTags.slice(common).replace(/\s*$/, '');
result = this._tagDeleted([fromLine]);
if (fromTags.length > 0) {
fromTags = this._repeat('\t', common) + fromTags;
result = result.concat(this._tagDifference([fromTags]));
}
result = result.concat(this._tagInserted([toLine]));
if (toTags.length > 0) {
toTags = this._repeat('\t', common) + toTags;
result = result.concat(this._tagDifference([toTags]));
}
return result;
}
_nLeadingCharacters(string, character) {
let n = 0;
while (string[n] == character) {
n++;
}
return n;
}
_isSpaceCharacter(character) {
return character == ' ' || character == '\t';
}
_repeat(string, n) {
let result = '';
for (; n > 0; n--) {
result += string;
}
return result;
}
};
export const Diff = {
readable(from, to, encoded) {
const differ = new ReadableDiffer(this._splitWithLine(from), this._splitWithLine(to));
return encoded ?
differ.encodedDiff() :
differ.diff(encoded).join('\n') ;
},
foldedReadable(from, to, encoded) {
const differ = new ReadableDiffer(this._splitWithLine(this._fold(from)),
this._splitWithLine(this._fold(to)));
return encoded ?
differ.encodedDiff() :
differ.diff(encoded).join('\n') ;
},
isInterested(diff) {
if (!diff)
return false;
if (diff.length == 0)
return false;
if (!diff.match(/^[-+]/mg))
return false;
if (diff.match(/^[ ?]/mg))
return true;
if (diff.match(/(?:.*\n){2,}/g))
return true;
if (this.needFold(diff))
return true;
return false;
},
needFold(diff) {
if (!diff)
return false;
if (diff.match(/^[-+].{79}/mg))
return true;
return false;
},
_splitWithLine(string) {
string = String(string);
return string.length == 0 ? [] : string.split(/\r?\n/);
},
_fold(string) {
string = String(string);
const foldedLines = string.split('\n').map(line => line.replace(/(.{78})/g, '$1\n'));
return foldedLines.join('\n');
}
};
export const DOMUpdater = {
/**
* method
* @param before {Node} - the node to be updated, e.g. Element
* @param after {Node} - the node describing updated state,
* e.g. DocumentFragment
* @return count {number} - the count of appied changes
*/
update(before, after, counter = { count: 0 }) {
if (before.nodeValue !== null ||
after.nodeValue !== null) {
if (before.nodeValue != after.nodeValue) {
//console.log('node value: ', after.nodeValue);
before.nodeValue = after.nodeValue;
counter.count++;
}
return counter.count;
}
const beforeNodes = Array.from(before.childNodes, this._getDiffableNodeString);
const afterNodes = Array.from(after.childNodes, this._getDiffableNodeString);
const nodeOerations = (new SequenceMatcher(beforeNodes, afterNodes)).operations();
// Update from back to front for safety!
for (const operation of nodeOerations.reverse()) {
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
switch (tag) {
case 'equal':
for (let i = 0, maxi = fromEnd - fromStart; i < maxi; i++) {
this.update(
before.childNodes[fromStart + i],
after.childNodes[toStart + i],
counter
);
}
break;
case 'delete':
for (let i = fromEnd - 1; i >= fromStart; i--) {
//console.log('delete: delete node: ', i, before.childNodes[i]);
before.removeChild(before.childNodes[i]);
counter.count++;
}
break;
case 'insert': {
const reference = before.childNodes[fromStart] || null;
for (let i = toStart; i < toEnd; i++) {
if (!after.childNodes[i])
continue;
//console.console.log('insert: insert node: ', i, after.childNodes[i]);
before.insertBefore(after.childNodes[i].cloneNode(true), reference);
counter.count++;
}
}; break;
case 'replace': {
for (let i = fromEnd - 1; i >= fromStart; i--) {
//console.log('replace: delete node: ', i, before.childNodes[i]);
before.removeChild(before.childNodes[i]);
counter.count++;
}
const reference = before.childNodes[fromStart] || null;
for (let i = toStart; i < toEnd; i++) {
if (!after.childNodes[i])
continue;
//console.log('replace: insert node: ', i, after.childNodes[i]);
before.insertBefore(after.childNodes[i].cloneNode(true), reference);
counter.count++;
}
}; break;
}
}
if (before.nodeType == before.ELEMENT_NODE &&
after.nodeType == after.ELEMENT_NODE) {
const beforeAttrs = Array.from(before.attributes, attr => `${attr.name}:${attr.value}`). sort();
const afterAttrs = Array.from(after.attributes, attr => `${attr.name}:${attr.value}`). sort();
const attrOerations = (new SequenceMatcher(beforeAttrs, afterAttrs)).operations();
for (const operation of attrOerations) {
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
switch (tag) {
case 'equal':
break;
case 'delete':
for (let i = fromStart; i < fromEnd; i++) {
const name = beforeAttrs[i].split(':')[0];
//console.log('delete: delete attr: ', name);
before.removeAttribute(name);
counter.count++;
}
break;
case 'insert':
for (let i = toStart; i < toEnd; i++) {
const attr = afterAttrs[i].split(':');
const name = attr[0];
const value = attr.slice(1).join(':');
//console.log('insert: set attr: ', name, value);
before.setAttribute(name, value);
counter.count++;
}
break;
case 'replace':
const insertedAttrs = new Set();
for (let i = toStart; i < toEnd; i++) {
const attr = afterAttrs[i].split(':');
const name = attr[0];
const value = attr.slice(1).join(':');
//console.log('replace: set attr: ', name, value);
before.setAttribute(name, value);
insertedAttrs.add(name);
counter.count++;
}
for (let i = fromStart; i < fromEnd; i++) {
const name = beforeAttrs[i].split(':')[0];
if (insertedAttrs.has(name))
continue;
//console.log('replace: delete attr: ', name);
before.removeAttribute(name);
counter.count++;
}
break;
}
}
}
return counter.count;
},
_getDiffableNodeString(node) {
if (node.nodeType == node.ELEMENT_NODE)
return `element:${node.tagName}#${node.id}#${node.getAttribute('anonid')}`;
else
return `node:${node.nodeType}`;
}
};

View File

@@ -0,0 +1,103 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
(function() {
if (window.handleAccelKeyLoaded)
return;
function stringifySoloModifier(event) {
switch (event.key) {
case 'Alt':
return (
!event.ctrlKey &&
!event.metaKey
) ? 'alt' : null;
case 'Control':
return (
!event.altKey &&
!event.metaKey
) ? 'control' : null;
case 'Meta':
return (
!event.altKey &&
!event.ctrlKey
) ? 'meta' : null;
default:
return null;
}
}
function stringifyModifier(event) {
if (event.altKey &&
!event.ctrlKey &&
!event.metaKey)
return 'alt';
if (!event.altKey &&
event.ctrlKey &&
!event.metaKey)
return 'control';
if (!event.altKey &&
!event.ctrlKey &&
event.metaKey)
return 'meta';
return null;
}
function stringifyUnshiftedSoloModifier(event) {
if (event.key != 'Shift')
return null;
return stringifyModifier(event);
}
function stringifyMayTabSwitchModifier(event) {
if (!/^(Tab|Shift|PageUp|PageDown)$/.test(event.key))
return null;
return stringifyModifier(event);
}
function onKeyDown(event) {
const modifier = stringifySoloModifier(event);
const mayTabSwitchModifier = stringifyMayTabSwitchModifier(event);
if (modifier ||
mayTabSwitchModifier)
browser.runtime.sendMessage({
type: 'ws:notify-may-start-tab-switch',
modifier: modifier || mayTabSwitchModifier
});
}
function onKeyUp(event) {
const modifier = stringifySoloModifier(event);
const unshiftedModifier = stringifyUnshiftedSoloModifier(event);
const mayTabSwitchModifier = stringifyMayTabSwitchModifier(event);
if (modifier ||
(!unshiftedModifier &&
!mayTabSwitchModifier))
browser.runtime.sendMessage({
type: 'ws:notify-may-end-tab-switch',
modifier: modifier || stringifyModifier(event)
});
}
function init() {
window.handleAccelKeyLoaded = true;
window.addEventListener('keydown', onKeyDown, { capture: true });
window.addEventListener('keyup', onKeyUp, { capture: true });
window.addEventListener('pagehide', () => {
window.addEventListener('keydown', onKeyDown, { capture: true });
window.addEventListener('keyup', onKeyUp, { capture: true });
window.addEventListener('pageshow', init, { once: true });
}, { once: true });
}
init();
})();

View File

@@ -0,0 +1,362 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
notify,
configs
} from './common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from './constants.js';
function log(...args) {
internalLogger('common/permissions', ...args);
}
export const ALL_URLS = { origins: ['<all_urls>'] };
export const BOOKMARKS = { permissions: ['bookmarks'] };
export const CLIPBOARD_READ = { permissions: ['clipboardRead'] };
export const TAB_HIDE = { permissions: ['tabHide'] };
const checkboxesForPermission = new Map();
export function clearRequest() {
configs.requestingPermissions = null;
}
const cachedGranted = new Map();
export async function isGranted(permissions) {
try {
const granted = await browser.permissions.contains(permissions).catch(ApiTabs.createErrorHandler());
cachedGranted.set(JSON.stringify(permissions), granted);
return granted;
}
catch(error) {
console.error(error);
return Promise.reject(new Error('unsupported permission'));
}
}
export function isGrantedSync(permissions) {
return cachedGranted.get(JSON.stringify(permissions));
}
// cache last state
for (const permissions of [ALL_URLS, BOOKMARKS, CLIPBOARD_READ, TAB_HIDE]) {
isGranted(permissions);
}
const CUSTOM_PANEL_AVAILABLE_URLS_MATCHER = new RegExp(`^((https?|data):|moz-extension://${location.host}/)`);
export async function canInjectScriptToTab(tab) {
if (!tab ||
!CUSTOM_PANEL_AVAILABLE_URLS_MATCHER.test(tab.url))
return false;
return isGranted(ALL_URLS);
}
export function canInjectScriptToTabSync(tab) {
if (!tab ||
!CUSTOM_PANEL_AVAILABLE_URLS_MATCHER.test(tab.url))
return false;
return isGrantedSync(ALL_URLS);
}
const mRequests = new Map();
function destroyRequest(request) {
const permissions = JSON.stringify(request.permissions);
const requests = mRequests.get(permissions);
if (requests)
requests.delete(request);
const onChanged = request.resolve;
const checkbox = request.url;
request.permissions = undefined;
request.onChanged = undefined;
request.checkbox = undefined;
return { onChanged, checkbox };
}
browser.runtime.onMessage.addListener((message, _sender) => {
if (!message ||
!message.type ||
message.type != Constants.kCOMMAND_NOTIFY_PERMISSIONS_GRANTED)
return;
const permissions = JSON.stringify(message.permissions);
isGranted(message.permissions); // to cache latest state
const requests = mRequests.get(permissions);
if (!requests)
return;
mRequests.delete(permissions);
for (const request of requests) {
const { onChanged, checkbox } = destroyRequest(request);
const checked =onChanged ?
onChanged(true) :
undefined;
checkbox.checked = checked !== undefined ? !!checked : true;
}
});
/*
// These events are not available yet on Firefox...
browser.permissions.onAdded.addListener(addedPermissions => {
const permissions = JSON.stringify(addedPermissions.permissions);
const requests = mRequests.get(permissions);
if (!requests)
return;
mRequests.delete(permissions);
for (const request of requests) {
const { checkbox } = destroyRequest(request);
checkbox.checked = true;
}
});
browser.permissions.onRemoved.addListener(removedPermissions => {
const permissions = JSON.stringify(addedPermissions.permissions);
const requests = mRequests.get(permissions);
if (!requests)
return;
mRequests.delete(permissions);
for (const request of requests) {
const { checkbox } = destroyRequest(request);
checkbox.checked = false;
}
});
*/
export function bindToCheckbox(permissions, checkbox, options = {}) {
const checkboxes = checkboxesForPermission.get(permissions) || [];
checkboxes.push(checkbox);
checkboxesForPermission.set(permissions, checkboxes);
isGranted(permissions)
.then(granted => {
const checked = options.onInitialized ?
options.onInitialized(granted) :
checkbox.dataset.relatedConfigKey ?
configs[checkbox.dataset.relatedConfigKey] :
undefined;
checkbox.checked = checked !== undefined ? !!checked : granted;
})
.catch(_error => {
checkbox.setAttribute('readonly', true);
checkbox.setAttribute('disabled', true);
const label = checkbox.closest('label') || document.querySelector(`label[for=${checkbox.id}]`);
if (label)
label.setAttribute('disabled', true);
});
checkbox.addEventListener('change', _event => {
checkbox.requestPermissions()
});
const key = JSON.stringify(permissions);
const requests = mRequests.get(key) || new Set();
const request = {
permissions,
onChanged: options.onChanged,
checkbox,
};
requests.add(request);
mRequests.set(key, requests);
checkbox.requestPermissions = async () => {
log('permission requested: ', permissions);
const checkboxes = checkboxesForPermission.get(permissions);
try {
log('checkboxes: ', checkboxes);
log('checkbox.checked: ', checkbox.checked);
if (!checkbox.checked) {
if (checkbox.dataset.relatedConfigKey)
configs[checkbox.dataset.relatedConfigKey] = false;
if (options.onChanged)
options.onChanged(false);
const canRevoke = Array.from(checkboxes, checkbox => checkbox.dataset.relatedConfigKey ? configs[checkbox.dataset.relatedConfigKey] : null).filter(state => state !== null).every(state => !state);
log('canRevoke: ', canRevoke);
if (!canRevoke)
return;
log('revoking the permission');
await browser.permissions.remove(permissions).catch(ApiTabs.createErrorSuppressor());
for (const otherCheckbox of checkboxes) {
if (otherCheckbox != checkbox &&
otherCheckbox.dataset.relatedConfigKey)
continue;
otherCheckbox.checked = false;
}
return;
}
for (const otherCheckbox of checkboxes) {
if (otherCheckbox != checkbox &&
otherCheckbox.dataset.relatedConfigKey)
continue;
otherCheckbox.checked = false;
}
if (configs.requestingPermissionsNatively)
return;
log('requesting the permission');
configs.requestingPermissionsNatively = permissions;
let granted = await browser.permissions.request(permissions).catch(ApiTabs.createErrorHandler());
configs.requestingPermissionsNatively = null;
log('granted: ', granted);
if (granted === undefined) {
granted = await isGranted(permissions);
log('granted (retry): ', granted);
}
else if (!granted) {
log('not granted: cacneled');
return;
}
if (granted) {
if (checkbox.dataset.relatedConfigKey)
configs[checkbox.dataset.relatedConfigKey] = true;
const configValue = checkbox.dataset.relatedConfigKey ? true : null;
const onChangedResult = options.onChanged && options.onChanged(true);
const checked = configValue !== null ? configValue :
options.onChanged ?
onChangedResult :
undefined;
log('update checkboxes with checked state ', checked);
for (const otherCheckbox of checkboxes) {
if (otherCheckbox != checkbox &&
otherCheckbox.dataset.relatedConfigKey)
continue;
otherCheckbox.checked = checked !== undefined ? !!checked : true;
}
browser.runtime.sendMessage({
type: Constants.kCOMMAND_NOTIFY_PERMISSIONS_GRANTED,
permissions
}).catch(_error => {});
log('finish');
return;
}
log('fallback to the failsafe method');
configs.requestingPermissions = permissions;
browser.browserAction.setBadgeText({ text: '!' });
browser.browserAction.setPopup({ popup: '' });
notify({
title: browser.i18n.getMessage('config_requestPermissions_fallbackToToolbarButton_title'),
message: browser.i18n.getMessage('config_requestPermissions_fallbackToToolbarButton_message'),
icon: 'resources/24x24.svg#default'
});
return;
}
catch(error) {
console.log(error);
}
for (const checkbox of checkboxes) {
checkbox.checked = false;
}
};
}
export function bindToClickable(permissions, node, { onChanged } = {}) {
node.addEventListener('click', _event => {
node.requestPermissions()
});
if (node.requestPermissions)
return;
node.requestPermissions = async () => {
try {
const checkboxes = checkboxesForPermission.get(permissions);
if (configs.requestingPermissionsNatively ||
checkboxes.every(checkbox => checkbox.checked))
return;
configs.requestingPermissionsNatively = permissions;
// We need to call this without delay to avoid "permissions.request may only be called from a user input handler" error.
let granted = await browser.permissions.request(permissions).catch(ApiTabs.createErrorHandler());
configs.requestingPermissionsNatively = null;
if (granted === undefined)
granted = await isGranted(permissions);
else if (!granted)
return;
if (granted) {
for (const checkbox of checkboxes) {
checkbox.checked = true;
}
if (onChanged)
onChanged(true);
browser.runtime.sendMessage({
type: Constants.kCOMMAND_NOTIFY_PERMISSIONS_GRANTED,
permissions
}).catch(_error => {});
return;
}
configs.requestingPermissions = permissions;
browser.browserAction.setBadgeText({ text: '!' });
browser.browserAction.setPopup({ popup: '' });
notify({
title: browser.i18n.getMessage('config_requestPermissions_fallbackToToolbarButton_title'),
message: browser.i18n.getMessage('config_requestPermissions_fallbackToToolbarButton_message'),
icon: 'resources/24x24.svg#default'
});
return;
}
catch(error) {
console.log(error);
}
};
}
export function requestPostProcess() {
if (!configs.requestingPermissions)
return false;
const permissions = configs.requestingPermissions;
configs.requestingPermissions = null;
configs.requestingPermissionsNatively = permissions;
browser.browserAction.setBadgeText({ text: '' });
browser.permissions.request(permissions)
.then(granted => {
log('permission requested: ', permissions, granted);
if (granted)
browser.runtime.sendMessage({
type: Constants.kCOMMAND_NOTIFY_PERMISSIONS_GRANTED,
permissions
}).catch(_error => {});
})
.catch(ApiTabs.createErrorSuppressor())
.finally(() => {
configs.requestingPermissionsNatively = null;
});
return true;
}
configs.$loaded.then(() => {
configs.requestingPermissionsNatively = null;
});

View File

@@ -0,0 +1,197 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
} from './common.js';
import * as Permissions from './permissions.js';
function log(...args) {
internalLogger('common/retrieve-url', ...args);
}
export const kTYPE_PLAIN_TEXT = 'text/plain';
export const kTYPE_X_MOZ_URL = 'text/x-moz-url';
export const kTYPE_URI_LIST = 'text/uri-list';
export const kTYPE_MOZ_TEXT_INTERNAL = 'text/x-moz-text-internal';
const kBOOKMARK_FOLDER = 'x-moz-place:';
const ACCEPTABLE_DATA_TYPES = [
kTYPE_URI_LIST,
kTYPE_X_MOZ_URL,
kTYPE_MOZ_TEXT_INTERNAL,
kTYPE_PLAIN_TEXT,
];
let mFileURLResolver = null;
export async function registerFileURLResolver(resolver) {
mFileURLResolver = resolver;
}
export async function fromDragEvent(event) {
log('fromDragEvent ', event);
const dt = event.dataTransfer;
const urls = [];
if (dt.files.length > 0) {
for (const file of dt.files) {
if (typeof mFileURLResolver == 'function') {
urls.push(await mFileURLResolver(file));
}
else {
// Created object URLs need to be revoked by tryRevokeObjectURL()
// in common/common.js, after they are loaded.
urls.push(URL.createObjectURL(file));
}
}
}
else {
for (const type of ACCEPTABLE_DATA_TYPES) {
const urlData = dt.getData(type);
if (urlData)
urls.push(...fromData(urlData, type));
if (urls.length)
break;
}
for (const type of dt.types) {
if (!/^application\/x-ws-drag-data;(.+)$/.test(type))
continue;
const params = RegExp.$1;
const providerId = /provider=([^;&]+)/.test(params) && RegExp.$1;
const dataId = /id=([^;&]+)/.test(params) && RegExp.$1;
try {
const dragData = await browser.runtime.sendMessage(providerId, {
type: 'get-drag-data',
id: dataId
});
if (!dragData || typeof dragData != 'object')
break;
for (const type of ACCEPTABLE_DATA_TYPES) {
const urlData = dragData[type];
if (urlData)
urls.push(...fromData(urlData, type));
if (urls.length)
break;
}
}
catch(_error) {
}
}
}
log(' => retrieved: ', urls);
return sanitizeURLs(urls);
}
let mSelectionClipboardProvider = null;
/* provider should have two methods:
isAvailable(): returns boolean which indicates the selection clipboard is available or not.
getTextData(): returns a string from the selection clipboard.
*/
export function registerSelectionClipboardProvider(provider) {
mSelectionClipboardProvider = provider;
}
export async function fromClipboard({ selection } = {}) {
const urls = [];
if (selection && mSelectionClipboardProvider) {
if (await mSelectionClipboardProvider.isAvailable()) {
const maybeUrlString = await mSelectionClipboardProvider.getTextData();
if (maybeUrlString)
urls.push(...fromData(maybeUrlString, kTYPE_PLAIN_TEXT));
return sanitizeURLs(urls);
}
}
if (!(await Permissions.isGranted(Permissions.CLIPBOARD_READ)) ||
typeof navigator.clipboard.read != 'function')
return urls;
const clipboardContents = await navigator.clipboard.read();
log('fromClipboard ', clipboardContents);
for (const item of clipboardContents) {
for (const type of item.types) {
const maybeUrlBlob = await item.getType(type);
const maybeUrlString = await maybeUrlBlob.text();
if (maybeUrlString)
urls.push(...fromData(maybeUrlString, type));
if (urls.length)
break;
}
}
log(' => retrieved: ', urls);
return sanitizeURLs(urls);
}
function sanitizeURLs(urls) {
const filteredUrls = urls.filter(url =>
url &&
url.length &&
url.indexOf(kBOOKMARK_FOLDER) == 0 ||
!/^\s*(javascript|data):/.test(url)
);
log('sanitizeURLs filtered: ', filteredUrls);
const fixedUrls = filteredUrls.map(fixupURIFromText);
log('sanitizeURLs fixed: ', fixedUrls);
return fixedUrls;
}
function fromData(data, type) {
log('fromData: ', type, data);
switch (type) {
case kTYPE_URI_LIST:
return data
.replace(/\r/g, '\n')
.replace(/\n\n+/g, '\n')
.split('\n')
.filter(line => {
return line.charAt(0) != '#';
});
case kTYPE_X_MOZ_URL:
return data
.trim()
.replace(/\r/g, '\n')
.replace(/\n\n+/g, '\n')
.split('\n')
.filter((_line, index) => {
return index % 2 == 0;
});
case kTYPE_MOZ_TEXT_INTERNAL:
return data
.replace(/\r/g, '\n')
.replace(/\n\n+/g, '\n')
.trim()
.split('\n');
case kTYPE_PLAIN_TEXT:
return data
.replace(/\r/g, '\n')
.replace(/\n\n+/g, '\n')
.trim()
.split('\n')
.map(line => {
return /^\w+:\/\/.+/.test(line) ? line : `ext+ws:search:${line}`;
});
}
return [];
}
function fixupURIFromText(maybeURI) {
if (/^(ext\+)?\w+:/.test(maybeURI))
return maybeURI;
if (/^([^\.\s]+\.)+[^\.\s]{2}/.test(maybeURI))
return `http://${maybeURI}`;
return maybeURI;
}

View File

@@ -0,0 +1,305 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
log as internalLogger,
mapAndFilterUniq,
configs,
wait,
} from './common.js';
import * as Constants from './constants.js';
import * as TabsStore from './tabs-store.js';
function log(...args) {
internalLogger('common/sidebar-connection', ...args);
}
export const onMessage = new EventListenerManager();
export const onConnected = new EventListenerManager();
export const onDisconnected = new EventListenerManager();
const mConnections = new Map();
const mReceivers = new Map();
const mFocusState = new Map();
let mIsListening = false;
export function isInitialized() {
return mIsListening;
}
export function isSidebarOpen(windowId) {
if (!mIsListening ||
Constants.IS_SIDEBAR)
return false;
// for automated tests
if (configs.sidebarVirtuallyOpenedWindows.length > 0 &&
configs.sidebarVirtuallyOpenedWindows.includes(windowId))
return true;
if (configs.sidebarVirtuallyClosedWindows.length > 0 &&
configs.sidebarVirtuallyClosedWindows.includes(windowId))
return false;
if (windowId in configs.sidebarWidthInWindow &&
configs.sidebarWidthInWindow[windowId] == 0)
return false;
const connections = mConnections.get(windowId);
if (!connections)
return false;
for (const connection of connections) {
if (connection.type == 'sidebar')
return true;
}
return false;
}
export function isOpen(windowId) {
if (!mIsListening ||
Constants.IS_SIDEBAR)
return false;
// for automated tests
if (configs.sidebarVirtuallyOpenedWindows.length > 0 &&
configs.sidebarVirtuallyOpenedWindows.includes(windowId))
return true;
if (configs.sidebarVirtuallyClosedWindows.length > 0 &&
configs.sidebarVirtuallyClosedWindows.includes(windowId))
return false;
if (windowId in configs.sidebarWidthInWindow &&
configs.sidebarWidthInWindow[windowId] == 0)
return false;
const connections = mConnections.get(windowId);
return connections && connections.size > 0;
}
export function hasFocus(windowId) {
return mFocusState.has(windowId)
}
export const counts = {
broadcast: {}
};
export function getOpenWindowIds() {
return mIsListening ? Array.from(mConnections.keys()) : [];
}
export function sendMessage(message) {
if (!mIsListening ||
Constants.IS_SIDEBAR)
return false;
if (Array.isArray(message))
log('Sending ', message.length, ' messages');
if (message.windowId) {
if (configs.loggingConnectionMessages) {
counts[message.windowId] = counts[message.windowId] || {};
const localCounts = counts[message.windowId];
localCounts[message.type] = localCounts[message.type] || 0;
localCounts[message.type]++;
}
const connections = mConnections.get(message.windowId);
if (!connections || connections.size == 0)
return false;
for (const connection of connections) {
sendMessageToPort(connection.port, message);
}
//port.postMessage(message);
return true;
}
// broadcast
counts.broadcast[message.type] = counts.broadcast[message.type] || 0;
counts.broadcast[message.type]++;
for (const connections of mConnections.values()) {
if (!connections || connections.size == 0)
continue;
for (const connection of connections) {
sendMessageToPort(connection.port, message);
}
//port.postMessage(message);
}
return true;
}
const mReservedTasks = new WeakMap();
// Se should not send messages immediately, instead we should throttle
// it and bulk-send multiple messages, for better user experience.
// Sending too much messages in one event loop may block everything
// and makes Firefox like frozen.
function sendMessageToPort(port, message) {
const task = mReservedTasks.get(port) || { messages: [] };
task.messages.push(message);
mReservedTasks.set(port, task);
if (!task.onFrame) {
task.onFrame = () => {
delete task.onFrame;
const messages = task.messages;
task.messages = [];
port.postMessage(messages);
if (configs.debug) {
const types = mapAndFilterUniq(messages,
message => message.type || undefined).join(', ');
log(`${messages.length} messages sent (${types}):`, messages);
}
};
// We should not use window.requestAnimationFrame for throttling,
// because it is quite lagged on some environment. Firefox may
// decelerate the method for an invisible document (the background
// page).
//window.requestAnimationFrame(task.onFrame);
setTimeout(task.onFrame, 0);
}
}
if (Constants.IS_BACKGROUND) {
const matcher = new RegExp(`^${Constants.kCOMMAND_REQUEST_CONNECT_PREFIX}([0-9]+):(.+)$`);
browser.runtime.onConnect.addListener(port => {
if (!mIsListening ||
!matcher.test(port.name))
return;
const windowId = parseInt(RegExp.$1);
const type = RegExp.$2;
const connection = { port, type };
const connections = mConnections.get(windowId) || new Set();
connections.add(connection);
mConnections.set(windowId, connections);
let connectionTimeoutTimer = null;
const updateTimeoutTimer = () => {
if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = null;
}
// On slow situation (like having too many tabs - 5000 or more)
// we should wait more for the pong. Otherwise the vital check
// may produce needless reloadings even if the sidebar is still alive.
// See also https://github.com/piroor/treestyletab/issues/3130
const timeout = configs.heartbeatInterval + Math.max(configs.connectionTimeoutDelay, TabsStore.tabs.size);
connectionTimeoutTimer = setTimeout(async () => {
log(`Missing heartbeat from window ${windowId}. Maybe disconnected or resumed.`);
try {
const pong = await browser.runtime.sendMessage({
type: Constants.kCOMMAND_PING_TO_SIDEBAR,
windowId
});
if (pong) {
log(`Sidebar for the window ${windowId} responded. Keep connected.`);
return;
}
}
catch(_error) {
}
log(`Sidebar for the window ${windowId} did not respond. Disconnect now.`);
cleanup(); // eslint-disable-line no-use-before-define
port.disconnect();
}, timeout);
};
const cleanup = _diconnectedPort => {
if (!port.onMessage.hasListener(receiver)) // eslint-disable-line no-use-before-define
return;
if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = null;
}
connections.delete(connection);
if (connections.size == 0) {
mConnections.delete(windowId);
const sidebarWidthInWindow = { ...configs.sidebarWidthInWindow };
delete sidebarWidthInWindow[windowId];
configs.sidebarWidthInWindow = sidebarWidthInWindow;
}
port.onMessage.removeListener(receiver); // eslint-disable-line no-use-before-define
mReceivers.delete(windowId);
mFocusState.delete(windowId);
onDisconnected.dispatch(windowId, connections.size);
// We need to notify this to some conetnt scripts, to destroy themselves.
/*
browser.runtime.sendMessage({
type: Constants.kCOMMAND_NOTIFY_SIDEBAR_CLOSED,
windowId,
});
*/
browser.windows.get(windowId, { populate: true }).then(async win => {
let count = 0;
for (const tab of win.tabs) {
count++;
if (count >= 20) {
// We should not block too long seconds on too much tabs case.
await wait(10);
count = 0;
}
try {
browser.tabs.sendMessage(tab.id, {
type: Constants.kCOMMAND_NOTIFY_SIDEBAR_CLOSED,
});
}
catch (_error) {
}
}
});
};
const receiver = message => {
if (Array.isArray(message))
return message.forEach(receiver);
if (message.type == Constants.kCONNECTION_HEARTBEAT)
updateTimeoutTimer();
else
onMessage.dispatch(windowId, message);
};
port.onMessage.addListener(receiver);
mReceivers.set(windowId, receiver);
onConnected.dispatch(windowId, connections.size);
port.onDisconnect.addListener(cleanup);
});
onMessage.addListener(async (windowId, message) => {
switch (message.type) {
case Constants.kNOTIFY_SIDEBAR_FOCUS:
mFocusState.set(windowId, true);
break;
case Constants.kNOTIFY_SIDEBAR_BLUR:
mFocusState.delete(windowId);
break;
}
});
}
export function init() {
if (mIsListening)
return;
mIsListening = true;
}
//===================================================================
// Logging
//===================================================================
browser.runtime.onMessage.addListener((message, _sender) => {
if (!mIsListening ||
!message ||
typeof message != 'object' ||
message.type != Constants.kCOMMAND_REQUEST_CONNECTION_MESSAGE_LOGS ||
!Constants.IS_BACKGROUND)
return;
browser.runtime.sendMessage({
type: Constants.kCOMMAND_RESPONSE_CONNECTION_MESSAGE_LOGS,
logs: JSON.parse(JSON.stringify(counts))
});
});

View File

@@ -0,0 +1,30 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import * as Sync from '/common/sync.js';
Sync.registerExternalProvider({
async getOtherDevices() {
return browser.waterfoxBridge.listSyncDevices();
},
sendTabsToDevice(tabs, deviceId) {
if (!Array.isArray(tabs))
tabs = [tabs];
return browser.waterfoxBridge.sendToDevice(tabs.map(tab => tab.id), deviceId);
},
sendTabsToAllDevices(tabs) {
if (!Array.isArray(tabs))
tabs = [tabs];
return browser.waterfoxBridge.sendToDevice(tabs.map(tab => tab.id));
},
manageDevices(windowId) {
return browser.waterfoxBridge.openSyncDeviceSettings(windowId)
},
});

View File

@@ -0,0 +1,515 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
log as internalLogger,
configs,
notify,
wait,
getChunkedConfig,
setChunkedConfig,
isLinux,
} from './common.js';
import * as Constants from '/common/constants.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import { Tab } from '/common/TreeItem.js';
function log(...args) {
internalLogger('common/sync', ...args);
}
export const onMessage = new EventListenerManager();
export const onNewDevice = new EventListenerManager();
export const onUpdatedDevice = new EventListenerManager();
export const onObsoleteDevice = new EventListenerManager();
const SEND_TABS_SIMULATOR_ID = 'send-tabs-to-device-simulator@piro.sakura.ne.jp';
// Workaround for https://github.com/piroor/treestyletab/blob/trunk/README.md#user-content-feature-requests-send-tab-tree-to-device-does-not-work
// and https://bugzilla.mozilla.org/show_bug.cgi?id=1417183 (resolved as WONTFIX)
// Firefox does not provide any API to access to Sync features directly.
// We need to provide it as experiments API or something way.
// This module is designed to work with a service provide which has features:
// * async getOtherDevices()
// - Returns an array of sync devices.
// - Retruned array should have 0 or more items like:
// { id: "identifier of the device",
// name: "name of the device",
// type: "type of the device (desktop, mobile, and so on)" }
// * sendTabsToDevice(tabs, deviceId)
// - Returns nothing.
// - Sends URLs of given tabs to the specified device.
// - The device is one of values returned by getOtherDevices().
// * sendTabsToAllDevices(tabs)
// - Returns nothing.
// - Sends URLs of given tabs to all other devices.
// * manageDevices(windowId)
// - Returns nothing.
// - Opens settings page for Sync devices.
// It will appear as a tab in the specified window.
let mExternalProvider = null;
export function registerExternalProvider(provider) {
mExternalProvider = provider;
}
export function hasExternalProvider() {
return !!mExternalProvider;
}
let mMyDeviceInfo = null;
async function getMyDeviceInfo() {
if (!configs.syncDeviceInfo || !configs.syncDeviceInfo.id) {
const newDeviceInfo = await generateDeviceInfo();
if (!configs.syncDeviceInfo)
configs.syncDeviceInfo = newDeviceInfo;
}
return mMyDeviceInfo = configs.syncDeviceInfo;
}
async function ensureDeviceInfoInitialized() {
await getMyDeviceInfo();
}
export async function waitUntilDeviceInfoInitialized() {
while (!configs.syncDeviceInfo) {
await wait(100);
}
mMyDeviceInfo = configs.syncDeviceInfo;
}
let initialized = false;
let preChanges = [];
function onConfigChanged(key, value = undefined) {
if (!initialized) {
preChanges.push({ key, value: value === undefined ? configs[key] : value });
return;
}
switch (key) {
case 'syncOtherDevicesDetected':
if (!configs.syncAvailableNotified) {
configs.syncAvailableNotified = true;
notify({
title: browser.i18n.getMessage('syncAvailable_notification_title'),
message: browser.i18n.getMessage(`syncAvailable_notification_message${isLinux() ? '_linux' : ''}`),
url: `${Constants.kSHORTHAND_URIS.options}#syncTabsToDeviceOptions`,
timeout: configs.syncAvailableNotificationTimeout
});
}
return;
case 'syncDevices':
// This may happen when all configs are reset.
// We need to try updating devices after syncDeviceInfo is completely cleared.
wait(100).then(updateDevices);
break;
case 'syncDeviceInfo':
if (configs.syncDeviceInfo &&
mMyDeviceInfo &&
configs.syncDeviceInfo.id == mMyDeviceInfo.id &&
configs.syncDeviceInfo.timestamp == mMyDeviceInfo.timestamp)
return; // ignore updating triggered by myself
mMyDeviceInfo = null;
updateSelf();
break;
default:
if (key.startsWith('chunkedSyncData'))
reserveToReceiveMessage();
break;
}
}
browser.runtime.onMessageExternal.addListener((message, sender) => {
if (!initialized ||
!message ||
typeof message != 'object' ||
typeof message.type != 'string' ||
sender.id != SEND_TABS_SIMULATOR_ID)
return;
switch (message.type) {
case 'ready':
try {
browser.runtime.sendMessage(SEND_TABS_SIMULATOR_ID, { type: 'register-self' }).catch(_error => {});
}
catch(_error) {
}
case 'device-added':
case 'device-updated':
case 'device-removed':
updateSelf();
break;
}
});
export async function init() {
configs.$addObserver(onConfigChanged); // we need to register the listener to collect pre-sent changes
await configs.$loaded;
await ensureDeviceInfoInitialized();
await updateSelf();
initialized = true;
reserveToReceiveMessage();
window.setInterval(updateSelf, 1000 * 60 * 60 * 24); // update info every day!
for (const change of preChanges) {
onConfigChanged(change.key, change.value);
}
preChanges = [];
try {
browser.runtime.sendMessage(SEND_TABS_SIMULATOR_ID, { type: 'register-self' }).catch(_error => {});
}
catch(_error) {
}
}
export async function generateDeviceInfo({ name, icon } = {}) {
const [platformInfo, browserInfo] = await Promise.all([
browser.runtime.getPlatformInfo(),
browser.runtime.getBrowserInfo()
]);
return {
id: `device-${Date.now()}-${Math.round(Math.random() * 65000)}`,
name: name === undefined ?
browser.i18n.getMessage('syncDeviceDefaultName', [toHumanReadableOSName(platformInfo.os), browserInfo.name]) :
(name || null),
icon: icon || 'device-desktop'
};
}
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/PlatformOs
function toHumanReadableOSName(os) {
switch (os) {
case 'mac': return 'macOS';
case 'win': return 'Windows';
case 'android': return 'Android';
case 'cros': return 'Chrome OS';
case 'linux': return 'Linux';
case 'openbsd': return 'Open/FreeBSD';
default: return 'Unknown Platform';
}
}
configs.$addObserver(key => {
switch (key) {
case 'syncUnsendableUrlPattern':
isSendableTab.unsendableUrlMatcher = null;
break;
default:
break;
}
});
async function updateSelf() {
if (updateSelf.updating)
return;
updateSelf.updating = true;
const [devices] = await Promise.all([
(async () => {
try {
return await browser.runtime.sendMessage(SEND_TABS_SIMULATOR_ID, { type: 'list-devices' });
}
catch(_error) {
}
})(),
ensureDeviceInfoInitialized(),
]);
if (devices) {
const myDeviceFromSimulator = devices.find(device => device.myself);
if (mMyDeviceInfo.simulatorId != myDeviceFromSimulator.id)
mMyDeviceInfo.simulatorId = myDeviceFromSimulator.id;
}
configs.syncDeviceInfo = mMyDeviceInfo = {
...clone(configs.syncDeviceInfo),
timestamp: Date.now(),
};
await updateDevices();
setTimeout(() => {
updateSelf.updating = false;
}, 250);
}
async function updateDevices() {
if (updateDevices.updating)
return;
updateDevices.updating = true;
const [devicesFromSimulator] = await Promise.all([
(async () => {
try {
return await browser.runtime.sendMessage(SEND_TABS_SIMULATOR_ID, { type: 'list-devices' });
}
catch(_error) {
}
})(),
waitUntilDeviceInfoInitialized(),
]);
const remote = clone(configs.syncDevices);
const local = clone(configs.syncDevicesLocalCache);
log('devices updated: ', local, remote);
for (const [id, info] of Object.entries(remote)) {
if (id == mMyDeviceInfo.id)
continue;
local[id] = info;
if (id in local) {
log('updated device: ', info);
onUpdatedDevice.dispatch(info);
}
else {
log('new device: ', info);
onNewDevice.dispatch(info);
}
}
if (devicesFromSimulator) {
const knownDeviceIdsFromSimulator = new Set(Object.values(local).map(device => device.simulatorId).filter(id => !!id));
for (const deviceFromSimulator of devicesFromSimulator) {
if (knownDeviceIdsFromSimulator.has(deviceFromSimulator.id))
continue;
const localId = `device-from-simulator:${deviceFromSimulator.id}`;
local[localId] = {
...deviceFromSimulator,
id: localId,
};
}
}
for (const [id, info] of Object.entries(local)) {
if (id in remote ||
id == mMyDeviceInfo.id)
continue;
log('obsolete device: ', info);
delete local[id];
onObsoleteDevice.dispatch(info);
}
if (configs.syncDeviceExpirationDays > 0) {
const expireDateInSeconds = Date.now() - (1000 * 60 * 60 * configs.syncDeviceExpirationDays);
for (const [id, info] of Object.entries(local)) {
if (info &&
info.timestamp < expireDateInSeconds) {
delete local[id];
log('expired device: ', info);
onObsoleteDevice.dispatch(info);
}
}
}
local[mMyDeviceInfo.id] = clone(mMyDeviceInfo);
log('store myself: ', mMyDeviceInfo, local[mMyDeviceInfo.id]);
if (!configs.syncOtherDevicesDetected && Object.keys(local).length > 1)
configs.syncOtherDevicesDetected = true;
configs.syncDevices = local;
configs.syncDevicesLocalCache = clone(local);
setTimeout(() => {
updateDevices.updating = false;
}, 250);
}
function reserveToReceiveMessage() {
if (reserveToReceiveMessage.reserved)
clearTimeout(reserveToReceiveMessage.reserved);
reserveToReceiveMessage.reserved = setTimeout(() => {
delete reserveToReceiveMessage.reserved;
receiveMessage();
}, 250);
}
async function receiveMessage() {
const myDeviceInfo = await getMyDeviceInfo();
try {
const messages = readMessages();
log('receiveMessage: queued messages => ', messages);
const restMessages = messages.filter(message => {
if (message.timestamp <= configs.syncLastMessageTimestamp)
return false;
if (message.to == myDeviceInfo.id) {
log('receiveMessage receive: ', message);
configs.syncLastMessageTimestamp = message.timestamp;
onMessage.dispatch(message);
return false;
}
return true;
});
log('receiveMessage: restMessages => ', restMessages);
if (restMessages.length != messages.length)
writeMessages(restMessages);
}
catch(error) {
log('receiveMessage fatal error: ', error);
writeMessages([]);
}
}
export async function sendMessage(to, data) {
const myDeviceInfo = await getMyDeviceInfo();
try {
const messages = readMessages();
messages.push({
timestamp: Date.now(),
from: myDeviceInfo.id,
to,
data
});
log('sendMessage: queued messages => ', messages);
writeMessages(messages);
}
catch(error) {
console.log('Sync.sendMessage: failed to send message ', error);
writeMessages([]);
}
}
function readMessages() {
try {
return uniqMessages([
...JSON.parse(getChunkedConfig('chunkedSyncDataLocal') || '[]'),
...JSON.parse(getChunkedConfig('chunkedSyncData') || '[]')
]);
}
catch(error) {
log('failed to read messages: ', error);
return [];
}
}
function writeMessages(messages) {
const stringified = JSON.stringify(messages || []);
setChunkedConfig('chunkedSyncDataLocal', stringified);
setChunkedConfig('chunkedSyncData', stringified);
}
function uniqMessages(messages) {
const knownMessages = new Set();
return messages.filter(message => {
const key = JSON.stringify(message);
if (knownMessages.has(key))
return false;
knownMessages.add(key);
return true;
});
}
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
export async function getOtherDevices() {
if (mExternalProvider)
return mExternalProvider.getOtherDevices();
await waitUntilDeviceInfoInitialized();
const devices = configs.syncDevices || {};
const result = [];
for (const [id, info] of Object.entries(devices)) {
if (id == mMyDeviceInfo.id ||
!info.id /* ignore invalid device info accidentally saved (see also https://github.com/piroor/treestyletab/issues/2922 ) */)
continue;
result.push(info);
}
return result.sort((a, b) => a.name > b.name);
}
export function getDeviceName(id) {
const devices = configs.syncDevices || {};
if (!(id in devices) || !devices[id])
return browser.i18n.getMessage('syncDeviceUnknownDevice');
return String(devices[id].name || '').trim() || browser.i18n.getMessage('syncDeviceMissingDeviceName');
}
// https://searchfox.org/mozilla-central/rev/d866b96d74ec2a63f09ee418f048d23f4fd379a2/browser/base/content/browser-sync.js#1176
export function isSendableTab(tab) {
if (!tab.url ||
tab.url.length > 65535)
return false;
if (!isSendableTab.unsendableUrlMatcher)
isSendableTab.unsendableUrlMatcher = new RegExp(configs.syncUnsendableUrlPattern);
return !isSendableTab.unsendableUrlMatcher.test(tab.url);
}
export function sendTabsToDevice(tabs, { to, recursively } = {}) {
if (recursively)
tabs = Tab.collectRootTabs(tabs).map(tab => [tab, ...tab.$TST.descendants]).flat();
tabs = tabs.filter(isSendableTab);
if (mExternalProvider)
return mExternalProvider.sendTabsToDevice(tabs, to);
sendMessage(to, getTabsDataToSend(tabs));
const multiple = tabs.length > 1 ? '_multiple' : '';
notify({
title: browser.i18n.getMessage(
`sentTabs_notification_title${multiple}`,
[getDeviceName(to)]
),
message: browser.i18n.getMessage(
`sentTabs_notification_message${multiple}`,
[getDeviceName(to)]
),
timeout: configs.syncSentTabsNotificationTimeout
});
}
export async function sendTabsToAllDevices(tabs, { recursively } = {}) {
if (recursively)
tabs = Tab.collectRootTabs(tabs).map(tab => [tab, ...tab.$TST.descendants]).flat();
tabs = tabs.filter(isSendableTab);
if (mExternalProvider)
return mExternalProvider.sendTabsToAllDevices(tabs);
const data = getTabsDataToSend(tabs);
const devices = await getOtherDevices();
for (const device of devices) {
sendMessage(device.id, data);
}
const multiple = tabs.length > 1 ? '_multiple' : '';
notify({
title: browser.i18n.getMessage(`sentTabsToAllDevices_notification_title${multiple}`),
message: browser.i18n.getMessage(`sentTabsToAllDevices_notification_message${multiple}`),
timeout: configs.syncSentTabsNotificationTimeout
});
}
function getTabsDataToSend(tabs) {
return {
type: Constants.kSYNC_DATA_TYPE_TABS,
tabs: tabs.map(tab => ({ url: tab.url, cookieStoreId: tab.cookieStoreId })),
structure : TreeBehavior.getTreeStructureFromTabs(tabs).map(item => item.parent)
};
}
export function manageDevices(windowId) {
if (mExternalProvider)
return mExternalProvider.manageDevices(windowId);
browser.tabs.create({
windowId,
url: `${Constants.kSHORTHAND_URIS.options}#syncTabsToDeviceOptions`
});
}

View File

@@ -0,0 +1,396 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
// internal operations means operations bypassing WebExtensions' tabs APIs.
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
log as internalLogger,
dumpTab,
mapAndFilter,
configs,
wait,
} from './common.js';
import * as ApiTabs from './api-tabs.js';
import * as Constants from './constants.js';
import * as SidebarConnection from './sidebar-connection.js';
import * as TabsStore from './tabs-store.js';
import * as TabsUpdate from './tabs-update.js';
import { Tab, TreeItem } from '/common/TreeItem.js';
function log(...args) {
internalLogger('common/tabs-internal-operation', ...args);
}
export const onBeforeTabsRemove = new EventListenerManager();
export async function activateTab(tab, { byMouseOperation, keepMultiselection, silently } = {}) {
if (!Constants.IS_BACKGROUND)
throw new Error('Error: TabsInternalOperation.activateTab is available only on the background page, use a `kCOMMAND_ACTIVATE_TAB` message instead.');
tab = TabsStore.ensureLivingItem(tab);
if (!tab)
return;
log('activateTab: ', dumpTab(tab));
const win = TabsStore.windows.get(tab.windowId);
win.internallyFocusingTabs.add(tab.id);
if (byMouseOperation)
win.internallyFocusingByMouseTabs.add(tab.id);
if (silently)
win.internallyFocusingSilentlyTabs.add(tab.id);
let tabs = [tab];
if (tab.$TST.hasOtherHighlighted &&
keepMultiselection) {
const highlightedTabs = Tab.getHighlightedTabs(tab.windowId);
if (highlightedTabs.some(highlightedTab => highlightedTab.id == tab.id)) {
// switch active tab with highlighted state
tabs = tabs.concat(mapAndFilter(highlightedTabs,
highlightedTab => highlightedTab.id != tab.id && highlightedTab || undefined));
}
}
if (tabs.length == 1)
win.tabsToBeHighlightedAlone.add(tab.id);
highlightTabs(tabs);
}
export async function blurTab(bluredTabs, { windowId, silently, keepDiscarded } = {}) {
if (bluredTabs &&
!Array.isArray(bluredTabs))
bluredTabs = [bluredTabs];
const bluredTabIds = new Set(Array.from(bluredTabs || [], tab => tab.id || tab));
// First, try to find successor based on successorTabId from left tabs.
let successorTab = Tab.get(bluredTabs.find(tab => tab.active)?.successorTabId);
const scannedTabIds = new Set();
while (bluredTabIds.has(successorTab?.id) ||
(keepDiscarded &&
successorTab?.discarded)) {
if (scannedTabIds.has(successorTab.id))
break; // prevent infinite loop!
scannedTabIds.add(successorTab.id);
const nextSuccessorTab = (successorTab.successorTabId > 0 && successorTab.successorTabId != successorTab.id) ?
Tab.get(successorTab.successorTabId) :
null;
if (!nextSuccessorTab)
break;
successorTab = nextSuccessorTab;
}
log('blurTab/step 1: found successor = ', successorTab?.id);
// Second, try to detect successor based on their order.
if (!successorTab ||
bluredTabIds.has(successorTab.id) ||
(keepDiscarded &&
successorTab.discarded)) {
if (successorTab)
log(' => it cannot become the successor, find again');
let bluredTabsFound = false;
for (const tab of Tab.getVisibleTabs(windowId || bluredTabs[0].windowId)) {
if (keepDiscarded &&
tab.discarded) {
continue;
}
const blured = bluredTabIds.has(tab.id);
if (blured)
bluredTabsFound = true;
if (!bluredTabsFound)
successorTab = tab;
if (bluredTabsFound &&
!blured) {
successorTab = tab;
break;
}
}
log('blurTab/step 2: found successor = ', successorTab?.id);
}
if (successorTab)
await activateTab(successorTab, { silently });
return successorTab;
}
export function removeTab(tab) {
return removeTabs([tab]);
}
export async function removeTabs(tabs, { keepDescendants, byMouseOperation, originalStructure, triggerTab } = {}) {
if (!Constants.IS_BACKGROUND)
throw new Error('TabsInternalOperation.removeTabs is available only on the background page, use a `kCOMMAND_REMOVE_TABS_INTERNALLY` message instead.');
log('TabsInternalOperation.removeTabs: ', () => tabs.map(dumpTab));
if (tabs.length == 0)
return;
await onBeforeTabsRemove.dispatch(tabs);
const win = TabsStore.windows.get(tabs[0].windowId);
const tabIds = [];
let willChangeFocus = false;
tabs = tabs.filter(tab => {
if ((!win ||
!win.internalClosingTabs.has(tab.id)) &&
TabsStore.ensureLivingItem(tab)) {
tabIds.push(tab.id);
if (tab.active)
willChangeFocus = true;
return true;
}
return false;
});
log(' => ', () => tabs.map(dumpTab));
if (!tabs.length)
return;
if (win) {
// Flag tabs to be closed at a time. With this flag TST skips some
// operations on tab close (for example, opening a group tab to replace
// a closed parent tab to keep the tree structure).
for (const tab of tabs) {
win.internalClosingTabs.add(tab.id);
tab.$TST.addState(Constants.kTAB_STATE_TO_BE_REMOVED);
clearCache(tab);
if (keepDescendants)
win.keepDescendantsTabs.add(tab.id);
if (willChangeFocus && byMouseOperation) {
win.internallyFocusingByMouseTabs.add(tab.id);
setTimeout(() => { // the operation can be canceled
win.internallyFocusingByMouseTabs.delete(tab.id);
}, 250);
}
}
}
const sortedTabs = TreeItem.sort(Array.from(tabs));
Tab.onMultipleTabsRemoving.dispatch(sortedTabs, { triggerTab, originalStructure });
const promisedRemoved = browser.tabs.remove(tabIds).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
if (win) {
promisedRemoved.then(() => {
// "beforeunload" listeners in tabs blocks the operation and the
// returned promise is resolved after all "beforeunload" listeners
// are processed and "browser.tabs.onRemoved()" listeners are
// processed for really closed tabs.
// In other words, there may be some "canceled tab close"s and
// we need to clear "to-be-closed" flags for such tabs.
// See also: https://github.com/piroor/treestyletab/issues/2384
const canceledTabs = new Set(tabs.filter(tab => tab.$TST && !tab.$TST.destroyed));
log(`${canceledTabs.size} tabs may be canceled to close.`);
if (canceledTabs.size == 0) {
Tab.onMultipleTabsRemoved.dispatch(sortedTabs, { triggerTab, originalStructure });
return;
}
log(`Clearing "to-be-removed" flag for requested ${tabs.length} tabs...`);
for (const tab of canceledTabs) {
tab.$TST.removeState(Constants.kTAB_STATE_TO_BE_REMOVED);
win.internalClosingTabs.delete(tab.id);
if (keepDescendants)
win.keepDescendantsTabs.delete(tab.id);
}
Tab.onMultipleTabsRemoved.dispatch(sortedTabs.filter(tab => !canceledTabs.has(tab)), { triggerTab, originalStructure });
});
}
return promisedRemoved;
}
export function setTabActive(tab) {
const oldActiveTabs = clearOldActiveStateInWindow(tab.windowId, tab);
tab.active = true;
tab.$TST.addState(Constants.kTAB_STATE_ACTIVE);
tab.$TST.removeState(Constants.kTAB_STATE_NOT_ACTIVATED_SINCE_LOAD);
tab.$TST.removeState(Constants.kTAB_STATE_UNREAD, { permanently: true });
tab.$TST.removeState(Constants.kTAB_STATE_PENDING, { broadcast: Constants.IS_BACKGROUND });
TabsStore.activeTabsInWindow.get(tab.windowId).add(tab);
TabsStore.activeTabInWindow.set(tab.windowId, tab);
Tab.onActivated.dispatch(tab);
return oldActiveTabs;
}
export function clearOldActiveStateInWindow(windowId, exception) {
const oldTabs = TabsStore.activeTabsInWindow.get(windowId);
for (const oldTab of oldTabs) {
if (oldTab.id == exception?.id)
continue;
oldTab.$TST.removeState(Constants.kTAB_STATE_ACTIVE);
oldTab.active = false;
oldTabs.delete(oldTab);
Tab.onUnactivated.dispatch(oldTab);
}
return Array.from(oldTabs);
}
export function clearCache(tab) {
if (!tab)
return;
const errorHandler = ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError);
for (const key of Constants.kCACHE_KEYS) {
browser.sessions.removeTabValue(tab.id, key).catch(errorHandler);
}
}
// Note: this treats the first specified tab as active.
export async function highlightTabs(tabs, { inheritToCollapsedDescendants } = {}) {
if (!Constants.IS_BACKGROUND)
throw new Error('TabsInternalOperation.highlightTabs is available only on the background page, use a `kCOMMAND_HIGHLIGHT_TABS` message instead.');
if (!tabs || tabs.length == 0)
throw new Error('TabsInternalOperation.highlightTabs requires one or more tabs.');
const highlightedTabs = Tab.getHighlightedTabs(tabs[0].windowId);
if (tabs.map(tab => tab.id).join('\n') == highlightedTabs.map(tab => tab.id).join('\n')) {
log('highlightTabs: already highlighted');
return;
}
log('setting tabs highlighted ', tabs, { inheritToCollapsedDescendants });
const startAtTimestamp = Date.now();
const startAt = `${Date.now()}-${parseInt(Math.random() * 65000)}`;
highlightTabs.lastStartedAt = startAt;
const windowId = tabs[0].windowId;
const win = TabsStore.windows.get(windowId);
win.highlightingTabs.clear();
win.tabsMovedWhileHighlighting = false;
const tabIds = tabs.map(tab => {
win.highlightingTabs.add(tab.id);
return tab.id;
});
const toBeHighlightedTabIds = new Set([...win.highlightingTabs]);
TabsUpdate.updateTabsHighlighted({
windowId,
tabIds,
inheritToCollapsedDescendants,
});
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_HIGHLIGHTED_TABS_CHANGED,
windowId,
tabIds,
});
// for better performance, we should not call browser.tabs.update() for each tab.
const highlightedTabIds = new Set(tabIds);
const activeTab = Tab.getActiveTab(windowId);
const indices = mapAndFilter(highlightedTabIds,
id => id == activeTab.id ? undefined : Tab.get(id).index);
if (highlightedTabIds.has(activeTab.id))
indices.unshift(activeTab.index);
// highlight tabs progressively, because massinve change at once may block updating of highlighted appearance of tabs.
let count = 1; // 1 is for setActive()
while (highlightTabs.lastStartedAt == startAt) {
count += (configs.provressiveHighlightingStep <= 0 ? Number.MAX_SAFE_INTEGER : configs.provressiveHighlightingStep);
await browser.tabs.highlight({
windowId,
populate: false,
tabs: indices.slice(0, count),
}).catch(ApiTabs.createErrorSuppressor());
const progress = Math.ceil(Math.min(indices.length, count) / indices.length * 100);
log(`highlightTabs: ${progress} %`);
await wait(configs.progressievHighlightingInterval);
if (win.tabsMovedWhileHighlighting) {
log('highlightTabs: tabs are moved while highlighting, retry');
await wait(250);
return highlightTabs(tabs, { inheritToCollapsedDescendants });
}
if (win.highlightingTabs.size < toBeHighlightedTabIds.size) {
log('highlightTabs: someone cleared multiselection while in-progress ', toBeHighlightedTabIds.size, win.highlightingTabs.size);
break;
}
const unifiedHighlightTabIds = new Set([...toBeHighlightedTabIds, ...win.highlightingTabs]);
if (unifiedHighlightTabIds.size != toBeHighlightedTabIds.size) {
log('highlightTabs: someone tried multiselection again while in-progress ', toBeHighlightedTabIds.size, win.highlightingTabs.size);
break;
}
if (count >= indices.length)
break;
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TABS_HIGHLIGHTING_IN_PROGRESS,
windowId,
progress,
});
}
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TABS_HIGHLIGHTING_COMPLETE,
windowId,
});
log('highlightTabs done. ', Date.now() - startAtTimestamp, ' msec');
}
SidebarConnection.onMessage.addListener(async (windowId, message) => {
switch (message.type) {
case Constants.kCOMMAND_ACTIVATE_TAB: {
await Tab.waitUntilTracked(message.tabId);
const tab = Tab.get(message.tabId);
if (!tab)
return;
activateTab(tab, {
byMouseOperation: message.byMouseOperation,
keepMultiselection: message.keepMultiselection,
silently: message.silently
});
}; break;
case Constants.kCOMMAND_HIGHLIGHT_TABS: {
await Tab.waitUntilTracked(message.tabIds);
highlightTabs(message.tabIds.map(id => Tab.get(id)), {
inheritToCollapsedDescendants: message.inheritToCollapsedDescendants,
});
}; break;
case Constants.kCOMMAND_REMOVE_TABS_INTERNALLY:
await Tab.waitUntilTracked(message.tabIds);
removeTabs(message.tabIds.map(id => Tab.get(id)), {
byMouseOperation: message.byMouseOperation,
keepDescendants: message.keepDescendants
});
break;
case Constants.kCOMMAND_REMOVE_TABS_BY_MOUSE_OPERATION:
await Tab.waitUntilTracked(message.tabIds);
removeTabs(message.tabIds.map(id => Tab.get(id)), {
byMouseOperation: true,
keepDescendants: message.keepDescendants
});
break;
}
});
if (Constants.IS_BACKGROUND) {
browser.runtime.onMessage.addListener((message, _sender) => {
switch (message.type) {
// for operations from group-tab.html
case Constants.kCOMMAND_REMOVE_TABS_INTERNALLY:
Tab.waitUntilTracked(message.tabIds).then(() => {
removeTabs(message.tabIds.map(id => Tab.get(id)), {
byMouseOperation: message.byMouseOperation,
keepDescendants: message.keepDescendants,
});
});
break;
// for automated tests
case Constants.kCOMMAND_REMOVE_TABS_BY_MOUSE_OPERATION:
Tab.waitUntilTracked(message.tabIds).then(() => {
removeTabs(message.tabIds.map(id => Tab.get(id)), {
byMouseOperation: true
});
});
break;
}
});
}

View File

@@ -0,0 +1,798 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
configs
} from './common.js';
import * as Constants from './constants.js';
// eslint-disable-next-line no-unused-vars
function log(...args) {
internalLogger('common/tabs', ...args);
}
let mTargetWindow;
export function setCurrentWindowId(targetWindow) {
return mTargetWindow = targetWindow;
}
export function getCurrentWindowId() {
return mTargetWindow;
}
//===================================================================
// Tab Tracking
//===================================================================
export const windows = new Map();
export const tabs = new Map();
export const tabGroups = new Map();
export const tabsByUniqueId = new Map();
export function clear() {
tabs.clear();
tabsByUniqueId.clear();
}
export const queryLogs = [];
const MAX_LOGS = 100000;
const MATCHING_ATTRIBUTES = `
active
attention
audible
autoDiscardable
cookieStoreId
discarded
favIconUrl
hidden
highlighted
id
incognito
index
isArticle
isInReaderMode
pinned
sessionId
status
successorId
title
url
`.trim().split(/\s+/);
export function queryAll(query) {
if (configs.loggingQueries) {
queryLogs.push(query);
queryLogs.splice(0, Math.max(0, queryLogs.length - MAX_LOGS));
if (query.tabs?.name)
query.indexedTabs = query.tabs.name;
}
fixupQuery(query);
const startAt = Date.now();
if (query.windowId || query.ordered) {
let tabs = [];
for (const win of windows.values()) {
if (query.windowId && !matched(win.id, query.windowId))
continue;
const [sourceTabs, offset] = sourceTabsForQuery(query, win);
tabs = tabs.concat(query.iterator ? getMatchedTabsIterator(sourceTabs, query, offset) : extractMatchedTabs(sourceTabs, query, offset));
}
query.elapsed = Date.now() - startAt;
if (query.iterator) {
return (function* () {
for (const tabsIterator of tabs) {
for (const tab of tabsIterator) {
yield tab;
}
}
})();
}
else {
return tabs;
}
}
else {
const sourceTabs = (query.tabs || tabs).values();
const matchedTabs = query.iterator ? getMatchedTabsIterator(sourceTabs, query) : extractMatchedTabs(sourceTabs, query);
query.elapsed = Date.now() - startAt;
return matchedTabs;
}
}
function sourceTabsForQuery(query, win) {
let offset = 0;
if (!query.ordered)
return [query.tabs?.values() || win.tabs.values(), offset];
let fromId;
let toId = query.toId;
if (typeof query.index == 'number') {
fromId = win.order[query.index];
offset = query.index;
}
if (typeof query.fromIndex == 'number') {
fromId = win.order[query.fromIndex];
offset = query.fromIndex;
}
if (typeof query.toIndex == 'number') {
toId = win.order[query.toIndex];
}
if (typeof fromId != 'number') {
fromId = query.fromId;
offset = win.order.indexOf(query.fromId);
}
if (query.last || query.reversed)
return [win.getReversedOrderedTabs(fromId, toId, query.tabs), offset];
return [win.getOrderedTabs(fromId, toId, query.tabs), offset];
}
function extractMatchedTabs(tabsInStore, query, offset) {
const matchedTabs = [];
let firstTime = true;
let logicalIndex = offset || 0;
for (const tab of tabsInStore) {
if (!tabs.has(tab.id) ||
(!query.skipMatching &&
!matchedWithQuery(tab, query)))
continue;
if (!firstTime)
logicalIndex++;
firstTime = false;
if ('logicalIndex' in query &&
!matched(logicalIndex, query.logicalIndex))
continue;
matchedTabs.push(tab);
if (query.first || query.last)
break;
}
return matchedTabs;
}
function* getMatchedTabsIterator(tabsInStore, query, offset) {
let firstTime = true;
let logicalIndex = offset || 0;
for (const tab of tabsInStore) {
if (!tabs.has(tab.id) ||
(!query.skipMatching &&
!matchedWithQuery(tab, query)))
continue;
if (!firstTime)
logicalIndex++;
firstTime = false;
if ('logicalIndex' in query &&
!matched(logicalIndex, query.logicalIndex))
continue;
yield tab;
if (query.first || query.last)
break;
}
}
function matchedWithQuery(tab, query) {
for (const attribute of MATCHING_ATTRIBUTES) {
if (attribute in query &&
!matched(tab[attribute], query[attribute]))
return false;
const invertedAttribute = `!${attribute}`;
if (invertedAttribute in query &&
matched(tab[attribute], query[invertedAttribute]))
return false;
}
if (!tab.$TST)
return false;
const tabStates = tab.$TST.states;
if ('states' in query && tabStates) {
const queryStates = query.states;
for (let i = 0, maxi = queryStates.length; i < maxi; i += 2) {
const state = queryStates[i];
const pattern = queryStates[i+1];
if (!matched(tabStates.has(state), pattern))
return false;
}
}
const tabAttributes = tab.$TST.attributes;
if ('attributes' in query && tabAttributes) {
const queryAttributes = query.attributes;
for (let i = 0, maxi = queryAttributes.length; i < maxi; i += 2) {
const attribute = queryAttributes[i];
const pattern = queryAttributes[i+1];
if (!matched(tabAttributes[attribute], pattern))
return false;
}
}
if (query.living &&
!ensureLivingItem(tab))
return false;
if (query.normal &&
(tab.hidden ||
tabStates.has(Constants.kTAB_STATE_SHOWING) ||
tab.pinned))
return false;
if (query.pinned &&
(tab.hidden ||
tabStates.has(Constants.kTAB_STATE_SHOWING) ||
!tab.pinned))
return false;
if (query.visible &&
((tabStates.has(Constants.kTAB_STATE_COLLAPSED) &&
!tabStates.has(Constants.kTAB_STATE_EXPANDING)) ||
tab.hidden ||
tabStates.has(Constants.kTAB_STATE_SHOWING)))
return false;
if (query.hidden &&
!tab.hidden)
return false;
if (query.controllable &&
(tab.hidden ||
tabStates.has(Constants.kTAB_STATE_SHOWING)))
return false;
if ('hasChild' in query &&
query.hasChild != tab.$TST.hasChild)
return false;
if ('hasParent' in query &&
query.hasParent != tab.$TST.hasParent)
return false;
if ('childOf' in query &&
!tab.$TST.parentId != query.childOf)
return false;
if ('descendantOf' in query &&
!tab.$TST.ancestorIds.includes(query.descendantOf))
return false;
if (query.groupId &&
tab.groupId != query.groupId)
return false;
return true;
}
function matched(value, pattern) {
if (pattern instanceof RegExp &&
!pattern.test(String(value)))
return false;
if (pattern instanceof Set &&
!pattern.has(value))
return false;
if (Array.isArray(pattern) &&
!pattern.includes(value))
return false;
if (typeof pattern == 'function' &&
!pattern(value))
return false;
if (typeof pattern == 'boolean' &&
!!value !== pattern)
return false;
if (typeof pattern == 'string' &&
String(value || '') != pattern)
return false;
if (typeof pattern == 'number' &&
value != pattern)
return false;
return true;
}
export function query(query) {
if (configs.loggingQueries) {
queryLogs.push(query);
queryLogs.splice(0, Math.max(0, queryLogs.length - MAX_LOGS));
if (query.tabs?.name)
query.indexedTabs = query.tabs.name;
}
fixupQuery(query);
if (query.last)
query.ordered = true;
else
query.first = true;
const startAt = Date.now();
let tabs = [];
if (query.windowId || query.ordered) {
for (const win of windows.values()) {
if (query.windowId && !matched(win.id, query.windowId))
continue;
const [sourceTabs, offset] = sourceTabsForQuery(query, win);
tabs = tabs.concat(extractMatchedTabs(sourceTabs, query, offset));
if (tabs.length > 0)
break;
}
}
else {
tabs = extractMatchedTabs((query.tabs ||tabs).values(), query);
}
query.elapsed = Date.now() - startAt;
return tabs.length > 0 ? tabs[0] : null ;
}
function fixupQuery(query) {
if (query.fromId ||
query.toId ||
typeof query.index == 'number' ||
typeof query.fromIndex == 'number' ||
typeof query.logicalIndex == 'number')
query.ordered = true;
if ((query.normal ||
query.visible ||
query.controllable ||
query.pinned) &&
!('living' in query))
query.living = true;
}
//===================================================================
// Indexes for optimization
//===================================================================
export const activeTabInWindow = new Map();
export const activeTabsInWindow = new Map();
export const bundledActiveTabsInWindow = new Map();
export const livingTabsInWindow = new Map();
export const controllableTabsInWindow = new Map();
export const removingTabsInWindow = new Map();
export const removedTabsInWindow = new Map();
export const visibleTabsInWindow = new Map();
export const expandedTabsInWindow = new Map();
export const selectedTabsInWindow = new Map();
export const highlightedTabsInWindow = new Map();
export const pinnedTabsInWindow = new Map();
export const unpinnedTabsInWindow = new Map();
export const rootTabsInWindow = new Map();
export const groupTabsInWindow = new Map();
export const toBeExpandedTabsInWindow = new Map();
export const subtreeCollapsableTabsInWindow = new Map();
export const draggingTabsInWindow = new Map();
export const duplicatingTabsInWindow = new Map();
export const toBeGroupedTabsInWindow = new Map();
export const nativelyGroupedTabsInWindow = new Map();
export const loadingTabsInWindow = new Map();
export const unsynchronizedTabsInWindow = new Map();
export const virtualScrollRenderableTabsInWindow = new Map();
export const scrollPositionCalculationTargetTabsInWindow = new Map();
export const canBecomeStickyTabsInWindow = new Map();
function createMapWithName(name) {
const map = new Map();
map.name = name;
return map;
}
export function prepareIndexesForWindow(windowId) {
activeTabsInWindow.set(windowId, new Set());
bundledActiveTabsInWindow.set(windowId, createMapWithName(`bundled active tabs in window ${windowId}`));
livingTabsInWindow.set(windowId, createMapWithName(`living tabs in window ${windowId}`));
controllableTabsInWindow.set(windowId, createMapWithName(`controllable tabs in window ${windowId}`));
removingTabsInWindow.set(windowId, createMapWithName(`removing tabs in window ${windowId}`));
removedTabsInWindow.set(windowId, createMapWithName(`removed tabs in window ${windowId}`));
visibleTabsInWindow.set(windowId, createMapWithName(`visible tabs in window ${windowId}`));
expandedTabsInWindow.set(windowId, createMapWithName(`expanded tabs in window ${windowId}`));
selectedTabsInWindow.set(windowId, createMapWithName(`selected tabs in window ${windowId}`));
highlightedTabsInWindow.set(windowId, createMapWithName(`highlighted tabs in window ${windowId}`));
pinnedTabsInWindow.set(windowId, createMapWithName(`pinned tabs in window ${windowId}`));
unpinnedTabsInWindow.set(windowId, createMapWithName(`unpinned tabs in window ${windowId}`));
rootTabsInWindow.set(windowId, createMapWithName(`root tabs in window ${windowId}`));
groupTabsInWindow.set(windowId, createMapWithName(`group tabs in window ${windowId}`));
toBeExpandedTabsInWindow.set(windowId, createMapWithName(`to-be-expanded tabs in window ${windowId}`));
subtreeCollapsableTabsInWindow.set(windowId, createMapWithName(`collapsable parent tabs in window ${windowId}`));
draggingTabsInWindow.set(windowId, createMapWithName(`dragging tabs in window ${windowId}`));
duplicatingTabsInWindow.set(windowId, createMapWithName(`duplicating tabs in window ${windowId}`));
toBeGroupedTabsInWindow.set(windowId, createMapWithName(`to-be-grouped tabs in window ${windowId}`));
nativelyGroupedTabsInWindow.set(windowId, createMapWithName(`natively grouped tabs in window ${windowId}`));
loadingTabsInWindow.set(windowId, createMapWithName(`loading tabs in window ${windowId}`));
unsynchronizedTabsInWindow.set(windowId, createMapWithName(`unsynchronized tabs in window ${windowId}`));
virtualScrollRenderableTabsInWindow.set(windowId, createMapWithName(`virtual scroll renderable tabs in window ${windowId}`));
scrollPositionCalculationTargetTabsInWindow.set(windowId, createMapWithName(`scroll position calculation target tabs in window ${windowId}`));
canBecomeStickyTabsInWindow.set(windowId, createMapWithName(`can become sticky tabs in window ${windowId}`));
}
export function unprepareIndexesForWindow(windowId) {
activeTabInWindow.delete(windowId);
activeTabsInWindow.delete(windowId);
bundledActiveTabsInWindow.delete(windowId);
livingTabsInWindow.delete(windowId);
controllableTabsInWindow.delete(windowId);
removingTabsInWindow.delete(windowId);
removedTabsInWindow.delete(windowId);
visibleTabsInWindow.delete(windowId);
expandedTabsInWindow.delete(windowId);
selectedTabsInWindow.delete(windowId);
highlightedTabsInWindow.delete(windowId);
pinnedTabsInWindow.delete(windowId);
unpinnedTabsInWindow.delete(windowId);
rootTabsInWindow.delete(windowId);
groupTabsInWindow.delete(windowId);
toBeExpandedTabsInWindow.delete(windowId);
subtreeCollapsableTabsInWindow.delete(windowId);
toBeGroupedTabsInWindow.delete(windowId);
nativelyGroupedTabsInWindow.delete(windowId);
loadingTabsInWindow.delete(windowId);
unsynchronizedTabsInWindow.delete(windowId);
virtualScrollRenderableTabsInWindow.delete(windowId);
scrollPositionCalculationTargetTabsInWindow.delete(windowId);
canBecomeStickyTabsInWindow.delete(windowId);
}
export function getTabsMap(tabsStore, windowId = null) {
return windowId ?
tabsStore.get(windowId) :
new Map([...tabsStore.values()].map(tabs => [...tabs.entries()]).flat());
}
export function updateIndexesForTab(tab) {
addLivingTab(tab);
if (!tab.hidden)
addControllableTab(tab);
else
removeControllableTab(tab);
if (tab.$TST.collapsed)
removeExpandedTab(tab);
else
addExpandedTab(tab);
if (tab.hidden || tab.$TST.collapsed)
removeVisibleTab(tab);
else
addVisibleTab(tab);
if (tab.$TST.states.has(Constants.kTAB_STATE_SELECTED))
addSelectedTab(tab);
else
removeSelectedTab(tab);
if (tab.highlighted)
addHighlightedTab(tab);
else
removeHighlightedTab(tab);
if (tab.pinned) {
removeUnpinnedTab(tab);
addPinnedTab(tab);
}
else {
removePinnedTab(tab);
addUnpinnedTab(tab);
}
if (tab.$TST.isGroupTab)
addGroupTab(tab);
else
removeGroupTab(tab);
if (tab.$TST.duplicating)
addDuplicatingTab(tab);
else
removeDuplicatingTab(tab);
if (tab.$TST.getAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID) &&
!tab.$TST.getAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER))
addToBeGroupedTab(tab);
else
removeToBeGroupedTab(tab);
if (tab.$TST.parent)
removeRootTab(tab);
else
addRootTab(tab);
if (tab.$TST.hasChild &&
!tab.$TST.subtreeCollapsed &&
!tab.$TST.collapsed)
addSubtreeCollapsableTab(tab);
else
removeSubtreeCollapsableTab(tab);
if (tab.status == 'loading')
addLoadingTab(tab);
else
removeLoadingTab(tab);
if (tab.$TST.states.has(Constants.kTAB_STATE_BUNDLED_ACTIVE))
addBundledActiveTab(tab);
else
removeBundledActiveTab(tab);
if (tab.groupId && tab.groupId != -1)
addNativelyGroupedTab(tab);
else
removeNativelyGroupedTab(tab);
updateVirtualScrollRenderabilityIndexForTab(tab);
if (tab.$TST.states.has(Constants.kTAB_STATE_COLLAPSED_DONE))
removeScrollPositionCalculationTargetTab(tab);
else
addScrollPositionCalculationTargetTab(tab);
if (tab.$TST.canBecomeSticky)
addCanBecomeStickyTab(tab);
else
removeCanBecomeStickyTab(tab);
}
export function updateVirtualScrollRenderabilityIndexForTab(tab) {
if (tab.pinned ||
(tab.hidden &&
((tab.url == 'about:firefoxview' &&
tab.cookieStoreId == 'firefox-default') ||
!configs.renderHiddenTabs)) ||
tab.$TST.states.has(Constants.kTAB_STATE_COLLAPSED_DONE) ||
(tabGroups.get(tab.groupId)?.$TST.states.has(Constants.kTAB_STATE_COLLAPSED_DONE) &&
!tab.active))
removeVirtualScrollRenderableTab(tab);
else
addVirtualScrollRenderableTab(tab);
}
export function removeTabFromIndexes(tab) {
removeBundledActiveTab(tab);
removeLivingTab(tab);
removeControllableTab(tab);
removeRemovingTab(tab);
//removeRemovedTab(tab);
removeVisibleTab(tab);
removeExpandedTab(tab);
removeSelectedTab(tab);
removeHighlightedTab(tab);
removePinnedTab(tab);
removeUnpinnedTab(tab);
removeRootTab(tab);
removeGroupTab(tab);
removeToBeExpandedTab(tab);
removeSubtreeCollapsableTab(tab);
removeDuplicatingTab(tab);
removeDraggingTab(tab);
removeToBeGroupedTab(tab);
removeNativelyGroupedTab(tab);
removeLoadingTab(tab);
removeUnsynchronizedTab(tab);
//removeVirtualScrollRenderableTab(tab);
removeScrollPositionCalculationTargetTab(tab);
removeCanBecomeStickyTab(tab);
}
function addTabToIndex(tab, indexes, windowId = null) {
if (!tab)
throw new Error(`TabsStore.addTabToIndex gets non-tab parameter!: ${JSON.stringify(tab)} : ${new Error().stack}`);
const tabs = indexes.get(windowId || tab.windowId);
if (tabs)
tabs.set(tab.id, tab);
}
function removeTabFromIndex(tab, indexes, windowId = null) {
if (!tab)
throw new Error(`TabsStore.removeTabFromIndex gets non-tab parameter!: ${JSON.stringify(tab)} : ${new Error().stack}`);
const tabs = indexes.get(windowId || tab.windowId);
if (tabs)
tabs.delete(tab.id);
}
export function addLivingTab(tab) {
addTabToIndex(tab, livingTabsInWindow);
}
export function removeLivingTab(tab) {
removeTabFromIndex(tab, livingTabsInWindow);
}
export function addControllableTab(tab) {
addTabToIndex(tab, controllableTabsInWindow);
}
export function removeControllableTab(tab) {
removeTabFromIndex(tab, controllableTabsInWindow);
}
export function addRemovingTab(tab) {
addTabToIndex(tab, removingTabsInWindow);
removeTabFromIndexes(tab);
}
export function removeRemovingTab(tab) {
removeTabFromIndex(tab, removingTabsInWindow);
}
export function addRemovedTab(tab) {
addTabToIndex(tab, removedTabsInWindow);
setTimeout(removeRemovedTab, 100000, {
id: tab.id,
windowId: tab.windowId
});
}
export function removeRemovedTab(tab) {
removeTabFromIndex(tab, removedTabsInWindow);
}
export function addVisibleTab(tab) {
addTabToIndex(tab, visibleTabsInWindow);
}
export function removeVisibleTab(tab) {
removeTabFromIndex(tab, visibleTabsInWindow);
}
export function addExpandedTab(tab) {
addTabToIndex(tab, expandedTabsInWindow);
}
export function removeExpandedTab(tab) {
removeTabFromIndex(tab, expandedTabsInWindow);
}
export function addSelectedTab(tab) {
addTabToIndex(tab, selectedTabsInWindow);
}
export function removeSelectedTab(tab) {
removeTabFromIndex(tab, selectedTabsInWindow);
}
export function addHighlightedTab(tab) {
addTabToIndex(tab, highlightedTabsInWindow);
}
export function removeHighlightedTab(tab) {
removeTabFromIndex(tab, highlightedTabsInWindow);
}
export function addPinnedTab(tab) {
addTabToIndex(tab, pinnedTabsInWindow);
}
export function removePinnedTab(tab) {
removeTabFromIndex(tab, pinnedTabsInWindow);
}
export function addUnpinnedTab(tab) {
addTabToIndex(tab, unpinnedTabsInWindow);
}
export function removeUnpinnedTab(tab) {
removeTabFromIndex(tab, unpinnedTabsInWindow);
}
export function addRootTab(tab) {
addTabToIndex(tab, rootTabsInWindow);
}
export function removeRootTab(tab) {
removeTabFromIndex(tab, rootTabsInWindow);
}
export function addGroupTab(tab) {
addTabToIndex(tab, groupTabsInWindow);
}
export function removeGroupTab(tab) {
removeTabFromIndex(tab, groupTabsInWindow);
}
export function addToBeExpandedTab(tab) {
addTabToIndex(tab, toBeExpandedTabsInWindow);
}
export function removeToBeExpandedTab(tab) {
removeTabFromIndex(tab, toBeExpandedTabsInWindow);
}
export function addSubtreeCollapsableTab(tab) {
addTabToIndex(tab, subtreeCollapsableTabsInWindow);
}
export function removeSubtreeCollapsableTab(tab) {
removeTabFromIndex(tab, subtreeCollapsableTabsInWindow);
}
export function addDuplicatingTab(tab) {
addTabToIndex(tab, duplicatingTabsInWindow);
}
export function removeDuplicatingTab(tab) {
removeTabFromIndex(tab, duplicatingTabsInWindow);
}
export function addDraggingTab(tab) {
addTabToIndex(tab, draggingTabsInWindow);
}
export function removeDraggingTab(tab) {
removeTabFromIndex(tab, draggingTabsInWindow);
}
export function addToBeGroupedTab(tab) {
addTabToIndex(tab, toBeGroupedTabsInWindow);
}
export function removeToBeGroupedTab(tab) {
removeTabFromIndex(tab, toBeGroupedTabsInWindow);
}
export function addNativelyGroupedTab(tab, windowId = null) {
addTabToIndex(tab, nativelyGroupedTabsInWindow, windowId);
}
export function removeNativelyGroupedTab(tab, windowId = null) {
removeTabFromIndex(tab, nativelyGroupedTabsInWindow, windowId);
}
export function addLoadingTab(tab) {
addTabToIndex(tab, loadingTabsInWindow);
}
export function removeLoadingTab(tab) {
removeTabFromIndex(tab, loadingTabsInWindow);
}
export function addUnsynchronizedTab(tab) {
addTabToIndex(tab, unsynchronizedTabsInWindow);
}
export function removeUnsynchronizedTab(tab) {
removeTabFromIndex(tab, unsynchronizedTabsInWindow);
}
export function addBundledActiveTab(tab) {
addTabToIndex(tab, bundledActiveTabsInWindow);
}
export function removeBundledActiveTab(tab) {
removeTabFromIndex(tab, bundledActiveTabsInWindow);
}
export function addVirtualScrollRenderableTab(tab) {
addTabToIndex(tab, virtualScrollRenderableTabsInWindow);
}
export function removeVirtualScrollRenderableTab(tab) {
removeTabFromIndex(tab, virtualScrollRenderableTabsInWindow);
}
export function addScrollPositionCalculationTargetTab(tab) {
addTabToIndex(tab, scrollPositionCalculationTargetTabsInWindow);
}
export function removeScrollPositionCalculationTargetTab(tab) {
removeTabFromIndex(tab, scrollPositionCalculationTargetTabsInWindow);
}
export function addCanBecomeStickyTab(tab) {
addTabToIndex(tab, canBecomeStickyTabsInWindow);
}
export function removeCanBecomeStickyTab(tab) {
removeTabFromIndex(tab, canBecomeStickyTabsInWindow);
}
//===================================================================
// Utilities
//===================================================================
export function assertValidTab(tab) {
if (tab?.$TST)
return;
const error = new Error('FATAL ERROR: invalid tab is given');
console.log(error.message, tab, error.stack);
throw error;
}
export function ensureLivingItem(tab) {
const isNativeTabGroup = tab?.type == 'group';
if (!tab ||
!tab.id ||
!tab.$TST ||
(!isNativeTabGroup && !tabs.has(tab.id)) ||
tab.$TST.removing ||
!windows.get(tab.windowId) ||
(isNativeTabGroup && !windows.get(tab.windowId).tabGroups.has(tab.id)))
return null;
return tab;
}
//===================================================================
// Logging
//===================================================================
browser.runtime.onMessage.addListener((message, _sender) => {
if (!message ||
typeof message != 'object' ||
message.type != Constants.kCOMMAND_REQUEST_QUERY_LOGS)
return;
browser.runtime.sendMessage({
type: Constants.kCOMMAND_RESPONSE_QUERY_LOGS,
logs: JSON.parse(JSON.stringify(queryLogs)),
windowId: mTargetWindow || 'background'
});
});

View File

@@ -0,0 +1,444 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Tree Style Tab.
*
* The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
* Portions created by the Initial Developer are Copyright (C) 2011-2025
* the Initial Developer. All Rights Reserved.
*
* Contributor(s): YUKI "Piro" Hiroshi <piro.outsider.reflex@gmail.com>
* wanabe <https://github.com/wanabe>
* Tetsuharu OHZEKI <https://github.com/saneyuki>
* Xidorn Quan <https://github.com/upsuper> (Firefox 40+ support)
* lv7777 (https://github.com/lv7777)
*
* ***** END LICENSE BLOCK ******/
'use strict';
import {
log as internalLogger,
dumpTab,
wait,
mapAndFilter
} from './common.js';
import * as Constants from './constants.js';
import * as ContextualIdentities from './contextual-identities.js';
import * as SidebarConnection from './sidebar-connection.js';
import * as TabsStore from './tabs-store.js';
import { Tab } from './TreeItem.js';
function log(...args) {
internalLogger('common/tabs-update', ...args);
}
const mBufferedUpdates = new Map();
function getBufferedUpdate(tab) {
const update = mBufferedUpdates.get(tab.id) || {
windowId: tab.windowId,
tabId: tab.id,
attributes: {
updated: {},
added: {},
removed: new Set(),
},
isGroupTab: false,
updatedTitle: undefined,
updatedLabel: undefined,
favIconUrl: undefined,
loadingState: undefined,
loadingStateReallyChanged: undefined,
pinned: undefined,
hidden: undefined,
groupId: undefined,
soundStateChanged: false,
};
mBufferedUpdates.set(tab.id, update);
return update;
}
function flushBufferedUpdates() {
if (!Constants.IS_BACKGROUND) {
mBufferedUpdates.clear();
return;
}
const triedAt = `${Date.now()}-${parseInt(Math.random() * 65000)}`;
flushBufferedUpdates.triedAt = triedAt;
(Constants.IS_BACKGROUND ?
setTimeout : // because window.requestAnimationFrame is decelerate for an invisible document.
window.requestAnimationFrame)(() => {
if (flushBufferedUpdates.triedAt != triedAt)
return;
for (const update of mBufferedUpdates.values()) {
// no need to notify attributes broadcasted via Tab.broadcastState()
delete update.attributes.updated.highlighted;
delete update.attributes.updated.hidden;
delete update.attributes.updated.pinned;
delete update.attributes.updated.audible;
delete update.attributes.updated.mutedInfo;
delete update.attributes.updated.sharingState;
delete update.attributes.updated.incognito;
delete update.attributes.updated.attention;
delete update.attributes.updated.discarded;
if (Object.keys(update.attributes.updated).length > 0 ||
Object.keys(update.attributes.added).length > 0 ||
update.attributes.removed.size > 0 ||
update.soundStateChanged ||
update.sharingStateChanged)
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TAB_UPDATED,
windowId: update.windowId,
tabId: update.tabId,
updatedProperties: update.attributes.updated,
addedAttributes: update.attributes.added,
removedAttributes: [...update.attributes.removed],
soundStateChanged: update.soundStateChanged,
sharingStateChanged: update.sharingStateChanged,
});
// SidebarConnection.sendMessage() has its own bulk-send mechanism,
// so we don't need to bundle them like an array.
if (update.isGroupTab)
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_GROUP_TAB_DETECTED,
windowId: update.windowId,
tabId: update.tabId,
});
if (update.updatedTitle !== undefined)
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TREE_ITEM_LABEL_UPDATED,
windowId: update.windowId,
tabId: update.tabId,
title: update.updatedTitle,
label: update.updatedLabel,
});
if (update.favIconUrl !== undefined)
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TAB_FAVICON_UPDATED,
windowId: update.windowId,
tabId: update.tabId,
favIconUrl: update.favIconUrl,
});
if (update.loadingState !== undefined)
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_UPDATE_LOADING_STATE,
windowId: update.windowId,
tabId: update.tabId,
status: update.loadingState,
reallyChanged: update.loadingStateReallyChanged,
});
if (update.pinned !== undefined)
SidebarConnection.sendMessage({
type: update.pinned ? Constants.kCOMMAND_NOTIFY_TAB_PINNED : Constants.kCOMMAND_NOTIFY_TAB_UNPINNED,
windowId: update.windowId,
tabId: update.tabId,
});
if (update.hidden !== undefined)
SidebarConnection.sendMessage({
type: update.hidden ? Constants.kCOMMAND_NOTIFY_TAB_HIDDEN : Constants.kCOMMAND_NOTIFY_TAB_SHOWN,
windowId: update.windowId,
tabId: update.tabId,
});
}
mBufferedUpdates.clear();
}, 0);
}
export function updateTab(tab, newState = {}, options = {}) {
const update = getBufferedUpdate(tab);
const oldState = options.old || {};
if ('url' in newState) {
tab.$TST.setAttribute(Constants.kCURRENT_URI, update.attributes.added[Constants.kCURRENT_URI] = newState.url);
update.attributes.removed.delete(Constants.kCURRENT_URI);
}
if ('url' in newState &&
newState.url.indexOf(Constants.kGROUP_TAB_URI) == 0) {
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
update.isGroupTab = true;
Tab.onGroupTabDetected.dispatch(tab);
}
else if (tab.$TST.states.has(Constants.kTAB_STATE_GROUP_TAB) &&
!tab.$TST.hasGroupTabURL) {
tab.$TST.removeState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
update.isGroupTab = false;
}
if (options.forceApply ||
('title' in newState &&
newState.title != oldState.title)) {
if (options.forceApply) {
tab.$TST.getPermanentStates().then(states => {
tab.$TST.toggleState(
Constants.kTAB_STATE_UNREAD,
(states.includes(Constants.kTAB_STATE_UNREAD) &&
!tab.$TST.isGroupTab),
{ permanently: true }
);
});
}
else if (tab.$TST.isGroupTab) {
tab.$TST.removeState(Constants.kTAB_STATE_UNREAD, { permanently: true });
}
else if (!tab.active) {
tab.$TST.addState(Constants.kTAB_STATE_UNREAD, { permanently: true });
}
tab.$TST.label = newState.title;
Tab.onLabelUpdated.dispatch(tab);
update.updatedTitle = tab.title;
update.updatedLabel = tab.$TST.label;
}
const openerOfGroupTab = tab.$TST.isGroupTab && Tab.getOpenerFromGroupTab(tab);
if (openerOfGroupTab &&
openerOfGroupTab.favIconUrl) {
update.favIconUrl = openerOfGroupTab.favIconUrl;
}
else if (options.forceApply ||
'favIconUrl' in newState) {
tab.$TST.setAttribute(Constants.kCURRENT_FAVICON_URI, update.attributes.added[Constants.kCURRENT_FAVICON_URI] = tab.favIconUrl);
update.attributes.removed.delete(Constants.kCURRENT_FAVICON_URI);
// "favIconUrl" will be "undefined" if the website has no favicon.
// Keys with "undefined" value will be removed from JSON-stringified result,
// so we need to use "null" instead of it.
// See also: https://github.com/piroor/treestyletab/issues/3515
update.favIconUrl = tab.favIconUrl || null;
}
else if (tab.$TST.isGroupTab) {
// "about:ws-group" can set error icon for the favicon and
// reloading doesn't cloear that, so we need to clear favIconUrl manually.
tab.favIconUrl = null;
delete update.attributes.added[Constants.kCURRENT_FAVICON_URI];
update.attributes.removed.add(Constants.kCURRENT_URI);
tab.$TST.removeAttribute(Constants.kCURRENT_FAVICON_URI);
update.favIconUrl = null;
}
if ('status' in newState) {
const reallyChanged = !tab.$TST.states.has(newState.status);
const removed = newState.status == 'loading' ? 'complete' : 'loading';
tab.$TST.removeState(removed);
tab.$TST.addState(newState.status);
if (!options.forceApply) {
update.loadingState = tab.status;
update.loadingStateReallyChanged = reallyChanged;
}
}
if ((options.forceApply ||
'pinned' in newState) &&
newState.pinned != tab.$TST.states.has(Constants.kTAB_STATE_PINNED)) {
if (newState.pinned) {
tab.$TST.addState(Constants.kTAB_STATE_PINNED);
tab.$TST.removeAttribute(Constants.kLEVEL); // don't indent pinned tabs!
delete update.attributes.added[Constants.kLEVEL];
update.attributes.removed.add(Constants.kLEVEL);
Tab.onPinned.dispatch(tab);
update.pinned = true;
}
else {
tab.$TST.removeState(Constants.kTAB_STATE_PINNED);
Tab.onUnpinned.dispatch(tab);
update.pinned = false;
}
}
if (options.forceApply ||
'audible' in newState)
tab.$TST.toggleState(Constants.kTAB_STATE_AUDIBLE, newState.audible);
let soundStateChanged = false;
if (options.forceApply ||
'mutedInfo' in newState) {
soundStateChanged = true;
const muted = newState.mutedInfo?.muted;
tab.$TST.toggleState(Constants.kTAB_STATE_MUTED, muted, newState.audible);
Tab.onMutedStateChanged.dispatch(tab, muted);
}
if (options.forceApply ||
soundStateChanged ||
'audible' in newState) {
soundStateChanged = true;
tab.$TST.toggleState(
Constants.kTAB_STATE_SOUND_PLAYING,
(tab.audible &&
!tab.mutedInfo.muted)
);
}
if (soundStateChanged) {
const parent = tab.$TST.parent;
if (parent)
parent.$TST.inheritSoundStateFromChildren();
}
let sharingStateChanged = false;
if (options.forceApply ||
'sharingState' in newState) {
sharingStateChanged = true;
const sharingCamera = !!newState.sharingState?.camera;
const sharingMicrophone = !!newState.sharingState?.microphone;
const sharingScreen = !!newState.sharingState?.screen;
tab.$TST.toggleState(Constants.kTAB_STATE_SHARING_CAMERA, sharingCamera);
tab.$TST.toggleState(Constants.kTAB_STATE_SHARING_MICROPHONE, sharingMicrophone);
tab.$TST.toggleState(Constants.kTAB_STATE_SHARING_SCREEN, sharingScreen);
Tab.onSharingStateChanged.dispatch(tab, {
camera: sharingCamera,
microphone: sharingMicrophone,
screen: sharingScreen,
});
const parent = tab.$TST.parent;
if (parent)
parent.$TST.inheritSharingStateFromChildren();
}
if (options.forceApply ||
'cookieStoreId' in newState) {
for (const state of tab.$TST.states) {
if (String(state).startsWith('contextual-identity-'))
tab.$TST.removeState(state);
}
if (newState.cookieStoreId) {
const state = `contextual-identity-${newState.cookieStoreId}`;
tab.$TST.addState(state);
const identity = ContextualIdentities.get(newState.cookieStoreId);
if (identity)
tab.$TST.setAttribute(Constants.kCONTEXTUAL_IDENTITY_NAME, identity.name);
else
tab.$TST.removeAttribute(Constants.kCONTEXTUAL_IDENTITY_NAME);
}
else {
tab.$TST.removeAttribute(Constants.kCONTEXTUAL_IDENTITY_NAME);
}
}
if (options.forceApply ||
'incognito' in newState)
tab.$TST.toggleState(Constants.kTAB_STATE_PRIVATE_BROWSING, newState.incognito);
if (options.forceApply ||
'hidden' in newState) {
if (newState.hidden) {
if (!tab.$TST.states.has(Constants.kTAB_STATE_HIDDEN)) {
tab.$TST.addState(Constants.kTAB_STATE_HIDDEN);
Tab.onHidden.dispatch(tab);
update.hidden = true;
}
}
else if (tab.$TST.states.has(Constants.kTAB_STATE_HIDDEN)) {
tab.$TST.removeState(Constants.kTAB_STATE_HIDDEN);
Tab.onShown.dispatch(tab);
update.hidden = false;
}
}
if (options.forceApply ||
'highlighted' in newState)
tab.$TST.toggleState(Constants.kTAB_STATE_HIGHLIGHTED, newState.highlighted);
if (options.forceApply ||
'attention' in newState)
tab.$TST.toggleState(Constants.kTAB_STATE_ATTENTION, newState.attention);
if (options.forceApply ||
'discarded' in newState) {
wait(0).then(() => {
// Don't set this class immediately, because we need to know
// the newly active tab *was* discarded on onTabClosed handler.
tab.$TST.toggleState(Constants.kTAB_STATE_DISCARDED, newState.discarded);
});
}
if (options.forceApply ||
'groupId' in newState) {
tab.$TST.onNativeGroupModified(oldState.groupId);
update.attributes.added[Constants.kGROUP_ID] = newState.groupId;
}
update.soundStateChanged = update.soundStateChanged || soundStateChanged;
update.sharingStateChanged = update.sharingStateChanged || sharingStateChanged;
update.attributes.updated = {
...update.attributes.updated,
...(newState?.$TST?.sanitized || newState),
};
flushBufferedUpdates();
tab.$TST.invalidateCache();
}
export async function updateTabsHighlighted(highlightInfo) {
if (Tab.needToWaitTracked(highlightInfo.windowId))
await Tab.waitUntilTrackedAll(highlightInfo.windowId);
const win = TabsStore.windows.get(highlightInfo.windowId);
if (!win)
return;
//const startAt = Date.now();
const tabIds = highlightInfo.tabIds; // new Set(highlightInfo.tabIds);
const toBeUnhighlightedTabs = Tab.getHighlightedTabs(highlightInfo.windowId, {
ordered: false,
'!id': tabIds
});
const alreadyHighlightedTabs = TabsStore.highlightedTabsInWindow.get(highlightInfo.windowId);
const toBeHighlightedTabs = mapAndFilter(tabIds, id => {
const tab = win.tabs.get(id);
return tab && !alreadyHighlightedTabs.has(tab.id) && tab || undefined;
});
//console.log(`updateTabsHighlighted: ${Date.now() - startAt}ms`, { toBeHighlightedTabs, toBeUnhighlightedTabs});
const inheritToCollapsedDescendants = !!highlightInfo.inheritToCollapsedDescendants;
//log('updateTabsHighlighted ', { toBeHighlightedTabs, toBeUnhighlightedTabs});
for (const tab of toBeUnhighlightedTabs) {
TabsStore.removeHighlightedTab(tab);
updateTabHighlighted(tab, false, { inheritToCollapsedDescendants });
}
for (const tab of toBeHighlightedTabs) {
TabsStore.addHighlightedTab(tab);
updateTabHighlighted(tab, true, { inheritToCollapsedDescendants });
}
}
async function updateTabHighlighted(tab, highlighted, { inheritToCollapsedDescendants } = {}) {
log(`highlighted status of ${dumpTab(tab)}: `, { old: tab.highlighted, new: highlighted });
//if (tab.highlighted == highlighted)
// return false;
tab.$TST.toggleState(Constants.kTAB_STATE_HIGHLIGHTED, highlighted);
tab.highlighted = highlighted;
const win = TabsStore.windows.get(tab.windowId);
const inheritHighlighted = !win.tabsToBeHighlightedAlone.has(tab.id);
if (!inheritHighlighted)
win.tabsToBeHighlightedAlone.delete(tab.id);
updateTab(tab, { highlighted });
Tab.onUpdated.dispatch(tab, { highlighted }, {
inheritHighlighted: inheritToCollapsedDescendants && inheritHighlighted,
});
return true;
}
export async function completeLoadingTabs(windowId) {
const completedTabs = new Set((await browser.tabs.query({ windowId, status: 'complete' })).map(tab => tab.id));
for (const tab of Tab.getLoadingTabs(windowId, { ordered: false, iterator: true })) {
if (completedTabs.has(tab.id))
updateTab(tab, { status: 'complete' });
}
}

View File

@@ -0,0 +1,417 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
configs
} from './common.js';
import * as Constants from './constants.js';
import * as SidebarConnection from './sidebar-connection.js';
import { TreeItem } from './TreeItem.js';
function log(...args) {
internalLogger('common/tree-behavior', ...args);
}
export function getParentTabOperationBehavior(tab, { context, byInternalOperation, preventEntireTreeBehavior, parent, windowId } = {}) {
const sidebarVisible = SidebarConnection.isInitialized() ? ((windowId || tab) && SidebarConnection.isOpen(windowId || tab.windowId)) : true;
log('getParentTabOperationBehavior ', tab, { byInternalOperation, preventEntireTreeBehavior, parent, sidebarVisible /*, stack: configs.debug && new Error().stack */ });
// strategy: https://github.com/piroor/treestyletab/issues/2860#issuecomment-820622273
let behavior;
switch (configs.parentTabOperationBehaviorMode) {
case Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CONSISTENT:
log(' => kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CONSISTENT');
if (context == Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE) {
behavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE;
}
else {
behavior = tab.$TST.subtreeCollapsed ?
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE :
configs.closeParentBehavior_insideSidebar_expanded;
}
break;
default:
case Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_PARALLEL:
log(' => kPARENT_TAB_OPERATION_BEHAVIOR_MODE_PARALLEL');
if (context == Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE) {
behavior = byInternalOperation ?
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE :
configs.moveParentBehavior_outsideSidebar_expanded;
}
else {
behavior = byInternalOperation ?
(tab.$TST.subtreeCollapsed ?
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE :
configs.closeParentBehavior_insideSidebar_expanded) :
configs.closeParentBehavior_outsideSidebar_expanded;
}
break;
case Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CUSTOM: // kPARENT_TAB_BEHAVIOR_ONLY_ON_SIDEBAR
log(' => kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CUSTOM');
if (context == Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE) {
behavior = byInternalOperation ?
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE :
sidebarVisible ?
(tab.$TST.subtreeCollapsed ?
configs.moveParentBehavior_outsideSidebar_collapsed :
configs.moveParentBehavior_outsideSidebar_expanded) :
(tab.$TST.subtreeCollapsed ?
configs.moveParentBehavior_noSidebar_collapsed :
configs.moveParentBehavior_noSidebar_expanded);
}
else {
behavior = byInternalOperation ?
(tab.$TST.subtreeCollapsed ?
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE :
configs.closeParentBehavior_insideSidebar_expanded) :
sidebarVisible ?
(tab.$TST.subtreeCollapsed ?
configs.closeParentBehavior_outsideSidebar_collapsed :
configs.closeParentBehavior_outsideSidebar_expanded) :
(tab.$TST.subtreeCollapsed ?
configs.closeParentBehavior_noSidebar_collapsed :
configs.closeParentBehavior_noSidebar_expanded);
}
break;
}
const parentTab = parent || tab.$TST.parent;
log(' => behavior: ', behavior);
const replacedParentCount = tab?.$TST?.replacedParentGroupTabCount;
if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB &&
configs.closeParentBehavior_replaceWithGroup_thresholdToPrevent >= 0 &&
replacedParentCount &&
replacedParentCount >= configs.closeParentBehavior_replaceWithGroup_thresholdToPrevent) {
behavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY;
log(' => the group tab is already replaced parent, fallback to kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY');
}
if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE &&
preventEntireTreeBehavior) {
behavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY;
log(' => preventEntireTreeBehavior behavior, fallback to kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY');
}
if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY) {
behavior = parentTab ? Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN : Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD;
log(' => intelligent behavior: ', behavior);
}
// Promote all children to upper level, if this is the last child of the parent.
// This is similar to "taking by representation".
if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD &&
parentTab &&
parentTab.$TST.childIds.length == 1 &&
configs.promoteAllChildrenWhenClosedParentIsLastChild) {
behavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN;
log(' => blast child ehavior: ', behavior);
}
return behavior;
}
export function getClosingTabsFromParent(tab, removeInfo = {}) {
log('getClosingTabsFromParent: ', tab, removeInfo);
if (tab?.type == TreeItem.TYPE_GROUP) {
return tab.$TST.members;
}
const closeParentBehavior = getParentTabOperationBehavior(tab, {
...removeInfo,
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
});
log('getClosingTabsFromParent: closeParentBehavior ', closeParentBehavior);
if (closeParentBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
return [tab];
return [tab].concat(tab.$TST.descendants);
}
export function calculateReferenceItemsFromInsertionPosition(
item,
{ context, insertBefore, insertAfter } = {}
) {
let firstItem = (Array.isArray(item) ? item[0] : item) || item;
let lastItem = (Array.isArray(item) ? item[item.length - 1] : item) || item;
firstItem = firstItem?.$TST?.nativeTabGroup || firstItem;
lastItem = lastItem?.$TST?.nativeTabGroup?.collapsed && lastItem?.$TST?.nativeTabGroup || lastItem;
log('calculateReferenceItemsFromInsertionPosition ', {
firstItem: firstItem?.id,
lastItem: lastItem?.id,
insertBefore: insertBefore?.id,
insertAfter : insertAfter?.id
});
if (insertBefore) {
/* strategy for moved case
+------------------ CASE 1 ---------------------------
| <= detach from parent, and move
|[TARGET ]
+------------------ CASE 2 ---------------------------
| [ ]
| <= attach to the parent of the target, and move
|[TARGET ]
+------------------ CASE 3 ---------------------------
|[ ]
| <= attach to the parent of the target, and move
|[TARGET ]
+------------------ CASE 4 ---------------------------
|[ ]
| <= attach to the parent of the target (previous item), and move
| [TARGET]
+-----------------------------------------------------
*/
/* strategy for shown case
+------------------ CASE 5 ---------------------------
| <= detach from parent, and move
|[TARGET ]
+------------------ CASE 6 ---------------------------
| [ ]
| <= if the inserted item has a parent and it is not the parent of the target, attach to the parent of the target. Otherwise keep inserted as a root.
|[TARGET ]
+------------------ CASE 7 ---------------------------
|[ ]
| <= attach to the parent of the target, and move
|[TARGET ]
+------------------ CASE 8 ---------------------------
|[ ]
| <= attach to the parent of the target (previous item), and move
| [TARGET]
+-----------------------------------------------------
*/
if (insertBefore.type == TreeItem.TYPE_GROUP) {
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, special case for a group item');
return {
insertBefore,
};
}
let prevItem = insertBefore &&
(configs.fixupTreeOnTabVisibilityChanged ?
insertBefore.$TST.nearestVisiblePrecedingTab :
insertBefore.$TST.unsafeNearestExpandedPrecedingTab);
if (prevItem == lastItem) // failsafe
prevItem = !firstItem ? null :
configs.fixupTreeOnTabVisibilityChanged ?
firstItem?.$TST.nearestVisiblePrecedingTab :
firstItem?.$TST.unsafeNearestExpandedPrecedingTab;
if (!prevItem) {
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, CASE 1/5');
// allow to move pinned item to beside of another pinned item
if (!firstItem ||
!!firstItem.pinned == !!insertBefore?.pinned) {
return {
insertBefore
};
}
else {
return {};
}
}
else {
const prevLevel = Number(prevItem?.$TST?.getAttribute(Constants.kLEVEL) || 0);
const targetLevel = Number(insertBefore?.$TST?.getAttribute(Constants.kLEVEL) || 0);
let parent = null;
if (!firstItem || !firstItem.pinned) {
if (prevLevel < targetLevel) {
if (context == Constants.kINSERTION_CONTEXT_MOVED) {
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, CASE 4, prevItem = ', prevItem);
parent = prevItem;
}
else {
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, CASE 8, prevItem = ', prevItem);
parent = (firstItem?.$TST?.parent != prevItem) ? prevItem : null;
}
}
else {
const possibleParent = insertBefore?.$TST?.parent;
if (context == Constants.kINSERTION_CONTEXT_MOVED || prevLevel == targetLevel) {
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, CASE 2/3/7');
parent = possibleParent;
}
else {
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, CASE 6');
parent = firstItem?.$TST?.parent != possibleParent && possibleParent || firstItem?.$TST?.parent;
}
}
}
const result = {
parent,
insertAfter: prevItem,
insertBefore
};
log(' => ', result);
return result;
}
}
if (insertAfter) {
/* strategy for moved case
+------------------ CASE 1 ---------------------------
|[TARGET ]
| <= if the target has a parent, attach to it and and move
+------------------ CASE 2 ---------------------------
| [TARGET]
| <= attach to the parent of the target, and move
|[ ]
+------------------ CASE 3 ---------------------------
|[TARGET ]
| <= attach to the parent of the target, and move
|[ ]
+------------------ CASE 4 ---------------------------
|[TARGET ]
| <= attach to the target, and move
| [ ]
+-----------------------------------------------------
*/
/* strategy for shown case
+------------------ CASE 5 ---------------------------
|[TARGET ]
| <= if the inserted item has a parent, detach. Otherwise keep inserted as a root.
+------------------ CASE 6 ---------------------------
| [TARGET]
| <= if the inserted item has a parent and it is not the parent of the next item, attach to the parent of the target. Otherwise attach to the parent of the next item.
|[ ]
+------------------ CASE 7 ---------------------------
|[TARGET ]
| <= attach to the parent of the target, and move
|[ ]
+------------------ CASE 8 ---------------------------
|[TARGET ]
| <= attach to the target, and move
| [ ]
+-----------------------------------------------------
*/
// We need to refer unsafeNearestExpandedFollowingTab instead of a visible item, to avoid
// placing the item after hidden items (it is too far from the target).
let unsafeNextItem = insertAfter?.$TST?.unsafeNearestExpandedFollowingTab;
if (firstItem && unsafeNextItem == firstItem) // failsafe
unsafeNextItem = lastItem?.$TST?.unsafeNearestExpandedFollowingTab;
let nextItem = insertAfter &&
(configs.fixupTreeOnTabVisibilityChanged ?
insertAfter.$TST?.nearestVisibleFollowingTab :
unsafeNextItem);
if (firstItem && nextItem == firstItem) // failsafe
nextItem = configs.fixupTreeOnTabVisibilityChanged ?
lastItem?.$TST?.nearestVisibleFollowingTab :
unsafeNextItem;
if (!nextItem) {
let result;
if (context == Constants.kINSERTION_CONTEXT_MOVED) {
log('calculateReferenceItemsFromInsertionPosition: from insertAfter, CASE 1');
result = {
parent: insertAfter?.$TST?.parent,
insertBefore: unsafeNextItem,
insertAfter
};
}
else {
log('calculateReferenceItemsFromInsertionPosition: from insertAfter, CASE 5');
result = {
parent: firstItem?.$TST?.parent && insertAfter?.$TST?.parent,
insertBefore: unsafeNextItem,
insertAfter
};
}
log(' => ', result);
return result;
}
else {
const targetLevel = Number(insertAfter?.$TST?.getAttribute(Constants.kLEVEL) || 0);
const nextLevel = Number(nextItem?.$TST?.getAttribute(Constants.kLEVEL) || 0);
let parent = null;
if (!firstItem || !firstItem.pinned) {
if (targetLevel < nextLevel) {
log('calculateReferenceItemsFromInsertionPosition: from insertAfter, CASE 4/8');
parent = insertAfter;
}
else {
const possibleParent = insertAfter?.$TST?.parent;
if (context == Constants.kINSERTION_CONTEXT_MOVED || targetLevel == nextLevel) {
log('calculateReferenceItemsFromInsertionPosition: from insertAfter, CASE 2/3/7');
parent = possibleParent;
}
else {
log('calculateReferenceItemsFromInsertionPosition: from insertAfter, CASE 6');
parent = firstItem?.$TST?.parent != possibleParent && possibleParent || firstItem?.$TST.parent;
}
}
}
const result = {
parent,
insertBefore: unsafeNextItem || nextItem,
insertAfter
};
log(' => ', result);
return result;
}
}
throw new Error('calculateReferenceItemsFromInsertionPosition requires one of insertBefore or insertAfter parameter!');
}
export const STRUCTURE_NO_PARENT = -1;
export const STRUCTURE_KEEP_PARENT = -2;
export function getTreeStructureFromTabs(tabs, { full, keepParentOfRootTabs } = {}) {
if (!tabs || !tabs.length)
return [];
/* this returns...
[A] => STRUCTURE_NO_PARENT (parent is not in this tree)
[B] => 0 (parent is 1st item in this tree)
[C] => 0 (parent is 1st item in this tree)
[D] => 2 (parent is 2nd in this tree)
[E] => STRUCTURE_NO_PARENT (parent is not in this tree, and this creates another tree)
[F] => 0 (parent is 1st item in this another tree)
*/
const tabIds = tabs.map(tab => tab.id);
return cleanUpTreeStructureArray(
tabs.map((tab, index) => {
const parentId = tab.$TST.parentId;
const indexInGivenTabs = parent ? tabIds.indexOf(parentId) : STRUCTURE_NO_PARENT ;
return indexInGivenTabs >= index ? STRUCTURE_NO_PARENT : indexInGivenTabs ;
}),
STRUCTURE_NO_PARENT
).map((parentIndex, index) => {
if (parentIndex == STRUCTURE_NO_PARENT &&
keepParentOfRootTabs)
parentIndex = STRUCTURE_KEEP_PARENT;
const tab = tabs[index];
const item = {
id: tab.$TST.uniqueId.id,
parent: parentIndex,
collapsed: tab.$TST.subtreeCollapsed
};
if (full) {
item.title = tab.title;
item.url = tab.url;
item.pinned = tab.pinned;
item.originalId = tab.id;
}
return item;
});
}
function cleanUpTreeStructureArray(treeStructure, defaultParent) {
let offset = 0;
treeStructure = treeStructure
.map((position, index) => {
return (position == index) ? STRUCTURE_NO_PARENT : position ;
})
.map((position, index) => {
if (position == STRUCTURE_NO_PARENT) {
offset = index;
return position;
}
return position - offset;
});
/* The final step, this validates all of values.
Smaller than STRUCTURE_NO_PARENT is invalid, so it becomes to STRUCTURE_NO_PARENT. */
treeStructure = treeStructure.map(index => {
return index < STRUCTURE_NO_PARENT ? defaultParent : index ;
});
return treeStructure;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,181 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
configs,
wait,
} from './common.js';
import * as ApiTabs from './api-tabs.js';
import * as Constants from './constants.js';
import * as TabsStore from './tabs-store.js';
// eslint-disable-next-line no-unused-vars
function log(...args) {
internalLogger('common/unique-id', ...args);
}
//===================================================================
// Unique Tab ID
//===================================================================
// for generated IDs
const kID_ADJECTIVES = `
Agile
Breezy
Cheerful
Dapper
Edgy
Feisty
Gutsy
Hoary
Intrepid
Jaunty
Karmic
Lucid
Marveric
Natty
Oneiric
Precise
Quantal
Raring
Saucy
Trusty
Utopic
Vivid
Warty
Xenial
Yakkety
Zesty
`.toLowerCase().trim().split(/\s+/);
const kID_NOUNS = `
Alpaca
Badger
Cat
Drake
Eft
Fawn
Gibbon
Heron
Ibis
Jackalope
Koala
Lynx
Meerkat
Narwhal
Ocelot
Pangolin
Quetzal
Ringtail
Salamander
Tahr
Unicorn
Vervet
Werwolf
Xerus
Yak
Zapus
`.toLowerCase().trim().split(/\s+/);
let mReadyToDetectDuplicatedTab = false;
export function readyToDetectDuplicatedTab() {
mReadyToDetectDuplicatedTab = true;
}
export async function request(tabOrId, options = {}) {
if (typeof options != 'object')
options = {};
let tab = tabOrId;
if (typeof tabOrId == 'number')
tab = TabsStore.tabs.get(tabOrId);
if (TabsStore.getCurrentWindowId()) {
return browser.runtime.sendMessage({
type: Constants.kCOMMAND_REQUEST_UNIQUE_ID,
tabId: tab.id
}).catch(ApiTabs.createErrorHandler());
}
let originalId = null;
let originalTabId = null;
let duplicated = false;
if (!options.forceNew) {
// https://github.com/piroor/treestyletab/issues/2845
// This delay may break initial restoration of tabs, so we should
// ignore it until all restoration processes are finished.
if (mReadyToDetectDuplicatedTab &&
configs.delayForDuplicatedTabDetection > 0)
await wait(configs.delayForDuplicatedTabDetection);
let oldId = await browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_ID).catch(ApiTabs.createErrorHandler());
if (oldId && !oldId.tabId) // ignore broken information!
oldId = null;
if (oldId) {
// If the tab detected from stored tabId is different, it is duplicated tab.
try {
const tabWithOldId = TabsStore.tabs.get(oldId.tabId);
if (!tabWithOldId)
throw new Error(`Invalid tab ID: ${oldId.tabId}`);
originalId = (tabWithOldId.$TST.uniqueId || await tabWithOldId.$TST.promisedUniqueId).id;
duplicated = tab && tabWithOldId.id != tab.id && originalId == oldId.id;
if (duplicated)
originalTabId = oldId.tabId;
else
throw new Error(`Invalid tab ID: ${oldId.tabId}`);
}
catch(e) {
ApiTabs.handleMissingTabError(e);
// It fails if the tab doesn't exist.
// There is no live tab for the tabId, thus
// this seems to be a tab restored from session.
// We need to update the related tab id.
browser.sessions.setTabValue(tab.id, Constants.kPERSISTENT_ID, {
id: oldId.id,
tabId: tab.id
}).catch(ApiTabs.createErrorSuppressor());
return {
id: oldId.id,
originalId: null,
originalTabId: oldId.tabId,
restored: true
};
}
}
}
const id = `tab-${generate()}`;
// tabId is for detecttion of duplicated tabs
await browser.sessions.setTabValue(tab.id, Constants.kPERSISTENT_ID, { id, tabId: tab.id }).catch(ApiTabs.createErrorSuppressor());
return { id, originalId, originalTabId, duplicated };
}
function generate() {
const adjective = kID_ADJECTIVES[Math.floor(Math.random() * kID_ADJECTIVES.length)];
const noun = kID_NOUNS[Math.floor(Math.random() * kID_NOUNS.length)];
const randomValue = Math.floor(Math.random() * 1000);
return `${adjective}-${noun}-${Date.now()}-${randomValue}`;
}
export async function getFromTabs(tabs) {
return Promise.all(tabs.map(tab =>
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_ID).catch(ApiTabs.createErrorHandler())
));
}
export async function ensureWindowId(windowId) {
const storedUniqueId = await browser.sessions.getWindowValue(windowId, 'uniqueId').catch(_ => null);
if (storedUniqueId)
return storedUniqueId;
const uniqueId = `window-${generate()}`;
await browser.sessions.setWindowValue(windowId, 'uniqueId', uniqueId).catch(_ => null);
return uniqueId;
}

View File

@@ -0,0 +1,109 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
configs
} from './common.js';
import * as Constants from './constants.js';
import * as SidebarConnection from './sidebar-connection.js';
import * as TabsStore from './tabs-store.js';
function log(...args) {
internalLogger('common/user-operation-blocker', ...args);
}
let mBlockingCount = 0;
let mBlockingThrobberCount = 0;
const mProgressbar = document.querySelector('#blocking-screen progress');
export function block({ throbber, shade } = {}) {
mBlockingCount++;
document.documentElement.classList.add(Constants.kTABBAR_STATE_BLOCKING);
log('block ', mBlockingCount, () => new Error().stack);
if (throbber) {
mBlockingThrobberCount++;
mProgressbar.delayedShow = setTimeout(() => {
mProgressbar.delayedShow = null;
mProgressbar.classList.add('shown');
}, configs.delayToShowProgressForBlockedUserOperation);
document.documentElement.classList.add(Constants.kTABBAR_STATE_BLOCKING_WITH_THROBBER);
}
else if (shade) {
document.documentElement.classList.add(Constants.kTABBAR_STATE_BLOCKING_WITH_SHADE);
}
}
export function setProgress(percentage, windowId = null) {
percentage = Math.max(0, Math.min(100, percentage));
if (mProgressbar)
mProgressbar.value = percentage;
if (windowId && !TabsStore.getCurrentWindowId())
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_PROGRESS_USER_OPERATIONS,
windowId,
percentage
});
}
export function blockIn(windowId, { throbber, shade } = {}) {
const targetWindow = TabsStore.getCurrentWindowId();
if (targetWindow && targetWindow != windowId)
return;
log(`blockIn(${windowId}) `, () => new Error().stack);
if (!targetWindow) {
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_BLOCK_USER_OPERATIONS,
windowId,
throbber: !!throbber,
shade: !!shade
});
return;
}
block({ throbber, shade });
}
export function unblock() {
mBlockingThrobberCount--;
log('unblock ', mBlockingCount, () => new Error().stack);
if (mBlockingThrobberCount < 0)
mBlockingThrobberCount = 0;
if (mBlockingThrobberCount == 0) {
setProgress(0);
mProgressbar.classList.remove('shown');
if (mProgressbar.delayedShow)
clearTimeout(mProgressbar.delayedShow);
document.documentElement.classList.remove(Constants.kTABBAR_STATE_BLOCKING_WITH_THROBBER);
document.documentElement.classList.remove(Constants.kTABBAR_STATE_BLOCKING_WITH_SHADE);
}
mBlockingCount--;
if (mBlockingCount < 0)
mBlockingCount = 0;
if (mBlockingCount == 0)
document.documentElement.classList.remove(Constants.kTABBAR_STATE_BLOCKING);
}
export function unblockIn(windowId, { throbber, shade } = {}) {
const targetWindow = TabsStore.getCurrentWindowId();
if (targetWindow && targetWindow != windowId)
return;
log(`unblockIn(${windowId}) `, () => new Error().stack);
if (!targetWindow) {
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_UNBLOCK_USER_OPERATIONS,
windowId,
throbber: !!throbber,
shade: !!shade
});
return;
}
unblock({ throbber, shade });
}

View File

@@ -0,0 +1,165 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Original: https://github.com/mozilla-extensions/webcompat-addon/blob/main/src/experiment-apis/aboutConfigPrefs.js */
'use strict';
const AboutPreferencesWatcher = {
BASE_URL: null, // this need to be replaced with "moz-extension://..../"
BASE_PREF: 'browser.sidebar.', // null,
onPrefChanged(name) {
switch (name) {
case 'browser.tabs.selectOwnerOnClose':
Services.prefs.setBoolPref(`${this.BASE_PREF}simulateSelectOwnerOnClose`, Services.prefs.getBoolPref(name));
break;
case 'browser.tabs.loadInBackground':
Services.prefs.setBoolPref(`${this.BASE_PREF}simulateTabsLoadInBackgroundInverted`, Services.prefs.getBoolPref(name));
break;
case 'browser.tabs.warnOnClose':
Services.prefs.setBoolPref(`${this.BASE_PREF}warnOnCloseTabs`, Services.prefs.getBoolPref(name));
break;
case 'browser.tabs.searchclipboardfor.middleclick':
Services.prefs.setBoolPref(`${this.BASE_PREF}middleClickPasteURLOnNewTabButton`, Services.prefs.getBoolPref(name));
break;
case 'browser.tabs.insertAfterCurrent':
case 'browser.tabs.insertRelatedAfterCurrent': {
const insertAfterCurrent = Services.prefs.getBoolPref('browser.tabs.insertAfterCurrent');
const insertRelatedAfterCurrent = Services.prefs.getBoolPref('browser.tabs.insertRelatedAfterCurrent');
const useTree = (
Services.prefs.getBoolPref(`${this.BASE_PREF}autoAttach`, false) &&
Services.prefs.getBoolPref(`${this.BASE_PREF}syncParentTabAndOpenerTab`, false)
);
Services.prefs.setStringPref(`${this.BASE_PREF}autoAttachOnOpenedWithOwner`,
!useTree ? -1 :
insertRelatedAfterCurrent ? 5 :
insertAfterCurrent ? 6 :
0);
Services.prefs.setStringPref(`${this.BASE_PREF}insertNewTabFromPinnedTabAt`,
!useTree ? -1 :
insertRelatedAfterCurrent ? 3 :
insertAfterCurrent ? 0 :
1);
Services.prefs.setStringPref(`${this.BASE_PREF}insertNewTabFromFirefoxViewAt`,
!useTree ? -1 :
insertRelatedAfterCurrent ? 3 :
insertAfterCurrent ? 0 :
1);
}; break;
}
},
// as an XPCOM component...
classDescription: 'Waterfox Chrome Window Watcher for about:preferences',
contractID: '@waterfox.net/chrome-window-watche-about-preferences;1',
classID: Components.ID('{c8a990cf-b9a3-4b4c-829c-a1dfc5753527}'),
QueryInterface: ChromeUtils.generateQI([
'nsIObserver',
'nsISupportsWeakReference',
]),
// nsIObserver
observe(subject, topic, data) {
switch (topic) {
case 'nsPref:changed':
this.onPrefChanged(data);
break;
}
},
createInstance(iid) {
return this.QueryInterface(iid);
},
};
this.prefs = class extends ExtensionAPI {
getAPI(context) {
const EventManager = ExtensionCommon.EventManager;
const extensionIDBase = context.extension.id.split('@')[0];
AboutPreferencesWatcher.BASE_URL = context.extension.baseURL;
// Synchronize simulation configs with the browser's preferences
for (const [source, dest] of Object.entries({
'browser.tabs.selectOwnerOnClose': `${AboutPreferencesWatcher.BASE_PREF}simulateSelectOwnerOnClose`,
'browser.tabs.loadInBackground': `${AboutPreferencesWatcher.BASE_PREF}simulateTabsLoadInBackgroundInverted`,
'browser.tabs.warnOnClose': `${AboutPreferencesWatcher.BASE_PREF}warnOnCloseTabs`,
'browser.tabs.searchclipboardfor.middleclick': `${AboutPreferencesWatcher.BASE_PREF}middleClickPasteURLOnNewTabButton`,
})) {
Services.prefs.setBoolPref(dest, Services.prefs.getBoolPref(source));
}
Services.prefs.addObserver('browser.tabs.', AboutPreferencesWatcher);
AboutPreferencesWatcher.onPrefChanged('browser.tabs.insertAfterCurrent');
return {
prefs: {
onChanged: new EventManager({
context,
name: 'prefs.onChanged',
register: (fire) => {
const observe = (_subject, _topic, data) => {
fire.async(data.replace(AboutPreferencesWatcher.BASE_PREF, '')).catch(() => {}); // ignore Message Manager disconnects
};
Services.prefs.addObserver(AboutPreferencesWatcher.BASE_PREF, observe);
return () => {
Services.prefs.removeObserver(AboutPreferencesWatcher.BASE_PREF, observe);
};
},
}).api(),
async getBoolValue(name, defaultValue = false) {
try {
return Services.prefs.getBoolPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, defaultValue);
}
catch(_error) {
return defaultValue;
}
},
async setBoolValue(name, value) {
Services.prefs.setBoolPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
},
async setDefaultBoolValue(name, value) {
Services.prefs.getDefaultBranch(null).setBoolPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
},
async getStringValue(name, defaultValue = '') {
try {
return Services.prefs.getStringPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, defaultValue);
}
catch(_error) {
return defaultValue;
}
},
async setStringValue(name, value) {
Services.prefs.setStringPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
},
async setDefaultStringValue(name, value) {
Services.prefs.getDefaultBranch(null).setStringPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
},
async getIntValue(name, defaultValue = 0) {
try {
return Services.prefs.getIntPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, defaultValue);
}
catch(_error) {
return defaultValue;
}
},
async setIntValue(name, value) {
Services.prefs.setIntPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
},
async setDefaultIntValue(name, value) {
Services.prefs.getDefaultBranch(null).setIntPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
},
},
};
}
onShutdown(isAppShutdown) {
if (isAppShutdown)
return;
Services.prefs.removeObserver('browser.tabs.', AboutPreferencesWatcher);
}
};

View File

@@ -0,0 +1,199 @@
[
{
"namespace": "prefs",
"description": "",
"functions": [
{
"name": "getBoolValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "boolean",
"name": "default",
"optional": true,
"description": ""
}
],
"returns": {
"type": "boolean",
"description": ""
},
"async": true
},
{
"name": "setBoolValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "boolean",
"name": "value",
"description": ""
}
],
"async": true
},
{
"name": "setDefaultBoolValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "boolean",
"name": "value",
"description": ""
}
],
"async": true
},
{
"name": "getStringValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "string",
"name": "default",
"optional": true,
"description": ""
}
],
"returns": {
"type": "string",
"description": ""
},
"async": true
},
{
"name": "setStringValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "string",
"name": "value",
"description": ""
}
],
"async": true
},
{
"name": "setDefaultStringValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "string",
"name": "value",
"description": ""
}
],
"async": true
},
{
"name": "getIntValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "integer",
"name": "default",
"optional": true,
"description": ""
}
],
"returns": {
"type": "integer",
"description": ""
},
"async": true
},
{
"name": "setIntValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "integer",
"name": "value",
"description": ""
}
],
"async": true
},
{
"name": "setDefaultIntValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "integer",
"name": "value",
"description": ""
}
],
"async": true
}
],
"events": [
{
"name": "onChanged",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
}
]
}
]
}
]

View File

@@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Original: https://github.com/mozilla-extensions/webcompat-addon/blob/main/src/experiment-apis/aboutConfigPrefsChild.js */
'use strict';
this.syncPrefs = class extends ExtensionAPI {
getAPI(context) {
const extensionIDBase = context.extension.id.split('@')[0];
const extensionPrefNameBase = `extensions.${extensionIDBase}.`;
return {
syncPrefs: {
getBoolValue(name, defaultValue = false) {
try {
return Services.prefs.getBoolPref(`${extensionPrefNameBase}${name}`, defaultValue);
}
catch(_error) {
return defaultValue;
}
},
getStringValue(name, defaultValue = '') {
try {
return Services.prefs.getStringPref(`${extensionPrefNameBase}${name}`, defaultValue);
}
catch(_error) {
return defaultValue;
}
},
getIntValue(name, defaultValue = 0) {
try {
return Services.prefs.getIntPref(`${extensionPrefNameBase}${name}`, defaultValue);
}
catch(_error) {
return defaultValue;
}
},
},
};
}
};

View File

@@ -0,0 +1,76 @@
[
{
"namespace": "syncPrefs",
"description": "",
"functions": [
{
"name": "getBoolValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "boolean",
"name": "default",
"optional": true,
"description": ""
}
],
"returns": {
"type": "boolean",
"description": ""
}
},
{
"name": "getStringValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "string",
"name": "default",
"optional": true,
"description": ""
}
],
"returns": {
"type": "string",
"description": ""
}
},
{
"name": "getIntValue",
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"name": "name",
"description": ""
},
{
"type": "integer",
"name": "default",
"optional": true,
"description": ""
}
],
"returns": {
"type": "integer",
"description": ""
}
}
],
"events": [
]
}
]

View File

@@ -0,0 +1,887 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const HTML = 'http://www.w3.org/1999/xhtml';
const XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
const TYPE_TREE = 'application/x-ws-tree';
const TST_ID = 'treestyletab@piro.sakura.ne.jp';
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: 'resource://gre/modules/AddonManager.sys.mjs',
CustomizableUI: 'resource:///modules/CustomizableUI.sys.mjs',
ExtensionPermissions: 'resource://gre/modules/ExtensionPermissions.sys.mjs',
PageThumbs: 'resource://gre/modules/PageThumbs.sys.mjs',
PlacesUtils: 'resource://gre/modules/PlacesUtils.sys.mjs',
});
// Range.createContextualFragment() unexpectedly drops XUL elements.
// Moreover, the security mechanism of the browser rejects adoptation of elements
// created by DOMParser(). Thus we need to create elements manually...
function element(document, NS, localName, attributes, children) {
if (Array.isArray(attributes)) {
children = attributes;
attributes = {};
}
const element = document.createElementNS(NS, localName);
if (attributes) {
for (const [name, value] of Object.entries(attributes)) {
element.setAttribute(name, value);
}
}
if (children) {
for (const child of children) {
if (typeof child == 'string')
element.appendChild(document.createTextNode(child));
else
element.appendChild(child);
}
}
return element;
}
const BrowserWindowWatcher = {
WATCHING_URLS: [
'chrome://browser/content/browser.xhtml',
],
BASE_URL: null, // this need to be replaced with "moz-extension://..../"
BASE_PREF: 'browser.sidebar.', // null,
locale: null, // this need to be replaced with a map
loadingForbiddenURLs: [],
autoplayBlockedListeners: new Set(),
autoplayUnblockedListeners: new Set(),
visibilityChangedListeners: new Set(),
menuCommandListeners: new Set(),
sidebarShownListeners: new Set(),
sidebarHiddenListeners: new Set(),
lastTransferredFiles: new Map(),
handleWindow(win) {
if (!win ||
!win.location)
return false;
const document = win.document;
if (!document)
return false;
if (win.location.href.startsWith('chrome://browser/content/browser.xhtml')) {
const installed = this.installTabsSidebar(win);
if (installed) {
win.addEventListener('DOMAudioPlaybackBlockStarted', this, { capture: true });
win.addEventListener('DOMAudioPlaybackBlockStopped', this, { capture: true });
win.addEventListener('visibilitychange', this);
win.addEventListener('TreeVerticalTabsShown', this);
win.addEventListener('TreeVerticalTabsHidden', this);
}
return installed;
}
return true;
},
unhandleWindow(win) {
if (!win ||
!win.location)
return;
const document = win.document;
if (!document)
return;
if (win.location.href.startsWith('chrome://browser/content/browser.xhtml')) {
this.uninstallTabsSidebar(win);
try {
win.removeEventListener('DOMAudioPlaybackBlockStarted', this, { capture: true });
win.removeEventListener('DOMAudioPlaybackBlockStopped', this, { capture: true });
win.removeEventListener('visibilitychange', this);
win.removeEventListener('TreeVerticalTabsShown', this);
win.removeEventListener('TreeVerticalTabsHidden', this);
}
catch(_error) {
}
}
},
installTabsSidebar(win) {
const document = win.document;
const tabsSidebarElement = document.querySelector('#tree-vertical-tabs-box');
if (tabsSidebarElement?.getAttribute('initialized') == 'true')
return true;
if (tabsSidebarElement) {
tabsSidebarElement.setAttribute('initialized', 'true');
tabsSidebarElement.addEventListener('dragover', this, { capture: true });
} else {
console.error('WaterfoxBridge: #tree-vertical-tabs element not found. Cannot attach event listeners or load panel.');
}
document.addEventListener('command', this);
document.addEventListener('customizationchange', this, { capture: true });
this.updateToggleButton(document);
return true;
},
getKeyFromFile(file) {
if (!file)
return '';
return `${file.name}?lastModified=${file.lastModified}&size=${file.size}&type=${file.type}`;
},
getFileURL(file) {
if (!file)
return '';
return this.lastTransferredFiles[this.getKeyFromFile(file)];
},
uninstallTabsSidebar(win) {
const document = win.document;
document.removeEventListener('command', this);
document.removeEventListener('customizationchange', this, { capture: true });
const tabsSidebarElement = document.querySelector('#tree-vertical-tabs-box');
if (tabsSidebarElement?.getAttribute('initialized') == 'true') {
tabsSidebarElement.removeAttribute('initialized');
tabsSidebarElement.removeEventListener('dragover', this, { capture: true });
}
},
updateToggleButton(document, button) {
button = button || document.querySelector('#toggle-tree-vertical-tabs');
if (!button)
return;
button.removeAttribute('disabled');
},
*iterateTargetWindows() {
const browserWindows = Services.wm.getEnumerator('navigator:browser');
while (browserWindows.hasMoreElements()) {
const win = browserWindows.getNext()/*.QueryInterface(Components.interfaces.nsIDOMWindow)*/
yield win;
}
return;
},
openOptions(win, full = false) {
const url = full ? `${this.BASE_URL}options/options.html#!` : 'about:preferences#tabsSidebar';
const windows = Services.wm.getEnumerator('navigator:browser');
while (windows.hasMoreElements()) {
const win = windows.getNext()/*.QueryInterface(Components.interfaces.nsIDOMWindow)*/;
if (!win.gBrowser)
continue;
for (const tab of win.gBrowser.tabs) {
if (tab.linkedBrowser.currentURI.spec != url)
continue;
win.gBrowser.selectedTab = tab;
return;
}
}
(win || Services.wm.getMostRecentBrowserWindow())
.openLinkIn(url, 'tab', {
allowThirdPartyFixup: false,
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
inBackground: false,
});
},
handleEvent(event) {
const win = event.target.ownerDocument?.defaultView || event.target.defaultView;
switch (event.type) {
case 'command':
switch (event.target.id) {
case 'toggle-tree-vertical-tabs':
case 'toggle-tree-vertical-tabs-command':
case 'viewmenu-toggle-tree-vertical-tabs':
this.updateToggleButton(event.target.ownerDocument);
break;
}
break;
case 'customizationchange':
this.updateToggleButton(event.target.ownerDocument);
break;
case 'DOMAudioPlaybackBlockStarted': {
const gBrowser = event.target.ownerDocument.defaultView.gBrowser;
const tab = gBrowser.getTabForBrowser(event.target);
for (const listener of this.autoplayBlockedListeners) {
listener(tab);
}
}; break;
case 'DOMAudioPlaybackBlockStopped': {
const gBrowser = event.target.ownerDocument.defaultView.gBrowser;
const tab = gBrowser.getTabForBrowser(event.target);
for (const listener of this.autoplayUnblockedListeners) {
listener(tab);
}
}; break;
case 'visibilitychange':
for (const listener of this.visibilityChangedListeners) {
listener(event.currentTarget);
}
break;
case 'TreeVerticalTabsShown':
for (const listener of this.sidebarShownListeners) {
listener(event.target.ownerDocument.defaultView);
}
break;
case 'TreeVerticalTabsHidden':
for (const listener of this.sidebarHiddenListeners) {
listener(event.target.ownerDocument.defaultView);
}
break;
case 'dragover': {
const tabsSidebarElement = event.currentTarget;
this.lastTransferredFiles.clear();
for (const file of event.dataTransfer.files) {
const fileInternal = Cc['@mozilla.org/file/local;1']
.createInstance(Components.interfaces.nsIFile);
fileInternal.initWithPath(file.mozFullPath);
const url = Services.io.getProtocolHandler('file')
.QueryInterface(Components.interfaces.nsIFileProtocolHandler)
.getURLSpecFromActualFile(fileInternal);
this.lastTransferredFiles[this.getKeyFromFile(file)] = url;
}
}; break;
}
},
tryHidePopup(event) {
if (event.target.closest)
event.target.closest('panel')?.hidePopup();
},
// as an XPCOM component...
classDescription: 'Waterfox Chrome Window Watcher for Browser Windows',
contractID: '@waterfox.net/chrome-window-watche-browser-windows;1',
classID: Components.ID('{8d25e5cc-1d67-4556-819e-e25bd37c79c5}'),
QueryInterface: ChromeUtils.generateQI([
'nsIContentPolicy',
'nsIObserver',
'nsISupportsWeakReference',
]),
// nsIContentPolicy
shouldLoad(contentLocation, loadInfo, mimeTypeGuess) {
const FORBIDDEN_URL_MATCHER = /^about:blank\?forbidden-url=/;
if (FORBIDDEN_URL_MATCHER.test(contentLocation.spec)) {
const url = contentLocation.spec.replace(FORBIDDEN_URL_MATCHER, '');
const index = this.loadingForbiddenURLs.indexOf(url);
if (index > -1) {
this.loadingForbiddenURLs.splice(index, 1);
const browser = loadInfo.browsingContext.embedderElement;
browser.loadURI(Services.io.newURI(url), {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
return Components.interfaces.nsIContentPolicy.REJECT_REQUEST;
}
}
if (this.WATCHING_URLS.some(url => contentLocation.spec.startsWith(url))) {
const startAt = Date.now();
const topWin = loadInfo.browsingContext.topChromeWindow;
const timer = topWin.setInterval(() => {
if (Date.now() - startAt > 1000) {
// timeout
topWin.clearInterval(timer);
return;
}
const win = loadInfo.browsingContext.window;
if (!win)
return;
try {
if (this.handleWindow(win))
topWin.clearInterval(timer);
}
catch(_error) {
}
}, 250);
}
return Components.interfaces.nsIContentPolicy.ACCEPT;
},
shouldProcess(contentLocation, loadInfo, mimeTypeGuess) {
return Components.interfaces.nsIContentPolicy.ACCEPT;
},
// nsIObserver
observe(subject, topic, data) {
switch (topic) {
case 'domwindowopened':
subject
//.QueryInterface(Components.interfaces.nsIDOMWindow)
.addEventListener('DOMContentLoaded', () => {
this.handleWindow(subject);
}, { once: true });
break;
}
},
createInstance(iid) {
return this.QueryInterface(iid);
},
// AddonManager listener callbacks
async tryConfirmUsingTST() {
const ignorePrefKey = `${this.BASE_PREF}.ignoreConflictionWithTST`;
if (Services.prefs.getBoolPref(ignorePrefKey, false))
return;
const nsIPrompt = Components.interfaces.nsIPrompt;
const shouldAsk = { value: true };
const result = Services.prompt.confirmEx(
Services.wm.getMostRecentBrowserWindow(),
this.locale.get('tryConfirmUsingTST_title'),
this.locale.get('tryConfirmUsingTST_message'),
(nsIPrompt.BUTTON_TITLE_IS_STRING * nsIPrompt.BUTTON_POS_0 |
nsIPrompt.BUTTON_TITLE_IS_STRING * nsIPrompt.BUTTON_POS_1 |
nsIPrompt.BUTTON_TITLE_IS_STRING * nsIPrompt.BUTTON_POS_2),
this.locale.get('tryConfirmUsingTST_WS'),
this.locale.get('tryConfirmUsingTST_both'),
this.locale.get('tryConfirmUsingTST_TST'),
this.locale.get('tryConfirmUsingTST_ask'),
shouldAsk
);
if (result > -1 &&
!shouldAsk.value)
Services.prefs.setBoolPref(ignorePrefKey, true);
switch (result) {
case 0: {
const addon = await lazy.AddonManager.getAddonByID(TST_ID);
addon.disable();
}; return;
case 2:
Services.prefs.setBoolPref('browser.sidebar.enabled', false);
return;
default:
return;
}
},
// install listener callbacks
onNewInstall(_install) {},
onInstallCancelled(_install) {},
onInstallPostponed(_install) {},
onInstallFailed(_install) {},
onInstallEnded(install) {
if (install.addon.id == TST_ID)
this.tryConfirmUsingTST();
},
onDownloadStarted(_install) {},
onDownloadCancelled(_install) {},
onDownloadEnded(_install) {},
onDownloadFailed(_install) {},
// addon listener callbacks
onUninstalled(_addon) {},
onEnabled(addon) {
if (addon.id == TST_ID)
this.tryConfirmUsingTST();
},
onDisabled(_addon) {},
};
this.waterfoxBridge = class extends ExtensionAPI {
getAPI(context) {
const EventManager = ExtensionCommon.EventManager;
return {
waterfoxBridge: {
async initUI() {
BrowserWindowWatcher.EXTENSION_ID = context.extension.id;
BrowserWindowWatcher.BASE_URL = context.extension.baseURL;
//BrowserWindowWatcher.BASE_PREF = `extensions.${context.extension.id.split('@')[0]}.`;
BrowserWindowWatcher.locale = {
get(key) {
key = key.toLowerCase();
if (this.selected.has(key))
return this.selected.get(key);
return this.default.get(key) || key;
},
default: context.extension.localeData.messages.get(context.extension.localeData.defaultLocale),
selected: context.extension.localeData.messages.get(context.extension.localeData.selectedLocale),
};
//const resourceURI = Services.io.newURI('resources', null, context.extension.rootURI);
//const handler = Cc['@mozilla.org/network/protocol;1?name=resource'].getService(Components.interfaces.nsISubstitutingProtocolHandler);
//handler.setSubstitution('waterfox-bridge', resourceURI);
// watch loading of about:preferences in subframes
const registrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
registrar.registerFactory(
BrowserWindowWatcher.classID,
BrowserWindowWatcher.classDescription,
BrowserWindowWatcher.contractID,
BrowserWindowWatcher
);
Services.catMan.addCategoryEntry(
'content-policy',
BrowserWindowWatcher.contractID,
BrowserWindowWatcher.contractID,
false,
true
);
// handle loading of browser windows
Services.ww.registerNotification(BrowserWindowWatcher);
// handle already opened browser windows
const windows = BrowserWindowWatcher.iterateTargetWindows();
while (true) {
const win = windows.next();
if (win.done)
break;
BrowserWindowWatcher.handleWindow(win.value);
}
// grant special permissions by default
if (!Services.prefs.getBoolPref(`${BrowserWindowWatcher.BASE_PREF}permissionsGranted`, false)) {
lazy.ExtensionPermissions.add(context.extension.id, {
origins: ['<all_urls>'],
permissions: ['internal:privateBrowsingAllowed'],
}, true);
Services.prefs.setBoolPref(`${BrowserWindowWatcher.BASE_PREF}permissionsGranted`, true);
}
// auto detection and warning for TST
lazy.AddonManager.addInstallListener(BrowserWindowWatcher);
lazy.AddonManager.addAddonListener(BrowserWindowWatcher);
const installedTST = await lazy.AddonManager.getAddonByID(TST_ID);
if (installedTST?.isActive)
BrowserWindowWatcher.tryConfirmUsingTST();
},
async reserveToLoadForbiddenURL(url) {
BrowserWindowWatcher.loadingForbiddenURLs.push(url);
},
async getFileURL(file) {
return BrowserWindowWatcher.getFileURL(file);
},
async getTabPreview(tabId) {
const info = {
url: null,
found: false,
};
const tab = context.extension.tabManager.get(tabId);
if (!tab)
return info;
const nativeTab = tab.nativeTab;
const window = nativeTab.ownerDocument.defaultView;
try {
const canvas = await window.tabPreviews.get(nativeTab);
/*
// We can get a URL like "https%3A%2F%2Fwww.example.com.org%2F&revision=0000"
// but Firefox does not allow loading of such a special internal URL from
// addon's sidebar page.
const image = await window.tabPreviews.get(nativeTab);
return image.src;
*/
if (canvas) {
info.url = canvas.toDataURL('image/png');
info.found = true;
return info;
}
}
catch (_error) { // tabPreviews.capture() raises error if the tab is discarded.
// console.error('waterfoxBridge: failed to take a tab preview: ', tabId, error);
}
// simulate default preview
// see also: https://searchfox.org/mozilla-esr115/rev/d0623081f317c92e0c7bc2a8b1b138687bdb23f5/browser/themes/shared/ctrlTab.css#85-94
const canvas = lazy.PageThumbs.createCanvas(window);
try {
// TODO: we should change the fill color to "CanvasText"...
const image = new window.Image();
await new Promise((resolve, reject) => {
image.addEventListener('load', resolve, { once: true });
image.addEventListener('error', reject, { once: true });
image.src = 'chrome://global/skin/icons/defaultFavicon.svg';
});
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'Canvas';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const iconSize = canvas.width * 0.2;
ctx.drawImage(
image,
0,
0,
image.width,
image.height,
(canvas.width - iconSize) / 2,
(canvas.height - iconSize) / 2,
iconSize,
iconSize
);
}
catch (_error) {
}
info.url = canvas.toDataURL('image/png');
return info;
},
async showPreviewPanel(tabId, top) {
const tab = tabId && context.extension.tabManager.get(tabId);
if (!tab)
return;
const document = tab.nativeTab.ownerDocument;
const tabbrowserTabs = document.getElementById('tabbrowser-tabs');
if (!tabbrowserTabs)
return;
if (!tabbrowserTabs.previewPanel) {
// load the tab preview component
const TabHoverPreviewPanel = ChromeUtils.importESModule(
'chrome://browser/content/tabbrowser/tab-hover-preview.mjs'
).default;
tabbrowserTabs.previewPanel = new TabHoverPreviewPanel(
document.getElementById('tab-preview-panel')
);
}
tabbrowserTabs.previewPanel.__ws__top = top;
tabbrowserTabs.previewPanel.activate(tab.nativeTab);
},
async hidePreviewPanel(windowId) {
const win = windowId && context.extension.windowManager.get(windowId);
if (!win || !win.window)
return;
try {
// Access the document through the window object
const document = win.window.document;
const tabPreview = document.getElementById('tabbrowser-tabs')?.previewPanel;
if (!tabPreview)
return;
tabPreview.__ws__top = null;
} catch (error) {
console.log("Error in hidePreviewPanel:", error);
}
},
async openPreferences() {
BrowserWindowWatcher.openOptions();
},
onWindowVisibilityChanged: new EventManager({
context,
name: 'waterfoxBridge.onWindowVisibilityChanged',
register: (fire) => {
const onChanged = win => {
const wrappedWindow = context.extension.windowManager.getWrapper(win);
if (wrappedWindow)
fire.async(wrappedWindow.id, win.document.visibilityState).catch(() => {}); // ignore Message Manager disconnects
};
BrowserWindowWatcher.visibilityChangedListeners.add(onChanged);
return () => {
BrowserWindowWatcher.visibilityChangedListeners.delete(onChanged);
};
},
}).api(),
onMenuCommand: new EventManager({
context,
name: 'waterfoxBridge.onMenuCommand',
register: (fire) => {
const onCommand = event => {
fire.async({
itemId: event.target.id,
detail: event.detail,
button: event.button,
altKey: event.altKey,
ctrlKey: event.ctrlKey,
metaKey: event.metaKey,
shiftKey: event.shiftKey,
}).catch(() => {}); // ignore Message Manager disconnects
};
BrowserWindowWatcher.menuCommandListeners.add(onCommand);
return () => {
BrowserWindowWatcher.menuCommandListeners.delete(onCommand);
};
},
}).api(),
onSidebarShown: new EventManager({
context,
name: 'waterfoxBridge.onSidebarShown',
register: (fire) => {
const onShown = win => {
const wrappedWindow = context.extension.windowManager.getWrapper(win);
if (wrappedWindow)
fire.async(wrappedWindow.id).catch(() => {}); // ignore Message Manager disconnects
};
BrowserWindowWatcher.sidebarShownListeners.add(onShown);
return () => {
BrowserWindowWatcher.sidebarShownListeners.delete(onShown);
};
},
}).api(),
onSidebarHidden: new EventManager({
context,
name: 'waterfoxBridge.onSidebarHidden',
register: (fire) => {
const onHidden = win => {
const wrappedWindow = context.extension.windowManager.getWrapper(win);
if (wrappedWindow)
fire.async(wrappedWindow.id).catch(() => {}); // ignore Message Manager disconnects
};
BrowserWindowWatcher.sidebarHiddenListeners.add(onHidden);
return () => {
BrowserWindowWatcher.sidebarHiddenListeners.delete(onHidden);
};
},
}).api(),
async listSyncDevices() {
const devices = [];
const targets = Services.wm.getMostRecentBrowserWindow().gSync.getSendTabTargets();
for (const target of targets) {
devices.push({
id: target.id,
name: target.name,
type: target.type,
});
}
return devices;
},
async sendToDevice(tabIds, deviceId) {
if (!Array.isArray(tabIds))
tabIds = [tabIds];
const gSync = Services.wm.getMostRecentBrowserWindow().gSync;
const tabs = tabIds.map(id => context.extension.tabManager.get(id));
const targets = gSync.getSendTabTargets().filter(target => !deviceId || target.id == deviceId);
for (const tab of tabs) {
gSync.sendTabToDevice(
tab.nativeTab.linkedBrowser.currentURI.spec,
targets,
tab.nativeTab.linkedBrowser.contentTitle
);
}
},
async openSyncDeviceSettings(windowId) {
let DOMWin = null;
try {
const win = windowId && context.extension.windowManager.get(windowId)
DOMWin = win?.window;
}
catch (_error) {
}
(DOMWin || Services.wm.getMostRecentBrowserWindow()).gSync.openDevicesManagementPage('sendtab');
},
async listSharingServices(tabId) {
const tab = tabId && context.extension.tabManager.get(tabId);
const services = [];
const win = Services.wm.getMostRecentBrowserWindow();
const sharingService = win.gBrowser.MacSharingService;
if (!sharingService)
return services;
const uri = win.gURLBar.makeURIReadable(
tab?.nativeTab.linkedBrowser.currentURI ||
Services.io.newURI('https://waterfox.net/', null, null)
).displaySpec;
for (const service of sharingService.getSharingProviders(uri)) {
services.push({
name: service.name,
title: service.menuItemTitle,
image: service.image,
});
}
return services;
},
async share(tabIds, shareName) {
if (!Array.isArray(tabIds))
tabIds = [tabIds];
const tabs = tabIds.map(id => context.extension.tabManager.get(id));
// currently we can share only one URL at a time...
const tab = tabs[0];
const win = Services.wm.getMostRecentBrowserWindow();
const uri = win.gURLBar.makeURIReadable(tab.nativeTab.linkedBrowser.currentURI).displaySpec;
if (AppConstants.platform == 'win') {
win.WindowsUIUtils.shareUrl(uri, tab.nativeTab.linkedBrowser.contentTitle);
return;
}
if (shareName) { // for macOS
win.gBrowser.MacSharingService.shareUrl(shareName, uri, tab.nativeTab.linkedBrowser.contentTitle);
return;
}
},
async openSharingPreferences() {
Services.wm.getMostRecentBrowserWindow().gBrowser.MacSharingService.openSharingPreferences();
},
async listAutoplayBlockedTabs(windowId) {
const tabs = new Set();
const windows = windowId ?
[context.extension.windowManager.get(windowId)] :
context.extension.windowManager.getAll();
for (const win of windows) {
if (!win.window.gBrowser)
continue;
for (const tab of win.window.document.querySelectorAll('tab[activemedia-blocked="true"]')) {
const wrappedTab = context.extension.tabManager.getWrapper(tab);
if (wrappedTab)
tabs.add(wrappedTab.convert());
}
}
return [...tabs].sort((a, b) => a.index - b.index);
},
async isAutoplayBlockedTab(tabId) {
const tab = context.extension.tabManager.get(tabId);
if (!tab)
return false;
return tab.nativeTab.getAttribute('activemedia-blocked') == 'true';
},
async unblockAutoplay(tabIds) {
if (!Array.isArray(tabIds))
tabIds = [tabIds];
const tabs = tabIds.map(id => context.extension.tabManager.get(id));
for (const tab of tabs) {
tab.nativeTab.linkedBrowser.resumeMedia();
}
},
onAutoplayBlocked: new EventManager({
context,
name: 'waterfoxBridge.onAutoplayBlocked',
register: (fire) => {
const onBlocked = tab => {
const wrappedTab = context.extension.tabManager.getWrapper(tab);
if (wrappedTab)
fire.async(wrappedTab.convert()).catch(() => {}); // ignore Message Manager disconnects
};
BrowserWindowWatcher.autoplayBlockedListeners.add(onBlocked);
return () => {
BrowserWindowWatcher.autoplayBlockedListeners.delete(onBlocked);
};
},
}).api(),
onAutoplayUnblocked: new EventManager({
context,
name: 'waterfoxBridge.onAutoplayUnblocked',
register: (fire) => {
const onUnblocked = tab => {
const wrappedTab = context.extension.tabManager.getWrapper(tab);
if (wrappedTab)
fire.async(wrappedTab.convert()).catch(() => {}); // ignore Message Manager disconnects
};
BrowserWindowWatcher.autoplayUnblockedListeners.add(onUnblocked);
return () => {
BrowserWindowWatcher.autoplayUnblockedListeners.delete(onUnblocked);
};
},
}).api(),
async isSelectionClipboardAvailable() {
try {
return Services.clipboard.isClipboardTypeSupported(Services.clipboard.kSelectionClipboard);
}
catch(_error) {
return false;
}
},
async getSelectionClipboardContents() {
try {
const transferable = Components.classes['@mozilla.org/widget/transferable;1']
.createInstance(Components.interfaces.nsITransferable);
const loadContext = Services.wm.getMostRecentBrowserWindow()
.docShell.QueryInterface(Components.interfaces.nsILoadContext);
transferable.init(loadContext);
transferable.addDataFlavor('text/plain');
Services.clipboard.getData(transferable, Services.clipboard.kSelectionClipboard);
const data = {};
transferable.getTransferData('text/plain', data);
if (data) {
data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
return data.data;
}
}
catch(_error) {
return '';
}
},
},
};
}
onShutdown(isAppShutdown) {
if (isAppShutdown)
return;
lazy.AddonManager.removeInstallListener(BrowserWindowWatcher);
lazy.AddonManager.removeAddonListener(BrowserWindowWatcher);
if (lazy.PlacesUtils.__ws_orig__unwrapNodes) {
lazy.PlacesUtils.unwrapNodes = lazy.PlacesUtils.__ws_orig__unwrapNodes;
lazy.PlacesUtils.__ws_orig__unwrapNodes = null;
}
const registrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
registrar.unregisterFactory(
BrowserWindowWatcher.classID,
BrowserWindowWatcher
);
Services.catMan.deleteCategoryEntry(
'content-policy',
BrowserWindowWatcher.contractID,
false
);
Services.ww.unregisterNotification(BrowserWindowWatcher);
const windows = BrowserWindowWatcher.iterateTargetWindows();
while (true) {
const win = windows.next();
if (win.done)
break;
BrowserWindowWatcher.unhandleWindow(win.value);
}
//const handler = Cc['@mozilla.org/network/protocol;1?name=resource'].getService(Components.interfaces.nsISubstitutingProtocolHandler);
//handler.setSubstitution('waterfox-bridge', null);
Services.prefs.removeObserver('', BrowserWindowWatcher);
}
};

View File

@@ -0,0 +1,459 @@
[
{
"namespace": "waterfoxBridge",
"types": [
{
"id": "TabPreviewInfo",
"type": "object",
"description": "Information of a tab preview image.",
"properties": {
"url": {
"type": "string",
"description": "A URL string to a tab preview image."
},
"found": {
"type": "boolean",
"description": "Indicates availability of the preview image."
}
}
},
{
"id": "SyncDeviceInfo",
"type": "object",
"description": "An object indicating sync device.",
"properties": {
"id": {
"type": "string",
"description": "The identifier string of a sync device."
},
"name": {
"type": "string",
"description": "The visible name of a sync device."
},
"target": {
"type": "string",
"description": "The type string of a sync device."
}
}
},
{
"id": "ShareService",
"type": "object",
"description": "An object indicating sync device.",
"properties": {
"name": {
"type": "string",
"description": "The name of a sharing service."
},
"title": {
"type": "string",
"description": "The menu label for a sharing service."
},
"image": {
"type": "string",
"description": "The image for a sharing device."
}
}
}
],
"functions": [
{
"name": "initUI",
"type": "function",
"description": "Initializes the custom UI for Waterfox.",
"async": true,
"parameters": []
},
{
"name": "reserveToLoadForbiddenURL",
"type": "function",
"description": "Reserve a URL to load by Waterfox.",
"async": true,
"parameters": [
{
"type": "string",
"name": "url"
}
]
},
{
"name": "getFileURL",
"type": "function",
"description": "Get file: URL from a file.",
"async": true,
"parameters": [
{
"name": "file",
"type": "object",
"description": "Information of a file.",
"properties": {
"lastModified": {
"type": "integer",
"description": "Last modified timestamp of the file."
},
"name": {
"type": "string",
"description": "Name of the file."
},
"size": {
"type": "integer",
"description": "Size of the file."
},
"type": {
"type": "string",
"description": "MIME type of the file."
}
}
}
],
"returns": {
"type": "string",
"name": "url"
}
},
{
"name": "getTabPreview",
"type": "function",
"description": "Returns a tab preview image URL and its metadata.",
"async": true,
"parameters": [
{
"type": "integer",
"name": "tabId",
"minimum": 0
}
],
"returns": {
"type": "array",
"items": {
"$ref": "waterfoxBridge.TabPreviewInfo"
}
}
},
{
"name": "showPreviewPanel",
"type": "function",
"description": "Shows the preview panel for the tab.",
"async": true,
"parameters": [
{
"type": "integer",
"name": "tabId",
"minimum": 0
},
{
"type": "integer",
"name": "top"
}
]
},
{
"name": "hidePreviewPanel",
"type": "function",
"description": "Hides the preview panel for the window.",
"async": true,
"parameters": [
{
"type": "integer",
"name": "windowId",
"minimum": 0
}
]
},
{
"name": "openPreferences",
"type": "function",
"description": "Opens the preferences.",
"async": true,
"parameters": []
},
{
"name": "listSyncDevices",
"type": "function",
"description": "Returns an array of sync device information.",
"async": true,
"parameters": [],
"returns": {
"type": "array",
"items": {
"$ref": "waterfoxBridge.SyncDeviceInfo"
}
}
},
{
"name": "sendToDevice",
"type": "function",
"description": "Sends tabs to a device or all devices.",
"async": true,
"parameters": [
{
"name": "tabIds",
"description": "The tab or list of tabs to send.",
"type": "array",
"choices": [
{ "type": "integer", "minimum": 0 },
{ "type": "array", "items": { "type": "integer", "minimum": 0 } }
]
},
{
"name": "deviceId",
"type": "string",
"description": "The ID of a device",
"optional": true
}
]
},
{
"name": "openSyncDeviceSettings",
"type": "function",
"description": "Opens the settings of sync devices.",
"async": true,
"parameters": [
{
"type": "integer",
"name": "windowId",
"minimum": 0,
"optional": true
}
]
},
{
"name": "listSharingServices",
"type": "function",
"description": "Returns an array of share service on macOS.",
"async": true,
"parameters": [
{
"type": "integer",
"name": "tabId",
"minimum": 0,
"optional": true
}
],
"returns": {
"type": "array",
"items": {
"$ref": "waterfoxBridge.ShareService"
}
}
},
{
"name": "share",
"type": "function",
"description": "Shares tabs via browser's share feature.",
"async": true,
"parameters": [
{
"name": "tabIds",
"description": "The tab or list of tabs to share.",
"type": "array",
"choices": [
{ "type": "integer", "minimum": 0 },
{ "type": "array", "items": { "type": "integer", "minimum": 0 } }
]
},
{
"name": "shareName",
"type": "string",
"description": "The name of a sharing method on macOS",
"optional": true
}
]
},
{
"name": "openSharingPreferences",
"type": "function",
"description": "Opens the preferences of sharing services on macOS.",
"async": true,
"parameters": []
},
{
"name": "listAutoplayBlockedTabs",
"type": "function",
"description": "Returns an array of autoplay blocked tabs in a window.",
"async": true,
"parameters": [
{
"type": "integer",
"name": "windowId",
"minimum": 0,
"optional": true
}
],
"returns": {
"type": "array",
"items": {
"$ref": "tabs.Tab"
}
}
},
{
"name": "isAutoplayBlockedTab",
"type": "function",
"description": "Returns the tab's autoplay blocked state.",
"async": true,
"parameters": [
{
"type": "integer",
"name": "windowId",
"minimum": 0
}
],
"returns": {
"type": "boolean",
"name": "autoplayBlocked"
}
},
{
"name": "unblockAutoplay",
"type": "function",
"description": "Resumes blocked autoplay of tabs.",
"async": true,
"parameters": [
{
"name": "tabIds",
"description": "The tab or list of tabs to resume autoplay.",
"type": "array",
"choices": [
{ "type": "integer", "minimum": 0 },
{ "type": "array", "items": { "type": "integer", "minimum": 0 } }
]
}
]
},
{
"name": "isSelectionClipboardAvailable",
"type": "function",
"description": "Returns availability of the selection clipboard.",
"async": true,
"parameters": [
],
"returns": {
"type": "boolean",
"name": "selectionClipboardAvailable"
}
},
{
"name": "getSelectionClipboardContents",
"type": "function",
"description": "Returns the contents of the selection clipboard as a string.",
"async": true,
"parameters": [
],
"returns": {
"type": "string",
"name": "selectionClipboardContents"
}
}
],
"events": [
{
"name": "onWindowVisibilityChanged",
"description": "Notified when the minimized state of a window is changed.",
"type": "function",
"parameters": [
{
"type": "integer",
"name": "windowId",
"minimum": 0
},
{
"type": "string",
"name": "visibilityState"
}
]
},
{
"name": "onMenuCommand",
"description": "Notified when a menu command is invoked.",
"type": "function",
"parameters": [
{
"name": "eventInfo",
"type": "object",
"description": "An object indicating command event.",
"properties": {
"itemId": {
"type": "string",
"description": "The id of the event target."
},
"detail": {
"type": "integer",
"description": "Corresponding to MouseEvent.prototype.detail."
},
"button": {
"type": "integer",
"description": "Corresponding to MouseEvent.prototype.button."
},
"altKey": {
"type": "boolean",
"description": "Corresponding to MouseEvent.prototype.altKey."
},
"ctrlKey": {
"type": "boolean",
"description": "Corresponding to MouseEvent.prototype.ctrlKey."
},
"metaKey": {
"type": "boolean",
"description": "Corresponding to MouseEvent.prototype.metaKey."
},
"shiftKey": {
"type": "boolean",
"description": "Corresponding to MouseEvent.prototype.shiftKey."
}
}
}
]
},
{
"name": "onSidebarShown",
"description": "Notified when the sidebar is shown.",
"type": "function",
"parameters": [
{
"type": "integer",
"name": "windowId"
}
]
},
{
"name": "onSidebarHidden",
"description": "Notified when the sidebar is hidden.",
"type": "function",
"parameters": [
{
"type": "integer",
"name": "windowId"
}
]
},
{
"name": "onAutoplayBlocked",
"description": "Notified when a tab's autoplay is blocked.",
"type": "function",
"parameters": [
{
"$ref": "tabs.Tab"
}
]
},
{
"name": "onAutoplayUnblocked",
"description": "Notified when a tab's autoplay is unblocked.",
"type": "function",
"parameters": [
{
"$ref": "tabs.Tab"
}
]
}
]
}
]

View File

@@ -0,0 +1,630 @@
/*
license: The MIT License, Copyright (c) 2016-2023 YUKI "Piro" Hiroshi
original:
http://github.com/piroor/webextensions-lib-configs
*/
'use strict';
/*
There are multiple level values:
(higher priority)
* [default] locked managed values (given via GPO or policies.json)
* [default] locked default values (given to the constructor)
* [user] locked user values (given via API)
=> [default] it should fallback to the default value if there is no user value
* [user] user values (local storage)
* [default] non-locked managed values (given via GPO or policies.json)
* [default] overridden default values (given via API)
* [default] built-in default values (given to the constructor)
(lower priority)
Only values different from [default] are stored and synchronized.
*/
const OBSERVABLE_AREA = new Set([
'internal', // TST internal
'local',
'sync',
'managed',
]);
// eslint-disable-next-line no-unused-vars
class Configs {
constructor(
defaults,
{ logging, logger, localKeys, syncKeys, sync } = { syncKeys: [], logger: null }
) {
this._defaultValues = {
...this._clone(defaults),
__ConfigsMigration__userValeusSameToDefaultAreCleared: false,
};
this._lockedDefaultKeys = new Set();
this._managedValues = {};
this._lockedManagedKeys = new Set();
this._userValues = {};
this._lockedUserKeys = new Set();
this._fetchedValues = {};
this.$default = {};
this.$all = {};
for (const key of Object.keys(this._defaultValues)) {
Object.defineProperty(this.$default, key, {
get: () => this._getDefaultValue(key),
set: (value) => this._setDefaultValue(key, value),
enumerable: true,
});
const description = {
get: () => this._getValue(key),
set: (value) => this._setValue(key, value),
enumerable: true,
};
Object.defineProperty(this, key, description);
Object.defineProperty(this.$all, key, description);
}
for (const [key, locked] of Object.entries(defaults)) {
if (!key.endsWith(':locked'))
continue;
if (locked)
this._lockedDefaultKeys.add(key.replace(/:locked$/, ''));
delete defaults[key];
}
this.$logging = logging || false;
this.$logs = [];
this.$logger = logger;
this.sync = sync === undefined ? true : !!sync;
this._updating = new Map();
this._observers = new Set();
this._changedObservers = new Set();
this._localLoadedObservers = new Set();
this._syncKeys = [
...(localKeys ?
Object.keys(defaults).filter(x => !localKeys.includes(x)) :
(syncKeys || [])),
'__ConfigsMigration__userValeusSameToDefaultAreCleared',
];
this.$loaded = this._load();
this.$preReceivedChanges = [];
this.$listeningChanges = false;
browser.storage.onChanged.addListener(this._onChanged.bind(this));
this.$preReceivedMessages = [];
this.$listeningMessages = false;
browser.runtime.onMessage.addListener(this._onMessage.bind(this));
}
$reset(key, { broadcast } = {}) {
if (!key) {
for (const key of Object.keys(this._defaultValues)) {
this.$reset(key);
}
return;
}
if (!this._defaultValues.hasOwnProperty(key))
throw new Error(`failed to reset unknown key: ${key}`);
this._setValue(key, this._getDefaultValue(key), true, { broadcast });
}
$cleanUp({ broadcast } = {}) {
for (const [key, defaultValue] of Object.entries(this.$default)) {
if (!this._userValues.hasOwnProperty(key))
continue;
const value = JSON.stringify(this._getNonDefaultValue(key));
if (value == JSON.stringify(defaultValue) ||
(this._managedValues.hasOwnProperty(key) &&
value == JSON.stringify(this._managedValues[key])))
this.$reset(key, { broadcast });
}
}
_getDefaultValue(key) {
if (this._managedValues.hasOwnProperty(key))
return this._managedValues[key];
return this._defaultValues[key];
}
_setDefaultValue(key, value, { broadcast } = {}) {
if (!key)
throw new Error(`missing key for default value ${value}`);
if (!this._defaultValues.hasOwnProperty(key))
throw new Error(`failed to set default value for unknown key: ${key}`);
const currentValue = this[key];
const currentDefaultValue = this._getDefaultValue(key);
this._defaultValues[key] = this._clone(value);
const defaultValue = this._getDefaultValue(key);
if (JSON.stringify(defaultValue) == JSON.stringify(this._getNonDefaultValue[key]))
this.$reset(key, { broadcast });
const newDefaultValue = this._getDefaultValue(key);
if (currentValue == currentDefaultValue &&
currentValue != newDefaultValue &&
this[key] == newDefaultValue) {
const observers = [...this._observers, ...this._changedObservers];
this.$notifyToObservers(key, value, observers, 'onChangeConfig');
}
if (broadcast === false)
return;
try {
browser.runtime.sendMessage({
type: 'Configs:updateDefaultValue',
key: key,
value: defaultValue,
}).catch(_error => {});
}
catch(_error) {
}
}
_getNonDefaultValue(key) {
if (this._userValues.hasOwnProperty(key))
return this._userValues[key];
if (this._managedValues.hasOwnProperty(key) &&
!this._lockedManagedKeys.has(key))
return this._managedValues[key];
return undefined;
}
$addLocalLoadedObserver(observer) {
if (!this._localLoadedObservers.has(observer))
this._localLoadedObservers.add(observer);
}
$removeLocalLoadedObserver(observer) {
this._localLoadedObservers.delete(observer);
}
$addChangedObserver(observer) {
if (!this._changedObservers.has(observer))
this._changedObservers.add(observer);
}
$removeChangedObserver(observer) {
this._changedObservers.delete(observer);
}
$addObserver(observer) {
// for backward compatibility
if (typeof observer == 'function')
this.$addChangedObserver(observer);
else if (!this._observers.has(observer))
this._observers.add(observer);
}
$removeObserver(observer) {
// for backward compatibility
if (typeof observer == 'function')
this.$removeChangedObserver(observer);
else
this._observers.delete(observer);
}
_log(message, ...args) {
message = `Configs[${location.href}] ${message}`;
this.$logs = this.$logs.slice(-1000);
if (!this.$logging)
return;
if (typeof this.$logger === 'function')
this.$logger(message, ...args);
else
console.log(message, ...args);
}
_load() {
return this.$_promisedLoad ||
(this.$_promisedLoad = this._tryLoad());
}
async _tryLoad() {
this._log('load');
try {
this._log(`load: try load from storage on ${location.href}`);
const [localValues, managedValues, lockedKeys] = await Promise.all([
(async () => {
try {
const localValues = await browser.storage.local.get(null); // keys must be "null" to get only stored values
this._log('load: successfully loaded local storage');
const observers = [...this._observers, ...this._localLoadedObservers];
for (const [key, value] of Object.entries(localValues)) {
this.$notifyToObservers(key, value, observers, 'onLocalLoaded');
}
return localValues;
}
catch(e) {
this._log('load: failed to load local storage: ', String(e));
}
return {};
})(),
(async () => {
if (!browser.storage.managed) {
this._log('load: skip managed storage');
return null;
}
return new Promise(async (resolve, _reject) => {
const loadManagedStorage = () => {
let resolved = false;
return new Promise((resolve, reject) => {
browser.storage.managed.get().then(managedValues => {
if (resolved)
return;
resolved = true;
this._log('load: successfully loaded managed storage');
resolve(managedValues || null);
}).catch(error => {
if (resolved)
return;
resolved = true;
this._log('load: failed to load managed storage: ', String(error));
reject(error);
});
// storage.managed.get() fails on options page in Thunderbird.
// The problem should be fixed by Thunderbird side.
setTimeout(() => {
if (resolved)
return;
resolved = true;
this._log('load: failed to load managed storage: timeout');
reject(new Error('timeout'));
}, 250);
});
};
for (let i = 0, maxi = 10; i < maxi; i++) {
try {
const result = await loadManagedStorage();
// On old versions Firefox and Thunderbird, a value with
// REG_MULTI_SZ type is always delivered as a simple string,
// thus we need to parse it by self.
for (const [key, value] of Object.entries(result)) {
const defaultValue = this._defaultValues[key];
if (typeof value != 'string')
continue;
const trimmed = value.trim();
if (Array.isArray(defaultValue)) {
result[key] = (trimmed.startsWith('[') && trimmed.endsWith(']')) ?
JSON.parse(value) :
trimmed.includes('\n') ?
trimmed.split('\n') :
trimmed.split(',');
}
else if (defaultValue &&
typeof defaultValue == 'object' &&
trimmed.startsWith('{') &&
trimmed.endsWith('}')) {
result[key] = JSON.parse(trimmed);
}
}
resolve(result);
return;
}
catch(error) {
if (error.message != 'timeout') {
console.log('managed storage is not provided');
resolve(null);
return;
}
console.log('failed to load managed storage ', error);
}
await new Promise(resolve => setTimeout(resolve, 250));
}
console.log('failed to load managed storage with 10 times retly');
resolve(null);
});
})(),
(async () => {
try {
const lockedKeys = await browser.runtime.sendMessage({
type: 'Configs:getLockedKeys'
});
this._log('load: successfully synchronized locked state');
return lockedKeys || [];
}
catch(e) {
this._log('load: failed to synchronize locked state: ', String(e));
}
return [];
})()
]);
this._log(`load: loaded:`, { localValues, managedValues, lockedKeys });
lockedKeys.push(...this._lockedDefaultKeys);
if (managedValues) {
for (const [key, value] of Object.entries(managedValues)) {
if (key.endsWith(':locked'))
continue;
const locked = managedValues[`${key}:locked`] !== false;
this._managedValues[key] = value;
if (locked)
this._lockedManagedKeys.add(key);
}
}
this._userValues = this._clone({ ...(localValues || {}) });
this._log('load: values are applied');
for (const key of new Set(lockedKeys)) {
this._updateLocked(key, true);
}
this._log('load: locked state is applied');
this.$listeningChanges = true;
if (this.sync &&
(this._syncKeys ||
this._syncKeys.length > 0)) {
try {
browser.storage.sync.get(this._syncKeys).then(syncedValues => {
this._log('load: successfully loaded sync storage');
if (!syncedValues)
return;
for (const key of Object.keys(syncedValues)) {
this[key] = syncedValues[key];
}
});
}
catch(e) {
this._log('load: failed to read sync storage: ', String(e));
return null;
}
}
this.$listeningMessages = true;
if (!this.__ConfigsMigration__userValeusSameToDefaultAreCleared) {
this.$cleanUp();
this.__ConfigsMigration__userValeusSameToDefaultAreCleared = true;
}
this.$_promisedLoad = this.$_promisedLoad.then(() => {
if (this.$preReceivedChanges.length > 0) {
const changes = [...this.$preReceivedChanges];
this.$preReceivedChanges = [];
for (const change of changes) {
this._onChanged(change, 'internal');
}
}
if (this.$preReceivedMessages.length > 0) {
const messages = [...this.$preReceivedMessages];
this.$preReceivedMessages = [];
for (const message of messages) {
this._onMessage(message.message, message.sender);
}
}
});
return this.$all;
}
catch(e) {
this._log('load: fatal error: ', e, e.stack);
throw e;
}
}
_getValue(key) {
if (this._lockedManagedKeys.has(key))
return this._managedValues[key];
if (this._lockedDefaultKeys.has(key))
return this._defaultValues[key];
if (this._lockedUserKeys.has(key))
return this._userValues[key] || this._getDefaultValue(key);
if (this._userValues.hasOwnProperty(key))
return this._userValues[key];
if (this._managedValues.hasOwnProperty(key))
return this._managedValues[key];
if (this._defaultValues.hasOwnProperty(key))
return this._defaultValues[key];
throw new Error(`invalid access: unknown key ${key}`);
}
_setValue(key, value, force = false, { broadcast } = {}) {
const newValue = this._clone(value);
if (this._lockedDefaultKeys.has(key) ||
this._lockedManagedKeys.has(key) ||
this._lockedUserKeys.has(key)) {
this._log(`warning: ${key} is locked and not updated`);
return newValue;
}
const stringified = JSON.stringify(value);
if (stringified == JSON.stringify(this._userValues[key]) && !force) {
this._log(`skip: ${key} is not changed`);
return newValue;
}
const oldValue = this._getValue(key);
const shouldReset = stringified == JSON.stringify(this._getDefaultValue(key));
this._log(`set: ${key} = ${value}${shouldReset ? ' (reset to default)' : ''}`);
if (shouldReset)
delete this._userValues[key];
else
this._userValues[key] = newValue;
if (broadcast === false)
return newValue;
const update = {};
update[key] = newValue;
try {
const updatingValues = this._updating.get(key) || [];
updatingValues.push(newValue);
this._updating.set(key, updatingValues);
const updated = shouldReset ?
browser.storage.local.remove([key]).then(() => {
this._log('local: successfully removed ', key);
}) :
browser.storage.local.set(update).then(() => {
this._log('local: successfully saved ', update);
});
updated
.then(() => {
setTimeout(() => {
const updatingValues = this._updating.get(key);
if (!updatingValues ||
!updatingValues.includes(newValue))
return;
// failsafe: on Thunderbird updates sometimes won't be notified to the page itself.
const changes = {};
changes[key] = {
oldValue,
newValue,
};
this._onChanged(changes, 'internal');
}, 250);
});
}
catch(e) {
this._log('save: failed', e);
}
try {
if (this.sync && this._syncKeys.includes(key)) {
if (shouldReset)
browser.storage.sync.remove([key]).then(() => {
this._log('sync: successfully removed', update);
});
else
browser.storage.sync.set(update).then(() => {
this._log('sync: successfully synced', update);
});
}
}
catch(e) {
this._log('sync: failed', e);
}
return newValue;
}
$lock(key) {
this._log('locking: ' + key);
this._updateLocked(key, true);
}
$unlock(key) {
this._log('unlocking: ' + key);
this._updateLocked(key, false);
}
$isLocked(key) {
return this._lockedUserKeys.has(key);
}
_updateLocked(key, locked, { broadcast } = {}) {
if (locked)
this._lockedUserKeys.add(key);
else
this._lockedUserKeys.delete(key);
if (browser.runtime &&
broadcast !== false) {
try {
browser.runtime.sendMessage({
type: 'Configs:updateLocked',
key: key,
locked: this._lockedUserKeys.has(key),
}).catch(_error => {});
}
catch(_error) {
}
}
}
_onMessage(message, sender) {
if (!message ||
typeof message.type != 'string')
return;
if (!this.$listeningMessages) {
this.$preReceivedMessages.push({ message, sender });
return;
}
this._log(`onMessage: ${message.type}`, message, sender);
switch (message.type) {
case 'Configs:getLockedKeys':
return Promise.resolve(Array.from(this._lockedUserKeys));
case 'Configs:updateLocked':
this._updateLocked(message.key, message.locked, { broadcast: false });
break;
case 'Configs:updateDefaultValue':
this._setDefaultValue(message.key, message.value, { broadcast: false });
break;
}
}
_onChanged(changes, areaName) {
if (!OBSERVABLE_AREA.has(areaName))
return;
if (!this.$listeningChanges) {
this.$preReceivedChanges.push(changes);
return;
}
this._log('_onChanged ', areaName, changes);
const observers = [...this._observers, ...this._changedObservers];
for (const [key, change] of Object.entries(changes)) {
// storage.local.onChanged is sometimes notified with delay, and it
// unexpctedly reverts stored user value after it is changed multiple
// times in short time range, and it may produce "ghost value" problem, like:
// 1. setting to "true" (updates the stored value to "true" immediately)
// 2. setting to "false" (updates the stored value to "false" immediately)
// 3. "true" is notified (updates the stored value to "true" with delay)
// 4. getting the value - it gots "true" instead of "false"!
// To avoid such problems, we need to skip applying notified new value
// if the notification is from a local change.
const updatingValues = this._updating.get(key);
if (updatingValues &&
updatingValues[0] == change.newValue) {
updatingValues.shift();
}
else {
if ('newValue' in change)
this._userValues[key] = this._clone(change.newValue);
else
delete this._userValues[key];
}
if (!updatingValues || updatingValues.length == 0)
this._updating.delete(key);
else
this._updating.set(key, updatingValues);
const value = this._getNonDefaultValue(key);
if (JSON.stringify(value) == JSON.stringify(this._getDefaultValue(key)))
return;
this.$notifyToObservers(key, value, observers, 'onChangeConfig');
}
}
$notifyToObservers(key, value, observers, observerMethod) {
for (const observer of observers) {
if (typeof observer === 'function')
observer(key, value);
else if (observer && typeof observer[observerMethod] === 'function')
observer[observerMethod](key, value);
}
}
_clone(value) {
return JSON.parse(JSON.stringify(value));
}
};
export default Configs;

View File

@@ -0,0 +1,117 @@
/*
license: The MIT License, Copyright (c) 2018 YUKI "Piro" Hiroshi
original:
https://github.com/piroor/webextensions-lib-event-listener-manager
*/
'use strict';
const TIMEOUT = 2000;
export default class EventListenerManager {
constructor() {
this._listeners = new Set();
this._stacksOnListenerAdded = new WeakMap();
this._sourceOfListeners = new WeakMap();
}
addListener(listener) {
const listeners = this._listeners;
if (!listeners.has(listener)) {
listeners.add(listener);
if (EventListenerManager.debug) {
const stack = new Error().stack;
this._stacksOnListenerAdded.set(listener, stack);
this._sourceOfListeners.set(listener, stack.split('\n')[1]);
}
}
}
removeListener(listener) {
this._listeners.delete(listener);
this._stacksOnListenerAdded.delete(listener);
this._sourceOfListeners.delete(listener);
}
removeAllListeners() {
this._listeners.clear();
this._stacksOnListenerAdded.clear();
this._sourceOfListeners.clear();
}
hasListener() {
return this._listeners.size > 0;
}
dispatch(...args) {
const results = this.dispatchWithDetails(...args);
if (results instanceof Promise)
return results.then(this.normalizeResults);
else
return this.normalizeResults(results);
}
// We hope to process results synchronously if possibly,
// so this method must not be "async".
dispatchWithDetails(...args) {
const results = Array(this._listeners.size);
let index = 0;
for (let listener of this._listeners) {
const startAt = EventListenerManager.debug && Date.now();
const timer = EventListenerManager.debug && setTimeout(() => {
const listenerAddedStack = this._stacksOnListenerAdded.get(listener);
console.log(`listener does not respond in ${TIMEOUT}ms.\n----------------------\n${listenerAddedStack || 'non debug mode or already removed listener:\n'+listener.toString()}\n----------------------\n${new Error().stack}\n----------------------\nargs:`, args);
}, TIMEOUT);
try {
const result = listener(...args);
if (result instanceof Promise)
results[index++] = result
.catch(e => {
console.log(e);
})
.then(result => {
if (timer)
clearTimeout(timer);
return {
value: result,
elapsed: EventListenerManager.debug && (Date.now() - startAt),
async: true,
listener: EventListenerManager.debug && this._sourceOfListeners.get(listener)
};
});
if (timer)
clearTimeout(timer);
results[index++] = {
value: result,
elapsed: EventListenerManager.debug && (Date.now() - startAt),
async: false,
listener: EventListenerManager.debug && this._sourceOfListeners.get(listener)
};
}
catch(e) {
console.log(e);
if (timer)
clearTimeout(timer);
}
}
if (results.some(result => result instanceof Promise))
return Promise.all(results);
else
return results;
}
normalizeResults(results) {
if (results.length == 1)
return results[0].value;
for (const result of results) {
if (result.value === false)
return false;
}
return true;
}
}
EventListenerManager.debug = false;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,420 @@
/*
license: The MIT License, Copyright (c) 2016-2020 YUKI "Piro" Hiroshi
original:
http://github.com/piroor/webextensions-lib-options
*/
class Options {
constructor(configs, { steps, onImporting, onImported, onExporting, onExported } = {}) {
this.configs = configs;
this.steps = steps || {};
this.$onImporting = onImporting;
this.$onImported = onImported;
this.$onExporting = onExporting;
this.$onExported = onExported;
this.uiNodes = new Map();
this.throttleTimers = new Map();
this.onReady = this.onReady.bind(this);
this.onConfigChanged = this.onConfigChanged.bind(this)
document.addEventListener('DOMContentLoaded', this.onReady);
}
findUIsForKey(key) {
key = this._sanitizeForSelector(key);
if (!key)
return [];
return document.querySelectorAll(`[name="${key}"], #${key}, [data-config-key="${key}"]`);
}
detectUIType(node) {
if (!node)
return this.UI_MISSING;
if (node.localName == 'textarea')
return this.UI_TYPE_TEXT_FIELD;
if (node.localName == 'select')
return this.UI_TYPE_SELECTBOX;
if (node.localName != 'input')
return this.UI_TYPE_UNKNOWN;
switch (node.type) {
case 'text':
case 'password':
case 'number':
case 'color':
return this.UI_TYPE_TEXT_FIELD;
case 'checkbox':
return this.UI_TYPE_CHECKBOX;
case 'radio':
return this.UI_TYPE_RADIO;
default:
return this.UI_TYPE_UNKNOWN;
}
}
throttledUpdate(key, uiNode, value) {
if (this.throttleTimers.has(key))
clearTimeout(this.throttleTimers.get(key));
uiNode.dataset.configValueUpdating = true;
this.throttleTimers.set(key, setTimeout(() => {
this.throttleTimers.delete(key);
this.configs[key] = this.UIValueToConfigValue(key, value);
setTimeout(() => {
uiNode.dataset.configValueUpdating = false;
}, 50);
}, 250));
}
UIValueToConfigValue(key, value) {
switch (typeof this.configs.$default[key]) {
case 'string':
return String(value);
case 'number':
return Number(value);
case 'boolean':
if (typeof value == 'string')
return value != 'false';
else
return Boolean(value);
default: // object
if (typeof value == 'string')
return JSON.parse(value || 'null');
else
return value;
}
}
configValueToUIValue(value) {
if (typeof value == 'object') {
value = JSON.stringify(value);
if (value == 'null')
value = '';
return value;
}
else
return value;
}
applyLocked(node, key) {
const locked = this.configs.$isLocked(key);
node.disabled = locked;
const selector = node.id && `label[for="${this._sanitizeForSelector(node.id)}"]`;
const label = node.closest('label') || (node.id && node.ownerDocument.querySelector(selector)) || node;
if (label)
label.classList.toggle('locked', locked);
}
bindToCheckbox(key, node) {
node.checked = this.configValueToUIValue(this.configs[key]);
node.addEventListener('change', () => {
this.throttledUpdate(key, node, node.checked);
});
this.applyLocked(node, key);
this.addResetMethod(key, node);
const nodes = this.uiNodes.get(key) || [];
nodes.push(node);
this.uiNodes.set(key, nodes);
}
bindToRadio(key, node) {
const group = node.getAttribute('name');
const radios = document.querySelectorAll(`input[name="${this._sanitizeForSelector(group)}"]`);
let activated = false;
for (const radio of radios) {
const nodes = this.uiNodes.get(key) || [];
if (nodes.includes(radio))
continue;
this.applyLocked(radio, key);
nodes.push(radio);
this.uiNodes.set(key, nodes);
radio.addEventListener('change', () => {
if (!activated)
return;
const stringifiedValue = this.configs[key];
if (stringifiedValue != radio.value)
this.throttledUpdate(key, radio, radio.value);
});
}
const selector = `input[type="radio"][value=${JSON.stringify(String(this.configs[key]))}]`;
const chosens = (this.uiNodes.get(key) || []).filter(node => node.matches(selector));
if (chosens && chosens.length > 0)
chosens.map(chosen => { chosen.checked = true; });
setTimeout(() => {
activated = true;
}, 0);
}
bindToTextField(key, node) {
node.value = this.configValueToUIValue(this.configs[key]);
node.addEventListener('input', () => {
this.throttledUpdate(key, node, node.value);
});
this.applyLocked(node, key);
this.addResetMethod(key, node);
const nodes = this.uiNodes.get(key) || [];
nodes.push(node);
this.uiNodes.set(key, nodes);
}
bindToSelectBox(key, node) {
node.value = this.configValueToUIValue(this.configs[key]);
node.addEventListener('change', () => {
this.throttledUpdate(key, node, node.value);
});
this.applyLocked(node, key);
this.addResetMethod(key, node);
const nodes = this.uiNodes.get(key) || [];
nodes.push(node);
this.uiNodes.set(key, nodes);
}
addResetMethod(key, node) {
node.$reset = () => {
this.configs.$reset(key);
const value = this.configs.$default[key];
if (this.detectUIType(node) == this.UI_TYPE_CHECKBOX)
node.checked = value;
else
node.value = value;
};
}
async onReady() {
document.removeEventListener('DOMContentLoaded', this.onReady);
if (!this.configs || !this.configs.$loaded)
throw new Error('you must give configs!');
this.configs.$addObserver(this.onConfigChanged);
await this.configs.$loaded;
for (const key of Object.keys(this.configs.$default)) {
const nodes = this.findUIsForKey(key);
if (!nodes.length)
continue;
for (const node of nodes) {
switch (this.detectUIType(node)) {
case this.UI_TYPE_CHECKBOX:
this.bindToCheckbox(key, node);
break;
case this.UI_TYPE_RADIO:
this.bindToRadio(key, node);
break;
case this.UI_TYPE_TEXT_FIELD:
this.bindToTextField(key, node);
break;
case this.UI_TYPE_SELECTBOX:
this.bindToSelectBox(key, node);
break;
case this.UI_MISSING:
continue;
default:
throw new Error(`unknown type UI element for ${key}`);
}
}
}
}
onConfigChanged(key) {
const nodes = this.uiNodes.get(key);
if (!nodes || !nodes.length)
return;
for (const node of nodes) {
if (node.dataset.configValueUpdating == 'true')
continue;
if (node.matches('input[type="radio"]')) {
node.checked = this.configs[key] == node.value;
}
else if (node.matches('input[type="checkbox"]')) {
node.checked = !!this.configs[key];
}
else {
node.value = this.configValueToUIValue(this.configs[key]);
}
this.applyLocked(node, key);
}
}
buildUIForAllConfigs(parent) {
parent = parent || document.body;
const range = document.createRange();
range.selectNodeContents(parent);
range.collapse(false);
const rows = [];
for (const key of Object.keys(this.configs.$default).sort()) {
const value = this.configs.$default[key];
const type = typeof value == 'number' ? 'number' :
typeof value == 'boolean' ? 'checkbox' :
'text' ;
// To accept decimal values like "1.1", we need to set "step" with decmimal values.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number
const step = type != 'number' ? '' : `step="${this.sanitizeForHTMLText(key in this.steps ? this.steps[key] : String(1.75).replace(/[1-9]/g, '0').replace(/0$/, '1'))}"`;
const placeholder = type == 'checkbox' ? '' : `placeholder=${JSON.stringify(this.sanitizeForHTMLText(String(value)))}`;
rows.push(`
<tr ${rows.length > 0 ? 'style="border-top: 1px solid rgba(0, 0, 0, 0.2);"' : ''}>
<td style="width: 45%; word-break: break-all;">
<label for="allconfigs-field-${this.sanitizeForHTMLText(key)}">${this.sanitizeForHTMLText(key)}</label>
</td>
<td style="width: 35%;">
<input id="allconfigs-field-${this.sanitizeForHTMLText(key)}"
type="${type}"
${type != 'checkbox' && type != 'radio' ? 'style="width: 100%;"' : ''}
${step}
${placeholder}>
</td>
<td>
<button id="allconfigs-reset-${this.sanitizeForHTMLText(key)}">Reset</button>
</td>
</tr>
`);
}
const fragment = range.createContextualFragment(`
<table id="allconfigs-table"
style="border-collapse: collapse">
<tbody>${rows.join('')}</tbody>
</table>
<div>
<button id="allconfigs-reset-all">Reset All</button>
<button id="allconfigs-export">Export</button>
<a id="allconfigs-export-file"
type="application/json"
download="configs-${browser.runtime.id}.json"
style="display:none"></a>
<button id="allconfigs-import">Import</button>
<input id="allconfigs-import-file"
type="file"
accept="application/json"
style="display:none">
</div>
`);
range.insertNode(fragment);
range.detach();
const table = document.getElementById('allconfigs-table');
for (const input of table.querySelectorAll('input')) {
const key = input.id.replace(/^allconfigs-field-/, '');
switch (this.detectUIType(input)) {
case this.UI_TYPE_CHECKBOX:
this.bindToCheckbox(key, input);
break;
case this.UI_TYPE_TEXT_FIELD:
this.bindToTextField(key, input);
break;
}
const button = table.querySelector(`#allconfigs-reset-${this._sanitizeForSelector(key)}`);
button.addEventListener('click', () => {
input.$reset();
});
button.addEventListener('keyup', event => {
if (event.key == 'Enter' || event.key == ' ')
input.$reset();
});
}
const resetAllButton = document.getElementById('allconfigs-reset-all');
resetAllButton.addEventListener('keydown', event => {
if (event.key == 'Enter' || event.key == ' ')
this.resetAll();
});
resetAllButton.addEventListener('click', event => {
if (event.button == 0)
this.resetAll();
});
const exportButton = document.getElementById('allconfigs-export');
exportButton.addEventListener('keydown', event => {
if (event.key == 'Enter' || event.key == ' ')
this.exportToFile();
});
exportButton.addEventListener('click', event => {
if (event.button == 0)
this.exportToFile();
});
const importButton = document.getElementById('allconfigs-import');
importButton.addEventListener('keydown', event => {
if (event.key == 'Enter' || event.key == ' ')
this.importFromFile();
});
importButton.addEventListener('click', event => {
if (event.button == 0)
this.importFromFile();
});
const fileField = document.getElementById('allconfigs-import-file');
fileField.addEventListener('change', async _event => {
const text = await fileField.files.item(0).text();
let values = JSON.parse(text);
if (typeof this.$onImporting == 'function')
values = await this.$onImporting(values);
for (const key of Object.keys(this.configs.$default)) {
const value = values[key] !== undefined ? values[key] : this.configs.$default[key];
const changed = value != this.configs[key];
this.configs[key] = value;
if (changed)
this.onConfigChanged(key);
}
if (typeof this.$onImported == 'function')
await this.$onImported();
});
}
sanitizeForHTMLText(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
resetAll() {
this.configs.$reset();
}
importFromFile() {
document.getElementById('allconfigs-import-file').click();
}
async exportToFile() {
let values = {};
for (const key of Object.keys(this.configs.$default).sort()) {
const defaultValue = JSON.stringify(this.configs.$default[key]);
const currentValue = JSON.stringify(this.configs[key]);
if (defaultValue !== currentValue) {
values[key] = this.configs[key];
}
}
if (typeof this.$onExporting == 'function')
values = await this.$onExporting(values);
// Pretty print the exported JSON, because some major addons
// including Stylus and uBlock do that.
const exported = JSON.stringify(values, null, 2);
const browserInfo = browser.runtime.getBrowserInfo && await browser.runtime.getBrowserInfo();
switch (browserInfo && browserInfo.name) {
case 'Thunderbird':
window.open(`data:application/json,${encodeURIComponent(exported)}`);
break;
default:
const link = document.getElementById('allconfigs-export-file');
link.href = URL.createObjectURL(new Blob([exported], { type: 'application/json' }));
link.click();
break;
}
if (typeof this.$onExported == 'function')
await this.$onExported();
}
_sanitizeForSelector(string) {
return string.replace(/[:\[\]()]/g, '\\$&');
}
};
Options.prototype.UI_TYPE_UNKNOWN = 0;
Options.prototype.UI_TYPE_TEXT_FIELD = 1 << 0;
Options.prototype.UI_TYPE_CHECKBOX = 1 << 1;
Options.prototype.UI_TYPE_RADIO = 1 << 2;
Options.prototype.UI_TYPE_SELECTBOX = 1 << 3;
export default Options;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,677 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
original:
http://github.com/piroor/webextensions-lib-tab-favicon-helper
*/
'use strict';
const TabFavIconHelper = {
LAST_EFFECTIVE_FAVICON: 'last-effective-favIcon',
VALID_FAVICON_PATTERN: /^(about|app|chrome|data|file|ftp|https?|moz-extension|resource):/,
DRAWABLE_FAVICON_PATTERN: /^(https?|moz-extension|resource):/,
// original: chrome://browser/content/aboutlogins/icons/favicon.svg
FAVICON_LOCKWISE: `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32">
<defs>
<linearGradient id="b" x1="24.684" y1="5.756" x2="6.966" y2="26.663" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ff9640"/>
<stop offset=".6" stop-color="#fc4055"/>
<stop offset="1" stop-color="#e31587"/>
</linearGradient>
<linearGradient id="c" x1="26.362" y1="4.459" x2="10.705" y2="21.897" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff36e" stop-opacity=".8"/>
<stop offset=".094" stop-color="#fff36e" stop-opacity=".699"/>
<stop offset=".752" stop-color="#fff36e" stop-opacity="0"/>
</linearGradient>
<linearGradient id="d" x1="7.175" y1="27.275" x2="23.418" y2="11.454" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#0090ed"/>
<stop offset=".386" stop-color="#5b6df8"/>
<stop offset=".629" stop-color="#9059ff"/>
<stop offset="1" stop-color="#b833e1"/>
</linearGradient>
<linearGradient id="a" x1="29.104" y1="15.901" x2="26.135" y2="21.045" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#722291" stop-opacity=".5"/>
<stop offset=".5" stop-color="#722291" stop-opacity="0"/>
</linearGradient>
<linearGradient id="e" x1="20.659" y1="21.192" x2="13.347" y2="28.399" xlink:href="#a"/>
<linearGradient id="f" x1="2.97" y1="19.36" x2="10.224" y2="21.105" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#054096" stop-opacity=".5"/>
<stop offset=".054" stop-color="#0f3d9c" stop-opacity=".441"/>
<stop offset=".261" stop-color="#2f35b1" stop-opacity=".249"/>
<stop offset=".466" stop-color="#462fbf" stop-opacity=".111"/>
<stop offset=".669" stop-color="#542bc8" stop-opacity=".028"/>
<stop offset=".864" stop-color="#592acb" stop-opacity="0"/>
</linearGradient>
</defs>
<path d="M15.986 31.076A5.635 5.635 0 0 1 12.3 29.76 103.855 103.855 0 0 1 2.249 19.7a5.841 5.841 0 0 1-.006-7.4A103.792 103.792 0 0 1 12.3 2.247a5.837 5.837 0 0 1 7.4-.006A104.1 104.1 0 0 1 29.751 12.3a5.842 5.842 0 0 1 .006 7.405c-.423.484-.584.661-.917 1.025l-.234.255a1.749 1.749 0 1 1-2.585-2.357l.234-.258c.314-.344.467-.511.86-.961a2.352 2.352 0 0 0-.008-2.814 100.308 100.308 0 0 0-9.7-9.714 2.352 2.352 0 0 0-2.814.007 100.323 100.323 0 0 0-9.7 9.7 2.354 2.354 0 0 0 .006 2.815 100.375 100.375 0 0 0 9.7 9.708 2.352 2.352 0 0 0 2.815-.006c1.311-1.145 2.326-2.031 3.434-3.086l-3.4-3.609a2.834 2.834 0 0 1-.776-2.008 2.675 2.675 0 0 1 .834-1.9l.194-.184a2.493 2.493 0 0 0 1.124-2.333 2.81 2.81 0 0 0-5.6 0 2.559 2.559 0 0 0 1.092 2.279l.127.118a2.735 2.735 0 0 1 .324 3.7 1.846 1.846 0 0 1-.253.262l-1.578 1.326a1.75 1.75 0 0 1-2.252-2.68l.755-.634a5.758 5.758 0 0 1-1.715-4.366 6.305 6.305 0 0 1 12.6 0 5.642 5.642 0 0 1-1.854 4.528l3.84 4.082a2.071 2.071 0 0 1 .59 1.446 2.128 2.128 0 0 1-.628 1.526c-1.6 1.592-2.895 2.72-4.533 4.15a5.745 5.745 0 0 1-3.753 1.354z" fill="url(#b)"/>
<path d="M15.986 31.076A5.635 5.635 0 0 1 12.3 29.76 103.855 103.855 0 0 1 2.249 19.7a5.841 5.841 0 0 1-.006-7.4A103.792 103.792 0 0 1 12.3 2.247a5.837 5.837 0 0 1 7.4-.006A104.1 104.1 0 0 1 29.751 12.3a5.842 5.842 0 0 1 .006 7.405c-.423.484-.584.661-.917 1.025l-.234.255a1.749 1.749 0 1 1-2.585-2.357l.234-.258c.314-.344.467-.511.86-.961a2.352 2.352 0 0 0-.008-2.814 100.308 100.308 0 0 0-9.7-9.714 2.352 2.352 0 0 0-2.814.007 100.323 100.323 0 0 0-9.7 9.7 2.354 2.354 0 0 0 .006 2.815 100.375 100.375 0 0 0 9.7 9.708 2.352 2.352 0 0 0 2.815-.006c1.311-1.145 2.326-2.031 3.434-3.086l-3.4-3.609a2.834 2.834 0 0 1-.776-2.008 2.675 2.675 0 0 1 .834-1.9l.194-.184a2.493 2.493 0 0 0 1.124-2.333 2.81 2.81 0 0 0-5.6 0 2.559 2.559 0 0 0 1.092 2.279l.127.118a2.735 2.735 0 0 1 .324 3.7 1.846 1.846 0 0 1-.253.262l-1.578 1.326a1.75 1.75 0 0 1-2.252-2.68l.755-.634a5.758 5.758 0 0 1-1.715-4.366 6.305 6.305 0 0 1 12.6 0 5.642 5.642 0 0 1-1.854 4.528l3.84 4.082a2.071 2.071 0 0 1 .59 1.446 2.128 2.128 0 0 1-.628 1.526c-1.6 1.592-2.895 2.72-4.533 4.15a5.745 5.745 0 0 1-3.753 1.354z" fill="url(#c)"/>
<path d="M29.58 17.75a34.267 34.267 0 0 0-2.473-3.15 2.352 2.352 0 0 1 .008 2.814c-.393.45-.546.617-.86.961l-.234.258a1.749 1.749 0 1 0 2.585 2.357l.234-.255c.231-.253.379-.415.59-.653a2.161 2.161 0 0 0 .15-2.332zm-8.734 6.275c-1.108 1.055-2.123 1.941-3.434 3.086a2.352 2.352 0 0 1-2.815.006 100.375 100.375 0 0 1-9.7-9.708 2.354 2.354 0 0 1-.006-2.815s-2.131 1.984-2.4 3.424a2.956 2.956 0 0 0 .724 2.782 103.772 103.772 0 0 0 9.085 8.96 5.635 5.635 0 0 0 3.683 1.316 5.745 5.745 0 0 0 3.753-1.351c.926-.808 1.741-1.52 2.565-2.273a1.558 1.558 0 0 0 0-1.476 11.55 11.55 0 0 0-1.455-1.951z" fill="url(#d)"/>
<path d="M29.43 20.079c-.211.238-.359.4-.59.653l-.234.255a1.749 1.749 0 1 1-2.585-2.357l.234-.258c.314-.344.467-.511.86-.961a2.352 2.352 0 0 0-.008-2.814 34.267 34.267 0 0 1 2.473 3.153 2.161 2.161 0 0 1-.15 2.329z" fill="url(#a)"/>
<path d="M22.3 25.976a11.55 11.55 0 0 0-1.458-1.951c-1.108 1.055-2.123 1.941-3.434 3.086a2.352 2.352 0 0 1-2.815.006c-1.414-1.234-2.768-2.5-4.095-3.8L8.023 25.8c1.384 1.35 2.8 2.668 4.28 3.958a5.635 5.635 0 0 0 3.683 1.316 5.745 5.745 0 0 0 3.753-1.351c.926-.808 1.741-1.52 2.565-2.273a1.558 1.558 0 0 0-.004-1.474z" fill="url(#e)"/>
<path d="M4.892 17.409a2.354 2.354 0 0 1-.006-2.815s-2.131 1.984-2.4 3.424a2.956 2.956 0 0 0 .729 2.782c1.56 1.739 3.158 3.4 4.808 5.007l2.479-2.48c-1.935-1.891-3.802-3.844-5.61-5.918z" opacity=".9" fill="url(#f)"/>
</svg>
`,
// original: chrome://browser/content/robot.ico
FAVICON_ROBOT: `
data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACGFjVEwAAAASAAAAAJNtBPIAAAAaZmNUTAAAAAAAAAAQAAAAEAAAAAAAAAAALuAD6AABhIDeugAAALhJREFUOI2Nk8sNxCAMRDlGohauXFOMpfTiAlxICqAELltHLqlgctg1InzMRhpFAc+LGWTnmoeZYamt78zXdZmaQtQMADlnU0OIAlbmJUBEcO4bRKQY2rUXIPmAGnDuG/Bx3/fvOPVaDUg+oAPUf1PArIMCSD5glMEsUGaG+kyAFWIBaCsKuA+HGCNijLgP133XgOEtaPFMy2vUolEGJoCIzBmoRUR9+7rxj16DZaW/mgtmxnJ8V3oAnApQwNS5zpcAAAAaZmNUTAAAAAEAAAAQAAAAEAAAAAAAAAAAAB4D6AIB52fclgAAACpmZEFUAAAAAjiNY2AYBVhBc3Pzf2LEcGreqcbwH1kDNjHauWAUjAJyAADymxf9WF+u8QAAABpmY1RMAAAAAwAAABAAAAAQAAAAAAAAAAAAHgPoAgEK8Q9/AAAAFmZkQVQAAAAEOI1jYBgFo2AUjAIIAAAEEAAB0xIn4wAAABpmY1RMAAAABQAAABAAAAAQAAAAAAAAAAAAHgPoAgHnO30FAAAAQGZkQVQAAAAGOI1jYBieYKcaw39ixHCC/6cwFWMTw2rz/1MM/6Vu/f///xTD/51qEIwuRjsXILuEGLFRMApgAADhNCsVfozYcAAAABpmY1RMAAAABwAAABAAAAAQAAAAAAAAAAAAHgPoAgEKra7sAAAAFmZkQVQAAAAIOI1jYBgFo2AUjAIIAAAEEAABM9s3hAAAABpmY1RMAAAACQAAABAAAAAQAAAAAAAAAAAAHgPoAgHn3p+wAAAAKmZkQVQAAAAKOI1jYBgFWEFzc/N/YsRwat6pxvAfWQM2Mdq5YBSMAnIAAPKbF/1BhPl6AAAAGmZjVEwAAAALAAAAEAAAABAAAAAAAAAAAAAeA+gCAQpITFkAAAAWZmRBVAAAAAw4jWNgGAWjYBSMAggAAAQQAAHaszpmAAAAGmZjVEwAAAANAAAAEAAAABAAAAAAAAAAAAAeA+gCAeeCPiMAAABAZmRBVAAAAA44jWNgGJ5gpxrDf2LEcIL/pzAVYxPDavP/Uwz/pW79////FMP/nWoQjC5GOxcgu4QYsVEwCmAAAOE0KxUmBL0KAAAAGmZjVEwAAAAPAAAAEAAAABAAAAAAAAAAAAAeA+gCAQoU7coAAAAWZmRBVAAAABA4jWNgGAWjYBSMAggAAAQQAAEpOBELAAAAGmZjVEwAAAARAAAAEAAAABAAAAAAAAAAAAAeA+gCAeYVWtoAAAAqZmRBVAAAABI4jWNgGAVYQXNz839ixHBq3qnG8B9ZAzYx2rlgFIwCcgAA8psX/WvpAecAAAAaZmNUTAAAABMAAAAQAAAAEAAAAAAAAAAAAB4D6AIBC4OJMwAAABZmZEFUAAAAFDiNY2AYBaNgFIwCCAAABBAAAcBQHOkAAAAaZmNUTAAAABUAAAAQAAAAEAAAAAAAAAAAAB4D6AIB5kn7SQAAAEBmZEFUAAAAFjiNY2AYnmCnGsN/YsRwgv+nMBVjE8Nq8/9TDP+lbv3///8Uw/+dahCMLkY7FyC7hBixUTAKYAAA4TQrFc+cEoQAAAAaZmNUTAAAABcAAAAQAAAAEAAAAAAAAAAAAB4D6AIBC98ooAAAABZmZEFUAAAAGDiNY2AYBaNgFIwCCAAABBAAASCZDI4AAAAaZmNUTAAAABkAAAAQAAAAEAAAAAAAAAAAAB4D6AIB5qwZ/AAAACpmZEFUAAAAGjiNY2AYBVhBc3Pzf2LEcGreqcbwH1kDNjHauWAUjAJyAADymxf9cjJWbAAAABpmY1RMAAAAGwAAABAAAAAQAAAAAAAAAAAAHgPoAgELOsoVAAAAFmZkQVQAAAAcOI1jYBgFo2AUjAIIAAAEEAAByfEBbAAAABpmY1RMAAAAHQAAABAAAAAQAAAAAAAAAAAAHgPoAgHm8LhvAAAAQGZkQVQAAAAeOI1jYBieYKcaw39ixHCC/6cwFWMTw2rz/1MM/6Vu/f///xTD/51qEIwuRjsXILuEGLFRMApgAADhNCsVlxR3/gAAABpmY1RMAAAAHwAAABAAAAAQAAAAAAAAAAAAHgPoAgELZmuGAAAAFmZkQVQAAAAgOI1jYBgFo2AUjAIIAAAEEAABHP5cFQAAABpmY1RMAAAAIQAAABAAAAAQAAAAAAAAAAAAHgPoAgHlgtAOAAAAKmZkQVQAAAAiOI1jYBgFWEFzc/N/YsRwat6pxvAfWQM2Mdq5YBSMAnIAAPKbF/0/MvDdAAAAAElFTkSuQmCC
`.trim(),
// original: chrome://browser/skin/controlcenter/dashboard.svg
FAVICON_DASHBOARD: `
<svg data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M15 12H4a2 2 0 0 1-2-2V3a1 1 0 0 0-2 0v7a4 4 0 0 0 4 4h11a1 1 0 0 0 0-2z"/>
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M4 11a1 1 0 0 0 1-1V8a1 1 0 0 0-2 0v2a1 1 0 0 0 1 1zM7 11a1 1 0 0 0 1-1V4a1 1 0 0 0-2 0v6a1 1 0 0 0 1 1zM10 11a1 1 0 0 0 1-1V6a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1zM13 11a1 1 0 0 0 1-1V7a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1z"/>
</svg>
`,
// original: chrome://browser/skin/developer.svg
FAVICON_DEVELOPER: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.555 3.2l-2.434 2.436a1.243 1.243 0 1 1-1.757-1.757L12.8 1.445A3.956 3.956 0 0 0 11 1a3.976 3.976 0 0 0-3.434 6.02l-6.273 6.273a1 1 0 1 0 1.414 1.414L8.98 8.434A3.96 3.96 0 0 0 11 9a4 4 0 0 0 4-4 3.956 3.956 0 0 0-.445-1.8z"/>
</svg>
`,
// original: chrome://browser/skin/privatebrowsing/favicon.svg
FAVICON_PRIVATE_BROWSING: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="8" fill="#8d20ae"/>
<circle cx="8" cy="8" r="7.5" stroke="#7b149a" stroke-width="1" fill="none"/>
<path d="M11.309,10.995C10.061,10.995,9.2,9.5,8,9.5s-2.135,1.5-3.309,1.5c-1.541,0-2.678-1.455-2.7-3.948C1.983,5.5,2.446,5.005,4.446,5.005S7.031,5.822,8,5.822s1.555-.817,3.555-0.817S14.017,5.5,14.006,7.047C13.987,9.54,12.85,10.995,11.309,10.995ZM5.426,6.911a1.739,1.739,0,0,0-1.716.953A2.049,2.049,0,0,0,5.3,8.544c0.788,0,1.716-.288,1.716-0.544A1.428,1.428,0,0,0,5.426,6.911Zm5.148,0A1.429,1.429,0,0,0,8.981,8c0,0.257.928,0.544,1.716,0.544a2.049,2.049,0,0,0,1.593-.681A1.739,1.739,0,0,0,10.574,6.911Z" stroke="#670c83" stroke-width="2" fill="none"/>
<path d="M11.309,10.995C10.061,10.995,9.2,9.5,8,9.5s-2.135,1.5-3.309,1.5c-1.541,0-2.678-1.455-2.7-3.948C1.983,5.5,2.446,5.005,4.446,5.005S7.031,5.822,8,5.822s1.555-.817,3.555-0.817S14.017,5.5,14.006,7.047C13.987,9.54,12.85,10.995,11.309,10.995ZM5.426,6.911a1.739,1.739,0,0,0-1.716.953A2.049,2.049,0,0,0,5.3,8.544c0.788,0,1.716-.288,1.716-0.544A1.428,1.428,0,0,0,5.426,6.911Zm5.148,0A1.429,1.429,0,0,0,8.981,8c0,0.257.928,0.544,1.716,0.544a2.049,2.049,0,0,0,1.593-.681A1.739,1.739,0,0,0,10.574,6.911Z" fill="#fff"/>
</svg>
`,
// original: chrome://browser/skin/settings.svg
FAVICON_SETTINGS: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M15 7h-2.1a4.967 4.967 0 0 0-.732-1.753l1.49-1.49a1 1 0 0 0-1.414-1.414l-1.49 1.49A4.968 4.968 0 0 0 9 3.1V1a1 1 0 0 0-2 0v2.1a4.968 4.968 0 0 0-1.753.732l-1.49-1.49a1 1 0 0 0-1.414 1.415l1.49 1.49A4.967 4.967 0 0 0 3.1 7H1a1 1 0 0 0 0 2h2.1a4.968 4.968 0 0 0 .737 1.763c-.014.013-.032.017-.045.03l-1.45 1.45a1 1 0 1 0 1.414 1.414l1.45-1.45c.013-.013.018-.031.03-.045A4.968 4.968 0 0 0 7 12.9V15a1 1 0 0 0 2 0v-2.1a4.968 4.968 0 0 0 1.753-.732l1.49 1.49a1 1 0 0 0 1.414-1.414l-1.49-1.49A4.967 4.967 0 0 0 12.9 9H15a1 1 0 0 0 0-2zM5 8a3 3 0 1 1 3 3 3 3 0 0 1-3-3z"/>
</svg>
`,
// original: chrome://browser/skin/window.svg
FAVICON_WINDOW: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13 1H3a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h11a2 2 0 0 0 2-2V4a3 3 0 0 0-3-3zm1 11a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h12zm0-7H2V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1z"/>
</svg>
`,
// original: chrome://devtools/skin/images/profiler-stopwatch.svg
FAVICON_PROFILER: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M10.85 6.85a.5.5 0 0 0-.7-.7l-2.5 2.5.7.7 2.5-2.5zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 1a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2H6a1 1 0 0 1-1-1zM8 4a5 5 0 1 0 0 10A5 5 0 0 0 8 4zM1 9a7 7 0 1 1 14 0A7 7 0 0 1 1 9z"/>
</svg>
`,
// original: chrome://global/skin/icons/performance.svg
FAVICON_PERFORMANCE: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="context-fill" d="M8 1a8 8 0 0 0-8 8 7.89 7.89 0 0 0 .78 3.43 1 1 0 0 0 .9.57.94.94 0 0 0 .43-.1 1 1 0 0 0 .47-1.33A6.07 6.07 0 0 1 2 9a6 6 0 0 1 12 0 5.93 5.93 0 0 1-.59 2.57 1 1 0 0 0 1.81.86A7.89 7.89 0 0 0 16 9a8 8 0 0 0-8-8z"/>
<path fill="context-fill" d="M11.77 7.08a.5.5 0 0 0-.69.15L8.62 11.1A2.12 2.12 0 0 0 8 11a2 2 0 0 0 0 4 2.05 2.05 0 0 0 1.12-.34 2.31 2.31 0 0 0 .54-.54 2 2 0 0 0 0-2.24 2.31 2.31 0 0 0-.2-.24l2.46-3.87a.5.5 0 0 0-.15-.69z"/>
</svg>
`,
// original: chrome://global/skin/icons/warning.svg
FAVICON_WARNING: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.742 12.106L9.789 2.2a2 2 0 0 0-3.578 0l-4.953 9.91A2 2 0 0 0 3.047 15h9.905a2 2 0 0 0 1.79-2.894zM7 5a1 1 0 0 1 2 0v4a1 1 0 0 1-2 0zm1 8.25A1.25 1.25 0 1 1 9.25 12 1.25 1.25 0 0 1 8 13.25z"/>
</svg>
`,
// original: chrome://mozapps/skin/extensions/extensionGeneric-16.svg
FAVICON_EXTENSION: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M14.5 8c-.971 0-1 1-1.75 1a.765.765 0 0 1-.75-.75V5a1 1 0 0 0-1-1H7.75A.765.765 0 0 1 7 3.25c0-.75 1-.779 1-1.75C8 .635 7.1 0 6 0S4 .635 4 1.5c0 .971 1 1 1 1.75a.765.765 0 0 1-.75.75H1a1 1 0 0 0-1 1v2.25A.765.765 0 0 0 .75 8c.75 0 .779-1 1.75-1C3.365 7 4 7.9 4 9s-.635 2-1.5 2c-.971 0-1-1-1.75-1a.765.765 0 0 0-.75.75V15a1 1 0 0 0 1 1h3.25a.765.765 0 0 0 .75-.75c0-.75-1-.779-1-1.75 0-.865.9-1.5 2-1.5s2 .635 2 1.5c0 .971-1 1-1 1.75a.765.765 0 0 0 .75.75H11a1 1 0 0 0 1-1v-3.25a.765.765 0 0 1 .75-.75c.75 0 .779 1 1.75 1 .865 0 1.5-.9 1.5-2s-.635-2-1.5-2z"/>
</svg>
`,
// original: globe-16.svg
FAVICON_GLOBE: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" d="M8 0a8 8 0 1 0 8 8 8.009 8.009 0 0 0-8-8zm5.163 4.958h-1.552a7.7 7.7 0 0 0-1.051-2.376 6.03 6.03 0 0 1 2.603 2.376zM14 8a5.963 5.963 0 0 1-.335 1.958h-1.821A12.327 12.327 0 0 0 12 8a12.327 12.327 0 0 0-.156-1.958h1.821A5.963 5.963 0 0 1 14 8zm-6 6c-1.075 0-2.037-1.2-2.567-2.958h5.135C10.037 12.8 9.075 14 8 14zM5.174 9.958a11.084 11.084 0 0 1 0-3.916h5.651A11.114 11.114 0 0 1 11 8a11.114 11.114 0 0 1-.174 1.958zM2 8a5.963 5.963 0 0 1 .335-1.958h1.821a12.361 12.361 0 0 0 0 3.916H2.335A5.963 5.963 0 0 1 2 8zm6-6c1.075 0 2.037 1.2 2.567 2.958H5.433C5.963 3.2 6.925 2 8 2zm-2.56.582a7.7 7.7 0 0 0-1.051 2.376H2.837A6.03 6.03 0 0 1 5.44 2.582zm-2.6 8.46h1.549a7.7 7.7 0 0 0 1.051 2.376 6.03 6.03 0 0 1-2.603-2.376zm7.723 2.376a7.7 7.7 0 0 0 1.051-2.376h1.552a6.03 6.03 0 0 1-2.606 2.376z"/>
</svg>
`,
async _urlToKey(url) { // sha1 hash
const encoder = new TextEncoder();
const data = encoder.encode(url);
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('');
return hashHex;
},
DB_NAME: 'TabFavIconHelper',
DB_VERSION: 2,
STORE_FAVICONS: 'favIcons',
STORE_EFFECTIVE_FAVICONS: 'effectiveFavIcons',
STORE_UNEFFECTIVE_FAVICONS: 'uneffectiveFavIcons',
EXPIRATION_TIME_IN_MSEC: 7 * 24 * 60 * 60 * 1000, // 7 days
async _openDB() {
if (this._openedDB)
return this._openedDB;
return new Promise((resolve, _reject) => {
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
request.onerror = () => {
// This can fail if this is in a private window.
// See: https://github.com/piroor/treestyletab/issues/3387
//reject(new Error('Failed to open database'));
resolve(null);
};
request.onsuccess = () => {
const db = request.result;
this._openedDB = db;
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
const objectStores = db.objectStoreNames;
const needToUpgrade = event.oldVersion < this.DB_VERSION;
if (needToUpgrade) {
if (objectStores.contains(this.STORE_FAVICONS))
db.deleteObjectStore(this.STORE_FAVICONS);
if (objectStores.contains(this.STORE_EFFECTIVE_FAVICONS))
db.deleteObjectStore(this.STORE_EFFECTIVE_FAVICONS);
if (objectStores.contains(this.STORE_UNEFFECTIVE_FAVICONS))
db.deleteObjectStore(this.STORE_UNEFFECTIVE_FAVICONS);
}
if (needToUpgrade ||
!objectStores.contains(this.STORE_FAVICONS)) {
const favIconsStore = db.createObjectStore(this.STORE_FAVICONS, { keyPath: 'key', unique: true });
favIconsStore.createIndex('urlKey', 'urlKey', { unique: false });
favIconsStore.createIndex('timestamp', 'timestamp');
}
if (needToUpgrade ||
!objectStores.contains(this.STORE_EFFECTIVE_FAVICONS)) {
const effectiveFavIconsStore = db.createObjectStore(this.STORE_EFFECTIVE_FAVICONS, { keyPath: 'urlKey', unique: true });
effectiveFavIconsStore.createIndex('timestamp', 'timestamp');
effectiveFavIconsStore.createIndex('favIconKey', 'favIconKey', { unique: false });
}
if (needToUpgrade ||
!objectStores.contains(this.STORE_UNEFFECTIVE_FAVICONS)) {
const uneffectiveFavIconsStore = db.createObjectStore(this.STORE_UNEFFECTIVE_FAVICONS, { keyPath: 'urlKey', unique: true });
uneffectiveFavIconsStore.createIndex('timestamp', 'timestamp');
uneffectiveFavIconsStore.createIndex('favIconKey', 'favIconKey', { unique: false });
}
};
});
},
async _associateFavIconUrlToTabUrl({ favIconUrl, tabUrl, store } = {}) {
const [db, tabUrlKey, favIconKey] = await Promise.all([
this._openDB(),
this._urlToKey(tabUrl),
this._urlToKey(favIconUrl),
]);
if (!db)
return;
try {
const transaction = db.transaction([store, this.STORE_FAVICONS], 'readwrite');
const associationStore = transaction.objectStore(store);
const favIconStore = transaction.objectStore(this.STORE_FAVICONS);
const timestamp = Date.now();
const associationRequest = associationStore.put({ urlKey: tabUrlKey, favIconKey, timestamp });
const favIconRequest = favIconStore.put({ key: favIconKey, url: favIconUrl, timestamp });
transaction.oncomplete = () => {
//db.close();
this._reserveToExpireOldEntries();
favIconUrl = undefined;
tabUrl = undefined;
store = undefined;
};
associationRequest.onerror = event => {
console.error(`Failed to associate favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, event);
};
favIconRequest.onerror = event => {
console.error(`Failed to store favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, event);
};
}
catch(error) {
console.error(`Failed to associate favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, error);
}
},
async _unassociateFavIconUrlFromTabUrl({ tabUrl, store } = {}) {
const [db, tabUrlKey] = await Promise.all([
this._openDB(),
this._urlToKey(tabUrl),
]);
if (!db)
return;
try {
const transaction = db.transaction([store], 'readwrite');
const associationStore = transaction.objectStore(store);
const unassociationRequest = associationStore.delete(tabUrlKey);
transaction.oncomplete = () => {
//db.close();
this._reserveToExpireOldEntries();
tabUrl = undefined;
store = undefined;
};
unassociationRequest.onerror = event => {
console.error(`Failed to unassociate favIconUrl from tabUrl ${tabUrl} in the store ${store}`, event);
};
}
catch(error) {
console.error(`Failed to unassociate favIconUrl from tabUrl ${tabUrl} in the store ${store}`, error);
}
},
async _getAssociatedFavIconUrlFromTabUrl({ tabUrl, store } = {}) {
return new Promise(async (resolve, _reject) => {
const [db, tabUrlKey] = await Promise.all([
this._openDB(),
this._urlToKey(tabUrl),
]);
if (!db) {
resolve(null);
return;
}
try {
const transaction = db.transaction([store, this.STORE_FAVICONS], 'readonly');
const associationStore = transaction.objectStore(store);
const favIconStore = transaction.objectStore(this.STORE_FAVICONS);
const associationRequest = associationStore.get(tabUrlKey);
associationRequest.onsuccess = () => {
const association = associationRequest.result;
if (!association) {
//console.log(`No associated favIconUrl for the tabUrl ${tabUrl} in the store ${store}`);
resolve(null);
return;
}
const favIconRequest = favIconStore.get(association.favIconKey);
favIconRequest.onsuccess = () => {
let favIcon = favIconRequest.result;
if (!favIcon) {
//console.log(`FavIcon data not found for the tabUrl ${tabUrl} in the store ${store}`);
resolve(null);
return;
}
if (favIcon.timestamp < Date.now() - this.EXPIRATION_TIME_IN_MSEC) {
//console.log(`FavIcon data is expired for the tabUrl ${tabUrl} in the store ${store}`);
this._reserveToExpireOldEntries();
resolve(null);
return;
}
resolve(favIcon.url);
favIcon.url = undefined;
favIcon = undefined;
};
favIconRequest.onerror = event => {
console.error(`Failed to get favIconUrl from tabUrl ${tabUrl}`, event);
resolve(null);
};
};
associationRequest.onerror = event => {
console.error(`Failed to get favIcon association from tabUrl ${tabUrl}`, event);
resolve(null);
};
transaction.oncomplete = () => {
//db.close();
tabUrl = undefined;
store = undefined;
};
}
catch(error) {
console.error('Failed to get from cache:', error);
resolve(null);
}
});
},
async _reserveToExpireOldEntries() {
if (this._reservedExpiration)
clearTimeout(this._reservedExpiration);
this._reservedExpiration = setTimeout(() => {
this._reservedExpiration = null;
this._expireOldEntries();
}, 500);
},
async _expireOldEntries() {
return new Promise(async (resolve, reject) => {
const db = await this._openDB();
if (!db) {
resolve();
return;
}
try {
const transaction = db.transaction([this.STORE_FAVICONS, this.STORE_EFFECTIVE_FAVICONS, this.STORE_UNEFFECTIVE_FAVICONS], 'readwrite');
const favIconsStore = transaction.objectStore(this.STORE_FAVICONS);
const effectiveFavIconsStore = transaction.objectStore(this.STORE_EFFECTIVE_FAVICONS);
const uneffectiveFavIconsStore = transaction.objectStore(this.STORE_UNEFFECTIVE_FAVICONS);
const favIconIndex = favIconsStore.index('timestamp');
const effectiveFavIconIndex = effectiveFavIconsStore.index('timestamp');
const uneffectiveFavIconIndex = uneffectiveFavIconsStore.index('timestamp');
const expirationTimestamp = Date.now() - this.EXPIRATION_TIME_IN_MSEC;
const favIconRequest = favIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
favIconRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor)
return;
const key = cursor.primaryKey;
cursor.continue();
const deleteRequest = favIconsStore.delete(key);
deleteRequest.onerror = event => {
console.error(`Failed to clear favicon index`, event);
};
};
favIconRequest.onerror = event => {
console.error(`Failed to retrieve favicon index`, event);
};
const effectiveFavIconRequest = effectiveFavIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
effectiveFavIconRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor)
return;
const url = cursor.primaryKey;
cursor.continue();
const deleteRequest = effectiveFavIconsStore.delete(url);
deleteRequest.onerror = event => {
console.error(`Failed to clear effective favicon index`, event);
};
};
effectiveFavIconRequest.onerror = event => {
console.error(`Failed to retrieve effective favicon index`, event);
};
const uneffectiveFavIconRequest = uneffectiveFavIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
uneffectiveFavIconRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor)
return;
const url = cursor.primaryKey;
cursor.continue();
const deleteRequest = uneffectiveFavIconsStore.delete(url);
deleteRequest.onerror = event => {
console.error(`Failed to clear uneffective favicon index`, event);
};
};
uneffectiveFavIconRequest.onerror = event => {
console.error(`Failed to retrieve uneffective favicon index`, event);
};
transaction.oncomplete = () => {
//db.close();
resolve();
};
}
catch(error) {
console.error('Failed to expire old entries:', error);
reject(error);
}
});
},
_tasks: [],
_processStep: 5,
FAVICON_SIZE: 16,
_init() {
this._onTabUpdated = this._onTabUpdated.bind(this);
browser.tabs.onUpdated.addListener(this._onTabUpdated);
this.canvas = document.createElement('canvas');
this.canvas.width = this.canvas.height = this.FAVICON_SIZE;
this.canvas.setAttribute('style', `
visibility: hidden;
pointer-events: none;
position: fixed
`);
document.body.appendChild(this.canvas);
window.addEventListener('unload', () => {
browser.tabs.onUpdated.removeListener(this._onTabUpdated);
}, { once: true });
},
_sessionAPIAvailable: (
browser.sessions &&
browser.sessions.getTabValue &&
browser.sessions.setTabValue &&
browser.sessions.removeTabValue
),
_addTask(task) {
this._tasks.push(task);
this._run();
},
_run() {
if (this._running)
return;
this._running = true;
const processOneTask = () => {
if (this._tasks.length == 0) {
this._running = false;
}
else {
const tasks = this._tasks.splice(0, this._processStep);
while (tasks.length > 0) {
tasks.shift()();
}
window.requestAnimationFrame(processOneTask);
}
};
processOneTask();
},
// public
loadToImage(params = {}) {
this._addTask(() => {
this._getEffectiveFavIconURL(params.tab, params.url)
.then(url => {
params.image.src = url;
params.image.classList.remove('error');
url = undefined;
},
_error => {
params.image.src = '';
params.image.classList.add('error');
});
});
},
// public
maybeImageTab(_tab) { // for backward compatibility
return false;
},
_getSafeFaviconUrl(url) {
switch (url) {
case 'chrome://browser/content/aboutlogins/icons/favicon.svg':
return this._getSVGDataURI(this.FAVICON_LOCKWISE);
case 'chrome://browser/content/robot.ico':
return this.FAVICON_ROBOT;
case 'chrome://browser/skin/controlcenter/dashboard.svg':
return this._getSVGDataURI(this.FAVICON_DASHBOARD);
case 'chrome://browser/skin/developer.svg':
return this._getSVGDataURI(this.FAVICON_DEVELOPER);
case 'chrome://browser/skin/privatebrowsing/favicon.svg':
return this._getSVGDataURI(this.FAVICON_PRIVATE_BROWSING);
case 'chrome://browser/skin/settings.svg':
return this._getSVGDataURI(this.FAVICON_SETTINGS);
case 'chrome://browser/skin/window.svg':
return this._getSVGDataURI(this.FAVICON_WINDOW);
case 'chrome://devtools/skin/images/profiler-stopwatch.svg':
return this._getSVGDataURI(this.FAVICON_PROFILER);
case 'chrome://global/skin/icons/performance.svg':
return this._getSVGDataURI(this.FAVICON_PERFORMANCE);
case 'chrome://global/skin/icons/warning.svg':
return this._getSVGDataURI(this.FAVICON_WARNING);
case 'chrome://mozapps/skin/extensions/extensionGeneric-16.svg':
return this._getSVGDataURI(this.FAVICON_EXTENSION);
default:
if (/^chrome:\/\//.test(url) &&
!/^chrome:\/\/branding\//.test(url))
return this._getSVGDataURI(this.FAVICON_GLOBE);
break;
}
return url;
},
_getSVGDataURI(svg) {
return `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
},
// public
async getLastEffectiveFavIconURL(tab) {
if (tab.favIconUrl?.startsWith('data:'))
return tab.favIconUrl;
const uneffectiveFavIconUrl = await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS });
if (uneffectiveFavIconUrl)
return null;
const favIconUrl = await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
if (favIconUrl)
return favIconUrl;
if (!this._sessionAPIAvailable)
return null;
const lastData = await browser.sessions.getTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON);
return lastData && lastData.url == tab.url && lastData.favIconUrl;
},
async _getEffectiveFavIconURL(tab, favIconUrl = null) {
if (tab.favIconUrl?.startsWith('data:')) {
browser.sessions.removeTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON);
this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS });
return tab.favIconUrl;
}
return new Promise(async (resolve, reject) => {
favIconUrl = this._getSafeFaviconUrl(favIconUrl || tab.favIconUrl);
let storedFavIconUrl;
if (!favIconUrl && tab.discarded) {
// discarded tab doesn't have favIconUrl, so we should use cached data.
storedFavIconUrl = favIconUrl = await this.getLastEffectiveFavIconURL(tab);
}
let loader, onLoad, onError;
const clear = (() => {
if (loader) {
loader.removeEventListener('load', onLoad, { once: true });
loader.removeEventListener('error', onError, { once: true });
}
loader = onLoad = onError = favIconUrl = storedFavIconUrl = undefined;
});
onLoad = async foundFavIconUrl => {
let dataURL = null;
if (this.DRAWABLE_FAVICON_PATTERN.test(favIconUrl)) {
const context = this.canvas.getContext('2d');
context.clearRect(0, 0, this.FAVICON_SIZE, this.FAVICON_SIZE);
context.drawImage(loader, 0, 0, this.FAVICON_SIZE, this.FAVICON_SIZE);
try {
dataURL = this.canvas.toDataURL('image/png');
}
catch(_error) {
// it can fail due to security reasons
}
}
const oldFavIconUrl = foundFavIconUrl || await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
if (!oldFavIconUrl ||
oldFavIconUrl != favIconUrl) {
if (this._sessionAPIAvailable)
browser.sessions.setTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON, {
url: tab.url,
favIconUrl,
});
}
this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_EFFECTIVE_FAVICONS });
this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS });
resolve(dataURL || favIconUrl);
clear();
};
onError = async error => {
this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_UNEFFECTIVE_FAVICONS });
if (this._sessionAPIAvailable)
browser.sessions.removeTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON);
clear();
reject(error || new Error('No effective icon'));
};
storedFavIconUrl = storedFavIconUrl || await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
if (storedFavIconUrl)
return onLoad(storedFavIconUrl);
if (!favIconUrl ||
!this.VALID_FAVICON_PATTERN.test(favIconUrl)) {
onError();
return;
}
loader = new Image();
if (/^https?:/.test(favIconUrl))
loader.crossOrigin = 'anonymous';
loader.addEventListener('load', () => onLoad(), { once: true });
loader.addEventListener('error', onError, { once: true });
try {
loader.src = favIconUrl;
}
catch(error) {
this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_UNEFFECTIVE_FAVICONS });
onError(error);
}
});
},
_onTabUpdated(tabId, changeInfo, _tab) {
if (!this._hasFavIconInfo(changeInfo))
return;
let timer = this._updatingTabs.get(tabId);
if (timer)
clearTimeout(timer);
// Updating of last effective favicon must be done after the loading
// of the tab itself is correctly done, to avoid cookie problems on
// some websites.
// See also: https://github.com/piroor/treestyletab/issues/2064
timer = setTimeout(async () => {
this._updatingTabs.delete(tabId);
const tab = await browser.tabs.get(tabId);
if (!tab ||
(changeInfo.favIconUrl &&
tab.favIconUrl != changeInfo.favIconUrl) ||
(changeInfo.url &&
tab.url != changeInfo.url) ||
!this._hasFavIconInfo(tab))
return; // expired
await this._getEffectiveFavIconURL(
tab,
changeInfo.favIconUrl
).catch(_error => {});
}, 5000);
this._updatingTabs.set(tabId, timer);
},
_hasFavIconInfo(tabOrChangeInfo) {
return 'favIconUrl' in tabOrChangeInfo;
},
_updatingTabs: new Map(),
};
TabFavIconHelper._init(); // eslint-disable-line no-underscore-dangle
export default TabFavIconHelper;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,863 @@
/* CodeMirror version 5.65.19 */
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode("css", function(config, parserConfig) {
var inline = parserConfig.inline
if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css");
var indentUnit = config.indentUnit,
tokenHooks = parserConfig.tokenHooks,
documentTypes = parserConfig.documentTypes || {},
mediaTypes = parserConfig.mediaTypes || {},
mediaFeatures = parserConfig.mediaFeatures || {},
mediaValueKeywords = parserConfig.mediaValueKeywords || {},
propertyKeywords = parserConfig.propertyKeywords || {},
nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {},
fontProperties = parserConfig.fontProperties || {},
counterDescriptors = parserConfig.counterDescriptors || {},
colorKeywords = parserConfig.colorKeywords || {},
valueKeywords = parserConfig.valueKeywords || {},
allowNested = parserConfig.allowNested,
lineComment = parserConfig.lineComment,
supportsAtComponent = parserConfig.supportsAtComponent === true,
highlightNonStandardPropertyKeywords = config.highlightNonStandardPropertyKeywords !== false;
var type, override;
function ret(style, tp) { type = tp; return style; }
// Tokenizers
function tokenBase(stream, state) {
var ch = stream.next();
if (tokenHooks[ch]) {
var result = tokenHooks[ch](stream, state);
if (result !== false) return result;
}
if (ch == "@") {
stream.eatWhile(/[\w\\\-]/);
return ret("def", stream.current());
} else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) {
return ret(null, "compare");
} else if (ch == "\"" || ch == "'") {
state.tokenize = tokenString(ch);
return state.tokenize(stream, state);
} else if (ch == "#") {
stream.eatWhile(/[\w\\\-]/);
return ret("atom", "hash");
} else if (ch == "!") {
stream.match(/^\s*\w*/);
return ret("keyword", "important");
} else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) {
stream.eatWhile(/[\w.%]/);
return ret("number", "unit");
} else if (ch === "-") {
if (/[\d.]/.test(stream.peek())) {
stream.eatWhile(/[\w.%]/);
return ret("number", "unit");
} else if (stream.match(/^-[\w\\\-]*/)) {
stream.eatWhile(/[\w\\\-]/);
if (stream.match(/^\s*:/, false))
return ret("variable-2", "variable-definition");
return ret("variable-2", "variable");
} else if (stream.match(/^\w+-/)) {
return ret("meta", "meta");
}
} else if (/[,+>*\/]/.test(ch)) {
return ret(null, "select-op");
} else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
return ret("qualifier", "qualifier");
} else if (/[:;{}\[\]\(\)]/.test(ch)) {
return ret(null, ch);
} else if (stream.match(/^[\w-.]+(?=\()/)) {
if (/^(url(-prefix)?|domain|regexp)$/i.test(stream.current())) {
state.tokenize = tokenParenthesized;
}
return ret("variable callee", "variable");
} else if (/[\w\\\-]/.test(ch)) {
stream.eatWhile(/[\w\\\-]/);
return ret("property", "word");
} else {
return ret(null, null);
}
}
function tokenString(quote) {
return function(stream, state) {
var escaped = false, ch;
while ((ch = stream.next()) != null) {
if (ch == quote && !escaped) {
if (quote == ")") stream.backUp(1);
break;
}
escaped = !escaped && ch == "\\";
}
if (ch == quote || !escaped && quote != ")") state.tokenize = null;
return ret("string", "string");
};
}
function tokenParenthesized(stream, state) {
stream.next(); // Must be '('
if (!stream.match(/^\s*[\"\')]/, false))
state.tokenize = tokenString(")");
else
state.tokenize = null;
return ret(null, "(");
}
// Context management
function Context(type, indent, prev) {
this.type = type;
this.indent = indent;
this.prev = prev;
}
function pushContext(state, stream, type, indent) {
state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context);
return type;
}
function popContext(state) {
if (state.context.prev)
state.context = state.context.prev;
return state.context.type;
}
function pass(type, stream, state) {
return states[state.context.type](type, stream, state);
}
function popAndPass(type, stream, state, n) {
for (var i = n || 1; i > 0; i--)
state.context = state.context.prev;
return pass(type, stream, state);
}
// Parser
function wordAsValue(stream) {
var word = stream.current().toLowerCase();
if (valueKeywords.hasOwnProperty(word))
override = "atom";
else if (colorKeywords.hasOwnProperty(word))
override = "keyword";
else
override = "variable";
}
var states = {};
states.top = function(type, stream, state) {
if (type == "{") {
return pushContext(state, stream, "block");
} else if (type == "}" && state.context.prev) {
return popContext(state);
} else if (supportsAtComponent && /@component/i.test(type)) {
return pushContext(state, stream, "atComponentBlock");
} else if (/^@(-moz-)?document$/i.test(type)) {
return pushContext(state, stream, "documentTypes");
} else if (/^@(media|supports|(-moz-)?document|import)$/i.test(type)) {
return pushContext(state, stream, "atBlock");
} else if (/^@(font-face|counter-style)/i.test(type)) {
state.stateArg = type;
return "restricted_atBlock_before";
} else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/i.test(type)) {
return "keyframes";
} else if (type && type.charAt(0) == "@") {
return pushContext(state, stream, "at");
} else if (type == "hash") {
override = "builtin";
} else if (type == "word") {
override = "tag";
} else if (type == "variable-definition") {
return "maybeprop";
} else if (type == "interpolation") {
return pushContext(state, stream, "interpolation");
} else if (type == ":") {
return "pseudo";
} else if (allowNested && type == "(") {
return pushContext(state, stream, "parens");
}
return state.context.type;
};
states.block = function(type, stream, state) {
if (type == "word") {
var word = stream.current().toLowerCase();
if (propertyKeywords.hasOwnProperty(word)) {
override = "property";
return "maybeprop";
} else if (nonStandardPropertyKeywords.hasOwnProperty(word)) {
override = highlightNonStandardPropertyKeywords ? "string-2" : "property";
return "maybeprop";
} else if (allowNested) {
override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag";
return "block";
} else {
override += " error";
return "maybeprop";
}
} else if (type == "meta") {
return "block";
} else if (!allowNested && (type == "hash" || type == "qualifier")) {
override = "error";
return "block";
} else {
return states.top(type, stream, state);
}
};
states.maybeprop = function(type, stream, state) {
if (type == ":") return pushContext(state, stream, "prop");
return pass(type, stream, state);
};
states.prop = function(type, stream, state) {
if (type == ";") return popContext(state);
if (type == "{" && allowNested) return pushContext(state, stream, "propBlock");
if (type == "}" || type == "{") return popAndPass(type, stream, state);
if (type == "(") return pushContext(state, stream, "parens");
if (type == "hash" && !/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(stream.current())) {
override += " error";
} else if (type == "word") {
wordAsValue(stream);
} else if (type == "interpolation") {
return pushContext(state, stream, "interpolation");
}
return "prop";
};
states.propBlock = function(type, _stream, state) {
if (type == "}") return popContext(state);
if (type == "word") { override = "property"; return "maybeprop"; }
return state.context.type;
};
states.parens = function(type, stream, state) {
if (type == "{" || type == "}") return popAndPass(type, stream, state);
if (type == ")") return popContext(state);
if (type == "(") return pushContext(state, stream, "parens");
if (type == "interpolation") return pushContext(state, stream, "interpolation");
if (type == "word") wordAsValue(stream);
return "parens";
};
states.pseudo = function(type, stream, state) {
if (type == "meta") return "pseudo";
if (type == "word") {
override = "variable-3";
return state.context.type;
}
return pass(type, stream, state);
};
states.documentTypes = function(type, stream, state) {
if (type == "word" && documentTypes.hasOwnProperty(stream.current())) {
override = "tag";
return state.context.type;
} else {
return states.atBlock(type, stream, state);
}
};
states.atBlock = function(type, stream, state) {
if (type == "(") return pushContext(state, stream, "atBlock_parens");
if (type == "}" || type == ";") return popAndPass(type, stream, state);
if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top");
if (type == "interpolation") return pushContext(state, stream, "interpolation");
if (type == "word") {
var word = stream.current().toLowerCase();
if (word == "only" || word == "not" || word == "and" || word == "or")
override = "keyword";
else if (mediaTypes.hasOwnProperty(word))
override = "attribute";
else if (mediaFeatures.hasOwnProperty(word))
override = "property";
else if (mediaValueKeywords.hasOwnProperty(word))
override = "keyword";
else if (propertyKeywords.hasOwnProperty(word))
override = "property";
else if (nonStandardPropertyKeywords.hasOwnProperty(word))
override = highlightNonStandardPropertyKeywords ? "string-2" : "property";
else if (valueKeywords.hasOwnProperty(word))
override = "atom";
else if (colorKeywords.hasOwnProperty(word))
override = "keyword";
else
override = "error";
}
return state.context.type;
};
states.atComponentBlock = function(type, stream, state) {
if (type == "}")
return popAndPass(type, stream, state);
if (type == "{")
return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false);
if (type == "word")
override = "error";
return state.context.type;
};
states.atBlock_parens = function(type, stream, state) {
if (type == ")") return popContext(state);
if (type == "{" || type == "}") return popAndPass(type, stream, state, 2);
return states.atBlock(type, stream, state);
};
states.restricted_atBlock_before = function(type, stream, state) {
if (type == "{")
return pushContext(state, stream, "restricted_atBlock");
if (type == "word" && state.stateArg == "@counter-style") {
override = "variable";
return "restricted_atBlock_before";
}
return pass(type, stream, state);
};
states.restricted_atBlock = function(type, stream, state) {
if (type == "}") {
state.stateArg = null;
return popContext(state);
}
if (type == "word") {
if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) ||
(state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase())))
override = "error";
else
override = "property";
return "maybeprop";
}
return "restricted_atBlock";
};
states.keyframes = function(type, stream, state) {
if (type == "word") { override = "variable"; return "keyframes"; }
if (type == "{") return pushContext(state, stream, "top");
return pass(type, stream, state);
};
states.at = function(type, stream, state) {
if (type == ";") return popContext(state);
if (type == "{" || type == "}") return popAndPass(type, stream, state);
if (type == "word") override = "tag";
else if (type == "hash") override = "builtin";
return "at";
};
states.interpolation = function(type, stream, state) {
if (type == "}") return popContext(state);
if (type == "{" || type == ";") return popAndPass(type, stream, state);
if (type == "word") override = "variable";
else if (type != "variable" && type != "(" && type != ")") override = "error";
return "interpolation";
};
return {
startState: function(base) {
return {tokenize: null,
state: inline ? "block" : "top",
stateArg: null,
context: new Context(inline ? "block" : "top", base || 0, null)};
},
token: function(stream, state) {
if (!state.tokenize && stream.eatSpace()) return null;
var style = (state.tokenize || tokenBase)(stream, state);
if (style && typeof style == "object") {
type = style[1];
style = style[0];
}
override = style;
if (type != "comment")
state.state = states[state.state](type, stream, state);
return override;
},
indent: function(state, textAfter) {
var cx = state.context, ch = textAfter && textAfter.charAt(0);
var indent = cx.indent;
if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev;
if (cx.prev) {
if (ch == "}" && (cx.type == "block" || cx.type == "top" ||
cx.type == "interpolation" || cx.type == "restricted_atBlock")) {
// Resume indentation from parent context.
cx = cx.prev;
indent = cx.indent;
} else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") ||
ch == "{" && (cx.type == "at" || cx.type == "atBlock")) {
// Dedent relative to current context.
indent = Math.max(0, cx.indent - indentUnit);
}
}
return indent;
},
electricChars: "}",
blockCommentStart: "/*",
blockCommentEnd: "*/",
blockCommentContinue: " * ",
lineComment: lineComment,
fold: "brace"
};
});
function keySet(array) {
var keys = {};
for (var i = 0; i < array.length; ++i) {
keys[array[i].toLowerCase()] = true;
}
return keys;
}
var documentTypes_ = [
"domain", "regexp", "url", "url-prefix"
], documentTypes = keySet(documentTypes_);
var mediaTypes_ = [
"all", "aural", "braille", "handheld", "print", "projection", "screen",
"tty", "tv", "embossed"
], mediaTypes = keySet(mediaTypes_);
var mediaFeatures_ = [
"width", "min-width", "max-width", "height", "min-height", "max-height",
"device-width", "min-device-width", "max-device-width", "device-height",
"min-device-height", "max-device-height", "aspect-ratio",
"min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio",
"min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color",
"max-color", "color-index", "min-color-index", "max-color-index",
"monochrome", "min-monochrome", "max-monochrome", "resolution",
"min-resolution", "max-resolution", "scan", "grid", "orientation",
"device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio",
"pointer", "any-pointer", "hover", "any-hover", "prefers-color-scheme",
"dynamic-range", "video-dynamic-range"
], mediaFeatures = keySet(mediaFeatures_);
var mediaValueKeywords_ = [
"landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover",
"interlace", "progressive",
"dark", "light",
"standard", "high"
], mediaValueKeywords = keySet(mediaValueKeywords_);
var propertyKeywords_ = [
"align-content", "align-items", "align-self", "alignment-adjust",
"alignment-baseline", "all", "anchor-point", "animation", "animation-delay",
"animation-direction", "animation-duration", "animation-fill-mode",
"animation-iteration-count", "animation-name", "animation-play-state",
"animation-timing-function", "appearance", "azimuth", "backdrop-filter",
"backface-visibility", "background", "background-attachment",
"background-blend-mode", "background-clip", "background-color",
"background-image", "background-origin", "background-position",
"background-position-x", "background-position-y", "background-repeat",
"background-size", "baseline-shift", "binding", "bleed", "block-size",
"bookmark-label", "bookmark-level", "bookmark-state", "bookmark-target",
"border", "border-bottom", "border-bottom-color", "border-bottom-left-radius",
"border-bottom-right-radius", "border-bottom-style", "border-bottom-width",
"border-collapse", "border-color", "border-image", "border-image-outset",
"border-image-repeat", "border-image-slice", "border-image-source",
"border-image-width", "border-left", "border-left-color", "border-left-style",
"border-left-width", "border-radius", "border-right", "border-right-color",
"border-right-style", "border-right-width", "border-spacing", "border-style",
"border-top", "border-top-color", "border-top-left-radius",
"border-top-right-radius", "border-top-style", "border-top-width",
"border-width", "bottom", "box-decoration-break", "box-shadow", "box-sizing",
"break-after", "break-before", "break-inside", "caption-side", "caret-color",
"clear", "clip", "color", "color-profile", "column-count", "column-fill",
"column-gap", "column-rule", "column-rule-color", "column-rule-style",
"column-rule-width", "column-span", "column-width", "columns", "contain",
"content", "counter-increment", "counter-reset", "crop", "cue", "cue-after",
"cue-before", "cursor", "direction", "display", "dominant-baseline",
"drop-initial-after-adjust", "drop-initial-after-align",
"drop-initial-before-adjust", "drop-initial-before-align", "drop-initial-size",
"drop-initial-value", "elevation", "empty-cells", "fit", "fit-content", "fit-position",
"flex", "flex-basis", "flex-direction", "flex-flow", "flex-grow",
"flex-shrink", "flex-wrap", "float", "float-offset", "flow-from", "flow-into",
"font", "font-family", "font-feature-settings", "font-kerning",
"font-language-override", "font-optical-sizing", "font-size",
"font-size-adjust", "font-stretch", "font-style", "font-synthesis",
"font-variant", "font-variant-alternates", "font-variant-caps",
"font-variant-east-asian", "font-variant-ligatures", "font-variant-numeric",
"font-variant-position", "font-variation-settings", "font-weight", "gap",
"grid", "grid-area", "grid-auto-columns", "grid-auto-flow", "grid-auto-rows",
"grid-column", "grid-column-end", "grid-column-gap", "grid-column-start",
"grid-gap", "grid-row", "grid-row-end", "grid-row-gap", "grid-row-start",
"grid-template", "grid-template-areas", "grid-template-columns",
"grid-template-rows", "hanging-punctuation", "height", "hyphens", "icon",
"image-orientation", "image-rendering", "image-resolution", "inline-box-align",
"inset", "inset-block", "inset-block-end", "inset-block-start", "inset-inline",
"inset-inline-end", "inset-inline-start", "isolation", "justify-content",
"justify-items", "justify-self", "left", "letter-spacing", "line-break",
"line-height", "line-height-step", "line-stacking", "line-stacking-ruby",
"line-stacking-shift", "line-stacking-strategy", "list-style",
"list-style-image", "list-style-position", "list-style-type", "margin",
"margin-bottom", "margin-left", "margin-right", "margin-top", "marks",
"marquee-direction", "marquee-loop", "marquee-play-count", "marquee-speed",
"marquee-style", "mask-clip", "mask-composite", "mask-image", "mask-mode",
"mask-origin", "mask-position", "mask-repeat", "mask-size","mask-type",
"max-block-size", "max-height", "max-inline-size",
"max-width", "min-block-size", "min-height", "min-inline-size", "min-width",
"mix-blend-mode", "move-to", "nav-down", "nav-index", "nav-left", "nav-right",
"nav-up", "object-fit", "object-position", "offset", "offset-anchor",
"offset-distance", "offset-path", "offset-position", "offset-rotate",
"opacity", "order", "orphans", "outline", "outline-color", "outline-offset",
"outline-style", "outline-width", "overflow", "overflow-style",
"overflow-wrap", "overflow-x", "overflow-y", "padding", "padding-bottom",
"padding-left", "padding-right", "padding-top", "page", "page-break-after",
"page-break-before", "page-break-inside", "page-policy", "pause",
"pause-after", "pause-before", "perspective", "perspective-origin", "pitch",
"pitch-range", "place-content", "place-items", "place-self", "play-during",
"position", "presentation-level", "punctuation-trim", "quotes",
"region-break-after", "region-break-before", "region-break-inside",
"region-fragment", "rendering-intent", "resize", "rest", "rest-after",
"rest-before", "richness", "right", "rotate", "rotation", "rotation-point",
"row-gap", "ruby-align", "ruby-overhang", "ruby-position", "ruby-span",
"scale", "scroll-behavior", "scroll-margin", "scroll-margin-block",
"scroll-margin-block-end", "scroll-margin-block-start", "scroll-margin-bottom",
"scroll-margin-inline", "scroll-margin-inline-end",
"scroll-margin-inline-start", "scroll-margin-left", "scroll-margin-right",
"scroll-margin-top", "scroll-padding", "scroll-padding-block",
"scroll-padding-block-end", "scroll-padding-block-start",
"scroll-padding-bottom", "scroll-padding-inline", "scroll-padding-inline-end",
"scroll-padding-inline-start", "scroll-padding-left", "scroll-padding-right",
"scroll-padding-top", "scroll-snap-align", "scroll-snap-type",
"shape-image-threshold", "shape-inside", "shape-margin", "shape-outside",
"size", "speak", "speak-as", "speak-header", "speak-numeral",
"speak-punctuation", "speech-rate", "stress", "string-set", "tab-size",
"table-layout", "target", "target-name", "target-new", "target-position",
"text-align", "text-align-last", "text-combine-upright", "text-decoration",
"text-decoration-color", "text-decoration-line", "text-decoration-skip",
"text-decoration-skip-ink", "text-decoration-style", "text-emphasis",
"text-emphasis-color", "text-emphasis-position", "text-emphasis-style",
"text-height", "text-indent", "text-justify", "text-orientation",
"text-outline", "text-overflow", "text-rendering", "text-shadow",
"text-size-adjust", "text-space-collapse", "text-transform",
"text-underline-position", "text-wrap", "top", "touch-action", "transform", "transform-origin",
"transform-style", "transition", "transition-delay", "transition-duration",
"transition-property", "transition-timing-function", "translate",
"unicode-bidi", "user-select", "vertical-align", "visibility", "voice-balance",
"voice-duration", "voice-family", "voice-pitch", "voice-range", "voice-rate",
"voice-stress", "voice-volume", "volume", "white-space", "widows", "width",
"will-change", "word-break", "word-spacing", "word-wrap", "writing-mode", "z-index",
// SVG-specific
"clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color",
"flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events",
"color-interpolation", "color-interpolation-filters",
"color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering",
"marker", "marker-end", "marker-mid", "marker-start", "paint-order", "shape-rendering", "stroke",
"stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin",
"stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering",
"baseline-shift", "dominant-baseline", "glyph-orientation-horizontal",
"glyph-orientation-vertical", "text-anchor", "writing-mode",
], propertyKeywords = keySet(propertyKeywords_);
var nonStandardPropertyKeywords_ = [
"accent-color", "aspect-ratio", "border-block", "border-block-color", "border-block-end",
"border-block-end-color", "border-block-end-style", "border-block-end-width",
"border-block-start", "border-block-start-color", "border-block-start-style",
"border-block-start-width", "border-block-style", "border-block-width",
"border-inline", "border-inline-color", "border-inline-end",
"border-inline-end-color", "border-inline-end-style",
"border-inline-end-width", "border-inline-start", "border-inline-start-color",
"border-inline-start-style", "border-inline-start-width",
"border-inline-style", "border-inline-width", "content-visibility", "margin-block",
"margin-block-end", "margin-block-start", "margin-inline", "margin-inline-end",
"margin-inline-start", "overflow-anchor", "overscroll-behavior", "padding-block", "padding-block-end",
"padding-block-start", "padding-inline", "padding-inline-end",
"padding-inline-start", "scroll-snap-stop", "scrollbar-3d-light-color",
"scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color",
"scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color",
"scrollbar-track-color", "searchfield-cancel-button", "searchfield-decoration",
"searchfield-results-button", "searchfield-results-decoration", "shape-inside", "zoom"
], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_);
var fontProperties_ = [
"font-display", "font-family", "src", "unicode-range", "font-variant",
"font-feature-settings", "font-stretch", "font-weight", "font-style"
], fontProperties = keySet(fontProperties_);
var counterDescriptors_ = [
"additive-symbols", "fallback", "negative", "pad", "prefix", "range",
"speak-as", "suffix", "symbols", "system"
], counterDescriptors = keySet(counterDescriptors_);
var colorKeywords_ = [
"aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
"bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
"burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue",
"cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod",
"darkgray", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen",
"darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
"darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet",
"deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick",
"floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite",
"gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew",
"hotpink", "indianred", "indigo", "ivory", "khaki", "lavender",
"lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral",
"lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey", "lightpink",
"lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey",
"lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta",
"maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple",
"mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise",
"mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin",
"navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered",
"orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
"papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue",
"purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown",
"salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue",
"slateblue", "slategray", "slategrey", "snow", "springgreen", "steelblue", "tan",
"teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white",
"whitesmoke", "yellow", "yellowgreen"
], colorKeywords = keySet(colorKeywords_);
var valueKeywords_ = [
"above", "absolute", "activeborder", "additive", "activecaption", "afar",
"after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate",
"always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
"arabic-indic", "armenian", "asterisks", "attr", "auto", "auto-flow", "avoid", "avoid-column", "avoid-page",
"avoid-region", "axis-pan", "background", "backwards", "baseline", "below", "bidi-override", "binary",
"bengali", "blink", "block", "block-axis", "blur", "bold", "bolder", "border", "border-box",
"both", "bottom", "break", "break-all", "break-word", "brightness", "bullets", "button",
"buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian",
"capitalize", "caps-lock-indicator", "caption", "captiontext", "caret",
"cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch",
"cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
"col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse",
"compact", "condensed", "conic-gradient", "contain", "content", "contents",
"content-box", "context-menu", "continuous", "contrast", "copy", "counter", "counters", "cover", "crop",
"cross", "crosshair", "cubic-bezier", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal",
"decimal-leading-zero", "default", "default-button", "dense", "destination-atop",
"destination-in", "destination-out", "destination-over", "devanagari", "difference",
"disc", "discard", "disclosure-closed", "disclosure-open", "document",
"dot-dash", "dot-dot-dash",
"dotted", "double", "down", "drop-shadow", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out",
"element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede",
"ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er",
"ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er",
"ethiopic-halehame-aa-et", "ethiopic-halehame-am-et",
"ethiopic-halehame-gez", "ethiopic-halehame-om-et",
"ethiopic-halehame-sid-et", "ethiopic-halehame-so-et",
"ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig",
"ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed",
"extra-expanded", "fantasy", "fast", "fill", "fill-box", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes",
"forwards", "from", "geometricPrecision", "georgian", "grayscale", "graytext", "grid", "groove",
"gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew",
"help", "hidden", "hide", "higher", "highlight", "highlighttext",
"hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "hue-rotate", "icon", "ignore",
"inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite",
"infobackground", "infotext", "inherit", "initial", "inline", "inline-axis",
"inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert",
"italic", "japanese-formal", "japanese-informal", "justify", "kannada",
"katakana", "katakana-iroha", "keep-all", "khmer",
"korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal",
"landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten",
"line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem",
"local", "logical", "loud", "lower", "lower-alpha", "lower-armenian",
"lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian",
"lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "manipulation", "match", "matrix", "matrix3d",
"media-play-button", "media-slider", "media-sliderthumb",
"media-volume-slider", "media-volume-sliderthumb", "medium",
"menu", "menulist", "menulist-button",
"menutext", "message-box", "middle", "min-intrinsic",
"mix", "mongolian", "monospace", "move", "multiple", "multiple_mask_images", "multiply", "myanmar", "n-resize",
"narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
"no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
"ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "opacity", "open-quote",
"optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
"outside", "outside-shape", "overlay", "overline", "padding", "padding-box",
"painted", "page", "paused", "persian", "perspective", "pinch-zoom", "plus-darker", "plus-lighter",
"pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d",
"progress", "push-button", "radial-gradient", "radio", "read-only",
"read-write", "read-write-plaintext-only", "rectangle", "region",
"relative", "repeat", "repeating-linear-gradient", "repeating-radial-gradient",
"repeating-conic-gradient", "repeat-x", "repeat-y", "reset", "reverse",
"rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY",
"rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running",
"s-resize", "sans-serif", "saturate", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen",
"scroll", "scrollbar", "scroll-position", "se-resize", "searchfield",
"searchfield-cancel-button", "searchfield-decoration",
"searchfield-results-button", "searchfield-results-decoration", "self-start", "self-end",
"semi-condensed", "semi-expanded", "separate", "sepia", "serif", "show", "sidama",
"simp-chinese-formal", "simp-chinese-informal", "single",
"skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal",
"slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow",
"small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali",
"source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "space-evenly", "spell-out", "square",
"square-button", "start", "static", "status-bar", "stretch", "stroke", "stroke-box", "sub",
"subpixel-antialiased", "svg_masks", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table",
"table-caption", "table-cell", "table-column", "table-column-group",
"table-footer-group", "table-header-group", "table-row", "table-row-group",
"tamil",
"telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai",
"thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
"threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
"tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
"trad-chinese-formal", "trad-chinese-informal", "transform",
"translate", "translate3d", "translateX", "translateY", "translateZ",
"transparent", "ultra-condensed", "ultra-expanded", "underline", "unidirectional-pan", "unset", "up",
"upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
"upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
"var", "vertical", "vertical-text", "view-box", "visible", "visibleFill", "visiblePainted",
"visibleStroke", "visual", "w-resize", "wait", "wave", "wider",
"window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor",
"xx-large", "xx-small"
], valueKeywords = keySet(valueKeywords_);
var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_)
.concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_)
.concat(valueKeywords_);
CodeMirror.registerHelper("hintWords", "css", allWords);
function tokenCComment(stream, state) {
var maybeEnd = false, ch;
while ((ch = stream.next()) != null) {
if (maybeEnd && ch == "/") {
state.tokenize = null;
break;
}
maybeEnd = (ch == "*");
}
return ["comment", "comment"];
}
CodeMirror.defineMIME("text/css", {
documentTypes: documentTypes,
mediaTypes: mediaTypes,
mediaFeatures: mediaFeatures,
mediaValueKeywords: mediaValueKeywords,
propertyKeywords: propertyKeywords,
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
fontProperties: fontProperties,
counterDescriptors: counterDescriptors,
colorKeywords: colorKeywords,
valueKeywords: valueKeywords,
tokenHooks: {
"/": function(stream, state) {
if (!stream.eat("*")) return false;
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
}
},
name: "css"
});
CodeMirror.defineMIME("text/x-scss", {
mediaTypes: mediaTypes,
mediaFeatures: mediaFeatures,
mediaValueKeywords: mediaValueKeywords,
propertyKeywords: propertyKeywords,
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
colorKeywords: colorKeywords,
valueKeywords: valueKeywords,
fontProperties: fontProperties,
allowNested: true,
lineComment: "//",
tokenHooks: {
"/": function(stream, state) {
if (stream.eat("/")) {
stream.skipToEnd();
return ["comment", "comment"];
} else if (stream.eat("*")) {
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
} else {
return ["operator", "operator"];
}
},
":": function(stream) {
if (stream.match(/^\s*\{/, false))
return [null, null]
return false;
},
"$": function(stream) {
stream.match(/^[\w-]+/);
if (stream.match(/^\s*:/, false))
return ["variable-2", "variable-definition"];
return ["variable-2", "variable"];
},
"#": function(stream) {
if (!stream.eat("{")) return false;
return [null, "interpolation"];
}
},
name: "css",
helperType: "scss"
});
CodeMirror.defineMIME("text/x-less", {
mediaTypes: mediaTypes,
mediaFeatures: mediaFeatures,
mediaValueKeywords: mediaValueKeywords,
propertyKeywords: propertyKeywords,
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
colorKeywords: colorKeywords,
valueKeywords: valueKeywords,
fontProperties: fontProperties,
allowNested: true,
lineComment: "//",
tokenHooks: {
"/": function(stream, state) {
if (stream.eat("/")) {
stream.skipToEnd();
return ["comment", "comment"];
} else if (stream.eat("*")) {
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
} else {
return ["operator", "operator"];
}
},
"@": function(stream) {
if (stream.eat("{")) return [null, "interpolation"];
if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/i, false)) return false;
stream.eatWhile(/[\w\\\-]/);
if (stream.match(/^\s*:/, false))
return ["variable-2", "variable-definition"];
return ["variable-2", "variable"];
},
"&": function() {
return ["atom", "atom"];
}
},
name: "css",
helperType: "less"
});
CodeMirror.defineMIME("text/x-gss", {
documentTypes: documentTypes,
mediaTypes: mediaTypes,
mediaFeatures: mediaFeatures,
propertyKeywords: propertyKeywords,
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
fontProperties: fontProperties,
counterDescriptors: counterDescriptors,
colorKeywords: colorKeywords,
valueKeywords: valueKeywords,
supportsAtComponent: true,
tokenHooks: {
"/": function(stream, state) {
if (!stream.eat("*")) return false;
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
}
},
name: "css",
helperType: "gss"
});
});

View File

@@ -0,0 +1,41 @@
/*
Name: 3024 day
Author: Jan T. Sott (http://github.com/idleberg)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
.cm-s-3024-day.CodeMirror { background: #f7f7f7; color: #3a3432; }
.cm-s-3024-day div.CodeMirror-selected { background: #d6d5d4; }
.cm-s-3024-day .CodeMirror-line::selection, .cm-s-3024-day .CodeMirror-line > span::selection, .cm-s-3024-day .CodeMirror-line > span > span::selection { background: #d6d5d4; }
.cm-s-3024-day .CodeMirror-line::-moz-selection, .cm-s-3024-day .CodeMirror-line > span::-moz-selection, .cm-s-3024-day .CodeMirror-line > span > span::selection { background: #d9d9d9; }
.cm-s-3024-day .CodeMirror-gutters { background: #f7f7f7; border-right: 0px; }
.cm-s-3024-day .CodeMirror-guttermarker { color: #db2d20; }
.cm-s-3024-day .CodeMirror-guttermarker-subtle { color: #807d7c; }
.cm-s-3024-day .CodeMirror-linenumber { color: #807d7c; }
.cm-s-3024-day .CodeMirror-cursor { border-left: 1px solid #5c5855; }
.cm-s-3024-day span.cm-comment { color: #cdab53; }
.cm-s-3024-day span.cm-atom { color: #a16a94; }
.cm-s-3024-day span.cm-number { color: #a16a94; }
.cm-s-3024-day span.cm-property, .cm-s-3024-day span.cm-attribute { color: #01a252; }
.cm-s-3024-day span.cm-keyword { color: #db2d20; }
.cm-s-3024-day span.cm-string { color: #fded02; }
.cm-s-3024-day span.cm-variable { color: #01a252; }
.cm-s-3024-day span.cm-variable-2 { color: #01a0e4; }
.cm-s-3024-day span.cm-def { color: #e8bbd0; }
.cm-s-3024-day span.cm-bracket { color: #3a3432; }
.cm-s-3024-day span.cm-tag { color: #db2d20; }
.cm-s-3024-day span.cm-link { color: #a16a94; }
.cm-s-3024-day span.cm-error { background: #db2d20; color: #5c5855; }
.cm-s-3024-day .CodeMirror-activeline-background { background: #e8f2ff; }
.cm-s-3024-day .CodeMirror-matchingbracket { text-decoration: underline; color: #a16a94 !important; }

View File

@@ -0,0 +1,39 @@
/*
Name: 3024 night
Author: Jan T. Sott (http://github.com/idleberg)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
.cm-s-3024-night.CodeMirror { background: #090300; color: #d6d5d4; }
.cm-s-3024-night div.CodeMirror-selected { background: #3a3432; }
.cm-s-3024-night .CodeMirror-line::selection, .cm-s-3024-night .CodeMirror-line > span::selection, .cm-s-3024-night .CodeMirror-line > span > span::selection { background: rgba(58, 52, 50, .99); }
.cm-s-3024-night .CodeMirror-line::-moz-selection, .cm-s-3024-night .CodeMirror-line > span::-moz-selection, .cm-s-3024-night .CodeMirror-line > span > span::-moz-selection { background: rgba(58, 52, 50, .99); }
.cm-s-3024-night .CodeMirror-gutters { background: #090300; border-right: 0px; }
.cm-s-3024-night .CodeMirror-guttermarker { color: #db2d20; }
.cm-s-3024-night .CodeMirror-guttermarker-subtle { color: #5c5855; }
.cm-s-3024-night .CodeMirror-linenumber { color: #5c5855; }
.cm-s-3024-night .CodeMirror-cursor { border-left: 1px solid #807d7c; }
.cm-s-3024-night span.cm-comment { color: #cdab53; }
.cm-s-3024-night span.cm-atom { color: #a16a94; }
.cm-s-3024-night span.cm-number { color: #a16a94; }
.cm-s-3024-night span.cm-property, .cm-s-3024-night span.cm-attribute { color: #01a252; }
.cm-s-3024-night span.cm-keyword { color: #db2d20; }
.cm-s-3024-night span.cm-string { color: #fded02; }
.cm-s-3024-night span.cm-variable { color: #01a252; }
.cm-s-3024-night span.cm-variable-2 { color: #01a0e4; }
.cm-s-3024-night span.cm-def { color: #e8bbd0; }
.cm-s-3024-night span.cm-bracket { color: #d6d5d4; }
.cm-s-3024-night span.cm-tag { color: #db2d20; }
.cm-s-3024-night span.cm-link { color: #a16a94; }
.cm-s-3024-night span.cm-error { background: #db2d20; color: #807d7c; }
.cm-s-3024-night .CodeMirror-activeline-background { background: #2F2F2F; }
.cm-s-3024-night .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }

View File

@@ -0,0 +1,268 @@
/*
* abbott.css
* A warm, dark theme for prose and code, with pastels and pretty greens.
*
* Ported from abbott.vim (https://github.com/bcat/abbott.vim) version 2.1.
* Original design and CodeMirror port by Jonathan Rascher.
*
* This theme shares the following color palette with the Vim color scheme.
*
* Brown shades:
* bistre: #231c14
* chocolate: #3c3022
* cocoa: #745d42
* vanilla_cream: #fef3b4
*
* Red shades:
* crimson: #d80450
* cinnabar: #f63f05
*
* Green shades:
* dark_olive: #273900
* forest_green: #24a507
* chartreuse: #a0ea00
* pastel_chartreuse: #d8ff84
*
* Yellow shades:
* marigold: #fbb32f
* lemon_meringue: #fbec5d
*
* Blue shades:
* cornflower_blue: #3f91f1
* periwinkle_blue: #8ccdf0
*
* Magenta shades:
* french_pink: #ec6c99
* lavender: #e6a2f3
*
* Cyan shades:
* zomp: #39a78d
* seafoam_green: #00ff7f
*/
/* Style the UI: */
/* Equivalent to Vim's Normal group. */
.cm-s-abbott.CodeMirror {
background: #231c14 /* bistre */;
color: #d8ff84 /* pastel_chartreuse */;
}
/* Roughly equivalent to Vim's LineNr group. */
.cm-s-abbott .CodeMirror-gutters {
background: #231c14 /* bistre */;
border: none;
}
.cm-s-abbott .CodeMirror-linenumber { color: #fbec5d /* lemon_meringue */; }
.cm-s-abbott .CodeMirror-guttermarker { color: #f63f05 /* cinnabar */; }
/* Roughly equivalent to Vim's FoldColumn group. */
.cm-s-abbott .CodeMirror-guttermarker-subtle { color: #fbb32f /* marigold */; }
/*
* Roughly equivalent to Vim's CursorColumn group. (We use a brighter color
* since Vim's cursorcolumn option highlights a whole column, whereas
* CodeMirror's rule just highlights a thin line.)
*/
.cm-s-abbott .CodeMirror-ruler { border-color: #745d42 /* cocoa */; }
/* Equivalent to Vim's Cursor group in insert mode. */
.cm-s-abbott .CodeMirror-cursor { border-color: #a0ea00 /* chartreuse */; }
/* Equivalent to Vim's Cursor group in normal mode. */
.cm-s-abbott.cm-fat-cursor .CodeMirror-cursor,
.cm-s-abbott .cm-animate-fat-cursor {
/*
* CodeMirror doesn't allow changing the foreground color of the character
* under the cursor, so we can't use a reverse video effect for the cursor.
* Instead, make it semitransparent.
*/
background: rgba(160, 234, 0, 0.5) /* chartreuse */;
}
.cm-s-abbott.cm-fat-cursor .CodeMirror-cursors {
/*
* Boost the z-index so the fat cursor shows up on top of text and
* matchingbracket/matchingtag highlights.
*/
z-index: 3;
}
/* Equivalent to Vim's Cursor group in replace mode. */
.cm-s-abbott .CodeMirror-overwrite .CodeMirror-cursor {
border-bottom: 1px solid #a0ea00 /* chartreuse */;
border-left: none;
width: auto;
}
/* Roughly equivalent to Vim's CursorIM group. */
.cm-s-abbott .CodeMirror-secondarycursor {
border-color: #00ff7f /* seafoam_green */;
}
/* Roughly equivalent to Vim's Visual group. */
.cm-s-abbott .CodeMirror-selected,
.cm-s-abbott.CodeMirror-focused .CodeMirror-selected {
background: #273900 /* dark_olive */;
}
.cm-s-abbott .CodeMirror-line::selection,
.cm-s-abbott .CodeMirror-line > span::selection,
.cm-s-abbott .CodeMirror-line > span > span::selection {
background: #273900 /* dark_olive */;
}
.cm-s-abbott .CodeMirror-line::-moz-selection,
.cm-s-abbott .CodeMirror-line > span::-moz-selection,
.cm-s-abbott .CodeMirror-line > span > span::-moz-selection {
background: #273900 /* dark_olive */;
}
/* Roughly equivalent to Vim's SpecialKey group. */
.cm-s-abbott .cm-tab { color: #00ff7f /* seafoam_green */; }
/* Equivalent to Vim's Search group. */
.cm-s-abbott .cm-searching {
background: #fef3b4 /* vanilla_cream */ !important;
color: #231c14 /* bistre */ !important;
}
/* Style syntax highlighting modes: */
/* Equivalent to Vim's Comment group. */
.cm-s-abbott span.cm-comment {
color: #fbb32f /* marigold */;
font-style: italic;
}
/* Equivalent to Vim's String group. */
.cm-s-abbott span.cm-string,
.cm-s-abbott span.cm-string-2 {
color: #e6a2f3 /* lavender */;
}
/* Equivalent to Vim's Constant group. */
.cm-s-abbott span.cm-number,
.cm-s-abbott span.cm-string.cm-url { color: #f63f05 /* cinnabar */; }
/* Roughly equivalent to Vim's SpecialKey group. */
.cm-s-abbott span.cm-invalidchar { color: #00ff7f /* seafoam_green */; }
/* Equivalent to Vim's Special group. */
.cm-s-abbott span.cm-atom { color: #fef3b4 /* vanilla_cream */; }
/* Equivalent to Vim's Delimiter group. */
.cm-s-abbott span.cm-bracket,
.cm-s-abbott span.cm-punctuation {
color: #fef3b4 /* vanilla_cream */;
}
/* Equivalent Vim's Operator group. */
.cm-s-abbott span.cm-operator { font-weight: bold; }
/* Roughly equivalent to Vim's Identifier group. */
.cm-s-abbott span.cm-def,
.cm-s-abbott span.cm-variable,
.cm-s-abbott span.cm-variable-2,
.cm-s-abbott span.cm-variable-3 {
color: #8ccdf0 /* periwinkle_blue */;
}
/* Roughly equivalent to Vim's Function group. */
.cm-s-abbott span.cm-builtin,
.cm-s-abbott span.cm-property,
.cm-s-abbott span.cm-qualifier {
color: #3f91f1 /* cornflower_blue */;
}
/* Equivalent to Vim's Type group. */
.cm-s-abbott span.cm-type { color: #24a507 /* forest_green */; }
/* Equivalent to Vim's Keyword group. */
.cm-s-abbott span.cm-keyword {
color: #d80450 /* crimson */;
font-weight: bold;
}
/* Equivalent to Vim's PreProc group. */
.cm-s-abbott span.cm-meta { color: #ec6c99 /* french_pink */; }
/* Equivalent to Vim's htmlTagName group (linked to Statement). */
.cm-s-abbott span.cm-tag {
color: #d80450 /* crimson */;
font-weight: bold;
}
/* Equivalent to Vim's htmlArg group (linked to Type). */
.cm-s-abbott span.cm-attribute { color: #24a507 /* forest_green */; }
/* Equivalent to Vim's htmlH1, markdownH1, etc. groups (linked to Title). */
.cm-s-abbott span.cm-header {
color: #d80450 /* crimson */;
font-weight: bold;
}
/* Equivalent to Vim's markdownRule group (linked to PreProc). */
.cm-s-abbott span.cm-hr { color: #ec6c99 /* french_pink */; }
/* Roughly equivalent to Vim's Underlined group. */
.cm-s-abbott span.cm-link { color: #e6a2f3 /* lavender */; }
/* Equivalent to Vim's diffRemoved group. */
.cm-s-abbott span.cm-negative {
background: #d80450 /* crimson */;
color: #231c14 /* bistre */;
}
/* Equivalent to Vim's diffAdded group. */
.cm-s-abbott span.cm-positive {
background: #a0ea00 /* chartreuse */;
color: #231c14 /* bistre */;
font-weight: bold;
}
/* Equivalent to Vim's Error group. */
.cm-s-abbott span.cm-error {
background: #d80450 /* crimson */;
color: #231c14 /* bistre */;
}
/* Style addons: */
/* Equivalent to Vim's MatchParen group. */
.cm-s-abbott span.CodeMirror-matchingbracket {
background: #745d42 /* cocoa */ !important;
color: #231c14 /* bistre */ !important;
font-weight: bold;
}
/*
* Roughly equivalent to Vim's Error group. (Vim doesn't seem to have a direct
* equivalent in its own matchparen plugin, but many syntax highlighting plugins
* mark mismatched brackets as Error.)
*/
.cm-s-abbott span.CodeMirror-nonmatchingbracket {
background: #d80450 /* crimson */ !important;
color: #231c14 /* bistre */ !important;
}
.cm-s-abbott .CodeMirror-matchingtag,
.cm-s-abbott .cm-matchhighlight {
outline: 1px solid #39a78d /* zomp */;
}
/* Equivalent to Vim's CursorLine group. */
.cm-s-abbott .CodeMirror-activeline-background,
.cm-s-abbott .CodeMirror-activeline-gutter {
background: #3c3022 /* chocolate */;
}
/* Equivalent to Vim's CursorLineNr group. */
.cm-s-abbott .CodeMirror-activeline-gutter .CodeMirror-linenumber {
color: #d8ff84 /* pastel_chartreuse */;
font-weight: bold;
}
/* Roughly equivalent to Vim's Folded group. */
.cm-s-abbott .CodeMirror-foldmarker {
color: #f63f05 /* cinnabar */;
text-shadow: none;
}

View File

@@ -0,0 +1,32 @@
.cm-s-abcdef.CodeMirror { background: #0f0f0f; color: #defdef; }
.cm-s-abcdef div.CodeMirror-selected { background: #515151; }
.cm-s-abcdef .CodeMirror-line::selection, .cm-s-abcdef .CodeMirror-line > span::selection, .cm-s-abcdef .CodeMirror-line > span > span::selection { background: rgba(56, 56, 56, 0.99); }
.cm-s-abcdef .CodeMirror-line::-moz-selection, .cm-s-abcdef .CodeMirror-line > span::-moz-selection, .cm-s-abcdef .CodeMirror-line > span > span::-moz-selection { background: rgba(56, 56, 56, 0.99); }
.cm-s-abcdef .CodeMirror-gutters { background: #555; border-right: 2px solid #314151; }
.cm-s-abcdef .CodeMirror-guttermarker { color: #222; }
.cm-s-abcdef .CodeMirror-guttermarker-subtle { color: azure; }
.cm-s-abcdef .CodeMirror-linenumber { color: #FFFFFF; }
.cm-s-abcdef .CodeMirror-cursor { border-left: 1px solid #00FF00; }
.cm-s-abcdef span.cm-keyword { color: darkgoldenrod; font-weight: bold; }
.cm-s-abcdef span.cm-atom { color: #77F; }
.cm-s-abcdef span.cm-number { color: violet; }
.cm-s-abcdef span.cm-def { color: #fffabc; }
.cm-s-abcdef span.cm-variable { color: #abcdef; }
.cm-s-abcdef span.cm-variable-2 { color: #cacbcc; }
.cm-s-abcdef span.cm-variable-3, .cm-s-abcdef span.cm-type { color: #def; }
.cm-s-abcdef span.cm-property { color: #fedcba; }
.cm-s-abcdef span.cm-operator { color: #ff0; }
.cm-s-abcdef span.cm-comment { color: #7a7b7c; font-style: italic;}
.cm-s-abcdef span.cm-string { color: #2b4; }
.cm-s-abcdef span.cm-meta { color: #C9F; }
.cm-s-abcdef span.cm-qualifier { color: #FFF700; }
.cm-s-abcdef span.cm-builtin { color: #30aabc; }
.cm-s-abcdef span.cm-bracket { color: #8a8a8a; }
.cm-s-abcdef span.cm-tag { color: #FFDD44; }
.cm-s-abcdef span.cm-attribute { color: #DDFF00; }
.cm-s-abcdef span.cm-error { color: #FF0000; }
.cm-s-abcdef span.cm-header { color: aquamarine; font-weight: bold; }
.cm-s-abcdef span.cm-link { color: blueviolet; }
.cm-s-abcdef .CodeMirror-activeline-background { background: #314151; }

View File

@@ -0,0 +1,5 @@
.cm-s-ambiance.CodeMirror {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,44 @@
/* Based on https://github.com/dempfi/ayu */
.cm-s-ayu-dark.CodeMirror { background: #0a0e14; color: #b3b1ad; }
.cm-s-ayu-dark div.CodeMirror-selected { background: #273747; }
.cm-s-ayu-dark .CodeMirror-line::selection, .cm-s-ayu-dark .CodeMirror-line > span::selection, .cm-s-ayu-dark .CodeMirror-line > span > span::selection { background: rgba(39, 55, 71, 99); }
.cm-s-ayu-dark .CodeMirror-line::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 55, 71, 99); }
.cm-s-ayu-dark .CodeMirror-gutters { background: #0a0e14; border-right: 0px; }
.cm-s-ayu-dark .CodeMirror-guttermarker { color: white; }
.cm-s-ayu-dark .CodeMirror-guttermarker-subtle { color: #3d424d; }
.cm-s-ayu-dark .CodeMirror-linenumber { color: #3d424d; }
.cm-s-ayu-dark .CodeMirror-cursor { border-left: 1px solid #e6b450; }
.cm-s-ayu-dark.cm-fat-cursor .CodeMirror-cursor { background-color: #a2a8a175 !important; }
.cm-s-ayu-dark .cm-animate-fat-cursor { background-color: #a2a8a175 !important; }
.cm-s-ayu-dark span.cm-comment { color: #626a73; }
.cm-s-ayu-dark span.cm-atom { color: #ae81ff; }
.cm-s-ayu-dark span.cm-number { color: #e6b450; }
.cm-s-ayu-dark span.cm-comment.cm-attribute { color: #ffb454; }
.cm-s-ayu-dark span.cm-comment.cm-def { color: rgba(57, 186, 230, 80); }
.cm-s-ayu-dark span.cm-comment.cm-tag { color: #39bae6; }
.cm-s-ayu-dark span.cm-comment.cm-type { color: #5998a6; }
.cm-s-ayu-dark span.cm-property, .cm-s-ayu-dark span.cm-attribute { color: #ffb454; }
.cm-s-ayu-dark span.cm-keyword { color: #ff8f40; }
.cm-s-ayu-dark span.cm-builtin { color: #e6b450; }
.cm-s-ayu-dark span.cm-string { color: #c2d94c; }
.cm-s-ayu-dark span.cm-variable { color: #b3b1ad; }
.cm-s-ayu-dark span.cm-variable-2 { color: #f07178; }
.cm-s-ayu-dark span.cm-variable-3 { color: #39bae6; }
.cm-s-ayu-dark span.cm-type { color: #ff8f40; }
.cm-s-ayu-dark span.cm-def { color: #ffee99; }
.cm-s-ayu-dark span.cm-bracket { color: #f8f8f2; }
.cm-s-ayu-dark span.cm-tag { color: rgba(57, 186, 230, 80); }
.cm-s-ayu-dark span.cm-header { color: #c2d94c; }
.cm-s-ayu-dark span.cm-link { color: #39bae6; }
.cm-s-ayu-dark span.cm-error { color: #ff3333; }
.cm-s-ayu-dark .CodeMirror-activeline-background { background: #01060e; }
.cm-s-ayu-dark .CodeMirror-matchingbracket {
text-decoration: underline;
color: white !important;
}

View File

@@ -0,0 +1,45 @@
/* Based on https://github.com/dempfi/ayu */
.cm-s-ayu-mirage.CodeMirror { background: #1f2430; color: #cbccc6; }
.cm-s-ayu-mirage div.CodeMirror-selected { background: #34455a; }
.cm-s-ayu-mirage .CodeMirror-line::selection, .cm-s-ayu-mirage .CodeMirror-line > span::selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::selection { background: #34455a; }
.cm-s-ayu-mirage .CodeMirror-line::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::-moz-selection { background: rgba(25, 30, 42, 99); }
.cm-s-ayu-mirage .CodeMirror-gutters { background: #1f2430; border-right: 0px; }
.cm-s-ayu-mirage .CodeMirror-guttermarker { color: white; }
.cm-s-ayu-mirage .CodeMirror-guttermarker-subtle { color: rgba(112, 122, 140, 66); }
.cm-s-ayu-mirage .CodeMirror-linenumber { color: rgba(61, 66, 77, 99); }
.cm-s-ayu-mirage .CodeMirror-cursor { border-left: 1px solid #ffcc66; }
.cm-s-ayu-mirage.cm-fat-cursor .CodeMirror-cursor {background-color: #a2a8a175 !important;}
.cm-s-ayu-mirage .cm-animate-fat-cursor { background-color: #a2a8a175 !important; }
.cm-s-ayu-mirage span.cm-comment { color: #5c6773; font-style:italic; }
.cm-s-ayu-mirage span.cm-atom { color: #ae81ff; }
.cm-s-ayu-mirage span.cm-number { color: #ffcc66; }
.cm-s-ayu-mirage span.cm-comment.cm-attribute { color: #ffd580; }
.cm-s-ayu-mirage span.cm-comment.cm-def { color: #d4bfff; }
.cm-s-ayu-mirage span.cm-comment.cm-tag { color: #5ccfe6; }
.cm-s-ayu-mirage span.cm-comment.cm-type { color: #5998a6; }
.cm-s-ayu-mirage span.cm-property { color: #f29e74; }
.cm-s-ayu-mirage span.cm-attribute { color: #ffd580; }
.cm-s-ayu-mirage span.cm-keyword { color: #ffa759; }
.cm-s-ayu-mirage span.cm-builtin { color: #ffcc66; }
.cm-s-ayu-mirage span.cm-string { color: #bae67e; }
.cm-s-ayu-mirage span.cm-variable { color: #cbccc6; }
.cm-s-ayu-mirage span.cm-variable-2 { color: #f28779; }
.cm-s-ayu-mirage span.cm-variable-3 { color: #5ccfe6; }
.cm-s-ayu-mirage span.cm-type { color: #ffa759; }
.cm-s-ayu-mirage span.cm-def { color: #ffd580; }
.cm-s-ayu-mirage span.cm-bracket { color: rgba(92, 207, 230, 80); }
.cm-s-ayu-mirage span.cm-tag { color: #5ccfe6; }
.cm-s-ayu-mirage span.cm-header { color: #bae67e; }
.cm-s-ayu-mirage span.cm-link { color: #5ccfe6; }
.cm-s-ayu-mirage span.cm-error { color: #ff3333; }
.cm-s-ayu-mirage .CodeMirror-activeline-background { background: #191e2a; }
.cm-s-ayu-mirage .CodeMirror-matchingbracket {
text-decoration: underline;
color: white !important;
}

View File

@@ -0,0 +1,40 @@
/*
Name: Base16 Default Dark
Author: Chris Kempson (http://chriskempson.com)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
.cm-s-base16-dark.CodeMirror { background: #151515; color: #e0e0e0; }
.cm-s-base16-dark div.CodeMirror-selected { background: #303030; }
.cm-s-base16-dark .CodeMirror-line::selection, .cm-s-base16-dark .CodeMirror-line > span::selection, .cm-s-base16-dark .CodeMirror-line > span > span::selection { background: rgba(48, 48, 48, .99); }
.cm-s-base16-dark .CodeMirror-line::-moz-selection, .cm-s-base16-dark .CodeMirror-line > span::-moz-selection, .cm-s-base16-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(48, 48, 48, .99); }
.cm-s-base16-dark .CodeMirror-gutters { background: #151515; border-right: 0px; }
.cm-s-base16-dark .CodeMirror-guttermarker { color: #ac4142; }
.cm-s-base16-dark .CodeMirror-guttermarker-subtle { color: #505050; }
.cm-s-base16-dark .CodeMirror-linenumber { color: #505050; }
.cm-s-base16-dark .CodeMirror-cursor { border-left: 1px solid #b0b0b0; }
.cm-s-base16-dark.cm-fat-cursor .CodeMirror-cursor { background-color: #8e8d8875 !important; }
.cm-s-base16-dark .cm-animate-fat-cursor { background-color: #8e8d8875 !important; }
.cm-s-base16-dark span.cm-comment { color: #8f5536; }
.cm-s-base16-dark span.cm-atom { color: #aa759f; }
.cm-s-base16-dark span.cm-number { color: #aa759f; }
.cm-s-base16-dark span.cm-property, .cm-s-base16-dark span.cm-attribute { color: #90a959; }
.cm-s-base16-dark span.cm-keyword { color: #ac4142; }
.cm-s-base16-dark span.cm-string { color: #f4bf75; }
.cm-s-base16-dark span.cm-variable { color: #90a959; }
.cm-s-base16-dark span.cm-variable-2 { color: #6a9fb5; }
.cm-s-base16-dark span.cm-def { color: #d28445; }
.cm-s-base16-dark span.cm-bracket { color: #e0e0e0; }
.cm-s-base16-dark span.cm-tag { color: #ac4142; }
.cm-s-base16-dark span.cm-link { color: #aa759f; }
.cm-s-base16-dark span.cm-error { background: #ac4142; color: #b0b0b0; }
.cm-s-base16-dark .CodeMirror-activeline-background { background: #202020; }
.cm-s-base16-dark .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }

View File

@@ -0,0 +1,38 @@
/*
Name: Base16 Default Light
Author: Chris Kempson (http://chriskempson.com)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
.cm-s-base16-light.CodeMirror { background: #f5f5f5; color: #202020; }
.cm-s-base16-light div.CodeMirror-selected { background: #e0e0e0; }
.cm-s-base16-light .CodeMirror-line::selection, .cm-s-base16-light .CodeMirror-line > span::selection, .cm-s-base16-light .CodeMirror-line > span > span::selection { background: #e0e0e0; }
.cm-s-base16-light .CodeMirror-line::-moz-selection, .cm-s-base16-light .CodeMirror-line > span::-moz-selection, .cm-s-base16-light .CodeMirror-line > span > span::-moz-selection { background: #e0e0e0; }
.cm-s-base16-light .CodeMirror-gutters { background: #f5f5f5; border-right: 0px; }
.cm-s-base16-light .CodeMirror-guttermarker { color: #ac4142; }
.cm-s-base16-light .CodeMirror-guttermarker-subtle { color: #b0b0b0; }
.cm-s-base16-light .CodeMirror-linenumber { color: #b0b0b0; }
.cm-s-base16-light .CodeMirror-cursor { border-left: 1px solid #505050; }
.cm-s-base16-light span.cm-comment { color: #8f5536; }
.cm-s-base16-light span.cm-atom { color: #aa759f; }
.cm-s-base16-light span.cm-number { color: #aa759f; }
.cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute { color: #90a959; }
.cm-s-base16-light span.cm-keyword { color: #ac4142; }
.cm-s-base16-light span.cm-string { color: #f4bf75; }
.cm-s-base16-light span.cm-variable { color: #90a959; }
.cm-s-base16-light span.cm-variable-2 { color: #6a9fb5; }
.cm-s-base16-light span.cm-def { color: #d28445; }
.cm-s-base16-light span.cm-bracket { color: #202020; }
.cm-s-base16-light span.cm-tag { color: #ac4142; }
.cm-s-base16-light span.cm-link { color: #aa759f; }
.cm-s-base16-light span.cm-error { background: #ac4142; color: #505050; }
.cm-s-base16-light .CodeMirror-activeline-background { background: #DDDCDC; }
.cm-s-base16-light .CodeMirror-matchingbracket { color: #f5f5f5 !important; background-color: #6A9FB5 !important}

View File

@@ -0,0 +1,34 @@
/*
Name: Bespin
Author: Mozilla / Jan T. Sott
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
.cm-s-bespin.CodeMirror {background: #28211c; color: #9d9b97;}
.cm-s-bespin div.CodeMirror-selected {background: #59554f !important;}
.cm-s-bespin .CodeMirror-gutters {background: #28211c; border-right: 0px;}
.cm-s-bespin .CodeMirror-linenumber {color: #666666;}
.cm-s-bespin .CodeMirror-cursor {border-left: 1px solid #797977 !important;}
.cm-s-bespin span.cm-comment {color: #937121;}
.cm-s-bespin span.cm-atom {color: #9b859d;}
.cm-s-bespin span.cm-number {color: #9b859d;}
.cm-s-bespin span.cm-property, .cm-s-bespin span.cm-attribute {color: #54be0d;}
.cm-s-bespin span.cm-keyword {color: #cf6a4c;}
.cm-s-bespin span.cm-string {color: #f9ee98;}
.cm-s-bespin span.cm-variable {color: #54be0d;}
.cm-s-bespin span.cm-variable-2 {color: #5ea6ea;}
.cm-s-bespin span.cm-def {color: #cf7d34;}
.cm-s-bespin span.cm-error {background: #cf6a4c; color: #797977;}
.cm-s-bespin span.cm-bracket {color: #9d9b97;}
.cm-s-bespin span.cm-tag {color: #cf6a4c;}
.cm-s-bespin span.cm-link {color: #9b859d;}
.cm-s-bespin .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;}
.cm-s-bespin .CodeMirror-activeline-background { background: #404040; }

View File

@@ -0,0 +1,32 @@
/* Port of TextMate's Blackboard theme */
.cm-s-blackboard.CodeMirror { background: #0C1021; color: #F8F8F8; }
.cm-s-blackboard div.CodeMirror-selected { background: #253B76; }
.cm-s-blackboard .CodeMirror-line::selection, .cm-s-blackboard .CodeMirror-line > span::selection, .cm-s-blackboard .CodeMirror-line > span > span::selection { background: rgba(37, 59, 118, .99); }
.cm-s-blackboard .CodeMirror-line::-moz-selection, .cm-s-blackboard .CodeMirror-line > span::-moz-selection, .cm-s-blackboard .CodeMirror-line > span > span::-moz-selection { background: rgba(37, 59, 118, .99); }
.cm-s-blackboard .CodeMirror-gutters { background: #0C1021; border-right: 0; }
.cm-s-blackboard .CodeMirror-guttermarker { color: #FBDE2D; }
.cm-s-blackboard .CodeMirror-guttermarker-subtle { color: #888; }
.cm-s-blackboard .CodeMirror-linenumber { color: #888; }
.cm-s-blackboard .CodeMirror-cursor { border-left: 1px solid #A7A7A7; }
.cm-s-blackboard .cm-keyword { color: #FBDE2D; }
.cm-s-blackboard .cm-atom { color: #D8FA3C; }
.cm-s-blackboard .cm-number { color: #D8FA3C; }
.cm-s-blackboard .cm-def { color: #8DA6CE; }
.cm-s-blackboard .cm-variable { color: #FF6400; }
.cm-s-blackboard .cm-operator { color: #FBDE2D; }
.cm-s-blackboard .cm-comment { color: #AEAEAE; }
.cm-s-blackboard .cm-string { color: #61CE3C; }
.cm-s-blackboard .cm-string-2 { color: #61CE3C; }
.cm-s-blackboard .cm-meta { color: #D8FA3C; }
.cm-s-blackboard .cm-builtin { color: #8DA6CE; }
.cm-s-blackboard .cm-tag { color: #8DA6CE; }
.cm-s-blackboard .cm-attribute { color: #8DA6CE; }
.cm-s-blackboard .cm-header { color: #FF6400; }
.cm-s-blackboard .cm-hr { color: #AEAEAE; }
.cm-s-blackboard .cm-link { color: #8DA6CE; }
.cm-s-blackboard .cm-error { background: #9D1E15; color: #F8F8F8; }
.cm-s-blackboard .CodeMirror-activeline-background { background: #3C3636; }
.cm-s-blackboard .CodeMirror-matchingbracket { outline:1px solid grey;color:white !important; }

View File

@@ -0,0 +1,25 @@
.cm-s-cobalt.CodeMirror { background: #002240; color: white; }
.cm-s-cobalt div.CodeMirror-selected { background: #b36539; }
.cm-s-cobalt .CodeMirror-line::selection, .cm-s-cobalt .CodeMirror-line > span::selection, .cm-s-cobalt .CodeMirror-line > span > span::selection { background: rgba(179, 101, 57, .99); }
.cm-s-cobalt .CodeMirror-line::-moz-selection, .cm-s-cobalt .CodeMirror-line > span::-moz-selection, .cm-s-cobalt .CodeMirror-line > span > span::-moz-selection { background: rgba(179, 101, 57, .99); }
.cm-s-cobalt .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; }
.cm-s-cobalt .CodeMirror-guttermarker { color: #ffee80; }
.cm-s-cobalt .CodeMirror-guttermarker-subtle { color: #d0d0d0; }
.cm-s-cobalt .CodeMirror-linenumber { color: #d0d0d0; }
.cm-s-cobalt .CodeMirror-cursor { border-left: 1px solid white; }
.cm-s-cobalt span.cm-comment { color: #08f; }
.cm-s-cobalt span.cm-atom { color: #845dc4; }
.cm-s-cobalt span.cm-number, .cm-s-cobalt span.cm-attribute { color: #ff80e1; }
.cm-s-cobalt span.cm-keyword { color: #ffee80; }
.cm-s-cobalt span.cm-string { color: #3ad900; }
.cm-s-cobalt span.cm-meta { color: #ff9d00; }
.cm-s-cobalt span.cm-variable-2, .cm-s-cobalt span.cm-tag { color: #9effff; }
.cm-s-cobalt span.cm-variable-3, .cm-s-cobalt span.cm-def, .cm-s-cobalt .cm-type { color: white; }
.cm-s-cobalt span.cm-bracket { color: #d8d8d8; }
.cm-s-cobalt span.cm-builtin, .cm-s-cobalt span.cm-special { color: #ff9e59; }
.cm-s-cobalt span.cm-link { color: #845dc4; }
.cm-s-cobalt span.cm-error { color: #9d1e15; }
.cm-s-cobalt .CodeMirror-activeline-background { background: #002D57; }
.cm-s-cobalt .CodeMirror-matchingbracket { outline:1px solid grey;color:white !important; }

View File

@@ -0,0 +1,33 @@
.cm-s-colorforth.CodeMirror { background: #000000; color: #f8f8f8; }
.cm-s-colorforth .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; }
.cm-s-colorforth .CodeMirror-guttermarker { color: #FFBD40; }
.cm-s-colorforth .CodeMirror-guttermarker-subtle { color: #78846f; }
.cm-s-colorforth .CodeMirror-linenumber { color: #bababa; }
.cm-s-colorforth .CodeMirror-cursor { border-left: 1px solid white; }
.cm-s-colorforth span.cm-comment { color: #ededed; }
.cm-s-colorforth span.cm-def { color: #ff1c1c; font-weight:bold; }
.cm-s-colorforth span.cm-keyword { color: #ffd900; }
.cm-s-colorforth span.cm-builtin { color: #00d95a; }
.cm-s-colorforth span.cm-variable { color: #73ff00; }
.cm-s-colorforth span.cm-string { color: #007bff; }
.cm-s-colorforth span.cm-number { color: #00c4ff; }
.cm-s-colorforth span.cm-atom { color: #606060; }
.cm-s-colorforth span.cm-variable-2 { color: #EEE; }
.cm-s-colorforth span.cm-variable-3, .cm-s-colorforth span.cm-type { color: #DDD; }
.cm-s-colorforth span.cm-property {}
.cm-s-colorforth span.cm-operator {}
.cm-s-colorforth span.cm-meta { color: yellow; }
.cm-s-colorforth span.cm-qualifier { color: #FFF700; }
.cm-s-colorforth span.cm-bracket { color: #cc7; }
.cm-s-colorforth span.cm-tag { color: #FFBD40; }
.cm-s-colorforth span.cm-attribute { color: #FFF700; }
.cm-s-colorforth span.cm-error { color: #f00; }
.cm-s-colorforth div.CodeMirror-selected { background: #333d53; }
.cm-s-colorforth span.cm-compilation { background: rgba(255, 255, 255, 0.12); }
.cm-s-colorforth .CodeMirror-activeline-background { background: #253540; }

View File

@@ -0,0 +1,53 @@
/**
Name: IntelliJ IDEA darcula theme
From IntelliJ IDEA by JetBrains
*/
.cm-s-darcula { font-family: Consolas, Menlo, Monaco, 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace, serif;}
.cm-s-darcula.CodeMirror { background: #2B2B2B; color: #A9B7C6; }
.cm-s-darcula span.cm-meta { color: #BBB529; }
.cm-s-darcula span.cm-number { color: #6897BB; }
.cm-s-darcula span.cm-keyword { color: #CC7832; line-height: 1em; font-weight: bold; }
.cm-s-darcula span.cm-def { color: #A9B7C6; font-style: italic; }
.cm-s-darcula span.cm-variable { color: #A9B7C6; }
.cm-s-darcula span.cm-variable-2 { color: #A9B7C6; }
.cm-s-darcula span.cm-variable-3 { color: #9876AA; }
.cm-s-darcula span.cm-type { color: #AABBCC; font-weight: bold; }
.cm-s-darcula span.cm-property { color: #FFC66D; }
.cm-s-darcula span.cm-operator { color: #A9B7C6; }
.cm-s-darcula span.cm-string { color: #6A8759; }
.cm-s-darcula span.cm-string-2 { color: #6A8759; }
.cm-s-darcula span.cm-comment { color: #61A151; font-style: italic; }
.cm-s-darcula span.cm-link { color: #CC7832; }
.cm-s-darcula span.cm-atom { color: #CC7832; }
.cm-s-darcula span.cm-error { color: #BC3F3C; }
.cm-s-darcula span.cm-tag { color: #629755; font-weight: bold; font-style: italic; text-decoration: underline; }
.cm-s-darcula span.cm-attribute { color: #6897bb; }
.cm-s-darcula span.cm-qualifier { color: #6A8759; }
.cm-s-darcula span.cm-bracket { color: #A9B7C6; }
.cm-s-darcula span.cm-builtin { color: #FF9E59; }
.cm-s-darcula span.cm-special { color: #FF9E59; }
.cm-s-darcula span.cm-matchhighlight { color: #FFFFFF; background-color: rgba(50, 89, 48, .7); font-weight: normal;}
.cm-s-darcula span.cm-searching { color: #FFFFFF; background-color: rgba(61, 115, 59, .7); font-weight: normal;}
.cm-s-darcula .CodeMirror-cursor { border-left: 1px solid #A9B7C6; }
.cm-s-darcula .CodeMirror-activeline-background { background: #323232; }
.cm-s-darcula .CodeMirror-gutters { background: #313335; border-right: 1px solid #313335; }
.cm-s-darcula .CodeMirror-guttermarker { color: #FFEE80; }
.cm-s-darcula .CodeMirror-guttermarker-subtle { color: #D0D0D0; }
.cm-s-darcula .CodeMirrir-linenumber { color: #606366; }
.cm-s-darcula .CodeMirror-matchingbracket { background-color: #3B514D; color: #FFEF28 !important; font-weight: bold; }
.cm-s-darcula div.CodeMirror-selected { background: #214283; }
.CodeMirror-hints.darcula {
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
color: #9C9E9E;
background-color: #3B3E3F !important;
}
.CodeMirror-hints.darcula .CodeMirror-hint-active {
background-color: #494D4E !important;
color: #9C9E9E !important;
}

View File

@@ -0,0 +1,40 @@
/*
Name: dracula
Author: Michael Kaminsky (http://github.com/mkaminsky11)
Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)
*/
.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {
background-color: #282a36 !important;
color: #f8f8f2 !important;
border: none;
}
.cm-s-dracula .CodeMirror-gutters { color: #282a36; }
.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }
.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }
.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }
.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }
.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }
.cm-s-dracula span.cm-comment { color: #6272a4; }
.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }
.cm-s-dracula span.cm-number { color: #bd93f9; }
.cm-s-dracula span.cm-variable { color: #50fa7b; }
.cm-s-dracula span.cm-variable-2 { color: white; }
.cm-s-dracula span.cm-def { color: #50fa7b; }
.cm-s-dracula span.cm-operator { color: #ff79c6; }
.cm-s-dracula span.cm-keyword { color: #ff79c6; }
.cm-s-dracula span.cm-atom { color: #bd93f9; }
.cm-s-dracula span.cm-meta { color: #f8f8f2; }
.cm-s-dracula span.cm-tag { color: #ff79c6; }
.cm-s-dracula span.cm-attribute { color: #50fa7b; }
.cm-s-dracula span.cm-qualifier { color: #50fa7b; }
.cm-s-dracula span.cm-property { color: #66d9ef; }
.cm-s-dracula span.cm-builtin { color: #50fa7b; }
.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }
.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }
.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }

View File

@@ -0,0 +1,35 @@
/*
Name: DuoTone-Dark
Author: by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
CodeMirror template by Jan T. Sott (https://github.com/idleberg), adapted by Bram de Haan (https://github.com/atelierbram/)
*/
.cm-s-duotone-dark.CodeMirror { background: #2a2734; color: #6c6783; }
.cm-s-duotone-dark div.CodeMirror-selected { background: #545167!important; }
.cm-s-duotone-dark .CodeMirror-gutters { background: #2a2734; border-right: 0px; }
.cm-s-duotone-dark .CodeMirror-linenumber { color: #545167; }
/* begin cursor */
.cm-s-duotone-dark .CodeMirror-cursor { border-left: 1px solid #ffad5c; /* border-left: 1px solid #ffad5c80; */ border-right: .5em solid #ffad5c; /* border-right: .5em solid #ffad5c80; */ opacity: .5; }
.cm-s-duotone-dark .CodeMirror-activeline-background { background: #363342; /* background: #36334280; */ opacity: .5;}
.cm-s-duotone-dark .cm-fat-cursor .CodeMirror-cursor { background: #ffad5c; /* background: #ffad5c80; */ opacity: .5;}
/* end cursor */
.cm-s-duotone-dark span.cm-atom, .cm-s-duotone-dark span.cm-number, .cm-s-duotone-dark span.cm-keyword, .cm-s-duotone-dark span.cm-variable, .cm-s-duotone-dark span.cm-attribute, .cm-s-duotone-dark span.cm-quote, .cm-s-duotone-dark span.cm-hr, .cm-s-duotone-dark span.cm-link { color: #ffcc99; }
.cm-s-duotone-dark span.cm-property { color: #9a86fd; }
.cm-s-duotone-dark span.cm-punctuation, .cm-s-duotone-dark span.cm-unit, .cm-s-duotone-dark span.cm-negative { color: #e09142; }
.cm-s-duotone-dark span.cm-string { color: #ffb870; }
.cm-s-duotone-dark span.cm-operator { color: #ffad5c; }
.cm-s-duotone-dark span.cm-positive { color: #6a51e6; }
.cm-s-duotone-dark span.cm-variable-2, .cm-s-duotone-dark span.cm-variable-3, .cm-s-duotone-dark span.cm-type, .cm-s-duotone-dark span.cm-string-2, .cm-s-duotone-dark span.cm-url { color: #7a63ee; }
.cm-s-duotone-dark span.cm-def, .cm-s-duotone-dark span.cm-tag, .cm-s-duotone-dark span.cm-builtin, .cm-s-duotone-dark span.cm-qualifier, .cm-s-duotone-dark span.cm-header, .cm-s-duotone-dark span.cm-em { color: #eeebff; }
.cm-s-duotone-dark span.cm-bracket, .cm-s-duotone-dark span.cm-comment { color: #a7a5b2; }
/* using #f00 red for errors, don't think any of the colorscheme variables will stand out enough, ... maybe by giving it a background-color ... */
.cm-s-duotone-dark span.cm-error, .cm-s-duotone-dark span.cm-invalidchar { color: #f00; }
.cm-s-duotone-dark span.cm-header { font-weight: normal; }
.cm-s-duotone-dark .CodeMirror-matchingbracket { text-decoration: underline; color: #eeebff !important; }

Some files were not shown because too many files have changed in this diff Show More