-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshared.js
More file actions
126 lines (108 loc) · 4.28 KB
/
shared.js
File metadata and controls
126 lines (108 loc) · 4.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
export const BRAND_MARK = '⧉'; // 使用零宽空格作为隐形标识
export const GROUP_PREFIX = `${BRAND_MARK}`;
export const MANAGER_PAGE = 'manager.html';
export const TAB_GROUP_NONE = -1;
export const GROUP_COLORS = ['blue', 'cyan', 'green', 'orange', 'pink', 'purple', 'red'];
export const COLOR_HEX = {
blue: '#2563eb',
cyan: '#0891b2',
green: '#059669',
orange: '#ea580c',
pink: '#db2777',
purple: '#7c3aed',
red: '#dc2626',
grey: '#64748b',
yellow: '#ca8a04'
};
export const FALLBACK_FAVICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij48cmVjdCBmaWxsPSIjZTZmMGZmIiByeD0iMTYiIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIvPjxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjE4IiBmaWxsPSIjMjU2M2ViIiBvcGFjaXR5PSIwLjE4Ii8+PHBhdGggZD0iTTMyIDE0YzkuOTQxIDAgMTggOC4wNTkgMTggMThTNDEuOTQxIDUwIDMyIDUwIDE0IDQxLjk0MSAxNCAzMiAyMi4wNTkgMTQgMzIgMTRabTAgNWMtNy4xOCAwLTEzIDUuODItMTMgMTNzNS44MiAxMyAxMyAxMyAxMy01LjgyIDEzLTEzLTUuODItMTMtMTMtMTNaIiBmaWxsPSIjMjU2M2ViIi8+PHBhdGggZD0iTTMyIDIwYzMuNDI2IDAgNi4yIDUuMzcxIDYuMiAxMnMtMi43NzQgMTItNi4yIDEyLTYuMi01LjM3MS02LjItMTIgMi43NzQtMTIgNi4yLTEyWm0wIDVjLS44MjcgMC0xLjY4NiAxLjk2Ny0xLjY4NiA3czAuODU5IDcgMS42ODYgN2MuODI3IDAgMS42ODYtMS45NjcgMS42ODYtN3MtMC44NTktNy0xLjY4Ni03WiIgZmlsbD0iIzI1NjNlYiIvPjwvc3ZnPg==';
export function isManagedGroupTitle(title = '') {
// 兼容旧的 emoji 图标和新的隐形空格
return title.startsWith('\u200B') || title.startsWith('⧉');
}
export function isManagedGroup(group) {
return Boolean(group && isManagedGroupTitle(group.title || ''));
}
export function stripGroupPrefix(title = '') {
let t = String(title || '');
// 强力替换旧版冗余图标和新版隐形空格
return t.replace(/^⧉\s*/, '').replace(/^\u200B\s*/, '').trim();
}
export function normalizeTitle(title = '', fallback = 'Stack', maxLength = 18) {
const cleaned = String(title || '').replace(/\s+/g, ' ').trim();
const base = cleaned || fallback;
if (base.length <= maxLength) {
return base;
}
return `${base.slice(0, maxLength - 1).trimEnd()}…`;
}
export function hashString(input = '') {
let hash = 0;
for (let i = 0; i < input.length; i += 1) {
hash = ((hash << 5) - hash) + input.charCodeAt(i);
hash |= 0;
}
return hash;
}
export function pickGroupColor(seed = '') {
return GROUP_COLORS[Math.abs(hashString(seed)) % GROUP_COLORS.length];
}
export function buildUniqueManagedTitle(baseName, existingGroups = [], excludeGroupId = null, fallback = 'Stack') {
const cleanBase = normalizeTitle(baseName, fallback);
const usedTitles = new Set(
existingGroups
.filter((group) => group.id !== excludeGroupId && isManagedGroup(group))
.map((group) => group.title)
);
const initialTitle = `${GROUP_PREFIX}${cleanBase}`;
if (!usedTitles.has(initialTitle)) {
return initialTitle;
}
let suffix = 2;
while (usedTitles.has(`${GROUP_PREFIX}${cleanBase} · ${suffix}`)) {
suffix += 1;
}
return `${GROUP_PREFIX}${cleanBase} · ${suffix}`;
}
export function colorHex(color = 'blue') {
return COLOR_HEX[color] || COLOR_HEX.blue;
}
export function faviconSrc(tab) {
return tab?.favIconUrl || FALLBACK_FAVICON;
}
export function tabHost(tab) {
try {
const url = new URL(tab?.url || tab?.pendingUrl || '');
return url.hostname.replace(/^www\./, '');
} catch {
return '';
}
}
export function isInternalExtensionTab(tab) {
const url = tab?.url || tab?.pendingUrl || '';
return url.startsWith('chrome-extension://') || url.startsWith('edge-extension://');
}
export function sortTabs(tabs = []) {
return [...tabs].sort((a, b) => a.index - b.index);
}
export function createElement(tagName, className = '', text = null) {
const element = document.createElement(tagName);
if (className) {
element.className = className;
}
if (text !== null && text !== undefined) {
element.textContent = text;
}
return element;
}
export function queryParam(name) {
return new URLSearchParams(window.location.search).get(name);
}
export function matchesTabKeyword(tab, keyword = '') {
if (!keyword) {
return true;
}
const needle = keyword.toLowerCase();
return [tab?.title, tab?.url, tab?.pendingUrl, tabHost(tab)]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(needle));
}