/*
* 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 });
}
})();