icloud-manager.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. (function attachSidepanelIcloudManager(globalScope) {
  2. function createIcloudManager(context = {}) {
  3. const {
  4. dom,
  5. helpers,
  6. runtime,
  7. } = context;
  8. let refreshQueued = false;
  9. let renderedAliases = [];
  10. let selectedEmails = new Set();
  11. let searchTerm = '';
  12. let filterMode = 'all';
  13. function normalizeIcloudSearchText(value) {
  14. return String(value || '').trim().toLowerCase();
  15. }
  16. function getFilteredIcloudAliases(aliases = renderedAliases) {
  17. const normalizedSearchTerm = normalizeIcloudSearchText(searchTerm);
  18. return (Array.isArray(aliases) ? aliases : []).filter((alias) => {
  19. const matchesFilter = (() => {
  20. switch (filterMode) {
  21. case 'active': return Boolean(alias.active);
  22. case 'used': return Boolean(alias.used);
  23. case 'unused': return !alias.used;
  24. case 'preserved': return Boolean(alias.preserved);
  25. default: return true;
  26. }
  27. })();
  28. if (!matchesFilter) return false;
  29. if (!normalizedSearchTerm) return true;
  30. const haystack = [
  31. alias.email,
  32. alias.label,
  33. alias.note,
  34. alias.used ? '已用 used' : '未用 unused',
  35. alias.active ? '可用 active' : '不可用 inactive',
  36. alias.preserved ? '保留 preserved' : '',
  37. ].join(' ').toLowerCase();
  38. return haystack.includes(normalizedSearchTerm);
  39. });
  40. }
  41. function pruneIcloudSelection(aliases = renderedAliases) {
  42. const existing = new Set((Array.isArray(aliases) ? aliases : []).map((alias) => alias.email));
  43. selectedEmails = new Set([...selectedEmails].filter((email) => existing.has(email)));
  44. }
  45. function updateIcloudBulkUI(visibleAliases = getFilteredIcloudAliases()) {
  46. if (!dom.checkboxIcloudSelectAll || !dom.icloudSelectionSummary) {
  47. return;
  48. }
  49. const visibleEmails = visibleAliases.map((alias) => alias.email);
  50. const selectedVisibleCount = visibleEmails.filter((email) => selectedEmails.has(email)).length;
  51. const hasVisible = visibleEmails.length > 0;
  52. dom.checkboxIcloudSelectAll.checked = hasVisible && selectedVisibleCount === visibleEmails.length;
  53. dom.checkboxIcloudSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleEmails.length;
  54. dom.checkboxIcloudSelectAll.disabled = !hasVisible;
  55. dom.icloudSelectionSummary.textContent = `已选 ${selectedEmails.size} 个(当前显示 ${visibleEmails.length} 个)`;
  56. const hasSelection = selectedEmails.size > 0;
  57. if (dom.btnIcloudBulkUsed) dom.btnIcloudBulkUsed.disabled = !hasSelection;
  58. if (dom.btnIcloudBulkUnused) dom.btnIcloudBulkUnused.disabled = !hasSelection;
  59. if (dom.btnIcloudBulkPreserve) dom.btnIcloudBulkPreserve.disabled = !hasSelection;
  60. if (dom.btnIcloudBulkUnpreserve) dom.btnIcloudBulkUnpreserve.disabled = !hasSelection;
  61. if (dom.btnIcloudBulkDelete) dom.btnIcloudBulkDelete.disabled = !hasSelection;
  62. }
  63. function setIcloudLoadingState(loading, summary = '') {
  64. if (dom.btnIcloudRefresh) dom.btnIcloudRefresh.disabled = loading;
  65. if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = loading;
  66. if (dom.btnIcloudLoginDone) dom.btnIcloudLoginDone.disabled = loading;
  67. if (dom.inputIcloudSearch) dom.inputIcloudSearch.disabled = loading;
  68. if (dom.selectIcloudFilter) dom.selectIcloudFilter.disabled = loading;
  69. if (dom.checkboxIcloudSelectAll) dom.checkboxIcloudSelectAll.disabled = loading || getFilteredIcloudAliases().length === 0;
  70. if (dom.btnIcloudBulkUsed) dom.btnIcloudBulkUsed.disabled = loading || selectedEmails.size === 0;
  71. if (dom.btnIcloudBulkUnused) dom.btnIcloudBulkUnused.disabled = loading || selectedEmails.size === 0;
  72. if (dom.btnIcloudBulkPreserve) dom.btnIcloudBulkPreserve.disabled = loading || selectedEmails.size === 0;
  73. if (dom.btnIcloudBulkUnpreserve) dom.btnIcloudBulkUnpreserve.disabled = loading || selectedEmails.size === 0;
  74. if (dom.btnIcloudBulkDelete) dom.btnIcloudBulkDelete.disabled = loading || selectedEmails.size === 0;
  75. if (summary && dom.icloudSummary) dom.icloudSummary.textContent = summary;
  76. }
  77. function showIcloudLoginHelp(payload = {}) {
  78. if (!dom.icloudLoginHelp) return;
  79. const loginUrl = String(payload.loginUrl || '').trim();
  80. let host = 'icloud.com.cn / icloud.com';
  81. if (loginUrl) {
  82. try {
  83. host = new URL(loginUrl).host;
  84. } catch {
  85. host = loginUrl;
  86. }
  87. }
  88. if (dom.icloudLoginHelpTitle) dom.icloudLoginHelpTitle.textContent = '需要登录 iCloud';
  89. if (dom.icloudLoginHelpText) dom.icloudLoginHelpText.textContent = `我已经为你打开 ${host}。请在那个页面完成登录,然后回到这里点击“我已登录”。`;
  90. dom.icloudLoginHelp.style.display = 'flex';
  91. }
  92. function hideIcloudLoginHelp() {
  93. if (dom.icloudLoginHelp) {
  94. dom.icloudLoginHelp.style.display = 'none';
  95. }
  96. }
  97. function renderIcloudAliases(aliases = []) {
  98. if (!dom.icloudList || !dom.icloudSummary) return;
  99. renderedAliases = Array.isArray(aliases) ? aliases : [];
  100. pruneIcloudSelection(renderedAliases);
  101. dom.icloudList.innerHTML = '';
  102. if (!aliases.length) {
  103. selectedEmails.clear();
  104. dom.icloudList.innerHTML = '<div class="icloud-empty">未找到 iCloud Hide My Email 别名。</div>';
  105. dom.icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。';
  106. if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = true;
  107. updateIcloudBulkUI([]);
  108. return;
  109. }
  110. const usedCount = aliases.filter((alias) => alias.used).length;
  111. const deletableUsedCount = aliases.filter((alias) => alias.used && !alias.preserved).length;
  112. dom.icloudSummary.textContent = `已加载 ${aliases.length} 个别名,其中 ${usedCount} 个已标记为已用。`;
  113. if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = deletableUsedCount === 0;
  114. const visibleAliases = getFilteredIcloudAliases(aliases);
  115. if (!visibleAliases.length) {
  116. dom.icloudList.innerHTML = '<div class="icloud-empty">没有匹配当前筛选条件的别名。</div>';
  117. updateIcloudBulkUI([]);
  118. return;
  119. }
  120. for (const alias of visibleAliases) {
  121. const item = document.createElement('div');
  122. item.className = 'icloud-item';
  123. item.innerHTML = `
  124. <input class="icloud-item-check" type="checkbox" data-action="select" ${selectedEmails.has(alias.email) ? 'checked' : ''} />
  125. <div class="icloud-item-main">
  126. <div class="icloud-item-email">${helpers.escapeHtml(alias.email)}</div>
  127. <div class="icloud-item-meta">
  128. ${alias.used ? '<span class="icloud-tag used">已用</span>' : ''}
  129. ${!alias.used && alias.active ? '<span class="icloud-tag active">可用</span>' : ''}
  130. ${alias.preserved ? '<span class="icloud-tag">保留</span>' : ''}
  131. ${alias.label ? `<span class="icloud-tag">${helpers.escapeHtml(alias.label)}</span>` : ''}
  132. ${alias.note ? `<span class="icloud-tag">${helpers.escapeHtml(alias.note)}</span>` : ''}
  133. </div>
  134. </div>
  135. <div class="icloud-item-actions">
  136. <button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${helpers.escapeHtml(alias.used ? '标记未用' : '标记已用')}</button>
  137. <button class="btn btn-outline btn-xs" type="button" data-action="toggle-preserved">${helpers.escapeHtml(alias.preserved ? '取消保留' : '保留')}</button>
  138. <button class="btn btn-outline btn-xs" type="button" data-action="delete">删除</button>
  139. </div>
  140. `;
  141. item.querySelector('[data-action="select"]').addEventListener('change', (event) => {
  142. if (event.target.checked) {
  143. selectedEmails.add(alias.email);
  144. } else {
  145. selectedEmails.delete(alias.email);
  146. }
  147. updateIcloudBulkUI(visibleAliases);
  148. });
  149. item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => {
  150. await setSingleIcloudAliasUsedState(alias, !alias.used);
  151. });
  152. item.querySelector('[data-action="toggle-preserved"]').addEventListener('click', async () => {
  153. await setSingleIcloudAliasPreservedState(alias, !alias.preserved);
  154. });
  155. item.querySelector('[data-action="delete"]').addEventListener('click', async () => {
  156. await deleteSingleIcloudAlias(alias);
  157. });
  158. dom.icloudList.appendChild(item);
  159. }
  160. updateIcloudBulkUI(visibleAliases);
  161. }
  162. async function refreshIcloudAliases(options = {}) {
  163. const { silent = false } = options;
  164. if (!dom.icloudSection || dom.icloudSection.style.display === 'none') {
  165. return;
  166. }
  167. if (!silent) setIcloudLoadingState(true, '正在加载 iCloud 别名...');
  168. try {
  169. const response = await runtime.sendMessage({
  170. type: 'LIST_ICLOUD_ALIASES',
  171. source: 'sidepanel',
  172. payload: {},
  173. });
  174. if (response?.error) throw new Error(response.error);
  175. hideIcloudLoginHelp();
  176. renderIcloudAliases(response?.aliases || []);
  177. } catch (err) {
  178. selectedEmails.clear();
  179. if (dom.icloudList) {
  180. dom.icloudList.innerHTML = '<div class="icloud-empty">无法加载 iCloud 别名。</div>';
  181. }
  182. if (dom.icloudSummary) {
  183. dom.icloudSummary.textContent = err.message;
  184. }
  185. updateIcloudBulkUI([]);
  186. if (!silent) helpers.showToast(`iCloud 别名加载失败:${err.message}`, 'error');
  187. } finally {
  188. setIcloudLoadingState(false);
  189. }
  190. }
  191. function queueIcloudAliasRefresh() {
  192. if (refreshQueued) return;
  193. refreshQueued = true;
  194. setTimeout(async () => {
  195. refreshQueued = false;
  196. await refreshIcloudAliases({ silent: true });
  197. }, 150);
  198. }
  199. async function deleteSingleIcloudAlias(alias) {
  200. const confirmed = await helpers.openConfirmModal({
  201. title: '删除 iCloud 别名',
  202. message: `确认删除 ${alias.email} 吗?此操作不可撤销。`,
  203. confirmLabel: '确认删除',
  204. confirmVariant: 'btn-danger',
  205. });
  206. if (!confirmed) {
  207. return;
  208. }
  209. setIcloudLoadingState(true, `正在删除 ${alias.email} ...`);
  210. try {
  211. const response = await runtime.sendMessage({
  212. type: 'DELETE_ICLOUD_ALIAS',
  213. source: 'sidepanel',
  214. payload: { email: alias.email, anonymousId: alias.anonymousId },
  215. });
  216. if (response?.error) throw new Error(response.error);
  217. helpers.showToast(`已删除 ${alias.email}`, 'success', 2200);
  218. await refreshIcloudAliases({ silent: true });
  219. } catch (err) {
  220. if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
  221. helpers.showToast(`删除 iCloud 别名失败:${err.message}`, 'error');
  222. } finally {
  223. setIcloudLoadingState(false);
  224. }
  225. }
  226. async function setSingleIcloudAliasUsedState(alias, used) {
  227. setIcloudLoadingState(true, `正在更新 ${alias.email} 的使用状态...`);
  228. try {
  229. const response = await runtime.sendMessage({
  230. type: 'SET_ICLOUD_ALIAS_USED_STATE',
  231. source: 'sidepanel',
  232. payload: { email: alias.email, used },
  233. });
  234. if (response?.error) throw new Error(response.error);
  235. helpers.showToast(`${alias.email} 已${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
  236. await refreshIcloudAliases({ silent: true });
  237. } catch (err) {
  238. if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
  239. helpers.showToast(`更新 iCloud 使用状态失败:${err.message}`, 'error');
  240. } finally {
  241. setIcloudLoadingState(false);
  242. }
  243. }
  244. async function setSingleIcloudAliasPreservedState(alias, preserved) {
  245. setIcloudLoadingState(true, `正在更新 ${alias.email} 的保留状态...`);
  246. try {
  247. const response = await runtime.sendMessage({
  248. type: 'SET_ICLOUD_ALIAS_PRESERVED_STATE',
  249. source: 'sidepanel',
  250. payload: { email: alias.email, preserved },
  251. });
  252. if (response?.error) throw new Error(response.error);
  253. helpers.showToast(`${alias.email} 已${preserved ? '设为保留' : '取消保留'}`, 'success', 2200);
  254. await refreshIcloudAliases({ silent: true });
  255. } catch (err) {
  256. if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
  257. helpers.showToast(`更新 iCloud 保留状态失败:${err.message}`, 'error');
  258. } finally {
  259. setIcloudLoadingState(false);
  260. }
  261. }
  262. async function runBulkIcloudAction(action) {
  263. const selectedAliases = renderedAliases.filter((alias) => selectedEmails.has(alias.email));
  264. if (!selectedAliases.length) {
  265. updateIcloudBulkUI();
  266. return;
  267. }
  268. if (action === 'delete') {
  269. const confirmed = await helpers.openConfirmModal({
  270. title: '批量删除 iCloud 别名',
  271. message: `确认删除选中的 ${selectedAliases.length} 个 iCloud 别名吗?此操作不可撤销。`,
  272. confirmLabel: '确认删除',
  273. confirmVariant: 'btn-danger',
  274. });
  275. if (!confirmed) {
  276. return;
  277. }
  278. }
  279. const actionLabelMap = {
  280. used: '标记已用',
  281. unused: '标记未用',
  282. preserve: '保留',
  283. unpreserve: '取消保留',
  284. delete: '删除',
  285. };
  286. setIcloudLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} iCloud 别名...`);
  287. try {
  288. for (const alias of selectedAliases) {
  289. let response = null;
  290. if (action === 'used' || action === 'unused') {
  291. response = await runtime.sendMessage({
  292. type: 'SET_ICLOUD_ALIAS_USED_STATE',
  293. source: 'sidepanel',
  294. payload: { email: alias.email, used: action === 'used' },
  295. });
  296. } else if (action === 'preserve' || action === 'unpreserve') {
  297. response = await runtime.sendMessage({
  298. type: 'SET_ICLOUD_ALIAS_PRESERVED_STATE',
  299. source: 'sidepanel',
  300. payload: { email: alias.email, preserved: action === 'preserve' },
  301. });
  302. } else if (action === 'delete') {
  303. response = await runtime.sendMessage({
  304. type: 'DELETE_ICLOUD_ALIAS',
  305. source: 'sidepanel',
  306. payload: { email: alias.email, anonymousId: alias.anonymousId },
  307. });
  308. selectedEmails.delete(alias.email);
  309. }
  310. if (response?.error) {
  311. throw new Error(response.error);
  312. }
  313. }
  314. helpers.showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedAliases.length} 个 iCloud 别名`, 'success', 2400);
  315. await refreshIcloudAliases({ silent: true });
  316. } catch (err) {
  317. if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
  318. helpers.showToast(`批量处理 iCloud 别名失败:${err.message}`, 'error');
  319. } finally {
  320. setIcloudLoadingState(false);
  321. updateIcloudBulkUI();
  322. }
  323. }
  324. async function deleteUsedIcloudAliases() {
  325. const confirmed = await helpers.openConfirmModal({
  326. title: '删除已用 iCloud 别名',
  327. message: '确认删除所有未保留的已用 iCloud 别名吗?此操作不可撤销。',
  328. confirmLabel: '确认删除',
  329. confirmVariant: 'btn-danger',
  330. });
  331. if (!confirmed) {
  332. return;
  333. }
  334. setIcloudLoadingState(true, '正在删除已用 iCloud 别名...');
  335. try {
  336. const response = await runtime.sendMessage({
  337. type: 'DELETE_USED_ICLOUD_ALIASES',
  338. source: 'sidepanel',
  339. payload: {},
  340. });
  341. if (response?.error) throw new Error(response.error);
  342. const deleted = response?.deleted || [];
  343. const skipped = response?.skipped || [];
  344. helpers.showToast(`已删除 ${deleted.length} 个已用别名,跳过 ${skipped.length} 个`, skipped.length ? 'warn' : 'success', 2800);
  345. await refreshIcloudAliases({ silent: true });
  346. } catch (err) {
  347. if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
  348. helpers.showToast(`删除已用 iCloud 别名失败:${err.message}`, 'error');
  349. } finally {
  350. setIcloudLoadingState(false);
  351. }
  352. }
  353. async function handleLoginDone() {
  354. if (dom.btnIcloudLoginDone) {
  355. dom.btnIcloudLoginDone.disabled = true;
  356. }
  357. try {
  358. const response = await runtime.sendMessage({
  359. type: 'CHECK_ICLOUD_SESSION',
  360. source: 'sidepanel',
  361. payload: {},
  362. });
  363. if (response?.error) {
  364. throw new Error(response.error);
  365. }
  366. hideIcloudLoginHelp();
  367. helpers.showToast('iCloud 会话已恢复,别名列表已刷新。', 'success', 2600);
  368. await refreshIcloudAliases({ silent: true });
  369. } catch (err) {
  370. helpers.showToast(`看起来还没有登录完成:${err.message}`, 'warn', 4200);
  371. } finally {
  372. if (dom.btnIcloudLoginDone) {
  373. dom.btnIcloudLoginDone.disabled = false;
  374. }
  375. }
  376. }
  377. function reset() {
  378. selectedEmails.clear();
  379. renderedAliases = [];
  380. searchTerm = '';
  381. filterMode = 'all';
  382. refreshQueued = false;
  383. if (dom.inputIcloudSearch) dom.inputIcloudSearch.value = '';
  384. if (dom.selectIcloudFilter) dom.selectIcloudFilter.value = 'all';
  385. if (dom.icloudList) dom.icloudList.innerHTML = '';
  386. if (dom.icloudSummary) dom.icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。';
  387. updateIcloudBulkUI([]);
  388. hideIcloudLoginHelp();
  389. }
  390. function hasDeletableUsedAliases() {
  391. return renderedAliases.some((alias) => alias.used && !alias.preserved);
  392. }
  393. function bindIcloudEvents() {
  394. dom.btnIcloudRefresh?.addEventListener('click', async () => {
  395. await refreshIcloudAliases();
  396. });
  397. dom.btnIcloudDeleteUsed?.addEventListener('click', async () => {
  398. await deleteUsedIcloudAliases();
  399. });
  400. dom.inputIcloudSearch?.addEventListener('input', () => {
  401. searchTerm = dom.inputIcloudSearch.value || '';
  402. renderIcloudAliases(renderedAliases);
  403. });
  404. dom.selectIcloudFilter?.addEventListener('change', () => {
  405. filterMode = dom.selectIcloudFilter.value || 'all';
  406. renderIcloudAliases(renderedAliases);
  407. });
  408. dom.checkboxIcloudSelectAll?.addEventListener('change', () => {
  409. const visibleAliases = getFilteredIcloudAliases();
  410. if (dom.checkboxIcloudSelectAll.checked) {
  411. visibleAliases.forEach((alias) => selectedEmails.add(alias.email));
  412. } else {
  413. visibleAliases.forEach((alias) => selectedEmails.delete(alias.email));
  414. }
  415. renderIcloudAliases(renderedAliases);
  416. });
  417. dom.btnIcloudBulkUsed?.addEventListener('click', async () => {
  418. await runBulkIcloudAction('used');
  419. });
  420. dom.btnIcloudBulkUnused?.addEventListener('click', async () => {
  421. await runBulkIcloudAction('unused');
  422. });
  423. dom.btnIcloudBulkPreserve?.addEventListener('click', async () => {
  424. await runBulkIcloudAction('preserve');
  425. });
  426. dom.btnIcloudBulkUnpreserve?.addEventListener('click', async () => {
  427. await runBulkIcloudAction('unpreserve');
  428. });
  429. dom.btnIcloudBulkDelete?.addEventListener('click', async () => {
  430. await runBulkIcloudAction('delete');
  431. });
  432. dom.btnIcloudLoginDone?.addEventListener('click', handleLoginDone);
  433. }
  434. return {
  435. bindIcloudEvents,
  436. hideIcloudLoginHelp,
  437. hasDeletableUsedAliases,
  438. queueIcloudAliasRefresh,
  439. refreshIcloudAliases,
  440. renderIcloudAliases,
  441. reset,
  442. showIcloudLoginHelp,
  443. updateIcloudBulkUI,
  444. };
  445. }
  446. globalScope.SidepanelIcloudManager = {
  447. createIcloudManager,
  448. };
  449. })(window);