// ---------------------------------------
// 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
| # | name | id | version | state | |
${rows}
`;
}
// ---------------------------------------
// 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(/