/* * Vivaldi Extension Manager * Version: 2026.07.10 * Author: sudenim & luetage * Description: Provides various functions and tools for extensions * link * * GNU General Public License v3.0 */ (() => { console.log("Vivaldi Extension Manager loaded"); 'use strict'; const NAME = 'Extension Manager'; const WEB_PANEL_ID = 'WEBPANEL_extension-manager-panel-0001'; const PANEL_URL = 'data:text/html,Extension Manager'; // ICON STYLE // -------------------------------------------------- const style = document.createElement('style'); style.textContent = ` button[data-name="${WEB_PANEL_ID}"] > img { display: none; } button[data-name="${WEB_PANEL_ID}"]:before { width: 16px; height: 16px; content: ""; display: block; background-color: currentColor; -webkit-mask-image: url("data:image/svg+xml,"); -webkit-mask-repeat: no-repeat; -webkit-mask-size: contain; -webkit-mask-position: center; } #em-refresh:before { width: 16px; height: 16px; content: ""; display: block; background-color: currentColor; -webkit-mask-image: url("data:image/svg+xml,"); -webkit-mask-repeat: no-repeat; -webkit-mask-size: contain; -webkit-mask-position: center; } #em-privacy:before { width: 16px; height: 16px; content: ""; display: block; background-color: currentColor; -webkit-mask-image: url("data:image/svg+xml,"); -webkit-mask-repeat: no-repeat; -webkit-mask-size: contain; -webkit-mask-position: center; } #em-extensions:before { width: 16px; height: 16px; content: ""; display: block; background-color: currentColor; -webkit-mask-image: url("data:image/svg+xml,"); -webkit-mask-repeat: no-repeat; -webkit-mask-size: contain; -webkit-mask-position: center; } `; document.head.appendChild(style); // STORAGE // -------------------------------------------------- function getDisabled(cb) { chrome.storage.local.get({ em_disabled: [] }, (data) => cb(data.em_disabled)); } function setDisabled(arr) { chrome.storage.local.set({ em_disabled: arr }); } function getPrivacyDisabled(cb) { chrome.storage.local.get({ em_privacy_disabled: [] }, (data) => cb(data.em_privacy_disabled)); } function setPrivacyDisabled(arr) { chrome.storage.local.set({ em_privacy_disabled: arr }); } function getPrivacyMode(cb) { chrome.storage.local.get({ em_privacy_mode: false }, (data) => cb(data.em_privacy_mode)); } function setPrivacyMode(val) { chrome.storage.local.set({ em_privacy_mode: val }); } function getCustomNames(cb) { chrome.storage.local.get({ em_custom_names: {} }, (data) => cb(data.em_custom_names)); } function setCustomNames(obj) { chrome.storage.local.set({ em_custom_names: obj }); } // PANEL CONTENT // -------------------------------------------------- const panelContent = document.createElement('div'); panelContent.className = 'em-content'; panelContent.innerHTML = `
`; const switcher = panelContent.querySelector('#em-switcher'); let privacyMode = false; const toolbar = document.createElement('div'); toolbar.id = 'em-toolbar'; toolbar.className = 'toolbar-default'; toolbar.innerHTML = `
`; const searchInput = toolbar.querySelector('#em-search'); const refreshBtn = toolbar.querySelector('#em-refresh'); const extensionsBtn = toolbar.querySelector('#em-extensions'); const privacyBtn = toolbar.querySelector('#em-privacy'); extensionsBtn.addEventListener('click', () => { chrome.tabs.create({ url: 'chrome://extensions/' }); }); // PRIVACY MODE TOGGLE // -------------------------------------------------- function applyPrivacyMode(active) { privacyMode = active; privacyBtn.classList.toggle('em-privacy-active', active); privacyBtn.title = active ? 'Privacy mode (on)' : 'Privacy mode'; setPrivacyMode(active); } function switchMode(toPrivacy) { chrome.management.getAll(function (info) { const extensions = info.filter(ext => ext.type === 'extension'); const currentlyDisabled = extensions.filter(e => !e.enabled).map(e => e.id); if (toPrivacy) { setDisabled(currentlyDisabled); getPrivacyDisabled(function (privacyDisabled) { applyPrivacyMode(true); const allIds = extensions.map(e => e.id); const isFirstRun = privacyDisabled.length === 0; const listToApply = isFirstRun ? allIds : privacyDisabled; if (isFirstRun) setPrivacyDisabled(allIds); applyDisabledList(extensions, listToApply); }); } else { setPrivacyDisabled(currentlyDisabled); getDisabled(function (normalDisabled) { applyPrivacyMode(false); applyDisabledList(extensions, normalDisabled); }); } }); } function applyDisabledList(extensions, disabledIds) { let pending = extensions.length; if (pending === 0) { load(); return; } extensions.forEach(ext => { const shouldBeDisabled = disabledIds.includes(ext.id); if (ext.enabled === shouldBeDisabled) { chrome.management.setEnabled(ext.id, !shouldBeDisabled, () => { if (--pending === 0) load(); }); } else { if (--pending === 0) load(); } }); } privacyBtn.addEventListener('click', () => { switchMode(!privacyMode); }); // MENU HELPERS // -------------------------------------------------- function rmMenu() { const menu = panelContent.querySelector('#em-menu'); if (!menu) return; menu.addEventListener('animationend', () => menu.remove(), { once: true }); menu.classList.add('em-menu-closing'); } // SEARCH & REFRESH // -------------------------------------------------- function applyFilter() { const query = searchInput.value.trim().toLowerCase(); switcher.querySelectorAll('.em-extension').forEach(item => { const name = item.querySelector('div')?.textContent.trim().toLowerCase() ?? ''; item.style.display = name.includes(query) ? '' : 'none'; }); const menu = panelContent.querySelector('#em-menu'); if (menu) { const prev = menu.previousElementSibling; if (prev && prev.style.display === 'none') rmMenu(); } } searchInput.addEventListener('input', applyFilter); refreshBtn.addEventListener('click', () => { searchInput.value = ''; load(); }); // LOAD EXTENSIONS // -------------------------------------------------- let loading = false; function load() { if (loading) return; loading = true; switcher.innerHTML = ''; chrome.management.getAll(function (info) { if (chrome.runtime.lastError) { console.error('[EM] getAll error:', chrome.runtime.lastError.message); loading = false; return; } info.sort((a, b) => a.shortName.trim().localeCompare(b.shortName.trim())); const getDisabledFn = privacyMode ? getPrivacyDisabled : getDisabled; const setDisabledFn = privacyMode ? setPrivacyDisabled : setDisabled; getDisabledFn(function (disabled) { getCustomNames(function (customNames) { const extensions = info.filter(ext => ext.type === 'extension'); const cleanedDisabled = disabled.filter(id => extensions.some(e => e.id === id) ); if (cleanedDisabled.length !== disabled.length) { setDisabledFn(cleanedDisabled); disabled = cleanedDisabled; } for (const ext of extensions) { if (disabled.includes(ext.id) && ext.enabled) { chrome.management.setEnabled(ext.id, false, () => { if (chrome.runtime.lastError) { console.error('[EM] setEnabled error:', chrome.runtime.lastError.message); } }); } } extensions.forEach((ext, i) => { const isEnabled = !disabled.includes(ext.id) && ext.enabled; const displayName = customNames[ext.id] || ext.shortName; const hasCustomName = !!customNames[ext.id]; const item = document.createElement('div'); item.className = 'em-extension' + (isEnabled ? ' em-enabled' : ''); if (i % 2 === 0) item.classList.add('em-odd'); item.dataset.id = ext.id; item.dataset.originalName = ext.shortName; item.setAttribute('role', 'switch'); item.setAttribute('aria-checked', String(isEnabled)); item.setAttribute('aria-label', displayName); item.tabIndex = 0; const nameDiv = document.createElement('div'); nameDiv.textContent = displayName; item.appendChild(nameDiv); const resetBtn = document.createElement('button'); resetBtn.className = 'em-reset'; resetBtn.title = 'Reset to original name'; resetBtn.style.display = hasCustomName ? '' : 'none'; resetBtn.innerHTML = ``; item.appendChild(resetBtn); const renameBtn = document.createElement('button'); renameBtn.className = 'em-rename'; renameBtn.title = 'Rename'; renameBtn.innerHTML = ``; item.appendChild(renameBtn); switcher.appendChild(item); }); attachListeners(); applyFilter(); loading = false; }); }); }); } // INTERACTION // -------------------------------------------------- function attachListeners() { switcher.querySelectorAll('.em-extension').forEach(function (item) { const resetBtn = item.querySelector('.em-reset'); const renameBtn = item.querySelector('.em-rename'); let renaming = false; function handleToggle() { if (renaming) return; rmMenu(); if (item.dataset.busy) return; const id = item.dataset.id; const enabling = !item.classList.contains('em-enabled'); item.dataset.busy = 'true'; item.style.opacity = '0.5'; item.style.pointerEvents = 'none'; chrome.management.setEnabled(id, enabling, function () { delete item.dataset.busy; item.style.opacity = ''; item.style.pointerEvents = ''; if (chrome.runtime.lastError) { console.error('[EM] setEnabled error:', chrome.runtime.lastError.message); return; } item.classList.toggle('em-enabled', enabling); item.setAttribute('aria-checked', String(enabling)); const getDisabledFn = privacyMode ? getPrivacyDisabled : getDisabled; const setDisabledFn = privacyMode ? setPrivacyDisabled : setDisabled; getDisabledFn(function (disabled) { setDisabledFn( enabling ? disabled.filter(d => d !== id) : disabled.includes(id) ? disabled : [...disabled, id] ); }); }); } item.addEventListener('click', function (e) { if (renaming) return; if (e.target.closest('.em-reset, .em-rename')) return; handleToggle(); }); item.addEventListener('keydown', function (e) { if (renaming) return; if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); handleToggle(); } }); // RESET BUTTON resetBtn.addEventListener('click', function (e) { e.stopPropagation(); if (renaming) return; const id = item.dataset.id; const originalName = item.dataset.originalName; getCustomNames(function (customNames) { delete customNames[id]; setCustomNames(customNames); const nameDiv = item.querySelector('div'); nameDiv.textContent = originalName; item.setAttribute('aria-label', originalName); resetBtn.style.display = 'none'; }); }); // RENAME BUTTON renameBtn.addEventListener('click', function (e) { e.stopPropagation(); if (renaming) return; renaming = true; const id = item.dataset.id; const originalName = item.dataset.originalName; const nameDiv = item.querySelector('div'); const currentName = nameDiv.textContent; const wrapper = document.createElement('div'); wrapper.className = 'em-rename-wrapper'; const input = document.createElement('input'); input.type = 'text'; input.value = currentName; input.className = 'em-rename-input'; wrapper.appendChild(input); nameDiv.replaceWith(wrapper); renameBtn.style.display = 'none'; resetBtn.style.display = 'none'; input.focus(); input.select(); function confirmRename() { if (!renaming) return; renaming = false; const newName = input.value.trim() || originalName; getCustomNames(function (customNames) { if (newName === originalName) { delete customNames[id]; } else { customNames[id] = newName; } setCustomNames(customNames); const newDiv = document.createElement('div'); newDiv.textContent = newName; wrapper.replaceWith(newDiv); item.setAttribute('aria-label', newName); renameBtn.style.display = ''; resetBtn.style.display = newName !== originalName ? '' : 'none'; }); } function cancelRename() { if (!renaming) return; renaming = false; const newDiv = document.createElement('div'); newDiv.textContent = currentName; wrapper.replaceWith(newDiv); renameBtn.style.display = ''; resetBtn.style.display = currentName !== originalName ? '' : 'none'; } input.addEventListener('keydown', function (e) { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); confirmRename(); } if (e.key === 'Escape') { e.preventDefault(); cancelRename(); } }); input.addEventListener('click', function (e) { e.stopPropagation(); }); }); // CONTEXT MENU item.addEventListener('contextmenu', function (e) { e.preventDefault(); if (renaming) return; const existing = panelContent.querySelector('#em-menu'); if (existing && existing.previousElementSibling === item) { rmMenu(); return; } rmMenu(); const id = item.dataset.id; const menu = document.createElement('div'); menu.id = 'em-menu'; chrome.management.get(id, function (ext) { if (chrome.runtime.lastError) return; const info = document.createElement('div'); info.className = 'em-info'; info.innerHTML = `

version ${ext.version}

` + `

id ${id}

` + (ext.homepageUrl ? `

homepage

` : ''); menu.appendChild(info); const copyBtn = info.querySelector('#em-copy'); function doCopy() { navigator.clipboard.writeText(id).then(() => { copyBtn.textContent = 'copied'; copyBtn.style.cursor = 'default'; }).catch(() => { copyBtn.textContent = 'copy failed'; }); } copyBtn.addEventListener('click', doCopy); copyBtn.addEventListener('keydown', (e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); doCopy(); } }); const btnRow = document.createElement('div'); btnRow.className = 'em-btn-row'; if (ext.optionsUrl) { const options = document.createElement('button'); options.className = 'em-btn em-options' + (ext.enabled ? ' em-opt-active' : ''); options.textContent = 'options'; if (ext.enabled) { options.addEventListener('click', function () { chrome.tabs.create({ url: ext.optionsUrl }); }); } btnRow.appendChild(options); } const unInst = document.createElement('button'); unInst.className = 'em-btn em-uninstall'; unInst.textContent = 'uninstall'; unInst.addEventListener('click', function () { rmMenu(); if (!ext.enabled) { chrome.management.setEnabled(id, true, () => { vivaldi.extensionActionUtils.removeExtension(id, vivaldiWindowId); }); } else { vivaldi.extensionActionUtils.removeExtension(id, vivaldiWindowId); } const check = setInterval(() => { chrome.management.get(id, () => { if (chrome.runtime.lastError) { clearInterval(check); load(); } }); }, 200); }); btnRow.appendChild(unInst); menu.appendChild(btnRow); }); item.insertAdjacentElement('afterend', menu); }); }); } // PANEL REGISTRATION // -------------------------------------------------- function registerPanel() { vivaldi.prefs.get('vivaldi.panels.web.elements', (r) => { const elements = r.value; if (!Array.isArray(elements)) return; let element = elements.find(e => e.id === WEB_PANEL_ID); if (!element) { element = { activeUrl: PANEL_URL, faviconUrl: '', faviconUrlValid: false, id: WEB_PANEL_ID, mobileMode: false, origin: 'user', resizable: false, title: NAME, url: PANEL_URL, width: -1, zoom: 1, }; elements.unshift(element); vivaldi.prefs.set({ path: 'vivaldi.panels.web.elements', value: elements }); } Promise.all([ 'vivaldi.toolbars.panel', 'vivaldi.toolbars.navigation', 'vivaldi.toolbars.status', 'vivaldi.toolbars.mail', ].map(path => vivaldi.prefs.get(path))).then(toolbars => { const alreadyPresent = toolbars.some(t => Array.isArray(t.value) && t.value.includes(WEB_PANEL_ID) ); if (!alreadyPresent) { const panels = toolbars[0].value; const insertAt = panels.findIndex(p => p.startsWith('WEBPANEL_')); panels.splice(insertAt === -1 ? 0 : insertAt, 0, WEB_PANEL_ID); vivaldi.prefs.set({ path: 'vivaldi.toolbars.panel', value: panels }); } }); }); } // INJECT CONTENT INTO PANEL // -------------------------------------------------- function findAndInject() { const webPanelStack = document.querySelector('.panel-group .webpanel-stack'); if (!webPanelStack) return; const reactKey = Object.keys(webPanelStack).find(k => k.startsWith('__reactProps')); if (!reactKey) return; const children = webPanelStack[reactKey]?.children?.filter(Boolean) ?? []; const index = children.findIndex(c => c.key === WEB_PANEL_ID); if (index === -1) return; const panel = webPanelStack.querySelector(`.panel.webpanel:nth-child(${index + 1})`); if (!panel) return; if (panel.contains(panelContent)) return; panel.dataset.extensionManager = 'true'; panel.querySelector('.webpanel-header')?.style.setProperty('display', 'none'); panel.querySelector('.webpanel-content')?.style.setProperty('display', 'none'); if (!panel.querySelector('header.em-injected')) { const header = document.createElement('header'); header.className = 'em-injected'; header.innerHTML = '

Extension Manager

'; header.appendChild(toolbar); panel.appendChild(header); } panel.appendChild(panelContent); // Restore privacy mode state before loading getPrivacyMode(function (active) { applyPrivacyMode(active); load(); }); // Guard against stacking listeners on repeated injections if (!panel.dataset.listenersAttached) { panel.dataset.listenersAttached = 'true'; panel.addEventListener('mouseleave', () => { rmMenu(); }); chrome.management.onInstalled.addListener((ext) => { if (ext.installType === 'normal') load(); }); chrome.management.onUninstalled.addListener(() => load()); } } // MUTATION OBSERVER // -------------------------------------------------- let injectRaf = null; function scheduleInject() { if (injectRaf) return; injectRaf = requestAnimationFrame(() => { injectRaf = null; findAndInject(); }); } // BOOT // -------------------------------------------------- function boot() { if (!document.getElementById('browser')) return false; setTimeout(function wait() { if (window.vivaldiWindowId == null) { setTimeout(wait, 300); return; } registerPanel(); const obs = new MutationObserver(scheduleInject); obs.observe(document, { childList: true, subtree: true }); findAndInject(); }, 300); return true; } // Re-disable extensions on startup due to Vivaldi API limitation. const STARTUP_WINDOW_MS = 10000; // 10s of protection after boot const POLL_INTERVAL_MS = 500; function reapplyDisabledOnStartup() { getPrivacyMode(function (active) { const getDisabledFn = active ? getPrivacyDisabled : getDisabled; getDisabledFn(function (disabled) { if (disabled.length === 0) return; const startupDeadline = Date.now() + STARTUP_WINDOW_MS; let finished = false; function reDisable(id, shortName) { chrome.management.setEnabled(id, false, () => { if (chrome.runtime.lastError) { console.error('[EM] startup setEnabled error:', shortName || id, chrome.runtime.lastError.message); } else { console.log('[EM] re-disabled after late enable:', shortName || id); } }); } // 1: event-driven function onEnabledHandler(ext) { if (finished) return; if (Date.now() > startupDeadline) { cleanup(); return; } if (disabled.includes(ext.id)) { console.log('[EM] detected late re-enable of', ext.shortName, '- reverting'); reDisable(ext.id, ext.shortName); } } chrome.management.onEnabled.addListener(onEnabledHandler); // 2: polling backstop function pollTick() { if (finished) return; if (Date.now() > startupDeadline) { cleanup(); return; } chrome.management.getAll(function (info) { if (chrome.runtime.lastError) { console.error('[EM] startup getAll error:', chrome.runtime.lastError.message); } else { disabled.forEach(id => { const ext = info.find(e => e.id === id); if (ext && ext.enabled) { reDisable(id, ext.shortName); } }); } if (!finished) setTimeout(pollTick, POLL_INTERVAL_MS); }); } function cleanup() { if (finished) return; finished = true; chrome.management.onEnabled.removeListener(onEnabledHandler); // Final refresh so the panel reflects true settled state if (typeof load === 'function') load(); console.log('[EM] startup protection window closed'); } chrome.management.getAll(function (info) { if (!chrome.runtime.lastError) { disabled.forEach(id => { const ext = info.find(e => e.id === id); if (ext && ext.enabled) reDisable(id, ext.shortName); }); } pollTick(); }); // Hard stop in case pollTick's own scheduling ever stalls setTimeout(cleanup, STARTUP_WINDOW_MS + POLL_INTERVAL_MS); }); }); } // Wait for vivaldiWindowId (same readiness gate boot() uses) before touching management API (function waitForReadyThenReapply() { if (window.vivaldiWindowId == null) { setTimeout(waitForReadyThenReapply, 300); return; } reapplyDisabledOnStartup(); })(); if (!boot()) { const startObs = new MutationObserver(() => { if (boot()) startObs.disconnect(); }); startObs.observe(document.body, { childList: true, subtree: true }); } })();