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
+9 -2
View File
@@ -162,6 +162,7 @@
import { DragDropProvider } from '@dnd-kit/vue'; import { DragDropProvider } from '@dnd-kit/vue';
import { isSortable } from '@dnd-kit/vue/sortable'; import { isSortable } from '@dnd-kit/vue/sortable';
import { openExternalLink } from '@/shared/utils/common'; import { openExternalLink } from '@/shared/utils/common';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -172,7 +173,7 @@
import { isToolNavKey } from '../../shared/constants'; import { isToolNavKey } from '../../shared/constants';
import { navDefinitions } from '../../shared/constants/ui.js'; import { navDefinitions } from '../../shared/constants/ui.js';
import { DASHBOARD_NAV_KEY_PREFIX, DEFAULT_DASHBOARD_ICON } from '../../shared/constants/dashboard'; 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'; import SortableTreeNode from './SortableTreeNode.vue';
@@ -207,6 +208,7 @@
const { t } = useI18n(); const { t } = useI18n();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const modalStore = useModalStore(); const modalStore = useModalStore();
const { notificationLayout } = storeToRefs(useNotificationsSettingsStore());
const cloneLayout = (source) => { const cloneLayout = (source) => {
if (!Array.isArray(source)) return []; if (!Array.isArray(source)) return [];
@@ -270,7 +272,12 @@
const map = new Map(); const map = new Map();
const source = props.definitions?.length ? props.definitions : navDefinitions; const source = props.definitions?.length ? props.definitions : navDefinitions;
source.forEach((def) => { 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; return map;
}); });
@@ -30,6 +30,8 @@ import {
} from '../navConfigUtils'; } from '../navConfigUtils';
import { normalizeHiddenKeys, sanitizeLayout } from '../navMenuUtils'; import { normalizeHiddenKeys, sanitizeLayout } from '../navMenuUtils';
import { useNotificationsSettingsStore } from '../../../stores/settings/notifications';
export function useNavLayout({ export function useNavLayout({
t, t,
locale, locale,
@@ -42,15 +44,17 @@ export function useNavLayout({
const navLayout = ref([]); const navLayout = ref([]);
const navLayoutReady = ref(false); const navLayoutReady = ref(false);
const navHiddenKeys = ref([]); const navHiddenKeys = ref([]);
const notificationsSettingsStore = useNotificationsSettingsStore();
const allNavDefinitions = computed(() => [ const allNavDefinitions = computed(() => [
...navDefinitions, ...navDefinitions,
...dashboardStore.getDashboardNavDefinitions() ...dashboardStore.getDashboardNavDefinitions()
]); ]);
const navDefinitionMap = computed(() => const navDefinitionMap = computed(() => {
createNavDefinitionMap(allNavDefinitions.value) const map = createNavDefinitionMap(allNavDefinitions.value);
); return map;
});
// Tool nav items are add/remove only; they no longer participate in hidden state. // Tool nav items are add/remove only; they no longer participate in hidden state.
const getDefaultHiddenKeys = (layout = []) => { const getDefaultHiddenKeys = (layout = []) => {
@@ -60,9 +64,24 @@ export function useNavLayout({
const createDefaultNavLayout = () => createBaseDefaultNavLayout(t); const createDefaultNavLayout = () => createBaseDefaultNavLayout(t);
const menuItems = computed(() => const menuItems = computed(() => {
buildMenuItems(navLayout.value, navDefinitionMap.value, t) 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) => { const getFirstNavEntryLocal = (layout) => {
return findFirstNavEntry(layout, navDefinitionMap.value); return findFirstNavEntry(layout, navDefinitionMap.value);
+3
View File
@@ -950,6 +950,9 @@
"notifications": { "notifications": {
"notifications": { "notifications": {
"header": "Notifications", "header": "Notifications",
"layout": "Notification Layout",
"layout_notification_center": "Notification Center",
"layout_table": "Table",
"notification_filter": "Notification Filter", "notification_filter": "Notification Filter",
"test_notification": "Test Notification", "test_notification": "Test Notification",
"steamvr_notifications": { "steamvr_notifications": {
+19 -2
View File
@@ -119,6 +119,7 @@ export const useNotificationsSettingsStore = defineStore(
const notificationTTSTest = ref(''); const notificationTTSTest = ref('');
const notificationPosition = ref('topCenter'); const notificationPosition = ref('topCenter');
const notificationTimeout = ref(3000); const notificationTimeout = ref(3000);
const notificationLayout = ref('notification-center');
async function initNotificationsSettings() { async function initNotificationsSettings() {
const [ const [
@@ -136,7 +137,8 @@ export const useNotificationsSettingsStore = defineStore(
sharedFeedFiltersConfig, sharedFeedFiltersConfig,
notificationTTSVoiceConfig, notificationTTSVoiceConfig,
notificationPositionConfig, notificationPositionConfig,
notificationTimeoutConfig notificationTimeoutConfig,
notificationLayoutConfig
] = await Promise.all([ ] = await Promise.all([
configRepository.getString('VRCX_overlayToast', 'Game Running'), configRepository.getString('VRCX_overlayToast', 'Game Running'),
configRepository.getBool('VRCX_overlayNotifications', true), configRepository.getBool('VRCX_overlayNotifications', true),
@@ -158,7 +160,11 @@ export const useNotificationsSettingsStore = defineStore(
'VRCX_notificationPosition', 'VRCX_notificationPosition',
'topCenter' 'topCenter'
), ),
configRepository.getString('VRCX_notificationTimeout', '3000') configRepository.getString('VRCX_notificationTimeout', '3000'),
configRepository.getString(
'VRCX_notificationLayout',
'notification-center'
)
]); ]);
overlayToast.value = overlayToastConfig; overlayToast.value = overlayToastConfig;
@@ -177,6 +183,7 @@ export const useNotificationsSettingsStore = defineStore(
TTSvoices.value = speechSynthesis.getVoices(); TTSvoices.value = speechSynthesis.getVoices();
notificationPosition.value = notificationPositionConfig; notificationPosition.value = notificationPositionConfig;
notificationTimeout.value = Number(notificationTimeoutConfig); notificationTimeout.value = Number(notificationTimeoutConfig);
notificationLayout.value = notificationLayoutConfig;
initSharedFeedFilters(); initSharedFeedFilters();
@@ -424,6 +431,14 @@ export const useNotificationsSettingsStore = defineStore(
vrStore.updateVRConfigVars(); vrStore.updateVRConfigVars();
} }
/**
* @param {string} value
*/
function setNotificationLayout(value) {
notificationLayout.value = value;
configRepository.setString('VRCX_notificationLayout', value);
}
function promptNotificationTimeout() { function promptNotificationTimeout() {
modalStore modalStore
.prompt({ .prompt({
@@ -470,6 +485,7 @@ export const useNotificationsSettingsStore = defineStore(
notificationTTSTest, notificationTTSTest,
notificationPosition, notificationPosition,
notificationTimeout, notificationTimeout,
notificationLayout,
setOverlayToast, setOverlayToast,
setOpenVR, setOpenVR,
@@ -488,6 +504,7 @@ export const useNotificationsSettingsStore = defineStore(
testNotificationTTS, testNotificationTTS,
speak, speak,
changeNotificationPosition, changeNotificationPosition,
setNotificationLayout,
setNotificationTimeout, setNotificationTimeout,
promptNotificationTimeout promptNotificationTimeout
}; };
+16 -1
View File
@@ -16,6 +16,7 @@ import { showAvatarDialog } from '../coordinators/avatarCoordinator';
import { showUserDialog } from '../coordinators/userCoordinator'; import { showUserDialog } from '../coordinators/userCoordinator';
import { useInstanceStore } from './instance'; import { useInstanceStore } from './instance';
import { useNotificationStore } from './notification'; import { useNotificationStore } from './notification';
import { useNotificationsSettingsStore } from './settings/notifications';
import { useSearchStore } from './search'; import { useSearchStore } from './search';
import { useUserStore } from './user'; import { useUserStore } from './user';
import { useWorldStore } from './world'; import { useWorldStore } from './world';
@@ -286,6 +287,11 @@ export const useUiStore = defineStore('Ui', () => {
const name = String(routeName); const name = String(routeName);
removeNotify(name); removeNotify(name);
if (name === 'notification') { if (name === 'notification') {
const notificationsSettingsStore = useNotificationsSettingsStore();
if (notificationsSettingsStore.notificationLayout === 'notification-center') {
router.replace({ name: 'feed' });
return;
}
notificationStore.clearUnseenNotifications(); notificationStore.clearUnseenNotifications();
} }
} }
@@ -314,10 +320,19 @@ export const useUiStore = defineStore('Ui', () => {
} }
function updateTrayIconNotify(force = false) { function updateTrayIconNotify(force = false) {
const newState = const notificationsSettingsStore = useNotificationsSettingsStore();
let newState;
if (notificationsSettingsStore.notificationLayout === 'notification-center') {
newState =
appearanceSettings.notificationIconDot &&
(notificationStore.hasUnseenNotifications ||
notifiedMenus.value.includes('friend-log'));
} else {
newState =
appearanceSettings.notificationIconDot && appearanceSettings.notificationIconDot &&
(notifiedMenus.value.includes('notification') || (notifiedMenus.value.includes('notification') ||
notifiedMenus.value.includes('friend-log')); notifiedMenus.value.includes('friend-log'));
}
if (trayIconNotify.value !== newState || force) { if (trayIconNotify.value !== newState || force) {
trayIconNotify.value = newState; trayIconNotify.value = newState;
@@ -1,6 +1,24 @@
<template> <template>
<div class="flex flex-col gap-10 py-2"> <div class="flex flex-col gap-10 py-2">
<SettingsGroup :title="t('view.settings.notifications.notifications.header')"> <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')"> <SettingsItem :label="t('view.settings.notifications.notifications.notification_filter')">
<Button size="sm" variant="outline" @click="showNotyFeedFiltersDialog">{{ <Button size="sm" variant="outline" @click="showNotyFeedFiltersDialog">{{
t('view.settings.notifications.notifications.notification_filter') t('view.settings.notifications.notifications.notification_filter')
@@ -163,7 +181,8 @@
notificationTTSNickName, notificationTTSNickName,
isTestTTSVisible, isTestTTSVisible,
notificationTTSTest, notificationTTSTest,
TTSvoices TTSvoices,
notificationLayout
} = storeToRefs(notificationsSettingsStore); } = storeToRefs(notificationsSettingsStore);
const { const {
@@ -173,7 +192,8 @@
getTTSVoiceName, getTTSVoiceName,
changeTTSVoice, changeTTSVoice,
saveNotificationTTS, saveNotificationTTS,
testNotificationTTS testNotificationTTS,
setNotificationLayout
} = notificationsSettingsStore; } = notificationsSettingsStore;
const { testNotification } = useNotificationStore(); const { testNotification } = useNotificationStore();
+5 -1
View File
@@ -26,6 +26,7 @@
<RefreshCw v-else /> <RefreshCw v-else />
</Button> </Button>
</TooltipWrapper> </TooltipWrapper>
<template v-if="notificationLayout !== 'table'">
<ContextMenu v-if="hasUnseenNotifications"> <ContextMenu v-if="hasUnseenNotifications">
<ContextMenuTrigger as-child> <ContextMenuTrigger as-child>
<TooltipWrapper side="bottom" :content="t('side_panel.notification_center.title')"> <TooltipWrapper side="bottom" :content="t('side_panel.notification_center.title')">
@@ -55,6 +56,7 @@
<Bell /> <Bell />
</Button> </Button>
</TooltipWrapper> </TooltipWrapper>
</template>
<Popover v-model:open="isSettingsPopoverOpen"> <Popover v-model:open="isSettingsPopoverOpen">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Button class="rounded-full" variant="ghost" size="icon-sm"> <Button class="rounded-full" variant="ghost" size="icon-sm">
@@ -334,7 +336,8 @@
useFavoriteStore, useFavoriteStore,
useFriendStore, useFriendStore,
useGroupStore, useGroupStore,
useNotificationStore useNotificationStore,
useNotificationsSettingsStore
} from '../../stores'; } from '../../stores';
import { runRefreshFriendsListFlow } from '../../coordinators/friendSyncCoordinator'; import { runRefreshFriendsListFlow } from '../../coordinators/friendSyncCoordinator';
import { normalizeFavoriteGroupsChange, resolveFavoriteGroups } from './sidebarSettingsUtils'; import { normalizeFavoriteGroupsChange, resolveFavoriteGroups } from './sidebarSettingsUtils';
@@ -350,6 +353,7 @@
const { groupInstances } = storeToRefs(useGroupStore()); const { groupInstances } = storeToRefs(useGroupStore());
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
const { isNotificationCenterOpen, hasUnseenNotifications } = storeToRefs(notificationStore); const { isNotificationCenterOpen, hasUnseenNotifications } = storeToRefs(notificationStore);
const { notificationLayout } = storeToRefs(useNotificationsSettingsStore());
const quickSearchStore = useQuickSearchStore(); const quickSearchStore = useQuickSearchStore();
const { t } = useI18n(); const { t } = useI18n();