// --------------------------------------- // portage by pvrz // github: https://github.com/pvrzz/portage // file name: popup.js // --------------------------------------- // --------------------------------------- // state // --------------------------------------- const state = { type: "extensions", mode: "installed", selectedIds: new Set(), expandedDomains: new Set(), data: { extensionsInstalled: [], extensionsImported: [], cookiesInstalled: {}, cookiesImported: {}, }, }; // --------------------------------------- // constants // --------------------------------------- const WEBSTORE_BASE = "https://chromewebstore.google.com/detail"; const SELF_ID = chrome.runtime.id; const PBKDF2_ITERATIONS = 250000; const COOKIES_FORMAT = "portage-cookies"; const COOKIES_VERSION = 1; const EXTENSIONS_FORMAT = "portage-extensions"; // --------------------------------------- // dom refs // --------------------------------------- const $ = (id) => document.getElementById(id); const els = { list: $("ext-list"), emptyState: $("empty-state"), emptyTitle: $("empty-title"), emptySub: $("empty-sub"), subtitle: $("subtitle"), selectedCount: $("selected-count"), primaryBtn: $("primary-btn"), primaryLabel: $("primary-label"), statusEl: $("status"), importInput: $("import-input"), modal: $("modal-backdrop"), modalTitle: $("modal-title"), modalDesc: $("modal-desc"), modalPass: $("modal-passphrase"), modalPassConfirm: $("modal-passphrase-confirm"), modalSkipWrap: $("modal-skip-encrypt-wrap"), modalSkip: $("modal-skip-encrypt"), modalWarn: $("modal-warn"), modalConfirm: $("modal-confirm"), modalCancel: $("modal-cancel"), modalClose: $("modal-close"), }; // --------------------------------------- // initialization // --------------------------------------- document.addEventListener("DOMContentLoaded", async () => { await loadExtensions(); await loadCookies(); attachEventListeners(); render(); }); // --------------------------------------- // data loading // --------------------------------------- async function loadExtensions() { const all = await chrome.management.getAll(); state.data.extensionsInstalled = all .filter((ext) => ext.type === "extension" && ext.id !== SELF_ID) .sort((a, b) => a.name.localeCompare(b.name)); } async function loadCookies() { const all = await chrome.cookies.getAll({}); state.data.cookiesInstalled = groupCookiesByDomain(all); } function groupCookiesByDomain(cookies) { const grouped = {}; for (const c of cookies) { const key = c.domain.startsWith(".") ? c.domain.slice(1) : c.domain; if (!grouped[key]) grouped[key] = []; grouped[key].push(c); } const sorted = {}; for (const k of Object.keys(grouped).sort()) { sorted[k] = grouped[k].sort((a, b) => a.name.localeCompare(b.name)); } return sorted; } // --------------------------------------- // current view helpers // --------------------------------------- function currentList() { if (state.type === "extensions") { return state.mode === "installed" ? state.data.extensionsInstalled : state.data.extensionsImported; } else { return state.mode === "installed" ? state.data.cookiesInstalled : state.data.cookiesImported; } } function currentItemIds() { const list = currentList(); if (state.type === "extensions") { return list.map((e) => e.id); } else { return Object.keys(list); } } // --------------------------------------- // render // --------------------------------------- function render() { updateSubtitle(); updateTabsActive(); updatePrimaryButton(); updateImportAccept(); const list = currentList(); const empty = state.type === "extensions" ? list.length === 0 : Object.keys(list).length === 0; if (empty) { els.list.innerHTML = ""; els.emptyState.classList.remove("hidden"); updateEmptyState(); } else { els.emptyState.classList.add("hidden"); if (state.type === "extensions") { renderExtensionList(list); } else { renderCookieGroups(list); } } updateSelectionUi(); } function updateSubtitle() { const list = currentList(); if (state.type === "extensions") { els.subtitle.textContent = `${list.length} ${state.mode === "installed" ? "installed" : "imported"}`; } else { const domainCount = Object.keys(list).length; let cookieCount = 0; for (const k of Object.keys(list)) cookieCount += list[k].length; els.subtitle.textContent = `${cookieCount} cookies · ${domainCount} domains`; } } function updateTabsActive() { document.querySelectorAll(".type-tab").forEach((tab) => { tab.classList.toggle("active", tab.dataset.type === state.type); }); document.querySelectorAll(".mode-tab").forEach((tab) => { tab.classList.toggle("active", tab.dataset.mode === state.mode); }); } function updatePrimaryButton() { const hasSelection = state.selectedIds.size > 0; els.primaryBtn.disabled = !hasSelection; if (state.type === "extensions") { els.primaryLabel.textContent = "open in tab group"; } else if (state.mode === "installed") { els.primaryLabel.textContent = "export selected"; } else { els.primaryLabel.textContent = "apply to browser"; } } function updateImportAccept() { els.importInput.accept = state.type === "extensions" ? ".html,.htm" : ".json"; } function updateEmptyState() { if (state.type === "extensions") { els.emptyTitle.textContent = state.mode === "imported" ? "no imported extensions" : "no extensions found"; els.emptySub.textContent = state.mode === "imported" ? "import an html export to see it here" : "you have no installable extensions"; } else { els.emptyTitle.textContent = state.mode === "imported" ? "no imported cookies" : "no cookies found"; els.emptySub.textContent = state.mode === "imported" ? "import a cookies json file to see them here" : "your browser has no cookies stored"; } } function renderExtensionList(list) { els.list.innerHTML = ""; for (const ext of list) { const li = document.createElement("li"); li.className = "ext-item"; li.dataset.id = ext.id; if (state.selectedIds.has(ext.id)) li.classList.add("selected"); const iconUrl = getBestIcon(ext); const isDisabled = ext.enabled === false; const isSideload = ext.installType && ext.installType !== "normal" && ext.installType !== "admin"; li.innerHTML = ` ${iconUrl ? `` : `
`}
${escapeHtml(ext.name)}
${escapeHtml(ext.id)} ${isDisabled ? 'off' : ""} ${isSideload ? 'sideload' : ""}
`; li.addEventListener("click", (e) => { if (e.target.classList.contains("ext-checkbox")) return; toggleSelection(ext.id); }); li.querySelector(".ext-checkbox").addEventListener("change", () => { toggleSelection(ext.id); }); els.list.appendChild(li); } } function renderCookieGroups(grouped) { els.list.innerHTML = ""; for (const domain of Object.keys(grouped)) { const cookies = grouped[domain]; const isSelected = state.selectedIds.has(domain); const isExpanded = state.expandedDomains.has(domain); const groupLi = document.createElement("li"); groupLi.className = "domain-group"; const header = document.createElement("div"); header.className = "domain-header"; if (isSelected) header.classList.add("selected"); header.dataset.domain = domain; header.innerHTML = ` ${escapeHtml(domain)} ${cookies.length} `; header.addEventListener("click", (e) => { if (e.target.classList.contains("ext-checkbox")) return; if (e.target.classList.contains("domain-chevron") || e.target.closest(".domain-chevron")) { toggleDomainExpansion(domain); } else { toggleSelection(domain); } }); header.querySelector(".ext-checkbox").addEventListener("change", () => { toggleSelection(domain); }); groupLi.appendChild(header); if (isExpanded) { const cookieList = document.createElement("ul"); cookieList.className = "cookie-list"; for (const c of cookies) { const cookieLi = document.createElement("li"); cookieLi.className = "cookie-item"; const flags = []; if (c.secure) flags.push('s'); if (c.httpOnly) flags.push('h'); if (c.session) flags.push('session'); const truncatedValue = c.value.length > 30 ? c.value.slice(0, 30) + "…" : c.value; cookieLi.innerHTML = ` ${escapeHtml(c.name)} ${escapeHtml(truncatedValue)} ${flags.join("")} `; cookieList.appendChild(cookieLi); } groupLi.appendChild(cookieList); } els.list.appendChild(groupLi); } } function getBestIcon(ext) { if (!ext.icons || ext.icons.length === 0) return null; return (ext.icons.find((i) => i.size === 48) || ext.icons.find((i) => i.size === 32) || ext.icons[ext.icons.length - 1]).url; } // --------------------------------------- // selection // --------------------------------------- function toggleSelection(id) { if (state.selectedIds.has(id)) { state.selectedIds.delete(id); } else { state.selectedIds.add(id); } render(); } function toggleDomainExpansion(domain) { if (state.expandedDomains.has(domain)) { state.expandedDomains.delete(domain); } else { state.expandedDomains.add(domain); } render(); } function selectAll() { state.selectedIds = new Set(currentItemIds()); render(); } function selectNone() { state.selectedIds.clear(); render(); } function selectInvert() { const ids = currentItemIds(); const next = new Set(); for (const id of ids) { if (!state.selectedIds.has(id)) next.add(id); } state.selectedIds = next; render(); } function updateSelectionUi() { els.selectedCount.textContent = state.selectedIds.size; updatePrimaryButton(); } // --------------------------------------- // primary action dispatcher // --------------------------------------- async function handlePrimaryAction() { if (state.type === "extensions") { await openSelectedInTabGroup(); } else if (state.mode === "installed") { await exportCookies(); } else { await applyCookies(); } } // --------------------------------------- // extension tab group action // --------------------------------------- async function openSelectedInTabGroup() { const list = currentList(); const selected = list.filter((e) => state.selectedIds.has(e.id)); if (selected.length === 0) return; if (selected.length > 10) { if (!confirm(`open ${selected.length} tabs at once?`)) return; } showStatus(`opening ${selected.length} tab${selected.length === 1 ? "" : "s"}...`); try { const tabs = []; for (const ext of selected) { const tab = await chrome.tabs.create({ url: `${WEBSTORE_BASE}/${ext.id}`, active: false, }); tabs.push(tab); } const groupId = await chrome.tabs.group({ tabIds: tabs.map((t) => t.id) }); await chrome.tabGroups.update(groupId, { title: state.mode === "imported" ? `portage: migrate ${selected.length}` : `portage (${selected.length})`, color: "cyan", collapsed: false, }); showStatus(`opened ${tabs.length} tabs in a group`, "success"); } catch (err) { console.error(err); showStatus(`error: ${err.message}`, "error"); } } // --------------------------------------- // export dispatcher // --------------------------------------- function handleExport() { if (state.type === "extensions") { exportExtensionsToHtml(); } else { exportCookies(); } } // --------------------------------------- // extension export // --------------------------------------- function exportExtensionsToHtml() { const list = currentList(); const toExport = state.selectedIds.size > 0 ? list.filter((e) => state.selectedIds.has(e.id)) : list; if (toExport.length === 0) { showStatus("nothing to export", "error"); return; } const exportData = toExport.map((ext) => ({ id: ext.id, name: ext.name, version: ext.version || "", enabled: ext.enabled !== false, homepageUrl: ext.homepageUrl || "", })); const html = buildExportHtml(exportData); downloadBlob(new Blob([html], { type: "text/html" }), `portage-extensions-${dateStamp()}.html`); showStatus(`exported ${toExport.length} extensions`, "success"); } function buildExportHtml(data) { const jsonBlob = JSON.stringify({ type: EXTENSIONS_FORMAT, version: 1, exportedAt: new Date().toISOString(), extensions: data, }, null, 2); const rows = data.map((ext, i) => ` ${i + 1} ${escapeHtml(ext.name)} ${escapeHtml(ext.id)} ${escapeHtml(ext.version)} ${ext.enabled ? "" : "off"} install → `).join(""); return ` portage export

