extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.
at master 163 lines 4.0 kB view raw
1import { browser } from '$app/environment'; 2 3export class GameNotifications { 4 private originalTitle: string; 5 private titleInterval: ReturnType<typeof setInterval> | null = null; 6 private notificationPermission: NotificationPermission = 'default'; 7 8 constructor() { 9 if (browser) { 10 this.originalTitle = document.title; 11 // iOS Safari doesn't support the Notification API 12 this.notificationPermission = ('Notification' in window) ? Notification.permission : 'denied'; 13 } else { 14 this.originalTitle = ''; 15 } 16 } 17 18 /** 19 * Request notification permission from the user 20 */ 21 async requestPermission(): Promise<boolean> { 22 if (!browser || !('Notification' in window)) { 23 return false; 24 } 25 26 if (Notification.permission === 'granted') { 27 this.notificationPermission = 'granted'; 28 return true; 29 } 30 31 if (Notification.permission !== 'denied') { 32 const permission = await Notification.requestPermission(); 33 this.notificationPermission = permission; 34 return permission === 'granted'; 35 } 36 37 return false; 38 } 39 40 /** 41 * Start flashing the page title to get attention 42 */ 43 startTitleFlash(message: string) { 44 if (!browser) return; 45 46 this.stopTitleFlash(); // Clear any existing flash 47 this.originalTitle = document.title; 48 49 let isOriginal = true; 50 this.titleInterval = setInterval(() => { 51 document.title = isOriginal ? message : this.originalTitle; 52 isOriginal = !isOriginal; 53 }, 1000); 54 } 55 56 /** 57 * Stop flashing the title and restore the original 58 */ 59 stopTitleFlash() { 60 if (!browser) return; 61 62 if (this.titleInterval) { 63 clearInterval(this.titleInterval); 64 this.titleInterval = null; 65 } 66 document.title = this.originalTitle; 67 } 68 69 /** 70 * Show a browser push notification 71 */ 72 async showNotification(title: string, options?: NotificationOptions) { 73 if (!browser || !('Notification' in window)) { 74 return; 75 } 76 77 // Auto-request permission if not denied 78 if (this.notificationPermission === 'default') { 79 await this.requestPermission(); 80 } 81 82 if (this.notificationPermission === 'granted') { 83 try { 84 const notification = new Notification(title, { 85 icon: '/favicon.png', 86 badge: '/favicon.png', 87 ...options 88 }); 89 90 // Auto-close after 5 seconds 91 setTimeout(() => notification.close(), 5000); 92 93 return notification; 94 } catch (err) { 95 console.error('Failed to show notification:', err); 96 } 97 } 98 } 99 100 /** 101 * Notify user of their turn with both title flash and push notification 102 */ 103 notifyYourTurn(opponentHandle?: string) { 104 const message = opponentHandle 105 ? `Your turn vs ${opponentHandle}!` 106 : 'Your turn!'; 107 108 this.startTitleFlash(`${message}`); 109 this.showNotification('Cloud Go - Your Turn', { 110 body: message, 111 tag: 'your-turn', 112 requireInteraction: false 113 }); 114 } 115 116 /** 117 * Notify user of a new move in the game they're watching 118 */ 119 notifyNewMove(playerHandle?: string) { 120 const message = playerHandle 121 ? `${playerHandle} played a move` 122 : 'New move played'; 123 124 this.startTitleFlash(`🔄 ${message}`); 125 this.showNotification('Cloud Go - New Move', { 126 body: message, 127 tag: 'new-move', 128 requireInteraction: false 129 }); 130 } 131 132 /** 133 * Clean up when component unmounts 134 */ 135 cleanup() { 136 this.stopTitleFlash(); 137 } 138} 139 140/** 141 * Check if the page is currently visible/focused 142 */ 143export function isPageVisible(): boolean { 144 if (!browser) return false; 145 return document.visibilityState === 'visible'; 146} 147 148/** 149 * Listen for visibility changes 150 */ 151export function onVisibilityChange(callback: (visible: boolean) => void): () => void { 152 if (!browser) return () => {}; 153 154 const handler = () => { 155 callback(document.visibilityState === 'visible'); 156 }; 157 158 document.addEventListener('visibilitychange', handler); 159 160 return () => { 161 document.removeEventListener('visibilitychange', handler); 162 }; 163}