mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-05 06:16:05 +02:00
add img fallback
This commit is contained in:
@@ -291,7 +291,12 @@
|
||||
@click="showUserDialog(favorite.id)">
|
||||
<div class="favorites-search-card__content">
|
||||
<div class="favorites-search-card__avatar">
|
||||
<img :src="userImage(favorite, true)" loading="lazy" />
|
||||
<Avatar class="size-full">
|
||||
<AvatarImage :src="userImage(favorite, true)" class="object-cover" loading="lazy" />
|
||||
<AvatarFallback>
|
||||
<User class="size-5 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div class="favorites-search-card__detail">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -325,8 +330,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Ellipsis, MoreHorizontal, Plus, RefreshCcw, RefreshCw } from 'lucide-vue-next';
|
||||
import { Ellipsis, MoreHorizontal, Plus, RefreshCcw, RefreshCw, User } from 'lucide-vue-next';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DataTableEmpty } from '@/components/ui/data-table';
|
||||
import { InputGroupField } from '@/components/ui/input-group';
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
decoding="async"
|
||||
fetchpriority="low"
|
||||
class="rounded-sm object-cover" />
|
||||
<AvatarFallback class="rounded-sm">{{ avatarFallback }}</AvatarFallback>
|
||||
<AvatarFallback class="rounded-sm">
|
||||
<Image class="size-4 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent class="min-w-0">
|
||||
@@ -98,7 +100,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { AlertTriangle, Lock, MoreHorizontal, Trash2 } from 'lucide-vue-next';
|
||||
import { AlertTriangle, Image, Lock, MoreHorizontal, Trash2 } from 'lucide-vue-next';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -146,10 +148,6 @@
|
||||
|
||||
const localFavFakeRef = computed(() => (props.isLocalFavorite ? props.favorite : props.favorite?.ref));
|
||||
|
||||
const displayName = computed(() => localFavFakeRef.value?.name || props.favorite?.name || props.favorite?.id);
|
||||
|
||||
const avatarFallback = computed(() => displayName.value?.charAt(0)?.toUpperCase() || '?');
|
||||
|
||||
const showUnavailable = computed(() => !props.isLocalFavorite && props.favorite?.deleted);
|
||||
|
||||
const isPrivateAvatar = computed(() => !props.isLocalFavorite && props.favorite?.ref?.releaseStatus === 'private');
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
decoding="async"
|
||||
fetchpriority="low"
|
||||
class="rounded-sm object-cover" />
|
||||
<AvatarFallback class="rounded-sm">{{ avatarFallback }}</AvatarFallback>
|
||||
<AvatarFallback class="rounded-sm">
|
||||
<Image class="size-4 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent class="min-w-0">
|
||||
@@ -65,7 +67,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MoreHorizontal } from 'lucide-vue-next';
|
||||
import { Image, MoreHorizontal } from 'lucide-vue-next';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -102,8 +104,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
const avatarFallback = computed(() => props.favorite.name?.charAt(0)?.toUpperCase() || '?');
|
||||
|
||||
const itemStyle = computed(() => ({
|
||||
padding: 'var(--favorites-card-padding-y, 8px) var(--favorites-card-padding-x, 10px)',
|
||||
gap: 'var(--favorites-card-content-gap, 10px)',
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
<ItemMedia variant="image">
|
||||
<Avatar>
|
||||
<AvatarImage :src="userImage(favorite.ref, true)" loading="lazy" />
|
||||
<AvatarFallback>{{ avatarFallback }}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
<User class="size-4 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent class="min-w-0">
|
||||
@@ -114,7 +116,9 @@
|
||||
<Item variant="outline" class="favorites-item hover:bg-muted x-hover-list" :style="itemStyle">
|
||||
<ItemMedia variant="image">
|
||||
<Avatar>
|
||||
<AvatarFallback>{{ avatarFallback }}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
<User class="size-4 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent class="min-w-0">
|
||||
@@ -135,7 +139,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MoreHorizontal, Trash2 } from 'lucide-vue-next';
|
||||
import { MoreHorizontal, Trash2, User } from 'lucide-vue-next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
@@ -195,8 +199,6 @@
|
||||
|
||||
const displayName = computed(() => props.favorite?.ref?.displayName || props.favorite?.name || props.favorite?.id);
|
||||
|
||||
const avatarFallback = computed(() => displayName.value?.charAt(0)?.toUpperCase() || '?');
|
||||
|
||||
const displayNameStyle = computed(() => {
|
||||
if (props.favorite?.ref?.$userColour) {
|
||||
return {
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
decoding="async"
|
||||
fetchpriority="low"
|
||||
class="rounded-sm object-cover" />
|
||||
<AvatarFallback class="rounded-sm">{{ avatarFallback }}</AvatarFallback>
|
||||
<AvatarFallback class="rounded-sm">
|
||||
<Image class="size-4 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent class="min-w-0">
|
||||
@@ -77,7 +79,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { AlertTriangle, Lock, MoreHorizontal } from 'lucide-vue-next';
|
||||
import { AlertTriangle, Image, Lock, MoreHorizontal } from 'lucide-vue-next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
@@ -129,8 +131,6 @@
|
||||
|
||||
const displayName = computed(() => localFavRef.value?.name || props.favorite?.name || props.favorite?.id);
|
||||
|
||||
const avatarFallback = computed(() => displayName.value?.charAt(0)?.toUpperCase() || '?');
|
||||
|
||||
const showUnavailable = computed(() => !props.isLocalFavorite && props.favorite?.deleted);
|
||||
|
||||
const isPrivateWorld = computed(() => localFavRef.value?.releaseStatus === 'private');
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
<div>
|
||||
<Avatar :style="{ width: `${avatarSize}px`, height: `${avatarSize}px` }">
|
||||
<AvatarImage :src="userImage(friend.ref, true)" />
|
||||
<AvatarFallback>{{ avatarFallback }}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
<User class="text-muted-foreground" :size="Math.max(16, 20 * cardScale)" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<span
|
||||
@@ -79,7 +81,7 @@
|
||||
} from '@/components/ui/context-menu';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Pencil } from 'lucide-vue-next';
|
||||
import { Pencil, User } from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
@@ -132,7 +134,6 @@
|
||||
paddingBottom: `${36 * props.cardScale * props.cardSpacing}px !important`
|
||||
}));
|
||||
|
||||
const avatarFallback = computed(() => props.friend?.name?.charAt(0) ?? '?');
|
||||
|
||||
const statusDotClass = computed(() => {
|
||||
const status = userStatusClass(props.friend.ref, props.friend.pendingOffline);
|
||||
|
||||
@@ -92,13 +92,26 @@ export function getColumns({
|
||||
cell: ({ row }) => {
|
||||
const ref = row.original;
|
||||
return (
|
||||
<img
|
||||
src={ref.thumbnailImageUrl}
|
||||
class="avatar-table-thumbnail cursor-pointer rounded-sm object-cover"
|
||||
style="width: 34px; height: 22px;"
|
||||
loading="lazy"
|
||||
onClick={() => onShowAvatarDialog(ref.id)}
|
||||
/>
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
src={ref.thumbnailImageUrl}
|
||||
class="avatar-table-thumbnail cursor-pointer rounded-sm object-cover"
|
||||
style="width: 34px; height: 22px;"
|
||||
loading="lazy"
|
||||
onClick={() => onShowAvatarDialog(ref.id)}
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
e.target.nextElementSibling.style.display = '';
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="rounded-sm bg-muted flex items-center justify-center cursor-pointer"
|
||||
style="width: 34px; height: 22px; display: none"
|
||||
onClick={() => onShowAvatarDialog(ref.id)}
|
||||
>
|
||||
<Image class="h-3 w-3 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
:class="isActive ? 'border-2 border-primary' : 'border border-border/50'">
|
||||
<div class="w-full aspect-5/2 overflow-hidden bg-muted relative">
|
||||
<img
|
||||
v-if="avatar.thumbnailImageUrl"
|
||||
v-if="avatar.thumbnailImageUrl && !imageLoadError"
|
||||
:src="avatar.thumbnailImageUrl"
|
||||
:alt="avatar.name"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
fetchpriority="low"
|
||||
class="w-full h-full object-cover block" />
|
||||
class="w-full h-full object-cover block"
|
||||
@error="imageLoadError = true" />
|
||||
<div v-else class="w-full h-full grid place-items-center">
|
||||
<ImageIcon class="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
@@ -252,6 +253,7 @@
|
||||
|
||||
const hoverOpen = ref(false);
|
||||
const contextMenuOpen = ref(false);
|
||||
const imageLoadError = ref(false);
|
||||
|
||||
const handleContextMenuOpen = (open) => {
|
||||
contextMenuOpen.value = open;
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Ban,
|
||||
BellOff,
|
||||
Check,
|
||||
ImageOff,
|
||||
Image,
|
||||
Link,
|
||||
MessageCircle,
|
||||
Reply,
|
||||
@@ -439,7 +439,7 @@ export const createColumns = ({
|
||||
class="object-cover"
|
||||
/>
|
||||
<AvatarFallback class="rounded">
|
||||
<ImageOff class="size-4 text-muted-foreground" />
|
||||
<Image class="size-4 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
@@ -456,7 +456,7 @@ export const createColumns = ({
|
||||
>
|
||||
<AvatarImage src={imgUrl} class="object-cover" />
|
||||
<AvatarFallback class="rounded">
|
||||
<ImageOff class="size-4 text-muted-foreground" />
|
||||
<Image class="size-4 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
|
||||
@@ -7,11 +7,19 @@
|
||||
style="display: flex; min-height: 120px"
|
||||
class="mb-7">
|
||||
<img
|
||||
v-if="!worldImageError"
|
||||
:src="currentInstanceWorld.ref.thumbnailImageUrl"
|
||||
class="cursor-pointer"
|
||||
style="flex: none; width: 160px; height: 120px; border-radius: var(--radius-md)"
|
||||
@click="showFullscreenImageDialog(currentInstanceWorld.ref.imageUrl)"
|
||||
@error="worldImageError = true"
|
||||
loading="lazy" />
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center justify-center bg-muted"
|
||||
style="flex: none; width: 160px; height: 120px; border-radius: var(--radius-md)">
|
||||
<Image class="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
<div class="ml-2" style="display: flex; flex-direction: column; min-width: 320px; width: 100%">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
@@ -174,7 +182,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, defineAsyncComponent, onActivated, onMounted, ref, watch } from 'vue';
|
||||
import { Apple, Home, Monitor, Smartphone } from 'lucide-vue-next';
|
||||
import { Apple, Home, Image, Monitor, Smartphone } from 'lucide-vue-next';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -207,6 +215,15 @@
|
||||
|
||||
const { lastLocation } = storeToRefs(useLocationStore());
|
||||
const { currentInstanceLocation, currentInstanceWorld, currentInstanceUsersData } = storeToRefs(useInstanceStore());
|
||||
|
||||
const worldImageError = ref(false);
|
||||
|
||||
watch(
|
||||
() => currentInstanceWorld.value?.ref?.id,
|
||||
() => {
|
||||
worldImageError.value = false;
|
||||
}
|
||||
);
|
||||
const { getCurrentInstanceUserList } = useInstanceStore();
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Apple,
|
||||
ArrowUpDown,
|
||||
IdCard,
|
||||
User,
|
||||
Monitor,
|
||||
Smartphone
|
||||
} from 'lucide-vue-next';
|
||||
@@ -84,7 +85,17 @@ export const createColumns = ({
|
||||
src={src}
|
||||
class="h-4 w-4 rounded-sm object-cover"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
e.target.nextElementSibling.style.display = '';
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="h-4 w-4 rounded-sm bg-muted flex items-center justify-center"
|
||||
style="display: none"
|
||||
>
|
||||
<User class="h-3 w-3 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
import Location from '@/components/Location.vue';
|
||||
import Timer from '@/components/Timer.vue';
|
||||
|
||||
import { useAppearanceSettingsStore, useFriendStore, useUserStore } from '../../../stores';
|
||||
import { useAppearanceSettingsStore, useFriendStore } from '../../../stores';
|
||||
import { useUserDisplay } from '../../../composables/useUserDisplay';
|
||||
|
||||
import '@/styles/status-icon.css';
|
||||
|
||||
@@ -36,10 +36,12 @@
|
||||
<div
|
||||
class="relative inline-block flex-none size-9 mr-2.5"
|
||||
:class="userStatusClass(currentUser)">
|
||||
<img
|
||||
class="size-full rounded-full object-cover"
|
||||
:src="userImage(currentUser)"
|
||||
loading="lazy" />
|
||||
<Avatar class="size-full rounded-full">
|
||||
<AvatarImage :src="userImage(currentUser)" class="object-cover" />
|
||||
<AvatarFallback>
|
||||
<User class="size-5 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden h-9 flex flex-col justify-between">
|
||||
<span
|
||||
@@ -176,7 +178,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { ChevronDown } from 'lucide-vue-next';
|
||||
import { ChevronDown, User } from 'lucide-vue-next';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -193,6 +195,7 @@
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger
|
||||
} from '../../../components/ui/context-menu';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '../../../components/ui/avatar';
|
||||
import {
|
||||
useAdvancedSettingsStore,
|
||||
useAppearanceSettingsStore,
|
||||
|
||||
@@ -37,10 +37,12 @@
|
||||
@click="showGroupDialog(item.row.ownerId)">
|
||||
<template v-if="item.row.isVisible">
|
||||
<div class="relative inline-block flex-none size-9 mr-2.5">
|
||||
<img
|
||||
class="size-full rounded-full object-cover"
|
||||
:src="getSmallGroupIconUrl(item.row.iconUrl)"
|
||||
loading="lazy" />
|
||||
<Avatar class="size-9">
|
||||
<AvatarImage :src="getSmallGroupIconUrl(item.row.iconUrl)" class="object-cover" />
|
||||
<AvatarFallback>
|
||||
<Users class="size-4 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">
|
||||
@@ -82,7 +84,8 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { ChevronDown } from 'lucide-vue-next';
|
||||
import { ChevronDown, Users } from 'lucide-vue-next';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -206,7 +206,8 @@ vi.mock('../FriendItem.vue', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('lucide-vue-next', () => ({
|
||||
ChevronDown: { template: '<span data-testid="chevron" />' }
|
||||
ChevronDown: { template: '<span data-testid="chevron" />' },
|
||||
User: { template: '<i />' }
|
||||
}));
|
||||
|
||||
import FriendsSidebar from '../FriendsSidebar.vue';
|
||||
|
||||
@@ -117,7 +117,8 @@ vi.mock('../../../../components/Location.vue', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('lucide-vue-next', () => ({
|
||||
ChevronDown: { template: '<i />' }
|
||||
ChevronDown: { template: '<i />' },
|
||||
Users: { template: '<i />' }
|
||||
}));
|
||||
|
||||
import GroupsSidebar from '../GroupsSidebar.vue';
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
:class="cardClass"
|
||||
@mouseenter="openEventPopover"
|
||||
@mouseleave="scheduleCloseEventPopover">
|
||||
<img :src="bannerUrl" @click="showFullscreenImageDialog(bannerUrl)" class="banner" />
|
||||
<img v-if="!bannerError" :src="bannerUrl" @click="showFullscreenImageDialog(bannerUrl)" @error="bannerError = true" class="banner" />
|
||||
<div v-else class="banner flex items-center justify-center bg-muted">
|
||||
<Image class="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
<div class="event-content">
|
||||
<div class="event-title">
|
||||
<div v-if="showGroupName" class="event-group-name" @click="onGroupClick">
|
||||
@@ -106,7 +109,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Calendar, Download, Share2, Star } from 'lucide-vue-next';
|
||||
import { Calendar, Download, Image, Share2, Star } from 'lucide-vue-next';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { computed, ref } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -162,6 +165,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
const bannerError = ref(false);
|
||||
|
||||
const groupName = computed(() => {
|
||||
if (!props.event) return '';
|
||||
return cachedGroups.get(props.event.ownerId)?.name || '';
|
||||
|
||||
Reference in New Issue
Block a user