iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
AI Engineering

從行程到應援:30 天做出追星實用工具箱系列 第 2

Day 2:模組一/追星行程追蹤與本地通知提醒功能實作Local Notification

  • 分享至 

  • xImage
  •  

一、前言

通常一開直播時,官方程式會立刻跳出通知(Weverse、Berriz、YT、Bubble),此模組結合即時翻譯,在官方跳通知時,本app也會跳出通知,點擊通知將會立刻連結直播平台,並自動開啟即時翻譯功能,無縫銜接直播內容與中文字幕!

二、今日任務

1. 安裝與設定套件

在終端機執行指令安裝通知套件與時間處理套件:

flutter pub add flutter_local_notifications timezone

2. 建立通知服務類別

class NotificationService {
  static #instance = null;

  constructor() {
    this.onNotificationClick = null;
    this.serviceWorkerRegistration = null;
  }

  static getInstance() {
    if (!NotificationService.#instance) {
      NotificationService.#instance = new NotificationService();
    }
    return NotificationService.#instance;
  }

  async init({ onClick } = {}) {
    if (onClick) {
      this.onNotificationClick = onClick;
    }

    // 1. 主動請求通知權限
    if ('Notification' in window) {
      const permission = await Notification.requestPermission();
      if (permission !== 'granted') {
        console.warn('使用者未授權通知權限');
      }
    }

    // 2. 註冊 Service Worker,讓通知在背景也能運作
    if ('serviceWorker' in navigator) {
      this.serviceWorkerRegistration =
        await navigator.serviceWorker.register('/sw.js');
    }

    // 3. 綁定點擊事件(透過 Service Worker 回傳訊息)
    navigator.serviceWorker?.addEventListener('message', (event) => {
      if (event.data?.type === 'notification-click') {
        this.onNotificationClick?.(event.data.payload);
      }
    });
  }

  async showNotification({ id, title, body, payload }) {
    if (Notification.permission !== 'granted') return;

    if (this.serviceWorkerRegistration) {
      await this.serviceWorkerRegistration.showNotification(title, {
        body,
        tag: String(id),
        data: { payload },
        icon: '/icons/app-icon.png',
      });
    } else {
      const notification = new Notification(title, { body });
      notification.onclick = () => this.onNotificationClick?.(payload);
    }
  }

  scheduleNotification({ id, title, body, scheduledDate, payload }) {
    const delay = scheduledDate.getTime() - Date.now();

    if (delay <= 0) {
      this.showNotification({ id, title, body, payload });
      return;
    }

    setTimeout(() => {
      this.showNotification({ id, title, body, payload });
    }, delay);
  }
}

export default NotificationService.getInstance();

3. 建立行程追蹤介面

import notificationService from './notification-service.js';

export function renderSchedulePage(container) {
  container.innerHTML = `
    <h1>追星行程提醒</h1>
    <input
      id="title-input"
      type="text"
      placeholder="行程名稱 (例如:偶像 Live 直播)"
    />
    <button id="test-notify-btn">🔔 測試即時提醒</button>
    <button id="schedule-notify-btn">⏱ 設定 5 秒後提醒</button>
    <p id="snackbar" hidden></p>
  `;

  const titleInput = container.querySelector('#title-input');
  const snackbar = container.querySelector('#snackbar');

  container.querySelector('#test-notify-btn').addEventListener('click', () => {
    notificationService.showNotification({
      id: Date.now(), // 補上 id
      title: '行程提醒!',
      body: titleInput.value || '偶像直播準備開始囉!',
    });
  });

  container.querySelector('#schedule-notify-btn').addEventListener('click', () => {
    const scheduledDate = new Date(Date.now() + 5000);

    notificationService.scheduleNotification({
      id: Date.now(),
      title: '偶像直播開始囉!',
      body: '點擊立即開啟 AI 即時字幕牆翻譯',
      scheduledDate,
      payload: 'open_translate', // 👈 帶入指令標籤
    });

    snackbar.textContent = '已設定 5 秒後通知!';
    snackbar.hidden = false;
    setTimeout(() => { snackbar.hidden = true; }, 3000);
  });
}

4.初始化並載入頁面

import notificationService from './notification-service.js';
import { renderSchedulePage } from './schedule-page.js';
import { renderLiveTranslatePage } from './live-translate-page.js';

// 全局頁面容器,方便在任何地方切換頁面
const appRoot = document.getElementById('app');

function navigateTo(pageRenderer) {
  appRoot.innerHTML = '';
  pageRenderer(appRoot);
}

async function main() {
  // 初始化通知,並傳入點擊處置
  await notificationService.init({
    onClick: (payload) => {
      if (payload === 'open_translate') {
        // 點擊通知後,直接跳轉到即時翻譯頁面
        navigateTo(renderLiveTranslatePage);
      }
    },
  });

  navigateTo(renderSchedulePage);
}

main();

最後檢查

  • [x] 執行 flutter run 啟動 App。
    螢幕擷取畫面 2026-09-04 010354
  • [x] 點擊「測試即時提醒」,確認手機上方跳出 notification。
    螢幕擷取畫面 2026-09-04 010430
  • [x] 點擊「設定 5 秒後提醒」,把 App 切到背景,確認 5 秒後跳出 notification。
    螢幕擷取畫面 2026-09-08 020713

上一篇
Day 1:系統架構設計與 Flutter 專案初始化
下一篇
Day 3:模組二/專輯與周邊收藏管理
系列文
從行程到應援:30 天做出追星實用工具箱7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言