mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
refactor: separate out instance card content
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { PageRoundIcon } from '@modrinth/assets'
|
||||
import { Avatar, TagItem } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed } from 'vue'
|
||||
import { TagItem } from '@modrinth/ui'
|
||||
|
||||
import InstanceFileIcon from '@/assets/icons/instance-file.svg'
|
||||
import InstanceCardContent from '@/components/ui/library/instance-group/instance-card-content.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
instance: GameInstance
|
||||
count?: number
|
||||
@@ -16,17 +13,6 @@ const props = withDefaults(
|
||||
count: 1,
|
||||
},
|
||||
)
|
||||
|
||||
const instanceType = computed(() => {
|
||||
if (
|
||||
props.instance.link?.type === 'server_project' ||
|
||||
props.instance.link?.type === 'server_project_modpack'
|
||||
) {
|
||||
return 'SRV'
|
||||
}
|
||||
|
||||
return props.instance.link?.type === 'modrinth_modpack' ? 'MPK' : 'CST'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -42,41 +28,13 @@ const instanceType = computed(() => {
|
||||
<div
|
||||
class="relative flex min-h-[76px] w-full items-center justify-center gap-2 overflow-clip rounded-[20px] border border-solid border-surface-4 bg-surface-3 p-4 text-left opacity-90 shadow-lg"
|
||||
>
|
||||
<Avatar
|
||||
v-if="instance.icon_path"
|
||||
class="pointer-events-none !border-none !bg-transparent !rounded-[26px] !rounded-br-[42px] !absolute -top-[26px] right-[20px] opacity-50 [mask-image:linear-gradient(135deg,transparent_16%,black_100%)]"
|
||||
size="84px"
|
||||
:src="convertFileSrc(instance.icon_path)"
|
||||
:tint-by="instance.id"
|
||||
alt=""
|
||||
no-shadow
|
||||
/>
|
||||
<PageRoundIcon
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute -top-[52px] right-[0px] size-[124px] opacity-10 [mask-image:linear-gradient(135deg,transparent_16%,black_100%)]"
|
||||
/>
|
||||
<InstanceCardContent :instance="instance" />
|
||||
<TagItem
|
||||
v-if="count > 1"
|
||||
class="!absolute right-3 top-3 z-[2] border-surface-5 bg-surface-2 font-semibold tabular-nums text-contrast"
|
||||
>
|
||||
{{ count }}
|
||||
</TagItem>
|
||||
<div class="relative z-[1] flex min-w-0 flex-1 items-center gap-2 pr-20">
|
||||
<div
|
||||
class="flex size-10 shrink-0 flex-col items-center gap-px overflow-clip rounded-[14px] px-[3px] py-0.5 text-primary"
|
||||
>
|
||||
<InstanceFileIcon class="h-[21px] w-[31px] shrink-0 text-primary [&_path]:fill-current" />
|
||||
<span class="h-3.5 text-sm font-extrabold leading-[13px]">{{ instanceType }}</span>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-1 flex-col justify-center gap-1">
|
||||
<p class="m-0 truncate text-base font-semibold leading-5 text-contrast">
|
||||
{{ instance.name }}
|
||||
</p>
|
||||
<p class="m-0 truncate text-sm font-medium capitalize leading-[18px] text-primary">
|
||||
{{ instance.loader }} {{ instance.game_version }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import Instance from '@/components/ui/library/instance-group/instance.vue'
|
||||
import InstanceCard from '@/components/ui/library/instance-group/instance-card.vue'
|
||||
import type {
|
||||
InstanceCard,
|
||||
InstanceCard as InstanceCardExposed,
|
||||
InstanceGroup as InstanceGroupType,
|
||||
} from '@/components/ui/library/use-library'
|
||||
import { useLibrary } from '@/components/ui/library/use-library'
|
||||
@@ -50,7 +50,7 @@ const {
|
||||
getInstanceGroupDropState,
|
||||
} = useLibrary()
|
||||
|
||||
const instanceComponents = new Map<string, InstanceCard>()
|
||||
const instanceComponents = new Map<string, InstanceCardExposed>()
|
||||
const groupDropTarget = ref<HTMLElement>()
|
||||
const groupAccordion = ref<InstanceType<typeof Accordion>>()
|
||||
const groupOptions = ref<InstanceType<typeof ContextMenu>>()
|
||||
@@ -123,7 +123,7 @@ function openInstanceContextMenu(event: MouseEvent, instanceId: string, instance
|
||||
|
||||
function setInstanceComponent(instanceId: string, component: unknown) {
|
||||
if (component) {
|
||||
instanceComponents.set(instanceId, component as InstanceCard)
|
||||
instanceComponents.set(instanceId, component as InstanceCardExposed)
|
||||
} else {
|
||||
instanceComponents.delete(instanceId)
|
||||
}
|
||||
@@ -360,7 +360,7 @@ watch(
|
||||
class="grid min-h-[45px] mt-2.5 w-full grid-cols-[repeat(auto-fill,minmax(20rem,22rem))] gap-3 overflow-y-auto scroll-smooth"
|
||||
>
|
||||
<div v-for="instance in instanceGroup.instances" :key="instance.id" class="min-w-0 w-full">
|
||||
<Instance
|
||||
<InstanceCard
|
||||
:ref="(component: unknown) => setInstanceComponent(instance.id, component)"
|
||||
:instance="instance"
|
||||
:instance-group-id="instanceGroup.id"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { PageRoundIcon } from '@modrinth/assets'
|
||||
import { Avatar } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import InstanceFileIcon from '@/assets/icons/instance-file.svg'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
}>()
|
||||
|
||||
const instanceType = computed(() => {
|
||||
if (
|
||||
props.instance.link?.type === 'server_project' ||
|
||||
props.instance.link?.type === 'server_project_modpack'
|
||||
) {
|
||||
return 'SRV'
|
||||
}
|
||||
|
||||
return props.instance.link?.type === 'modrinth_modpack' ? 'MPK' : 'CST'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Avatar
|
||||
v-if="instance.icon_path"
|
||||
class="pointer-events-none !border-none !bg-transparent !rounded-[26px] !rounded-br-[42px] !absolute -top-[26px] right-[20px] opacity-50 [mask-image:linear-gradient(135deg,transparent_16%,black_100%)]"
|
||||
size="84px"
|
||||
:src="convertFileSrc(instance.icon_path)"
|
||||
:tint-by="instance.id"
|
||||
alt=""
|
||||
no-shadow
|
||||
/>
|
||||
<PageRoundIcon
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute -top-[52px] right-[0px] size-[124px] opacity-10 [mask-image:linear-gradient(135deg,transparent_16%,black_100%)]"
|
||||
/>
|
||||
<div class="relative z-[1] flex min-w-0 flex-1 items-center gap-2 pr-20">
|
||||
<slot name="leading" :instance-type="instanceType">
|
||||
<div
|
||||
class="flex size-10 shrink-0 flex-col items-center gap-px overflow-clip rounded-[14px] px-[3px] py-0.5 text-primary"
|
||||
>
|
||||
<InstanceFileIcon class="h-[21px] w-[31px] shrink-0 text-primary [&_path]:fill-current" />
|
||||
<span class="h-3.5 text-sm font-extrabold leading-[13px]">{{ instanceType }}</span>
|
||||
</div>
|
||||
</slot>
|
||||
<div class="flex min-w-0 flex-1 flex-col justify-center gap-1">
|
||||
<p class="m-0 truncate text-base font-semibold leading-5 text-contrast">
|
||||
{{ instance.name }}
|
||||
</p>
|
||||
<p class="m-0 truncate text-sm font-medium capitalize leading-[18px] text-primary">
|
||||
{{ instance.loader }} {{ instance.game_version }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+69
-102
@@ -1,20 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useDraggable } from '@dnd-kit/vue'
|
||||
import {
|
||||
CheckIcon,
|
||||
DownloadIcon,
|
||||
PageRoundIcon,
|
||||
PlayIcon,
|
||||
SpinnerIcon,
|
||||
StopCircleIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, injectNotificationManager } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { CheckIcon, DownloadIcon, PlayIcon, SpinnerIcon, StopCircleIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import InstanceFileIcon from '@/assets/icons/instance-file.svg'
|
||||
import InstanceCardContent from '@/components/ui/library/instance-group/instance-card-content.vue'
|
||||
import { getLibraryInstanceSelectionKey, useLibrary } from '@/components/ui/library/use-library'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
@@ -50,7 +43,7 @@ const emit = defineEmits<{
|
||||
(e: 'toggle-selection', shiftKey: boolean): void
|
||||
}>()
|
||||
|
||||
const instanceCard = ref<HTMLElement | null>(null)
|
||||
const instanceCardElement = ref<HTMLElement | null>(null)
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
const currentEvent = ref<ProcessEvent | null>(null)
|
||||
@@ -74,7 +67,7 @@ const holdingShift = computed(() => keys.shift.value)
|
||||
const isPartOfActiveDrag = computed(() => activeDraggedInstanceKeys.value.has(selectionKey.value))
|
||||
const { isDragging } = useDraggable({
|
||||
id: computed(() => `instance:${props.instanceGroupId}:${props.instance.id}`),
|
||||
element: instanceCard,
|
||||
element: instanceCardElement,
|
||||
disabled: computed(() => displayState.value.group !== 'Group'),
|
||||
data: computed(() => ({
|
||||
instanceId: props.instance.id,
|
||||
@@ -82,17 +75,6 @@ const { isDragging } = useDraggable({
|
||||
})),
|
||||
})
|
||||
|
||||
const instanceType = computed(() => {
|
||||
if (
|
||||
props.instance.link?.type === 'server_project' ||
|
||||
props.instance.link?.type === 'server_project_modpack'
|
||||
) {
|
||||
return 'SRV'
|
||||
}
|
||||
|
||||
return props.instance.link?.type === 'modrinth_modpack' ? 'MPK' : 'CST'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const seeInstance = async () => {
|
||||
@@ -220,10 +202,10 @@ onUnmounted(() => unlisten())
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="instanceCard"
|
||||
ref="instanceCardElement"
|
||||
class="group/card relative flex min-h-[76px] w-full cursor-pointer items-center justify-center gap-2 -outline-offset-2 overflow-clip focus-visible:!outline-2 rounded-[20px] border border-solid bg-surface-3 p-4 text-left transition-all hover:brightness-110 select-none"
|
||||
:class="{
|
||||
'[border-color:color-mix(in_srgb,var(--color-text-primary)_20%,transparent)] brightness-110':
|
||||
'[border-color:color-mix(in_srgb,var(--color-text-primary)_15%,transparent)] brightness-110':
|
||||
selected,
|
||||
'border-surface-4': !selected,
|
||||
'!scale-100': isDragging,
|
||||
@@ -246,19 +228,6 @@ onUnmounted(() => unlisten())
|
||||
@keydown="handleCardKeydown"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<Avatar
|
||||
v-if="instance.icon_path"
|
||||
class="pointer-events-none !border-none !bg-transparent !rounded-[26px] !rounded-br-[42px] !absolute -top-[26px] right-[20px] opacity-50 [mask-image:linear-gradient(135deg,transparent_16%,black_100%)]"
|
||||
size="84px"
|
||||
:src="convertFileSrc(instance.icon_path)"
|
||||
:tint-by="instance.id"
|
||||
alt=""
|
||||
no-shadow
|
||||
/>
|
||||
<PageRoundIcon
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute -top-[52px] right-[0px] size-[124px] opacity-10 [mask-image:linear-gradient(135deg,transparent_16%,black_100%)]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="selection-button group/selection absolute right-0 top-0 z-[2] flex size-[50px] h-full cursor-pointer items-start pt-4 justify-center border-0 bg-transparent p-0"
|
||||
@@ -280,73 +249,71 @@ onUnmounted(() => unlisten())
|
||||
<CheckIcon v-if="selected" class="relative size-4 invert [stroke-width:3] top-px" />
|
||||
</span>
|
||||
</button>
|
||||
<div class="relative z-[1] flex min-w-0 flex-1 items-center gap-2 pr-20">
|
||||
<div class="relative flex size-10 shrink-0 items-center justify-center">
|
||||
<div
|
||||
v-if="!playing && !modLoading && !installing"
|
||||
class="flex w-10 flex-col items-center gap-px overflow-clip rounded-[14px] px-[3px] py-0.5 text-primary transition-opacity"
|
||||
:class="{
|
||||
'group-hover/card:scale-75 group-hover/card:opacity-0':
|
||||
!instance.quarantined && !isLibraryInstanceSelectionActive,
|
||||
}"
|
||||
>
|
||||
<InstanceFileIcon class="h-[21px] w-[31px] shrink-0 text-primary [&_path]:fill-current" />
|
||||
<span class="h-3.5 text-sm font-extrabold leading-[13px]">{{ instanceType }}</span>
|
||||
</div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<ButtonStyled v-if="playing" color="red" circular>
|
||||
<button
|
||||
v-tooltip="'Stop'"
|
||||
class="card-shadow"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<SpinnerIcon
|
||||
v-else-if="modLoading || installing"
|
||||
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
||||
class="size-8 animate-spin"
|
||||
tabindex="-1"
|
||||
/>
|
||||
<ButtonStyled
|
||||
v-else-if="!isLibraryInstanceSelectionActive && !installed && !instance.quarantined"
|
||||
color="brand"
|
||||
circular
|
||||
<InstanceCardContent :instance="instance">
|
||||
<template #leading="{ instanceType }">
|
||||
<div class="relative flex size-10 shrink-0 items-center justify-center">
|
||||
<div
|
||||
v-if="!playing && !modLoading && !installing"
|
||||
class="flex w-10 flex-col items-center gap-px overflow-clip rounded-[14px] px-[3px] py-0.5 text-primary transition-opacity"
|
||||
:class="{
|
||||
'group-hover/card:scale-75 group-hover/card:opacity-0':
|
||||
!instance.quarantined && !isLibraryInstanceSelectionActive,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
v-tooltip="'Repair'"
|
||||
class="card-shadow origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
|
||||
@click="(e) => repair(e)"
|
||||
<InstanceFileIcon
|
||||
class="h-[21px] w-[31px] shrink-0 text-primary [&_path]:fill-current"
|
||||
/>
|
||||
<span class="h-3.5 text-sm font-extrabold leading-[13px]">
|
||||
{{ instanceType }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<ButtonStyled v-if="playing" color="red" circular>
|
||||
<button
|
||||
v-tooltip="'Stop'"
|
||||
class="card-shadow"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<SpinnerIcon
|
||||
v-else-if="modLoading || installing"
|
||||
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
||||
class="size-8 animate-spin"
|
||||
tabindex="-1"
|
||||
/>
|
||||
<ButtonStyled
|
||||
v-else-if="!isLibraryInstanceSelectionActive && !installed && !instance.quarantined"
|
||||
color="brand"
|
||||
circular
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="!isLibraryInstanceSelectionActive && !instance.quarantined"
|
||||
color="brand"
|
||||
circular
|
||||
>
|
||||
<button
|
||||
v-tooltip="'Play'"
|
||||
class="card-shadow origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
<button
|
||||
v-tooltip="'Repair'"
|
||||
class="card-shadow origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
|
||||
@click="(e) => repair(e)"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="!isLibraryInstanceSelectionActive && !instance.quarantined"
|
||||
color="brand"
|
||||
circular
|
||||
>
|
||||
<PlayIcon class="translate-x-px" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<button
|
||||
v-tooltip="'Play'"
|
||||
class="card-shadow origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<PlayIcon class="translate-x-px" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-1 flex-col justify-center gap-1">
|
||||
<p class="m-0 truncate text-base font-semibold leading-5 text-contrast">
|
||||
{{ instance.name }}
|
||||
</p>
|
||||
<p class="m-0 truncate text-sm font-medium capitalize leading-[18px] text-primary">
|
||||
{{ instance.loader }} {{ instance.game_version }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</InstanceCardContent>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user