Feat: allow selecting browser as target for toasts

This commit is contained in:
arabcoders 2025-11-13 22:21:16 +03:00
parent d835c027d5
commit 8e6f5fc293
3 changed files with 127 additions and 24 deletions

View file

@ -29,8 +29,6 @@
</template>
<script setup lang="ts">
import {defineEmits, defineProps} from 'vue'
withDefaults(defineProps<{
/** Title text for the notification */
title?: string | null
@ -64,4 +62,4 @@ defineEmits<{
/** Emitted when the close button is clicked */
(e: 'close'): void
}>()
</script>
</script>

View file

@ -2,6 +2,7 @@ import { useStorage } from '@vueuse/core'
import { POSITION, useToast } from "vue-toastification"
export type notificationType = 'info' | 'success' | 'warning' | 'error'
export type notificationTarget = 'toast' | 'browser'
export interface notification {
id: string;
@ -25,9 +26,77 @@ export interface notificationOptions {
const allowToast = useStorage<boolean>('allow_toasts', true)
const toastPosition = useStorage<POSITION>('toast_position', POSITION.TOP_RIGHT)
const toastDismissOnClick = useStorage<boolean>('toast_dismiss_on_click', true)
const toastTarget = useStorage<notificationTarget>('toast_target', 'toast')
const toast = useToast()
function notify(type: notificationType, message: string, opts?: notificationOptions): void {
const requestBrowserPermission = async (): Promise<NotificationPermission> => {
if (!('Notification' in window)) {
return 'denied'
}
if (Notification.permission === 'granted') {
return 'granted'
}
if (Notification.permission !== 'denied') {
return await Notification.requestPermission()
}
return Notification.permission
}
const sendMessage = (type: notificationType, id: string, message: string, opts?: notificationOptions): void => {
const notificationStore = useNotificationStore()
const useToastNotification = !window.isSecureContext || 'toast' === toastTarget.value ||
!('Notification' in window) || 'granted' !== Notification.permission;
console.log('useToastNotification', useToastNotification,{
windowIsSecureContext: window.isSecureContext,
notificationTarget: toastTarget.value,
notificationInWindow: 'Notification' in window,
notificationPermission: Notification.permission
});
if (useToastNotification) {
switch (type) {
case 'info':
toast.info(message, opts)
break;
case 'success':
toast.success(message, opts)
break;
case 'warning':
toast.warning(message, opts)
break;
case 'error':
toast.error(message, opts)
break;
default:
toast.error(`Unknown notification type: ${type}. ${message}`, opts)
break;
}
return
}
const notification = new Notification('YTPTube', {
body: message,
icon: '/favicon.ico',
badge: '/favicon.ico',
tag: `ytptube-${type}`,
silent: false
})
setTimeout(() => notification.close(), 5000)
notification.onclick = () => {
notificationStore.markRead(id)
window.focus()
notification.close()
}
}
const notify = (type: notificationType, message: string, opts?: notificationOptions): void => {
const notificationStore = useNotificationStore()
if (!opts) {
@ -63,23 +132,7 @@ function notify(type: notificationType, message: string, opts?: notificationOpti
}
}
switch (type) {
case 'info':
toast.info(message, opts)
break;
case 'success':
toast.success(message, opts)
break;
case 'warning':
toast.warning(message, opts)
break;
case 'error':
toast.error(message, opts)
break;
default:
toast.error(`Unknown notification type: ${type}. ${message}`, opts)
break;
}
sendMessage(type, id, message, opts)
}
export const useNotification = () => {
@ -88,6 +141,7 @@ export const useNotification = () => {
success: (message: string, opts?: notificationOptions) => notify('success', message, opts),
warning: (message: string, opts?: notificationOptions) => notify('warning', message, opts),
error: (message: string, opts?: notificationOptions) => notify('error', message, opts),
notify
notify,
requestBrowserPermission,
}
}

View file

@ -199,7 +199,7 @@
<span class="field title is-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-bell" /></span>
<p class="card-header-title">On-Screen Notifications</p>
<p class="card-header-title">Notifications</p>
</span>
</span>
<span class="field is-horizontal" />
@ -222,6 +222,33 @@
</div>
<div class="field is-horizontal" v-if="allow_toasts">
<div class="field-label is-normal">
<label class="label">Notification target</label>
</div>
<div class="field-body">
<div class="field is-narrow">
<div class="control">
<div class="select is-fullwidth">
<select v-model="toast_target" @change="onNotificationTargetChange">
<option value="toast">Toast</option>
<option value="browser" :disabled="!isSecureContext">Browser</option>
</select>
</div>
</div>
<p class="help">
<span class="icon"><i class="fa-solid fa-info-circle" /></span>
<template v-if="!isSecureContext">
Browser notifications require HTTPS connection.
</template>
<template v-else>
Choose where to display notifications. Browser requires permission.
</template>
</p>
</div>
</div>
</div>
<div class="field is-horizontal" v-if="allow_toasts && toast_target === 'toast'">
<div class="field-label is-normal">
<label class="label">Notifications position</label>
</div>
@ -243,7 +270,7 @@
</div>
</div>
<div class="field is-horizontal" v-if="allow_toasts">
<div class="field is-horizontal" v-if="allow_toasts && toast_target === 'toast'">
<div class="field-label is-normal">
<label class="label">Dismiss notification on click</label>
</div>
@ -266,13 +293,17 @@
<script setup lang="ts">
import 'assets/css/bulma-switch.css'
import { useStorage } from '@vueuse/core'
import { ref, onMounted } from 'vue'
import { POSITION } from 'vue-toastification'
import { useConfigStore } from '~/stores/ConfigStore'
import { useNotification } from '~/composables/useNotification'
import type { notificationTarget } from '~/composables/useNotification'
defineProps<{ isLoading: boolean }>()
defineEmits<{ (e: 'reload_bg'): void }>()
const config = useConfigStore()
const notification = useNotification()
const bg_enable = useStorage<boolean>('random_bg', true)
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95)
@ -280,8 +311,28 @@ const selectedTheme = useStorage<'auto' | 'light' | 'dark'>('theme', 'auto')
const allow_toasts = useStorage<boolean>('allow_toasts', true)
const toast_position = useStorage<POSITION>('toast_position', POSITION.TOP_RIGHT)
const toast_dismiss_on_click = useStorage<boolean>('toast_dismiss_on_click', true)
const toast_target = useStorage<notificationTarget>('toast_target', 'toast')
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'is-3by1')
const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',')
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false)
const isSecureContext = ref<boolean>(false)
onMounted(async () => {
isSecureContext.value = window.isSecureContext
await nextTick()
if ('browser' === toast_target.value && !isSecureContext.value) {
toast_target.value = 'toast'
}
})
const onNotificationTargetChange = async (): Promise<void> => {
if ('browser' === toast_target.value) {
const permission = await notification.requestBrowserPermission()
if ('granted' !== permission) {
toast_target.value = 'toast'
notification.warning('Browser notification permission denied. Reverting to toast notifications.')
}
}
}
</script>