(() => {
'use strict';
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Global lexical declarations *************************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
const Symbols_X = {
arrow: '⇒', // \u21D2
bolt: '⚡', // \u26A1
bookmark: '🔖', // \u{1F516}
bulb: '💡', // \u{1F4A1}
bullet: '•', // \u2022
calendar: '📅', // \u{1F4C5}
caution: '⚠️', // Emoji copy & paste
check: '✓', // \u2713
clock1: '⏰', // \u23F0
clock2: '🕑', // \u{1F551}
color: '🎨', // \u{1F3A8}
cut: '✂', // \u2702
down: '▼', // \u25BC
downArrow: '↓', // \u2193
eye: '👁️', // \u{1F441}
flame: '🔥', // \u{1F525}
folder: '📁', // \u{1F4C1}
heart: '❤️', // \u2764
hourglass: '⏳', // \u23F3
key: '🔑', // \u{1F511}
memo: '📝', // \u{1F4DD}
moon: '🌙', // Emoji copy & paste
multiply: '❌', // Emoji copy & paste
paintbrush: '🖌', // \u{1F58C}
plus: '➕', // Emoji copy & paste
pointer: '►', // \u25BA
puzzle: '🧩', // \u{1F9E9}
rocket: '🚀', // \u{1F680}
search: '🔍', // \u{1F50D}
settings: '⚙', // Emoji
space: '\u3000', // Large Gap
stack: '☰', // \u2630
star1: '★', // \u2605
star2: '☆', // \u2606
sun: '🌞', // Emoji copy & paste
target: '🎯', // \u{1F3AF}
thumbDown: '👎', // \u{1F44E}
thumbUp: '👍', // \u{1F44D}
tools: '🛠', // \u{1F6E0}
trash: '🗑', // \u{1F5D1}
wall: '🧱', // \u{1F9F1}
warning: '☠️', // Emoji copy & paste
};
const Texts_X = {
customTime: 'Custom Time\n• Left-click to change format',
dateTooltip: 'Calendar / Date\n• Mouseover to update Calendar\n• Left-click to change format',
favInterval: 'Fav Interval',
optionsClose: 'Closes options menu',
optionsMenu: 'Options Menu',
optionsMenuTooltip: 'Open/Close Options Menu',
resizeDelay: 'Resize Delay',
setOrder: '☰ \u3000 \u3000 Set Toolbar Order \u3000 \u3000 ☰',
toggleTooltip: 'Hide/Show Toolbars Except Tabs',
button12: 'Click to insert selector\nWill ignore duplicate entries',
div21: 'Toolbar order from top (left in input field) to bottom (right in input field)',
label0: "(1) Enter non-conflicting keyCodes / combinations\n(2) Comma separate (space optional) for multiple entries\n(3) Leave input field blank for no toggling effect",
label0a: "(1) Enter non-conflicting keyCodes / combinations\n(2) Comma separate (space optional) for multiple entries\n(3) Hides checked toolbars in Toggle Toolbars Button\n(4) Leave input field blank for no toggling effect",
label1: 'Replaces bookmark folders wtih custom icon',
label2: 'Custom CSS',
label3: 'Date Formats:\n 1 = '+ returnDateFormat(1) +'\n 2 = '+ returnDateFormat(2) +'\n 3 = '+ returnDateFormat(3) +'\n 4 = '+ returnDateFormat(4) +'',
label4: 'In Settings > General > Homepage >\nSpecific Page > enter: vivaldi://restart',
label5: 'Displays site favicon in urlbar',
label6: 'Tabs close buttons styled',
label7: 'Moves clicked/active tab to first position in tabbar',
label8: 'Extension Icons large',
label9: 'Show/Hide Workspaces Menu Button In Tabbar',
label13: 'Show/Hide Rewind and Forward buttons',
label14: 'Show/Hide Search Field Input Box',
label15: 'Use mask icon in tabbar to toggle\nvisibility of checked toolbars below',
label16: 'Hide Footer when toggled',
label17: 'Hide Bookmark-bar when toggled',
label18: 'Hide Mainbar when toggled',
label22: 'Time Formats:\n 1 = '+ returnTimeFormat(1) +' 12hr\n 2 = '+ returnTimeFormat(2) +' 12hr\n 3 = '+ returnTimeFormat(3) +' 24hr\n 4 = '+ returnTimeFormat(4) +' 24hr\n 2 & 4 updates every second\n 1 & 3 updates every 10 seconds',
input10: '20 - 2000 (Default: 1000 milliseconds)',
span0a: 'Toggle Auto Hide Options Menu and Tabbar',
span1: 'Bookmark Custom Icons',
span2: "Custom CSS For Vivaldi's Issuna Theme",
span3a: 'Date Before Time',
span4: 'Home Button To Restart Button',
span5: 'Site Favicon In Urlbar',
span6: 'Tabs Close Button Styled',
span7: 'Tab Active Moves To First Position',
span8: 'Extension Icons Large',
span9: 'Workspaces Menu Button',
span10a: 'Increase time in milliseconds to acquire site favicon before sending to urlbar',
span10b: 'Increase time in milliseconds for toolbars to properly load after exiting fullscreen mode',
span11a: 'Clear input field',
span13: 'Rewind / Fast Forward Buttons',
span14: 'Search Field Input Box',
span15: 'Toggle Toolbars Button',
span16: 'Hide Footer',
span17: 'Hide Bookmark-bar',
span18: 'Hide Mainbar',
span22: 'Custom Time',
span23: '🔑',
};
const Timers_X = {
callDialogInt: 500,
delayedInitializeInt: 200,
divOptionInt: 500,
initializeInt: 20,
moveTabInt: 20,
oneSecondInt: 1000,
positionMenuInt: 200,
tabbarInt: 200,
tenSecondInt: 10000,
waitDialogInt: 300,
};
const UPGRADE_VERSION = 3;
const CURRENT_MOD_ID = '_srazzano_ui_mod_';
const OLD_PREFIX_V2 = 'srazzano_';
const UNPREFIXED_V1 = [
'closeButton', 'customCss', 'dateFormat', 'extensionIcons', 'favInterval', 'favInUrl',
'folderImage', 'hideFooter', 'hideBookmark', 'hideMainbar', 'homeRestart', 'keyCodes',
'moveActiveTab', 'positionMenu', 'resizeDelay', 'rewindForward', 'searchbar', 'showDate',
'showTime', 'showWorkspaces', 'timeFormat', 'toolbarList', 'toolbarToggle'
];
const $ = (sel, ctx = document) => ctx?.querySelector(sel) ?? null;
const $$ = (sel, ctx = document) => Array.from(ctx?.querySelectorAll(sel) ?? []);
const $id = (id) => document.getElementById(id);
const $c = (type, props = {}, ...children) => {
const node = document.createElement(type);
Object.entries(props).forEach(([key, value]) => {
if (value === undefined || value === null) return;
if (key.startsWith('on') && typeof value === 'function') {
const event = key.substring(2).toLowerCase();
node.addEventListener(event, value);
}
else if (key === 'style' && typeof value === 'object') {
Object.assign(node.style, value);
}
else if (key === 'className' || key === 'class') {
node.className = Array.isArray(value) ? value.join(' ') : value;
}
else if (key in node) {
node[key] = value;
}
else {
node.setAttribute(key, value);
}
});
children.flat(Infinity).forEach(child => {
if (child == null) return;
if (typeof child === 'string' || typeof child === 'number') {
node.appendChild(document.createTextNode(child));
}
else if (child instanceof Node) {
node.appendChild(child);
}
});
return node;
};
const handleDrag = (event) => {
event.preventDefault?.();
setTimeout(() => {
homeToRestart(s.homeRestart), s.resizeDelay;});
};
const handleFocus = () => {
setTimeout(() => {
reloadElements(), s.resizeDelay});
};
const handleLayout = () => {
setTimeout(() => {
reloadElements();
getToolbarList(s.toolbarList);
}, s.resizeDelay);
};
const handleLoad = () => {
setTimeout(() => {
initialize(), Timers.delayedInitialize;
});
};
const insertAfter = (newNode, ref) => {
ref?.parentNode?.insertBefore(newNode, ref.nextSibling);
};
const removeDupes = (className) => {
const elements = document.getElementsByClassName(className);
if (elements.length <= 1) return;
[...elements].slice(1).forEach(el => el.remove()); // Usage: removeDupes?.('aCal');
};
const storage = {
// ──────────────────────────────────────────
// Non-prefixed (legacy / direct access)
// ──────────────────────────────────────────
async get(key, defaultValue = undefined) {
try {
const result = await chrome.storage.local.get(key);
return result[key] ?? defaultValue;
} catch (error) {
console.error(`Failed to get storage key "${key}":`, error);
return defaultValue;
}
},
async getAll() {
try {
const result = await chrome.storage.local.get(null);
return result || {};
} catch (error) {
console.error("Failed to get all storage:", error);
return {};
}
},
async set(key, value) {
try {
await chrome.storage.local.set({ [key]: value });
} catch (error) {
console.error(`Failed to set storage key "${key}":`, error);
throw error;
}
},
async remove(key) {
try {
await chrome.storage.local.remove(key);
} catch (error) {
console.error(`Failed to remove storage key "${key}":`, error);
throw error;
}
},
// ──────────────────────────────────────────
// Prefixed methods (For all new code)
// ──────────────────────────────────────────
getModKey(shortKey) {
return `${CURRENT_MOD_ID}${shortKey}`;
},
async getMod(shortKey, defaultValue = undefined) {
try {
const fullKey = this.getModKey(shortKey);
const result = await chrome.storage.local.get(fullKey);
return result[fullKey] ?? defaultValue;
} catch (error) {
console.error(`Failed to get mod key "${shortKey}":`, error);
return defaultValue;
}
},
async setMod(shortKey, value) {
try {
const fullKey = this.getModKey(shortKey);
await chrome.storage.local.set({ [fullKey]: value });
} catch (error) {
console.error(`Failed to set mod key "${shortKey}":`, error);
throw error;
}
},
async removeMod(shortKey) {
try {
const fullKey = this.getModKey(shortKey);
await chrome.storage.local.remove(fullKey);
} catch (error) {
console.error(`Failed to remove mod key "${shortKey}":`, error);
throw error;
}
},
// ──────────────────────────────────────────
// Get ALL mod-prefixed keys
// ──────────────────────────────────────────
async getAllMod() {
try {
const result = await chrome.storage.local.get(null);
return Object.fromEntries(Object.entries(result)
.filter(([key]) => key.startsWith(CURRENT_MOD_ID))
.map(([key, value]) => [key.slice(CURRENT_MOD_ID.length), value])
);
} catch (error) {
console.error("Failed to get all mod storage:", error);
return {};
}
},
// ──────────────────────────────────────────
// Clear ONLY mod-prefixed keys (safer)
// ──────────────────────────────────────────
async clearAllMod() {
try {
const result = await chrome.storage.local.get(null);
const keysToRemove = Object.keys(result).filter(key => {
key.startsWith(CURRENT_MOD_ID);
});
if (keysToRemove.length) {
await chrome.storage.local.remove(keysToRemove);
}
} catch (error) {
console.error("Failed to clear all mod storage:", error);
throw error;
}
},
// ──────────────────────────────────────────
// ☠️☠️☠️ Clear everything – USE WITH CAUTION
// ──────────────────────────────────────────
async clearAll() {
try {
await chrome.storage.local.clear();
} catch (error) {
console.error("Failed to clear all storage:", error);
throw error;
}
}
};
// ──────────────────────────────────────────
// ↓ Prefixed keys ↓
// ──────────────────────────────────────────
let closeButton = false;
let customCss = false;
let dateFormat = 1;
let extensionIcons = false;
let favInterval = 1000;
let favInUrl = false;
let folderImage = false;
let hideBookmark = false;
let hideFooter = false;
let hideMainbar = false;
let hideTabbar = false;
let homeRestart = false;
let keyCodes = '';
let keyCodes2 = '';
let moveActiveTab = false;
let positionMenu = 1;
let resizeDelay = 1000;
let rewindForward = false;
let searchbar = false;
let showDate = false;
let showTime = false;
let showWorkspaces = false;
let timeFormat = 1;
let toolbarList = '.mainbar, .bookmark-bar';
let toolbarToggle = false;
// ──────────────────────────────────────────
// ↓ DO NOT PREFIX ↓
// ──────────────────────────────────────────
let favTimer = null;
let hasMigrationData = false;
let hasSettingsData = false;
let initInterval = null;
let timeTimer = null;
let s = {};
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Functions initialize and loadAllSettings ************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
async function initialize() {
await loadAllSettings();
const browser = $id('browser');
const main = $id('main');
const mainbar = $('.mainbar', main);
const exts = $('.toolbar-extensions > .button-toolbar', main);
const footer2 = $('.dialog-footer');
const tabsContainer = $id('tabs-container');
const workspace = $('.tabbar-workspace-button .button-title', tabsContainer);
const HIDE_KEY = 'hideTabbar';
if (storage.getMod(HIDE_KEY) === null) {
await storage.setMod(HIDE_KEY, false);
}
tabbarShow();
if (browser) {
const setAttr = (name, val, attrVal = 'true') => {
val ? browser.setAttribute(name, attrVal) : browser.removeAttribute(name);
};
mainbar.id = 'mainbar';
setAttr('custom-close', s.closeButton);
setAttr('custom-css', s.customCss);
setAttr('custom-folder', s.folderImage);
setAttr('extension-icons', s.extensionIcons);
setAttr('fav-in-url', s.favInUrl);
setAttr('footer-in-header', s.toolbarList.includes('#footer'));
setAttr('hide-bookmark', s.hideBookmark);
setAttr('hide-footer', s.hideFooter);
setAttr('hide-mainbar', s.hideMainbar);
setAttr('hide-tabbar', s.hideTabbar);
setAttr('home-to-restart', s.homeRestart);
setAttr('move-active-tab', s.moveActiveTab);
setAttr('rewind-forward', s.rewindForward);
setAttr('searchbar', s.searchbar);
setAttr('show-date', s.showDate, `Format ${s.dateFormat}`);
setAttr('show-time', s.showTime, `Format ${s.timeFormat}`);
setAttr('show-workspaces', s.showWorkspaces);
setAttr('toolbar-toggle', s.toolbarToggle);
}
if (s.showTime) {
startTime();
}
if (exts) {
exts.style.setProperty('--extensionsExpanded', '1');
}
if (footer2 && !browser.contains(footer2)) {
browser.appendChild(footer2);
}
if (workspace) {
workspace.innerHTML = '';
}
if (!hasSettingsData) {
console.log('Vivaldi UI Mod Settings:', s);
hasSettingsData = true;
} }
async function loadAllSettings() {
const defaults = {
closeButton: false,
customCss: false,
dateFormat: 1,
extensionIcons: false,
favInterval: 1000,
favInUrl: false,
folderImage: false,
hideFooter: false,
hideBookmark: false,
hideMainbar: false,
hideTabbar: false,
homeRestart: false,
keyCodes: '',
keyCodes2: '',
moveActiveTab: false,
positionMenu: 1,
resizeDelay: 1000,
rewindForward: false,
searchbar: false,
showDate: false,
showTime: false,
showWorkspaces: false,
timeFormat: 1,
toolbarList: '.mainbar, .bookmark-bar',
toolbarToggle: false,
};
try {
const allData = await chrome.storage.local.get(null);
const allKeys = Object.keys(allData);
const hasOldPrefixV2 = allKeys.some(key => key.startsWith(OLD_PREFIX_V2));
const hasUnprefixed = UNPREFIXED_V1.some(key => allKeys.includes(key));
const hasCurrentPrefix = allKeys.some(key => key.startsWith(CURRENT_MOD_ID));
const needsMigration = hasOldPrefixV2 || hasUnprefixed || !hasCurrentPrefix;
if (needsMigration) {
console.log(`[${CURRENT_MOD_ID}] Old data detected. Starting migration to version ${UPGRADE_VERSION}...`);
await migrateToCurrentVer();
} else if (!hasMigrationData) {
console.log(`[${CURRENT_MOD_ID}] Storage is already up to date (version ${UPGRADE_VERSION}).`);
hasMigrationData = true;
}
const stored = await storage.getAllMod();
s = { ...defaults, ...stored };
const fRaw = Number(s.favInterval);
s.favInterval = Number.isInteger(fRaw) ? Math.max(20, Math.min(2000, fRaw)) : 1000;
const rRaw = Number(s.resizeDelay);
s.resizeDelay = Number.isInteger(rRaw) ? Math.max(20, Math.min(2000, rRaw)) : 1000;
s.positionMenu = Number(s.positionMenu) || 1;
favImage(s.favInUrl);
getToolbarList(s.toolbarList);
homeToRestart(s.homeRestart);
moveTab(s.moveActiveTab);
returnDateFormat(s.dateFormat);
dateHolder();
setOptionsButton();
setOptionsMenu();
setToggleButton();
timeHolder();
} catch (err) {
console.error('Failed to load settings:', err);
s = { ...defaults };
} }
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Migrate old prefixed / unprefixed keys from V1 / V2 to current CURRENT_MOD_ID V3 only if detected ***************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
async function migrateToCurrentVer() {
const allData = await chrome.storage.local.get(null);
const allKeys = Object.keys(allData);
const newData = {};
for (const key of UNPREFIXED_V1) {
if (allData[key] !== undefined) newData[`${CURRENT_MOD_ID}${key}`] = allData[key];
}
for (const key of allKeys) {
if (key.startsWith(OLD_PREFIX_V2)) {
const shortKey = key.slice(OLD_PREFIX_V2.length);
newData[`${CURRENT_MOD_ID}${shortKey}`] = allData[key];
} }
if (Object.keys(newData).length > 0) {
await chrome.storage.local.set(newData);
const keysToRemove = [
...UNPREFIXED_V1.filter(k => allData[k] !== undefined),
...allKeys.filter(k => k.startsWith(OLD_PREFIX_V2))
];
await chrome.storage.local.remove(keysToRemove);
console.log(`[${CURRENT_MOD_ID}] Migration completed. Moved ${Object.keys(newData).length} keys.`);
}
await storage.set('_srazzano_migrationVersion', UPGRADE_VERSION);
hasMigrationData = true;
}
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Date / calendar *************************************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
function dateHolder() {
const main = $id('main');
const statusBar = $('.toolbar-statusbar', main);
if (!statusBar) return;
const div = $c('div', { id: 'calendar', className: 'aCal', title: Texts_X.dateTooltip });
try {
if (typeof s.dateFormat !== 'number' || s.dateFormat < 1 || s.dateFormat > 4) {
s.dateFormat = 1;
}
div.textContent = returnDateFormat(s.dateFormat);
div.onmouseover = setDateText;
div.onclick = () => selectDateFormat();
statusBar.insertBefore(div, statusBar.lastChild);
removeDupes?.('aCal');
} catch (err) {
console.error('dateHolder error:', err);
} }
function returnDateFormat(int) {
if (!Number.isInteger(int) || int < 1 || int > 4) {
throw new RangeError('int must be an integer between 1 and 4');
}
const date = new Date();
const locale = navigator.language;
const getPart = (options) => new Intl.DateTimeFormat(locale, options).format(date);
const parts = new Intl.DateTimeFormat(locale, {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
}).formatToParts(date);
const map = Object.fromEntries(parts.map(p => [p.type, p.value]));
const day = parseInt(map.day);
const dayPadded = map.day.padStart(2, '0');
const year = map.year;
const suffix = ['th', 'st', 'nd', 'rd'][(day % 10 > 3 || Math.floor(day / 10) === 1 ? 0 : day % 10)] || 'th';
const ordinal = day + suffix;
const formats = [
null,
() => `${map.weekday} ⇒ ${map.month} ${ordinal}, ${year}`, // 1: Monday ⇒ January 1st, 2026
() => `${getPart({weekday: 'short'})} * ${getPart({month: 'short'})} ${day}, ${year}`, // 2: Mon * Jan 1, 2026
() => `${map.weekday} • ${getPart({month: 'numeric'})}/${dayPadded}/${year}`, // 3: Monday • 1/01/2026
() => `${getPart({weekday: 'short'})} :: ${getPart({month: '2-digit'})}-${dayPadded}-${year}` // 4: Mon :: 01-01-2026
];
return formats[int]();
}
async function selectDateFormat() {
if (!s.showDate) return;
s.dateFormat = (s.dateFormat % 4) + 1;
await storage.setMod('dateFormat', s.dateFormat);
const calendar = $id('calendar');
const input3 = $id('input3');
const span3b = $id('span3b');
if (calendar) {
calendar.textContent = returnDateFormat(s.dateFormat);
}
if (input3) {
input3.value = s.dateFormat;
}
if (span3b) {
span3b.textContent = `Format ${s.dateFormat}`;
} }
function setDateText() {
if (!s.showDate) return;
const calendar = $id('calendar');
if (calendar) {
calendar.textContent = returnDateFormat(s.dateFormat);
} }
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Time / clock and digitalClock ***********************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
function timeHolder() {
const cal = $id('calendar');
const main = $id('main');
const clk = $('.ClockButton > button', main);
if (!cal || !clk) return;
const div = $c('div', { id: 'digitalClock', className: 'aClk', title: Texts_X.customTime });
try {
if (typeof s.timeFormat !== 'number' || s.timeFormat < 1 || s.timeFormat > 4) {
s.timeFormat = 1;
}
clk.id = 'clock';
insertAfter(div, cal);
div.textContent = returnTimeFormat(s.timeFormat);
div.onclick = () => selectTimeFormat();
removeDupes?.('aClk');
} catch (err) {
console.error('timeHolder error:', err);
} }
function createTimeFormats() {
const date = new Date();
const h12 = String(date.getHours() % 12 || 12); // 3:14 = 3:14 No padStart
const h12p = h12.padStart(2, '0'); // 3:14 = 03:14 padStart
const h24 = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
const sec = String(date.getSeconds()).padStart(2, '0');
const ampm = date.getHours() < 12 ? 'AM' : 'PM';
return {
time12: `${h12}:${min} ${ampm}`, // 3:14 PM No padStart
time12Full: `${h12}:${min}:${sec} ${ampm}`, // 3:14:55 PM No padStart
time24: `${h24}:${min}`, // 15:14
time24Full: `${h24}:${min}:${sec}`, // 15:14:55
};
}
function returnTimeFormat(format = s.timeFormat) {
const times = createTimeFormats();
return {
1: times.time12,
2: times.time12Full,
3: times.time24,
4: times.time24Full
}[format] ?? times.time12Full;
}
async function selectTimeFormat() {
if (!s.showTime) return;
s.timeFormat = (s.timeFormat % 4) + 1;
await storage.setMod('timeFormat', s.timeFormat);
const digitalClock = $id('digitalClock');
const span22b = $id('span22b');
if (digitalClock) {
digitalClock.textContent = returnTimeFormat(s.timeFormat);
}
if (span22b) {
span22b.textContent = `Format ${s.timeFormat}`;
} }
function startTime() {
if (timeTimer) {
clearInterval(timeTimer);
timeTimer = null;
}
if (s.showTime) {
const intervalMs = (s.timeFormat === 2 || s.timeFormat === 4) ? Timers_X.oneSecondInt : Timers_X.tenSecondInt;
setTimeDisplay();
timeTimer = setInterval(setTimeDisplay, intervalMs);
} }
function setTimeDisplay() {
const element = $id('digitalClock');
if (element) {
element.textContent = returnTimeFormat(s.timeFormat);
} }
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Options button and menu *****************************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
function setOptionsButton() {
const optBtn = $c('button', {id: 'optionsButton', className: 'ToolbarButton-Button custom-button optionsButton',
draggable: 'false', tabindex: '-1', title: Texts_X.optionsMenuTooltip, type: 'button', onclick: onOptions});
const main = $id('main');
const footer = $('#footer > div');
const statusBar = $('.toolbar-statusbar', main);
try {
if (footer) {
footer.insertBefore(optBtn, footer.firstChild);
}
} catch (err) {};
removeDupes?.('optionsButton');
}
function setOptionsMenu() {
const browser = $id('browser');
const main = $id('main');
const inner = $('.inner', main);
const optMenu = $c('div', { id: 'optionsMenu', className: 'options-menu-popup' });
const div0 = $c('div', { id: 'div0' });
const optionsHeader = $c('div', { id: 'optionsHeader', className: '' });
[1, 2, 3, 4].forEach(n => {
const input = $c('input', { id: `position${n}`, className: 'radio', type: 'radio', value: n,
title: `Position Menu > ${n === 1 ? 'TOP LEFT' : n === 2 ? 'TOP CENTER' : n === 3 ? 'TOP RIGHT' : 'CENTERED'}` });
div0.appendChild(input);
});
optionsHeader.appendChild($c('svg', { id: 'optionsMenuIcon' }));
optionsHeader.appendChild($c('span', { id: 'optionsMenuText', textContent: Texts_X.optionsMenu }));
optionsHeader.appendChild($c('button', { id: 'optionsMenuClose', className: 'button', title: Texts_X.optionsClose }));
div0.appendChild(optionsHeader);
optMenu.appendChild(div0);
function Row1(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.span1 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row2(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label2 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row3(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const btn = $c('button', { id: `button${id}`, className: 'button' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label22 }));
btn.appendChild($c('span', { id: `span${id}b`, className: 'span', textContent: `Format ${s.timeFormat}` }));
label.appendChild(btn);
return label;
}
function Row4(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const btn = $c('button', { id: `button${id}`, className: 'button' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label3 }));
btn.appendChild($c('span', { id: `span${id}b`, className: 'span', textContent: `Format ${s.dateFormat}` }));
label.appendChild(btn);
return label;
}
function Row5(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label8 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row6(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label4 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row7(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label13 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row8(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label14 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row9(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const span = $c('span', { id: 'currentIcon', className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label5 }));
span.appendChild($c('img', { id: 'currentI', className: 'icon', src: '' }));
label.appendChild(span);
return label;
}
function Row10(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label7 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row11(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label6 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row22(id, labelText, spanText, tooltip, value) {
const label = $c('label', { id: `label${id}`, className: 'label' });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
label.appendChild($c('label', { id: `label${id}`, textContent: 'Toggle Options Menu' }));
label.appendChild($c('input', { id: `input${id}`, type: 'text', value: value }));
label.appendChild($c('svg', { id: `svg${id}b`, className: 'svg image' }));
return label;
}
function Row23(id, labelText, spanText, tooltip, value) {
const label = $c('label', { id: `label${id}`, className: 'label' });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
label.appendChild($c('label', { id: `label${id}`, textContent: 'Toggle Header Tabbar' }));
label.appendChild($c('input', { id: `input${id}`, type: 'text', value: value }));
label.appendChild($c('svg', { id: `svg${id}b`, className: 'svg image' }));
return label;
}
function Row13(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label15 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row14(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label indent', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}a`, className: 'span', textContent: spanText || Texts_X.label17 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row15(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label indent', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}a`, className: 'span', textContent: spanText || Texts_X.label18 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row16(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label indent', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}a`, className: 'span', textContent: spanText || Texts_X.label16 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row17(id, labelText, spanText, tooltip, checked) {
const label = $c('label', { id: `label${id}`, className: 'label', title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
label.appendChild($c('input', { id: `input${id}`, className: 'input checkbox', type: 'checkbox', checked: checked }));
label.appendChild($c('span', { id: `span${id}`, className: 'span', textContent: spanText || Texts_X.label9 }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
label.appendChild(iconSpan);
return label;
}
function Row18(id, labelText, spanText, tooltip) {
const div = $c('div', { id: `div${id}`, title: tooltip });
const iconSpan = $c('span', { id: `icon${id}`, className: 'icon' });
div.appendChild($c('span', { id: `span${id}a`, className: '', textContent: Texts_X.favInterval, title: Texts_X.span10a }));
div.appendChild($c('input', { id: `input${id}a`, className: 'input input-timer', type: 'number', title: Texts_X.input10, value: s.favInterval }));
div.appendChild($c('span', { id: `span${id}b`, className: '', textContent: Texts_X.resizeDelay, title: Texts_X.span10b }));
div.appendChild($c('input', { id: `input${id}b`, className: 'input input-timer', type: 'number', title: Texts_X.input10, value: s.resizeDelay }));
iconSpan.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
div.appendChild(iconSpan);
return div;
}
function Row19(id, labelText, spanText, tooltip) {
const div = $c('div', {id: `div${id}`, className: 'div order'});
div.appendChild($c('span', {id: `span${id}`, textContent: `${Texts_X.setOrder}`}));
return div;
}
function Row20(id, labelText, spanText, tooltip, value) {
const div = $c('div', { id: `div${id}`, title: tooltip });
const cont = $c('div', { id: `container${id}` });
cont.appendChild($c('input', { id: `input${id}`, className: 'input', type: 'text', value: value }));
cont.appendChild($c('svg', { id: `svg${id}`, className: 'svg image' }));
div.appendChild(cont);
return div;
}
function Row21(id, labelText, spanText, tooltip) {
const div = $c('div', {id: `div${id}`, title: tooltip});
const btn1 = $c('button', {id: `button${id}a`, className: 'button', title: `${Texts_X.button12}`});
const btn2 = $c('button', {id: `button${id}b`, className: 'button', title: `${Texts_X.button12}`});
const btn3 = $c('button', {id: `button${id}c`, className: 'button', title: `${Texts_X.button12}`});
btn1.appendChild($c('span', {id: `span${id}a`, className: 'span', textContent: `.mainbar`}));
btn2.appendChild($c('span', {id: `span${id}b`, className: 'span', textContent: `.bookmark-bar`}));
btn3.appendChild($c('span', {id: `span${id}c`, className: 'span', textContent: `#footer`}));
div.appendChild(btn1);
div.appendChild(btn2);
div.appendChild(btn3);
return div;
}
optMenu.appendChild(Row1(1, null, Texts_X.span1, Texts_X.label1, s.folderImage));
optMenu.appendChild(Row2(2, null, Texts_X.span2, Texts_X.label2, s.customCss));
optMenu.appendChild(Row3(22, null, Texts_X.span22, Texts_X.label22, s.showTime));
optMenu.appendChild(Row4(3, null, Texts_X.span3a, Texts_X.label3, s.showDate));
optMenu.appendChild(Row5(8, null, Texts_X.span8, Texts_X.label8, s.extensionIcons));
optMenu.appendChild(Row6(4, null, Texts_X.span4, Texts_X.label4, s.homeRestart));
optMenu.appendChild(Row7(13, null, Texts_X.span13, Texts_X.label13, s.rewindForward));
optMenu.appendChild(Row8(14, null, Texts_X.span14, Texts_X.label14, s.searchbar));
optMenu.appendChild(Row9(5, null, Texts_X.span5, Texts_X.label5, s.favInUrl));
optMenu.appendChild(Row10(7, null, Texts_X.span7, Texts_X.label7, s.moveActiveTab));
optMenu.appendChild(Row11(6, null, Texts_X.span6, Texts_X.label6, s.closeButton));
optMenu.appendChild(Row22(23, null, Texts_X.span23, Texts_X.label0, s.keyCodes));
optMenu.appendChild(Row23(24, null, Texts_X.span24, Texts_X.label0a, s.keyCodes2));
optMenu.appendChild(Row13(15, null, Texts_X.span15, Texts_X.label15, s.toolbarToggle));
optMenu.appendChild(Row14(17, null, Texts_X.span17, Texts_X.label17, s.hideBookmark));
optMenu.appendChild(Row15(18, null, Texts_X.span18, Texts_X.label18, s.hideMainbar));
optMenu.appendChild(Row16(16, null, Texts_X.span16, Texts_X.label16, s.hideFooter));
optMenu.appendChild(Row17(9, null, Texts_X.span9, Texts_X.label9, s.showWorkspaces));
optMenu.appendChild(Row18(10, null, null, null));
optMenu.appendChild(Row19(20, null, null, null));
optMenu.appendChild(Row20(21, null, null, Texts_X.div21, s.toolbarList));
optMenu.appendChild(Row21(12, null, null, null));
if (inner) inner.appendChild(optMenu);
try {
if (browser) {
$id('input1').checked = s.folderImage;
$id('input3').checked = s.showDate;
$id('input2').checked = s.customCss;
$id('input8').checked = s.extensionIcons;
$id('input4').checked = s.homeRestart;
$id('input13').checked = s.rewindForward;
$id('input14').checked = s.searchbar;
$id('input5').checked = s.favInUrl;
$id('input7').checked = s.moveActiveTab;
$id('input6').checked = s.closeButton;
$id('input15').checked = s.toolbarToggle;
$id('input22').checked = s.showTime;
$id('input17').checked = s.hideBookmark;
$id('input18').checked = s.hideMainbar;
$id('input16').checked = s.hideFooter;
$id('input9').checked = s.showWorkspaces;
const menu = $id('optionsMenu');
if (menu) {
$$('.radio', menu).forEach(radio => {
radio.checked = parseInt(radio.value) === s.positionMenu;
radio.onclick = e => onOptionsMenuRadio(parseInt(e.target.value));
});
$$('.checkbox', menu).forEach(el => {
el.onclick = e => onOptionsMenuInput(e.target.id);
});
}
$id('optionsMenuClose').onclick = onOptions;
$id('button3').onclick = () => selectDateFormat();
$id('button22').onclick = () => selectTimeFormat();
$id('button12a').onclick = () => onSelector('button12a');
$id('button12b').onclick = () => onSelector('button12b');
$id('button12c').onclick = () => onSelector('button12c');
$id('svg21').onclick = onClearField;
$id('input10a').oninput = () => onOptionsMenuInput('input10a');
$id('input10b').oninput = () => onOptionsMenuInput('input10b');
$id('input23').oninput = () => onOptionsMenuInput('input23');
$id('input24').oninput = () => onOptionsMenuInput('input24');
$id('input10a').value = s.favInterval ?? 1000;
$id('input10b').value = s.resizeDelay ?? 1000;
$id('input21').value = s.toolbarList ?? '.mainbar, .bookmark-bar';
removeDupes?.('options-menu-popup');
}
} catch (err) {
console.error('setOptionsMenu error:', err);
} }
function onOptions() {
const browser = $id('browser');
if (browser.toggleAttribute('options-menu')) {
$id('span3b').textContent = `Format ${s.dateFormat}`;
$id('span22b').textContent = `Format ${s.timeFormat}`;
}
onOptionsMenuPosition(s.positionMenu);
}
async function onOptionsMenuInput(id) {
const browser = $id('browser');
const el = $id(id);
if (!el) {
console.warn(`Element not found: #${id}`);
return;
}
const config = {
'input1': {
save: () => {
s.folderImage = el.checked;
return ['folderImage', s.folderImage];
},
attr: 'custom-folder'
},
'input2': {
save: () => {
s.customCss = el.checked;
return ['customCss', s.customCss];
},
attr: 'custom-css'
},
'input3': {
save: () => {
s.showDate = el.checked;
return ['showDate', s.showDate];
},
attr: 'show-date'
},
'input4': {
save: () => {
s.homeRestart = el.checked;
homeToRestart(s.homeRestart);
return ['homeRestart', s.homeRestart];
},
attr: 'home-to-restart'
},
'input5': {
save: () => {
s.favInUrl = el.checked;
if (s.favInUrl) {
favImage(s.favInUrl);
}
return ['favInUrl', s.favInUrl];
},
attr: 'fav-in-url'
},
'input6': {
save: () => {
s.closeButton = el.checked;
return ['closeButton', s.closeButton];
},
attr: 'custom-close'
},
'input7': {
save: () => {
s.moveActiveTab = el.checked;
moveTab(s.moveActiveTab);
return ['moveActiveTab', s.moveActiveTab];
},
attr: 'move-active-tab'
},
'input8': {
save: () => {
s.extensionIcons = el.checked;
return ['extensionIcons', s.extensionIcons];
},
attr: 'extension-icons'
},
'input9': {
save: () => {
s.showWorkspaces = el.checked;
return ['showWorkspaces', s.showWorkspaces];
},
attr: 'show-workspaces'
},
'input10a': {
save: () => {
const val = Number(el.value);
s.favInterval = Number.isInteger(val) ? Math.max(20, Math.min(2000, val)) : 1000;
return ['favInterval', s.favInterval];
}
},
'input10b': {
save: () => {
const val = Number(el.value);
s.resizeDelay = Number.isInteger(val) ? Math.max(20, Math.min(2000, val)) : 1000;
return ['resizeDelay', s.resizeDelay];
}
},
'input13': {
save: () => {
s.rewindForward = el.checked;
return ['rewindForward', s.rewindForward];
},
attr: 'rewind-forward'
},
'input14': {
save: () => {
s.searchbar = el.checked;
return ['searchbar', s.searchbar];
},
attr: 'searchbar'
},
'input15': {
save: () => {
const label16 = $id('label16');
const label17 = $id('label17');
const label18 = $id('label18');
s.toolbarToggle = el.checked;
if (s.toolbarToggle) {
label16?.removeAttribute('disabled');
label17?.removeAttribute('disabled');
label18?.removeAttribute('disabled');
} else {
label16?.setAttribute('disabled', true);
label17?.setAttribute('disabled', true);
label18?.setAttribute('disabled', true);
}
return ['toolbarToggle', s.toolbarToggle];
},
attr: 'toolbar-toggle'
},
'input16': {
save: () => {
s.hideFooter = el.checked;
return ['hideFooter', s.hideFooter];
},
attr: 'hide-footer'
},
'input17': {
save: () => {
s.hideBookmark = el.checked;
return ['hideBookmark', s.hideBookmark];
},
attr: 'hide-bookmark'
},
'input18': {
save: () => {
s.hideMainbar = el.checked;
return ['hideMainbar', s.hideMainbar];
},
attr: 'hide-mainbar'
},
'input22': {
save: () => {
s.showTime = el.checked;
browser?.[s.showTime ? 'setAttribute' : 'removeAttribute']('show-time', `Format ${s.timeFormat}`);
clearInterval(timeTimer);
timeTimer = null;
if (s.showTime) {
startTime();
}
return ['showTime', s.showTime];
}
},
'input23': {
save: () => {
s.keyCodes = el.value.trim();
return ['keyCodes', s.keyCodes];
}
},
'input24': {
save: () => {
s.keyCodes2 = el.value.trim();
return ['keyCodes2', s.keyCodes2];
}
}
};
const item = config[id];
if (!item) {
console.warn('Unknown input:', id);
return;
}
const [storageKey, value] = item.save();
await storage.setMod(storageKey, value);
if (item.attr) {
browser?.[value ? 'setAttribute' : 'removeAttribute'](item.attr, true);
} }
function onOptionsMenuPosition(position) {
const main = $id('main');
const inner = $('.inner', main);
const menu = $id('optionsMenu');
if (!inner || !menu) return;
const viewportWidth = window.innerWidth;
const innerRect = inner.getBoundingClientRect();
const menuRect = menu.getBoundingClientRect();
$$('.radio', menu).forEach(radio => radio.checked = false);
const radio = $id(`position${position}`);
if (radio) {
radio.checked = true;
}
let top = inner.clientTop + 'px';
let left = '0px';
const positions = {
1: () => { },
2: () => {
const centerX = viewportWidth / 2;
left = `${centerX - (menuRect.width / 2)}px`;
},
3: () => {
left = `${viewportWidth - menuRect.width - 10}px`;
},
4: () => {
const centerX = viewportWidth / 2;
const centerY = innerRect.top + (innerRect.height / 2);
top = `${centerY - (menuRect.height / 2)}px`;
left = `${centerX - (menuRect.width / 2)}px`;
}
};
const positionHandler = positions[Number(position)];
if (positionHandler) {
positionHandler();
} else {
console.warn(`Unknown menu position option: ${position}`);
}
menu.style.top = top;
menu.style.left = left;
}
async function onOptionsMenuRadio(position) {
s.positionMenu = Number(position);
await storage.setMod('positionMenu', s.positionMenu);
onOptionsMenuPosition(s.positionMenu);
getCurrentTab();
}
async function onSelector(buttonId) {
const input = $id('input21');
if (!input) return;
const browser = $id('browser');
const selectorMap = {
button12a: '.mainbar',
button12b: '.bookmark-bar',
button12c: '#footer',
};
const selector = selectorMap[buttonId];
if (!selector) return;
let currentList = input.value.split(',').map(s => s.trim()).filter(Boolean);
if (currentList.includes(selector)) return;
currentList.push(selector);
const newValue = currentList.join(', ');
input.value = newValue;
s.toolbarList = newValue;
await storage.setMod('toolbarList', s.toolbarList);
if (browser) {
const hasFooter = newValue.includes('#footer');
browser?.[hasFooter ? 'setAttribute' : 'removeAttribute']('footer-in-header', true);
}
if (buttonId === 'button12c') {
setTimeout(() => onOptionsMenuPosition(s.positionMenu), Timers_X.positionMenuInt);
}
getToolbarList(newValue);
}
async function onClearField() {
const browser = $id('browser');
const inp21 = $id('input21');
inp21.value = '';
s.toolbarList = '.mainbar, .bookmark-bar';
await storage.setMod('toolbarList', s.toolbarList);
if (browser) {
browser.appendChild($id('footer'));
browser.removeAttribute('footer-in-header');
}
getToolbarList(s.toolbarList);
onOptionsMenuPosition(s.positionMenu);
}
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Favicon in Url **************************************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
function favImage(bol) {
const browser = $id('browser');
const favImg = $id('favImg');
const main = $id('main');
const field = $('.UrlField', main);
const img = $c('img', {id: 'favImg'});
try {
if (favImg) {
field.removeChild(img);
return;
}
browser?.[bol ? 'setAttribute' : 'removeAttribute']('fav-in-url', true);
field.insertBefore(img, field.firstChild);
getCurrentTabUpdated();
} catch (err) {}
}
function getCurrentTab() {
const browser = $id('browser');
if (!browser) return;
const container = $id('webview-container');
const img = $id('favImg');
const current = $id('currentI');
const SHIFT_PX = '-3px 0px';
const sitesToShift = [
'https://www.youtube.com',
'https://forum.vivaldi.net'
];
if (!container) {
console.warn('Webview container not found');
}
if (browser.hasAttribute('fav-in-url')) {
chrome.tabs.query({ currentWindow: true, active: true }, ([tab]) => {
if (!tab) return;
const shouldShift = sitesToShift.some(site => tab.url.startsWith(site));
if (container) {
container.style.margin = shouldShift ? SHIFT_PX : '0px';
}
if (img) {
if (tab.favIconUrl) {
try {
const faviconUrl = new URL(tab.favIconUrl);
if (['http:', 'https:', 'data:'].includes(faviconUrl.protocol)) {
img.src = tab.favIconUrl;
} else {
img.src = '/style/icons/page.png';
}
} catch (e) {
img.src = '/style/icons/page.png';
}
} else {
if (tab.url.startsWith('chrome-extension://')) {
img.src = '/style/icons/settings.png';
} else {
img.src = '/style/icons/page.png';
} } }
if (current && img) {
if (tab.url.startsWith('https://github')) {
current.src = '/style/icons/github.png';
} else if (tab.url.startsWith('https://www.youtube')) {
current.src = '/style/icons/youtube.png';
} else if (tab.url.startsWith('https://tv.youtube')) {
current.src = '/style/icons/youtubetv.png';
} else {
current.src = img.src;
} }
});
} else {
if (current) {
current.src = '/style/icons/page.png';
} } }
function getCurrentTabUpdated() {
favTimer = setInterval(() => {
getCurrentTab(); reloadElements();
}, s.favInterval);
}
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Toolbar list ****************************************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
function getToolbarList(list) {
const selectors = list.split(',')
.map(s => s.trim())
.filter(s => s && /^(?:\.[a-z-]+|#(?:footer|[\w-]+))$/i.test(s));
for (const selector of selectors.toReversed()) {
try {
const element = $(selector);
if (element) {
main.insertBefore(element, main.firstChild);
}
} catch (err) {
console.warn(`Invalid selector skipped: ${selector}`, err);
} } }
function setToolbars() {
const browser = $id('browser');
browser.toggleAttribute('toggle-toolbars');
}
async function onClearField() {
const browser = $id('browser');
const main = $id('main');
const inner = $('.inner', main);
const footer = $id('footer');
const inp21 = $id('input21');
inp21.value = '';
browser.appendChild(footer);
browser.removeAttribute('footer-in-header');
s.toolbarList = '.mainbar, .bookmark-bar';
await storage.setMod('toolbarList', s.toolbarList);
getToolbarList(s.toolbarList);
onOptionsMenuPosition(s.positionMenu);
}
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Home button to restart button ***********************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
function getHomeButton() {
const mainbar = $('.mainbar');
const button = $('button[aria-label="Homepage" i][data-name="Home" i]', mainbar);
return button ? { button } : null;
}
function homeToRestart(bol) {
const el = getHomeButton();
if (!el) return;
const { button } = el;
const browser = $id('browser');
const config = {
id: bol ? 'restartButton' : 'homeButton',
title: bol ? 'Restart browser' : 'Go to homepage',
add: bol ? ['custom-button', 'restart-button'] : ['ToolbarButton-Button'],
remove: bol ? ['ToolbarButton-Button'] : ['custom-button', 'restart-button']
};
button.id = config.id;
button.title = config.title;
button.classList.remove(...config.remove);
button.classList.add(...config.add);
button.removeEventListener('dragend', handleDrag);
if (bol) {
button.addEventListener('dragend', handleDrag);
}
if (browser) {
if (bol) browser.setAttribute('home-to-restart', 'true');
else browser.removeAttribute('home-to-restart');
} }
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Move tab ********************************************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
async function moveTab(e) {
const browser = $id('browser');
if (e) {
chrome.tabs.query({currentWindow: true, active: true}, tabs => {
if (tabs[0]) {
chrome.tabs.move(tabs[0].id, {index: 0});
}
});
}
browser?.[s.moveActiveTab ? 'setAttribute' : 'removeAttribute']('move-active-tab', true);
await storage.setMod('moveActiveTab', s.moveActiveTab);
}
function moveTabPosition(e) {
if (!s.moveActiveTab) return;
try {
chrome.tabs.move(e.tabId, {index: 0});
} catch (err) {
if (err === "Error: Tabs can't be edited right now.") setTimeout(() => moveTabPosition(e), Timers_X.moveTabInt);
} }
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Toggle toolbars button hides toolbars checked '✓ Hide ...' ******************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
function setToggleButton() {
const tabs = $id('tabs-container');
const toggle = $c('button', {id: 'toggleToolbars', className: 'ToolbarButton-Button custom-button toggle-toolbars', title: Texts_X.toggleTooltip, onclick: () => { setToolbars(); reloadElements(); if ($id('optionsMenu')) onOptionsMenuPosition(s.positionMenu)}});
try {
tabs.insertBefore(toggle, tabs.firstChild.nextSibling);
removeDupes?.('toggle-toolbars');
} catch (err) {}
}
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Hides the tab bar and also toolbars checked in 'Toggle Toolbars Button ✓ Hide ...' ******************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
$id('input23')?.addEventListener('change', async (e) => {
const newKey = e.target.value;
s.keyCodes = newKey;
await storage.setMod('keyCodes', newKey);
console.log(`[${CURRENT_MOD_ID}] Shortcut updated to: ${newKey}`);
});
$id('input24')?.addEventListener('change', async (e) => {
const newKey2 = e.target.value;
s.keyCodes2 = newKey2;
await storage.setMod('keyCodes2', newKey2);
console.log(`[${CURRENT_MOD_ID}] Shortcut updated to: ${newKey2}`);
});
const tabbarHide = () => {
const browser = $id('browser');
const header = $id('header');
if (header) {
header.hidden = true;
}
};
const tabbarShow = () => {
const browser = $id('browser');
const header = $id('header');
if (header) {
header.hidden = false;
}
if (browser) {
browser.classList.remove('address-top-off');
browser.classList.add('address-top');
}
};
async function initTabbarMod() {
const webView = $id('webview-container');
if (!webView) return;
let isEnabled = await storage.getMod('hideTabbar', s.hideTabbar);
if (!$id('mod-hidden-style')) {
const style = $c('style');
style.id = 'mod-hidden-style';
style.textContent = '[hidden] { display: none !important; }';
document.head.appendChild(style);
}
const toggleTabbar = async () => {
isEnabled = !isEnabled;
await storage.setMod('hideTabbar', isEnabled);
if (isEnabled) {
tabbarHide();
} else {
tabbarShow();
}
const browser = $id('browser');
if (browser) {
isEnabled ? browser.setAttribute('hide-tabbar', 'true') : browser.removeAttribute('hide-tabbar');
}
};
vivaldi.tabsPrivate.onKeyboardShortcut.addListener((id, combo) => {
if (!s.keyCodes || typeof s.keyCodes !== 'string') return;
const normalize = (str) => str.toLowerCase().replace(/\s+/g, '');
const keyList = s.keyCodes.split(',').map(k => normalize(k)).filter(Boolean);
const current = normalize(combo);
if (!keyList.length) return;
const [first, ...rest] = keyList;
if (current === first) {
onOptions();
} else if (rest.includes(current)) {
onOptions();
}
});
vivaldi.tabsPrivate.onKeyboardShortcut.addListener((id, com) => {
if (!s.keyCodes2 || typeof s.keyCodes2 !== 'string') return;
const normalize2 = (str) => str.toLowerCase().replace(/\s+/g, '');
const keyList2 = s.keyCodes2.split(',').map(k => normalize2(k)).filter(Boolean);
const current2 = normalize2(com);
if (!keyList2.length) return;
const [one, ...rest] = keyList2;
if (current2 === one) {
toggleTabbar();
} else if (rest.includes(current2)) {
toggleTabbar();
}
});
if (isEnabled) {
tabbarHide();
browser?.setAttribute('hide-tabbar', 'true');
} }
let tabbarInterval = setInterval(() => {
if ($id('webview-container')) {
clearInterval(tabbarInterval);
initTabbarMod();
}
}, Timers_X.tabbarInt);
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Reloading elements **********************************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
function reloadElements() {
const cal = $id('calendar');
const optBtn = $id('optionsButton');
const optMenu = $id('optionsMenu');
const restartBtn = $id('restart-browser');
const togToolbars = $id('toggleToolbars');
const main = $id('main');
const inner = $('.inner', main);
try {
if (!cal) {
dateHolder();
}
if (!restartBtn && s.homeRestart) {
homeToRestart(s.homeRestart);
}
if (!optBtn) {
setOptionsButton();
}
if (!optMenu) {
setOptionsMenu();
}
if (!togToolbars) {
setToggleButton();
}
} catch (err) {}
}
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// On shutDown *****************************************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
function shutDown() {
const homeBtn = $id('restart-browser');
chrome.tabs.onActivated.removeListener(moveTabPosition);
chrome.tabs.onHighlighted.removeListener(getCurrentTab);
clearInterval(favTimer);
Promise.all(UNPREFIXED_V1.map(key => storage.setMod(key, settings[key])))
.then(() => console.log(`[${CURRENT_MOD_ID}] All settings saved.`))
.catch(err => console.error(`[${CURRENT_MOD_ID}] Save failed:`, err));
if (homeBtn) { homeBtn.removeEventListener('dragend', handleDrag); }
window.removeEventListener('focus', handleFocus);
window.removeEventListener('fullscreenchange', handleLayout);
window.removeEventListener('resize', handleLayout);
window.removeEventListener('load', handleLoad);
window.removeEventListener('unload', shutDown);
}
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Global addListeners and addEventListeners ***********************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
chrome.tabs.onActivated.addListener(e => moveTabPosition(e));
chrome.tabs.onHighlighted.addListener((tabId, changeInfo, tab) => getCurrentTab());
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === 'complete' || changeInfo.favIconUrl) getCurrentTab(); });
window.addEventListener('focus', handleFocus);
window.addEventListener('fullscreenchange', handleLayout);
window.addEventListener('resize', handleLayout);
window.addEventListener('load', handleLoad);
window.addEventListener('unload', shutDown);
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Start initializing **********************************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
initInterval = setInterval(() => {
if (!$id('optionsButton')) {
initialize();
} else {
initialize();
clearInterval(initInterval);
}
}, Timers_X.initializeInt);
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Open in Preview *************************************************************************************************************
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
const KEYCODE_CONFIG = [
{ keys: ['Ctrl+Alt+Period', 'Ctrl+Shift+F'], action: 'searchText' },
{ keys: ['Esc', 'Alt+Period'], action: 'removePreview' },
{ keys: ['Alt+/'], action: 'toggleInput' }
];
const PREVIEW_CONFIG = {
width: 100, // Percentage value
height: 100, // Percentage value
};
const CONTEXT_MENU_CONFIG = {
linkMenuTitle: 'Open in Preview',
searchMenuTitle: 'Search in Preview',
selectSearchMenuTitle: 'Select Search for Preview',
};
const ICON_CONFIG = {
linkIcon: '',
linkIconInteractionOnHover: false,
preview: '🖥️ ', // Alternatives 🔍 🎬 📸
showIconDelay: 100,
showPreviewOnHoverDelay: 100,
};
const TIMING_CONFIG = {
closeTimeout: 800,
fade: 200,
fadeDelay: 200,
middleClickDelay: 400,
optionsHideDelay: 800,
previewDelay: 200,
progressEasing: 0.12,
titleFetchDelay: 2000,
};
const chromeAsync = {
getLastFocusedWindow: () => new Promise(resolve => chrome.windows.getLastFocused(resolve)),
getCurrentWindow: () => new Promise(resolve => chrome.windows.getCurrent(resolve)),
queryTabs: query => new Promise(resolve => chrome.tabs.query(query, resolve)),
removeTab: tabId => new Promise(resolve => chrome.tabs.remove(tabId, resolve)),
getSelectedText: tabId => new Promise(resolve => vivaldi.utilities.getSelectedText(tabId, resolve))
};
let showUrlInput;
async function init() {
document.removeEventListener('DOMContentLoaded', init);
const SHOW_KEY = 'showUrlInput';
if (storage.getMod(SHOW_KEY) === null) {
await storage.setMod(SHOW_KEY, false);
}
let isEnab = await storage.getMod(SHOW_KEY, s.showUrlInput);
showUrlInput = isEnab;
waitForPreview();
}
function waitForPreview() {
const maxAttempts = 50;
let attempts = 0;
const delay = TIMING_CONFIG.previewDelay;
const poll = async () => {
const browser = document.getElementById('browser');
if (browser) {
try {
new PreviewWindow();
return;
} catch (err) {
console.error("Failed to create PreviewWindow:", err);
return;
} }
attempts++;
if (attempts < maxAttempts) {
await new Promise(r => setTimeout(r, delay));
poll();
} else {
console.warn("Browser element not found after maximum attempts");
}
};
poll();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
class PreviewWindow {
rootBrowser = document.getElementById('browser');
#canvasContext = document.createElement('canvas').getContext('2d');
webviews = new Map();
addListener(target, event, handler, options) {
target.addEventListener(event, handler, options);
this._listeners.push({ target, event, handler, options });
return handler;
}
removeAllListeners() {
for (const l of this._listeners) {
l.target.removeEventListener(l.event, l.handler, l.options);
}
this._listeners.length = 0;
}
#iconUtils;
get iconUtils() {
return this.#iconUtils ??= new IconUtils();
}
#renderer;
get renderer() {
return this.#renderer ??= new PreviewRenderer(this);
}
searchEngineUtils = new SearchEngineUtils(
url => this.previewWindow(url),
(engineId, searchText) => this.previewWindowSearch(engineId, searchText),
CONTEXT_MENU_CONFIG
);
READER_VIEW_URL = 'https://www.smry.ai/proxy?url=';
constructor() {
this._listeners = [];
this.KEY_ACTIONS = new Map();
this.setupKeyActions();
this.registerKeyboardListener();
new WebsiteInjectionUtils(
navigationDetails => this.getWebviewConfig(navigationDetails),
(url, fromPanel, origin) => this.previewWindow(url, fromPanel, origin),
ICON_CONFIG
);
window.addEventListener('unload', () => this.cleanupAll());
}
setupKeyActions() {
this.KEY_ACTIONS.clear();
const normalize = (str) => str.toLowerCase().replace(/\s+/g, '');
for (const config of KEYCODE_CONFIG) {
if (!Array.isArray(config.keys)) continue;
config.keys.forEach(keyStr => {
const normalizedKey = normalize(keyStr);
if (!normalizedKey) return;
let actionFn;
switch (config.action) {
case 'searchText':
actionFn = () => this.searchForSelectedText();
break;
case 'removePreview':
actionFn = (viewId) => this.removePreview(viewId);
break;
case 'toggleInput':
actionFn = () => this.toggleInput();
break;
default:
console.warn('Unknown key action:', config.action);
return;
}
this.KEY_ACTIONS.set(normalizedKey, {
fn: actionFn,
requiresViewId: config.action === 'removePreview'
});
});
} }
registerKeyboardListener() {
const listener = (id, combination) => {
if (!combination) return;
const normalize = (str) => str.toLowerCase().replace(/\s+/g, '');
const normalizedCombo = normalize(combination);
const action = this.KEY_ACTIONS.get(normalizedCombo);
if (!action) return;
const webviewValues = Array.from(this.webviews.values());
let webviewData = webviewValues.at(-1);
if (!webviewData.fromPanel) {
const tabId = Number(this.getActiveWebview()?.tab_id);
webviewData = webviewValues.findLast(_data => _data.tabId === tabId);
}
const viewId = webviewData?.webview?.id;
if (action.requiresViewId && viewId) {
action.fn(viewId);
} else {
action.fn();
}
};
vivaldi.tabsPrivate.onKeyboardShortcut.addListener(listener);
this._listeners.push(listener);
}
destroy() {
this._listeners.forEach(listener => {
vivaldi.tabsPrivate.onKeyboardShortcut.removeListener(listener);
});
this._listeners = [];
this.KEY_ACTIONS.clear();
}
getWebviewConfig(navigationDetails) {
if (navigationDetails.frameType !== 'outermost_frame') {
return { webview: null, fromPanel: false };
}
const tabSelector = `webview[tab_id="${navigationDetails.tabId}"]`;
const webview = document.querySelector(tabSelector);
if (webview) {
return { webview, fromPanel: webview.name === 'vivaldi-webpanel' };
}
const panelView = [...this.webviews.values()].find(v => v.fromPanel)?.webview;
if (panelView) {
return { webview: panelView, fromPanel: true };
}
const active = this.getActiveWebview();
const container = active?.closest('.preview-container');
const lastWebviewId = container?.querySelector('webview')?.id;
return { webview: this.webviews.get(lastWebviewId)?.webview, fromPanel: false };
}
getActiveWebview() {
return document.querySelector('.active.visible.webpageview webview');
}
async searchForSelectedText() {
try {
const tabs = await chromeAsync.queryTabs({ active: true, currentWindow: true });
const tab = tabs[0];
if (!tab) return;
let text = await chromeAsync.getSelectedText(tab.id);
if (!text) return;
this.previewWindowSearch(this.searchEngineUtils.defaultSearchId, text);
} catch (e) {
console.error('searchForSelectedText failed:', e);
} }
async previewWindowSearch(engineId, selectionText) {
const searchRequest = await vivaldi.searchEngines.getSearchRequest(engineId, selectionText);
this.previewWindow(searchRequest.url);
}
async toggleInput() {
showUrlInput = !showUrlInput;
await storage.setMod('showUrlInput', showUrlInput);
}
removePreview(webviewId) {
const data = this.webviews.get(webviewId);
if (!data) return;
const container = data.divContainer;
const previewWindow = container?.querySelector('.preview-window');
if (!container || !previewWindow) return;
if (container.dataset.closing === '1') return;
container.dataset.closing = '1';
const pointerX = Number(container.dataset.pointerX ?? window.innerWidth / 2);
const pointerY = Number(container.dataset.pointerY ?? window.innerHeight / 2);
this.setAnchoredTransformVars(previewWindow, pointerX, pointerY);
requestAnimationFrame(() => {
container.classList.remove('is-open');
container.classList.add('is-leave');
container.style.backdropFilter = 'none';
previewWindow.classList.add('animating-close');
const finishRemoval = async () => {
const tabs = await chromeAsync.queryTabs({});
const tab = tabs.find(t =>String(t.vivExtData || '').includes(`${webviewId}tabId`));
if (tab) {
await chromeAsync.removeTab(tab.id);
}
container.classList.remove('is-leave');
data.divContainer.remove();
if (data.tabCloseListener) {
chrome.tabs.onRemoved.removeListener(data.tabCloseListener);
}
if (data.pointerdownListener) {
document.body.removeEventListener('pointerdown', data.pointerdownListener);
}
this.webviews.delete(webviewId);
};
const onCloseEnd = e => {
if (e.animationName === 'preview-window-close-anchored') {
previewWindow.removeEventListener('animationend', onCloseEnd);
finishRemoval();
}
};
previewWindow.addEventListener('animationend', onCloseEnd);
setTimeout(finishRemoval, TIMING_CONFIG.closeTimeout);
});
}
async previewWindow(linkUrl, fromPanel = undefined, origin = undefined) {
let lastFocused, current;
try {
[lastFocused, current] = await Promise.all([
chromeAsync.getLastFocusedWindow(),
chromeAsync.getCurrentWindow()
]);
} catch (err) {
console.error('Failed to get windows:', err);
return;
}
const isValidWindow =
lastFocused.id === current.id &&
lastFocused.state !== chrome.windows.WindowState.MINIMIZED;
if (!isValidWindow) return;
const url = await UrlUtils.normalizeOrSearch(linkUrl, this.searchEngineUtils);
this.showPreview(url, fromPanel, origin);
}
showPreview(linkUrl, fromPanel, origin) {
const webviewId = `dialog-${this.getWebviewId()}`;
const {
previewContainer,
previewWindow,
webview,
optionsContainer,
progressBar
} = this.renderer.createBaseElements(webviewId, linkUrl);
if (fromPanel === undefined && this.webviews.size !== 0) {
fromPanel = Array.from(this.webviews.values()).at(-1).fromPanel;
}
const activeWebview = this.getActiveWebview();
const tabId = !fromPanel && activeWebview ? Number(activeWebview.tab_id) : null;
this.webviews.set(webviewId, {
divContainer: previewContainer,
webview: webview,
fromPanel: fromPanel,
tabId: tabId,
pointerdownListener: null,
pointerdownAttached: false
});
if (!fromPanel) {
const clearWebviews = closedTabId => {
if (tabId === closedTabId) {
this.webviews.forEach((view, key) => view.tabCloseListener === clearWebviews && this.removePreview(key));
chrome.tabs.onRemoved.removeListener(clearWebviews);
}
};
this.webviews.get(webviewId).tabCloseListener = clearWebviews;
chrome.tabs.onRemoved.addListener(clearWebviews);
this._tabListeners ??= new Set();
this._tabListeners.add(clearWebviews);
}
previewWindow.setAttribute('class', 'preview-window');
this.renderer.applyInitialSizing(previewWindow, this.webviews.size);
optionsContainer.setAttribute('class', 'options-container');
let pageTitle = linkUrl;
const fadeDuration = TIMING_CONFIG.fade;
let timeout;
let showingOptions = false;
optionsContainer.textContent = ICON_CONFIG.preview + pageTitle;
optionsContainer.addEventListener('mouseover', () => {
if (!showingOptions) {
optionsContainer.classList.add('fade-out');
setTimeout(() => {
optionsContainer.innerHTML = '';
this.showWebviewOptions(webviewId, optionsContainer);
optionsContainer.classList.remove('fade-out');
showingOptions = true;
const siteUrl = webview.src;
let btn = document.querySelector('.options-button');
let inp = document.querySelector('.url-input');
let len = inp.value.length;
if (siteUrl.includes('youtube.com')) {
if (this.rootBrowser.classList.contains('normal')) {
if (len < 26) inp.style.marginRight = '-10.5vw';
else inp.style.marginRight = '-4vw';
} else {
if (len < 26) el.style.marginRight = '-15.5vw';
else inp.style.marginRight = '-11.70vw';
} }
if (siteUrl.includes('earth.google.com')) {
inp.style.marginRight = '.3vw';
}
}, fadeDuration);
}
clearTimeout(timeout);
});
optionsContainer.addEventListener('mouseleave', () => {
timeout = setTimeout(() => {
optionsContainer.classList.add('fade-out');
setTimeout(() => {
optionsContainer.textContent = ICON_CONFIG.preview + pageTitle;
optionsContainer.classList.remove('fade-out');
showingOptions = false;
}, fadeDuration);
}, TIMING_CONFIG.optionsHideDelay);
});
let currentPageUrl = '';
let titleFetched = false;
webview.id = webviewId;
webview.tab_id = `${webviewId}tabId`;
webview.setAttribute('src', linkUrl);
currentPageUrl = linkUrl;
titleFetched = false;
let isLoading = false;
webview.addEventListener('loadstart', () => {
webview.style.backgroundColor = 'var(--colorBorder)';
progressBar.start();
if (showUrlInput) {
const input = document.getElementById(`input-${webview.id}`);
if (input !== null) {
input.value = webview.src;
} }
isLoading = true;
webview.focus();
});
webview.addEventListener('loadcommit', () => {
titleFetched = false;
progressBar.clear(true);
});
webview.addEventListener('loadstop', () => {
progressBar.clear(true);
const expectedSrc = webview.src;
setTimeout(() => {
let title = '';
try {
if (webview.getTitle) {
title = webview.getTitle();
}
if (!title) {
webview.executeScript({ code: 'document.title' }, (results) => {
if (!results || !results[0]) return;
const resolvedTitle = results[0];
if (webview.src === expectedSrc && resolvedTitle) {
pageTitle = resolvedTitle;
titleFetched = true;
if (!showingOptions) {
optionsContainer.textContent = ICON_CONFIG.preview + pageTitle;
} }
});
} else {
if (webview.src === expectedSrc) {
pageTitle = title;
titleFetched = true;
if (!showingOptions) {
optionsContainer.textContent = ICON_CONFIG.preview + pageTitle;
} } }
} catch (e) {
console.error('Title fetch failed:', e);
}
}, TIMING_CONFIG.titleFetchDelay);
});
previewContainer.setAttribute('class', 'preview-container');
const pointerX = origin?.x ?? window.innerWidth / 2;
const pointerY = origin?.y ?? window.innerHeight / 2;
previewContainer.dataset.pointerX = String(pointerX);
previewContainer.dataset.pointerY = String(pointerY);
const stopEvent = event => {
event.preventDefault();
event.stopPropagation();
if (showUrlInput && event.target.id === `input-${webviewId}`) {
const inputElement = event.target;
const offsetX = event.clientX - inputElement.getBoundingClientRect().left;
this.#canvasContext.font = window.getComputedStyle(inputElement).font;
const text = inputElement.value;
let low = 0;
let high = text.length;
while (low < high) {
const mid = (low + high) >> 1;
const width = this.#canvasContext.measureText(text.slice(0, mid)).width;
if (width < offsetX) low = mid + 1;
else high = mid;
}
const cursorPosition = low;
inputElement.focus({ preventScroll: true });
inputElement.setSelectionRange(cursorPosition, cursorPosition);
}
};
if (fromPanel) {
const boundStopEvent = stopEvent.bind(this);
this.addListener(document.body, 'pointerdown', boundStopEvent);
this.webviews.get(webviewId).pointerdownListener = boundStopEvent;
}
previewContainer.addEventListener('click', event => {
if (event.target === previewContainer) {
this.removePreview(webviewId);
}
});
this.renderer.attachStructure({
previewContainer,
previewWindow,
optionsContainer,
progressBar,
webview
});
this.renderer.mount(previewContainer, fromPanel, this.rootBrowser);
this.renderer.runOpenAnimation(
previewWindow,
previewContainer,
pointerX,
pointerY,
this.setAnchoredTransformVars.bind(this),
TIMING_CONFIG
);
}
setAnchoredTransformVars(previewWindow, viewportX, viewportY, s0 = 0.1) {
const rect = previewWindow.getBoundingClientRect();
const dx = viewportX - rect.left;
const dy = viewportY - rect.top;
const t0x = (1 - s0) * dx;
const t0y = (1 - s0) * dy;
previewWindow.style.setProperty('--s0', String(s0));
previewWindow.style.setProperty('--tx0', `${t0x}px`);
previewWindow.style.setProperty('--ty0', `${t0y}px`);
return { t0x, t0y, s0 };
}
showWebviewOptions(webviewId, thisElement) {
let inputId = `input-${webviewId}`;
let data = this.webviews.get(webviewId);
let webview = data ? data.webview : undefined;
if (webview && document.getElementById(inputId) === null) {
let input = null;
if (showUrlInput) {
input = document.createElement('input');
input.value = webview.src;
input.id = inputId;
input.setAttribute('class', 'url-input');
input.addEventListener('keydown', async event => {
if (event.key === 'Enter') {
let value = input.value;
const resolvedUrl = await UrlUtils.normalizeOrSearch(value, this.searchEngineUtils);
webview.src = resolvedUrl;
}
});
}
const fragment = document.createDocumentFragment();
const kb1 = ' ( Keyboard: ';
const kb2 = ' )';
const removePreviewKeys = kb1 + KEYCODE_CONFIG[1].keys.join(', ') + kb2;
const toggleInputKeys = kb1 + KEYCODE_CONFIG[2].keys.join(', ') + kb2;
const buttons = [
{ content: this.iconUtils.back,
action: () => webview.back(),
cls: 'back-button',
tooltip: 'Back' },
{ content: this.iconUtils.forward,
action: () => webview.forward(),
cls: 'forward-button',
tooltip: 'Forward' },
{ content: this.iconUtils.reload,
action: () => webview.reload(),
cls: 'reload-button',
tooltip: 'Reload page' },
{ content: this.iconUtils.readerView,
action: this.showReaderView.bind(this, webview),
cls: 'reader-button',
tooltip: 'Toggle Reader View' },
{ content: this.iconUtils.newTab,
action: () =>
showUrlInput
? this.openNewTab(inputId, true)
: this.openNewTabFromWebview(webview, true),
cls: 'newtab-button',
tooltip: 'Open in new tab' },
{ content: this.iconUtils.backgroundTab,
action: () =>
showUrlInput
? this.openNewTab(inputId, false)
: this.openNewTabFromWebview(webview, false),
cls: 'background-button',
tooltip: 'Open in background tab' },
{ content: this.iconUtils.toggleInput,
action: () => this.toggleInput(),
cls: 'toggle-input',
tooltip: 'Toggle url-input' + toggleInputKeys },
{ content: this.iconUtils.closeBtn,
action: () => this.removePreview(webviewId),
cls: 'close-button',
tooltip: 'Close preview' + removePreviewKeys }
];
buttons.forEach(button =>
fragment.appendChild(
this.createOptionsButton(
button.content,
button.action,
button.cls || '',
button.tooltip
)
)
);
if (input) fragment.appendChild(input);
thisElement.append(fragment);
} }
createOptionsButton(content, clickListenerCallback, cls = '', tooltip = '') {
const button = document.createElement('button');
button.className = `options-button ${cls}`.trim();
button.addEventListener('click', clickListenerCallback);
if (tooltip) {
button.dataset.tooltip = tooltip;
}
if (typeof content === 'string') {
button.innerHTML = content;
} else {
button.appendChild(content);
}
return button;
}
getWebviewId() {
const timestamp = Date.now();
const randomPart = Math.random().toString(36).substring(2, 11);
return `${timestamp}-${randomPart}`;
}
showReaderView(webview) {
const previewWindow = webview.parentElement;
if (webview.src.includes(this.READER_VIEW_URL)) {
webview.src = webview.src.replace(this.READER_VIEW_URL, '');
previewWindow.classList.remove('reader-open');
} else {
webview.src = this.READER_VIEW_URL + webview.src;
previewWindow.classList.add('reader-open');
} }
async openNewTab(inputId, active) {
const input = document.getElementById(inputId).value;
const url = await UrlUtils.normalizeOrSearch(input, this.searchEngineUtils);
chrome.tabs.create({ url, active });
}
openNewTabFromWebview(webview, active) {
chrome.tabs.create({ url: webview.src, active });
}
cleanupAll() {
this.removeAllListeners();
if (this._tabListeners) {
for (const fn of this._tabListeners) {
chrome.tabs.onRemoved.removeListener(fn);
}
this._tabListeners.clear();
}
this.webviews.clear();
webview.remove();
container.remove();
this.webviews.delete(webviewId);
if (this.lastWebviewId === webviewId) {
this.lastWebviewId = Array.from(this.webviews.keys()).at(-1)?? null;
} } }
class PreviewRenderer {
constructor(context) {
this.ctx = context;
}
createBaseElements(webviewId, linkUrl) {
const previewContainer = document.createElement('div');
const previewWindow = document.createElement('div');
const webview = document.createElement('webview');
const optionsContainer = document.createElement('div');
const progressBar = new ProgressBar(webviewId);
previewWindow.className = 'preview-window';
optionsContainer.className = 'options-container';
previewContainer.className = 'preview-container';
webview.id = webviewId;
webview.tab_id = `${webviewId}tabId`;
webview.setAttribute('src', linkUrl);
return {
previewContainer,
previewWindow,
webview,
optionsContainer,
progressBar
};
}
attachStructure({ previewContainer, previewWindow, optionsContainer, progressBar, webview }) {
previewWindow.appendChild(optionsContainer);
previewWindow.appendChild(progressBar.element);
previewWindow.appendChild(webview);
previewContainer.appendChild(previewWindow);
}
mount(previewContainer, fromPanel, rootBrowser) {
(fromPanel
? (rootBrowser || document.querySelector('#browser'))
: document.querySelector('.active.visible.webpageview')
).appendChild(previewContainer);
}
async applyInitialSizing(previewWindow, stackIndex) {
previewWindow.style.width = PREVIEW_CONFIG.width * stackIndex + '%';
previewWindow.style.height = PREVIEW_CONFIG.height * stackIndex + '%';
previewWindow.style.visibility = 'hidden';
}
runOpenAnimation(previewWindow, previewContainer, pointerX, pointerY, setAnchoredTransformVars, durations) {
requestAnimationFrame(() => {
const t = setAnchoredTransformVars(previewWindow, pointerX, pointerY);
Object.assign(previewWindow.style, {
transform: `translate(${t.t0x}px, ${t.t0y}px) scale(${t.s0})`,
opacity: '0',
visibility: 'visible'
});
requestAnimationFrame(() => {
previewWindow.getBoundingClientRect();
requestAnimationFrame(() => {
previewContainer.classList.add('is-open');
});
});
requestAnimationFrame(() => {
previewContainer.classList.add('is-open');
setTimeout(() => {
previewWindow.classList.add('animating-open');
const onOpenEnd = e => {
if (e.animationName === 'preview-window-open-anchored') {
previewWindow.classList.remove('animating-open');
previewWindow.style.removeProperty('transform');
previewWindow.style.removeProperty('opacity');
previewWindow.removeEventListener('animationend', onOpenEnd);
}
};
previewWindow.addEventListener('animationend', onOpenEnd);
}, durations.fadeDelay);
});
});
} }
class UrlUtils {
static VALID_PREFIXES = [
'http://',
'https://',
'file://',
'vivaldi://',
'chrome://',
'chrome-extension://',
'data:',
'blob:'
];
static BLOCKED_SCHEMES = [
'javascript:',
'vbscript:'
];
static isValid(url) {
if (!url || typeof url !== 'string') return false;
const trimmed = url.trim().toLowerCase();
if (this.BLOCKED_SCHEMES.some(s => trimmed.startsWith(s))) {
return false;
}
if (trimmed.startsWith('about:')) return true;
return this.VALID_PREFIXES.some(prefix => trimmed.startsWith(prefix));
}
static async normalizeOrSearch(input, searchEngineUtils) {
if (this.isValid(input)) {
return input;
}
const searchRequest = await vivaldi.searchEngines.getSearchRequest(
searchEngineUtils.defaultSearchId,
input
);
return searchRequest.url;
} }
class WebsiteInjectionUtils {
constructor(getWebviewConfig, openPreview, iconConfig) {
this.iconConfig = JSON.stringify(iconConfig);
chrome.webNavigation.onCompleted.addListener(navigationDetails => {
const { webview, fromPanel } = getWebviewConfig(navigationDetails);
webview && this.injectCode(webview, fromPanel);
});
chrome.runtime.onMessage.addListener(message => {
if (message.url) {
openPreview(message.url, message.fromPanel, message.origin);
}
});
}
injectCode(webview, fromPanel) {
const handler = WebsiteLinkInteractionHandler.toString();
const instantiationCode = `
if (window.__dialogHandlerInitialized) return;
window.__dialogHandlerInitialized = true;
window.__previewInjectedCleanupRun = () => {
window.__previewInjectedCleanup?.forEach(fn => fn());
window.__previewInjectedCleanup?.clear?.();
};
window.addEventListener('beforeunload', () => {
window.__previewInjectedCleanupRun?.();
});
window.addEventListener('pagehide', () => {
window.__previewInjectedCleanupRun?.();
});
new (${handler})(${fromPanel}, ${this.iconConfig});
`;
try {
webview.executeScript({ code: instantiationCode }, () => {
if (chrome.runtime.lastError) {
console.debug('Preview mod: Script injection failed:', chrome.runtime.lastError.message);
}
});
} catch (error) {
console.debug('Preview mod: Failed to execute script:', error);
} } }
class WebsiteLinkInteractionHandler {
constructor(fromPanel, config) {
window.__previewInjectedCleanup ??= new Set();
this.fromPanel = fromPanel;
this.config = config;
this.icon = null;
this.timers = { showIcon: null, showPreview: null, hideIcon: null };
this.boundHideIcon = this.#hideLinkIcon.bind(this);
this.#initialize();
}
#initialize() {
this.#setupMouseHandling();
if (this.config.linkIcon) {
this.#setupIconHandling();
} }
#setupMouseHandling() {
let holdTimerForMiddleClick;
const pointerDownHandler = event => {
if (event.ctrlKey && event.altKey && [0, 1].includes(event.button)) {
this.#callPreview(event);
} else if (event.button === 1) {
const link = this.#getLinkElement(event);
if (!link) return;
const px = event.clientX;
const py = event.clientY;
const href = link.href;
holdTimerForMiddleClick = setTimeout(() => {
this.#sendPreviewMessage(href, px, py);
}, TIMING_CONFIG.middleClickDelay);
}
};
const pointerUpHandler = event => {
if (event.button === 1) clearTimeout(holdTimerForMiddleClick);
};
document.addEventListener('pointerdown', pointerDownHandler);
document.addEventListener('pointerup', pointerUpHandler);
window.__previewInjectedCleanup ??= new Set();
window.__previewInjectedCleanup.add(() => {
document.removeEventListener('pointerdown', pointerDownHandler);
document.removeEventListener('pointerup', pointerUpHandler);
});
}
#setupIconHandling() {
this.#createIcon();
this.#createIconStyle();
document.addEventListener(
'mouseover',
this.debounce(event => {
const link = this.#getLinkElement(event);
if (!link) return;
clearTimeout(this.timers.hideIcon);
requestAnimationFrame(() => {
const rect = link.getBoundingClientRect();
Object.assign(this.icon.style, {
display: 'block',
left: `${rect.right + 5}px`,
top: `${rect.top + window.scrollY}px`
});
});
this.icon.dataset.targetUrl = link.href;
this.currentLinkEl = link;
link.addEventListener('mouseleave', this.boundHideIcon);
}, this.config.showIconDelay)
);
}
#createIcon() {
const icon = document.createElement('div');
icon.className = `link-icon ${this.config.linkIcon}`;
icon.style.display = 'none';
const getLinkCenter = () => {
const el = this.currentLinkEl;
if (el) {
const r = el.getBoundingClientRect();
return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2) };
}
return { x: Math.round(window.innerWidth / 2), y: Math.round(window.innerHeight / 2) };
};
if (this.config.linkIconInteractionOnHover) {
icon.addEventListener('mouseenter', () => {
this.timers.showPreview = setTimeout(() => {
const { x, y } = getLinkCenter();
this.#sendPreviewMessage(this.icon.dataset.targetUrl, x, y);
}, this.config.showPreviewOnHoverDelay);
});
icon.addEventListener('mouseleave', () => clearTimeout(this.timers.showPreview));
} else {
icon.addEventListener('click', () => {
const { x, y } = getLinkCenter();
this.#sendPreviewMessage(this.icon.dataset.targetUrl, x, y);
});
icon.addEventListener('mouseenter', () => clearTimeout(this.timers.hideIcon));
this.boundHideIcon = this.#hideLinkIcon.bind(this);
icon.addEventListener('mouseleave', this.boundHideIcon);
}
this.icon = icon;
document.body.appendChild(this.icon);
}
#hideLinkIcon() {
this.timers.hideIcon = setTimeout(
() => {
this.icon.style.display = 'none';
clearTimeout(this.timers.showIcon);
},
this.config.linkIconInteractionOnHover ? 300 : 600
);
}
#getLinkElement(event) {
return event.target.closest('a[href]:not([href="#"])');
}
#sendPreviewMessage(url, x, y) {
chrome.runtime.sendMessage({ url, fromPanel: this.fromPanel, origin: { x, y } });
}
#callPreview(event) {
let link = this.#getLinkElement(event);
if (link) {
event.preventDefault();
this.#sendPreviewMessage(link.href, event.clientX, event.clientY);
} }
#createIconStyle() {
const style = document.createElement('style');
style.textContent = `
.link-icon {
position: absolute;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
cursor: pointer;
z-index: 9999;
transition: opacity 0.2s ease;
}
.link-icon:hover {
opacity: 0.9;
}
`;
document.head.appendChild(style);
}
debounce(fn, delay) {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(fn.bind(this, ...args), delay);
};
} }
class SearchEngineUtils {
constructor(openLinkCallback, searchCallback, config = {}) {
this.openLinkCallback = openLinkCallback;
this.searchCallback = searchCallback;
this.linkMenuTitle = config.linkMenuTitle;
this.searchMenuTitle = config.searchMenuTitle;
this.selectSearchMenuTitle = config.selectSearchMenuTitle;
this.createdContextMenuMap = new Map();
this.searchEngineCollection = [];
this.defaultSearchId = null;
this.privateSearchId = null;
this.LINK_ID = 'preview-window-link';
this.SEARCH_ID = 'search-preview-window';
this.SELECT_SEARCH_ID = 'select-search-preview-window';
this.#initialize();
}
async #initialize() {
this.#createContextMenuOption();
this.#updateSearchEnginesAndContextMenu();
vivaldi.searchEngines.onTemplateUrlsChanged.addListener(() => {
this.#removeContextMenuSelectSearch();
this.#updateSearchEnginesAndContextMenu();
});
}
#createContextMenuOption() {
chrome.contextMenus.create({
id: this.LINK_ID,
title: `${this.linkMenuTitle}`,
contexts: ['link']
});
chrome.contextMenus.create({
id: this.SEARCH_ID,
title: `${this.searchMenuTitle}`,
contexts: ['selection']
});
chrome.contextMenus.create({
id: this.SELECT_SEARCH_ID,
title: `${this.selectSearchMenuTitle}`,
contexts: ['selection']
});
chrome.contextMenus.onClicked.addListener(itemInfo => {
const { menuItemId, parentMenuItemId, linkUrl, selectionText } = itemInfo;
if (menuItemId === this.LINK_ID) {
this.openLinkCallback(linkUrl);
} else if (menuItemId === this.SEARCH_ID) {
const engineId = window.incognito ? this.privateSearchId : this.defaultSearchId;
this.searchCallback(engineId, selectionText);
} else if (parentMenuItemId === this.SELECT_SEARCH_ID) {
const engineId = menuItemId.substr(parentMenuItemId.length);
this.searchCallback(engineId, selectionText);
}
});
}
async #updateSearchEnginesAndContextMenu() {
const searchEngines = await vivaldi.searchEngines.getTemplateUrls();
this.searchEngineCollection = searchEngines.templateUrls;
this.defaultSearchId = searchEngines.defaultSearch;
this.privateSearchId = searchEngines.defaultPrivate;
this.#createContextMenuSelectSearch();
}
#removeContextMenuSelectSearch() {
this.createdContextMenuMap.forEach((_, engineId) => {
const menuId = this.SELECT_SEARCH_ID + engineId;
chrome.contextMenus.remove(menuId);
});
this.createdContextMenuMap.clear();
}
#createContextMenuSelectSearch() {
this.searchEngineCollection.forEach(engine => {
if (!this.createdContextMenuMap.has(engine.guid)) {
chrome.contextMenus.create({
id: this.SELECT_SEARCH_ID + engine.guid,
parentId: this.SELECT_SEARCH_ID,
title: engine.name,
contexts: ['selection']
});
this.createdContextMenuMap.set(engine.guid, true);
}
});
} }
class ProgressBar {
static CLEAR_DELAY = 250;
constructor(webviewId) {
this.webviewId = webviewId;
this.progress = 0;
this.element = this.#createProgressBar(webviewId);
this._raf = null;
}
#createProgressBar(webviewId) {
const el = document.createElement('div');
el.className = 'progress-bar';
el.id = `progressBar-${webviewId}`;
return el;
}
start() {
this.element.style.visibility = 'visible';
this.element.classList.remove('is-complete');
this.progress = 0;
this.element.style.width = '0%';
this.#animateTo(85);
}
#animateTo(target) {
cancelAnimationFrame(this._raf);
const step = () => {
this.progress += (target - this.progress) * TIMING_CONFIG.progressEasing;
this.element.style.width = `${this.progress.toFixed(2)}%`;
if (this.progress < target - 0.5) {
this._raf = requestAnimationFrame(step);
}
};
this._raf = requestAnimationFrame(step);
}
clear(loadStop = false) {
cancelAnimationFrame(this._raf);
this.element.classList.add('is-complete');
if (loadStop) {
this.element.style.width = '100%';
setTimeout(() => {
this.progress = 0;
this.element.style.visibility = 'hidden';
this.element.style.width = '0%';
}, ProgressBar.CLEAR_DELAY);
}
}
destroy() {
cancelAnimationFrame(this._raf);
this._raf = null;
} }
class IconUtils {
static SVG = {
readerView:
'',
newTab:
'',
backgroundTab:
'',
toggleInput:
'',
closeBtn:
'',
};
static VIVALDI_BUTTONS = [
{
name: 'back',
buttonName: 'Back',
fallback:
''
},
{
name: 'forward',
buttonName: 'Forward',
fallback:
''
},
{
name: 'reload',
buttonName: 'Reload',
fallback:
''
}
];
#initialized = false;
#iconMap = new Map();
constructor() {
this.#initializeStaticIcons();
}
#initializeStaticIcons() {
Object.entries(IconUtils.SVG).forEach(([key, value]) => {
this.#iconMap.set(key, value);
});
}
#initializeVivaldiIcons() {
if (this.#initialized) return;
IconUtils.VIVALDI_BUTTONS.forEach(button => {
this.#iconMap.set(button.name, this.#getVivaldiButton(button.buttonName, button.fallback));
});
this.#initialized = true;
}
#getVivaldiButton(buttonName, fallbackSVG) {
const svg = document.querySelector(`.button-toolbar [data-name="${buttonName}"] svg`);
return svg ? svg.cloneNode(true).outerHTML : fallbackSVG;
}
getIcon(name) {
if (!this.#initialized && IconUtils.VIVALDI_BUTTONS.some(btn => btn.name === name)) {
this.#initializeVivaldiIcons();
}
return this.#iconMap.get(name) || '';
}
get back() {
return this.getIcon('back');
}
get forward() {
return this.getIcon('forward');
}
get reload() {
return this.getIcon('reload');
}
get readerView() {
return this.getIcon('readerView');
}
get newTab() {
return this.getIcon('newTab');
}
get backgroundTab() {
return this.getIcon('backgroundTab');
}
get toggleInput() {
return this.getIcon('toggleInput');
}
get closeBtn() {
return this.getIcon('closeBtn');
} }
})();