mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 22:33:50 +02:00
feat: add tool nav pinning and unpinning
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Dialog :open="visible" @update:open="(open) => (open ? null : handleClose())">
|
||||
<DialogContent class="sm:min-w-140">
|
||||
<DialogContent class="sm:min-w-180">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t('nav_menu.custom_nav.dialog_title') }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -169,6 +169,7 @@
|
||||
import { InputGroupButton, InputGroupField } from '../ui/input-group';
|
||||
import { Separator } from '../ui/separator';
|
||||
import { Tree } from '../ui/tree';
|
||||
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';
|
||||
@@ -188,6 +189,10 @@
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
defaultHiddenKeys: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
defaultLayout: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -305,7 +310,10 @@
|
||||
|
||||
const hiddenItems = computed(() =>
|
||||
(props.definitions?.length ? props.definitions : navDefinitions)
|
||||
.filter((def) => hiddenKeySet.value.has(def.key))
|
||||
.filter(
|
||||
(def) =>
|
||||
hiddenKeySet.value.has(def.key) && !isToolNavKey(def.key)
|
||||
)
|
||||
.map((def) => ({
|
||||
key: def.key,
|
||||
icon: def.icon,
|
||||
@@ -322,6 +330,11 @@
|
||||
};
|
||||
|
||||
const handleHideItem = (key) => {
|
||||
if (isToolNavKey(key)) {
|
||||
removeFromLayout(key);
|
||||
return;
|
||||
}
|
||||
|
||||
let placement = null;
|
||||
for (let i = 0; i < localLayout.value.length; i++) {
|
||||
const entry = localLayout.value[i];
|
||||
@@ -789,7 +802,7 @@
|
||||
|
||||
const handleReset = () => {
|
||||
localLayout.value = cloneLayout(props.defaultLayout || []);
|
||||
hiddenKeySet.value = new Set();
|
||||
hiddenKeySet.value = new Set(props.defaultHiddenKeys || []);
|
||||
hiddenPlacement.value = new Map();
|
||||
expandedKeys.value = localLayout.value.filter((e) => e.type === 'folder').map((e) => e.id);
|
||||
};
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
const isDashboard = computed(() => {
|
||||
return !isFolder.value && nodeValue.value?.key?.startsWith('dashboard-');
|
||||
});
|
||||
const isTool = computed(() => {
|
||||
return !isFolder.value && nodeValue.value?.key?.startsWith('tool-');
|
||||
});
|
||||
const hasChildren = computed(() => props.item.hasChildren);
|
||||
const level = computed(() => nodeValue.value?.level ?? 0);
|
||||
const nodeId = computed(() => (isFolder.value ? nodeValue.value?.id : nodeValue.value?.key));
|
||||
@@ -127,7 +130,11 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<DropdownMenuItem @click="emit('hide', nodeValue.key)">
|
||||
{{ t('nav_menu.custom_nav.hide') }}
|
||||
{{
|
||||
isTool
|
||||
? t('common.actions.delete')
|
||||
: t('nav_menu.custom_nav.hide')
|
||||
}}
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -66,9 +66,12 @@
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
</template>
|
||||
<ContextMenuItem @click="handleQuickCreateDashboard">
|
||||
{{ t('dashboard.new_dashboard') }}
|
||||
<ContextMenuItem
|
||||
v-if="isToolItem(item)"
|
||||
@click="handleUnpinToolItem(item)">
|
||||
{{ t('nav_menu.custom_nav.unpin_from_nav') }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator v-if="isToolItem(item)" />
|
||||
<ContextMenuItem @click="handleOpenCustomNavDialog">
|
||||
{{ t('nav_menu.custom_nav.header') }}
|
||||
</ContextMenuItem>
|
||||
@@ -86,13 +89,14 @@
|
||||
:is-entry-notified="isEntryNotified"
|
||||
:is-nav-item-notified="isNavItemNotified"
|
||||
:is-dashboard-item="isDashboardItem"
|
||||
:is-tool-item="isToolItem"
|
||||
@collapsed-dropdown-open-change="handleCollapsedDropdownOpenChange"
|
||||
@collapsed-submenu-select="handleCollapsedSubmenuSelect"
|
||||
@submenu-click="handleSubmenuClick"
|
||||
@clear-notifications="clearAllNotifications"
|
||||
@edit-dashboard="handleEditDashboard"
|
||||
@delete-dashboard="handleDeleteDashboard"
|
||||
@create-dashboard="handleQuickCreateDashboard"
|
||||
@unpin-tool="handleUnpinToolItem"
|
||||
@open-custom-nav="handleOpenCustomNavDialog" />
|
||||
</template>
|
||||
</SidebarMenu>
|
||||
@@ -147,6 +151,7 @@
|
||||
v-model:visible="customNavDialogVisible"
|
||||
:layout="navLayout"
|
||||
:hidden-keys="navHiddenKeys"
|
||||
:default-hidden-keys="defaultHiddenKeys"
|
||||
:default-layout="defaultNavLayout"
|
||||
:definitions="allNavDefinitions"
|
||||
@save="handleCustomNavSave"
|
||||
@@ -163,6 +168,8 @@
|
||||
|
||||
import { useNavLayout } from './composables/useNavLayout';
|
||||
import { useNavTheme } from './composables/useNavTheme';
|
||||
import { useToolActions } from '../../composables/useToolActions';
|
||||
import { useToolNavPinning } from '../../composables/useToolNavPinning';
|
||||
import { Kbd } from '@/components/ui/kbd';
|
||||
import {
|
||||
ContextMenu,
|
||||
@@ -216,6 +223,8 @@
|
||||
const { clearAllNotifications } = uiStore;
|
||||
|
||||
const { directAccessPaste } = useSearchStore();
|
||||
const { triggerTool } = useToolActions();
|
||||
const { unpinToolFromNav } = useToolNavPinning();
|
||||
const { logout } = useAuthStore();
|
||||
const modalStore = useModalStore();
|
||||
|
||||
@@ -243,6 +252,7 @@
|
||||
navLayout,
|
||||
navLayoutReady,
|
||||
navHiddenKeys,
|
||||
defaultHiddenKeys,
|
||||
menuItems,
|
||||
activeMenuIndex,
|
||||
allNavDefinitions,
|
||||
@@ -258,7 +268,8 @@
|
||||
router,
|
||||
dashboardStore,
|
||||
dashboards,
|
||||
directAccessPaste
|
||||
directAccessPaste,
|
||||
triggerTool
|
||||
});
|
||||
|
||||
const collapsedDropdownOpenId = ref(null);
|
||||
@@ -321,6 +332,14 @@
|
||||
};
|
||||
|
||||
const isDashboardItem = (item) => item?.index?.startsWith(DASHBOARD_NAV_KEY_PREFIX);
|
||||
const isToolItem = (item) => item?.index?.startsWith('tool-');
|
||||
|
||||
const handleUnpinToolItem = async (item) => {
|
||||
if (!isToolItem(item)) {
|
||||
return;
|
||||
}
|
||||
await unpinToolFromNav(item.index.replace(/^tool-/, ''));
|
||||
};
|
||||
|
||||
const handleQuickCreateDashboard = async () => {
|
||||
const dashboard = await dashboardStore.createDashboard(t('dashboard.default_name'));
|
||||
|
||||
@@ -110,9 +110,12 @@
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
</template>
|
||||
<ContextMenuItem @click="emit('create-dashboard')">
|
||||
{{ t('dashboard.new_dashboard') }}
|
||||
<ContextMenuItem
|
||||
v-if="isToolItem(entry)"
|
||||
@click="emit('unpin-tool', entry)">
|
||||
{{ t('nav_menu.custom_nav.unpin_from_nav') }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator v-if="isToolItem(entry)" />
|
||||
<ContextMenuItem @click="emit('open-custom-nav')">
|
||||
{{ t('nav_menu.custom_nav.header') }}
|
||||
</ContextMenuItem>
|
||||
@@ -130,9 +133,6 @@
|
||||
{{ t('nav_menu.mark_all_read') }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator v-if="hasNotifications" />
|
||||
<ContextMenuItem @click="emit('create-dashboard')">
|
||||
{{ t('dashboard.new_dashboard') }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="emit('open-custom-nav')">
|
||||
{{ t('nav_menu.custom_nav.header') }}
|
||||
</ContextMenuItem>
|
||||
@@ -199,6 +199,10 @@
|
||||
isDashboardItem: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
isToolItem: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
@@ -209,7 +213,7 @@
|
||||
'clear-notifications',
|
||||
'edit-dashboard',
|
||||
'delete-dashboard',
|
||||
'create-dashboard',
|
||||
'unpin-tool',
|
||||
'open-custom-nav'
|
||||
]);
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -115,6 +115,20 @@ vi.mock('../../../stores', () => ({
|
||||
useAuthStore: () => ({
|
||||
logout: (...args) => mocks.logout(...args)
|
||||
}),
|
||||
useToolsStore: () => ({
|
||||
openDialog: vi.fn(),
|
||||
closeDialog: vi.fn(),
|
||||
closeAllDialogs: vi.fn()
|
||||
}),
|
||||
useAdvancedSettingsStore: () => ({
|
||||
showVRChatConfig: vi.fn()
|
||||
}),
|
||||
useLaunchStore: () => ({
|
||||
showLaunchOptions: vi.fn()
|
||||
}),
|
||||
useVrcxStore: () => ({
|
||||
showRegistryBackupDialog: vi.fn()
|
||||
}),
|
||||
useAppearanceSettingsStore: () => ({
|
||||
themeMode: mocks.themeMode,
|
||||
tableDensity: mocks.tableDensity,
|
||||
@@ -148,11 +162,13 @@ vi.mock('../../../services/config', () => ({
|
||||
|
||||
vi.mock('../../../shared/constants', () => ({
|
||||
DASHBOARD_NAV_KEY_PREFIX: 'dashboard-',
|
||||
defaultHiddenToolNavKeys: [],
|
||||
THEME_CONFIG: {
|
||||
system: { name: 'System' },
|
||||
light: { name: 'Light' },
|
||||
dark: { name: 'Dark' }
|
||||
},
|
||||
isToolNavKey: (key) => typeof key === 'string' && key.startsWith('tool-'),
|
||||
links: {
|
||||
github: 'https://github.com/vrcx-team/VRCX'
|
||||
},
|
||||
@@ -171,7 +187,9 @@ vi.mock('../../../shared/constants', () => ({
|
||||
tooltip: 'nav_tooltip.direct_access',
|
||||
icon: 'ri-door-open-line'
|
||||
}
|
||||
]
|
||||
],
|
||||
toolDefinitionMap: new Map(),
|
||||
toolDefinitions: []
|
||||
}));
|
||||
|
||||
vi.mock('../navMenuUtils', () => ({
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import configRepository from '../../../services/config';
|
||||
import {
|
||||
DASHBOARD_NAV_KEY_PREFIX,
|
||||
isToolNavKey,
|
||||
navDefinitions
|
||||
} from '../../../shared/constants';
|
||||
import { triggerNavEntryAction } from '../navActionUtils';
|
||||
import {
|
||||
buildMenuItems,
|
||||
collectLayoutKeys,
|
||||
findFirstNavEntry,
|
||||
findFirstNavKey
|
||||
} from '../navLayoutHelpers';
|
||||
import {
|
||||
createBaseDefaultNavLayout,
|
||||
insertDashboardEntries
|
||||
} from '../navLayoutDefaults';
|
||||
import {
|
||||
dispatchNavLayoutUpdated,
|
||||
NAV_LAYOUT_UPDATED_EVENT
|
||||
} from '../navLayoutEvents';
|
||||
import {
|
||||
buildNavDefinitionsForLayout,
|
||||
createNavDefinitionMap,
|
||||
generateNavFolderId,
|
||||
loadStoredNavConfig,
|
||||
NAV_CONFIG_KEY
|
||||
} from '../navConfigUtils';
|
||||
import { normalizeHiddenKeys, sanitizeLayout } from '../navMenuUtils';
|
||||
|
||||
export function useNavLayout({
|
||||
@@ -22,7 +35,8 @@ export function useNavLayout({
|
||||
router,
|
||||
dashboardStore,
|
||||
dashboards,
|
||||
directAccessPaste
|
||||
directAccessPaste,
|
||||
triggerTool
|
||||
}) {
|
||||
const navLayout = ref([]);
|
||||
const navLayoutReady = ref(false);
|
||||
@@ -33,49 +47,17 @@ export function useNavLayout({
|
||||
...dashboardStore.getDashboardNavDefinitions()
|
||||
]);
|
||||
|
||||
const navDefinitionMap = computed(() => {
|
||||
const map = new Map();
|
||||
allNavDefinitions.value.forEach((item) => {
|
||||
map.set(item.key, item);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
const navDefinitionMap = computed(() =>
|
||||
createNavDefinitionMap(allNavDefinitions.value)
|
||||
);
|
||||
|
||||
const createDefaultNavLayout = () => [
|
||||
{ type: 'item', key: 'feed' },
|
||||
{ type: 'item', key: 'friends-locations' },
|
||||
{ type: 'item', key: 'game-log' },
|
||||
{ type: 'item', key: 'player-list' },
|
||||
{ type: 'item', key: 'search' },
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'default-folder-favorites',
|
||||
nameKey: 'nav_tooltip.favorites',
|
||||
name: t('nav_tooltip.favorites'),
|
||||
icon: 'ri-star-line',
|
||||
items: ['favorite-friends', 'favorite-worlds', 'favorite-avatars']
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'default-folder-social',
|
||||
nameKey: 'nav_tooltip.social',
|
||||
name: t('nav_tooltip.social'),
|
||||
icon: 'ri-group-line',
|
||||
items: ['friend-log', 'friend-list', 'moderation']
|
||||
},
|
||||
{ type: 'item', key: 'notification' },
|
||||
{ type: 'item', key: 'my-avatars' },
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'default-folder-charts',
|
||||
nameKey: 'nav_tooltip.charts',
|
||||
name: t('nav_tooltip.charts'),
|
||||
icon: 'ri-pie-chart-line',
|
||||
items: ['charts-instance', 'charts-mutual']
|
||||
},
|
||||
{ type: 'item', key: 'tools' },
|
||||
{ type: 'item', key: 'direct-access' }
|
||||
];
|
||||
// Tool nav items are add/remove only; they no longer participate in hidden state.
|
||||
const getDefaultHiddenKeys = (layout = []) => {
|
||||
void layout;
|
||||
return [];
|
||||
};
|
||||
|
||||
const createDefaultNavLayout = () => createBaseDefaultNavLayout(t);
|
||||
|
||||
const menuItems = computed(() =>
|
||||
buildMenuItems(navLayout.value, navDefinitionMap.value, t)
|
||||
@@ -95,43 +77,36 @@ export function useNavLayout({
|
||||
return `${DASHBOARD_NAV_KEY_PREFIX}${currentRoute.params.id}`;
|
||||
}
|
||||
const currentRouteName = currentRoute?.name;
|
||||
const navKey = currentRoute?.meta?.navKey || currentRouteName;
|
||||
if (!navKey) {
|
||||
const navKeys = Array.isArray(currentRoute?.meta?.navKeys)
|
||||
? currentRoute.meta.navKeys
|
||||
: [currentRoute?.meta?.navKey || currentRouteName].filter(Boolean);
|
||||
if (!navKeys.length) {
|
||||
return getFirstNavKeyLocal(navLayout.value) || 'feed';
|
||||
}
|
||||
|
||||
for (const entry of navLayout.value) {
|
||||
if (entry.type === 'item' && entry.key === navKey) {
|
||||
if (entry.type === 'item' && navKeys.includes(entry.key)) {
|
||||
return entry.key;
|
||||
}
|
||||
if (entry.type === 'folder' && entry.items?.includes(navKey)) {
|
||||
return navKey;
|
||||
if (entry.type === 'folder') {
|
||||
const matchedKey = navKeys.find((key) =>
|
||||
entry.items?.includes(key)
|
||||
);
|
||||
if (matchedKey) {
|
||||
return matchedKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
return getFirstNavKeyLocal(navLayout.value) || 'feed';
|
||||
});
|
||||
|
||||
const generateFolderId = () => {
|
||||
if (
|
||||
typeof crypto !== 'undefined' &&
|
||||
typeof crypto.randomUUID === 'function'
|
||||
) {
|
||||
return `nav-folder-${crypto.randomUUID()}`;
|
||||
}
|
||||
return `nav-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 4)}`;
|
||||
};
|
||||
|
||||
const getAppendDefinitions = (layout, hiddenKeys = []) => {
|
||||
const keysInLayout = collectLayoutKeys(layout);
|
||||
const hiddenSet = new Set(Array.isArray(hiddenKeys) ? hiddenKeys : []);
|
||||
const dashboardDefinitions = dashboardStore
|
||||
.getDashboardNavDefinitions()
|
||||
.filter(
|
||||
(definition) =>
|
||||
keysInLayout.has(definition.key) ||
|
||||
hiddenSet.has(definition.key)
|
||||
);
|
||||
return [...navDefinitions, ...dashboardDefinitions];
|
||||
return buildNavDefinitionsForLayout(
|
||||
navDefinitions,
|
||||
dashboardStore.getDashboardNavDefinitions(),
|
||||
layout,
|
||||
hiddenKeys
|
||||
);
|
||||
};
|
||||
|
||||
const sanitizeLayoutLocal = (layout, hiddenKeys = []) => {
|
||||
@@ -141,41 +116,36 @@ export function useNavLayout({
|
||||
navDefinitionMap.value,
|
||||
getAppendDefinitions(layout, hiddenKeys),
|
||||
t,
|
||||
generateFolderId
|
||||
generateNavFolderId
|
||||
);
|
||||
};
|
||||
|
||||
const defaultNavLayout = computed(() => {
|
||||
const base = createDefaultNavLayout();
|
||||
const dashboardEntries = dashboardStore
|
||||
.getDashboardNavDefinitions()
|
||||
.map((def) => ({ type: 'item', key: def.key }));
|
||||
if (dashboardEntries.length) {
|
||||
const directAccessIdx = base.findIndex(
|
||||
(entry) =>
|
||||
entry.type === 'item' && entry.key === 'direct-access'
|
||||
);
|
||||
if (directAccessIdx !== -1) {
|
||||
base.splice(directAccessIdx, 0, ...dashboardEntries);
|
||||
} else {
|
||||
base.push(...dashboardEntries);
|
||||
}
|
||||
}
|
||||
return sanitizeLayoutLocal(base, []);
|
||||
const base = insertDashboardEntries(
|
||||
createDefaultNavLayout(),
|
||||
dashboardStore.getDashboardNavDefinitions()
|
||||
);
|
||||
return sanitizeLayoutLocal(base, getDefaultHiddenKeys(base));
|
||||
});
|
||||
|
||||
const triggerNavAction = (entry) => {
|
||||
triggerNavEntryAction(entry, { router, directAccessPaste });
|
||||
const action = triggerNavEntryAction(entry, {
|
||||
router,
|
||||
directAccessPaste
|
||||
});
|
||||
if (action?.type === 'tool' && action.toolKey) {
|
||||
triggerTool?.(action.toolKey);
|
||||
}
|
||||
};
|
||||
|
||||
const saveNavLayout = async (layout, hiddenKeys = []) => {
|
||||
const normalizedHiddenKeys = normalizeHiddenKeys(
|
||||
hiddenKeys,
|
||||
[...hiddenKeys, ...getDefaultHiddenKeys(layout)],
|
||||
navDefinitionMap.value
|
||||
);
|
||||
try {
|
||||
await configRepository.setString(
|
||||
'VRCX_customNavMenuLayoutList',
|
||||
NAV_CONFIG_KEY,
|
||||
JSON.stringify({
|
||||
layout,
|
||||
hiddenKeys: normalizedHiddenKeys
|
||||
@@ -183,12 +153,15 @@ export function useNavLayout({
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to save custom nav', error);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatchNavLayoutUpdated();
|
||||
};
|
||||
|
||||
const applyCustomNavLayout = async (layout, hiddenKeys = []) => {
|
||||
const normalizedHiddenKeys = normalizeHiddenKeys(
|
||||
hiddenKeys,
|
||||
[...hiddenKeys, ...getDefaultHiddenKeys(layout)],
|
||||
navDefinitionMap.value
|
||||
);
|
||||
const sanitized = sanitizeLayoutLocal(layout, normalizedHiddenKeys);
|
||||
@@ -221,30 +194,26 @@ export function useNavLayout({
|
||||
let layoutData = null;
|
||||
let hiddenKeysData = [];
|
||||
try {
|
||||
const storedValue = await configRepository.getString(
|
||||
'VRCX_customNavMenuLayoutList'
|
||||
);
|
||||
if (storedValue) {
|
||||
const parsed = JSON.parse(storedValue);
|
||||
if (Array.isArray(parsed)) {
|
||||
layoutData = parsed;
|
||||
} else if (Array.isArray(parsed?.layout)) {
|
||||
layoutData = parsed.layout;
|
||||
hiddenKeysData = Array.isArray(parsed.hiddenKeys)
|
||||
? parsed.hiddenKeys
|
||||
: [];
|
||||
const loaded = await loadStoredNavConfig(
|
||||
configRepository,
|
||||
createDefaultNavLayout(),
|
||||
{
|
||||
configKey: NAV_CONFIG_KEY,
|
||||
filterHiddenKey: (key) => !isToolNavKey(key)
|
||||
}
|
||||
}
|
||||
);
|
||||
layoutData = loaded.layout;
|
||||
hiddenKeysData = loaded.hiddenKeys;
|
||||
} catch (error) {
|
||||
console.error('Failed to load custom nav', error);
|
||||
} finally {
|
||||
const fallbackLayout = layoutData?.length
|
||||
? layoutData
|
||||
: createDefaultNavLayout();
|
||||
const normalizedHiddenKeys = normalizeHiddenKeys(
|
||||
hiddenKeysData,
|
||||
navDefinitionMap.value
|
||||
);
|
||||
const fallbackLayout = layoutData?.length
|
||||
? layoutData
|
||||
: createDefaultNavLayout();
|
||||
const sanitized = sanitizeLayoutLocal(
|
||||
fallbackLayout,
|
||||
normalizedHiddenKeys
|
||||
@@ -339,10 +308,35 @@ export function useNavLayout({
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const handleExternalNavLayoutUpdate = async () => {
|
||||
await loadNavMenuConfig();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
window.addEventListener(
|
||||
NAV_LAYOUT_UPDATED_EVENT,
|
||||
handleExternalNavLayoutUpdate
|
||||
);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
window.removeEventListener(
|
||||
NAV_LAYOUT_UPDATED_EVENT,
|
||||
handleExternalNavLayoutUpdate
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
navLayout,
|
||||
navLayoutReady,
|
||||
navHiddenKeys,
|
||||
defaultHiddenKeys: computed(() => getDefaultHiddenKeys(defaultNavLayout.value)),
|
||||
menuItems,
|
||||
activeMenuIndex,
|
||||
allNavDefinitions,
|
||||
|
||||
@@ -40,6 +40,10 @@ export function triggerNavEntryAction(entry, { router, directAccessPaste }) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.action && typeof entry.action === 'object') {
|
||||
return entry.action;
|
||||
}
|
||||
|
||||
if (entry.routeName) {
|
||||
navigateToRoute(router, entry.routeName, entry.routeParams);
|
||||
return;
|
||||
|
||||
80
src/components/nav-menu/navConfigUtils.js
Normal file
80
src/components/nav-menu/navConfigUtils.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { isToolNavKey } from '../../shared/constants';
|
||||
import { collectLayoutKeys } from './navLayoutHelpers';
|
||||
|
||||
export const NAV_CONFIG_KEY = 'VRCX_customNavMenuLayoutList';
|
||||
|
||||
export function generateNavFolderId() {
|
||||
if (
|
||||
typeof crypto !== 'undefined' &&
|
||||
typeof crypto.randomUUID === 'function'
|
||||
) {
|
||||
return `nav-folder-${crypto.randomUUID()}`;
|
||||
}
|
||||
|
||||
return `nav-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 4)}`;
|
||||
}
|
||||
|
||||
export function createNavDefinitionMap(definitions = []) {
|
||||
const map = new Map();
|
||||
definitions.forEach((definition) => {
|
||||
if (definition?.key) {
|
||||
map.set(definition.key, definition);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
export function buildNavDefinitionsForLayout(
|
||||
baseDefinitions = [],
|
||||
dashboardDefinitions = [],
|
||||
layout = [],
|
||||
hiddenKeys = []
|
||||
) {
|
||||
const keysInLayout = collectLayoutKeys(layout);
|
||||
const hiddenSet = new Set(Array.isArray(hiddenKeys) ? hiddenKeys : []);
|
||||
const visibleBaseDefinitions = baseDefinitions.filter(
|
||||
(definition) =>
|
||||
!isToolNavKey(definition.key) || keysInLayout.has(definition.key)
|
||||
);
|
||||
const visibleDashboardDefinitions = dashboardDefinitions.filter(
|
||||
(definition) =>
|
||||
keysInLayout.has(definition.key) || hiddenSet.has(definition.key)
|
||||
);
|
||||
|
||||
return [...visibleBaseDefinitions, ...visibleDashboardDefinitions];
|
||||
}
|
||||
|
||||
export async function loadStoredNavConfig(
|
||||
repository,
|
||||
fallbackLayout,
|
||||
{
|
||||
configKey = NAV_CONFIG_KEY,
|
||||
filterHiddenKey = () => true
|
||||
} = {}
|
||||
) {
|
||||
let layout = fallbackLayout;
|
||||
let hiddenKeys = [];
|
||||
|
||||
const storedValue = await repository.getString(configKey);
|
||||
if (!storedValue) {
|
||||
return { layout, hiddenKeys };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(storedValue);
|
||||
if (Array.isArray(parsed)) {
|
||||
layout = parsed;
|
||||
} else if (Array.isArray(parsed?.layout)) {
|
||||
layout = parsed.layout;
|
||||
hiddenKeys = Array.isArray(parsed.hiddenKeys)
|
||||
? parsed.hiddenKeys.filter(filterHiddenKey)
|
||||
: [];
|
||||
}
|
||||
} catch {
|
||||
// keep defaults
|
||||
}
|
||||
|
||||
return { layout, hiddenKeys };
|
||||
}
|
||||
67
src/components/nav-menu/navLayoutDefaults.js
Normal file
67
src/components/nav-menu/navLayoutDefaults.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { DASHBOARD_NAV_KEY_PREFIX } from '../../shared/constants';
|
||||
|
||||
export function createBaseDefaultNavLayout(t) {
|
||||
return [
|
||||
{ type: 'item', key: 'feed' },
|
||||
{ type: 'item', key: 'friends-locations' },
|
||||
{ type: 'item', key: 'game-log' },
|
||||
{ type: 'item', key: 'player-list' },
|
||||
{ type: 'item', key: 'search' },
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'default-folder-favorites',
|
||||
nameKey: 'nav_tooltip.favorites',
|
||||
name: t('nav_tooltip.favorites'),
|
||||
icon: 'ri-star-line',
|
||||
items: ['favorite-friends', 'favorite-worlds', 'favorite-avatars']
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'default-folder-social',
|
||||
nameKey: 'nav_tooltip.social',
|
||||
name: t('nav_tooltip.social'),
|
||||
icon: 'ri-group-line',
|
||||
items: ['friend-log', 'friend-list', 'moderation']
|
||||
},
|
||||
{ type: 'item', key: 'notification' },
|
||||
{ type: 'item', key: 'my-avatars' },
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'default-folder-charts',
|
||||
nameKey: 'nav_tooltip.charts',
|
||||
name: t('nav_tooltip.charts'),
|
||||
icon: 'ri-pie-chart-line',
|
||||
items: ['charts-instance', 'charts-mutual']
|
||||
},
|
||||
{ type: 'item', key: 'tools' },
|
||||
{ type: 'item', key: 'direct-access' }
|
||||
];
|
||||
}
|
||||
|
||||
export function insertDashboardEntries(layout, dashboardDefinitions) {
|
||||
const nextLayout = Array.isArray(layout) ? [...layout] : [];
|
||||
const dashboardEntries = (dashboardDefinitions || []).map((def) => ({
|
||||
type: 'item',
|
||||
key: def.key
|
||||
}));
|
||||
|
||||
if (!dashboardEntries.length) {
|
||||
return nextLayout;
|
||||
}
|
||||
|
||||
const directAccessIdx = nextLayout.findIndex(
|
||||
(entry) => entry.type === 'item' && entry.key === 'direct-access'
|
||||
);
|
||||
|
||||
if (directAccessIdx !== -1) {
|
||||
nextLayout.splice(directAccessIdx, 0, ...dashboardEntries);
|
||||
} else {
|
||||
nextLayout.push(...dashboardEntries);
|
||||
}
|
||||
|
||||
return nextLayout;
|
||||
}
|
||||
|
||||
export function isDashboardNavKey(key) {
|
||||
return String(key || '').startsWith(DASHBOARD_NAV_KEY_PREFIX);
|
||||
}
|
||||
9
src/components/nav-menu/navLayoutEvents.js
Normal file
9
src/components/nav-menu/navLayoutEvents.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export const NAV_LAYOUT_UPDATED_EVENT = 'vrcx:nav-layout-updated';
|
||||
|
||||
export function dispatchNavLayoutUpdated() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent(NAV_LAYOUT_UPDATED_EVENT));
|
||||
}
|
||||
Reference in New Issue
Block a user