feat: add notification layout setting

This commit is contained in:
pa
2026-03-24 10:22:09 +09:00
parent 0af5e33684
commit bb5a01ae49
7 changed files with 131 additions and 46 deletions

View File

@@ -162,6 +162,7 @@
import { DragDropProvider } from '@dnd-kit/vue';
import { isSortable } from '@dnd-kit/vue/sortable';
import { openExternalLink } from '@/shared/utils/common';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import dayjs from 'dayjs';
@@ -172,7 +173,7 @@
import { isToolNavKey } from '../../shared/constants';
import { navDefinitions } from '../../shared/constants/ui.js';
import { DASHBOARD_NAV_KEY_PREFIX, DEFAULT_DASHBOARD_ICON } from '../../shared/constants/dashboard';
import { useDashboardStore, useModalStore } from '../../stores';
import { useDashboardStore, useModalStore, useNotificationsSettingsStore } from '../../stores';
import SortableTreeNode from './SortableTreeNode.vue';
@@ -207,6 +208,7 @@
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const modalStore = useModalStore();
const { notificationLayout } = storeToRefs(useNotificationsSettingsStore());
const cloneLayout = (source) => {
if (!Array.isArray(source)) return [];
@@ -270,7 +272,12 @@
const map = new Map();
const source = props.definitions?.length ? props.definitions : navDefinitions;
source.forEach((def) => {
if (def?.key) map.set(def.key, def);
if (def?.key) {
if (def.key === 'notification' && notificationLayout.value === 'notification-center') {
return;
}
map.set(def.key, def);
}
});
return map;
});

View File

