You've already forked AstralRinth
forked from didirus/AstralRinth
Misc fixes, new instance & project cards (#3040)
* Fix some TS errors, and misc settings fixes * New instance + project cards * bug fixes + lint * Quick instance switcher --------- Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
@@ -57,6 +57,7 @@ import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -393,6 +394,9 @@ function handleAuxClick(e) {
|
||||
<template #label>Library</template>
|
||||
</NavButton>
|
||||
<div class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
||||
<suspense>
|
||||
<QuickInstanceSwitcher />
|
||||
</suspense>
|
||||
<NavButton :to="() => $refs.installationModal.show()" :disabled="offline">
|
||||
<PlusIcon />
|
||||
<template #label>Create new instance</template>
|
||||
|
||||
@@ -117,4 +117,8 @@ img {
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.card-shadow {
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
@import '@modrinth/assets/omorphia.scss';
|
||||
|
||||
@@ -363,9 +363,9 @@ const filteredResults = computed(() => {
|
||||
|
||||
.instances {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
gap: 0.75rem;
|
||||
margin-right: auto;
|
||||
scroll-behavior: smooth;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -181,6 +181,7 @@ const handleOptionsClick = async (args) => {
|
||||
}
|
||||
}
|
||||
|
||||
const maxInstancesPerCompactRow = ref(1)
|
||||
const maxInstancesPerRow = ref(1)
|
||||
const maxProjectsPerRow = ref(1)
|
||||
|
||||
@@ -190,8 +191,20 @@ const calculateCardsPerRow = () => {
|
||||
// Convert container width from pixels to rem
|
||||
const containerWidthInRem =
|
||||
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 1) / 11)
|
||||
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 1) / 19)
|
||||
|
||||
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
||||
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
|
||||
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
||||
|
||||
if (maxInstancesPerRow.value < 5) {
|
||||
maxInstancesPerRow.value *= 2
|
||||
}
|
||||
if (maxInstancesPerCompactRow.value < 5) {
|
||||
maxInstancesPerCompactRow.value *= 2
|
||||
}
|
||||
if (maxProjectsPerRow.value < 3) {
|
||||
maxProjectsPerRow.value *= 2
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -213,17 +226,33 @@ onUnmounted(() => {
|
||||
proceed-label="Delete"
|
||||
@proceed="deleteProfile"
|
||||
/>
|
||||
<div class="content">
|
||||
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
|
||||
<div class="header">
|
||||
<router-link :to="row.route">{{ row.label }}</router-link>
|
||||
<ChevronRightIcon />
|
||||
</div>
|
||||
<section v-if="row.instance" ref="modsRow" class="instances">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-for="(row, rowIndex) in actualInstances" ref="rows" :key="row.label" class="row">
|
||||
<router-link
|
||||
class="flex mb-3 leading-none items-center gap-1 text-primary text-lg font-bold hover:underline group"
|
||||
:class="{ 'mt-1': rowIndex > 0 }"
|
||||
:to="row.route"
|
||||
>
|
||||
{{ row.label }}
|
||||
<ChevronRightIcon
|
||||
class="h-5 w-5 stroke-[3px] group-hover:translate-x-1 transition-transform group-hover:text-brand"
|
||||
/>
|
||||
</router-link>
|
||||
<section
|
||||
v-if="row.instance"
|
||||
ref="modsRow"
|
||||
class="instances"
|
||||
:class="{ compact: row.compact }"
|
||||
>
|
||||
<Instance
|
||||
v-for="instance in row.instances.slice(0, maxInstancesPerRow)"
|
||||
:key="(instance?.project_id || instance?.id) + instance.install_stage"
|
||||
v-for="(instance, instanceIndex) in row.instances.slice(
|
||||
0,
|
||||
row.compact ? maxInstancesPerCompactRow : maxInstancesPerRow,
|
||||
)"
|
||||
:key="row.label + instance.path"
|
||||
:instance="instance"
|
||||
:compact="row.compact"
|
||||
:first="instanceIndex === 0"
|
||||
@contextmenu.prevent.stop="(event) => handleInstanceRightClick(event, instance)"
|
||||
/>
|
||||
</section>
|
||||
@@ -308,16 +337,21 @@ onUnmounted(() => {
|
||||
|
||||
.instances {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
grid-gap: 0.75rem;
|
||||
width: 100%;
|
||||
|
||||
&.compact {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.projects {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
grid-gap: 1rem;
|
||||
grid-gap: 0.75rem;
|
||||
|
||||
.item {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { onUnmounted, ref, computed } from 'vue'
|
||||
import { onUnmounted, ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { StopCircleIcon, PlayIcon } from '@modrinth/assets'
|
||||
import { Card, Avatar, AnimatedLogo } from '@modrinth/ui'
|
||||
import { SpinnerIcon, GameIcon, TimerIcon, StopCircleIcon, PlayIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, Avatar } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { kill, run } from '@/helpers/profile'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
@@ -11,6 +11,13 @@ import { handleError } from '@/store/state.js'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import dayjs from 'dayjs'
|
||||
import duration from 'dayjs/plugin/duration'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
@@ -19,11 +26,23 @@ const props = defineProps({
|
||||
return {}
|
||||
},
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
first: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const playing = ref(false)
|
||||
|
||||
const modLoading = computed(() => props.instance.install_stage !== 'installed')
|
||||
const modLoading = computed(
|
||||
() =>
|
||||
currentEvent.value === 'installing' || (currentEvent.value === 'launched' && !playing.value),
|
||||
)
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -39,17 +58,15 @@ const checkProcess = async () => {
|
||||
|
||||
const play = async (e, context) => {
|
||||
e?.stopPropagation()
|
||||
modLoading.value = true
|
||||
await run(props.instance.path).catch((err) =>
|
||||
handleSevereError(err, { profilePath: props.instance.path }),
|
||||
)
|
||||
modLoading.value = false
|
||||
|
||||
trackEvent('InstancePlay', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
source: context,
|
||||
})
|
||||
await run(props.instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||
.finally(() => {
|
||||
trackEvent('InstancePlay', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
source: context,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const stop = async (e, context) => {
|
||||
@@ -85,175 +102,130 @@ defineExpose({
|
||||
instance: props.instance,
|
||||
})
|
||||
|
||||
const currentEvent = ref(null)
|
||||
|
||||
const unlisten = await process_listener((e) => {
|
||||
if (e.event === 'finished' && e.profile_path_id === props.instance.path) playing.value = false
|
||||
if (e.profile_path_id === props.instance.path) {
|
||||
currentEvent.value = e.event
|
||||
if (e.event === 'finished') {
|
||||
playing.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => checkProcess())
|
||||
onUnmounted(() => unlisten())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="instance">
|
||||
<Card class="instance-card-item button-base" @click="seeInstance" @mouseenter="checkProcess">
|
||||
<Avatar
|
||||
size="lg"
|
||||
:src="props.instance.icon_path ? convertFileSrc(props.instance.icon_path) : null"
|
||||
alt="Mod card"
|
||||
class="mod-image"
|
||||
/>
|
||||
<div class="project-info">
|
||||
<p class="title">{{ props.instance.name }}</p>
|
||||
<p
|
||||
v-if="
|
||||
props.instance.install_stage === 'installing' ||
|
||||
props.instance.install_stage === 'not_installed' ||
|
||||
props.instance.install_stage === 'pack_installing'
|
||||
"
|
||||
class="description"
|
||||
>
|
||||
Installing...
|
||||
</p>
|
||||
<p v-else class="description">
|
||||
{{ props.instance.loader }}
|
||||
{{ props.instance.game_version }}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<template v-if="compact">
|
||||
<div
|
||||
v-if="playing === true"
|
||||
class="stop cta button-base"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
class="button-base card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer active:scale-[0.98] transition-transform"
|
||||
@click="seeInstance"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
<Avatar
|
||||
size="48px"
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
:tint-by="instance.path"
|
||||
alt="Mod card"
|
||||
/>
|
||||
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
||||
<span class="line-clamp-2">{{ instance.name }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
|
||||
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
|
||||
<StopCircleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="modLoading" color="standard" circular>
|
||||
<button v-tooltip="'Instance is loading...'" disabled>
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else :color="first ? 'brand' : 'standard'" circular>
|
||||
<button
|
||||
v-tooltip="'Play'"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<!-- Translate for optical centering -->
|
||||
<PlayIcon class="translate-x-[1px]" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
||||
<TimerIcon />
|
||||
<span class="text-sm"> Played {{ dayjs(instance.last_played).fromNow() }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="modLoading === true && playing === false" class="cta loading-cta">
|
||||
<AnimatedLogo class="loading-indicator" />
|
||||
</div>
|
||||
<div v-else class="install cta button-base" @click="(e) => play(e, 'InstanceCard')">
|
||||
<PlayIcon />
|
||||
</template>
|
||||
<div v-else>
|
||||
<div
|
||||
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
|
||||
@click="seeInstance"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<div class="relative flex items-center justify-center">
|
||||
<Avatar
|
||||
size="96px"
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
:tint-by="instance.path"
|
||||
alt="Mod card"
|
||||
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||
/>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<ButtonStyled v-if="playing" size="large" color="red" circular>
|
||||
<button
|
||||
v-tooltip="'Stop'"
|
||||
:class="{ 'scale-100 opacity-100': playing }"
|
||||
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<SpinnerIcon
|
||||
v-else-if="modLoading || installing"
|
||||
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
||||
class="animate-spin w-8 h-8"
|
||||
/>
|
||||
<ButtonStyled v-else size="large" color="brand" circular>
|
||||
<button
|
||||
v-tooltip="'Play'"
|
||||
class="transition-all scale-75 group-hover:scale-100 origin-bottom opacity-0 group-hover:opacity-100 card-shadow"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<PlayIcon class="translate-x-[2px]" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="m-0 text-lg font-bold text-contrast leading-tight line-clamp-2">
|
||||
{{ instance.name }}
|
||||
</p>
|
||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
||||
<GameIcon class="shrink-0" />
|
||||
<span class="text-sm">
|
||||
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
||||
<TimerIcon class="shrink-0" />
|
||||
<span class="text-sm line-clamp-1">
|
||||
Played for
|
||||
{{
|
||||
dayjs
|
||||
.duration(instance.recent_time_played + instance.submitted_time_played, 'seconds')
|
||||
.humanize()
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.loading-indicator {
|
||||
width: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
|
||||
svg {
|
||||
width: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.instance {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.cta {
|
||||
opacity: 1;
|
||||
bottom: calc(var(--gap-md) + 4.25rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cta {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
z-index: 1;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
right: calc(var(--gap-md) * 2);
|
||||
bottom: 3.25rem;
|
||||
opacity: 0;
|
||||
transition:
|
||||
0.2s ease-in-out bottom,
|
||||
0.2s ease-in-out opacity,
|
||||
0.1s ease-in-out filter !important;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-floating);
|
||||
|
||||
svg {
|
||||
color: var(--color-accent-contrast);
|
||||
width: 1.5rem !important;
|
||||
height: 1.5rem !important;
|
||||
}
|
||||
|
||||
&.install {
|
||||
background: var(--color-brand);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.stop {
|
||||
background: var(--color-red);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.loading-cta {
|
||||
background: hsl(220, 11%, 10%) !important;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.instance-card-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: var(--gap-md);
|
||||
transition: 0.1s ease-in-out all !important; /* overrides Omorphia defaults */
|
||||
margin-bottom: 0;
|
||||
|
||||
.mod-image {
|
||||
--size: 100%;
|
||||
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
max-width: unset !important;
|
||||
max-height: unset !important;
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
color: var(--color-contrast);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
line-height: 110%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--color-base);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
font-size: 0.775rem;
|
||||
line-height: 125%;
|
||||
margin: 0.25rem 0 0;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -237,7 +237,7 @@ const display_icon = ref(null)
|
||||
const showAdvanced = ref(false)
|
||||
const creating = ref(false)
|
||||
const showSnapshots = ref(false)
|
||||
const creationType = ref('from file')
|
||||
const creationType = ref('custom')
|
||||
const isShowing = ref(false)
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -38,6 +38,7 @@ defineProps<{
|
||||
to: (() => void) | string
|
||||
isPrimary?: RouteFunction
|
||||
isSubpage?: RouteFunction
|
||||
highlightOverride?: boolean
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -157,8 +157,4 @@ watch(route, () => {
|
||||
all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0s,
|
||||
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
||||
}
|
||||
|
||||
.card-shadow {
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<script setup>
|
||||
import { Card, Avatar, Button } from '@modrinth/ui'
|
||||
import { DownloadIcon, HeartIcon, CalendarIcon } from '@modrinth/assets'
|
||||
import { Avatar, TagItem } from '@modrinth/ui'
|
||||
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
|
||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const router = useRouter()
|
||||
const installing = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
@@ -22,6 +20,17 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const featuredCategory = computed(() => {
|
||||
if (props.project.categories.includes('optimization')) {
|
||||
return 'optimization'
|
||||
}
|
||||
|
||||
if (props.project.categories.length > 0) {
|
||||
return props.project.categories[0]
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const toColor = computed(() => {
|
||||
let color = props.project.color
|
||||
|
||||
@@ -47,197 +56,66 @@ const toTransparent = computed(() => {
|
||||
'))'
|
||||
)
|
||||
})
|
||||
|
||||
const install = async (e) => {
|
||||
e?.stopPropagation()
|
||||
installing.value = true
|
||||
await installVersion(
|
||||
props.project.project_id,
|
||||
null,
|
||||
props.instance ? props.instance.path : null,
|
||||
'ProjectCard',
|
||||
() => {
|
||||
installing.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<Card class="project-card button-base" @click="router.push(`/project/${project.slug}`)">
|
||||
<div
|
||||
class="card-shadow button-base bg-bg-raised rounded-xl overflow-clip cursor-pointer active:scale-[0.98] transition-transform"
|
||||
@click="router.push(`/project/${project.slug}`)"
|
||||
>
|
||||
<div
|
||||
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
|
||||
:style="{
|
||||
'background-color': project.featured_gallery ?? project.gallery[0] ? null : toColor,
|
||||
'background-image': `url(${
|
||||
project.featured_gallery ??
|
||||
project.gallery[0] ??
|
||||
'https://launcher-files.modrinth.com/assets/maze-bg.png'
|
||||
})`,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="banner"
|
||||
:style="{
|
||||
'background-color': project.featured_gallery ?? project.gallery[0] ? null : toColor,
|
||||
'background-image': `url(${
|
||||
project.featured_gallery ??
|
||||
project.gallery[0] ??
|
||||
'https://launcher-files.modrinth.com/assets/maze-bg.png'
|
||||
})`,
|
||||
class="badges-wrapper"
|
||||
:class="{
|
||||
'no-image': !project.featured_gallery && !project.gallery[0],
|
||||
}"
|
||||
>
|
||||
<div class="badges">
|
||||
<div class="badge">
|
||||
<DownloadIcon />
|
||||
{{ formatNumber(project.downloads) }}
|
||||
</div>
|
||||
<div class="badge">
|
||||
<HeartIcon />
|
||||
{{ formatNumber(project.follows) }}
|
||||
</div>
|
||||
<div class="badge">
|
||||
<CalendarIcon />
|
||||
{{ formatCategory(dayjs(project.date_modified).fromNow()) }}
|
||||
</div>
|
||||
:style="{
|
||||
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center gap-2 px-4 py-3">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Avatar size="48px" :src="project.icon_url" />
|
||||
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
||||
<span class="line-clamp-2">{{ project.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0 text-sm font-medium line-clamp-3 leading-tight h-[3.25rem]">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
<div class="flex items-center gap-2 text-sm text-secondary font-semibold mt-auto">
|
||||
<div
|
||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ formatNumber(project.downloads) }}
|
||||
</div>
|
||||
<div
|
||||
class="badges-wrapper"
|
||||
:class="{
|
||||
'no-image': !project.featured_gallery && !project.gallery[0],
|
||||
}"
|
||||
:style="{
|
||||
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<Avatar class="icon" size="sm" :src="project.icon_url" />
|
||||
<div class="title">
|
||||
<div class="title-text">
|
||||
{{ project.title }}
|
||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||
>
|
||||
<HeartIcon />
|
||||
{{ formatNumber(project.follows) }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 pr-2">
|
||||
<TagIcon />
|
||||
<TagItem>
|
||||
{{ formatCategory(featuredCategory) }}
|
||||
</TagItem>
|
||||
</div>
|
||||
<div class="author">by {{ project.author }}</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
{{ project.description }}
|
||||
</div>
|
||||
</Card>
|
||||
<Button color="primary" class="install" :disabled="installing" @click="install">
|
||||
<DownloadIcon />
|
||||
{{ installing ? 'Installing' : 'Install' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wrapper {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
|
||||
&:hover {
|
||||
.install:enabled {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-card {
|
||||
display: grid;
|
||||
grid-gap: 1rem;
|
||||
grid-template:
|
||||
'. . . .' 0
|
||||
'. icon title .' 3rem
|
||||
'banner banner banner banner' auto
|
||||
'. description description .' 3.5rem
|
||||
'. . . .' 0 / 0 3rem minmax(0, 1fr) 0;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.icon {
|
||||
grid-area: icon;
|
||||
}
|
||||
|
||||
.title {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
grid-area: title;
|
||||
white-space: nowrap;
|
||||
|
||||
.title-text {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
font-size: var(--font-size-sm);
|
||||
grid-area: author;
|
||||
}
|
||||
|
||||
.banner {
|
||||
grid-area: banner;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
|
||||
.badges-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mix-blend-mode: hard-light;
|
||||
}
|
||||
|
||||
.badges {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--gap-sm);
|
||||
gap: var(--gap-xs);
|
||||
display: flex;
|
||||
z-index: 10;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
grid-area: description;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: var(--color-raised-bg);
|
||||
font-size: var(--font-size-xs);
|
||||
padding: var(--gap-xs) var(--gap-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: var(--gap-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.install {
|
||||
position: absolute;
|
||||
top: calc(5rem + var(--gap-sm));
|
||||
right: var(--gap-sm);
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup>
|
||||
import { list } from '@/helpers/profile'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import dayjs from 'dayjs'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { profile_listener } from '@/helpers/events.js'
|
||||
import NavButton from '@/components/ui/NavButton.vue'
|
||||
import { Avatar } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { SpinnerIcon } from '@modrinth/assets'
|
||||
|
||||
const recentInstances = ref([])
|
||||
const getInstances = async () => {
|
||||
const profiles = await list().catch(handleError)
|
||||
|
||||
recentInstances.value = profiles
|
||||
.sort((a, b) => {
|
||||
const dateA = dayjs(a.created > a.last_played ? a.last_played : a.created)
|
||||
const dateB = dayjs(b.created > b.last_played ? b.last_played : b.created)
|
||||
|
||||
if (dateA.isSame(dateB)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
|
||||
return dateB - dateA
|
||||
})
|
||||
.slice(0, 4)
|
||||
}
|
||||
|
||||
await getInstances()
|
||||
|
||||
const unlistenProfile = await profile_listener(async () => {
|
||||
await getInstances()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProfile()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavButton
|
||||
v-for="instance in recentInstances"
|
||||
:key="instance.id"
|
||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||
>
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
circle
|
||||
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||
/>
|
||||
<div
|
||||
v-if="instance.install_stage !== 'installed'"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<SpinnerIcon class="animate-spin w-4 h-4" />
|
||||
</div>
|
||||
<template #label>{{ instance.name }}</template>
|
||||
</NavButton>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -14,7 +14,7 @@
|
||||
<DownloadIcon />
|
||||
</Button>
|
||||
<div v-if="offline" class="status">
|
||||
<span class="circle stopped" />
|
||||
<UnplugIcon />
|
||||
<div class="running-text">
|
||||
<span> Offline </span>
|
||||
</div>
|
||||
@@ -108,7 +108,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { DownloadIcon, StopCircleIcon, TerminalSquareIcon, DropdownIcon } from '@modrinth/assets'
|
||||
import {
|
||||
DownloadIcon,
|
||||
StopCircleIcon,
|
||||
TerminalSquareIcon,
|
||||
DropdownIcon,
|
||||
UnplugIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, Card } from '@modrinth/ui'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="button-base p-4 bg-bg-raised rounded-xl flex gap-3 group"
|
||||
class="card-shadow button-base p-4 bg-bg-raised rounded-xl flex gap-3 group"
|
||||
@click="
|
||||
() => {
|
||||
emit('open')
|
||||
|
||||
@@ -250,7 +250,7 @@ onUnmounted(() => {
|
||||
</ModalWrapper>
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg m-0">Friends</h3>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<ButtonStyled v-if="userCredentials" type="transparent" circular>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
|
||||
@@ -17,19 +17,21 @@
|
||||
<tr class="content">
|
||||
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
|
||||
<td>
|
||||
<DropdownSelect
|
||||
<multiselect
|
||||
v-if="versions?.length > 1"
|
||||
v-model="selectedVersion"
|
||||
:options="versions"
|
||||
:searchable="true"
|
||||
placeholder="Select version"
|
||||
name="Version select"
|
||||
:display-name="
|
||||
open-direction="top"
|
||||
:show-labels="false"
|
||||
:custom-label="
|
||||
(version) =>
|
||||
`${version?.name} (${version?.loaders
|
||||
.map((name) => formatCategory(name))
|
||||
.join(', ')} - ${version?.game_versions.join(', ')})`
|
||||
"
|
||||
render-up
|
||||
:max-height="150"
|
||||
/>
|
||||
<span v-else>
|
||||
<span>
|
||||
@@ -56,12 +58,13 @@
|
||||
<script setup>
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { XIcon, DownloadIcon } from '@modrinth/assets'
|
||||
import { Button, DropdownSelect } from '@modrinth/ui'
|
||||
import { Button } from '@modrinth/ui'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
import { add_project_from_version as installMod } from '@/helpers/profile'
|
||||
import { ref } from 'vue'
|
||||
import { handleError } from '@/store/state.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import Multiselect from 'vue-multiselect'
|
||||
|
||||
const instance = ref(null)
|
||||
const project = ref(null)
|
||||
|
||||
@@ -357,7 +357,7 @@ const createInstance = async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
.profiles {
|
||||
@@ -378,7 +378,6 @@ const createInstance = async () => {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
img {
|
||||
|
||||
@@ -10,15 +10,14 @@ import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import type { InstanceSettingsTabProps, GameInstance } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const router = useRouter()
|
||||
|
||||
const deleteConfirmModal = ref()
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
}>()
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const title = ref(props.instance.name)
|
||||
const icon: Ref<string | undefined> = ref(props.instance.icon_path)
|
||||
@@ -215,6 +214,7 @@ const messages = defineMessages({
|
||||
:src="icon ? convertFileSrc(icon) : icon"
|
||||
size="108px"
|
||||
class="!border-4 group-hover:brightness-75"
|
||||
:tint-by="props.instance.path"
|
||||
no-shadow
|
||||
/>
|
||||
<div class="absolute top-0 right-0 m-2">
|
||||
|
||||
@@ -5,12 +5,11 @@ import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { edit } from '@/helpers/profile'
|
||||
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
}>()
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||
|
||||
|
||||
@@ -29,7 +29,11 @@ import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import { get_project, get_version_many } from '@/helpers/cache'
|
||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import type { GameInstance, ManifestLoaderVersion, Manifest } from '@/helpers/types.d.ts'
|
||||
import type {
|
||||
InstanceSettingsTabProps,
|
||||
ManifestLoaderVersion,
|
||||
Manifest,
|
||||
} from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -38,10 +42,7 @@ const modpackVersionModal = ref()
|
||||
const modalConfirmUnpair = ref()
|
||||
const modalConfirmReinstall = ref()
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
offline?: boolean
|
||||
}>()
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const loader = ref(props.instance.loader)
|
||||
const gameVersion = ref(props.instance.game_version)
|
||||
@@ -307,11 +308,11 @@ const messages = defineMessages({
|
||||
defaultMessage: '(unknown version)',
|
||||
},
|
||||
repairConfirmTitle: {
|
||||
id: 'instance.settings.tabs.installation.repair.confirm-title',
|
||||
id: 'instance.settings.tabs.installation.repair.confirm.title',
|
||||
defaultMessage: 'Repair instance?',
|
||||
},
|
||||
repairConfirmDescription: {
|
||||
id: 'instance.settings.tabs.installation.repair.description',
|
||||
id: 'instance.settings.tabs.installation.repair.confirm.description',
|
||||
defaultMessage:
|
||||
'Repairing reinstalls Minecraft dependencies and checks for corruption. This may resolve issues if your game is not launching due to launcher-related errors, but will not resolve issues or crashes related to installed mods.',
|
||||
},
|
||||
@@ -347,10 +348,26 @@ const messages = defineMessages({
|
||||
id: 'instance.settings.tabs.installation.change-version.button.installing',
|
||||
defaultMessage: 'Installing',
|
||||
},
|
||||
installInProgress: {
|
||||
id: 'instance.settings.tabs.installation.install.in-progress',
|
||||
defaultMessage: 'Installation in progress',
|
||||
},
|
||||
installButton: {
|
||||
id: 'instance.settings.tabs.installation.change-version.button.install',
|
||||
defaultMessage: 'Install',
|
||||
},
|
||||
alreadyInstalledVanilla: {
|
||||
id: 'instance.settings.tabs.installation.change-version.already-installed.vanilla',
|
||||
defaultMessage: 'Vanilla {game_version} already installed',
|
||||
},
|
||||
alreadyInstalledModded: {
|
||||
id: 'instance.settings.tabs.installation.change-version.already-installed.modded',
|
||||
defaultMessage: '{platform} {version} for Minecraft {game_version} already installed',
|
||||
},
|
||||
installAction: {
|
||||
id: 'instance.settings.tabs.installation.tooltip.action.install',
|
||||
defaultMessage: 'install',
|
||||
},
|
||||
installingNewVersion: {
|
||||
id: 'instance.settings.tabs.installation.change-version.in-progress',
|
||||
defaultMessage: 'Installing new version',
|
||||
@@ -393,11 +410,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Unlink instance',
|
||||
},
|
||||
unlinkInstanceConfirmTitle: {
|
||||
id: 'instance.settings.tabs.installation.unlink.title',
|
||||
id: 'instance.settings.tabs.installation.unlink.confirm.title',
|
||||
defaultMessage: 'Are you sure you want to unlink this instance?',
|
||||
},
|
||||
unlinkInstanceConfirmDescription: {
|
||||
id: 'instance.settings.tabs.installation.unlink.description',
|
||||
id: 'instance.settings.tabs.installation.unlink.confirm.description',
|
||||
defaultMessage:
|
||||
'If you proceed, you will not be able to re-link it without creating an entirely new instance. You will no longer receive modpack updates and it will become a normal.',
|
||||
},
|
||||
@@ -407,7 +424,7 @@ const messages = defineMessages({
|
||||
},
|
||||
reinstallModpackConfirmDescription: {
|
||||
id: 'instance.settings.tabs.installation.reinstall.confirm.description',
|
||||
defaultMessage: `Reinstalling will reset content provided by the modpack to their original state.`,
|
||||
defaultMessage: `Reinstalling will reset all installed or modified content to what is provided by the modpack, removing any mods or content you have added on top of the original installation. This may fix unexpected behavior if changes have been made to the instance, but if your worlds now depend on additional installed content, it may break existing worlds.`,
|
||||
},
|
||||
reinstallModpackTitle: {
|
||||
id: 'instance.settings.tabs.installation.reinstall.title',
|
||||
@@ -415,7 +432,7 @@ const messages = defineMessages({
|
||||
},
|
||||
reinstallModpackDescription: {
|
||||
id: 'instance.settings.tabs.installation.reinstall.description',
|
||||
defaultMessage: `Resets all content provided by the modpack to their original state. This may fix unexpected behavior if changes have been made to the instance.`,
|
||||
defaultMessage: `Resets the instance's content to its original state, removing any mods or content you have added on top of the original modpack.`,
|
||||
},
|
||||
reinstallModpackButton: {
|
||||
id: 'instance.settings.tabs.installation.reinstall.button',
|
||||
@@ -647,7 +664,34 @@ const messages = defineMessages({
|
||||
</template>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!isValid || !isChanged || editing" @click="saveGvLoaderEdits()">
|
||||
<button
|
||||
v-tooltip="
|
||||
installing || reinstalling
|
||||
? formatMessage(messages.installInProgress)
|
||||
: !isChanged
|
||||
? formatMessage(
|
||||
loader === 'vanilla'
|
||||
? messages.alreadyInstalledVanilla
|
||||
: messages.alreadyInstalledModded,
|
||||
{
|
||||
platform: formatCategory(loader),
|
||||
version: instance.loader_version,
|
||||
game_version: gameVersion,
|
||||
},
|
||||
)
|
||||
: repairing
|
||||
? formatMessage(messages.cannotWhileRepairing, {
|
||||
action: formatMessage(messages.installAction),
|
||||
})
|
||||
: offline
|
||||
? formatMessage(messages.cannotWhileOffline, {
|
||||
action: formatMessage(messages.installAction),
|
||||
})
|
||||
: null
|
||||
"
|
||||
:disabled="!isValid || !isChanged || editing || offline || repairing"
|
||||
@click="saveGvLoaderEdits()"
|
||||
>
|
||||
<SpinnerIcon v-if="editing" class="animate-spin" />
|
||||
<DownloadIcon v-else />
|
||||
{{
|
||||
|
||||
@@ -8,12 +8,11 @@ import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { get } from '@/helpers/settings'
|
||||
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
}>()
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@ import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { edit } from '@/helpers/profile'
|
||||
import type { InstanceSettingsTabProps, AppSettings } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
}>()
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
CoffeeIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { TabbedModal } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
||||
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
||||
@@ -92,7 +92,9 @@ function show() {
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
const isOpen = computed(() => modal.value?.isOpen)
|
||||
|
||||
defineExpose({ show, isOpen })
|
||||
|
||||
const version = await getVersion()
|
||||
const osPlatform = getOsPlatform()
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
MonitorIcon,
|
||||
CodeIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, TabbedModal } from '@modrinth/ui'
|
||||
import { Avatar, TabbedModal, type TabbedModalTab } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
@@ -17,19 +17,13 @@ import InstallationSettings from '@/components/ui/instance_settings/Installation
|
||||
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
|
||||
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
||||
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
||||
import type { InstanceSettingsTabProps } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
defineProps({
|
||||
instance: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
})
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const tabs = [
|
||||
const tabs: TabbedModalTab<InstanceSettingsTabProps>[] = [
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'instance.settings.tabs.general',
|
||||
@@ -92,12 +86,13 @@ const titleMessage = defineMessage({
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||
size="24px"
|
||||
:tint-by="props.instance.path"
|
||||
/>
|
||||
{{ instance.name }} <ChevronRightIcon />
|
||||
<span class="font-extrabold text-contrast">{{ formatMessage(titleMessage) }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<TabbedModal :tabs="tabs.map((tab) => ({ ...tab, props: { instance } }))" />
|
||||
<TabbedModal :tabs="tabs.map((tab) => ({ ...tab, props }))" />
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
5
apps/app-frontend/src/helpers/types.d.ts
vendored
5
apps/app-frontend/src/helpers/types.d.ts
vendored
@@ -103,3 +103,8 @@ type AppSettings = {
|
||||
prev_custom_dir?: string
|
||||
migrated: boolean
|
||||
}
|
||||
|
||||
export type InstanceSettingsTabProps = {
|
||||
instance: GameInstance
|
||||
offline?: boolean
|
||||
}
|
||||
|
||||
@@ -119,6 +119,12 @@
|
||||
"instance.settings.tabs.installation": {
|
||||
"message": "Installation"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.modded": {
|
||||
"message": "{platform} {version} for Minecraft {game_version} already installed"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
|
||||
"message": "Vanilla {game_version} already installed"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button": {
|
||||
"message": "Change version"
|
||||
},
|
||||
@@ -149,6 +155,9 @@
|
||||
"instance.settings.tabs.installation.install": {
|
||||
"message": "Install"
|
||||
},
|
||||
"instance.settings.tabs.installation.install.in-progress": {
|
||||
"message": "Installation in progress"
|
||||
},
|
||||
"instance.settings.tabs.installation.loader-version": {
|
||||
"message": "{loader} version"
|
||||
},
|
||||
@@ -174,13 +183,13 @@
|
||||
"message": "Reinstalling modpack"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.description": {
|
||||
"message": "Reinstalling will reset content provided by the modpack to their original state."
|
||||
"message": "Reinstalling will reset all installed or modified content to what is provided by the modpack, removing any mods or content you have added on top of the original installation. This may fix unexpected behavior if changes have been made to the instance, but if your worlds now depend on additional installed content, it may break existing worlds."
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.title": {
|
||||
"message": "Are you sure you want to reinstall this instance?"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.description": {
|
||||
"message": "Resets all content provided by the modpack to their original state. This may fix unexpected behavior if changes have been made to the instance."
|
||||
"message": "Resets the instance's content to its original state, removing any mods or content you have added on top of the original modpack."
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.title": {
|
||||
"message": "Reinstall modpack"
|
||||
@@ -191,12 +200,12 @@
|
||||
"instance.settings.tabs.installation.repair.button.repairing": {
|
||||
"message": "Repairing"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.confirm-title": {
|
||||
"message": "Repair instance?"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.description": {
|
||||
"instance.settings.tabs.installation.repair.confirm.description": {
|
||||
"message": "Repairing reinstalls Minecraft dependencies and checks for corruption. This may resolve issues if your game is not launching due to launcher-related errors, but will not resolve issues or crashes related to installed mods."
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.confirm.title": {
|
||||
"message": "Repair instance?"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.in-progress": {
|
||||
"message": "Repair in progress"
|
||||
},
|
||||
@@ -209,6 +218,9 @@
|
||||
"instance.settings.tabs.installation.tooltip.action.change-version": {
|
||||
"message": "change version"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.install": {
|
||||
"message": "install"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.reinstall": {
|
||||
"message": "reinstall"
|
||||
},
|
||||
@@ -230,12 +242,18 @@
|
||||
"instance.settings.tabs.installation.unlink.button": {
|
||||
"message": "Unlink instance"
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.description": {
|
||||
"instance.settings.tabs.installation.unlink.confirm.description": {
|
||||
"message": "If you proceed, you will not be able to re-link it without creating an entirely new instance. You will no longer receive modpack updates and it will become a normal."
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.title": {
|
||||
"instance.settings.tabs.installation.unlink.confirm.title": {
|
||||
"message": "Are you sure you want to unlink this instance?"
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.description": {
|
||||
"message": "This instance is linked to a modpack, which means mods can't be updated and you can't change the mod loader or Minecraft version. Unlinking will permanently disconnect this instance from the modpack."
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.title": {
|
||||
"message": "Unlink from modpack"
|
||||
},
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "Java and memory"
|
||||
},
|
||||
|
||||
@@ -394,13 +394,13 @@ await refreshSearch()
|
||||
<InstanceIndicator :instance="instance" />
|
||||
<h1 class="m-0 mb-1 text-xl">Install content to instance</h1>
|
||||
</template>
|
||||
<h1 v-else class="m-0 mb-1 text-2xl">Discover content</h1>
|
||||
<h1 v-else class="m-0 text-2xl">Discover content</h1>
|
||||
<NavTabs :links="selectableProjectTypes" />
|
||||
<div class="iconified-input">
|
||||
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||
<input
|
||||
v-model="query"
|
||||
class="h-12"
|
||||
class="h-12 card-shadow"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
|
||||
@@ -31,19 +31,21 @@ window.addEventListener('online', () => {
|
||||
const getInstances = async () => {
|
||||
const profiles = await list().catch(handleError)
|
||||
|
||||
recentInstances.value = profiles.sort((a, b) => {
|
||||
const dateA = dayjs(a.last_played ?? 0)
|
||||
const dateB = dayjs(b.last_played ?? 0)
|
||||
recentInstances.value = profiles
|
||||
.filter((x) => x.last_played)
|
||||
.sort((a, b) => {
|
||||
const dateA = dayjs(a.last_played)
|
||||
const dateB = dayjs(b.last_played)
|
||||
|
||||
if (dateA.isSame(dateB)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
if (dateA.isSame(dateB)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
|
||||
return dateB - dateA
|
||||
})
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
const filters = []
|
||||
for (const instance of recentInstances.value) {
|
||||
for (const instance of profiles) {
|
||||
if (instance.linked_data && instance.linked_data.project_id) {
|
||||
filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`)
|
||||
}
|
||||
@@ -100,25 +102,27 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="p-6 flex flex-col gap-2">
|
||||
<h1 class="m-0 text-2xl">Welcome back!</h1>
|
||||
<h1 v-if="recentInstances" class="m-0 text-2xl">Welcome back!</h1>
|
||||
<h1 v-else class="m-0 text-2xl">Welcome to Modrinth App!</h1>
|
||||
<RowDisplay
|
||||
v-if="total > 0"
|
||||
:instances="[
|
||||
{
|
||||
label: 'Jump back in',
|
||||
label: 'Recently played',
|
||||
route: '/library',
|
||||
instances: recentInstances,
|
||||
instance: true,
|
||||
downloaded: true,
|
||||
compact: true,
|
||||
},
|
||||
{
|
||||
label: 'Popular packs',
|
||||
label: 'Discover a modpack',
|
||||
route: '/browse/modpack',
|
||||
instances: featuredModpacks,
|
||||
downloaded: false,
|
||||
},
|
||||
{
|
||||
label: 'Popular mods',
|
||||
label: 'Discover mods',
|
||||
route: '/browse/mod',
|
||||
instances: featuredMods,
|
||||
downloaded: false,
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||
>
|
||||
<ExportModal ref="exportModal" :instance="instance" />
|
||||
<InstanceSettingsModal ref="settingsModal" :instance="instance" />
|
||||
<InstanceSettingsModal ref="settingsModal" :instance="instance" :offline="offline" />
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar :src="icon" :alt="instance.name" size="96px" />
|
||||
<Avatar :src="icon" :alt="instance.name" size="96px" :tint-by="instance.path" />
|
||||
</template>
|
||||
<template #title>
|
||||
{{ instance.name }}
|
||||
@@ -89,9 +89,13 @@
|
||||
<NavTabs :links="tabs" />
|
||||
</div>
|
||||
<div class="p-6 pt-4">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<RouterView v-slot="{ Component }" :key="instance.path">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
||||
<Suspense
|
||||
:key="instance.path"
|
||||
@pending="loadingBar.startLoading()"
|
||||
@resolve="loadingBar.stopLoading()"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
:instance="instance"
|
||||
@@ -164,7 +168,7 @@ import { get, get_full_path, kill, run } from '@/helpers/profile'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import { process_listener, profile_listener } from '@/helpers/events'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, onUnmounted, computed } from 'vue'
|
||||
import { ref, onUnmounted, computed, watch } from 'vue'
|
||||
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
@@ -187,7 +191,52 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const instance = ref(await get(route.params.id).catch(handleError))
|
||||
const offline = ref(!navigator.onLine)
|
||||
window.addEventListener('offline', () => {
|
||||
offline.value = true
|
||||
})
|
||||
window.addEventListener('online', () => {
|
||||
offline.value = false
|
||||
})
|
||||
|
||||
const instance = ref()
|
||||
const modrinthVersions = ref([])
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchInstance() {
|
||||
instance.value = await get(route.params.id).catch(handleError)
|
||||
|
||||
if (!offline.value && instance.value.linked_data && instance.value.linked_data.project_id) {
|
||||
get_project(instance.value.linked_data.project_id, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((project) => {
|
||||
if (project && project.versions) {
|
||||
get_version_many(project.versions, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((versions) => {
|
||||
modrinthVersions.value = versions.sort(
|
||||
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const runningProcesses = await get_by_profile_path(route.params.id).catch(handleError)
|
||||
|
||||
playing.value = runningProcesses.length > 0
|
||||
}
|
||||
|
||||
await fetchInstance()
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async () => {
|
||||
if (route.params.id && route.path.startsWith('/instance')) {
|
||||
await fetchInstance()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
@@ -213,18 +262,8 @@ breadcrumbs.setContext({
|
||||
query: route.query,
|
||||
})
|
||||
|
||||
const offline = ref(!navigator.onLine)
|
||||
window.addEventListener('offline', () => {
|
||||
offline.value = true
|
||||
})
|
||||
window.addEventListener('online', () => {
|
||||
offline.value = false
|
||||
})
|
||||
|
||||
const loadingBar = useLoading()
|
||||
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
const options = ref(null)
|
||||
|
||||
const startInstance = async (context) => {
|
||||
@@ -244,32 +283,6 @@ const startInstance = async (context) => {
|
||||
})
|
||||
}
|
||||
|
||||
const checkProcess = async () => {
|
||||
const runningProcesses = await get_by_profile_path(route.params.id).catch(handleError)
|
||||
|
||||
playing.value = runningProcesses.length > 0
|
||||
}
|
||||
|
||||
// Get information on associated modrinth versions, if any
|
||||
const modrinthVersions = ref([])
|
||||
if (!offline.value && instance.value.linked_data && instance.value.linked_data.project_id) {
|
||||
get_project(instance.value.linked_data.project_id, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((project) => {
|
||||
if (project && project.versions) {
|
||||
get_version_many(project.versions, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((versions) => {
|
||||
modrinthVersions.value = versions.sort(
|
||||
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await checkProcess()
|
||||
|
||||
const stopInstance = async (context) => {
|
||||
playing.value = false
|
||||
await kill(route.params.id).catch(handleError)
|
||||
@@ -354,7 +367,9 @@ const unlistenProfiles = await profile_listener(async (event) => {
|
||||
})
|
||||
|
||||
const unlistenProcesses = await process_listener((e) => {
|
||||
if (e.event === 'finished' && e.profile_path_id === route.params.id) playing.value = false
|
||||
if (e.event === 'finished' && e.profile_path_id === route.params.id) {
|
||||
playing.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const icon = computed(() =>
|
||||
|
||||
@@ -91,7 +91,10 @@
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
href: `/project/${$route.params.id}/versions`,
|
||||
href: {
|
||||
path: `/project/${$route.params.id}/versions`,
|
||||
query: { l: instance?.loader, g: instance?.game_version },
|
||||
},
|
||||
subpages: ['version'],
|
||||
},
|
||||
{
|
||||
@@ -212,13 +215,11 @@ async function fetchProjectData() {
|
||||
|
||||
await fetchProjectData()
|
||||
|
||||
const promo = ref(null)
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async () => {
|
||||
if (route.params.id && route.path.startsWith('/project')) {
|
||||
await fetchProjectData()
|
||||
promo.value.scroll()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -67,12 +67,12 @@
|
||||
<script setup>
|
||||
import { ProjectPageVersions, ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||
import { CheckIcon, DownloadIcon, ExternalIcon, MoreVerticalIcon } from '@modrinth/assets'
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
@@ -107,17 +107,6 @@ const [loaders, gameVersions] = await Promise.all([
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
])
|
||||
|
||||
const filterVersions = ref([])
|
||||
const filterLoader = ref(props.instance ? [props.instance?.loader] : [])
|
||||
const filterGameVersions = ref(props.instance ? [props.instance?.game_version] : [])
|
||||
|
||||
const currentPage = ref(1)
|
||||
|
||||
//watch all the filters and if a value changes, reset to page 1
|
||||
watch([filterVersions, filterLoader, filterGameVersions], () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<img
|
||||
v-if="src"
|
||||
ref="img"
|
||||
:style="`--_size: ${cssSize}`"
|
||||
class="`experimental-styles-within avatar"
|
||||
:style="`--_size: ${cssSize}`"
|
||||
:class="{
|
||||
circle: circle,
|
||||
'no-shadow': noShadow,
|
||||
@@ -18,8 +18,9 @@
|
||||
<svg
|
||||
v-else
|
||||
class="`experimental-styles-within avatar"
|
||||
:style="`--_size: ${cssSize}`"
|
||||
:style="`--_size: ${cssSize}${tint ? `;--_tint:oklch(50% 75% ${tint})` : ''}`"
|
||||
:class="{
|
||||
tint: tint,
|
||||
circle: circle,
|
||||
'no-shadow': noShadow,
|
||||
raised: raised,
|
||||
@@ -44,7 +45,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const pixelated = ref(false)
|
||||
const img = ref(null)
|
||||
@@ -78,6 +79,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tintBy: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const LEGACY_PRESETS = {
|
||||
@@ -97,6 +102,24 @@ function updatePixelated() {
|
||||
pixelated.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const tint = computed(() => {
|
||||
if (props.tintBy) {
|
||||
return hash(props.tintBy) % 360
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
function hash(str) {
|
||||
let hash = 0
|
||||
for (let i = 0, len = str.length; i < len; i++) {
|
||||
const chr = str.charCodeAt(i)
|
||||
hash = (hash << 5) - hash + chr
|
||||
hash |= 0
|
||||
}
|
||||
return hash
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -108,13 +131,14 @@ function updatePixelated() {
|
||||
background-color: var(--color-button-bg);
|
||||
object-fit: contain;
|
||||
border-radius: calc(16 / 96 * var(--_size));
|
||||
position: relative;
|
||||
|
||||
&.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&:not(.no-shadow) {
|
||||
box-shadow: var(--shadow-inset-lg), var(--shadow-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
&.no-shadow {
|
||||
@@ -128,5 +152,9 @@ function updatePixelated() {
|
||||
&.raised {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
&.tint {
|
||||
background-color: color-mix(in oklch, var(--color-button-bg) 100%, var(--_tint) 5%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,6 +50,7 @@ export { default as Modal } from './modal/Modal.vue'
|
||||
export { default as ConfirmModal } from './modal/ConfirmModal.vue'
|
||||
export { default as ShareModal } from './modal/ShareModal.vue'
|
||||
export { default as TabbedModal } from './modal/TabbedModal.vue'
|
||||
export type { Tab as TabbedModalTab } from './modal/TabbedModal.vue'
|
||||
|
||||
// Navigation
|
||||
export { default as Breadcrumbs } from './nav/Breadcrumbs.vue'
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useVIntl, type MessageDescriptor } from '@vintl/vintl'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
type Tab<Props> = {
|
||||
export type Tab<Props> = {
|
||||
name: MessageDescriptor
|
||||
icon: Component
|
||||
content: Component<Props>
|
||||
@@ -12,7 +12,8 @@ type Tab<Props> = {
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
tabs: Tab<unknown>[]
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tabs: Tab<any>[]
|
||||
}>()
|
||||
|
||||
const selectedTab = ref(0)
|
||||
|
||||
Reference in New Issue
Block a user