Files
v-iole-t--tab-harbor/extension/chrome-tab-groups-import.js
T
2026-07-13 12:34:04 +08:00

198 lines
7.0 KiB
JavaScript

'use strict';
(function attachChromeTabGroupImport(globalScope) {
const {
addSessionGroup,
normalizeSessionGroups,
} = globalScope.TabOutSessionGroups || {};
const EMPTY_META = {
entries: [],
};
function normalizeChromeImportedGroupMeta(input) {
const entries = Array.isArray(input?.entries)
? input.entries
.filter(entry => entry && entry.sessionGroupId)
.map(entry => ({
sessionGroupId: String(entry.sessionGroupId),
chromeGroupId: entry.chromeGroupId == null ? null : Number(entry.chromeGroupId),
windowId: entry.windowId == null ? 0 : Number(entry.windowId),
title: String(entry.title || 'Group').trim() || 'Group',
color: String(entry.color || 'grey'),
}))
: [];
return { entries };
}
function buildMetaSignature(entry) {
return [
String(entry.windowId ?? 0),
String(entry.title || 'Group').trim() || 'Group',
String(entry.color || 'grey'),
].join('::');
}
function buildUniqueGroupName(baseName, groups, excludeGroupId = '') {
const fallbackName = String(baseName || 'Group').trim() || 'Group';
const lowerFallback = fallbackName.toLowerCase();
const takenNames = new Set(
(groups || [])
.filter(group => group && group.id !== excludeGroupId)
.map(group => String(group.name || '').trim().toLowerCase())
.filter(Boolean)
);
if (!takenNames.has(lowerFallback)) return fallbackName;
let suffix = 2;
while (takenNames.has(`${lowerFallback} ${suffix}`)) suffix++;
return `${fallbackName} ${suffix}`;
}
function findReusableSessionGroupId(groups, assignments, nativeGroup) {
const nativeTabIds = Array.isArray(nativeGroup?.tabIds)
? nativeGroup.tabIds.map(tabId => String(tabId)).filter(Boolean).sort()
: [];
if (!nativeTabIds.length) return '';
const nativeTitle = String(nativeGroup?.title || '').trim().toLowerCase();
for (const group of groups || []) {
const groupId = String(group?.id || '');
if (!groupId) continue;
const assignedTabIds = Object.entries(assignments || {})
.filter(([, assignedGroupId]) => String(assignedGroupId) === groupId)
.map(([tabId]) => String(tabId))
.sort();
if (!assignedTabIds.length || assignedTabIds.length !== nativeTabIds.length) continue;
if (!assignedTabIds.every((tabId, index) => tabId === nativeTabIds[index])) continue;
const groupName = String(group?.name || '').trim().toLowerCase();
if (!nativeTitle || !groupName || nativeTitle === groupName) {
return groupId;
}
}
return '';
}
function reconcileChromeTabGroupImports({
currentState,
importedMeta,
nativeGroups,
}) {
if (typeof normalizeSessionGroups !== 'function' || typeof addSessionGroup !== 'function') {
throw new Error('Session group helpers are unavailable');
}
const normalizedState = normalizeSessionGroups(currentState);
const normalizedMeta = normalizeChromeImportedGroupMeta(importedMeta);
const managedIds = new Set(normalizedMeta.entries.map(entry => entry.sessionGroupId));
const sessionGroupIds = new Set(normalizedState.groups.map(group => group.id));
let groups = normalizedState.groups.filter(group => !managedIds.has(group.id) || sessionGroupIds.has(group.id));
let assignments = {};
for (const [tabId, groupId] of Object.entries(normalizedState.assignments)) {
if (managedIds.has(groupId)) continue;
assignments[tabId] = groupId;
}
const metaByChromeGroupId = new Map();
const metaBySignature = new Map();
for (const entry of normalizedMeta.entries) {
if (!sessionGroupIds.has(entry.sessionGroupId)) continue;
if (entry.chromeGroupId != null) metaByChromeGroupId.set(entry.chromeGroupId, entry);
metaBySignature.set(buildMetaSignature(entry), entry);
}
const nextMetaEntries = [];
const mappings = [];
for (const nativeGroup of (nativeGroups || [])) {
const chromeGroupId = nativeGroup?.chromeGroupId == null ? null : Number(nativeGroup.chromeGroupId);
const windowId = nativeGroup?.windowId == null ? 0 : Number(nativeGroup.windowId);
const title = String(nativeGroup?.title || 'Group').trim() || 'Group';
const color = String(nativeGroup?.color || 'grey');
const tabIds = Array.isArray(nativeGroup?.tabIds)
? nativeGroup.tabIds.filter(tabId => tabId != null).map(tabId => String(tabId))
: [];
if (tabIds.length === 0) continue;
const signature = buildMetaSignature({ windowId, title, color });
const existingMeta = (chromeGroupId != null && metaByChromeGroupId.get(chromeGroupId))
|| metaBySignature.get(signature)
|| null;
let sessionGroupId = existingMeta?.sessionGroupId || '';
let groupIndex = groups.findIndex(group => group.id === sessionGroupId);
if (groupIndex === -1) {
sessionGroupId = findReusableSessionGroupId(groups, normalizedState.assignments, nativeGroup);
groupIndex = groups.findIndex(group => group.id === sessionGroupId);
}
if (groupIndex === -1) {
const uniqueName = buildUniqueGroupName(title, groups);
const created = addSessionGroup({ groups, assignments }, uniqueName);
groups = created.state.groups;
assignments = created.state.assignments;
sessionGroupId = created.group.id;
groupIndex = groups.findIndex(group => group.id === sessionGroupId);
} else {
const uniqueName = buildUniqueGroupName(title, groups, sessionGroupId);
if (groups[groupIndex].name !== uniqueName) {
groups = groups.map(group => group.id === sessionGroupId
? { ...group, name: uniqueName }
: group);
}
}
for (const tabId of tabIds) {
assignments[tabId] = sessionGroupId;
}
if (chromeGroupId != null) {
mappings.push({
virtualGroupKey: `__session_group__:${sessionGroupId}`,
windowId,
chromeGroupId,
});
}
nextMetaEntries.push({
sessionGroupId,
chromeGroupId,
windowId,
title,
color,
});
}
const activeManagedIds = new Set(nextMetaEntries.map(entry => entry.sessionGroupId));
groups = groups.filter(group => !managedIds.has(group.id) || activeManagedIds.has(group.id));
assignments = Object.fromEntries(
Object.entries(assignments).filter(([, groupId]) => !managedIds.has(groupId) || activeManagedIds.has(groupId))
);
return {
state: normalizeSessionGroups({ groups, assignments }),
importedMeta: normalizeChromeImportedGroupMeta({ entries: nextMetaEntries }),
mappings,
};
}
const api = {
EMPTY_META,
normalizeChromeImportedGroupMeta,
reconcileChromeTabGroupImports,
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = api;
}
globalScope.TabOutChromeTabGroupImport = api;
})(typeof globalThis !== 'undefined' ? globalThis : window);