Files
VRCX/src/composables/useToolNavPinning.js
2026-03-16 10:44:54 +09:00

260 lines
7.4 KiB
JavaScript

import { useEventListener } from '@vueuse/core';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { toast } from 'vue-sonner';
import configRepository from '../services/config';
import {
navDefinitions
} from '../shared/constants';
import { useDashboardStore } from '../stores';
import {
createBaseDefaultNavLayout,
insertDashboardEntries
} from '../components/nav-menu/navLayoutDefaults';
import { collectLayoutKeys } from '../components/nav-menu/navLayoutHelpers';
import {
buildNavDefinitionsForLayout,
createNavDefinitionMap,
generateNavFolderId,
loadStoredNavConfig,
NAV_CONFIG_KEY
} from '../components/nav-menu/navConfigUtils';
import {
normalizeHiddenKeys,
sanitizeLayout
} from '../components/nav-menu/navMenuUtils';
import {
dispatchNavLayoutUpdated,
NAV_LAYOUT_UPDATED_EVENT
} from '../components/nav-menu/navLayoutEvents';
function insertToolNavItem(layout, navKey, t, placement = 'top-level') {
const nextLayout = Array.isArray(layout) ? [...layout] : [];
const alreadyExists = nextLayout.some((entry) => {
if (entry.type === 'item') {
return entry.key === navKey;
}
return entry.type === 'folder' && entry.items?.includes(navKey);
});
if (alreadyExists) {
return nextLayout;
}
const insertIdx = nextLayout.findIndex(
(entry) =>
entry.type === 'item' &&
(entry.key === 'tools' || entry.key === 'direct-access')
);
if (placement === 'top-level') {
if (insertIdx === -1) {
nextLayout.push({ type: 'item', key: navKey });
} else {
nextLayout.splice(insertIdx, 0, { type: 'item', key: navKey });
}
return nextLayout;
}
const folderWithTools = nextLayout.find(
(entry) =>
entry.type === 'folder' &&
(entry.items || []).some((key) => String(key).startsWith('tool-'))
);
if (folderWithTools) {
return nextLayout.map((entry) => {
if (entry !== folderWithTools) {
return entry;
}
return {
...entry,
items: [...(entry.items || []), navKey]
};
});
}
const toolsFolder = {
type: 'folder',
id: 'default-folder-tools-shortcuts',
nameKey: 'nav_tooltip.tools',
name: t('nav_tooltip.tools'),
icon: 'ri-tools-line',
items: [navKey]
};
if (insertIdx === -1) {
nextLayout.push(toolsFolder);
} else {
nextLayout.splice(insertIdx, 0, toolsFolder);
}
return nextLayout;
}
function removeToolNavItem(layout, navKey) {
if (!Array.isArray(layout)) {
return [];
}
return layout
.map((entry) => {
if (entry.type === 'item') {
return entry.key === navKey ? null : entry;
}
if (entry.type === 'folder') {
const nextItems = (entry.items || []).filter(
(key) => key !== navKey
);
if (!nextItems.length) {
return null;
}
return {
...entry,
items: nextItems
};
}
return entry;
})
.filter(Boolean);
}
export function useToolNavPinning() {
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const pinnedToolKeysRef = ref(new Set());
const buildDefinitions = () => [
...navDefinitions,
...dashboardStore.getDashboardNavDefinitions()
];
// Tool nav items are add/remove only; they do not use hidden state anymore.
const getDefaultHiddenKeys = () => [];
const buildDefaultLayout = () =>
insertDashboardEntries(
createBaseDefaultNavLayout(t),
dashboardStore.getDashboardNavDefinitions()
);
const buildSanitizeDefinitions = (layout = [], hiddenKeys = []) => {
return buildNavDefinitionsForLayout(
navDefinitions,
dashboardStore.getDashboardNavDefinitions(),
layout,
hiddenKeys
);
};
const loadConfig = async () => {
return loadStoredNavConfig(configRepository, buildDefaultLayout(), {
configKey: NAV_CONFIG_KEY,
filterHiddenKey: (key) => !key?.startsWith('tool-')
});
};
const refreshPinnedState = async () => {
const { layout, hiddenKeys } = await loadConfig();
const layoutKeys = collectLayoutKeys(layout);
const nextPinned = new Set();
layoutKeys.forEach((key) => {
if (key.startsWith('tool-')) {
nextPinned.add(key.replace(/^tool-/, ''));
}
});
pinnedToolKeysRef.value = nextPinned;
};
const pinToolToNav = async (toolKey, options = {}) => {
const navKey = `tool-${toolKey}`;
const { layout, hiddenKeys } = await loadConfig();
const nextLayout = insertToolNavItem(
layout,
navKey,
t,
options.placement
);
const nextHiddenKeys = hiddenKeys.filter((key) => key !== navKey);
const definitions = buildSanitizeDefinitions(nextLayout, nextHiddenKeys);
const definitionMap = createNavDefinitionMap(buildDefinitions());
const normalizedHiddenKeys = normalizeHiddenKeys(
nextHiddenKeys,
definitionMap
);
const sanitizedLayout = sanitizeLayout(
nextLayout,
normalizedHiddenKeys,
definitionMap,
definitions,
t,
generateNavFolderId
);
await configRepository.setString(
NAV_CONFIG_KEY,
JSON.stringify({
layout: sanitizedLayout,
hiddenKeys: normalizedHiddenKeys
})
);
await refreshPinnedState();
toast.success(t('nav_menu.custom_nav.pinned'));
dispatchNavLayoutUpdated();
};
const unpinToolFromNav = async (toolKey) => {
const navKey = `tool-${toolKey}`;
const { layout, hiddenKeys } = await loadConfig();
const nextLayout = removeToolNavItem(layout, navKey);
const nextHiddenKeys = hiddenKeys.filter((key) => key !== navKey);
const definitions = buildSanitizeDefinitions(nextLayout, nextHiddenKeys);
const definitionMap = createNavDefinitionMap(buildDefinitions());
const normalizedHiddenKeys = normalizeHiddenKeys(
nextHiddenKeys,
definitionMap
);
const sanitizedLayout = sanitizeLayout(
nextLayout,
normalizedHiddenKeys,
definitionMap,
definitions,
t,
generateNavFolderId
);
await configRepository.setString(
NAV_CONFIG_KEY,
JSON.stringify({
layout: sanitizedLayout,
hiddenKeys: normalizedHiddenKeys
})
);
await refreshPinnedState();
toast.success(t('nav_menu.custom_nav.unpinned'));
dispatchNavLayoutUpdated();
};
useEventListener(
typeof window !== 'undefined' ? window : undefined,
NAV_LAYOUT_UPDATED_EVENT,
refreshPinnedState
);
return {
pinnedToolKeys: computed(() => pinnedToolKeysRef.value),
pinToolToNav,
unpinToolFromNav,
refreshPinnedState
};
}