mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-17 05:43:51 +02:00
feat: add breadcrumb components and main dialog layout functionality
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
210
src/components/dialogs/MainDialogContainer.vue
Normal file
210
src/components/dialogs/MainDialogContainer.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<script setup>
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbEllipsis,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from '@/components/ui/breadcrumb';
|
||||
import { useAvatarStore, useGroupStore, useUiStore, useUserStore, useWorldStore } from '@/stores';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import AvatarDialog from './AvatarDialog/AvatarDialog.vue';
|
||||
import GroupDialog from './GroupDialog/GroupDialog.vue';
|
||||
import UserDialog from './UserDialog/UserDialog.vue';
|
||||
import WorldDialog from './WorldDialog/WorldDialog.vue';
|
||||
|
||||
const avatarStore = useAvatarStore();
|
||||
const groupStore = useGroupStore();
|
||||
const uiStore = useUiStore();
|
||||
const userStore = useUserStore();
|
||||
const worldStore = useWorldStore();
|
||||
|
||||
const isOpen = computed({
|
||||
get: () =>
|
||||
userStore.userDialog.visible ||
|
||||
worldStore.worldDialog.visible ||
|
||||
avatarStore.avatarDialog.visible ||
|
||||
groupStore.groupDialog.visible,
|
||||
set: (value) => {
|
||||
if (!value) {
|
||||
userStore.userDialog.visible = false;
|
||||
worldStore.worldDialog.visible = false;
|
||||
avatarStore.avatarDialog.visible = false;
|
||||
groupStore.groupDialog.visible = false;
|
||||
uiStore.clearDialogCrumbs();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const dialogCrumbs = computed(() => uiStore.dialogCrumbs);
|
||||
const activeCrumb = computed(() => dialogCrumbs.value[dialogCrumbs.value.length - 1] || null);
|
||||
const activeType = computed(() => {
|
||||
if (activeCrumb.value?.type) {
|
||||
return activeCrumb.value.type;
|
||||
}
|
||||
if (userStore.userDialog.visible) {
|
||||
return 'user';
|
||||
}
|
||||
if (worldStore.worldDialog.visible) {
|
||||
return 'world';
|
||||
}
|
||||
if (avatarStore.avatarDialog.visible) {
|
||||
return 'avatar';
|
||||
}
|
||||
if (groupStore.groupDialog.visible) {
|
||||
return 'group';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const activeComponent = computed(() => {
|
||||
switch (activeType.value) {
|
||||
case 'user':
|
||||
return UserDialog;
|
||||
case 'world':
|
||||
return WorldDialog;
|
||||
case 'avatar':
|
||||
return AvatarDialog;
|
||||
case 'group':
|
||||
return GroupDialog;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
const dialogClass = computed(() => {
|
||||
switch (activeType.value) {
|
||||
case 'world':
|
||||
return 'x-dialog x-world-dialog translate-y-0 sm:max-w-235';
|
||||
case 'avatar':
|
||||
return 'x-dialog x-avatar-dialog sm:max-w-235 translate-y-0';
|
||||
case 'group':
|
||||
return 'x-dialog x-group-dialog group-body translate-y-0 sm:max-w-235';
|
||||
case 'user':
|
||||
default:
|
||||
return 'x-dialog x-user-dialog sm:max-w-235 translate-y-0';
|
||||
}
|
||||
});
|
||||
|
||||
const shouldShowBreadcrumbs = computed(() => dialogCrumbs.value.length > 1);
|
||||
const shouldCollapseBreadcrumbs = computed(() => dialogCrumbs.value.length > 4);
|
||||
const middleBreadcrumbs = computed(() => {
|
||||
if (!shouldCollapseBreadcrumbs.value) {
|
||||
return [];
|
||||
}
|
||||
return dialogCrumbs.value.slice(1, -2);
|
||||
});
|
||||
|
||||
const handleBreadcrumbClick = (index) => {
|
||||
const item = dialogCrumbs.value[index];
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
uiStore.jumpDialogCrumb(index);
|
||||
if (item.type === 'user') {
|
||||
userStore.showUserDialog(item.id, { skipBreadcrumb: true });
|
||||
return;
|
||||
}
|
||||
if (item.type === 'world') {
|
||||
worldStore.showWorldDialog(item.id, null, { skipBreadcrumb: true });
|
||||
return;
|
||||
}
|
||||
if (item.type === 'avatar') {
|
||||
avatarStore.showAvatarDialog(item.id, { skipBreadcrumb: true });
|
||||
return;
|
||||
}
|
||||
if (item.type === 'group') {
|
||||
groupStore.showGroupDialog(item.id, { skipBreadcrumb: true });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="isOpen">
|
||||
<DialogContent :class="dialogClass" style="top: 10vh" :show-close-button="false">
|
||||
<Breadcrumb v-if="shouldShowBreadcrumbs" class="mb-2">
|
||||
<BreadcrumbList>
|
||||
<template v-if="shouldCollapseBreadcrumbs">
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="max-w-40 truncate text-left"
|
||||
@click="handleBreadcrumbClick(0)">
|
||||
{{ dialogCrumbs[0]?.label || dialogCrumbs[0]?.id }}
|
||||
</button>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger class="flex items-center gap-1">
|
||||
<BreadcrumbEllipsis class="h-4 w-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
v-for="(crumb, index) in middleBreadcrumbs"
|
||||
:key="`${crumb.type}-${crumb.id}`"
|
||||
@click="handleBreadcrumbClick(index + 1)">
|
||||
{{ crumb.label || crumb.id }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="max-w-40 truncate text-left"
|
||||
@click="handleBreadcrumbClick(dialogCrumbs.length - 2)">
|
||||
{{
|
||||
dialogCrumbs[dialogCrumbs.length - 2]?.label ||
|
||||
dialogCrumbs[dialogCrumbs.length - 2]?.id
|
||||
}}
|
||||
</button>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage class="max-w-40 truncate">
|
||||
{{
|
||||
dialogCrumbs[dialogCrumbs.length - 1]?.label ||
|
||||
dialogCrumbs[dialogCrumbs.length - 1]?.id
|
||||
}}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="(crumb, index) in dialogCrumbs" :key="`${crumb.type}-${crumb.id}`">
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink v-if="index < dialogCrumbs.length - 1" as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="max-w-40 truncate text-left"
|
||||
@click="handleBreadcrumbClick(index)">
|
||||
{{ crumb.label || crumb.id }}
|
||||
</button>
|
||||
</BreadcrumbLink>
|
||||
<BreadcrumbPage v-else class="max-w-40 truncate">
|
||||
{{ crumb.label || crumb.id }}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator v-if="index < dialogCrumbs.length - 1" />
|
||||
</template>
|
||||
</template>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
<component :is="activeComponent" v-if="activeComponent" />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
11
src/components/ui/breadcrumb/Breadcrumb.vue
Normal file
11
src/components/ui/breadcrumb/Breadcrumb.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav aria-label="breadcrumb" data-slot="breadcrumb" :class="props.class">
|
||||
<slot />
|
||||
</nav>
|
||||
</template>
|
||||
21
src/components/ui/breadcrumb/BreadcrumbEllipsis.vue
Normal file
21
src/components/ui/breadcrumb/BreadcrumbEllipsis.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { MoreHorizontal } from 'lucide-vue-next';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
:class="cn('flex size-9 items-center justify-center', props.class)">
|
||||
<slot>
|
||||
<MoreHorizontal class="size-4" />
|
||||
</slot>
|
||||
<span class="sr-only">More</span>
|
||||
</span>
|
||||
</template>
|
||||
13
src/components/ui/breadcrumb/BreadcrumbItem.vue
Normal file
13
src/components/ui/breadcrumb/BreadcrumbItem.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li data-slot="breadcrumb-item" :class="cn('inline-flex items-center gap-1.5', props.class)">
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
20
src/components/ui/breadcrumb/BreadcrumbLink.vue
Normal file
20
src/components/ui/breadcrumb/BreadcrumbLink.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import { Primitive } from 'reka-ui';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps({
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false, default: 'a' },
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="breadcrumb-link"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn('hover:text-foreground transition-colors', props.class)">
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
17
src/components/ui/breadcrumb/BreadcrumbList.vue
Normal file
17
src/components/ui/breadcrumb/BreadcrumbList.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
:class="
|
||||
cn('text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5', props.class)
|
||||
">
|
||||
<slot />
|
||||
</ol>
|
||||
</template>
|
||||
18
src/components/ui/breadcrumb/BreadcrumbPage.vue
Normal file
18
src/components/ui/breadcrumb/BreadcrumbPage.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
:class="cn('text-foreground font-normal', props.class)">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
20
src/components/ui/breadcrumb/BreadcrumbSeparator.vue
Normal file
20
src/components/ui/breadcrumb/BreadcrumbSeparator.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import { ChevronRight } from 'lucide-vue-next';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
:class="cn('[&>svg]:size-3.5', props.class)">
|
||||
<slot>
|
||||
<ChevronRight />
|
||||
</slot>
|
||||
</li>
|
||||
</template>
|
||||
7
src/components/ui/breadcrumb/index.js
Normal file
7
src/components/ui/breadcrumb/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export { default as Breadcrumb } from './Breadcrumb.vue';
|
||||
export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue';
|
||||
export { default as BreadcrumbItem } from './BreadcrumbItem.vue';
|
||||
export { default as BreadcrumbLink } from './BreadcrumbLink.vue';
|
||||
export { default as BreadcrumbList } from './BreadcrumbList.vue';
|
||||
export { default as BreadcrumbPage } from './BreadcrumbPage.vue';
|
||||
export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue';
|
||||
@@ -5,12 +5,13 @@ import { useAppearanceSettingsStore } from '../stores';
|
||||
|
||||
import configRepository from '../service/config';
|
||||
|
||||
export function useAuthenticatedLayoutResizable() {
|
||||
export function useMainLayoutResizable() {
|
||||
const asideMaxPx = 500;
|
||||
|
||||
const appearanceStore = useAppearanceSettingsStore();
|
||||
const { setAsideWidth } = appearanceStore;
|
||||
const { asideWidth, isSideBarTabShow, isNavCollapsed } = storeToRefs(appearanceStore);
|
||||
const { asideWidth, isSideBarTabShow, isNavCollapsed } =
|
||||
storeToRefs(appearanceStore);
|
||||
|
||||
const fallbackWidth =
|
||||
typeof window !== 'undefined' && window.innerWidth
|
||||
@@ -63,7 +64,9 @@ export function useAuthenticatedLayoutResizable() {
|
||||
const percentToPx = (percent, groupWidth) => (percent / 100) * groupWidth;
|
||||
|
||||
const isAsideCollapsed = (layout) =>
|
||||
Array.isArray(layout) && layout.length >= 2 && layout[layout.length - 1] <= 1;
|
||||
Array.isArray(layout) &&
|
||||
layout.length >= 2 &&
|
||||
layout[layout.length - 1] <= 1;
|
||||
|
||||
const asideDefaultSize = computed(() =>
|
||||
pxToPercent(asideWidth.value, undefined, 0)
|
||||
@@ -170,6 +173,7 @@ export function useAuthenticatedLayoutResizable() {
|
||||
asidePanelRef,
|
||||
asideDefaultSize,
|
||||
asideMaxSize,
|
||||
asideMaxPx,
|
||||
mainDefaultSize,
|
||||
handleLayout,
|
||||
setIsDragging,
|
||||
@@ -2,7 +2,7 @@ import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
|
||||
import { watchState } from './../service/watchState';
|
||||
|
||||
import AuthenticatedLayout from '../views/Layout/AuthenticatedLayout.vue';
|
||||
import MainLayout from '../views/Layout/MainLayout.vue';
|
||||
import Charts from './../views/Charts/Charts.vue';
|
||||
import FavoritesAvatar from './../views/Favorites/FavoritesAvatar.vue';
|
||||
import FavoritesFriend from './../views/Favorites/FavoritesFriend.vue';
|
||||
@@ -31,7 +31,7 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: AuthenticatedLayout,
|
||||
component: MainLayout,
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', redirect: { name: 'feed' } },
|
||||
|
||||
@@ -18,9 +18,12 @@ import { database } from '../service/database';
|
||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||
import { useAvatarProviderStore } from './avatarProvider';
|
||||
import { useFavoriteStore } from './favorite';
|
||||
import { useGroupStore } from './group';
|
||||
import { useModalStore } from './modal';
|
||||
import { useUiStore } from './ui';
|
||||
import { useUserStore } from './user';
|
||||
import { useVRCXUpdaterStore } from './vrcxUpdater';
|
||||
import { useWorldStore } from './world';
|
||||
import { watchState } from '../service/watchState';
|
||||
|
||||
import webApiService from '../service/webapi';
|
||||
@@ -31,7 +34,10 @@ export const useAvatarStore = defineStore('Avatar', () => {
|
||||
const vrcxUpdaterStore = useVRCXUpdaterStore();
|
||||
const advancedSettingsStore = useAdvancedSettingsStore();
|
||||
const userStore = useUserStore();
|
||||
const worldStore = useWorldStore();
|
||||
const groupStore = useGroupStore();
|
||||
const modalStore = useModalStore();
|
||||
const uiStore = useUiStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
let cachedAvatarModerations = new Map();
|
||||
@@ -172,8 +178,22 @@ export const useAvatarStore = defineStore('Avatar', () => {
|
||||
* @param {string} avatarId
|
||||
* @returns
|
||||
*/
|
||||
function showAvatarDialog(avatarId) {
|
||||
function showAvatarDialog(avatarId, options = {}) {
|
||||
const D = avatarDialog.value;
|
||||
if (
|
||||
!avatarDialog.value.visible &&
|
||||
!userStore.userDialog.visible &&
|
||||
!worldStore.worldDialog.visible &&
|
||||
!groupStore.groupDialog.visible
|
||||
) {
|
||||
uiStore.clearDialogCrumbs();
|
||||
}
|
||||
if (!options.skipBreadcrumb) {
|
||||
uiStore.pushDialogCrumb('avatar', avatarId);
|
||||
}
|
||||
userStore.userDialog.visible = false;
|
||||
worldStore.worldDialog.visible = false;
|
||||
groupStore.groupDialog.visible = false;
|
||||
D.visible = true;
|
||||
D.loading = true;
|
||||
D.id = avatarId;
|
||||
@@ -201,6 +221,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
|
||||
const ref2 = cachedAvatars.get(avatarId);
|
||||
if (typeof ref2 !== 'undefined') {
|
||||
D.ref = ref2;
|
||||
uiStore.setDialogCrumbLabel('avatar', D.id, D.ref?.name || D.id);
|
||||
updateVRChatAvatarCache();
|
||||
if (
|
||||
ref2.releaseStatus !== 'public' &&
|
||||
@@ -215,6 +236,11 @@ export const useAvatarStore = defineStore('Avatar', () => {
|
||||
.then((args) => {
|
||||
const ref = applyAvatar(args.json);
|
||||
D.ref = ref;
|
||||
uiStore.setDialogCrumbLabel(
|
||||
'avatar',
|
||||
D.id,
|
||||
D.ref?.name || D.id
|
||||
);
|
||||
getAvatarGallery(avatarId);
|
||||
updateVRChatAvatarCache();
|
||||
if (/quest/.test(ref.tags)) {
|
||||
|
||||
@@ -16,11 +16,14 @@ import {
|
||||
} from '../shared/utils';
|
||||
import { database } from '../service/database.js';
|
||||
import { groupDialogFilterOptions } from '../shared/constants/';
|
||||
import { useAvatarStore } from './avatar';
|
||||
import { useGameStore } from './game';
|
||||
import { useInstanceStore } from './instance';
|
||||
import { useModalStore } from './modal';
|
||||
import { useNotificationStore } from './notification';
|
||||
import { useUiStore } from './ui';
|
||||
import { useUserStore } from './user';
|
||||
import { useWorldStore } from './world';
|
||||
import { watchState } from '../service/watchState';
|
||||
|
||||
import configRepository from '../service/config';
|
||||
@@ -31,8 +34,11 @@ export const useGroupStore = defineStore('Group', () => {
|
||||
const instanceStore = useInstanceStore();
|
||||
const gameStore = useGameStore();
|
||||
const userStore = useUserStore();
|
||||
const worldStore = useWorldStore();
|
||||
const avatarStore = useAvatarStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const modalStore = useModalStore();
|
||||
const uiStore = useUiStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
let cachedGroups = new Map();
|
||||
@@ -124,10 +130,24 @@ export const useGroupStore = defineStore('Group', () => {
|
||||
{ flush: 'sync' }
|
||||
);
|
||||
|
||||
function showGroupDialog(groupId) {
|
||||
function showGroupDialog(groupId, options = {}) {
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!groupDialog.value.visible &&
|
||||
!userStore.userDialog.visible &&
|
||||
!worldStore.worldDialog.visible &&
|
||||
!avatarStore.avatarDialog.visible
|
||||
) {
|
||||
uiStore.clearDialogCrumbs();
|
||||
}
|
||||
if (!options.skipBreadcrumb) {
|
||||
uiStore.pushDialogCrumb('group', groupId);
|
||||
}
|
||||
userStore.userDialog.visible = false;
|
||||
worldStore.worldDialog.visible = false;
|
||||
avatarStore.avatarDialog.visible = false;
|
||||
const D = groupDialog.value;
|
||||
D.visible = true;
|
||||
D.loading = true;
|
||||
@@ -161,6 +181,11 @@ export const useGroupStore = defineStore('Group', () => {
|
||||
if (groupId === args.ref.id) {
|
||||
D.loading = false;
|
||||
D.ref = args.ref;
|
||||
uiStore.setDialogCrumbLabel(
|
||||
'group',
|
||||
D.id,
|
||||
D.ref?.name || D.id
|
||||
);
|
||||
D.inGroup = args.ref.membershipStatus === 'member';
|
||||
D.ownerDisplayName = args.ref.ownerId;
|
||||
userRequest
|
||||
|
||||
@@ -27,6 +27,7 @@ export const useUiStore = defineStore('Ui', () => {
|
||||
const notifiedMenus = ref([]);
|
||||
const shiftHeld = ref(false);
|
||||
const trayIconNotify = ref(false);
|
||||
const dialogCrumbs = ref([]);
|
||||
|
||||
watch(ctrlR, (isPressed) => {
|
||||
if (isPressed) {
|
||||
@@ -58,6 +59,54 @@ export const useUiStore = defineStore('Ui', () => {
|
||||
}
|
||||
});
|
||||
|
||||
function pushDialogCrumb(type, id, label = '') {
|
||||
if (!type || !id) {
|
||||
return;
|
||||
}
|
||||
const items = dialogCrumbs.value;
|
||||
const last = items[items.length - 1];
|
||||
if (last && last.type === type && last.id === id) {
|
||||
if (label && last.label !== label) {
|
||||
last.label = label;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const existingIndex = items.findIndex(
|
||||
(item) => item.type === type && item.id === id
|
||||
);
|
||||
if (existingIndex !== -1) {
|
||||
items.splice(existingIndex + 1);
|
||||
if (label) {
|
||||
items[existingIndex].label = label;
|
||||
}
|
||||
return;
|
||||
}
|
||||
items.push({ type, id, label: label || id });
|
||||
}
|
||||
|
||||
function setDialogCrumbLabel(type, id, label) {
|
||||
if (!type || !id || !label) {
|
||||
return;
|
||||
}
|
||||
const item = dialogCrumbs.value.find(
|
||||
(entry) => entry.type === type && entry.id === id
|
||||
);
|
||||
if (item) {
|
||||
item.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
function jumpDialogCrumb(index) {
|
||||
if (index < 0 || index >= dialogCrumbs.value.length) {
|
||||
return;
|
||||
}
|
||||
dialogCrumbs.value.splice(index + 1);
|
||||
}
|
||||
|
||||
function clearDialogCrumbs() {
|
||||
dialogCrumbs.value = [];
|
||||
}
|
||||
|
||||
// Make sure file drops outside of the screenshot manager don't navigate to the file path dropped.
|
||||
// This issue persists on prompts created with prompt(), unfortunately. Not sure how to fix that.
|
||||
document.body.addEventListener('drop', function (e) {
|
||||
@@ -133,10 +182,15 @@ export const useUiStore = defineStore('Ui', () => {
|
||||
return {
|
||||
notifiedMenus,
|
||||
shiftHeld,
|
||||
dialogCrumbs,
|
||||
|
||||
notifyMenu,
|
||||
removeNotify,
|
||||
showConsole,
|
||||
updateTrayIconNotify
|
||||
updateTrayIconNotify,
|
||||
pushDialogCrumb,
|
||||
setDialogCrumbLabel,
|
||||
jumpDialogCrumb,
|
||||
clearDialogCrumbs
|
||||
};
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ import { useNotificationStore } from './notification';
|
||||
import { usePhotonStore } from './photon';
|
||||
import { useSearchStore } from './search';
|
||||
import { useSharedFeedStore } from './sharedFeed';
|
||||
import { useUiStore } from './ui';
|
||||
import { useWorldStore } from './world';
|
||||
import { watchState } from '../service/watchState';
|
||||
|
||||
@@ -68,6 +69,7 @@ export const useUserStore = defineStore('User', () => {
|
||||
const groupStore = useGroupStore();
|
||||
const feedStore = useFeedStore();
|
||||
const worldStore = useWorldStore();
|
||||
const uiStore = useUiStore();
|
||||
const moderationStore = useModerationStore();
|
||||
const photonStore = usePhotonStore();
|
||||
const sharedFeedStore = useSharedFeedStore();
|
||||
@@ -310,6 +312,7 @@ export const useUserStore = defineStore('User', () => {
|
||||
customUserTags.clear();
|
||||
state.notes.clear();
|
||||
subsetOfLanguages.value = [];
|
||||
uiStore.clearDialogCrumbs();
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
@@ -755,7 +758,7 @@ export const useUserStore = defineStore('User', () => {
|
||||
*
|
||||
* @param {string} userId
|
||||
*/
|
||||
function showUserDialog(userId) {
|
||||
function showUserDialog(userId, options = {}) {
|
||||
if (
|
||||
!userId ||
|
||||
typeof userId !== 'string' ||
|
||||
@@ -763,6 +766,20 @@ export const useUserStore = defineStore('User', () => {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!userDialog.value.visible &&
|
||||
!worldStore.worldDialog.visible &&
|
||||
!avatarStore.avatarDialog.visible &&
|
||||
!groupStore.groupDialog.visible
|
||||
) {
|
||||
uiStore.clearDialogCrumbs();
|
||||
}
|
||||
if (!options.skipBreadcrumb) {
|
||||
uiStore.pushDialogCrumb('user', userId);
|
||||
}
|
||||
worldStore.worldDialog.visible = false;
|
||||
avatarStore.avatarDialog.visible = false;
|
||||
groupStore.groupDialog.visible = false;
|
||||
const D = userDialog.value;
|
||||
D.id = userId;
|
||||
D.treeData = {};
|
||||
@@ -846,6 +863,11 @@ export const useUserStore = defineStore('User', () => {
|
||||
if (args.ref.id === D.id) {
|
||||
requestAnimationFrame(() => {
|
||||
D.ref = args.ref;
|
||||
uiStore.setDialogCrumbLabel(
|
||||
'user',
|
||||
D.id,
|
||||
D.ref?.displayName || D.id
|
||||
);
|
||||
D.friend = friendStore.friends.get(D.id);
|
||||
D.isFriend = Boolean(D.friend);
|
||||
D.note = String(D.ref.note || '');
|
||||
|
||||
@@ -14,9 +14,12 @@ import {
|
||||
} from '../shared/utils';
|
||||
import { instanceRequest, miscRequest, worldRequest } from '../api';
|
||||
import { database } from '../service/database';
|
||||
import { useAvatarStore } from './avatar';
|
||||
import { useFavoriteStore } from './favorite';
|
||||
import { useGroupStore } from './group';
|
||||
import { useInstanceStore } from './instance';
|
||||
import { useLocationStore } from './location';
|
||||
import { useUiStore } from './ui';
|
||||
import { useUserStore } from './user';
|
||||
import { watchState } from '../service/watchState';
|
||||
|
||||
@@ -25,6 +28,9 @@ export const useWorldStore = defineStore('World', () => {
|
||||
const favoriteStore = useFavoriteStore();
|
||||
const instanceStore = useInstanceStore();
|
||||
const userStore = useUserStore();
|
||||
const avatarStore = useAvatarStore();
|
||||
const groupStore = useGroupStore();
|
||||
const uiStore = useUiStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const worldDialog = reactive({
|
||||
@@ -71,12 +77,26 @@ export const useWorldStore = defineStore('World', () => {
|
||||
* @param {string} tag
|
||||
* @param {string} shortName
|
||||
*/
|
||||
function showWorldDialog(tag, shortName = null) {
|
||||
function showWorldDialog(tag, shortName = null, options = {}) {
|
||||
const D = worldDialog;
|
||||
const L = parseLocation(tag);
|
||||
if (L.worldId === '') {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!worldDialog.visible &&
|
||||
!userStore.userDialog.visible &&
|
||||
!avatarStore.avatarDialog.visible &&
|
||||
!groupStore.groupDialog.visible
|
||||
) {
|
||||
uiStore.clearDialogCrumbs();
|
||||
}
|
||||
if (!options.skipBreadcrumb) {
|
||||
uiStore.pushDialogCrumb('world', L.worldId);
|
||||
}
|
||||
userStore.userDialog.visible = false;
|
||||
avatarStore.avatarDialog.visible = false;
|
||||
groupStore.groupDialog.visible = false;
|
||||
L.shortName = shortName;
|
||||
D.id = L.worldId;
|
||||
D.$location = L;
|
||||
@@ -141,6 +161,11 @@ export const useWorldStore = defineStore('World', () => {
|
||||
if (D.id === args.ref.id) {
|
||||
D.loading = false;
|
||||
D.ref = args.ref;
|
||||
uiStore.setDialogCrumbLabel(
|
||||
'world',
|
||||
D.id,
|
||||
D.ref?.name || D.id
|
||||
);
|
||||
D.isFavorite = favoriteStore.getCachedFavoritesByObjectId(
|
||||
D.id
|
||||
);
|
||||
|
||||
@@ -18,10 +18,7 @@
|
||||
<ResizablePanelGroup
|
||||
ref="panelGroupRef"
|
||||
direction="horizontal"
|
||||
:class="[
|
||||
'group/main-layout flex-1 h-full min-w-0',
|
||||
{ 'aside-collapsed': isAsideCollapsedStatic }
|
||||
]"
|
||||
:class="['group/main-layout flex-1 h-full min-w-0', { 'aside-collapsed': isAsideCollapsedStatic }]"
|
||||
@layout="handleLayout">
|
||||
<template #default="{ layout }">
|
||||
<ResizablePanel :default-size="mainDefaultSize" :order="1">
|
||||
@@ -46,7 +43,8 @@
|
||||
:max-size="asideMaxSize"
|
||||
:collapsed-size="0"
|
||||
collapsible
|
||||
:order="2">
|
||||
:order="2"
|
||||
:style="{ maxWidth: `${asideMaxPx}px` }">
|
||||
<Sidebar></Sidebar>
|
||||
</ResizablePanel>
|
||||
</template>
|
||||
@@ -56,13 +54,7 @@
|
||||
</SidebarProvider>
|
||||
|
||||
<!-- ## Dialogs ## -->
|
||||
<UserDialog></UserDialog>
|
||||
|
||||
<WorldDialog></WorldDialog>
|
||||
|
||||
<AvatarDialog></AvatarDialog>
|
||||
|
||||
<GroupDialog></GroupDialog>
|
||||
<MainDialogContainer></MainDialogContainer>
|
||||
|
||||
<GroupMemberModerationDialog></GroupMemberModerationDialog>
|
||||
|
||||
@@ -102,16 +94,15 @@
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../components/ui/resizable';
|
||||
import { SidebarInset, SidebarProvider } from '../../components/ui/sidebar';
|
||||
import { useAppearanceSettingsStore } from '../../stores';
|
||||
import { useAuthenticatedLayoutResizable } from '../../composables/useAuthenticatedLayoutResizable';
|
||||
import { useMainLayoutResizable } from '../../composables/useMainLayoutResizable';
|
||||
import { watchState } from '../../service/watchState';
|
||||
|
||||
import AvatarDialog from '../../components/dialogs/AvatarDialog/AvatarDialog.vue';
|
||||
import AvatarImportDialog from '../Favorites/dialogs/AvatarImportDialog.vue';
|
||||
import ChangelogDialog from '../Settings/dialogs/ChangelogDialog.vue';
|
||||
import ChooseFavoriteGroupDialog from '../../components/dialogs/ChooseFavoriteGroupDialog.vue';
|
||||
import MainDialogContainer from '../../components/dialogs/MainDialogContainer.vue';
|
||||
import FriendImportDialog from '../Favorites/dialogs/FriendImportDialog.vue';
|
||||
import FullscreenImagePreview from '../../components/FullscreenImagePreview.vue';
|
||||
import GroupDialog from '../../components/dialogs/GroupDialog/GroupDialog.vue';
|
||||
import GroupMemberModerationDialog from '../../components/dialogs/GroupDialog/GroupMemberModerationDialog.vue';
|
||||
import InviteGroupDialog from '../../components/dialogs/InviteGroupDialog.vue';
|
||||
import LaunchDialog from '../../components/dialogs/LaunchDialog.vue';
|
||||
@@ -121,9 +112,7 @@
|
||||
import PrimaryPasswordDialog from '../Settings/dialogs/PrimaryPasswordDialog.vue';
|
||||
import SendBoopDialog from '../../components/dialogs/SendBoopDialog.vue';
|
||||
import Sidebar from '../Sidebar/Sidebar.vue';
|
||||
import UserDialog from '../../components/dialogs/UserDialog/UserDialog.vue';
|
||||
import VRChatConfigDialog from '../Settings/dialogs/VRChatConfigDialog.vue';
|
||||
import WorldDialog from '../../components/dialogs/WorldDialog/WorldDialog.vue';
|
||||
import WorldImportDialog from '../Favorites/dialogs/WorldImportDialog.vue';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -180,16 +169,15 @@
|
||||
const {
|
||||
asideDefaultSize,
|
||||
asideMaxSize,
|
||||
asideMaxPx,
|
||||
mainDefaultSize,
|
||||
handleLayout,
|
||||
setIsDragging,
|
||||
isAsideCollapsed,
|
||||
isSideBarTabShow
|
||||
} = useAuthenticatedLayoutResizable();
|
||||
} = useMainLayoutResizable();
|
||||
|
||||
const isAsideCollapsedStatic = computed(
|
||||
() => !isSideBarTabShow.value || asideWidth.value === 0
|
||||
);
|
||||
const isAsideCollapsedStatic = computed(() => !isSideBarTabShow.value || asideWidth.value === 0);
|
||||
|
||||
watch(
|
||||
() => watchState.isLoggedIn,
|
||||
Reference in New Issue
Block a user