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 * ╔══╗─────────╔╗────╔═╗╔═╗╔╗───╔══╗╔═╗╔═╗╔═╦╗╔══╗──╔═╗╔═╗╔═╗ * ╚╗╔╝╔╦╗╔═╗╔═╗║║─╔═╗║═╣║═╣║╚╦╦╗╚╗╔╝║╬║║║║║║║║║╔═╣──║╬║║╬║║║║ * ─║║─║║║║╬║║╩╣║╚╗║╩╣╠═║╠═║║╬║║║─║║─║╗╣║║║║║║║║╚╗║╔╗║╔╝║╗╣║║║ * ─╚╝─╠╗║║╔╝╚═╝╚═╝╚═╝╚═╝╚═╝╚═╬╗║─╚╝─╚╩╝╚═╝╚╩═╝╚══╝╚╝╚╝─╚╩╝╚═╝ * ────╚═╝╚╝──────────────────╚═╝ */