<template> <q-layout view="hHh Lpr lff"> <q-header elevated class="bg-primary text-white"> <q-toolbar> <q-btn dense flat round icon="mdi-menu" @click="openMenu" /> <q-toolbar-title> <router-link :to="{ name: 'dashboard' }" style="text-decoration: none; color: inherit"> <q-avatar rounded> <img src="flaschengeist-logo-white.svg" /> </q-avatar> <span class="gt-xs"> Flaschengeist </span> </router-link> </q-toolbar-title> <!-- Hier kommen die Shortlinks hin --> <q-btn icon="mdi-message-bulleted" flat dense round> <q-badge color="negative" floating> {{ notifications.length }} </q-badge> <q-menu max-height="400px" style="min-width: 290px" class="q-pa-xs"> <q-btn v-if="useNative && noPermission" label="Benachrichtigungen erlauben" @click="requestPermission" /> <template v-if="notifications.length > 0"> <Notification v-for="(notification, index) in notifications" :key="index" :model-value="notification" @remove="remove" /> </template> <div v-else class="q-pa-sm">Keine neuen Benachrichtigungen</div> </q-menu> </q-btn> <drag v-model="shortCuts" item-key="link" ghost-class="ghost"> <template #item="{ element }"> <shortcut-link :shortcut="element" context @delete-shortcut="deleteShortcut" /> </template> </drag> <q-btn flat round dense icon="mdi-exit-to-app" @click="logout()" /> </q-toolbar> </q-header> <q-drawer v-model="leftDrawer" side="left" bordered :mini="leftDrawerMini" @click.capture="openMenu" > <!-- Plugins --> <essential-expansion-link v-for="(entry, index) in mainLinks" :key="'plugin' + index" :entry="entry" @add-short-cut="addShortcut" /> <q-separator /> <essential-link v-for="(entry, index) in essentials" :key="'essential' + index" :entry="entry" /> </q-drawer> <q-page-container> <router-view /> </q-page-container> </q-layout> </template> <script lang="ts"> import EssentialExpansionLink from 'components/navigation/EssentialExpansionLink.vue'; import EssentialLink from 'src/components/navigation/EssentialLink.vue'; import ShortcutLink from 'src/components/navigation/ShortcutLink.vue'; import Notification from 'src/components/Notification.vue'; import { defineComponent, ref, inject, computed, onBeforeMount, onBeforeUnmount } from 'vue'; import { Screen, Platform } from 'quasar'; import config from 'src/config'; import { useRouter } from 'vue-router'; import { useMainStore } from '@flaschengeist/api'; import { FG_Plugin } from '@flaschengeist/types'; import drag from 'vuedraggable'; const essentials: FG_Plugin.MenuLink[] = [ { title: 'Über Flaschengeist', link: 'about', icon: 'mdi-information', }, ]; export default defineComponent({ name: 'MainLayout', components: { EssentialExpansionLink, EssentialLink, ShortcutLink, Notification, drag, }, setup() { const router = useRouter(); const mainStore = useMainStore(); const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist'); const leftDrawer = ref(!Platform.is.mobile); const leftDrawerMini = ref(false); const mainLinks = flaschengeist?.menuLinks || []; const notifications = computed(() => mainStore.notifications.slice().reverse()); const polling = ref(NaN); const useNative = 'Notification' in window && window.Notification !== undefined; const noPermission = ref(!useNative || window.Notification.permission !== 'granted'); onBeforeMount(() => { polling.value = window.setInterval(() => pollNotification(), config.pollingInterval); pollNotification(); void mainStore.getShortcuts(); }); onBeforeUnmount(() => window.clearInterval(polling.value)); function openMenu(event: { target: HTMLInputElement }) { if (event.target.nodeName === 'DIV') leftDrawerMini.value = false; else { if (!leftDrawer.value || leftDrawerMini.value) { leftDrawer.value = true; leftDrawerMini.value = false; } else { leftDrawerMini.value = Screen.gt.sm && !leftDrawerMini.value; leftDrawer.value = leftDrawerMini.value; } } } function logout() { void router.push({ name: 'login', params: { logout: 'logout' } }); void mainStore.logout(); } async function remove(id: number) { await mainStore.removeNotification(id); } function requestPermission() { void window.Notification.requestPermission().then( (p) => (noPermission.value = p !== 'granted') ); } function pollNotification() { void mainStore .loadNotifications(<FG_Plugin.Flaschengeist>flaschengeist) .then((notifications) => { if (useNative && !noPermission.value) notifications.forEach( (notif) => new window.Notification(notif.text, { timestamp: notif.time.getTime(), }) ); }); } const shortCuts = computed({ get: () => mainStore.shortcuts, set: (val: Array<FG_Plugin.MenuLink>) => { mainStore.shortcuts = val; void mainStore.setShortcuts(); }, }); function addShortcut(val: FG_Plugin.MenuLink) { const idx = shortCuts.value.findIndex((a: FG_Plugin.MenuLink) => a.link === val.link); if (idx < 0) { shortCuts.value.push(val); void mainStore.setShortcuts(); } } function deleteShortcut(val: FG_Plugin.MenuLink) { const idx = shortCuts.value.findIndex((a: FG_Plugin.MenuLink) => a.link === val.link); if (idx > -1) { shortCuts.value.splice(idx, 1); void mainStore.setShortcuts(); } } return { essentials, leftDrawer, leftDrawerMini, logout, mainLinks, notifications, noPermission, openMenu, remove, requestPermission, useNative, shortCuts, addShortcut, deleteShortcut, }; }, }); </script> <style scoped lang="sass"> .ghost opacity: 0.5 background: $accent </style>