通常一開直播時,官方程式會立刻跳出通知(Weverse、Berriz、YT、Bubble),此模組結合即時翻譯,在官方跳通知時,本app也會跳出通知,點擊通知將會立刻連結直播平台,並自動開啟即時翻譯功能,無縫銜接直播內容與中文字幕!
在終端機執行指令安裝通知套件與時間處理套件:
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();
flutter run 啟動 App。