@@ -30,6 +30,8 @@ import {
} from '../navConfigUtils';
import { normalizeHiddenKeys, sanitizeLayout } from '../navMenuUtils';
import { useNotificationsSettingsStore } from '../../../stores/settings/notifications';
export function useNavLayout({
t,
locale,
@@ -42,15 +44,17 @@ export function useNavLayout({
const navLayout = ref([]);
const navLayoutReady = ref(false);
const navHiddenKeys = ref([]);
const notificationsSettingsStore = useNotificationsSettingsStore();
const allNavDefinitions = computed(() => [
...navDefinitions,
...dashboardStore.getDashboardNavDefinitions()
]);
const navDefinitionMap = computed(() =>
createNavDefinitionMap(allNavDefinitions.value)
);
const navDefinitionMap = computed(() => {
const map = createNavDefinitionMap(allNavDefinitions.value);
return map;
});
// Tool nav items are add/remove only; they no longer participate in hidden state.
const getDefaultHiddenKeys = (layout = []) => {
@@ -60,9 +64,24 @@ export function useNavLayout({
const createDefaultNavLayout = () => createBaseDefaultNavLayout(t);
const menuItems = computed(() =>
buildMenuItems(navLayout.value, navDefinitionMap.value, t)
);
const menuItems = computed(() => {
const items = buildMenuItems(navLayout.value, navDefinitionMap.value, t);
if (notificationsSettingsStore.notificationLayout === 'notification-center') {
return items.filter((item) => {
if (item.index === 'notification') {
return false;
}
if (item.children) {
item.children = item.children.filter(
(child) => child.index !== 'notification'
);
return item.children.length > 0;
}
return true;
});
}
return items;
});
const getFirstNavEntryLocal = (layout) => {
return findFirstNavEntry(layout, navDefinitionMap.value);

View File

@@ -950,6 +950,9 @@
"notifications": {
"notifications": {
"header": "Notifications",
"layout": "Notification Layout",
"layout_notification_center": "Notification Center",
"layout_table": "Table",
"notification_filter": "Notification Filter",
"test_notification": "Test Notification",
"steamvr_notifications": {

View File

@@ -119,6 +119,7 @@ export const useNotificationsSettingsStore = defineStore(
const notificationTTSTest = ref('');
const notificationPosition = ref('topCenter');
const notificationTimeout = ref(3000);
const notificationLayout = ref('notification-center');
async function initNotificationsSettings() {
const [
@@ -136,7 +137,8 @@ export const useNotificationsSettingsStore = defineStore(
sharedFeedFiltersConfig,
notificationTTSVoiceConfig,
notificationPositionConfig,
notificationTimeoutConfig
notificationTimeoutConfig,
notificationLayoutConfig
] = await Promise.all([
configRepository.getString('VRCX_overlayToast', 'Game Running'),
configRepository.getBool('VRCX_overlayNotifications', true),
@@ -158,7 +160,11 @@ export const useNotificationsSettingsStore = defineStore(
'VRCX_notificationPosition',
'topCenter'
),
configRepository.getString('VRCX_notificationTimeout', '3000')
configRepository.getString('VRCX_notificationTimeout', '3000'),
configRepository.getString(
'VRCX_notificationLayout',
'notification-center'
)
]);
overlayToast.value = overlayToastConfig;
@@ -177,6 +183,7 @@ export const useNotificationsSettingsStore = defineStore(
TTSvoices.value = speechSynthesis.getVoices();
notificationPosition.value = notificationPositionConfig;
notificationTimeout.value = Number(notificationTimeoutConfig);
notificationLayout.value = notificationLayoutConfig;
initSharedFeedFilters();
@@ -424,6 +431,14 @@ export const useNotificationsSettingsStore = defineStore(
vrStore.updateVRConfigVars();
}
/**
* @param {string} value
*/
function setNotificationLayout(value) {
notificationLayout.value = value;
configRepository.setString('VRCX_notificationLayout', value);
}
function promptNotificationTimeout() {
modalStore
.prompt({
@@ -470,6 +485,7 @@ export const useNotificationsSettingsStore = defineStore(
notificationTTSTest,
notificationPosition,
notificationTimeout,
notificationLayout,
setOverlayToast,
setOpenVR,
@@ -488,6 +504,7 @@ export const useNotificationsSettingsStore = defineStore(
testNotificationTTS,
speak,
changeNotificationPosition,
setNotificationLayout,
setNotificationTimeout,
promptNotificationTimeout
};

View File

@@ -16,6 +16,7 @@ import { showAvatarDialog } from '../coordinators/avatarCoordinator';
import { showUserDialog } from '../coordinators/userCoordinator';
import { useInstanceStore } from './instance';
import { useNotificationStore } from './notification';
import { useNotificationsSettingsStore } from './settings/notifications';
import { useSearchStore } from './search';
import { useUserStore } from './user';
import { useWorldStore } from './world';
@@ -286,6 +287,11 @@ export const useUiStore = defineStore('Ui', () => {
const name = String(routeName);
removeNotify(name);
if (name === 'notification') {
const notificationsSettingsStore = useNotificationsSettingsStore();
if (notificationsSettingsStore.notificationLayout === 'notification-center') {
router.replace({ name: 'feed' });
return;
}
notificationStore.clearUnseenNotifications();
}
}
@@ -314,10 +320,19 @@ export const useUiStore = defineStore('Ui', () => {
}
function updateTrayIconNotify(force = false) {
const newState =
appearanceSettings.notificationIconDot &&
(notifiedMenus.value.includes('notification') ||
notifiedMenus.value.includes('friend-log'));
const notificationsSettingsStore = useNotificationsSettingsStore();
let newState;
if (notificationsSettingsStore.notificationLayout === 'notification-center') {
newState =
appearanceSettings.notificationIconDot &&
(notificationStore.hasUnseenNotifications ||
notifiedMenus.value.includes('friend-log'));
} else {
newState =
appearanceSettings.notificationIconDot &&
(notifiedMenus.value.includes('notification') ||
notifiedMenus.value.includes('friend-log'));
}
if (trayIconNotify.value !== newState || force) {
trayIconNotify.value = newState;

View File

@@ -1,6 +1,24 @@
<template>
<div class="flex flex-col gap-10 py-2">
<SettingsGroup :title="t('view.settings.notifications.notifications.header')">
<SettingsItem :label="t('view.settings.notifications.notifications.layout')">
<Select
:model-value="notificationLayout"
@update:modelValue="setNotificationLayout">
<SelectTrigger size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="notification-center">{{
t('view.settings.notifications.notifications.layout_notification_center')
}}</SelectItem>
<SelectItem value="table">{{
t('view.settings.notifications.notifications.layout_table')
}}</SelectItem>
</SelectContent>
</Select>
</SettingsItem>
<SettingsItem :label="t('view.settings.notifications.notifications.notification_filter')">
<Button size="sm" variant="outline" @click="showNotyFeedFiltersDialog">{{
t('view.settings.notifications.notifications.notification_filter')
@@ -163,7 +181,8 @@
notificationTTSNickName,
isTestTTSVisible,
notificationTTSTest,
TTSvoices
TTSvoices,
notificationLayout
} = storeToRefs(notificationsSettingsStore);
const {
@@ -173,7 +192,8 @@
getTTSVoiceName,
changeTTSVoice,
saveNotificationTTS,
testNotificationTTS
testNotificationTTS,
setNotificationLayout
} = notificationsSettingsStore;
const { testNotification } = useNotificationStore();

View File

@@ -26,35 +26,37 @@
<RefreshCw v-else />
</Button>
</TooltipWrapper>
<ContextMenu v-if="hasUnseenNotifications">
<ContextMenuTrigger as-child>
<TooltipWrapper side="bottom" :content="t('side_panel.notification_center.title')">
<Button
class="rounded-full relative"
variant="ghost"
size="icon-sm"
@click="isNotificationCenterOpen = !isNotificationCenterOpen">
<Bell />
<span class="absolute top-1 right-1.25 size-1.5 rounded-full bg-red-500" />
</Button>
</TooltipWrapper>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem @click="markNotificationsRead">
{{ t('nav_menu.mark_all_read') }}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<TooltipWrapper v-else side="bottom" :content="t('side_panel.notification_center.title')">
<Button
class="rounded-full relative"
variant="ghost"
size="icon-sm"
@click="isNotificationCenterOpen = !isNotificationCenterOpen"
@contextmenu.prevent="toast.info(t('side_panel.notification_center.no_unseen_notifications'))">
<Bell />
</Button>
</TooltipWrapper>
<template v-if="notificationLayout !== 'table'">
<ContextMenu v-if="hasUnseenNotifications">
<ContextMenuTrigger as-child>
<TooltipWrapper side="bottom" :content="t('side_panel.notification_center.title')">
<Button
class="rounded-full relative"
variant="ghost"
size="icon-sm"
@click="isNotificationCenterOpen = !isNotificationCenterOpen">
<Bell />
<span class="absolute top-1 right-1.25 size-1.5 rounded-full bg-red-500" />
</Button>
</TooltipWrapper>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem @click="markNotificationsRead">
{{ t('nav_menu.mark_all_read') }}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<TooltipWrapper v-else side="bottom" :content="t('side_panel.notification_center.title')">
<Button
class="rounded-full relative"
variant="ghost"
size="icon-sm"
@click="isNotificationCenterOpen = !isNotificationCenterOpen"
@contextmenu.prevent="toast.info(t('side_panel.notification_center.no_unseen_notifications'))">
<Bell />
</Button>
</TooltipWrapper>
</template>
<Popover v-model:open="isSettingsPopoverOpen">
<PopoverTrigger as-child>
<Button class="rounded-full" variant="ghost" size="icon-sm">
@@ -334,7 +336,8 @@
useFavoriteStore,
useFriendStore,
useGroupStore,
useNotificationStore
useNotificationStore,
useNotificationsSettingsStore
} from '../../stores';
import { runRefreshFriendsListFlow } from '../../coordinators/friendSyncCoordinator';
import { normalizeFavoriteGroupsChange, resolveFavoriteGroups } from './sidebarSettingsUtils';
@@ -350,6 +353,7 @@
const { groupInstances } = storeToRefs(useGroupStore());
const notificationStore = useNotificationStore();
const { isNotificationCenterOpen, hasUnseenNotifications } = storeToRefs(notificationStore);
const { notificationLayout } = storeToRefs(useNotificationsSettingsStore());
const quickSearchStore = useQuickSearchStore();
const { t } = useI18n();