diff --git a/packages/frontend/src/scripts/icon-dot.ts b/packages/frontend/src/scripts/icon-dot.ts new file mode 100644 index 000000000..47fdadd1e --- /dev/null +++ b/packages/frontend/src/scripts/icon-dot.ts @@ -0,0 +1,107 @@ +class FavIconDot { + _canvas : HTMLCanvasElement; + private canvas() { + if (!this._canvas) { + this._canvas = document.createElement('canvas'); + } + return this._canvas; + } + + _ctx : CanvasRenderingContext2D | null = null; + private ctx() { + if (!this._ctx) { + this._ctx = this.canvas().getContext('2d'); + } + return this._ctx; + } + + _favIconEl : HTMLLinkElement; + private favIconEl() { + if (!this._favIconEl) { + this._favIconEl = document.querySelector('link[rel$=icon]') ?? this._createFaviconElem(); + } + return this._favIconEl; + } + + _favIconImage : HTMLImageElement | null = null; + private favIconImage() { + if (!this._favIconImage) { + const canvas = this.canvas(); + const image = document.createElement('img'); + + this._hasLoaded = new Promise((resolve) => { + image.onload = () => { + canvas.width = image.width; + canvas.height = image.height; + + resolve(); + }; + }); + + image.src = this.favIconEl().href; + + this._favIconImage = image; + } + return this._favIconImage; + } + + _hasLoaded : Promise; + private hasLoaded() { + if (!this._hasLoaded) { + this.favIconImage(); + } + return this._hasLoaded; + } + + private _createFaviconElem() { + const newLink = document.createElement('link'); + newLink.rel = 'icon'; + newLink.href = '/favicon.ico'; + document.head.appendChild(newLink); + return newLink; + } + + private _drawIcon() { + const ctx = this.ctx(); + const favIconImage = this.favIconImage(); + const canvas = this.canvas(); + + if (!ctx || !favIconImage) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(favIconImage, 0, 0, favIconImage.width, favIconImage.height); + } + + private _drawDot() { + const ctx = this.ctx(); + const favIconImage = this.favIconImage(); + + if (!ctx || !favIconImage) return; + + ctx.beginPath(); + ctx.arc(favIconImage.width - 10, 10, 10, 0, 2 * Math.PI); + ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator'); + ctx.strokeStyle = 'white'; + ctx.fill(); + ctx.stroke(); + } + + private _setFavicon() { + this.favIconEl().href = this.canvas().toDataURL('image/png'); + } + + async setVisible(isVisible : boolean) { + await this.hasLoaded(); + this._drawIcon(); + if (isVisible) this._drawDot(); + this._setFavicon(); + } +} + +let icon: FavIconDot; + +export function setFavIconDot(visible: boolean) { + if (!icon) { + icon = new FavIconDot(); + } + icon.setVisible(visible); +} diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index a5e5d19a3..89833fa82 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -60,6 +60,7 @@ import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { globalEvents } from '@/events.js'; +import { setFavIconDot } from '@/scripts/icon-dot.js'; const SkOneko = defineAsyncComponent(() => import('@/components/SkOneko.vue')); @@ -70,74 +71,6 @@ const dev = _DEV_; const notifications = ref([]); -class NotificationFavIconDot { - canvas : HTMLCanvasElement; - src : string | null = null; - ctx : CanvasRenderingContext2D | null = null; - favconImage : HTMLImageElement | null = null; - faviconEL : HTMLLinkElement; - hasLoaded : Promise; - - constructor() { - this.canvas = document.createElement('canvas'); - this.faviconEL = document.querySelector('link[rel$=icon]') ?? this._createFaviconElem(); - - this.src = this.faviconEL.getAttribute('href'); - this.ctx = this.canvas.getContext('2d'); - - this.favconImage = document.createElement('img'); - this.favconImage.src = this.faviconEL.href; - this.hasLoaded = new Promise((resolve, reject) => { - this.favconImage.onload = () => { - this.canvas.width = this.favconImage.width; - this.canvas.height = this.favconImage.height; - - // resolve(); - setTimeout(() => resolve(), 500); - }; - }); - } - - private _createFaviconElem() { - const newLink = document.createElement('link'); - newLink.rel = 'icon'; - newLink.href = '/favicon.ico'; - document.head.appendChild(newLink); - return newLink; - } - - private _drawIcon() { - if (!this.ctx || !this.favconImage) return; - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height); - } - - private _drawDot() { - if (!this.ctx || !this.favconImage) return; - this.ctx.beginPath(); - this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI); - this.ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator'); - this.ctx.strokeStyle = 'white'; - this.ctx.fill(); - this.ctx.stroke(); - } - - private _setFavicon() { - this.faviconEL.href = this.canvas.toDataURL('image/png'); - } - - async setVisible(isVisible : boolean) { - //Wait for it to have loaded the icon - await this.hasLoaded; - console.log(this.hasLoaded); - this._drawIcon(); - if (isVisible) this._drawDot(); - this._setFavicon(); - } -} - -const notificationDot = new NotificationFavIconDot(); - function onNotification(notification: Misskey.entities.Notification, isClient = false) { if (document.visibilityState === 'visible') { if (!isClient && notification.type !== 'test') { @@ -162,10 +95,13 @@ if ($i) { const connection = useStream().useChannel('main', null, 'UI'); connection.on('notification', onNotification); - watch(() => $i?.hasUnreadNotification, (hasAny) => notificationDot.setVisible((defaultStore.state.enableFaviconNotificationDot ? hasAny : false) ?? false)); + watch( + () => $i != null && $i.hasUnreadNotification, + () => setFavIconDot(defaultStore.state.enableFaviconNotificationDot ? $i.hasUnreadNotification : false ), + ); + + if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) setFavIconDot(true); - if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) notificationDot.setVisible(true); - globalEvents.on('clientNotification', notification => onNotification(notification, true)); //#region Listen message from SW