portage export

${data.length} extension${data.length === 1 ? "" : "s"} · exported ${new Date().toLocaleString()}

re-import this file in portage to use the tab-group flow
${rows}
#nameidversionstate
`; } // --------------------------------------- // cookie export // --------------------------------------- async function exportCookies() { if (state.selectedIds.size === 0) { showStatus("select at least one domain", "error"); return; } const grouped = state.data.cookiesInstalled; const collected = []; for (const domain of state.selectedIds) { if (grouped[domain]) collected.push(...grouped[domain]); } if (collected.length === 0) { showStatus("no cookies in selection", "error"); return; } openModal({ title: "encrypt cookies?", description: `${collected.length} cookies from ${state.selectedIds.size} domain${state.selectedIds.size === 1 ? "" : "s"} will be exported. encryption is strongly recommended — this file contains your login sessions.`, mode: "encrypt", onConfirm: async ({ passphrase, skip }) => { const payload = { type: COOKIES_FORMAT, version: COOKIES_VERSION, exportedAt: new Date().toISOString(), encrypted: !skip, }; if (skip) { payload.cookies = collected; } else { try { const encrypted = await encryptJson({ cookies: collected }, passphrase); Object.assign(payload, encrypted); } catch (err) { showStatus(`encryption failed: ${err.message}`, "error"); return; } } const json = JSON.stringify(payload, null, 2); downloadBlob(new Blob([json], { type: "application/json" }), `portage-cookies-${dateStamp()}.json`); showStatus(`exported ${collected.length} cookies${skip ? " (unencrypted)" : ""}`, "success"); }, }); } // --------------------------------------- // import dispatcher // --------------------------------------- function handleImport(file) { if (state.type === "extensions") { importExtensionsFromHtml(file); } else { importCookiesFromJson(file); } } // --------------------------------------- // extension import // --------------------------------------- function importExtensionsFromHtml(file) { const reader = new FileReader(); reader.onload = (e) => { try { const html = e.target.result; const match = html.match(/