mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 06:46:04 +02:00
feat: add tool nav pinning and unpinning
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog :open="visible" @update:open="(open) => (open ? null : handleClose())">
|
<Dialog :open="visible" @update:open="(open) => (open ? null : handleClose())">
|
||||||
<DialogContent class="sm:min-w-140">
|
<DialogContent class="sm:min-w-180">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{{ t('nav_menu.custom_nav.dialog_title') }}</DialogTitle>
|
<DialogTitle>{{ t('nav_menu.custom_nav.dialog_title') }}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -169,6 +169,7 @@
|
|||||||
import { InputGroupButton, InputGroupField } from '../ui/input-group';
|
import { InputGroupButton, InputGroupField } from '../ui/input-group';
|
||||||
import { Separator } from '../ui/separator';
|
import { Separator } from '../ui/separator';
|
||||||
import { Tree } from '../ui/tree';
|
import { Tree } from '../ui/tree';
|
||||||
|
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 } from '../../stores';
|
||||||
@@ -188,6 +189,10 @@
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
defaultHiddenKeys: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
defaultLayout: {
|
defaultLayout: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
@@ -305,7 +310,10 @@
|
|||||||
|
|
||||||
const hiddenItems = computed(() =>
|
const hiddenItems = computed(() =>
|
||||||
(props.definitions?.length ? props.definitions : navDefinitions)
|
(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) => ({
|
.map((def) => ({
|
||||||
key: def.key,
|
key: def.key,
|
||||||
icon: def.icon,
|
icon: def.icon,
|
||||||
@@ -322,6 +330,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleHideItem = (key) => {
|
const handleHideItem = (key) => {
|
||||||
|
if (isToolNavKey(key)) {
|
||||||
|
removeFromLayout(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let placement = null;
|
let placement = null;
|
||||||
for (let i = 0; i < localLayout.value.length; i++) {
|
for (let i = 0; i < localLayout.value.length; i++) {
|
||||||
const entry = localLayout.value[i];
|
const entry = localLayout.value[i];
|
||||||
@@ -789,7 +802,7 @@
|
|||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
localLayout.value = cloneLayout(props.defaultLayout || []);
|
localLayout.value = cloneLayout(props.defaultLayout || []);
|
||||||
hiddenKeySet.value = new Set();
|
hiddenKeySet.value = new Set(props.defaultHiddenKeys || []);
|
||||||
hiddenPlacement.value = new Map();
|
hiddenPlacement.value = new Map();
|
||||||
expandedKeys.value = localLayout.value.filter((e) => e.type === 'folder').map((e) => e.id);
|
expandedKeys.value = localLayout.value.filter((e) => e.type === 'folder').map((e) => e.id);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
const isDashboard = computed(() => {
|
const isDashboard = computed(() => {
|
||||||
return !isFolder.value && nodeValue.value?.key?.startsWith('dashboard-');
|
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 hasChildren = computed(() => props.item.hasChildren);
|
||||||
const level = computed(() => nodeValue.value?.level ?? 0);
|
const level = computed(() => nodeValue.value?.level ?? 0);
|
||||||
const nodeId = computed(() => (isFolder.value ? nodeValue.value?.id : nodeValue.value?.key));
|
const nodeId = computed(() => (isFolder.value ? nodeValue.value?.id : nodeValue.value?.key));
|
||||||
@@ -127,7 +130,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<DropdownMenuItem @click="emit('hide', nodeValue.key)">
|
<DropdownMenuItem @click="emit('hide', nodeValue.key)">
|
||||||
{{ t('nav_menu.custom_nav.hide') }}
|
{{
|
||||||
|
isTool
|
||||||
|
? t('common.actions.delete')
|
||||||
|
: t('nav_menu.custom_nav.hide')
|
||||||
|
}}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</template>
|
</template>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -66,9 +66,12 @@
|
|||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
</template>
|
</template>
|
||||||
<ContextMenuItem @click="handleQuickCreateDashboard">
|
<ContextMenuItem
|
||||||
{{ t('dashboard.new_dashboard') }}
|
v-if="isToolItem(item)"
|
||||||
|
@click="handleUnpinToolItem(item)">
|
||||||
|
{{ t('nav_menu.custom_nav.unpin_from_nav') }}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator v-if="isToolItem(item)" />
|
||||||
<ContextMenuItem @click="handleOpenCustomNavDialog">
|
<ContextMenuItem @click="handleOpenCustomNavDialog">
|
||||||
{{ t('nav_menu.custom_nav.header') }}
|
{{ t('nav_menu.custom_nav.header') }}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
@@ -86,13 +89,14 @@
|
|||||||
:is-entry-notified="isEntryNotified"
|
:is-entry-notified="isEntryNotified"
|
||||||
:is-nav-item-notified="isNavItemNotified"
|
:is-nav-item-notified="isNavItemNotified"
|
||||||
:is-dashboard-item="isDashboardItem"
|
:is-dashboard-item="isDashboardItem"
|
||||||
|
:is-tool-item="isToolItem"
|
||||||
@collapsed-dropdown-open-change="handleCollapsedDropdownOpenChange"
|
@collapsed-dropdown-open-change="handleCollapsedDropdownOpenChange"
|
||||||
@collapsed-submenu-select="handleCollapsedSubmenuSelect"
|
@collapsed-submenu-select="handleCollapsedSubmenuSelect"
|
||||||
@submenu-click="handleSubmenuClick"
|
@submenu-click="handleSubmenuClick"
|
||||||
@clear-notifications="clearAllNotifications"
|
@clear-notifications="clearAllNotifications"
|
||||||
@edit-dashboard="handleEditDashboard"
|
@edit-dashboard="handleEditDashboard"
|
||||||
@delete-dashboard="handleDeleteDashboard"
|
@delete-dashboard="handleDeleteDashboard"
|
||||||
@create-dashboard="handleQuickCreateDashboard"
|
@unpin-tool="handleUnpinToolItem"
|
||||||
@open-custom-nav="handleOpenCustomNavDialog" />
|
@open-custom-nav="handleOpenCustomNavDialog" />
|
||||||
</template>
|
</template>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
@@ -147,6 +151,7 @@
|
|||||||
v-model:visible="customNavDialogVisible"
|
v-model:visible="customNavDialogVisible"
|
||||||
:layout="navLayout"
|
:layout="navLayout"
|
||||||
:hidden-keys="navHiddenKeys"
|
:hidden-keys="navHiddenKeys"
|
||||||
|
:default-hidden-keys="defaultHiddenKeys"
|
||||||
:default-layout="defaultNavLayout"
|
:default-layout="defaultNavLayout"
|
||||||
:definitions="allNavDefinitions"
|
:definitions="allNavDefinitions"
|
||||||
@save="handleCustomNavSave"
|
@save="handleCustomNavSave"
|
||||||
@@ -163,6 +168,8 @@
|
|||||||
|
|
||||||
import { useNavLayout } from './composables/useNavLayout';
|
import { useNavLayout } from './composables/useNavLayout';
|
||||||
import { useNavTheme } from './composables/useNavTheme';
|
import { useNavTheme } from './composables/useNavTheme';
|
||||||
|
import { useToolActions } from '../../composables/useToolActions';
|
||||||
|
import { useToolNavPinning } from '../../composables/useToolNavPinning';
|
||||||
import { Kbd } from '@/components/ui/kbd';
|
import { Kbd } from '@/components/ui/kbd';
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
@@ -216,6 +223,8 @@
|
|||||||
const { clearAllNotifications } = uiStore;
|
const { clearAllNotifications } = uiStore;
|
||||||
|
|
||||||
const { directAccessPaste } = useSearchStore();
|
const { directAccessPaste } = useSearchStore();
|
||||||
|
const { triggerTool } = useToolActions();
|
||||||
|
const { unpinToolFromNav } = useToolNavPinning();
|
||||||
const { logout } = useAuthStore();
|
const { logout } = useAuthStore();
|
||||||
const modalStore = useModalStore();
|
const modalStore = useModalStore();
|
||||||
|
|
||||||
@@ -243,6 +252,7 @@
|
|||||||
navLayout,
|
navLayout,
|
||||||
navLayoutReady,
|
navLayoutReady,
|
||||||
navHiddenKeys,
|
navHiddenKeys,
|
||||||
|
defaultHiddenKeys,
|
||||||
menuItems,
|
menuItems,
|
||||||
activeMenuIndex,
|
activeMenuIndex,
|
||||||
allNavDefinitions,
|
allNavDefinitions,
|
||||||
@@ -258,7 +268,8 @@
|
|||||||
router,
|
router,
|
||||||
dashboardStore,
|
dashboardStore,
|
||||||
dashboards,
|
dashboards,
|
||||||
directAccessPaste
|
directAccessPaste,
|
||||||
|
triggerTool
|
||||||
});
|
});
|
||||||
|
|
||||||
const collapsedDropdownOpenId = ref(null);
|
const collapsedDropdownOpenId = ref(null);
|
||||||
@@ -321,6 +332,14 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isDashboardItem = (item) => item?.index?.startsWith(DASHBOARD_NAV_KEY_PREFIX);
|
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 handleQuickCreateDashboard = async () => {
|
||||||
const dashboard = await dashboardStore.createDashboard(t('dashboard.default_name'));
|
const dashboard = await dashboardStore.createDashboard(t('dashboard.default_name'));
|
||||||
|
|||||||
@@ -110,9 +110,12 @@
|
|||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
</template>
|
</template>
|
||||||
<ContextMenuItem @click="emit('create-dashboard')">
|
<ContextMenuItem
|
||||||
{{ t('dashboard.new_dashboard') }}
|
v-if="isToolItem(entry)"
|
||||||
|
@click="emit('unpin-tool', entry)">
|
||||||
|
{{ t('nav_menu.custom_nav.unpin_from_nav') }}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator v-if="isToolItem(entry)" />
|
||||||
<ContextMenuItem @click="emit('open-custom-nav')">
|
<ContextMenuItem @click="emit('open-custom-nav')">
|
||||||
{{ t('nav_menu.custom_nav.header') }}
|
{{ t('nav_menu.custom_nav.header') }}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
@@ -130,9 +133,6 @@
|
|||||||
{{ t('nav_menu.mark_all_read') }}
|
{{ t('nav_menu.mark_all_read') }}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuSeparator v-if="hasNotifications" />
|
<ContextMenuSeparator v-if="hasNotifications" />
|
||||||
<ContextMenuItem @click="emit('create-dashboard')">
|
|
||||||
{{ t('dashboard.new_dashboard') }}
|
|
||||||
</ContextMenuItem>
|
|
||||||
<ContextMenuItem @click="emit('open-custom-nav')">
|
<ContextMenuItem @click="emit('open-custom-nav')">
|
||||||
{{ t('nav_menu.custom_nav.header') }}
|
{{ t('nav_menu.custom_nav.header') }}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
@@ -199,6 +199,10 @@
|
|||||||
isDashboardItem: {
|
isDashboardItem: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
isToolItem: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,7 +213,7 @@
|
|||||||
'clear-notifications',
|
'clear-notifications',
|
||||||
'edit-dashboard',
|
'edit-dashboard',
|
||||||
'delete-dashboard',
|
'delete-dashboard',
|
||||||
'create-dashboard',
|
'unpin-tool',
|
||||||
'open-custom-nav'
|
'open-custom-nav'
|
||||||
]);
|
]);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|||||||
@@ -115,6 +115,20 @@ vi.mock('../../../stores', () => ({
|
|||||||
useAuthStore: () => ({
|
useAuthStore: () => ({
|
||||||
logout: (...args) => mocks.logout(...args)
|
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: () => ({
|
useAppearanceSettingsStore: () => ({
|
||||||
themeMode: mocks.themeMode,
|
themeMode: mocks.themeMode,
|
||||||
tableDensity: mocks.tableDensity,
|
tableDensity: mocks.tableDensity,
|
||||||
@@ -148,11 +162,13 @@ vi.mock('../../../services/config', () => ({
|
|||||||
|
|
||||||
vi.mock('../../../shared/constants', () => ({
|
vi.mock('../../../shared/constants', () => ({
|
||||||
DASHBOARD_NAV_KEY_PREFIX: 'dashboard-',
|
DASHBOARD_NAV_KEY_PREFIX: 'dashboard-',
|
||||||
|
defaultHiddenToolNavKeys: [],
|
||||||
THEME_CONFIG: {
|
THEME_CONFIG: {
|
||||||
system: { name: 'System' },
|
system: { name: 'System' },
|
||||||
light: { name: 'Light' },
|
light: { name: 'Light' },
|
||||||
dark: { name: 'Dark' }
|
dark: { name: 'Dark' }
|
||||||
},
|
},
|
||||||
|
isToolNavKey: (key) => typeof key === 'string' && key.startsWith('tool-'),
|
||||||
links: {
|
links: {
|
||||||
github: 'https://github.com/vrcx-team/VRCX'
|
github: 'https://github.com/vrcx-team/VRCX'
|
||||||
},
|
},
|
||||||
@@ -171,7 +187,9 @@ vi.mock('../../../shared/constants', () => ({
|
|||||||
tooltip: 'nav_tooltip.direct_access',
|
tooltip: 'nav_tooltip.direct_access',
|
||||||
icon: 'ri-door-open-line'
|
icon: 'ri-door-open-line'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
toolDefinitionMap: new Map(),
|
||||||
|
toolDefinitions: []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../navMenuUtils', () => ({
|
vi.mock('../navMenuUtils', () => ({
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
import { computed, ref, watch } from 'vue';
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
import configRepository from '../../../services/config';
|
import configRepository from '../../../services/config';
|
||||||
import {
|
import {
|
||||||
DASHBOARD_NAV_KEY_PREFIX,
|
DASHBOARD_NAV_KEY_PREFIX,
|
||||||
|
isToolNavKey,
|
||||||
navDefinitions
|
navDefinitions
|
||||||
} from '../../../shared/constants';
|
} from '../../../shared/constants';
|
||||||
import { triggerNavEntryAction } from '../navActionUtils';
|
import { triggerNavEntryAction } from '../navActionUtils';
|
||||||
import {
|
import {
|
||||||
buildMenuItems,
|
buildMenuItems,
|
||||||
collectLayoutKeys,
|
|
||||||
findFirstNavEntry,
|
findFirstNavEntry,
|
||||||
findFirstNavKey
|
findFirstNavKey
|
||||||
} from '../navLayoutHelpers';
|
} 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';
|
import { normalizeHiddenKeys, sanitizeLayout } from '../navMenuUtils';
|
||||||
|
|
||||||
export function useNavLayout({
|
export function useNavLayout({
|
||||||
@@ -22,7 +35,8 @@ export function useNavLayout({
|
|||||||
router,
|
router,
|
||||||
dashboardStore,
|
dashboardStore,
|
||||||
dashboards,
|
dashboards,
|
||||||
directAccessPaste
|
directAccessPaste,
|
||||||
|
triggerTool
|
||||||
}) {
|
}) {
|
||||||
const navLayout = ref([]);
|
const navLayout = ref([]);
|
||||||
const navLayoutReady = ref(false);
|
const navLayoutReady = ref(false);
|
||||||
@@ -33,49 +47,17 @@ export function useNavLayout({
|
|||||||
...dashboardStore.getDashboardNavDefinitions()
|
...dashboardStore.getDashboardNavDefinitions()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const navDefinitionMap = computed(() => {
|
const navDefinitionMap = computed(() =>
|
||||||
const map = new Map();
|
createNavDefinitionMap(allNavDefinitions.value)
|
||||||
allNavDefinitions.value.forEach((item) => {
|
);
|
||||||
map.set(item.key, item);
|
|
||||||
});
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
|
|
||||||
const createDefaultNavLayout = () => [
|
// Tool nav items are add/remove only; they no longer participate in hidden state.
|
||||||
{ type: 'item', key: 'feed' },
|
const getDefaultHiddenKeys = (layout = []) => {
|
||||||
{ type: 'item', key: 'friends-locations' },
|
void layout;
|
||||||
{ type: 'item', key: 'game-log' },
|
return [];
|
||||||
{ type: 'item', key: 'player-list' },
|
};
|
||||||
{ type: 'item', key: 'search' },
|
|
||||||
{
|
const createDefaultNavLayout = () => createBaseDefaultNavLayout(t);
|
||||||
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' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const menuItems = computed(() =>
|
const menuItems = computed(() =>
|
||||||
buildMenuItems(navLayout.value, navDefinitionMap.value, t)
|
buildMenuItems(navLayout.value, navDefinitionMap.value, t)
|
||||||
@@ -95,43 +77,36 @@ export function useNavLayout({
|
|||||||
return `${DASHBOARD_NAV_KEY_PREFIX}${currentRoute.params.id}`;
|
return `${DASHBOARD_NAV_KEY_PREFIX}${currentRoute.params.id}`;
|
||||||
}
|
}
|
||||||
const currentRouteName = currentRoute?.name;
|
const currentRouteName = currentRoute?.name;
|
||||||
const navKey = currentRoute?.meta?.navKey || currentRouteName;
|
const navKeys = Array.isArray(currentRoute?.meta?.navKeys)
|
||||||
if (!navKey) {
|
? currentRoute.meta.navKeys
|
||||||
|
: [currentRoute?.meta?.navKey || currentRouteName].filter(Boolean);
|
||||||
|
if (!navKeys.length) {
|
||||||
return getFirstNavKeyLocal(navLayout.value) || 'feed';
|
return getFirstNavKeyLocal(navLayout.value) || 'feed';
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of navLayout.value) {
|
for (const entry of navLayout.value) {
|
||||||
if (entry.type === 'item' && entry.key === navKey) {
|
if (entry.type === 'item' && navKeys.includes(entry.key)) {
|
||||||
return entry.key;
|
return entry.key;
|
||||||
}
|
}
|
||||||
if (entry.type === 'folder' && entry.items?.includes(navKey)) {
|
if (entry.type === 'folder') {
|
||||||
return navKey;
|
const matchedKey = navKeys.find((key) =>
|
||||||
|
entry.items?.includes(key)
|
||||||
|
);
|
||||||
|
if (matchedKey) {
|
||||||
|
return matchedKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getFirstNavKeyLocal(navLayout.value) || 'feed';
|
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 getAppendDefinitions = (layout, hiddenKeys = []) => {
|
||||||
const keysInLayout = collectLayoutKeys(layout);
|
return buildNavDefinitionsForLayout(
|
||||||
const hiddenSet = new Set(Array.isArray(hiddenKeys) ? hiddenKeys : []);
|
navDefinitions,
|
||||||
const dashboardDefinitions = dashboardStore
|
dashboardStore.getDashboardNavDefinitions(),
|
||||||
.getDashboardNavDefinitions()
|
layout,
|
||||||
.filter(
|
hiddenKeys
|
||||||
(definition) =>
|
|
||||||
keysInLayout.has(definition.key) ||
|
|
||||||
hiddenSet.has(definition.key)
|
|
||||||
);
|
);
|
||||||
return [...navDefinitions, ...dashboardDefinitions];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeLayoutLocal = (layout, hiddenKeys = []) => {
|
const sanitizeLayoutLocal = (layout, hiddenKeys = []) => {
|
||||||
@@ -141,41 +116,36 @@ export function useNavLayout({
|
|||||||
navDefinitionMap.value,
|
navDefinitionMap.value,
|
||||||
getAppendDefinitions(layout, hiddenKeys),
|
getAppendDefinitions(layout, hiddenKeys),
|
||||||
t,
|
t,
|
||||||
generateFolderId
|
generateNavFolderId
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultNavLayout = computed(() => {
|
const defaultNavLayout = computed(() => {
|
||||||
const base = createDefaultNavLayout();
|
const base = insertDashboardEntries(
|
||||||
const dashboardEntries = dashboardStore
|
createDefaultNavLayout(),
|
||||||
.getDashboardNavDefinitions()
|
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) {
|
return sanitizeLayoutLocal(base, getDefaultHiddenKeys(base));
|
||||||
base.splice(directAccessIdx, 0, ...dashboardEntries);
|
|
||||||
} else {
|
|
||||||
base.push(...dashboardEntries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sanitizeLayoutLocal(base, []);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const triggerNavAction = (entry) => {
|
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 saveNavLayout = async (layout, hiddenKeys = []) => {
|
||||||
const normalizedHiddenKeys = normalizeHiddenKeys(
|
const normalizedHiddenKeys = normalizeHiddenKeys(
|
||||||
hiddenKeys,
|
[...hiddenKeys, ...getDefaultHiddenKeys(layout)],
|
||||||
navDefinitionMap.value
|
navDefinitionMap.value
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await configRepository.setString(
|
await configRepository.setString(
|
||||||
'VRCX_customNavMenuLayoutList',
|
NAV_CONFIG_KEY,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
layout,
|
layout,
|
||||||
hiddenKeys: normalizedHiddenKeys
|
hiddenKeys: normalizedHiddenKeys
|
||||||
@@ -183,12 +153,15 @@ export function useNavLayout({
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save custom nav', error);
|
console.error('Failed to save custom nav', error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatchNavLayoutUpdated();
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyCustomNavLayout = async (layout, hiddenKeys = []) => {
|
const applyCustomNavLayout = async (layout, hiddenKeys = []) => {
|
||||||
const normalizedHiddenKeys = normalizeHiddenKeys(
|
const normalizedHiddenKeys = normalizeHiddenKeys(
|
||||||
hiddenKeys,
|
[...hiddenKeys, ...getDefaultHiddenKeys(layout)],
|
||||||
navDefinitionMap.value
|
navDefinitionMap.value
|
||||||
);
|
);
|
||||||
const sanitized = sanitizeLayoutLocal(layout, normalizedHiddenKeys);
|
const sanitized = sanitizeLayoutLocal(layout, normalizedHiddenKeys);
|
||||||
@@ -221,30 +194,26 @@ export function useNavLayout({
|
|||||||
let layoutData = null;
|
let layoutData = null;
|
||||||
let hiddenKeysData = [];
|
let hiddenKeysData = [];
|
||||||
try {
|
try {
|
||||||
const storedValue = await configRepository.getString(
|
const loaded = await loadStoredNavConfig(
|
||||||
'VRCX_customNavMenuLayoutList'
|
configRepository,
|
||||||
|
createDefaultNavLayout(),
|
||||||
|
{
|
||||||
|
configKey: NAV_CONFIG_KEY,
|
||||||
|
filterHiddenKey: (key) => !isToolNavKey(key)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
if (storedValue) {
|
layoutData = loaded.layout;
|
||||||
const parsed = JSON.parse(storedValue);
|
hiddenKeysData = loaded.hiddenKeys;
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
layoutData = parsed;
|
|
||||||
} else if (Array.isArray(parsed?.layout)) {
|
|
||||||
layoutData = parsed.layout;
|
|
||||||
hiddenKeysData = Array.isArray(parsed.hiddenKeys)
|
|
||||||
? parsed.hiddenKeys
|
|
||||||
: [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load custom nav', error);
|
console.error('Failed to load custom nav', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
const fallbackLayout = layoutData?.length
|
||||||
|
? layoutData
|
||||||
|
: createDefaultNavLayout();
|
||||||
const normalizedHiddenKeys = normalizeHiddenKeys(
|
const normalizedHiddenKeys = normalizeHiddenKeys(
|
||||||
hiddenKeysData,
|
hiddenKeysData,
|
||||||
navDefinitionMap.value
|
navDefinitionMap.value
|
||||||
);
|
);
|
||||||
const fallbackLayout = layoutData?.length
|
|
||||||
? layoutData
|
|
||||||
: createDefaultNavLayout();
|
|
||||||
const sanitized = sanitizeLayoutLocal(
|
const sanitized = sanitizeLayoutLocal(
|
||||||
fallbackLayout,
|
fallbackLayout,
|
||||||
normalizedHiddenKeys
|
normalizedHiddenKeys
|
||||||
@@ -339,10 +308,35 @@ export function useNavLayout({
|
|||||||
{ deep: true }
|
{ 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 {
|
return {
|
||||||
navLayout,
|
navLayout,
|
||||||
navLayoutReady,
|
navLayoutReady,
|
||||||
navHiddenKeys,
|
navHiddenKeys,
|
||||||
|
defaultHiddenKeys: computed(() => getDefaultHiddenKeys(defaultNavLayout.value)),
|
||||||
menuItems,
|
menuItems,
|
||||||
activeMenuIndex,
|
activeMenuIndex,
|
||||||
allNavDefinitions,
|
allNavDefinitions,
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ export function triggerNavEntryAction(entry, { router, directAccessPaste }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry.action && typeof entry.action === 'object') {
|
||||||
|
return entry.action;
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.routeName) {
|
if (entry.routeName) {
|
||||||
navigateToRoute(router, entry.routeName, entry.routeParams);
|
navigateToRoute(router, entry.routeName, entry.routeParams);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import {
|
||||||
|
toolDefinitionMap,
|
||||||
|
toolDefinitions
|
||||||
|
} from '../shared/constants';
|
||||||
|
import {
|
||||||
|
useAdvancedSettingsStore,
|
||||||
|
useLaunchStore,
|
||||||
|
useToolsStore,
|
||||||
|
useVrcxStore
|
||||||
|
} from '../stores';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} definition
|
||||||
|
* @param {object} deps
|
||||||
|
* @param {object} deps.router
|
||||||
|
* @param {Function} deps.t
|
||||||
|
* @param {object} deps.toolsStore
|
||||||
|
* @param {object} deps.advancedSettingsStore
|
||||||
|
* @param {object} deps.launchStore
|
||||||
|
* @param {object} deps.vrcxStore
|
||||||
|
*/
|
||||||
|
export async function executeToolAction(
|
||||||
|
definition,
|
||||||
|
{
|
||||||
|
router,
|
||||||
|
t,
|
||||||
|
toolsStore,
|
||||||
|
advancedSettingsStore,
|
||||||
|
launchStore,
|
||||||
|
vrcxStore
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (!definition?.action) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { action } = definition;
|
||||||
|
|
||||||
|
if (action.type === 'route') {
|
||||||
|
router.push({ name: action.routeName });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'dialog') {
|
||||||
|
toolsStore.openDialog(toDialogStoreKey(action.dialogKey));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'store-action') {
|
||||||
|
const targetStore = {
|
||||||
|
advancedSettings: advancedSettingsStore,
|
||||||
|
launch: launchStore,
|
||||||
|
vrcx: vrcxStore
|
||||||
|
}[action.target];
|
||||||
|
|
||||||
|
targetStore?.[action.method]?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'app-api') {
|
||||||
|
const result = await AppApi[action.method]();
|
||||||
|
if (result) {
|
||||||
|
toast.success(t(action.successMessageKey));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.error(t(action.errorMessageKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} dialogKey
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function toDialogStoreKey(dialogKey) {
|
||||||
|
return dialogKey.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToolActions() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const toolsStore = useToolsStore();
|
||||||
|
const advancedSettingsStore = useAdvancedSettingsStore();
|
||||||
|
const launchStore = useLaunchStore();
|
||||||
|
const vrcxStore = useVrcxStore();
|
||||||
|
|
||||||
|
async function triggerTool(toolOrKey) {
|
||||||
|
const definition =
|
||||||
|
typeof toolOrKey === 'string'
|
||||||
|
? toolDefinitionMap.get(toolOrKey)
|
||||||
|
: toolOrKey;
|
||||||
|
|
||||||
|
await executeToolAction(definition, {
|
||||||
|
router,
|
||||||
|
t,
|
||||||
|
toolsStore,
|
||||||
|
advancedSettingsStore,
|
||||||
|
launchStore,
|
||||||
|
vrcxStore
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolDefinitions,
|
||||||
|
triggerTool
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import { computed, onMounted, onUnmounted, 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();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.addEventListener(NAV_LAYOUT_UPDATED_EVENT, refreshPinnedState);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.removeEventListener(
|
||||||
|
NAV_LAYOUT_UPDATED_EVENT,
|
||||||
|
refreshPinnedState
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
pinnedToolKeys: computed(() => pinnedToolKeysRef.value),
|
||||||
|
pinToolToNav,
|
||||||
|
unpinToolFromNav,
|
||||||
|
refreshPinnedState
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
"open": "Open",
|
"open": "Open",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
|
"delete": "Delete",
|
||||||
|
"remove": "Remove",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"view_details": "View Details"
|
"view_details": "View Details"
|
||||||
},
|
},
|
||||||
@@ -75,6 +77,10 @@
|
|||||||
"hide": "Hide",
|
"hide": "Hide",
|
||||||
"show": "Show",
|
"show": "Show",
|
||||||
"hidden_items": "Hidden",
|
"hidden_items": "Hidden",
|
||||||
|
"pin_to_nav": "Pin to Navigation",
|
||||||
|
"unpin_from_nav": "Unpin",
|
||||||
|
"pinned": "Added to navigation",
|
||||||
|
"unpinned": "Removed from navigation",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"restore_default": "Restore Default",
|
"restore_default": "Restore Default",
|
||||||
@@ -2417,6 +2423,7 @@
|
|||||||
"file": {
|
"file": {
|
||||||
"not_image": "File isn't an image",
|
"not_image": "File isn't an image",
|
||||||
"too_large": "File size too large",
|
"too_large": "File size too large",
|
||||||
|
"folder_opened": "Folder opened",
|
||||||
"folder_missing": "Folder doesn't exist"
|
"folder_missing": "Folder doesn't exist"
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
|
|||||||
@@ -114,13 +114,13 @@ const routes = [
|
|||||||
path: 'tools/gallery',
|
path: 'tools/gallery',
|
||||||
name: 'gallery',
|
name: 'gallery',
|
||||||
component: Gallery,
|
component: Gallery,
|
||||||
meta: { navKey: 'tools' }
|
meta: { navKeys: ['tool-gallery', 'tools'] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tools/screenshot-metadata',
|
path: 'tools/screenshot-metadata',
|
||||||
name: 'screenshot-metadata',
|
name: 'screenshot-metadata',
|
||||||
component: ScreenshotMetadata,
|
component: ScreenshotMetadata,
|
||||||
meta: { navKey: 'tools' }
|
meta: { navKeys: ['tool-screenshot-metadata', 'tools'] }
|
||||||
},
|
},
|
||||||
{ path: 'settings', name: 'settings', component: Settings }
|
{ path: 'settings', name: 'settings', component: Settings }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ export * from './ui';
|
|||||||
export * from './accessType';
|
export * from './accessType';
|
||||||
export * from './tags';
|
export * from './tags';
|
||||||
export * from './dashboard';
|
export * from './dashboard';
|
||||||
|
export * from './tools';
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
const toolCategories = [
|
||||||
|
{ key: 'image', labelKey: 'view.tools.pictures.header' },
|
||||||
|
{ key: 'shortcuts', labelKey: 'view.tools.shortcuts.header' },
|
||||||
|
{ key: 'system', labelKey: 'view.tools.system_tools.header' },
|
||||||
|
{ key: 'group', labelKey: 'view.tools.group.header' },
|
||||||
|
{ key: 'user', labelKey: 'view.tools.export.header' },
|
||||||
|
{ key: 'other', labelKey: 'view.tools.other.header' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const toolDefinitions = [
|
||||||
|
{
|
||||||
|
key: 'screenshot-metadata',
|
||||||
|
category: 'image',
|
||||||
|
iconKey: 'camera',
|
||||||
|
navIcon: 'ri-camera-line',
|
||||||
|
titleKey: 'view.tools.pictures.screenshot',
|
||||||
|
descriptionKey: 'view.tools.pictures.screenshot_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: { type: 'route', routeName: 'screenshot-metadata' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gallery',
|
||||||
|
category: 'image',
|
||||||
|
iconKey: 'image',
|
||||||
|
navIcon: 'ri-image-line',
|
||||||
|
titleKey: 'view.tools.pictures.inventory',
|
||||||
|
descriptionKey: 'view.tools.pictures.inventory_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: { type: 'route', routeName: 'gallery' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vrc-photos',
|
||||||
|
category: 'shortcuts',
|
||||||
|
iconKey: 'folder-open',
|
||||||
|
navIcon: 'ri-folder-image-line',
|
||||||
|
titleKey: 'view.tools.pictures.pictures.vrc_photos',
|
||||||
|
descriptionKey: 'view.tools.pictures.pictures.vrc_photos_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: {
|
||||||
|
type: 'app-api',
|
||||||
|
method: 'OpenVrcPhotosFolder',
|
||||||
|
successMessageKey: 'message.file.folder_opened',
|
||||||
|
errorMessageKey: 'message.file.folder_missing'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'steam-screenshots',
|
||||||
|
category: 'shortcuts',
|
||||||
|
iconKey: 'folder-image',
|
||||||
|
navIcon: 'ri-folder-image-line',
|
||||||
|
titleKey: 'view.tools.pictures.pictures.steam_screenshots',
|
||||||
|
descriptionKey: 'view.tools.pictures.pictures.steam_screenshots_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: {
|
||||||
|
type: 'app-api',
|
||||||
|
method: 'OpenVrcScreenshotsFolder',
|
||||||
|
successMessageKey: 'message.file.folder_opened',
|
||||||
|
errorMessageKey: 'message.file.folder_missing'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vrcx-data',
|
||||||
|
category: 'shortcuts',
|
||||||
|
iconKey: 'folder-cog',
|
||||||
|
navIcon: 'ri-folder-settings-line',
|
||||||
|
titleKey: 'view.tools.shortcuts.vrcx_data',
|
||||||
|
descriptionKey: 'view.tools.shortcuts.vrcx_data_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: {
|
||||||
|
type: 'app-api',
|
||||||
|
method: 'OpenVrcxAppDataFolder',
|
||||||
|
successMessageKey: 'message.file.folder_opened',
|
||||||
|
errorMessageKey: 'message.file.folder_missing'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vrchat-data',
|
||||||
|
category: 'shortcuts',
|
||||||
|
iconKey: 'folder-cog',
|
||||||
|
navIcon: 'ri-folder-settings-line',
|
||||||
|
titleKey: 'view.tools.shortcuts.vrchat_data',
|
||||||
|
descriptionKey: 'view.tools.shortcuts.vrchat_data_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: {
|
||||||
|
type: 'app-api',
|
||||||
|
method: 'OpenVrcAppDataFolder',
|
||||||
|
successMessageKey: 'message.file.folder_opened',
|
||||||
|
errorMessageKey: 'message.file.folder_missing'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'crash-dumps',
|
||||||
|
category: 'shortcuts',
|
||||||
|
iconKey: 'folder-x',
|
||||||
|
navIcon: 'ri-folder-warning-line',
|
||||||
|
titleKey: 'view.tools.shortcuts.crash_dumps',
|
||||||
|
descriptionKey: 'view.tools.shortcuts.crash_dumps_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: {
|
||||||
|
type: 'app-api',
|
||||||
|
method: 'OpenCrashVrcCrashDumps',
|
||||||
|
successMessageKey: 'message.file.folder_opened',
|
||||||
|
errorMessageKey: 'message.file.folder_missing'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vrchat-config',
|
||||||
|
category: 'system',
|
||||||
|
iconKey: 'sliders-horizontal',
|
||||||
|
navIcon: 'ri-settings-3-line',
|
||||||
|
titleKey: 'view.tools.system_tools.vrchat_config',
|
||||||
|
descriptionKey: 'view.tools.system_tools.vrchat_config_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: {
|
||||||
|
type: 'store-action',
|
||||||
|
target: 'advancedSettings',
|
||||||
|
method: 'showVRChatConfig'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'launch-options',
|
||||||
|
category: 'system',
|
||||||
|
iconKey: 'terminal',
|
||||||
|
navIcon: 'ri-terminal-box-line',
|
||||||
|
titleKey: 'view.settings.advanced.advanced.launch_options',
|
||||||
|
descriptionKey: 'view.tools.system_tools.launch_options_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: {
|
||||||
|
type: 'store-action',
|
||||||
|
target: 'launch',
|
||||||
|
method: 'showLaunchOptions'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'registry-backup',
|
||||||
|
category: 'system',
|
||||||
|
iconKey: 'archive',
|
||||||
|
navIcon: 'ri-archive-stack-line',
|
||||||
|
titleKey: 'view.settings.advanced.advanced.vrc_registry_backup',
|
||||||
|
descriptionKey: 'view.tools.system_tools.registry_backup_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: {
|
||||||
|
type: 'store-action',
|
||||||
|
target: 'vrcx',
|
||||||
|
method: 'showRegistryBackupDialog'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'auto-change-status',
|
||||||
|
category: 'system',
|
||||||
|
iconKey: 'bot',
|
||||||
|
navIcon: 'ri-user-settings-line',
|
||||||
|
titleKey: 'view.settings.general.automation.auto_change_status',
|
||||||
|
descriptionKey: 'view.settings.general.automation.auto_state_change_tooltip',
|
||||||
|
navEligible: true,
|
||||||
|
action: { type: 'dialog', dialogKey: 'auto-change-status' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'group-calendar',
|
||||||
|
category: 'group',
|
||||||
|
iconKey: 'calendar',
|
||||||
|
navIcon: 'ri-calendar-event-line',
|
||||||
|
titleKey: 'view.tools.group.calendar',
|
||||||
|
descriptionKey: 'view.tools.group.calendar_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: { type: 'dialog', dialogKey: 'group-calendar' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'discord-names',
|
||||||
|
category: 'user',
|
||||||
|
iconKey: 'users',
|
||||||
|
navIcon: 'ri-discord-line',
|
||||||
|
titleKey: 'view.tools.export.discord_names',
|
||||||
|
descriptionKey: 'view.tools.user.discord_names_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: { type: 'dialog', dialogKey: 'export-discord-names' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'export-notes',
|
||||||
|
category: 'user',
|
||||||
|
iconKey: 'file-text',
|
||||||
|
navIcon: 'ri-file-list-3-line',
|
||||||
|
titleKey: 'view.tools.export.export_notes',
|
||||||
|
descriptionKey: 'view.tools.export.export_notes_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: { type: 'dialog', dialogKey: 'note-export' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'export-friend-list',
|
||||||
|
category: 'user',
|
||||||
|
iconKey: 'users',
|
||||||
|
navIcon: 'ri-file-list-3-line',
|
||||||
|
titleKey: 'view.tools.export.export_friend_list',
|
||||||
|
descriptionKey: 'view.tools.user.export_friend_list_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: { type: 'dialog', dialogKey: 'export-friends-list' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'export-own-avatars',
|
||||||
|
category: 'user',
|
||||||
|
iconKey: 'download',
|
||||||
|
navIcon: 'ri-file-list-3-line',
|
||||||
|
titleKey: 'view.tools.export.export_own_avatars',
|
||||||
|
descriptionKey: 'view.tools.user.export_own_avatars_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: { type: 'dialog', dialogKey: 'export-avatars-list' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'edit-invite-message',
|
||||||
|
category: 'other',
|
||||||
|
iconKey: 'pencil',
|
||||||
|
navIcon: 'ri-quill-pen-line',
|
||||||
|
titleKey: 'view.tools.other.edit_invite_message',
|
||||||
|
descriptionKey: 'view.tools.other.edit_invite_message_description',
|
||||||
|
navEligible: true,
|
||||||
|
action: { type: 'dialog', dialogKey: 'edit-invite-messages' }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const toolDefinitionMap = new Map(
|
||||||
|
toolDefinitions.map((tool) => [tool.key, tool])
|
||||||
|
);
|
||||||
|
|
||||||
|
const toolNavDefinitions = toolDefinitions
|
||||||
|
.filter((tool) => tool.navEligible)
|
||||||
|
.map((tool) => ({
|
||||||
|
key: `tool-${tool.key}`,
|
||||||
|
icon: tool.navIcon,
|
||||||
|
tooltip: tool.titleKey,
|
||||||
|
labelKey: tool.titleKey,
|
||||||
|
routeName: tool.action.type === 'route' ? tool.action.routeName : null,
|
||||||
|
action:
|
||||||
|
tool.action.type === 'route'
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
type: 'tool',
|
||||||
|
toolKey: tool.key
|
||||||
|
},
|
||||||
|
defaultHidden: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const defaultHiddenToolNavKeys = toolNavDefinitions.map((tool) => tool.key);
|
||||||
|
const isToolNavKey = (key) => typeof key === 'string' && key.startsWith('tool-');
|
||||||
|
|
||||||
|
function getToolsByCategory(categoryKey) {
|
||||||
|
return toolDefinitions.filter((tool) => tool.category === categoryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
defaultHiddenToolNavKeys,
|
||||||
|
isToolNavKey,
|
||||||
|
toolCategories,
|
||||||
|
toolDefinitions,
|
||||||
|
toolDefinitionMap,
|
||||||
|
toolNavDefinitions,
|
||||||
|
getToolsByCategory
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { toolNavDefinitions } from './tools';
|
||||||
|
|
||||||
const navDefinitions = [
|
const navDefinitions = [
|
||||||
{
|
{
|
||||||
key: 'feed',
|
key: 'feed',
|
||||||
@@ -117,7 +119,8 @@ const navDefinitions = [
|
|||||||
tooltip: 'prompt.direct_access_omni.header',
|
tooltip: 'prompt.direct_access_omni.header',
|
||||||
labelKey: 'prompt.direct_access_omni.header',
|
labelKey: 'prompt.direct_access_omni.header',
|
||||||
action: 'direct-access'
|
action: 'direct-access'
|
||||||
}
|
},
|
||||||
|
...toolNavDefinitions
|
||||||
];
|
];
|
||||||
|
|
||||||
export { navDefinitions };
|
export { navDefinitions };
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { usePhotonStore } from './photon';
|
|||||||
import { useSearchStore } from './search';
|
import { useSearchStore } from './search';
|
||||||
import { useSharedFeedStore } from './sharedFeed';
|
import { useSharedFeedStore } from './sharedFeed';
|
||||||
import { useUiStore } from './ui';
|
import { useUiStore } from './ui';
|
||||||
|
import { useToolsStore } from './tools';
|
||||||
import { useUpdateLoopStore } from './updateLoop';
|
import { useUpdateLoopStore } from './updateLoop';
|
||||||
import { useUserStore } from './user';
|
import { useUserStore } from './user';
|
||||||
import { useVRCXUpdaterStore } from './vrcxUpdater';
|
import { useVRCXUpdaterStore } from './vrcxUpdater';
|
||||||
@@ -154,6 +155,7 @@ export function createGlobalStores() {
|
|||||||
notification: useNotificationStore(),
|
notification: useNotificationStore(),
|
||||||
feed: useFeedStore(),
|
feed: useFeedStore(),
|
||||||
ui: useUiStore(),
|
ui: useUiStore(),
|
||||||
|
tools: useToolsStore(),
|
||||||
gameLog: useGameLogStore(),
|
gameLog: useGameLogStore(),
|
||||||
search: useSearchStore(),
|
search: useSearchStore(),
|
||||||
game: useGameStore(),
|
game: useGameStore(),
|
||||||
@@ -198,6 +200,7 @@ export {
|
|||||||
useGeneralSettingsStore,
|
useGeneralSettingsStore,
|
||||||
useNotificationsSettingsStore,
|
useNotificationsSettingsStore,
|
||||||
useWristOverlaySettingsStore,
|
useWristOverlaySettingsStore,
|
||||||
|
useToolsStore,
|
||||||
useUiStore,
|
useUiStore,
|
||||||
useUserStore,
|
useUserStore,
|
||||||
useVrStore,
|
useVrStore,
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { reactive, toRefs } from 'vue';
|
||||||
|
|
||||||
|
const initialDialogState = () => ({
|
||||||
|
groupCalendar: false,
|
||||||
|
noteExport: false,
|
||||||
|
exportDiscordNames: false,
|
||||||
|
exportFriendsList: false,
|
||||||
|
exportAvatarsList: false,
|
||||||
|
editInviteMessages: false,
|
||||||
|
autoChangeStatus: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useToolsStore = defineStore('Tools', () => {
|
||||||
|
const dialogs = reactive(initialDialogState());
|
||||||
|
|
||||||
|
function setDialogVisible(dialogKey, value) {
|
||||||
|
if (!(dialogKey in dialogs)) {
|
||||||
|
console.warn(
|
||||||
|
`[toolsStore] Unknown dialog key "${dialogKey}" passed to setDialogVisible`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialogs[dialogKey] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDialog(dialogKey) {
|
||||||
|
setDialogVisible(dialogKey, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog(dialogKey) {
|
||||||
|
setDialogVisible(dialogKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllDialogs() {
|
||||||
|
Object.keys(dialogs).forEach((dialogKey) => {
|
||||||
|
dialogs[dialogKey] = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(dialogs),
|
||||||
|
setDialogVisible,
|
||||||
|
openDialog,
|
||||||
|
closeDialog,
|
||||||
|
closeAllDialogs
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -84,6 +84,8 @@
|
|||||||
<SendBoopDialog></SendBoopDialog>
|
<SendBoopDialog></SendBoopDialog>
|
||||||
|
|
||||||
<ChangelogDialog></ChangelogDialog>
|
<ChangelogDialog></ChangelogDialog>
|
||||||
|
|
||||||
|
<GlobalToolsDialogs></GlobalToolsDialogs>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -103,6 +105,7 @@
|
|||||||
import ChooseFavoriteGroupDialog from '../../components/dialogs/ChooseFavoriteGroupDialog.vue';
|
import ChooseFavoriteGroupDialog from '../../components/dialogs/ChooseFavoriteGroupDialog.vue';
|
||||||
import FriendImportDialog from '../Favorites/dialogs/FriendImportDialog.vue';
|
import FriendImportDialog from '../Favorites/dialogs/FriendImportDialog.vue';
|
||||||
import FullscreenImagePreview from '../../components/FullscreenImagePreview.vue';
|
import FullscreenImagePreview from '../../components/FullscreenImagePreview.vue';
|
||||||
|
import GlobalToolsDialogs from '../Tools/components/GlobalToolsDialogs.vue';
|
||||||
import GroupMemberModerationDialog from '../../components/dialogs/GroupDialog/GroupMemberModerationDialog.vue';
|
import GroupMemberModerationDialog from '../../components/dialogs/GroupDialog/GroupMemberModerationDialog.vue';
|
||||||
import InviteGroupDialog from '../../components/dialogs/InviteGroupDialog.vue';
|
import InviteGroupDialog from '../../components/dialogs/InviteGroupDialog.vue';
|
||||||
import LaunchDialog from '../../components/dialogs/LaunchDialog.vue';
|
import LaunchDialog from '../../components/dialogs/LaunchDialog.vue';
|
||||||
|
|||||||
+118
-324
@@ -4,232 +4,134 @@
|
|||||||
<span class="header">{{ t('view.tools.header') }}</span>
|
<span class="header">{{ t('view.tools.header') }}</span>
|
||||||
|
|
||||||
<div class="mt-5 px-5">
|
<div class="mt-5 px-5">
|
||||||
<div class="mb-6">
|
<div
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.key"
|
||||||
|
class="mb-6">
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer flex items-center p-2 px-3 rounded-lg mb-3 transition-all duration-200 ease-in-out"
|
class="cursor-pointer flex items-center p-2 px-3 rounded-lg mb-3 transition-all duration-200 ease-in-out"
|
||||||
@click="toggleCategory('image')">
|
@click="toggleCategory(category.key)">
|
||||||
<ChevronDown
|
<i
|
||||||
class="text-sm mr-2 transition-transform duration-300"
|
class="ri-arrow-down-s-line mr-2 text-sm transition-transform duration-300"
|
||||||
:class="{ '-rotate-90': categoryCollapsed['image'] }" />
|
:class="{ '-rotate-90': categoryCollapsed[category.key] }" />
|
||||||
<span class="ml-1.5 text-base font-semibold">{{ t('view.tools.pictures.header') }}</span>
|
<span class="ml-1.5 text-base font-semibold">
|
||||||
</div>
|
{{ t(category.labelKey) }}
|
||||||
<div class="grid grid-cols-2 gap-4 ml-4" v-show="!categoryCollapsed['image']">
|
</span>
|
||||||
<ToolItem
|
|
||||||
:icon="Camera"
|
|
||||||
:title="t('view.tools.pictures.screenshot')"
|
|
||||||
:description="t('view.tools.pictures.screenshot_description')"
|
|
||||||
@click="showScreenshotMetadataPage" />
|
|
||||||
<ToolItem
|
|
||||||
:icon="Images"
|
|
||||||
:title="t('view.tools.pictures.inventory')"
|
|
||||||
:description="t('view.tools.pictures.inventory_description')"
|
|
||||||
@click="showGalleryPage" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer flex items-center p-2 px-3 rounded-lg mb-3 transition-all duration-200 ease-in-out"
|
class="grid grid-cols-2 gap-4 ml-4"
|
||||||
@click="toggleCategory('shortcuts')">
|
v-show="!categoryCollapsed[category.key]">
|
||||||
<ChevronDown
|
|
||||||
class="text-sm mr-2 transition-transform duration-300"
|
|
||||||
:class="{ '-rotate-90': categoryCollapsed['shortcuts'] }" />
|
|
||||||
<span class="ml-1.5 text-base font-semibold">{{ t('view.tools.shortcuts.header') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4 ml-4" v-show="!categoryCollapsed['shortcuts']">
|
|
||||||
<ToolItem
|
<ToolItem
|
||||||
:icon="Folder"
|
v-for="tool in category.tools"
|
||||||
:title="t('view.tools.pictures.pictures.vrc_photos')"
|
:key="tool.key"
|
||||||
:description="t('view.tools.pictures.pictures.vrc_photos_description')"
|
:icon="tool.navIcon"
|
||||||
@click="openVrcPhotosFolder" />
|
:title="t(tool.titleKey)"
|
||||||
<ToolItem
|
:description="t(tool.descriptionKey)"
|
||||||
:icon="Folder"
|
@click="triggerTool(tool)">
|
||||||
:title="t('view.tools.pictures.pictures.steam_screenshots')"
|
<template #actions>
|
||||||
:description="t('view.tools.pictures.pictures.steam_screenshots_description')"
|
<TooltipWrapper
|
||||||
@click="openVrcScreenshotsFolder" />
|
v-if="
|
||||||
<ToolItem
|
tool.navEligible &&
|
||||||
:icon="Folder"
|
pinnedToolKeys.has(tool.key)
|
||||||
:title="t('view.tools.shortcuts.vrcx_data')"
|
"
|
||||||
:description="t('view.tools.shortcuts.vrcx_data_description')"
|
side="top"
|
||||||
@click="openVrcxAppDataFolder" />
|
:content="
|
||||||
<ToolItem
|
t('nav_menu.custom_nav.unpin_from_nav')
|
||||||
:icon="Folder"
|
">
|
||||||
:title="t('view.tools.shortcuts.vrchat_data')"
|
<Button
|
||||||
:description="t('view.tools.shortcuts.vrchat_data_description')"
|
size="icon-xs"
|
||||||
@click="openVrcAppDataFolder" />
|
variant="secondary"
|
||||||
<ToolItem
|
class="opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
:icon="Folder"
|
:title="
|
||||||
:title="t('view.tools.shortcuts.crash_dumps')"
|
t(
|
||||||
:description="t('view.tools.shortcuts.crash_dumps_description')"
|
'nav_menu.custom_nav.unpin_from_nav'
|
||||||
@click="openCrashVrcCrashDumps" />
|
)
|
||||||
</div>
|
"
|
||||||
</div>
|
:aria-label="
|
||||||
|
t(
|
||||||
|
'nav_menu.custom_nav.unpin_from_nav'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@click.stop="unpinToolFromNav(tool.key)">
|
||||||
|
<span class="relative inline-flex size-4">
|
||||||
|
<i
|
||||||
|
class="ri-side-bar-line inline-flex size-4 items-center justify-center text-base" />
|
||||||
|
<span
|
||||||
|
class="absolute -right-1 -top-1 grid size-2.5 place-items-center rounded-full bg-background shadow-sm">
|
||||||
|
<i
|
||||||
|
class="ri-subtract-line inline-flex size-2 items-center justify-center text-[10px]" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipWrapper>
|
||||||
|
|
||||||
<div class="mb-6">
|
<TooltipWrapper
|
||||||
<div
|
v-else-if="tool.navEligible"
|
||||||
class="cursor-pointer flex items-center p-2 px-3 rounded-lg mb-3 transition-all duration-200 ease-in-out"
|
side="top"
|
||||||
@click="toggleCategory('system')">
|
:content="t('nav_menu.custom_nav.pin_to_nav')">
|
||||||
<ChevronDown
|
<Button
|
||||||
class="text-sm mr-2 transition-transform duration-300"
|
size="icon-xs"
|
||||||
:class="{ '-rotate-90': categoryCollapsed['system'] }" />
|
variant="ghost"
|
||||||
<span class="ml-1.5 text-base font-semibold">{{ t('view.tools.system_tools.header') }}</span>
|
class="opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
</div>
|
:title="
|
||||||
<div class="grid grid-cols-2 gap-4 ml-4" v-show="!categoryCollapsed['system']">
|
t('nav_menu.custom_nav.pin_to_nav')
|
||||||
<ToolItem
|
"
|
||||||
:icon="Settings"
|
:aria-label="
|
||||||
:title="t('view.tools.system_tools.vrchat_config')"
|
t('nav_menu.custom_nav.pin_to_nav')
|
||||||
:description="t('view.tools.system_tools.vrchat_config_description')"
|
"
|
||||||
@click="showVRChatConfig" />
|
@click.stop="pinToolToNav(tool.key)">
|
||||||
<ToolItem
|
<span class="relative inline-flex size-4">
|
||||||
:icon="Settings"
|
<i
|
||||||
:title="t('view.settings.advanced.advanced.launch_options')"
|
class="ri-side-bar-line inline-flex size-4 items-center justify-center text-base" />
|
||||||
:description="t('view.tools.system_tools.launch_options_description')"
|
<span
|
||||||
@click="showLaunchOptions" />
|
class="absolute -right-1 -top-1 grid size-2.5 place-items-center rounded-full bg-background shadow-sm">
|
||||||
<ToolItem
|
<i
|
||||||
:icon="Settings"
|
class="ri-add-line inline-flex size-2 items-center justify-center text-[10px]" />
|
||||||
:title="t('view.settings.advanced.advanced.vrc_registry_backup')"
|
</span>
|
||||||
:description="t('view.tools.system_tools.registry_backup_description')"
|
</span>
|
||||||
@click="showRegistryBackupDialog" />
|
</Button>
|
||||||
<ToolItem
|
</TooltipWrapper>
|
||||||
:icon="Settings"
|
|
||||||
:title="t('view.settings.general.automation.auto_change_status')"
|
|
||||||
:description="t('view.settings.general.automation.auto_state_change_tooltip')"
|
|
||||||
@click="showAutoChangeStatusDialog" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<div
|
|
||||||
class="cursor-pointer flex items-center p-2 px-3 rounded-lg mb-3 transition-all duration-200 ease-in-out"
|
|
||||||
@click="toggleCategory('group')">
|
|
||||||
<ChevronDown
|
|
||||||
class="text-sm mr-2 transition-transform duration-300"
|
|
||||||
:class="{ '-rotate-90': categoryCollapsed['group'] }" />
|
|
||||||
<span class="ml-1.5 text-base font-semibold">{{ t('view.tools.group.header') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4 ml-4" v-show="!categoryCollapsed['group']">
|
|
||||||
<ToolItem
|
|
||||||
:icon="CalendarDays"
|
|
||||||
:title="t('view.tools.group.calendar')"
|
|
||||||
:description="t('view.tools.group.calendar_description')"
|
|
||||||
@click="showGroupCalendarDialog" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<div
|
|
||||||
class="cursor-pointer flex items-center p-2 px-3 rounded-lg mb-3 transition-all duration-200 ease-in-out"
|
|
||||||
@click="toggleCategory('user')">
|
|
||||||
<ChevronDown
|
|
||||||
class="text-sm mr-2 transition-transform duration-300"
|
|
||||||
:class="{ '-rotate-90': categoryCollapsed['user'] }" />
|
|
||||||
<span class="ml-1.5 text-base font-semibold">{{ t('view.tools.export.header') }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 ml-4" v-show="!categoryCollapsed['user']">
|
|
||||||
<ToolItem
|
|
||||||
:icon="FolderInput"
|
|
||||||
:title="t('view.tools.export.discord_names')"
|
|
||||||
:description="t('view.tools.user.discord_names_description')"
|
|
||||||
@click="showExportDiscordNamesDialog" />
|
|
||||||
<ToolItem
|
|
||||||
:icon="FolderInput"
|
|
||||||
:title="t('view.tools.export.export_notes')"
|
|
||||||
:description="t('view.tools.export.export_notes_description')"
|
|
||||||
@click="showNoteExportDialog" />
|
|
||||||
<ToolItem
|
|
||||||
:icon="FolderInput"
|
|
||||||
:title="t('view.tools.export.export_friend_list')"
|
|
||||||
:description="t('view.tools.user.export_friend_list_description')"
|
|
||||||
@click="showExportFriendsListDialog" />
|
|
||||||
<ToolItem
|
|
||||||
:icon="FolderInput"
|
|
||||||
:title="t('view.tools.export.export_own_avatars')"
|
|
||||||
:description="t('view.tools.user.export_own_avatars_description')"
|
|
||||||
@click="showExportAvatarsListDialog" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<div
|
|
||||||
class="cursor-pointer flex items-center p-2 px-3 rounded-lg mb-3 transition-all duration-200 ease-in-out"
|
|
||||||
@click="toggleCategory('other')">
|
|
||||||
<ChevronDown
|
|
||||||
class="text-sm mr-2 transition-transform duration-300"
|
|
||||||
:class="{ '-rotate-90': categoryCollapsed['other'] }" />
|
|
||||||
<span class="ml-1.5 text-base font-semibold">{{ t('view.tools.other.header') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4 ml-4" v-show="!categoryCollapsed['other']">
|
|
||||||
<ToolItem
|
|
||||||
:icon="SquarePen"
|
|
||||||
:title="t('view.tools.other.edit_invite_message')"
|
|
||||||
:description="t('view.tools.other.edit_invite_message_description')"
|
|
||||||
@click="showEditInviteMessageDialog" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-if="isToolsTabVisible">
|
|
||||||
<GroupCalendarDialog
|
|
||||||
:visible="isGroupCalendarDialogVisible"
|
|
||||||
@close="isGroupCalendarDialogVisible = false" />
|
|
||||||
<NoteExportDialog
|
|
||||||
:isNoteExportDialogVisible="isNoteExportDialogVisible"
|
|
||||||
@close="isNoteExportDialogVisible = false" />
|
|
||||||
<ExportDiscordNamesDialog
|
|
||||||
v-model:discordNamesDialogVisible="isExportDiscordNamesDialogVisible"
|
|
||||||
:friends="friends" />
|
|
||||||
<ExportFriendsListDialog
|
|
||||||
v-model:isExportFriendsListDialogVisible="isExportFriendsListDialogVisible"
|
|
||||||
:friends="friends" />
|
|
||||||
<ExportAvatarsListDialog v-model:isExportAvatarsListDialogVisible="isExportAvatarsListDialogVisible" />
|
|
||||||
<EditInviteMessageDialog
|
|
||||||
v-model:isEditInviteMessagesDialogVisible="isEditInviteMessagesDialogVisible"
|
|
||||||
@close="isEditInviteMessagesDialogVisible = false" />
|
|
||||||
<RegistryBackupDialog />
|
|
||||||
<AutoChangeStatusDialog
|
|
||||||
:isAutoChangeStatusDialogVisible="isAutoChangeStatusDialogVisible"
|
|
||||||
@close="isAutoChangeStatusDialogVisible = false" />
|
|
||||||
</template>
|
</template>
|
||||||
|
</ToolItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { CalendarDays, Camera, ChevronDown, Folder, FolderInput, Images, Settings, SquarePen } from 'lucide-vue-next';
|
import { onMounted, ref } from 'vue';
|
||||||
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import ToolItem from './components/ToolItem.vue';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { toast } from 'vue-sonner';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { useFriendStore, useGalleryStore } from '../../stores';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAdvancedSettingsStore } from '../../stores/settings/advanced';
|
import { TooltipWrapper } from '@/components/ui/tooltip';
|
||||||
import { useLaunchStore } from '../../stores/launch';
|
import ToolItem from './components/ToolItem.vue';
|
||||||
import { useVrcxStore } from '../../stores/vrcx';
|
import { useToolActions } from '../../composables/useToolActions';
|
||||||
|
import { useToolNavPinning } from '../../composables/useToolNavPinning';
|
||||||
import AutoChangeStatusDialog from './dialogs/AutoChangeStatusDialog.vue';
|
import {
|
||||||
|
getToolsByCategory,
|
||||||
|
toolCategories
|
||||||
|
} from '../../shared/constants';
|
||||||
import configRepository from '../../services/config.js';
|
import configRepository from '../../services/config.js';
|
||||||
|
|
||||||
const GroupCalendarDialog = defineAsyncComponent(() => import('./dialogs/GroupCalendarDialog.vue'));
|
|
||||||
const NoteExportDialog = defineAsyncComponent(() => import('./dialogs/NoteExportDialog.vue'));
|
|
||||||
const EditInviteMessageDialog = defineAsyncComponent(() => import('./dialogs/EditInviteMessagesDialog.vue'));
|
|
||||||
const ExportDiscordNamesDialog = defineAsyncComponent(() => import('./dialogs/ExportDiscordNamesDialog.vue'));
|
|
||||||
const ExportFriendsListDialog = defineAsyncComponent(() => import('./dialogs/ExportFriendsListDialog.vue'));
|
|
||||||
const ExportAvatarsListDialog = defineAsyncComponent(() => import('./dialogs/ExportAvatarsListDialog.vue'));
|
|
||||||
import RegistryBackupDialog from './dialogs/RegistryBackupDialog.vue';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const { triggerTool } = useToolActions();
|
||||||
const route = useRoute();
|
const {
|
||||||
|
pinToolToNav,
|
||||||
const { showGalleryPage } = useGalleryStore();
|
pinnedToolKeys,
|
||||||
const { friends } = storeToRefs(useFriendStore());
|
refreshPinnedState,
|
||||||
const { showVRChatConfig } = useAdvancedSettingsStore();
|
unpinToolFromNav
|
||||||
const { showLaunchOptions } = useLaunchStore();
|
} =
|
||||||
const { showRegistryBackupDialog } = useVrcxStore();
|
useToolNavPinning();
|
||||||
const toolsCategoryCollapsedConfigKey = 'VRCX_toolsCategoryCollapsed';
|
const toolsCategoryCollapsedConfigKey = 'VRCX_toolsCategoryCollapsed';
|
||||||
|
|
||||||
|
const categories = toolCategories.map((category) => ({
|
||||||
|
...category,
|
||||||
|
tools: getToolsByCategory(category.key)
|
||||||
|
}));
|
||||||
|
|
||||||
const categoryCollapsed = ref({
|
const categoryCollapsed = ref({
|
||||||
group: false,
|
group: false,
|
||||||
image: false,
|
image: false,
|
||||||
@@ -239,34 +141,20 @@
|
|||||||
other: false
|
other: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const isGroupCalendarDialogVisible = ref(false);
|
|
||||||
const isNoteExportDialogVisible = ref(false);
|
|
||||||
const isExportDiscordNamesDialogVisible = ref(false);
|
|
||||||
const isExportFriendsListDialogVisible = ref(false);
|
|
||||||
const isExportAvatarsListDialogVisible = ref(false);
|
|
||||||
const isEditInviteMessagesDialogVisible = ref(false);
|
|
||||||
const isAutoChangeStatusDialogVisible = ref(false);
|
|
||||||
const isToolsTabVisible = computed(() => route.name === 'tools');
|
|
||||||
|
|
||||||
const showGroupCalendarDialog = () => {
|
|
||||||
isGroupCalendarDialogVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showScreenshotMetadataPage = () => {
|
|
||||||
router.push({ name: 'screenshot-metadata' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const showNoteExportDialog = () => {
|
|
||||||
isNoteExportDialogVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleCategory = (category) => {
|
const toggleCategory = (category) => {
|
||||||
categoryCollapsed.value[category] = !categoryCollapsed.value[category];
|
categoryCollapsed.value[category] = !categoryCollapsed.value[category];
|
||||||
configRepository.setString(toolsCategoryCollapsedConfigKey, JSON.stringify(categoryCollapsed.value));
|
configRepository.setString(
|
||||||
|
toolsCategoryCollapsedConfigKey,
|
||||||
|
JSON.stringify(categoryCollapsed.value)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const storedValue = await configRepository.getString(toolsCategoryCollapsedConfigKey, '{}');
|
await refreshPinnedState();
|
||||||
|
const storedValue = await configRepository.getString(
|
||||||
|
toolsCategoryCollapsedConfigKey,
|
||||||
|
'{}'
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(storedValue);
|
const parsed = JSON.parse(storedValue);
|
||||||
categoryCollapsed.value = {
|
categoryCollapsed.value = {
|
||||||
@@ -277,98 +165,4 @@
|
|||||||
// ignore invalid stored value and keep defaults
|
// ignore invalid stored value and keep defaults
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const showEditInviteMessageDialog = () => {
|
|
||||||
isEditInviteMessagesDialogVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showAutoChangeStatusDialog = () => {
|
|
||||||
isAutoChangeStatusDialogVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function showExportDiscordNamesDialog() {
|
|
||||||
isExportDiscordNamesDialogVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function showExportFriendsListDialog() {
|
|
||||||
isExportFriendsListDialogVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function showExportAvatarsListDialog() {
|
|
||||||
isExportAvatarsListDialogVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function openVrcPhotosFolder() {
|
|
||||||
AppApi.OpenVrcPhotosFolder().then((result) => {
|
|
||||||
if (result) {
|
|
||||||
toast.success('Folder opened');
|
|
||||||
} else {
|
|
||||||
toast.error(t('message.file.folder_missing'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function openVrcScreenshotsFolder() {
|
|
||||||
AppApi.OpenVrcScreenshotsFolder().then((result) => {
|
|
||||||
if (result) {
|
|
||||||
toast.success('Folder opened');
|
|
||||||
} else {
|
|
||||||
toast.error(t('message.file.folder_missing'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function openVrcxAppDataFolder() {
|
|
||||||
AppApi.OpenVrcxAppDataFolder().then((result) => {
|
|
||||||
if (result) {
|
|
||||||
toast.success('Folder opened');
|
|
||||||
} else {
|
|
||||||
toast.error(t('message.file.folder_missing'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function openVrcAppDataFolder() {
|
|
||||||
AppApi.OpenVrcAppDataFolder().then((result) => {
|
|
||||||
if (result) {
|
|
||||||
toast.success('Folder opened');
|
|
||||||
} else {
|
|
||||||
toast.error(t('message.file.folder_missing'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function openCrashVrcCrashDumps() {
|
|
||||||
AppApi.OpenCrashVrcCrashDumps().then((result) => {
|
|
||||||
if (result) {
|
|
||||||
toast.success('Folder opened');
|
|
||||||
} else {
|
|
||||||
toast.error(t('message.file.folder_missing'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<GroupCalendarDialog
|
||||||
|
:visible="groupCalendar"
|
||||||
|
@close="closeDialog('groupCalendar')" />
|
||||||
|
<NoteExportDialog
|
||||||
|
:isNoteExportDialogVisible="noteExport"
|
||||||
|
@close="closeDialog('noteExport')" />
|
||||||
|
<ExportDiscordNamesDialog
|
||||||
|
v-model:discordNamesDialogVisible="exportDiscordNames"
|
||||||
|
:friends="friends" />
|
||||||
|
<ExportFriendsListDialog
|
||||||
|
v-model:isExportFriendsListDialogVisible="exportFriendsList"
|
||||||
|
:friends="friends" />
|
||||||
|
<ExportAvatarsListDialog
|
||||||
|
v-model:isExportAvatarsListDialogVisible="exportAvatarsList" />
|
||||||
|
<EditInviteMessageDialog
|
||||||
|
v-model:isEditInviteMessagesDialogVisible="editInviteMessages"
|
||||||
|
@close="closeDialog('editInviteMessages')" />
|
||||||
|
<RegistryBackupDialog />
|
||||||
|
<AutoChangeStatusDialog
|
||||||
|
:isAutoChangeStatusDialogVisible="autoChangeStatus"
|
||||||
|
@close="closeDialog('autoChangeStatus')" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
import { useFriendStore, useToolsStore } from '../../../stores';
|
||||||
|
|
||||||
|
import AutoChangeStatusDialog from '../dialogs/AutoChangeStatusDialog.vue';
|
||||||
|
import RegistryBackupDialog from '../dialogs/RegistryBackupDialog.vue';
|
||||||
|
|
||||||
|
const GroupCalendarDialog = defineAsyncComponent(
|
||||||
|
() => import('../dialogs/GroupCalendarDialog.vue')
|
||||||
|
);
|
||||||
|
const NoteExportDialog = defineAsyncComponent(
|
||||||
|
() => import('../dialogs/NoteExportDialog.vue')
|
||||||
|
);
|
||||||
|
const EditInviteMessageDialog = defineAsyncComponent(
|
||||||
|
() => import('../dialogs/EditInviteMessagesDialog.vue')
|
||||||
|
);
|
||||||
|
const ExportDiscordNamesDialog = defineAsyncComponent(
|
||||||
|
() => import('../dialogs/ExportDiscordNamesDialog.vue')
|
||||||
|
);
|
||||||
|
const ExportFriendsListDialog = defineAsyncComponent(
|
||||||
|
() => import('../dialogs/ExportFriendsListDialog.vue')
|
||||||
|
);
|
||||||
|
const ExportAvatarsListDialog = defineAsyncComponent(
|
||||||
|
() => import('../dialogs/ExportAvatarsListDialog.vue')
|
||||||
|
);
|
||||||
|
|
||||||
|
const { friends } = storeToRefs(useFriendStore());
|
||||||
|
const toolsStore = useToolsStore();
|
||||||
|
const {
|
||||||
|
autoChangeStatus,
|
||||||
|
editInviteMessages,
|
||||||
|
exportAvatarsList,
|
||||||
|
exportDiscordNames,
|
||||||
|
exportFriendsList,
|
||||||
|
groupCalendar,
|
||||||
|
noteExport
|
||||||
|
} = storeToRefs(toolsStore);
|
||||||
|
|
||||||
|
const { closeDialog } = toolsStore;
|
||||||
|
</script>
|
||||||
@@ -2,19 +2,22 @@
|
|||||||
import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from '@/components/ui/item';
|
import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from '@/components/ui/item';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
icon: { type: [Object, Function], required: true },
|
icon: { type: String, required: true },
|
||||||
title: { type: String, required: true },
|
title: { type: String, required: true },
|
||||||
description: { type: String, required: true }
|
description: { type: String, required: true }
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Item variant="outline" class="cursor-pointer hover:bg-accent/50">
|
<Item variant="outline" class="group cursor-pointer hover:bg-accent/50">
|
||||||
<ItemMedia variant="icon" class="bg-transparent border-0">
|
<ItemMedia variant="icon" class="bg-transparent border-0">
|
||||||
<component :is="icon" class="text-2xl" />
|
<i :class="[icon, 'inline-flex items-center justify-center text-2xl']" />
|
||||||
</ItemMedia>
|
</ItemMedia>
|
||||||
<ItemContent>
|
<ItemContent>
|
||||||
<ItemTitle>{{ title }}</ItemTitle>
|
<div class="flex items-start gap-2">
|
||||||
|
<ItemTitle class="flex-1">{{ title }}</ItemTitle>
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
<ItemDescription>{{ description }}</ItemDescription>
|
<ItemDescription>{{ description }}</ItemDescription>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
</Item>
|
</Item>
|
||||||
|
|||||||
Reference in New Issue
Block a user