refactor: use a static favicon for the time tracking dot

Swap to a pre-rendered favicon instead of drawing the red dot on a
canvas at runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWfQvx8fzZoE7ozLk8RWRf
This commit is contained in:
Claude 2026-06-18 21:00:26 +00:00
parent b11d04726f
commit cfa61ccc7f
No known key found for this signature in database
2 changed files with 6 additions and 63 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -4,83 +4,26 @@ import {storeToRefs} from 'pinia'
import {useTimeTrackingStore} from '@/stores/timeTracking'
const FAVICON_SIZE = 32
// Drawn from a PNG rather than the .ico because ICO decoding into a canvas is
// unreliable across browsers.
const BASE_FAVICON = '/images/icons/favicon-32x32.png'
const DOT_COLOR = '#ff4136'
const TRACKING_FAVICON = '/images/icons/favicon-tracking-32x32.png'
function getFaviconLink(): HTMLLinkElement | null {
return document.querySelector<HTMLLinkElement>('link[rel="icon"]')
}
// Marks the favicon with a small red dot in the lower left corner while a timer
// Swaps in a favicon with a small red dot in the lower left corner while a timer
// is running, so an active time tracking session is visible even when the tab
// isn't focused.
export const useTimeTrackingFavicon = createSharedComposable(() => {
const {hasActiveTimer} = storeToRefs(useTimeTrackingStore())
const link = getFaviconLink()
const originalHref = link?.getAttribute('href') ?? '/favicon.ico'
let baseImage: HTMLImageElement | null = null
function loadBaseImage(): Promise<HTMLImageElement> {
if (baseImage?.complete) {
return Promise.resolve(baseImage)
}
return new Promise((resolve, reject) => {
const img = new Image()
img.addEventListener('load', () => {
baseImage = img
resolve(img)
})
img.addEventListener('error', reject)
img.src = BASE_FAVICON
})
}
async function drawBadgedFavicon() {
const targetLink = getFaviconLink()
if (targetLink === null) {
return
}
const img = await loadBaseImage()
const canvas = document.createElement('canvas')
canvas.width = FAVICON_SIZE
canvas.height = FAVICON_SIZE
const ctx = canvas.getContext('2d')
if (ctx === null) {
return
}
ctx.drawImage(img, 0, 0, FAVICON_SIZE, FAVICON_SIZE)
const radius = FAVICON_SIZE * 0.28
const cx = radius
const cy = FAVICON_SIZE - radius
ctx.beginPath()
ctx.arc(cx, cy, radius, 0, Math.PI * 2)
ctx.fillStyle = DOT_COLOR
ctx.fill()
targetLink.href = canvas.toDataURL('image/png')
}
function restoreFavicon() {
const targetLink = getFaviconLink()
if (targetLink !== null) {
targetLink.href = originalHref
}
}
const originalHref = getFaviconLink()?.getAttribute('href') ?? '/favicon.ico'
function update(active: boolean) {
if (active) {
void drawBadgedFavicon()
const link = getFaviconLink()
if (link === null) {
return
}
restoreFavicon()
link.href = active ? TRACKING_FAVICON : originalHref
}
watch(hasActiveTimer, update, {flush: 'post'})