To-Do List + Pomodoro Timer (Ctrl+T)

Manages tasks with a Pomodoro timer, accessible via Ctrl+T.

  1. // ==UserScript==
  2. // @name To-Do List + Pomodoro Timer (Ctrl+T)
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.4
  5. // @description Manages tasks with a Pomodoro timer, accessible via Ctrl+T.
  6. // @author kq (fixed by AI)
  7. // @match *://*/*
  8. // @grant GM_setValue
  9. // @grant GM_getValue
  10. // @grant GM_addStyle
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. const STORAGE_KEY = 'kq_todo_pomodoro_tasks';
  18. let tasks = [];
  19. let showExpired = false;
  20. let timerInterval = null;
  21. let timeLeft = 0; // in seconds
  22. let currentTaskForTimer = null;
  23. let isTimerPaused = false;
  24. let selectedTaskIndexForPanel = -1;
  25.  
  26. // --- Audio Setup ---
  27. let audioContext;
  28.  
  29. function setupAudio() {
  30. if (!audioContext) {
  31. audioContext = new (window.AudioContext || window.webkitAudioContext)();
  32. }
  33. }
  34.  
  35. function playAlarm() {
  36. if (!audioContext) setupAudio();
  37. if (!audioContext) return;
  38.  
  39. const oscillator = audioContext.createOscillator();
  40. const gainNode = audioContext.createGain();
  41.  
  42. oscillator.connect(gainNode);
  43. gainNode.connect(audioContext.destination);
  44.  
  45. oscillator.type = 'sine';
  46. oscillator.frequency.setValueAtTime(440, audioContext.currentTime); // A4
  47. gainNode.gain.setValueAtTime(0.5, audioContext.currentTime);
  48.  
  49. oscillator.start(audioContext.currentTime);
  50. oscillator.stop(audioContext.currentTime + 0.5);
  51.  
  52. if (navigator.vibrate) navigator.vibrate(200);
  53. }
  54.  
  55. // --- Data Management ---
  56. function loadTasks() {
  57. const tasksJSON = GM_getValue(STORAGE_KEY, '[]');
  58. tasks = JSON.parse(tasksJSON);
  59. }
  60.  
  61. function saveTasks() {
  62. GM_setValue(STORAGE_KEY, JSON.stringify(tasks));
  63. }
  64.  
  65. function getTaskById(id) {
  66. return tasks.find(task => task.id === id);
  67. }
  68.  
  69. // --- UI Elements ---
  70. let managementPanel, taskListDiv, floatingHud, hudText, hudTime, hudProgressBar;
  71.  
  72. function createManagementPanel() {
  73. const panel = document.createElement('div');
  74. panel.id = 'todo-pomodoro-panel';
  75. panel.innerHTML = `
  76. <div id="panel-header">
  77. <h2>To-Do List & Pomodoro (Ctrl+T)</h2>
  78. <button id="close-panel-btn">&times;</button>
  79. </div>
  80. <div id="task-input-area">
  81. <input type="text" id="new-task-name" placeholder="Task Name">
  82. <input type="number" id="new-task-duration" placeholder="Minutes (default 25)" min="1">
  83. <button id="add-task-btn">Add Task</button>
  84. </div>
  85. <div id="task-filters">
  86. <label><input type="checkbox" id="show-expired-checkbox"> Show Expired</label>
  87. </div>
  88. <div id="task-list-container"></div>
  89. <div id="panel-timer-controls">
  90. <h3>Timer for Selected Task</h3>
  91. <div id="selected-task-name-panel">No task selected</div>
  92. <div id="selected-task-timer-panel">00:00</div>
  93. <button id="start-task-btn" disabled>Start</button>
  94. <button id="pause-task-btn" disabled>Pause</button>
  95. <button id="stop-task-btn" disabled>Stop</button>
  96. </div>
  97. `;
  98. document.body.appendChild(panel);
  99. managementPanel = panel;
  100. taskListDiv = panel.querySelector('#task-list-container');
  101.  
  102. panel.querySelector('#close-panel-btn').addEventListener('click', togglePanel);
  103. panel.querySelector('#add-task-btn').addEventListener('click', handleAddTask);
  104.  
  105. panel.querySelector('#new-task-name').addEventListener('keypress', e => {
  106. if (e.key === 'Enter') handleAddTask();
  107. });
  108.  
  109. panel.querySelector('#show-expired-checkbox').addEventListener('change', e => {
  110. showExpired = e.target.checked;
  111. renderTaskList();
  112. });
  113.  
  114. panel.querySelector('#start-task-btn').addEventListener('click', handleStartPanelTimer);
  115. panel.querySelector('#pause-task-btn').addEventListener('click', handlePausePanelTimer);
  116. panel.querySelector('#stop-task-btn').addEventListener('click', handleStopPanelTimer);
  117. }
  118.  
  119. function createFloatingHUD() {
  120. const hud = document.createElement('div');
  121. hud.id = 'todo-pomodoro-hud';
  122. hud.innerHTML = `
  123. <div id="hud-task-info">
  124. <span id="hud-current-task-name">No active task</span>
  125. <span id="hud-completion-percentage">0%</span>
  126. </div>
  127. <div id="hud-timer-display">
  128. <svg id="hud-progress-svg" viewBox="0 0 108 108" width="108" height="108">
  129. <path id="hud-progress-bg"
  130. d="M54 6 a 48 48 0 0 1 0 96 a 48 48 0 0 1 0 -96"
  131. fill="none" stroke="#ddd" stroke-width="6"/>
  132. <path id="hud-progress-bar"
  133. d="M54 6 a 48 48 0 0 1 0 96 a 48 48 0 0 1 0 -96"
  134. fill="none" stroke="#4CAF50" stroke-width="6"
  135. stroke-dasharray="301.44, 301.44"
  136. stroke-dashoffset="301.44"/>
  137. </svg>
  138. <div id="hud-time-text">25:00</div>
  139. </div>
  140. `;
  141. document.body.appendChild(hud);
  142. floatingHud = hud;
  143. hudText = hud.querySelector('#hud-current-task-name');
  144. hudTime = hud.querySelector('#hud-time-text');
  145. hudProgressBar = hud.querySelector('#hud-progress-bar');
  146. updateFloatingHUD();
  147. floatingHud.style.pointerEvents = 'none';
  148. }
  149.  
  150. function togglePanel() {
  151. if (!managementPanel) createManagementPanel();
  152. managementPanel.style.position = 'fixed';
  153. managementPanel.style.top = '20%';
  154. managementPanel.style.right = '20%';
  155. managementPanel.style.left = 'auto';
  156. managementPanel.style.transform = 'translate(0, 0)';
  157. managementPanel.style.display = managementPanel.style.display === 'block' ? 'none' : 'block';
  158. if (managementPanel.style.display === 'block') {
  159. renderTaskList();
  160. updatePanelTimerControls();
  161. }
  162. updateFloatingHUDVisibility();
  163. }
  164.  
  165. function updateFloatingHUDVisibility() {
  166. if (floatingHud) {
  167. floatingHud.style.display = currentTaskForTimer ? 'flex' : 'none';
  168. }
  169. }
  170.  
  171. // --- Task Rendering ---
  172. function renderTaskList() {
  173. if (!taskListDiv) return;
  174. taskListDiv.innerHTML = '';
  175. const filteredTasks = tasks.filter(task => showExpired || !task.Expired);
  176.  
  177. if (filteredTasks.length === 0) {
  178. taskListDiv.innerHTML = '<p>No tasks yet. Add one!</p>';
  179. return;
  180. }
  181.  
  182. const ul = document.createElement('ul');
  183. filteredTasks.forEach((task, indexInFilteredTasks) => {
  184. const originalIndex = tasks.findIndex(t => t.id === task.id);
  185. const li = document.createElement('li');
  186. li.className = `task-item ${task.Done ? 'done' : ''} ${task.Expired ? 'expired' : ''}`;
  187. if (originalIndex === selectedTaskIndexForPanel) li.classList.add('selected-for-panel');
  188. li.dataset.taskId = task.id;
  189. li.innerHTML = `
  190. <span class="task-name">${task.Name} (${task.Duration} min)</span>
  191. <div class="task-actions">
  192. <button class="complete-btn">${task.Done ? 'Undo' : 'Done'}</button>
  193. <button class="expire-btn">${task.Expired ? 'Unexpire' : 'Expire'}</button>
  194. <button class="delete-btn">Delete</button>
  195. </div>
  196. `;
  197.  
  198. li.addEventListener('click', e => {
  199. if (e.target.tagName !== 'BUTTON') {
  200. selectedTaskIndexForPanel = originalIndex;
  201. updatePanelTimerControls();
  202. renderTaskList(); // Re-render to highlight selected item
  203. }
  204. });
  205.  
  206. li.querySelector('.complete-btn').addEventListener('click', () => toggleDone(task.id));
  207. li.querySelector('.expire-btn').addEventListener('click', () => toggleExpired(task.id));
  208. li.querySelector('.delete-btn').addEventListener('click', () => deleteTask(task.id));
  209.  
  210. ul.appendChild(li);
  211. });
  212.  
  213. taskListDiv.appendChild(ul);
  214. updateCompletionPercentage();
  215. }
  216.  
  217. function handleAddTask() {
  218. const nameInput = managementPanel.querySelector('#new-task-name');
  219. const durationInput = managementPanel.querySelector('#new-task-duration');
  220. const name = nameInput.value.trim();
  221. const duration = parseInt(durationInput.value) || 25;
  222.  
  223. if (!name) {
  224. alert('请输入任务名称');
  225. return;
  226. }
  227.  
  228. tasks.push({
  229. id: Date.now().toString(),
  230. Name: name,
  231. Duration: duration,
  232. Done: false,
  233. Expired: false
  234. });
  235.  
  236. saveTasks();
  237. renderTaskList();
  238. nameInput.value = '';
  239. durationInput.value = '';
  240. updateCompletionPercentage();
  241. }
  242.  
  243. function toggleDone(taskId) {
  244. const task = getTaskById(taskId);
  245. if (task) {
  246. task.Done = !task.Done;
  247. if (currentTaskForTimer && currentTaskForTimer.id === taskId) handleStopPanelTimer(true);
  248. saveTasks();
  249. renderTaskList();
  250. updateCompletionPercentage();
  251. }
  252. }
  253.  
  254. function toggleExpired(taskId) {
  255. const task = getTaskById(taskId);
  256. if (task) {
  257. task.Expired = !task.Expired;
  258. if (currentTaskForTimer && currentTaskForTimer.id === taskId) handleStopPanelTimer(true);
  259. saveTasks();
  260. renderTaskList();
  261. updateCompletionPercentage();
  262. }
  263. }
  264.  
  265. function deleteTask(taskId) {
  266. if (confirm('确定要删除这个任务吗?')) {
  267. tasks = tasks.filter(task => task.id !== taskId);
  268. if (currentTaskForTimer && currentTaskForTimer.id === taskId) handleStopPanelTimer(true);
  269. if (selectedTaskIndexForPanel !== -1 && tasks[selectedTaskIndexForPanel]?.id === taskId) {
  270. selectedTaskIndexForPanel = -1;
  271. }
  272. saveTasks();
  273. renderTaskList();
  274. updatePanelTimerControls();
  275. updateCompletionPercentage();
  276. }
  277. }
  278.  
  279. function updateCompletionPercentage() {
  280. const nonExpiredTasks = tasks.filter(task => !task.Expired);
  281. const completedNonExpired = nonExpiredTasks.filter(task => task.Done).length;
  282. const percentage = nonExpiredTasks.length > 0 ? Math.round((completedNonExpired / nonExpiredTasks.length) * 100) : 0;
  283. if (floatingHud) floatingHud.querySelector('#hud-completion-percentage').textContent = `${percentage}%`;
  284. }
  285.  
  286. // --- Timer Logic ---
  287. function formatTime(sec) {
  288. const m = Math.floor(sec / 60);
  289. const s = sec % 60;
  290. return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
  291. }
  292.  
  293. function updateTimerDisplay(totalSeconds, displayElement) {
  294. if (displayElement) {
  295. displayElement.textContent = formatTime(totalSeconds);
  296. }
  297. }
  298.  
  299. function updateFloatingHUD() {
  300. if (!floatingHud) return;
  301. const circumference = 2 * Math.PI * 48;
  302.  
  303. if (currentTaskForTimer && !isTimerPaused) {
  304. hudText.textContent = `当前任务:${currentTaskForTimer.Name}`;
  305. hudTime.textContent = formatTime(timeLeft);
  306. const total = currentTaskForTimer.Duration * 60;
  307. const progress = total > 0 ? (total - timeLeft) / total : 0;
  308. hudProgressBar.style.strokeDasharray = `${circumference}`;
  309. hudProgressBar.style.strokeDashoffset = `${circumference * (1 - progress)}`;
  310. hudProgressBar.style.stroke = '#4CAF50';
  311. } else if (currentTaskForTimer && isTimerPaused) {
  312. hudText.textContent = `已暂停:${currentTaskForTimer.Name}`;
  313. hudTime.textContent = formatTime(timeLeft);
  314. const total = currentTaskForTimer.Duration * 60;
  315. const progress = total > 0 ? (total - timeLeft) / total : 0;
  316. hudProgressBar.style.strokeDasharray = `${circumference}`;
  317. hudProgressBar.style.strokeDashoffset = `${circumference * (1 - progress)}`;
  318. hudProgressBar.style.stroke = '#FFC107';
  319. } else {
  320. hudText.textContent = "无活动任务";
  321. hudTime.textContent = "00:00";
  322. hudProgressBar.style.strokeDasharray = `${circumference}`;
  323. hudProgressBar.style.strokeDashoffset = `${circumference}`;
  324. hudProgressBar.style.stroke = '#ddd';
  325. }
  326.  
  327. updateCompletionPercentage();
  328. updateFloatingHUDVisibility();
  329. }
  330.  
  331. function timerTick() {
  332. if (isTimerPaused || !currentTaskForTimer) return;
  333. timeLeft--;
  334. updateTimerDisplay(timeLeft, managementPanel.querySelector('#selected-task-timer-panel'));
  335. updateFloatingHUD();
  336. // 更新网页标签标题为剩余时间
  337. document.title = formatTime(timeLeft); // 👈 新增这一行:设置网页标签名
  338. // 同步状态到其他页面
  339. saveSharedTimerState();
  340. if (timeLeft <= 0) {
  341. clearInterval(timerInterval);
  342. timerInterval = null;
  343. // 播放7次提示音
  344. let alarmCount = 0;
  345. const maxAlarms = 7;
  346. const alarmInterval = setInterval(() => {
  347. playAlarm();
  348. alarmCount++;
  349. if (alarmCount >= maxAlarms) {
  350. clearInterval(alarmInterval);
  351. }
  352. }, 500); // 每隔半秒响一次
  353. // 不再使用 alert 提醒
  354. currentTaskForTimer = null;
  355. isTimerPaused = false;
  356. updatePanelTimerControls();
  357. updateFloatingHUD();
  358. }
  359. }
  360.  
  361. function updatePanelTimerControls() {
  362. if (!managementPanel) return;
  363. const startBtn = managementPanel.querySelector('#start-task-btn');
  364. const pauseBtn = managementPanel.querySelector('#pause-task-btn');
  365. const stopBtn = managementPanel.querySelector('#stop-task-btn');
  366. const taskNameDisplay = managementPanel.querySelector('#selected-task-name-panel');
  367. const taskTimerDisplay = managementPanel.querySelector('#selected-task-timer-panel');
  368.  
  369. if (selectedTaskIndexForPanel !== -1 && tasks[selectedTaskIndexForPanel]) {
  370. const selectedTask = tasks[selectedTaskIndexForPanel];
  371.  
  372. taskNameDisplay.textContent = `任务:${selectedTask.Name}`;
  373.  
  374. if (currentTaskForTimer && currentTaskForTimer.id === selectedTask.id) {
  375. startBtn.disabled = true;
  376. pauseBtn.textContent = isTimerPaused ? "继续" : "暂停";
  377. pauseBtn.disabled = false;
  378. stopBtn.disabled = false;
  379. updateTimerDisplay(timeLeft, taskTimerDisplay);
  380. } else {
  381. startBtn.disabled = false;
  382. pauseBtn.disabled = true;
  383. stopBtn.disabled = true;
  384. pauseBtn.textContent = "暂停";
  385. updateTimerDisplay(selectedTask.Duration * 60, taskTimerDisplay);
  386. }
  387. } else {
  388. taskNameDisplay.textContent = '未选择任务';
  389. updateTimerDisplay(0, taskTimerDisplay);
  390. startBtn.disabled = true;
  391. pauseBtn.disabled = true;
  392. stopBtn.disabled = true;
  393. }
  394. }
  395. function saveSharedTimerState() {
  396. if (!currentTaskForTimer) {
  397. localStorage.removeItem("shared_timer_state");
  398. return;
  399. }
  400. const state = {
  401. taskId: currentTaskForTimer.id,
  402. taskName: currentTaskForTimer.Name,
  403. taskDuration: currentTaskForTimer.Duration,
  404. timeLeft,
  405. isTimerPaused
  406. };
  407. localStorage.setItem("shared_timer_state", JSON.stringify(state));
  408. syncTimerFromStorage(); // 立即同步
  409. }
  410. function handleStartPanelTimer() {
  411. if (selectedTaskIndexForPanel === -1 || !tasks[selectedTaskIndexForPanel]) return;
  412. const selectedTask = tasks[selectedTaskIndexForPanel];
  413. if (timerInterval) clearInterval(timerInterval);
  414. currentTaskForTimer = selectedTask;
  415. timeLeft = selectedTask.Duration * 60; // Use the task's duration
  416. isTimerPaused = false;
  417. timerInterval = setInterval(timerTick, 1000);
  418. saveSharedTimerState(); // Save immediately
  419. updatePanelTimerControls();
  420. updateFloatingHUD();
  421. }
  422.  
  423. function handlePausePanelTimer() {
  424. if (!currentTaskForTimer) return;
  425.  
  426. isTimerPaused = !isTimerPaused;
  427. updatePanelTimerControls();
  428. updateFloatingHUD();
  429. }
  430. let originalTitle = document.title; // 在顶部附近加上这行
  431. function handleStopPanelTimer(isSilent = false) {
  432. if (timerInterval) {
  433. clearInterval(timerInterval);
  434. timerInterval = null;
  435. }
  436. timeLeft = currentTaskForTimer?.Duration * 60 || 0;
  437. currentTaskForTimer = null;
  438. isTimerPaused = false;
  439. updatePanelTimerControls();
  440. updateFloatingHUD();
  441. document.title = originalTitle; // 还原原始标题
  442. }
  443.  
  444. // --- Styles ---
  445. function addStyles() {
  446. GM_addStyle(`
  447. #todo-pomodoro-panel {
  448. position: fixed; top: 20%; right: 20%;
  449. width: 450px; max-height: 80vh; background-color: #f9f9f9;
  450. border: 1px solid #ccc; box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  451. z-index: 99999; display: none; flex-direction: column;
  452. font-family: Arial, sans-serif;
  453. }
  454. #panel-header { display: flex; justify-content: space-between; align-items: center;
  455. padding: 10px 15px; background-color: #eee; border-bottom: 1px solid #ccc; }
  456. #panel-header h2 { margin: 0; font-size: 1.2em; }
  457. #close-panel-btn { background: none; border: none; font-size: 1.5em; cursor: pointer; }
  458. #task-input-area { padding: 15px; display: flex; gap: 10px; border-bottom: 1px solid #eee; }
  459. #task-input-area input[type="text"] { flex-grow: 1; padding: 8px; }
  460. #task-input-area input[type="number"] { width: 120px; padding: 8px; }
  461. #task-input-area button { padding: 8px 12px; cursor: pointer; background-color: #4CAF50; color: white; border: none; }
  462. #task-filters { padding: 10px 15px; border-bottom: 1px solid #eee; }
  463. #task-list-container {
  464. padding: 10px 15px; overflow-y: auto; flex-grow: 1;
  465. }
  466. #task-list-container ul { list-style: none; padding: 0; margin: 0; }
  467. .task-item {
  468. display: flex; justify-content: space-between; align-items: center;
  469. padding: 8px 5px; border-bottom: 1px solid #eee; cursor: default;
  470. }
  471. .task-item.selected-for-panel { background-color: #e0e0e0; font-weight: bold; }
  472. .task-item:hover:not(.selected-for-panel) { background-color: #f0f0f0; }
  473. .task-item.done .task-name { text-decoration: line-through; color: #888; }
  474. .task-item.expired .task-name { color: #aaa; font-style: italic; }
  475. .task-name { flex-grow: 1; }
  476. .task-actions button { margin-left: 5px; padding: 3px 6px; cursor: pointer; font-size: 0.8em; }
  477. #panel-timer-controls { padding: 15px; border-top: 1px solid #ccc; text-align: center; }
  478. #panel-timer-controls h3 { margin-top: 0; font-size: 1em; }
  479. #selected-task-name-panel { margin-bottom: 5px; font-style: italic; }
  480. #selected-task-timer-panel { font-size: 1.8em; margin-bottom: 10px; font-weight: bold; }
  481. #panel-timer-controls button { padding: 8px 15px; margin: 0 5px; cursor: pointer; }
  482. #panel-timer-controls button:disabled { background-color: #ccc; cursor: not-allowed; }
  483. #todo-pomodoro-hud {
  484. position: fixed; top: 20px; right: 20px; background-color: rgba(255, 255, 255, 0.7);
  485. border: 1px solid #ccc; border-radius: 10px; padding: 15px 20px;
  486. box-shadow: 0 2px 10px rgba(0,0,0,0.15); z-index: 99998; display: none;
  487. align-items: center; gap: 20px; font-family: Arial, sans-serif;
  488. min-width: 260px; pointer-events: none; transition: all 0.2s ease-in-out;
  489. }
  490. #hud-task-info { display: flex; flex-direction: column; flex-grow: 1; }
  491. #hud-current-task-name { font-size: 1em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; }
  492. #hud-completion-percentage { font-size: 0.9em; color: #555; }
  493. #hud-timer-display { position: relative; width: 108px; height: 108px; }
  494. #hud-progress-svg { transform: rotate(-90deg); transition: stroke-dashoffset 0.3s linear; }
  495. #hud-time-text {
  496. position: absolute; top: 50%; left: 50%;
  497. transform: translate(-50%, -50%);
  498. font-size: 1.2em; font-weight: bold;
  499. }
  500. `);
  501. }
  502. function syncTimerFromStorage() {
  503. try {
  504. const raw = localStorage.getItem("shared_timer_state");
  505. if (raw) {
  506. const state = JSON.parse(raw);
  507. if (state && state.taskId) {
  508. let task = getTaskById(state.taskId);
  509. if (!task) {
  510. // 动态创建临时任务
  511. task = {
  512. id: state.taskId,
  513. Name: state.taskName,
  514. Duration: state.taskDuration || 25,
  515. Done: false,
  516. Expired: false
  517. };
  518. tasks.unshift(task);
  519. renderTaskList();
  520. }
  521. if (currentTaskForTimer && currentTaskForTimer.id === state.taskId) {
  522. timeLeft = state.timeLeft;
  523. isTimerPaused = state.isTimerPaused;
  524. updateFloatingHUD();
  525. updatePanelTimerControls();
  526. return;
  527. }
  528. if (timerInterval) clearInterval(timerInterval);
  529. currentTaskForTimer = task;
  530. timeLeft = state.timeLeft;
  531. isTimerPaused = state.isTimerPaused;
  532. if (!isTimerPaused) {
  533. timerInterval = setInterval(timerTick, 1000); // ✅ 重启定时器
  534. }
  535. updateFloatingHUD();
  536. updatePanelTimerControls();
  537. } else if (currentTaskForTimer) {
  538. handleStopPanelTimer(true);
  539. }
  540. }
  541. } catch (e) {
  542. console.error("Failed to parse shared timer state", e);
  543. }
  544. }
  545. // Add storage event listener
  546. function setupStorageListener() {
  547. window.addEventListener('storage', (e) => {
  548. if (e.key === "shared_timer_state") {
  549. syncTimerFromStorage();
  550. }
  551. });
  552. }
  553. // --- Init ---
  554. function init() {
  555. loadTasks(); // 加载本地任务列表
  556. addStyles();
  557. createFloatingHUD();
  558. document.addEventListener('keydown', (e) => {
  559. if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 't') {
  560. e.preventDefault();
  561. togglePanel();
  562. }
  563. });
  564. setupAudio();
  565. // 首次同步
  566. setTimeout(() => {
  567. syncTimerFromStorage();
  568. setupStorageListener();
  569. setInterval(syncTimerFromStorage, 1000);
  570. }, 500);
  571. }
  572.  
  573. init();
  574. })();