console.log(`%c
╔════╗─────────────╔╗───────────────╔╗──────╔════╗╔═══╗╔═══╗╔═╗─╔╗╔═══╗──╔═══╗╔═══╗╔═══╗
║╔╗╔╗║─────────────║║───────────────║║──────║╔╗╔╗║║╔═╗║║╔═╗║║║╚╗║║║╔═╗║──║╔═╗║║╔═╗║║╔═╗║
╚╝║║╚╝╔╗─╔╗╔══╗╔══╗║║───╔══╗╔══╗╔══╗║╚═╦╗─╔╗╚╝║║╚╝║╚═╝║║║─║║║╔╗╚╝║║║─╚╝──║╚═╝║║╚═╝║║║─║║
──║║──║║─║║║╔╗║║║═╣║║─╔╗║║═╣║══╣║══╣║╔╗║║─║║──║║──║╔╗╔╝║║─║║║║╚╗║║║║╔═╗──║╔══╝║╔╗╔╝║║─║║
──║║──║╚═╝║║╚╝║║║═╣║╚═╝║║║═╣╠══║╠══║║╚╝║╚═╝║──║║──║║║╚╗║╚═╝║║║─║║║║╚╩═║╔╗║║───║║║╚╗║╚═╝║
──╚╝──╚═╗╔╝║╔═╝╚══╝╚═══╝╚══╝╚══╝╚══╝╚══╩═╗╔╝──╚╝──╚╝╚═╝╚═══╝╚╝─╚═╝╚═══╝╚╝╚╝───╚╝╚═╝╚═══╝
──────╔═╝║─║║──────────────────────────╔═╝║
──────╚══╝─╚╝──────────────────────────╚══╝
TypeLess - Auto Form Filler
v1.0.5 by TRONG.PRO
`, 'color: #667eea; font-weight: bold;');
// Popup script for profile management
document.addEventListener('DOMContentLoaded', async () => {
// Initialize i18n
await i18n.init();
translateUI();
// ── Detect restricted page and show warning banner ─────────────────────
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
const restrictedPrefixes = ['chrome://', 'edge://', 'about:', 'brave://', 'opera://'];
// Also block other extensions' pages, but NOT our own (e.g. options.html)
const ownExtOrigin = chrome.runtime.getURL('');
const isRestricted = activeTab && activeTab.url && (
restrictedPrefixes.some(p => activeTab.url.startsWith(p)) ||
(activeTab.url.startsWith('chrome-extension://') && !activeTab.url.startsWith(ownExtOrigin)) ||
(activeTab.url.startsWith('moz-extension://') && !activeTab.url.startsWith(ownExtOrigin))
);
if (isRestricted) {
const banner = document.getElementById('restricted-banner');
if (banner) {
banner.style.display = 'flex';
// Show human-readable URL type
const url = activeTab.url || '';
const protocol = url.split('://')[0] + '://';
const msgEl = banner.querySelector('#restricted-msg');
if (msgEl) {
// Store protocol for translateUI to use
msgEl.dataset.protocol = protocol;
msgEl.removeAttribute('data-i18n'); // use dynamic rendering instead
const rawMsg = i18n.t('popup.restricted_msg_protocol') || i18n.t('popup.restricted_msg');
msgEl.textContent = rawMsg.replace('{protocol}', protocol);
}
}
// Dim action buttons that won't work
const unavailableTitle = i18n.t('popup.restricted_unavailable');
['showToolbar', 'btn-unlock-right-click', 'btn-save-html',
'btn-screenshot-full', 'btn-screenshot'].forEach(id => {
const el = document.getElementById(id);
if (el) { el.disabled = true; el.style.opacity = '0.4'; el.title = unavailableTitle; }
});
}
// ─────────────────────────────────────────────────────────────────────────
// Listen for language changes
chrome.storage.onChanged.addListener(async (changes, area) => {
if (area === 'local' && changes.language) {
const newLang = changes.language.newValue;
if (newLang && newLang !== i18n.currentLang) {
await i18n.setLocale(newLang); // reload translation JSON
translateUI();
}
}
});
initTabs();
await loadProfiles();
// New features
initUtilities();
await initUserAgent();
attachEventListeners();
});
// Translate UI elements
function translateUI() {
const t = (key) => i18n.t(key);
// Translate elements with data-i18n attribute
// Uses TrustedHTML.setHTML — translation strings are internal/trusted, not user data.
document.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.getAttribute('data-i18n');
if (typeof TrustedHTML !== 'undefined') {
TrustedHTML.setHTML(element, t(key));
} else {
element.innerHTML = t(key);
}
});
// Translate placeholders
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
const key = element.getAttribute('data-i18n-placeholder');
element.placeholder = t(key);
});
// Translate titles
document.querySelectorAll('[data-i18n-title]').forEach(element => {
const key = element.getAttribute('data-i18n-title');
element.title = t(key);
});
// Re-render restricted banner message (dynamic: includes protocol token)
const msgEl = document.getElementById('restricted-msg');
if (msgEl && msgEl.dataset.protocol) {
const rawMsg = t('popup.restricted_msg_protocol') || t('popup.restricted_msg');
msgEl.textContent = rawMsg.replace('{protocol}', msgEl.dataset.protocol);
}
// Re-render disabled button titles for restricted pages
const unavailableTitle = t('popup.restricted_unavailable');
['showToolbar', 'btn-unlock-right-click', 'btn-save-html',
'btn-screenshot-full', 'btn-screenshot'].forEach(id => {
const el = document.getElementById(id);
if (el && el.disabled) el.title = unavailableTitle;
});
}
// Load and display all profiles
// --- Tab Switching Logic ---
function initTabs() {
const tabs = document.querySelectorAll('.tab');
const tabContents = document.querySelectorAll('.tab-content');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// Remove active class from all
tabs.forEach(t => t.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
// Add active class to clicked
tab.classList.add('active');
const tabId = tab.dataset.tab;
document.getElementById(`${tabId}-tab`).classList.add('active');
});
});
}
async function loadProfiles() {
const container = document.getElementById('profilesContainer');
const profiles = await StorageManager.getProfiles();
const t = (key, params) => i18n.t(key, params);
container.textContent = ''; // safe clear
if (profiles.length === 0) {
// Static layout — no user data, TrustedHTML is fine here
if (typeof TrustedHTML !== 'undefined') {
TrustedHTML.setHTML(container, `
${t('popup.empty')}
${t('popup.empty_hint')}
`);
} else {
container.innerHTML = `
${t('popup.empty')}
${t('popup.empty_hint')}
`;
}
return;
}
// ── Build profile cards imperatively — no innerHTML for user data ──────
const db = typeof DOMBuilder !== 'undefined' ? DOMBuilder : null;
const _mkIconImg = (name, extraStyle) => {
const img = document.createElement('img');
img.src = `icons/${name}.svg`;
img.className = 'icon-img';
if (extraStyle) img.style.cssText = extraStyle;
return img;
};
profiles.forEach(profile => {
const item = document.createElement('div');
item.className = 'tl-profile-item';
item.dataset.profileId = profile.id;
item.draggable = true;
// ── Header row ──
const header = document.createElement('div');
header.className = 'profile-header';
const drag = document.createElement('span');
drag.className = 'tl-drag-handle';
drag.title = 'Kéo để sắp xếp';
drag.textContent = '⠿';
const nameWrap = document.createElement('div');
nameWrap.className = 'tl-profile-name-wrap';
const nameSpan = document.createElement('span');
nameSpan.className = 'profile-name';
nameSpan.id = `pname-${profile.id}`;
nameSpan.textContent = profile.name; // XSS-safe: textContent
const nameEdit = document.createElement('input');
nameEdit.className = 'tl-profile-name-edit';
nameEdit.id = `pedit-${profile.id}`;
nameEdit.value = profile.name; // XSS-safe: .value property
nameEdit.style.display = 'none';
nameEdit.spellcheck = false;
nameEdit.autocomplete = 'off';
nameWrap.appendChild(nameSpan);
nameWrap.appendChild(nameEdit);
const actions = document.createElement('div');
actions.className = 'profile-actions';
const _mkBtn = (cls, profileId, titleKey, titleFallback, ...children) => {
const btn = document.createElement('button');
btn.className = `tl-icon-btn ${cls}`;
btn.dataset.profileId = profileId;
btn.title = t(titleKey) || titleFallback;
children.forEach(c => btn.appendChild(c));
return btn;
};
const renameBtn = _mkBtn('tl-rename-btn', profile.id, 'btn.rename_tooltip', 'Đổi tên');
renameBtn.textContent = '✏️';
const applyBtn = _mkBtn('apply-btn', profile.id, 'btn.apply_tooltip', 'Apply');
applyBtn.appendChild(_mkIconImg('play'));
const copyBtn = _mkBtn('copy-btn', profile.id, 'btn.copy_tooltip', 'Copy');
copyBtn.appendChild(_mkIconImg('copy'));
const deleteBtn = _mkBtn('delete-btn', profile.id, 'btn.delete_profile', 'Delete');
deleteBtn.appendChild(_mkIconImg('delete'));
actions.appendChild(renameBtn);
actions.appendChild(applyBtn);
actions.appendChild(copyBtn);
actions.appendChild(deleteBtn);
header.appendChild(drag);
header.appendChild(nameWrap);
header.appendChild(actions);
// ── Info row ──
const info = document.createElement('div');
info.className = 'profile-info';
const fieldCount = document.createElement('span');
fieldCount.appendChild(_mkIconImg('description', 'width:12px;height:12px;'));
fieldCount.appendChild(document.createTextNode(' ' + t('field.count', { count: profile.fields?.length || 0 })));
info.appendChild(fieldCount);
if (profile.createdAt) {
const dateSpan = document.createElement('span');
dateSpan.appendChild(_mkIconImg('calendar', 'width:12px;height:12px;'));
const locale = i18n.currentLang === 'vi' ? 'vi-VN' : 'en-US';
dateSpan.appendChild(document.createTextNode(' ' + new Date(profile.createdAt).toLocaleDateString(locale)));
info.appendChild(dateSpan);
}
// ── URL row ──
item.appendChild(header);
item.appendChild(info);
if (profile.url) {
const urlDiv = document.createElement('div');
urlDiv.className = 'profile-url';
urlDiv.title = profile.url; // XSS-safe: .title property
urlDiv.appendChild(_mkIconImg('link', 'width:12px;height:12px;vertical-align:middle'));
urlDiv.appendChild(document.createTextNode(' ' + profile.url)); // XSS-safe: text node
item.appendChild(urlDiv);
}
container.appendChild(item);
});
// ── Drag-and-drop reorder ─────────────────────────────────────────────
let _dndSrcId = null;
container.querySelectorAll('.tl-profile-item').forEach(item => {
item.addEventListener('dragstart', (e) => {
_dndSrcId = item.dataset.profileId;
item.classList.add('tl-is-dragging');
e.dataTransfer.effectAllowed = 'move';
});
item.addEventListener('dragend', () => {
item.classList.remove('tl-is-dragging');
container.querySelectorAll('.tl-profile-item').forEach(i => {
i.classList.remove('tl-drag-over-top', 'tl-drag-over-bottom');
});
});
item.addEventListener('dragover', (e) => {
e.preventDefault();
if (item.dataset.profileId === _dndSrcId) return;
const rect = item.getBoundingClientRect();
const midY = rect.top + rect.height / 2;
container.querySelectorAll('.tl-profile-item').forEach(i => i.classList.remove('tl-drag-over-top', 'tl-drag-over-bottom'));
item.classList.add(e.clientY < midY ? 'tl-drag-over-top' : 'tl-drag-over-bottom');
});
item.addEventListener('drop', async (e) => {
e.preventDefault();
if (!_dndSrcId || _dndSrcId === item.dataset.profileId) return;
container.querySelectorAll('.tl-profile-item').forEach(i => i.classList.remove('tl-drag-over-top', 'tl-drag-over-bottom'));
const allItems = Array.from(container.querySelectorAll('.tl-profile-item'));
const allIds = allItems.map(i => i.dataset.profileId);
const srcIdx = allIds.indexOf(_dndSrcId);
const dstIdx = allIds.indexOf(item.dataset.profileId);
// Determine insert position
const rect = item.getBoundingClientRect();
const insertBefore = e.clientY < rect.top + rect.height / 2;
// Reorder all profiles (global) — preserve other-page profiles order
const all = await StorageManager.getProfiles();
const globalIds = all.map(p => p.id);
const srcGlobal = globalIds.indexOf(_dndSrcId);
globalIds.splice(srcGlobal, 1); // remove src
const dstGlobal = globalIds.indexOf(item.dataset.profileId);
globalIds.splice(insertBefore ? dstGlobal : dstGlobal + 1, 0, _dndSrcId);
await StorageManager.reorderProfiles(globalIds);
_dndSrcId = null;
await loadProfiles();
// Notify toolbar to refresh
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) safeMsg(tab.id, { action: 'refreshProfiles' });
});
});
// ─────────────────────────────────────────────────────────────────────
// ── profile action buttons are handled by a single delegated listener
// attached once in attachEventListeners() — nothing to add here
}
// Copy a single profile
async function copyProfile(profileId) {
const t = (key) => i18n.t(key);
const profile = await StorageManager.getProfile(profileId);
if (!profile) return;
try {
await navigator.clipboard.writeText(JSON.stringify(profile, null, 2));
showNotification(t('notify.copied'));
} catch (err) {
console.error('Failed to copy: ', err);
showNotification(t('alert.error_copy'), 'error');
}
}
// ── Inline rename ─────────────────────────────────────────────────────────────
// Switches the profile-name to an , saves on Enter or blur.
let _activeRenameId = null; // guard: only one rename active at a time
function startRename(profileId, renameBtn) {
if (_activeRenameId && _activeRenameId !== profileId) {
cancelRename(_activeRenameId);
}
if (_activeRenameId === profileId) return; // already open
_activeRenameId = profileId;
const nameSpan = document.getElementById(`pname-${profileId}`);
const nameInput = document.getElementById(`pedit-${profileId}`);
if (!nameSpan || !nameInput) return;
// Switch to edit mode
nameSpan.style.display = 'none';
nameInput.style.display = 'block';
nameInput.value = nameSpan.textContent;
nameInput.focus();
nameInput.select();
// Change rename btn to ✔ confirm
renameBtn.textContent = '✔';
renameBtn.title = 'Lưu tên (Enter)';
renameBtn.classList.add('tl-rename-confirm');
const onConfirm = (e) => {
// If blur fired because user clicked the rename/confirm button,
// skip here — the delegated 'click' handler will call confirmRename.
if (e.type === 'blur' && e.relatedTarget === renameBtn) return;
confirmRename(profileId, renameBtn);
};
const onKeydown = (e) => {
if (e.key === 'Enter') { e.preventDefault(); onConfirm(e); }
if (e.key === 'Escape') { cancelRename(profileId, renameBtn); }
};
nameInput._onConfirm = onConfirm;
nameInput._onKeydown = onKeydown;
nameInput.addEventListener('blur', onConfirm);
nameInput.addEventListener('keydown', onKeydown);
// NOTE: no onclick override — delegation checks .tl-rename-confirm class instead
}
function cancelRename(profileId, renameBtn) {
_activeRenameId = null;
const nameSpan = document.getElementById(`pname-${profileId}`);
const nameInput = document.getElementById(`pedit-${profileId}`);
if (!nameSpan || !nameInput) return;
nameInput.style.display = 'none';
nameSpan.style.display = '';
if (nameInput._onConfirm) nameInput.removeEventListener('blur', nameInput._onConfirm);
if (nameInput._onKeydown) nameInput.removeEventListener('keydown', nameInput._onKeydown);
if (renameBtn) {
renameBtn.textContent = '✏️';
renameBtn.title = i18n.t('btn.rename_tooltip') || 'Đổi tên';
renameBtn.classList.remove('tl-rename-confirm');
}
}
async function confirmRename(profileId, renameBtn) {
if (_activeRenameId !== profileId) return; // already cancelled or different
_activeRenameId = null;
const nameSpan = document.getElementById(`pname-${profileId}`);
const nameInput = document.getElementById(`pedit-${profileId}`);
if (!nameSpan || !nameInput) return;
// Remove listeners before any await so they don't re-fire
if (nameInput._onConfirm) nameInput.removeEventListener('blur', nameInput._onConfirm);
if (nameInput._onKeydown) nameInput.removeEventListener('keydown', nameInput._onKeydown);
const newName = nameInput.value.trim();
if (newName && newName !== nameSpan.textContent) {
const ok = await StorageManager.renameProfile(profileId, newName);
if (ok) {
nameSpan.textContent = newName;
showNotification(`✏️ Đổi tên → "${escapeHtml(newName)}"`);
// Notify content script to refresh toolbar dropdown
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) safeMsg(tab.id, { action: 'refreshProfiles' });
} else {
showNotification(i18n.t('alert.error_save') || 'Lỗi lưu tên', 'error');
}
}
// Restore display
nameInput.style.display = 'none';
nameSpan.style.display = '';
if (renameBtn) {
renameBtn.textContent = '✏️';
renameBtn.title = i18n.t('btn.rename_tooltip') || 'Đổi tên';
renameBtn.classList.remove('tl-rename-confirm');
}
}
/**
* Safe wrapper for chrome.tabs.sendMessage.
* Silently swallows "Receiving end does not exist" errors that occur on
* restricted pages (chrome://, edge://, about:, file://) where content
* scripts cannot run.
*/
function safeMsg(tabId, msg, callback) {
if (!tabId) return;
try {
chrome.tabs.sendMessage(tabId, msg, (response) => {
if (chrome.runtime.lastError) {
// Suppress connection errors on restricted pages
const err = chrome.runtime.lastError.message || '';
if (!err.includes('Receiving end does not exist') &&
!err.includes('Could not establish connection') &&
!err.includes('Extension context invalidated')) {
console.warn('[TypeLess popup] sendMessage:', err);
}
}
if (callback) callback(response);
});
} catch (e) {
// chrome.tabs.sendMessage itself can throw on restricted tabs
if (callback) callback(undefined);
}
}
function attachEventListeners() {
const t = (key, params) => i18n.t(key, params);
// ── Single delegated listener for ALL profile card buttons ────────────────
// Lives here (not in loadProfiles) so it is attached exactly once,
// regardless of how many times the profile list is re-rendered.
const profilesContainer = document.getElementById('profilesContainer');
if (profilesContainer) {
profilesContainer.addEventListener('click', (e) => {
const btn = e.target.closest('.tl-icon-btn[data-profile-id]');
if (!btn) return;
const id = btn.dataset.profileId;
if (btn.classList.contains('apply-btn')) { applyProfile(id); }
else if (btn.classList.contains('copy-btn')) { copyProfile(id); }
else if (btn.classList.contains('delete-btn')) { deleteProfile(id); }
else if (btn.classList.contains('tl-rename-btn')) {
if (btn.classList.contains('tl-rename-confirm')) {
confirmRename(id, btn);
} else {
startRename(id, btn);
}
}
});
}
// Open Options Page
document.getElementById('btn-open-options')?.addEventListener('click', () => {
if (chrome.runtime.openOptionsPage) {
chrome.runtime.openOptionsPage();
} else {
window.open(chrome.runtime.getURL('options.html'));
}
});
// Donation
document.getElementById('copy-usdt')?.addEventListener('click', (e) => {
e.preventDefault();
navigator.clipboard.writeText('0x5E5b642C979401C3138379C314d1E229bDD31070');
// Simple tooltip or alert
const originalText = e.target.textContent;
e.target.textContent = i18n.t('notify.copied');
setTimeout(() => {
e.target.textContent = originalText;
}, 1500);
});
// Language switcher
document.getElementById('langBtn')?.addEventListener('click', async () => {
await i18n.switchLanguage();
translateUI();
});
document.getElementById('btn-ua-settings')?.addEventListener('click', () => {
chrome.runtime.openOptionsPage();
});
// Show/Hide Toolbar button (Toggle visibility)
document.getElementById('showToolbar')?.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
safeMsg(tab.id, { action: 'toggleHideToolbar' }, (response) => {
if (chrome.runtime.lastError) {
showNotification('Error: ' + escapeHtml(chrome.runtime.lastError.message), 'error');
}
});
});
// --- Tools Tab Actions ---
// --- Tools Tab Actions ---
// Unlock Right Click
document.getElementById('btn-unlock-right-click')?.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
safeMsg(tab.id, { action: 'triggerEnableRightClick' });
closeAndNotify(t('notify.right_click_enabled'));
});
// Copy Profiles (Backup All)
document.getElementById('btn-copy-profiles')?.addEventListener('click', async () => {
const backupData = await StorageManager.getBackupData();
try {
await navigator.clipboard.writeText(JSON.stringify(backupData, null, 2));
showNotification(t('notify.copied_all'));
} catch (err) {
console.error('Failed to copy: ', err);
showNotification(t('alert.error_copy'), 'error');
}
});
// Paste Profiles
document.getElementById('btn-paste-profiles')?.addEventListener('click', async () => {
try {
const text = await navigator.clipboard.readText();
let importedData;
try {
importedData = JSON.parse(text);
} catch (e) {
throw new Error('Invalid JSON');
}
const result = await StorageManager.restoreBackupData(importedData);
if (result.count > 0 || result.settingsRestored) {
const lines = [
`📋 ${t('notify.pasted')}`,
`+${result.added} ${t('notify.added_new') || 'mới'}`,
`↺${result.updated} ${t('notify.updated') || 'cập nhật'}`,
`⚠️${result.skipped} ${t('notify.skipped') || 'bỏ qua'}`,
];
showNotification(lines.join('
'));
await loadProfiles();
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) safeMsg(tab.id, { action: 'refreshProfiles' });
} else {
if (result.error) throw new Error(result.error);
showNotification(t('alert.no_fields'), 'error');
}
} catch (e) {
showNotification(t('alert.invalid_json'), 'error');
}
});
// Export Profiles
document.getElementById('btn-export-profiles')?.addEventListener('click', async () => {
const backupData = await StorageManager.getBackupData();
const blob = new Blob([JSON.stringify(backupData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const stamp = new Date().toISOString().replace('T', '_').replace(/:/g, '-').substring(0, 19);
a.download = `typeless-backup-${stamp}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification(t('notify.exported', { count: backupData.profiles?.length ?? 0 }));
});
// Save Rendered HTML
document.getElementById('btn-save-html')?.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) return;
safeMsg(tab.id, { action: 'getRenderedHTML' }, (response) => {
if (response && response.html) {
const blob = new Blob([response.html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const filename = (tab.title || 'page').replace(/[^a-z0-9]/gi, '_').substring(0, 50) + '.html';
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
});
closeAndNotify(t('notify.html_saved') || 'HTML saved to Downloads');
});
// Import Profiles
document.getElementById('btn-import-profiles')?.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
const importedData = JSON.parse(text);
const result = await StorageManager.restoreBackupData(importedData);
showNotification(t('notify.imported', {
added: result.added ?? result.count ?? 0,
updated: result.updated ?? 0,
skipped: result.skipped ?? 0
}));
await loadProfiles();
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) safeMsg(tab.id, { action: 'refreshProfiles' });
} catch (error) {
console.error(error);
showNotification(t('alert.error_import'), 'error');
}
};
input.click();
});
}
// Apply a profile to the current page
async function applyProfile(profileId) {
const t = (key, params) => i18n.t(key, params);
const profile = await StorageManager.getProfile(profileId);
if (!profile) {
showNotification(t('alert.profile_not_found'), 'error');
return;
}
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab || !tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('edge://') || tab.url.startsWith('about:')) {
showNotification(t('alert.page_not_supported'), 'error');
return;
}
safeMsg(tab.id, {
action: 'fillForm',
profile: profile
}, (response) => {
if (chrome.runtime.lastError) {
showNotification(t('alert.error_apply'), 'error');
} else {
// Apply shouldn't use system notification, user didn't ask for it.
// But let's check user request again.
// "hãy đóng popup ngay sau khi người dùng bấm các nút chức năng chỉ hiển thị text thông báo: +Utilities... +User Agent..."
// It seems "Utilities" list and "User Agent" list are the target.
// Apply Profile is not in that list.
showNotification(`
${t('notify.applied', { name: escapeHtml(profile.name) })}`);
setTimeout(() => window.close(), 1000);
}
});
}
// Delete a profile
async function deleteProfile(profileId) {
const t = (key, params) => i18n.t(key, params);
const profile = await StorageManager.getProfile(profileId);
if (!profile) return;
const confirmed = confirm(t('confirm.delete', { name: profile.name }));
if (!confirmed) return;
const success = await StorageManager.deleteProfile(profileId);
if (success) {
showNotification(`
${t('notify.deleted', { name: escapeHtml(profile.name) })}`);
await loadProfiles();
// Notify content script to refresh its list too
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) {
safeMsg(tab.id, { action: 'refreshProfiles' });
}
} else {
showNotification(t('alert.error_delete'), 'error');
}
}
// Show notification in popup
function showNotification(message, type = 'success') {
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
toast.className = 'toast';
document.body.appendChild(toast);
}
//toast.textContent = message; // Use textContent (not innerHTML) to prevent XSS
toast.innerHTML = message; // Use innerHTML to render icon images. Because the data is from an internal file (a trusted source).
toast.className = `toast show ${type}`;
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// Close popup and show in-page notification on the active tab
function closeAndNotify(message) {
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
if (tab && tab.id) {
safeMsg(tab.id, { action: 'showNotification', message }); // fire-and-forget; safeMsg handles errors internally
}
});
window.close();
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Initialize Utilities (CRX, Screenshot)
*/
function initUtilities() {
// Screenshot Full
document.getElementById('btn-screenshot-full')?.addEventListener('click', () => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs[0];
if (tab) {
chrome.runtime.sendMessage({ action: 'captureFullScreenshot', tabId: tab.id });
window.close();
}
});
});
// Screenshot Visible
document.getElementById('btn-screenshot')?.addEventListener('click', async () => {
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) {
chrome.runtime.sendMessage({ action: 'captureVisibleScreenshotAndSave', tabId: tab.id });
window.close();
}
} catch (e) {
console.error(e);
}
});
}
/**
* Generate default User Agents based on current browser version
*/
function getDefaultUserAgents() {
// Extract Chrome version from current UA
const match = navigator.userAgent.match(/Chrome\/(\d+\.\d+\.\d+\.\d+)/);
const chromeVersion = match ? match[1] : '130.0.0.0';
// Extract major version for Apple WebKit/Version (simplification)
const majorVersion = chromeVersion.split('.')[0];
return {
// Android 17 - Samsung Galaxy S25 Ultra
'ua_android': `Mozilla/5.0 (Linux; Android 16; SM-S938B Build/BP2A.250605.031.A3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Mobile Safari/537.36`,
// iOS 26.3 - iPhone 17 Pro Max
'ua_ios': `Mozilla/5.0 (iPhone; CPU iPhone OS 26_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.3 Mobile/15E148 Chrome/${chromeVersion} Safari/604.1`,
'ua_macos': `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`,
'ua_windows': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`,
'ua_linux': `Mozilla/5.0 (Linux x86_64; Ubuntu 25.04) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`
};
}
/**
* Initialize User Agent Switcher
*/
async function initUserAgent() {
const uaStatus = document.getElementById('ua-status');
const defaults = getDefaultUserAgents();
// Load custom UAs from storage
const settings = await chrome.storage.local.get(Object.keys(defaults));
const uas = {
'default': '', // Clear rule = use browser default
'android': settings.ua_android || defaults.ua_android,
'ios': settings.ua_ios || defaults.ua_ios,
'macos': settings.ua_macos || defaults.ua_macos,
'windows': settings.ua_windows || defaults.ua_windows,
'linux': settings.ua_linux || defaults.ua_linux
};
// Get current tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) return;
// Helper: apply highlight to whichever button matches currentUA
function highlightActive(currentUA) {
let anyActive = false;
document.querySelectorAll('.ua-btn').forEach(btn => {
const type = btn.dataset.ua;
// "default" is active when no UA override is stored (empty string)
const isActive = (type === 'default')
? !currentUA
: (!!currentUA && currentUA === uas[type]);
btn.style.background = isActive ? 'var(--primary-color, #667eea)' : '';
btn.style.color = isActive ? 'white' : '';
if (isActive) {
anyActive = true;
if (uaStatus) {
uaStatus.textContent = type === 'default'
? 'Using default User-Agent'
: `Active: ${type}`;
}
}
});
// Safety net: if nothing matched always fall back to "Default"
if (!anyActive) {
const defaultBtn = document.querySelector('.ua-btn[data-ua="default"]');
if (defaultBtn) {
defaultBtn.style.background = 'var(--primary-color, #667eea)';
defaultBtn.style.color = 'white';
if (uaStatus) uaStatus.textContent = 'Using default User-Agent';
}
}
}
// STEP 1 — Attach click handlers unconditionally, before any async call.
// BUG FIX: previously all handlers were wired inside the getUserAgent callback,
// so if that response was null/error the buttons were permanently unresponsive.
document.querySelectorAll('.ua-btn').forEach(btn => {
const type = btn.dataset.ua;
btn.addEventListener('click', () => {
const targetUA = uas[type];
const label = type === 'default'
? 'User-Agent reset to Default'
: `User-Agent switched to ${type.charAt(0).toUpperCase() + type.slice(1)}`;
// BUG FIX: wait for background ACK before reloading.
// Previously chrome.tabs.reload() fired immediately after sendMessage(),
// causing a race where the tab loaded before updateSessionRules() finished,
// so the new UA rule was not yet active on that first navigation.
chrome.runtime.sendMessage(
{ action: 'setUserAgent', tabId: tab.id, userAgent: targetUA },
() => {
chrome.tabs.reload(tab.id);
closeAndNotify(label);
}
);
});
});
// STEP 2 — Optimistic UI: show "Default" selected immediately.
highlightActive('');
// STEP 3 — Fetch real state and correct highlight if needed.
chrome.runtime.sendMessage({ action: 'getUserAgent', tabId: tab.id }, (response) => {
if (chrome.runtime.lastError || !response || response.error) return;
highlightActive(response.userAgent || '');
});
}
/**
* End of popup.js
* ╔══╗─────────╔╗────╔═╗╔═╗╔╗───╔══╗╔═╗╔═╗╔═╦╗╔══╗──╔═╗╔═╗╔═╗
* ╚╗╔╝╔╦╗╔═╗╔═╗║║─╔═╗║═╣║═╣║╚╦╦╗╚╗╔╝║╬║║║║║║║║║╔═╣──║╬║║╬║║║║
* ─║║─║║║║╬║║╩╣║╚╗║╩╣╠═║╠═║║╬║║║─║║─║╗╣║║║║║║║║╚╗║╔╗║╔╝║╗╣║║║
* ─╚╝─╠╗║║╔╝╚═╝╚═╝╚═╝╚═╝╚═╝╚═╬╗║─╚╝─╚╩╝╚═╝╚╩═╝╚══╝╚╝╚╝─╚╩╝╚═╝
* ────╚═╝╚╝──────────────────╚═╝
*/