diff --git a/Cmder/config/user_aliases.cmd b/Cmder/config/user_aliases.cmd index a41da6c..6563592 100755 --- a/Cmder/config/user_aliases.cmd +++ b/Cmder/config/user_aliases.cmd @@ -33,12 +33,10 @@ nginx=start /d d:\bin\Nginx /b d:\bin\Nginx\nginx.exe $* nmap=D:\bin\nettools\Nmap\nmap.exe $* nping=D:\bin\nettools\Nmap\nping.exe $* npp=d:\bin\Notepad++\notepad++.exe $* -pb=sh d:\bin\cygwin\usr\local\bin\pb $* putty=d:\bin\putty\putty.exe $* pwd=cd python=d:\bin\python\python.exe $* rm=rm -i $* -shn=curl -F "format=simple" -F "url=$1" "https://is.gd/create.php" sublime=d:\bin\Sublime\sublime_text.exe $* sudo=gsudo $* tc=d:\bin\totalcmd\totalcmd.exe $* @@ -48,3 +46,6 @@ tshark=d:\bin\Wireshark\tshark.exe $* unalias=alias /d $1 vi=vim $* winscp=d:\bin\WinSCP\winscp.exe $* +pb=curl -sF "files[]=@$*" "https://qu.ax/upload.php" $B jq -r '.files[].url' +shn=curl -F "format=simple" -F "url=$1" "https://is.gd/create.php" +cb=curl -s -F "reqtype=fileupload" -F "fileToUpload=@$*" "https://catbox.moe/user/api.php" diff --git a/Default.rdp b/Default.rdp index e58ba46..9c93738 100755 Binary files a/Default.rdp and b/Default.rdp differ diff --git a/Diverse/Classic Start Menu/Open-Shell-Menu Settings.xml b/Diverse/Classic Start Menu/Menu Settings.xml similarity index 94% rename from Diverse/Classic Start Menu/Open-Shell-Menu Settings.xml rename to Diverse/Classic Start Menu/Menu Settings.xml index c475aa4..78d5bbd 100755 --- a/Diverse/Classic Start Menu/Open-Shell-Menu Settings.xml +++ b/Diverse/Classic Start Menu/Menu Settings.xml @@ -1,6 +1,7 @@ - + + @@ -15,7 +16,9 @@ + + @@ -31,8 +34,7 @@ CENTER_NAME=0 SMALL_ICONS=0 LARGE_FONT=0 - ICON_FRAMES=1 - OPAQUE=0 + DISABLE_MASK=1 @@ -44,9 +46,9 @@ OPAQUE=0 + - - + diff --git a/Vivaldi/mods/CSS/custom.css b/Vivaldi/mods/CSS/custom.css index 8cc90b3..0348cc4 100755 --- a/Vivaldi/mods/CSS/custom.css +++ b/Vivaldi/mods/CSS/custom.css @@ -13,6 +13,11 @@ grid-template-columns: unset; } +/* Width of empty spacer before tabs */ +#browser #tabs-tabbar-container .toolbar-tabbar-before { + min-width: 0px; +} + /* Skinnier mainbar */ .mainbar .toolbar-mainbar.toolbar-addressbar, .mainbar .toolbar-mainbar .button-toolbar button { @@ -63,18 +68,13 @@ } /* Hide AddressField stuff */ -.UrlBar-AddressField .permission-popup, +.UrlBar-AddressField .permission-popup.is-blocking, .UrlBar-AddressField .UrlBar-UrlObfuscationWarning, .UrlBar-AddressField .ContentBlocker-Control, .UrlBar-AddressField .ToolbarButton-Button[title^="Translate"] { display: none; } -/* AddressField show allowed permission buttons */ -.UrlBar-AddressField .permission-popup.is-allowing { - display: flex; -} - /* AddressField URL scheme part */ .UrlFragment--Lowlight:first-of-type { color: var(--colorFgFadedMost); @@ -96,7 +96,7 @@ } /* Extensions toggle button */ -.toolbar-extensions .ToolbarButton-Button[name="Extensions"] .button-icon { +.toolbar-extensions .ExtensionToggleIcon .button-icon { opacity: 0.5 !important; } @@ -181,8 +181,3 @@ .overlayinfobubble { display: none; } - -/* Width of empty spacer before tabs */ -.tabs-top #tabs-tabbar-container .toolbar-tabbar-before:empty { - min-width: 0px; -} diff --git a/Vivaldi/mods/JS/tab-cycler-close.js b/Vivaldi/mods/JS/tab-cycler-close.js new file mode 100755 index 0000000..b9356f0 --- /dev/null +++ b/Vivaldi/mods/JS/tab-cycler-close.js @@ -0,0 +1,469 @@ +// ==UserScript== +// @name Mod: Use Tab cycler to close multiple tabs at once +// @description Mark multiple tabs with checkboxes as you navigate the Tab Cycler, then close them all at once. +// @version 1.0 +// @author barbudo2005 +// @match *://*/* +// ==/UserScript== + +(function() { + 'use strict'; + + let cyclerActive = false; + let markedTabIds = new Set(); + let processedTabs = new Set(); + let focusObserver = null; + + console.log('🚀 Tab Cycler Batch Close v1.0 loaded'); + + // M key - toggle checkbox on highlighted tab + window.addEventListener('keydown', function(e) { + if (!cyclerActive) return; + + if (e.code === 'Space' && e.ctrlKey) { + e.preventDefault(); + e.stopPropagation(); + + const activeLi = document.querySelector('.tabswitcher.list li.selected'); + if (!activeLi) return; + + const checkbox = activeLi.querySelector('.tab-checkbox'); + if (!checkbox) return; + + // Toggle checkbox + if (checkbox.checked) { + checkbox.checked = false; + checkbox.removeAttribute('checked'); + } else { + checkbox.checked = true; + checkbox.setAttribute('checked', ''); + } + + const tabId = parseInt(checkbox.dataset.tabId); + + if (checkbox.checked) { + markedTabIds.add(tabId); + } else { + markedTabIds.delete(tabId); + } + + return false; + } + }, true); + + // CTRL or ALT keyup - batch close + document.addEventListener('keyup', function(e) { + if (e.key === 'Control' || e.key === 'Alt') { + if (markedTabIds.size === 0) return; + executeBatchClose(); + } + }, true); + + function executeBatchClose() { + const idsToClose = Array.from(markedTabIds); + + chrome.tabs.remove(idsToClose, () => { + if (chrome.runtime.lastError) { + console.error('❌ Batch close error:', chrome.runtime.lastError); + } + markedTabIds.clear(); + }); + } + + // Polling + setInterval(function() { + const cycler = document.querySelector('.tabswitcher.list'); + + if (cycler) { + if (!cyclerActive) { + cyclerActive = true; + processedTabs.clear(); + markedTabIds.clear(); + applyCustomStyles(); + setupFocusObserver(cycler); + } + injectCheckboxes(cycler); + } else if (cyclerActive) { + cyclerActive = false; + processedTabs.clear(); + + // Batch close when cycler closes (any method) + if (markedTabIds.size > 0) { + executeBatchClose(); + } + + // Cleanup observer + if (focusObserver) { + focusObserver.disconnect(); + focusObserver = null; + } + } + }, 500); + + function setupFocusObserver(cycler) { + if (focusObserver) return; + + focusObserver = new MutationObserver(() => { + const selectedLi = cycler.querySelector('li.selected'); + if (selectedLi) { + const checkbox = selectedLi.querySelector('.tab-checkbox'); + if (checkbox && document.activeElement !== checkbox) { + checkbox.focus(); + } + } + }); + + focusObserver.observe(cycler, { + attributes: true, + attributeFilter: ['class'], + subtree: true + }); + } + + function applyCustomStyles() { + if (document.getElementById('tab-cycler-custom-styles')) return; + + const style = document.createElement('style'); + style.id = 'tab-cycler-custom-styles'; + style.textContent = ` + .tabswitcher.list li.active-page { + filter: brightness(1.4) !important; + } + + .tab-checkbox { + appearance: none; + -webkit-appearance: none; + width: 13px !important; + height: 13px !important; + min-width: 13px !important; + min-height: 13px !important; + border: 1px solid var(--colorFgFadedMost) !important; + border-radius: 2px !important; + background: transparent !important; + cursor: pointer !important; + position: absolute !important; + right: 6px !important; + top: 50% !important; + transform: translateY(-50%) !important; + z-index: 99999 !important; + transition: all 0.15s ease !important; + pointer-events: auto !important; + } + + .tab-checkbox:hover { + border-color: var(--colorFgFaded) !important; + } + + /* Override Vivaldi's checkbox ::before and ::after */ + input[type=checkbox].tab-checkbox::before, + input[type=checkbox].tab-checkbox::after { + display: none !important; + content: none !important; + } + + .tab-checkbox:checked { + border-color: var(--colorFg) !important; + background-color: var(--colorHighlightBg) !important; + } + + /* Our custom checkmark - more specific selector */ + + input[type=checkbox].tab-checkbox:checked::after { + content: '✓' !important; + display: block !important; + color: var(--colorFg) !important; + font-size: 10px !important; + font-weight: bold !important; + position: absolute !important; + top: -1px !important; + left: 1px !important; + transform: none !important; + background: none !important; + } + `; + document.head.appendChild(style); + } + + function injectCheckboxes(cycler) { + const tabItems = cycler.querySelectorAll('ul.listed-tabs > li.visual-list'); + + chrome.tabs.query({currentWindow: true}, (allTabs) => { + tabItems.forEach((li) => { + if (processedTabs.has(li)) return; + if (li.querySelector('.tab-checkbox')) { + processedTabs.add(li); + return; + } + + let matchingTab = null; + + // Get URL from favicon + const favicon = li.querySelector('.visual-tab-list-favicon'); + + if (favicon && favicon.srcset) { + const match = favicon.srcset.match(/chrome:\/\/favicon\/size\/\d+\/(.+?)\s/); + if (match) { + const tabUrl = match[1]; + matchingTab = allTabs.find(t => t.url === tabUrl); + } + } + + if (!matchingTab) { + processedTabs.add(li); + return; + } + + li.style.position = 'relative'; + li.style.display = 'flex'; + li.style.alignItems = 'center'; + li.style.gap = '8px'; + li.style.paddingRight = '25px'; + li.style.minWidth = '0'; + + const textNodes = Array.from(li.childNodes).filter(node => node.nodeType === Node.TEXT_NODE); + textNodes.forEach(textNode => { + if (textNode.textContent.trim()) { + const span = document.createElement('span'); + span.textContent = textNode.textContent; + span.style.overflow = 'hidden'; + span.style.textOverflow = 'ellipsis'; + span.style.whiteSpace = 'nowrap'; + span.style.flex = '1'; + span.style.minWidth = '0'; + span.style.cursor = 'pointer'; + + span.addEventListener('mousedown', function(e) { + chrome.tabs.update(matchingTab.id, {active: true}); + + if (markedTabIds.size > 0) { + executeBatchClose(); + } + }, true); + + li.replaceChild(span, textNode); + } + }); + + if (favicon) { + favicon.style.flexShrink = '0'; + } + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'tab-checkbox'; + checkbox.dataset.tabId = matchingTab.id; + + checkbox.addEventListener('change', function() { + const tabId = parseInt(checkbox.dataset.tabId); + + if (checkbox.checked) { + markedTabIds.add(tabId); + } else { + markedTabIds.delete(tabId); + } + }); + + // Block propagation to prevent tab activation + checkbox.addEventListener('mousedown', function(e) { + e.stopPropagation(); + e.stopImmediatePropagation(); + }, true); + + checkbox.addEventListener('mouseup', function(e) { + e.stopPropagation(); + e.stopImmediatePropagation(); + }, true); + + checkbox.addEventListener('click', function(e) { + e.stopPropagation(); + e.stopImmediatePropagation(); + }, true); + + li.appendChild(checkbox); + processedTabs.add(li); + }); + }); + } + + console.log('✅ Active'); +})(); + + +/*****************************************************************************************************************/ + +(async () => { + 'use strict'; + + const config = { + // タブスタックにベースドメインを使用する (true: 有効, false: 無効) + // Use the base domain for tab stacks (true: enabled, false: disabled) + base_domain: true, + + // タブスタックの名前を自動的に変更する (0: 無効, 1: ホスト名を使用, 2: ベースドメインから生成) + // Automatically change the name of the tab stack (0: disabled, 1: use hostname, 2: generate from base domain) + rename_stack: 0, + + // 自動タブスタックを許可するワークスペース (完全一致もしくは ) + // * 未設定の場合はすべてのワークスペースで自動タブスタックを許可する + // Workspaces that allow automatic tab stacking (exact match or ) + // * If not set, automatic tab stacking is allowed in all workspaces + allow_workspaces: [ + // "", + // "Shopping", + ], + + // 自動タブスタックを許可するドメイン (完全一致もしくは正規表現) + // * 未設定の場合はすべてのドメインで自動タブスタックを許可する + // Domains that allow automatic tab stacking (exact match or regular expression) + // * If not set, automatic tab stacking is allowed for all domains + allow_domains: [ + // "www.example.com", + // /^(.+\.)?example\.net$/, + ], + + // 自動タブスタックから除外するドメイン (完全一致もしくは正規表現) + // Domains to exclude from automatic tab stacking (exact match or regular expression) + block_domains: [ + // "www.example.com", + // /^(.+\.)?example\.net$/, + ], + }; + + const mergeArrays = (...arrays) => [...new Set(arrays.flat())]; + + const getUrlFragments = (url) => vivaldi.utilities.getUrlFragments(url); + + const getBaseDomain = (url) => { + const {hostForSecurityDisplay, tld} = getUrlFragments(url); + return hostForSecurityDisplay.match(`([^.]+\\.${ tld })$`)?.[1] || hostForSecurityDisplay; + }; + + const getHostname = (url) => { + const {hostForSecurityDisplay} = getUrlFragments(url); + return config.base_domain ? getBaseDomain(url) : hostForSecurityDisplay; + }; + + const matchHostRule = (url, rule) => { + const {hostForSecurityDisplay} = getUrlFragments(url); + return rule instanceof RegExp ? rule.test(hostForSecurityDisplay) : hostForSecurityDisplay === rule; + }; + + const getTab = async (tabId) => { + const tab = await chrome.tabs.get(tabId); + + if (tab.vivExtData) { + tab.vivExtData = JSON.parse(tab.vivExtData); + return tab; + } + }; + + const getTabIndex = async (tabId) => (await getTab(tabId)).index; + + const getWorkspaceName = async (workspaceId) => { + if (!workspaceId) { + return ''; + } + const workspaceList = await vivaldi.prefs.get('vivaldi.workspaces.list'); + return workspaceList.find(item => item.id === workspaceId).name; + }; + + const getTabsByWorkspace = async () => { + const tabs = (await chrome.tabs.query({ currentWindow: true })) + .filter(tab => tab.id !== -1 && tab.vivExtData) + .map(tab => Object.assign(tab, { vivExtData: JSON.parse(tab.vivExtData) })) + .filter(tab => !tab.pinned && !tab.vivExtData.panelId) + .filter(tab => !config.allow_domains.length || config.allow_domains.find(rule => matchHostRule(tab.url, rule))) + .filter(tab => !config.block_domains.length || !config.block_domains.find(rule => matchHostRule(tab.url, rule))); + + return Object.groupBy(tabs, tab => tab.vivExtData.workspaceId); + }; + + const getTabsByStack = (tabs) => Object.groupBy(tabs, tab => tab.vivExtData.group); + + const getTabsByHost = (tabs) => Object.groupBy(tabs, tab => getHostname(tab.url)); + + const getMaxTabsStackId = (tabsByStack, targetHost) => { + const counts = {}; + + for (const [stackId, tabs] of Object.entries(tabsByStack)) { + if (stackId !== 'undefined') { + const tabsByHost = getTabsByHost(tabs); + const count = tabsByHost[targetHost]?.length || 0; + + delete tabsByHost[targetHost]; + counts[stackId] = Object.values(tabsByHost) + .reduce((acc, tabs) => { + return acc > tabs.length ? acc : 0; + }, count); + } + } + + return Object.entries(counts) + .reduce((acc, [stackId, count]) => { + return acc[1] < count ? [stackId, count] : acc; + }, [, 0])[0]; + }; + + const getTabStackName = (url) => { + let stackName; + + switch (config.rename_stack) { + case 1: + stackName = getHostname(url); + break; + case 2: + stackName = getBaseDomain(url).split('.')[0]; + stackName = stackName.charAt(0).toUpperCase() + stackName.slice(1); + break; + } + return stackName; + }; + + const addTabStack = async (tabId, stackId, stackName) => { + const {vivExtData} = await getTab(tabId); + + if (stackName) { + vivExtData.fixedGroupTitle = stackName; + } + vivExtData.group = stackId; + chrome.tabs.update(tabId, { vivExtData: JSON.stringify(vivExtData) }); + }; + + const stackingTabs = async (workspaceId) => { + const workspaceName = await getWorkspaceName(workspaceId); + + if (!config.allow_workspaces.length || config.allow_workspaces.includes(workspaceName)) { + const tabsByWorkspace = await getTabsByWorkspace(); + const tabsByStack = getTabsByStack(tabsByWorkspace[workspaceId]); + const tabsByHost = getTabsByHost(tabsByWorkspace[workspaceId]); + + for (const [host, tabs] of Object.entries(tabsByHost)) { + const targetStackId = getMaxTabsStackId(tabsByStack, host) || crypto.randomUUID(); + const targetStackTabs = tabsByStack[targetStackId] ? getTabsByHost(tabsByStack[targetStackId])[host] : []; + const targetTabs = mergeArrays(targetStackTabs, tabs); + const targetStackName = getTabStackName(tabs[0].pendingUrl || tabs[0].url); + + let tabIndex = await getTabIndex(targetTabs[0].id); + + for (const tab of targetTabs) { + addTabStack(tab.id, targetStackId, targetStackName); + chrome.tabs.move(tab.id, { index: tabIndex }); + tabIndex++; + } + } + } + }; + + chrome.webNavigation.onCommitted.addListener(async details => { + if (details.tabId !== -1) { + const tab = await getTab(details.tabId); + + if (tab && !tab.pinned && !tab.vivExtData.panelId && details.frameType === 'outermost_frame') { + const workspaceId = tab.vivExtData.workspaceId; + stackingTabs(workspaceId); + } + } + }); +})(); + + diff --git a/Vivaldi/mods/custom.css b/Vivaldi/mods/custom.css index 408c2f9..de57d31 100755 --- a/Vivaldi/mods/custom.css +++ b/Vivaldi/mods/custom.css @@ -13,6 +13,11 @@ grid-template-columns: unset; } +/* Width of empty spacer before tabs */ +#browser #tabs-tabbar-container .toolbar-tabbar-before { + min-width: 0px; +} + /* Skinnier mainbar */ .mainbar .toolbar-mainbar.toolbar-addressbar, .mainbar .toolbar-mainbar .button-toolbar button { @@ -42,34 +47,6 @@ border-top: unset !important; } -/* SiteInfoButton */ -.SiteInfoButton { - opacity: unset !important; - height: 22px; - width: 22px; -} - -/* SiteInfo V logo */ -.SiteInfoButton.internal svg path, .SiteInfoButton.warning svg path { - d: path("M10.4 5c-.4-.8 0-1.8 1-2 .7 0 1.5.4 1.6 1.2a1.4 1.4 0 0 1-.2 1l-4 7c-.3.5-.7.8-1.2.8-.6 0-1-.2-1.3-.7L3.8 7.8 2.2 5c-.5-.8 0-2 1-2 .7 0 1 .2 1.4.7l1 2 1 1.4a2 2 0 0 0 1.7 1.5 2.2 2.2 0 0 0 2.3-2V6c0-.3 0-.6-.2-1z"); -} - -/* SiteInfo old style lock */ -.SiteInfoButton.secure svg path, .SiteInfoButton.certified svg path { - d: path("M12.2 6.7C12.2 4 10.5 2 8 2 5.8 2 3.8 4 3.8 6.7V7H3v7h10V7h-.8v-.3zM10.7 8H5.3V6.7c0-1.7 1.4-3 2.8-3 1.7 0 2.8 1.3 2.8 3V8z"); - fill: #00a100; -} - -/* SiteInfo secure background colour */ -.SiteInfoButton.secure, .SiteInfoButton.certified { - background-color: #004400 !important; -} - -/* SiteInfo hide EV/Not Secure text */ -.SiteInfoButton .siteinfo-text { - display: none; -} - /* Remove panels switch top padding */ #switch { padding-top: unset; @@ -124,7 +101,7 @@ } /* Extensions toggle button */ -.toolbar-extensions .ToolbarButton-Button[name="Extensions"] .button-icon { +.toolbar-extensions .ExtensionToggleIcon .button-icon { opacity: 0.5 !important; } @@ -210,12 +187,4 @@ display: none; } -/* Width of empty spacer before tabs */ -.tabs-top #tabs-tabbar-container .toolbar-tabbar-before:empty { - min-width: 0px; -} -/* TEMP FIX FOR TAB BAR */ -#tabs-tabbar-container.top { - padding-top: unset; -} diff --git a/Vivaldi/themes/Stians-Theme.zip b/Vivaldi/themes/Stians-Theme.zip index 76f7ec0..ad08545 100755 Binary files a/Vivaldi/themes/Stians-Theme.zip and b/Vivaldi/themes/Stians-Theme.zip differ diff --git a/Vivaldi/userscript/Override Client Hints API - Basic.user.js b/Vivaldi/userscript/Override Client Hints API - Basic.user.js new file mode 100755 index 0000000..9883d2e --- /dev/null +++ b/Vivaldi/userscript/Override Client Hints API - Basic.user.js @@ -0,0 +1,52 @@ +// ==UserScript== +// @name Override Client Hints API - Basic +// @namespace +// @version 1.0 +// @description A script to override the Client Hints JavaScript API +// @author +// @match https://*/* +// @run-at document-start +// @grant none +// @license MIT +// ==/UserScript== + +(function () { + 'use strict'; + + const ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"; + + function override(obj, prop, value) { + Object.defineProperty(obj, prop, { + get: () => value, + configurable: true + }); + } + + override(navigator, "userAgent", ua); + override(navigator, "appVersion", ua.slice("Mozilla/".length)); + override(navigator, "vendor", "Google Inc."); + override(navigator, "platform", "Win32"); + override(navigator, "hardwareConcurrency", 8); + override(navigator, "deviceMemory", 8); + + override(navigator, "userAgentData", { + brands: [ + { brand: "Vivaldi", version: "7.10" }, + { brand: "Chromium", version: "146" }, + { brand: "Not=A?Brand", version: "24" } + ], + fullVersionList: [ + { brand: "Vivaldi", version: "7.10" }, + { brand: "Chromium", version: "146.0.0.0" }, + { brand: "Not=A?Brand", version: "24.0.0.0" } + ], + uaFullVersion: "146.0.0.0", + platform: "Windows", + platformVersion: "15.0.0", + architecture: "x86", + bitness: "64", + mobile: false, + wow64: false + }); + +})(); diff --git a/Vivaldi/userscript/Override Client Hints API.user.js b/Vivaldi/userscript/Override Client Hints API.user.js new file mode 100755 index 0000000..36e8c48 --- /dev/null +++ b/Vivaldi/userscript/Override Client Hints API.user.js @@ -0,0 +1,92 @@ +// ==UserScript== +// @name Override Client Hints API +// @namespace +// @version 1.0 +// @description A script to override the Client Hints JavaScript API +// @author +// @match https://*/* +// @run-at document-start +// @grant none +// @license MIT +// ==/UserScript== + +(function() { + 'use strict'; + + // Use full version in UA string (e.g. Chrome/133.0.6943.126) to match real Chrome + const VIVALDI_VERSION = "7.10"; + const VIVALDI_FULL_VERSION = "7.10"; + const CHROME_VERSION = "146"; + const CHROME_FULL_VERSION = "146.0.7680.171"; + const chromeUA = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36`; + + function override(obj, prop, value) { + Object.defineProperty(obj, prop, { + get: () => value, + configurable: true, + enumerable: true + }); + } + + + // Basic navigator properties + override(Navigator.prototype, "userAgent", chromeUA); + override(Navigator.prototype, "appVersion", chromeUA.slice("Mozilla/".length)); + override(Navigator.prototype, "platform", "Win32"); + override(Navigator.prototype, "vendor", "Google Inc."); + override(Navigator.prototype, "oscpu", undefined); // Chrome doesn't expose this; Firefox does + override(Navigator.prototype, "productSub", "20030107"); // Chrome: "20030107", Firefox: "20100101" + + // User-Agent Client Hints + // GREASE brand string and version rotate based on Chrome major version to prevent fingerprinting + function getGreasedBrand(major) { + const n = parseInt(major, 10) % 3; + const brandStrings = ["Not A(Brand", "Not)A;Brand", "Not:A-Brand"]; + const brandVersions = ["8", "99", "24"]; + return { brand: brandStrings[n], version: brandVersions[n] }; + } + + const greaseBrand = getGreasedBrand(CHROME_VERSION); + const greaseBrandFull = { brand: greaseBrand.brand, version: `${greaseBrand.version}.0.0.0` }; + + const brands = [ + greaseBrand, + { brand: "Vivaldi", version: VIVALDI_VERSION }, + { brand: "Chromium", version: CHROME_VERSION } + ]; + const fullVersionList = [ + greaseBrandFull, + { brand: "Vivaldi", version: VIVALDI_FULL_VERSION }, + { brand: "Chromium", version: CHROME_FULL_VERSION } + ]; + + // Built once, reused on every getHighEntropyValues() call + const highEntropyMap = { + architecture: "x86", + bitness: "64", + brands, + fullVersionList, + mobile: false, + model: "", + platform: "Windows", + platformVersion: "15.0.0", + uaFullVersion: CHROME_FULL_VERSION + }; + + const uaData = { + brands, + mobile: false, + platform: "Windows", + getHighEntropyValues: async function(hints) { + const result = {}; + for (const hint of hints) { + if (hint in highEntropyMap) result[hint] = highEntropyMap[hint]; + } + return result; + }, + toJSON: function() { + return { brands, mobile: false, platform: "Windows" }; + } + }; + override(Navigator.prototype, "userAgentData", uaData); +})();