feat: app onboarding and library (#6923)

* refactor: remove unused legacy onboarded flag

* sqlx prepare

* feat: add onboarding checklist app db and add vue provider

* feat: add welcome page

* feat: add onboarding checklist component

* feat: move checklist visibility source of truth in app db

* feat: split up import modpack button

* feat-small: style tweak

* feat: remove discover section from homepage and add library

* refactor: move welcome screen to component

* refactor: move grid display into /library

* refactor: rename existing components

* refactor: split up the current library page files

* feat: add create group modal

* feat: add instance group definitions

* feat: update accordian for instance groups

* feat: add delete group

* feat: implement rename instance groups

* refactor: move instance group api out

* feat: add context menu for groups

* feat: style instance items in library

* feat: add selected instance item state

* feat: implement action bar for selection

* feat: drag and drop to move between groups

* feat: show ungrouped with header

* feat: improve edit group name

* small fix

* small style fixes

* feat: improve selection behaviour

* fix: dont toggle group when context menu open

* feat: drag and drop multiple selected

* small

* feat: add remove from group in action bar

* feat: add delay on icon hiding in inline input

* feat: improve icon hiding delay

* fix: bad routes.js

* feat: implement sort and filter options

* refactor: dont prefix filenames

* feat: change open/collapse group target

* feat: improve ungrouped behaviour

* feat: add shift to select multiple instances

* fix: icon disappear transition

* feat: add esc while dragging to cancel

* feat: update sorting and moved label

* feat: update context menu

* fix: multiple context menus can be opened

* refactor: instance groups to be identified by ID instead of name, names become display data

* feat: creating new group always adds instance

* feat: create group button, fix instance selection, and increase group name max length

* feat: strokes

* fix: show getting started checklist in all sidebar

* feat: set size on microsoft login screen

* fix: use ButtonStyled in Chips component

* feat: creating groups with selected instances and fix some moving selection and duplication bugs

* pnpm prepr

* feat: add tooltip for instance already in group

* feat: create instance with content in creation flow modal

- search modpack becomes search projects
- creation flow modal context receives prepareProjectInstall and createProjectInstall callbacks, from content-install.ts (shared with ContentInstallModal)

* feat: update instance card styles

* refactor: separate out instance card content

* update styles

* refactor: instance card

* fix: width of instance

* feat: update empty state

* feat: update sort and grouping dropdowns

* feat: better context menu options

* feat: add group action buttons

* feat: add/remove favourites group

* remove manual sort

* feat: improve the "New group" jank

* feat: add group by instance type and change group sorting

* feat: welcome screen styles match figma

* feat: not signed in skins demo

* feat: new hosting empty state

* feat: new instance card style

* feat: add tooltip and spice up animation more

* feat: improve the transitions for groups after dropping many instances

* feat: set instance group membership for bulk moving

* feat: remove fade out

* fix: allow dragging from buttons inside instance card

* fix: library page load in behaviour

* small fix

* feat: add group ordering and reordering

* feat: add drag to reorder groups

* fix: switching page not clearing selected instances

* style fixes

* style fix

* fix spacing

* feat: jump in changes

* feat: add styled newly created instances

* feat: improve jump in cards styles

* fix: welcome screen style

* fix: empty state styles

* feat: max 5 jump in items

* feat: max new instances to 3

* fix: search icon

* feat: clear selection after move

* feat: skins list use check icon styles

* feat: list will be deleted instances and default sort last played

* fix: stroke

* remove: no more group settings in instance general settings

* fix: resolving modpack server install

* feat: icon editor

* feat: add three hue shifted variants

* fix: surprise me doesnt pick symbol

* feat: add customize icon to create instance modal

* feat: skins loading state style

* fix: welcome logo

* fix: copy

* fix: left over usage of ButtonStyled from main's refactor

* feat: some leaked merge stuff

* feat: server empty state styles

* remove: instance helper text

* remove: auto link in set up stage

* feat: auto focus on search when modal open

* fix: delete instance modal style

* remove: modpack meta repeating

* fix: icon

* fix: font weight

* fix: dropdown filter preview heights

* pnpm prepr

* fix: sign into modrinth flashing change in position

* feat: add avatar transparent corner gets padding

* feat: drag and hold over group opens group

* feat: add random icon for custom instance

* feat: update loading icon

* polish: empty state for search

* polish: jump in cards

* fix: avatar could skip checking for image if tauri loads it first

* feat: add icon customize and randomizer in content install modal for add to new instances

* fix: avatar cannot read image pixels due to cors

* refactor: popup notifications toast type for instance sharing notifications

* feat: apply random icons modal and add popup notification for it

* polish: style

* fix: set_webview_visible

* QA

* qa

* feat: add inner white 15 opacity stroke around avatar

* add to catalog

* pnpm prepr

* fix: app event import

* feat: add new icons and backgrounds for icon editor

* feat: recents list generate from randomized icons and save icon in editor + icon editor modal fixes

* qa

* refactor: recipe -> config

* fix: event bus

* polish

* feat: add safe guard against randomizing empty icons

* feat: navigate to instance after creation

* fix: icon file names

* i18n on welcome screen

* i18n pass on PR

* fix: inconsistent max group name length

* refactor: use sqlx macro

* feat: add instance icon background selection drag

* feat: update catalog order

* fix: remove gradient for welcome screen

* remove: frog

* fix: min width

* feat: update corner size

* fix: context menu styles

* prepr

* fix: instance name trimming

* reorder

* fix: instance card loading state stayed until hovering

* feat: add icon randomizer blacklist

* blog

* prepr

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Truman Gao
2026-08-18 01:08:09 +00:00
committed by GitHub
co-authored by Prospector
parent d5556271f9
commit e79e0d024b
347 changed files with 12456 additions and 4505 deletions
@@ -9,7 +9,7 @@
:initial-loader="initialLoader"
:initial-game-version="initialGameVersion"
:fade="props.initialSetup ? undefined : 'danger'"
:search-modpacks="searchModpacks"
:search-projects="searchModpacks"
:get-project-versions="getProjectVersions"
:finish-disabled="!canCompleteSetup"
:finish-disabled-tooltip="!canCompleteSetup ? permissionDeniedMessage : undefined"
@@ -6,7 +6,7 @@ export type {
Gamemode,
GeneratorSettingsMode,
LoaderVersionType,
ModpackSearchResult,
ProjectSearchResult,
SetupType,
} from '../../flows/creation-flow-modal/creation-flow-context'
export { default as CreationFlowModal } from '../../flows/creation-flow-modal/index.vue'
Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

@@ -1,139 +1,74 @@
<template>
<div class="grid grid-cols-2 gap-8 items-center justify-center py-10 max-w-[760px]">
<!-- Left column -->
<div class="flex flex-col gap-8 items-start pr-8 shrink-0">
<!-- Heading -->
<div class="flex flex-col gap-2 items-start w-[300px]">
<p class="text-3xl leading-9 font-semibold text-contrast">
{{ formatMessage(messages.modrinthHostingLabel) }}
</p>
<p class="text-base font-normal text-primary">
{{ formatMessage(messages.noServersDescription) }}
</p>
</div>
<!-- Feature list -->
<div class="flex flex-col gap-4 items-start w-full">
<div class="flex gap-3 items-start">
<div
class="bg-surface-4 border border-solid border-surface-5 rounded-full shrink-0 size-8 flex items-center justify-center"
>
<PackageOpenIcon class="size-5 text-secondary" aria-hidden="true" />
<div class="flex w-full max-w-[56rem] h-full flex-col items-stretch justify-between gap-8 py-6">
<div class="flex w-full flex-wrap grow items-center justify-center gap-2">
<div class="mx-auto flex w-full max-w-[20rem] flex-col items-start gap-8">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3">
<h1 class="m-0 text-3xl font-semibold leading-9 text-contrast">
{{ formatMessage(messages.modrinthHostingLabel) }}
</h1>
</div>
<div class="flex flex-col gap-0.5">
<p class="text-base font-semibold text-contrast">
{{ formatMessage(messages.oneClickModInstallsTitle) }}
</p>
<p class="text-base font-normal text-primary">
{{ formatMessage(messages.oneClickModInstallsDescription) }}
</p>
<p class="m-0 text-base text-primary">
{{ formatMessage(messages.noServersDescription) }}
</p>
</div>
<div class="flex w-full flex-col gap-6">
<div v-for="feature in features" :key="feature.key" class="flex items-start gap-4">
<div
class="feature-icon relative flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-[0.875rem] border border-solid bg-surface-1 text-brand"
>
<div class="feature-icon-gradient absolute left-[-1px] top-[-1px] size-[6.25rem]" />
<div class="feature-icon-shade absolute left-[-1px] top-[-1px] size-[6.25rem]" />
<img
:src="iconTexture"
alt=""
class="absolute left-1/2 top-1/2 h-[6.25rem] w-[9.8125rem] max-w-none -translate-x-1/2 -translate-y-1/2 object-cover opacity-40 mix-blend-luminosity"
/>
<component
:is="feature.icon"
class="feature-icon-glyph relative size-5"
aria-hidden="true"
/>
</div>
<div class="flex min-w-0 flex-col gap-0.5">
<p class="m-0 text-lg font-semibold leading-6 text-contrast">
{{ feature.title }}
</p>
<p class="m-0 text-sm leading-5 text-primary">
{{ feature.description }}
</p>
</div>
</div>
</div>
<div class="flex gap-3 items-start">
<div
class="bg-surface-4 border border-solid border-surface-5 rounded-full shrink-0 size-8 flex items-center justify-center overflow-hidden"
>
<GlobeIcon class="size-5 text-secondary" aria-hidden="true" />
</div>
<div class="flex flex-col gap-0.5">
<p class="text-base font-semibold text-contrast">
{{ formatMessage(messages.simpleSetupTitle) }}
</p>
<p class="text-base font-normal text-primary">
{{ formatMessage(messages.simpleSetupDescription) }}
</p>
</div>
</div>
<div class="flex gap-3 items-start">
<div
class="bg-surface-4 border border-solid border-surface-5 rounded-full shrink-0 size-8 flex items-center justify-center overflow-hidden"
>
<UsersIcon class="size-5 text-secondary" aria-hidden="true" />
</div>
<div class="flex flex-col gap-0.5">
<p class="text-base font-semibold text-contrast">
{{ formatMessage(messages.playWithFriendsTitle) }}
</p>
<p class="text-base font-normal text-primary">
{{ formatMessage(messages.playWithFriendsDescription) }}
</p>
</div>
</div>
</div>
<!-- CTA section -->
<div class="flex flex-col gap-6 items-start">
<div class="flex flex-col gap-3 items-start">
<Button type="colored" color="brand" @click="onClickNewServer?.()">
<div class="flex flex-wrap items-center gap-4">
<Button type="colored" color="brand" size="lg" @click="onClickNewServer?.()">
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.newServerButton) }}
</Button>
<AutoLink
to="https://modrinth.com/hosting"
target="_blank"
class="flex items-center gap-1 hover:brightness-125"
class="flex items-center gap-1 hover:brightness-125 font-semibold"
>
{{ formatMessage(messages.learnMoreLink) }}
<RightArrowIcon class="size-5 shrink-0" aria-hidden="true" />
</AutoLink>
</div>
<template v-if="!loggedIn">
<div class="h-px w-full bg-surface-5" />
<div class="flex gap-3 items-center flex-wrap">
<p class="text-base font-normal text-primary">
{{ formatMessage(messages.alreadyHaveServerLabel) }}
</p>
<Button @click="onClickSignIn?.()">
<LogInIcon aria-hidden="true" />
{{ formatMessage(messages.signInButton) }}
</Button>
</div>
</template>
</div>
<ServerListEmptyPreview />
</div>
<!-- Right column - mod icon grid -->
<div
class="relative flex h-[617px] shrink-0 items-center justify-center overflow-hidden rounded-[40px] pointer-events-none select-none [mask-image:linear-gradient(to_bottom,black_0%,black_35%,transparent_100%)] [-webkit-mask-image:linear-gradient(to_bottom,black_0%,black_35%,transparent_100%)]"
>
<div class="rotate-[15deg]">
<div class="flex flex-col gap-4">
<div
v-for="row in GRID_ROWS"
:key="row"
class="flex gap-4 items-center shrink-0"
:class="animated ? (row % 2 === 1 ? 'drift-left' : 'drift-right relative left-14') : ''"
>
<div class="hidden drift-right drift-left"></div>
<div
v-for="col in GRID_COLS"
:key="col"
class="border border-surface-5 rounded-[20px] shrink-0 size-[112px] bg-surface-4 overflow-hidden"
>
<img :src="getGridImage(row - 1, col - 1)" alt="" class="size-full object-cover" />
</div>
<div
v-for="col in GRID_COLS"
:key="col"
class="border border-surface-5 rounded-[20px] shrink-0 size-[112px] bg-surface-4 overflow-hidden"
>
<img :src="getGridImage(row - 1, col - 1)" alt="" class="size-full object-cover" />
</div>
<div
v-for="col in GRID_COLS"
:key="col"
class="border border-surface-5 rounded-[20px] shrink-0 size-[112px] bg-surface-4 overflow-hidden"
>
<img :src="getGridImage(row - 1, col - 1)" alt="" class="size-full object-cover" />
</div>
</div>
</div>
</div>
<div v-if="!loggedIn" class="flex flex-col items-center gap-4 text-center">
<p class="m-0 text-sm text-secondary">
{{ formatMessage(messages.alreadyHaveServerLabel) }}
</p>
<Button @click="onClickSignIn?.()">
<LogInIcon aria-hidden="true" />
{{ formatMessage(messages.signInButton) }}
</Button>
</div>
</div>
</template>
@@ -147,46 +82,21 @@ import {
RightArrowIcon,
UsersIcon,
} from '@modrinth/assets'
import { AutoLink } from '@modrinth/ui'
import { computed } from 'vue'
import iconTexture from '#ui/assets/welcome/icon-texture.png'
import AutoLink from '#ui/components/base/AutoLink.vue'
import { Button } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import imgAircraft from './grid-images/aircraft.png'
import imgAlexs from "./grid-images/alex's.png"
import imgArtifacts from './grid-images/artifacts.png'
import imgBiomes from './grid-images/biomes.png'
import imgCatac from './grid-images/catac.png'
import imgCobble from './grid-images/cobble.png'
import imgComforts from './grid-images/comforts.png'
import imgCreate from './grid-images/create.png'
import imgCreate1 from './grid-images/create1.png'
import imgCreate2 from './grid-images/create2.png'
import imgCreate3 from './grid-images/create3.png'
import imgCreeper from './grid-images/creeper.png'
import imgFriends from './grid-images/friends.png'
import imgGeo from './grid-images/geo.png'
import imgNaturalist from './grid-images/naturalist.png'
import imgSeasons from './grid-images/seasons.png'
import imgTravellers from './grid-images/travellers.png'
import imgTree from './grid-images/tree.png'
import imgYum1 from './grid-images/yum1.png'
import imgYum2 from './grid-images/yum2.png'
import imgYum3 from './grid-images/yum3.png'
import imgYung from './grid-images/yung.png'
import ServerListEmptyPreview from './ServerListEmptyPreview.vue'
withDefaults(
defineProps<{
animated?: boolean
onClickNewServer?: () => void
onClickSignIn?: () => void
loggedIn?: boolean
}>(),
{ animated: false },
)
defineProps<{
onClickNewServer?: () => void
onClickSignIn?: () => void
loggedIn?: boolean
}>()
const GRID_ROWS = 6
const GRID_COLS = 5
const { formatMessage } = useVIntl()
const messages = defineMessages({
@@ -194,10 +104,6 @@ const messages = defineMessages({
id: 'servers.list-empty.modrinth-hosting-label',
defaultMessage: 'Modrinth Hosting',
},
noServersTitle: {
id: 'servers.list-empty.no-servers-title',
defaultMessage: 'No servers yet',
},
noServersDescription: {
id: 'servers.list-empty.no-servers-description',
defaultMessage: 'Install mods, invite friends, and play together all from the Modrinth App.',
@@ -224,7 +130,7 @@ const messages = defineMessages({
},
playWithFriendsDescription: {
id: 'servers.list-empty.play-with-friends-description',
defaultMessage: 'Invite friends and get them set up right in the Modrinth App.',
defaultMessage: 'Invite friends to play your server with one-click play from the Modrinth App.',
},
newServerButton: {
id: 'servers.list-empty.new-server-button',
@@ -232,7 +138,7 @@ const messages = defineMessages({
},
learnMoreLink: {
id: 'servers.list-empty.learn-more-link',
defaultMessage: 'Learn more about Modrinth Hosting',
defaultMessage: 'Learn more',
},
alreadyHaveServerLabel: {
id: 'servers.list-empty.already-have-server-label',
@@ -240,70 +146,55 @@ const messages = defineMessages({
},
signInButton: {
id: 'servers.list-empty.sign-in-button',
defaultMessage: 'Sign in',
defaultMessage: 'Sign in to Modrinth',
},
})
const GRID_IMAGES = [
imgYum1,
imgYum2,
imgYum3,
imgYung,
imgCreeper,
imgFriends,
imgNaturalist,
imgBiomes,
imgCatac,
imgCobble,
imgGeo,
imgCreate,
imgCreate1,
imgCreate2,
imgCreate3,
imgAircraft,
imgArtifacts,
imgComforts,
imgTravellers,
imgAlexs,
imgSeasons,
imgTree,
]
function getGridImage(row: number, col: number): string {
return GRID_IMAGES[(row * GRID_COLS + col) % GRID_IMAGES.length]
}
const features = computed(() => [
{
key: 'one-click-mod-installs',
icon: PackageOpenIcon,
title: formatMessage(messages.oneClickModInstallsTitle),
description: formatMessage(messages.oneClickModInstallsDescription),
},
{
key: 'simple-setup',
icon: GlobeIcon,
title: formatMessage(messages.simpleSetupTitle),
description: formatMessage(messages.simpleSetupDescription),
},
{
key: 'play-with-friends',
icon: UsersIcon,
title: formatMessage(messages.playWithFriendsTitle),
description: formatMessage(messages.playWithFriendsDescription),
},
])
</script>
<style scoped>
p {
margin: 0;
.feature-icon {
border-color: color-mix(in srgb, var(--color-text-primary) 10%, transparent);
box-shadow:
0 0 0 1px color-mix(in srgb, var(--color-brand) 30%, var(--surface-1)),
var(--shadow-card),
0 0 3.75rem color-mix(in srgb, var(--color-brand) 10%, transparent);
}
@keyframes drift-right {
from {
transform: translateX(-33%);
}
to {
transform: translateX(33%);
}
.feature-icon-gradient {
background: linear-gradient(180deg, var(--color-green-800) 0%, var(--color-green-950) 100%);
opacity: 0.5;
}
@keyframes drift-left {
from {
transform: translateX(33%);
}
to {
transform: translateX(-33%);
}
.feature-icon-shade {
background: linear-gradient(
-14deg,
color-mix(in srgb, var(--color-green-950) 37%, transparent) 8%,
transparent 86%
);
}
.drift-left {
animation: drift-left linear infinite alternate;
animation-duration: 400s;
}
.drift-right {
animation: drift-right linear infinite alternate;
animation-duration: 400s;
.feature-icon-glyph {
filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--color-green-950) 12%, transparent));
}
</style>
@@ -0,0 +1,245 @@
<template>
<div
inert
aria-hidden="true"
class="relative mx-auto h-[38rem] w-full max-w-[25rem] select-none -mb-10"
>
<div
class="absolute inset-x-0 top-0 h-full overflow-hidden rounded-2xl border border-solid border-surface-3 bg-surface-2 shadow-card"
>
<div
class="flex items-center justify-between border-0 border-b border-solid border-surface-3 p-4"
>
<div class="flex items-center gap-2 text-contrast">
<UserPlusIcon class="size-5 text-primary" />
<span class="text-lg font-semibold">{{ formatMessage(messages.heading) }}</span>
</div>
<XIcon class="size-5 text-secondary" />
</div>
<div class="flex gap-2 border-0 border-b border-solid border-surface-3 p-4">
<div
class="flex h-9 min-w-0 flex-1 items-center gap-2 rounded-xl bg-surface-3 px-3 text-sm text-secondary"
>
<SearchIcon class="size-4 shrink-0" />
<span class="truncate text-secondary">{{
formatMessage(messages.searchPlaceholder)
}}</span>
</div>
<Button type="colored" color="brand" class="!cursor-default" disabled>
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.addButton) }}
</Button>
</div>
<div class="bg-surface-1 py-3">
<p class="m-0 pb-2 text-sm font-semibold text-primary px-4">
{{ formatMessage(messages.friendsHeading, { count: totalFriendCount }) }}
</p>
<div
v-for="(friend, index) in friends"
:key="friend.username"
class="relative flex h-11 items-center justify-between gap-3 px-4"
:class="[index > 4 ? 'opacity-40' : '', friend.showPointer ? 'bg-surface-2' : '']"
>
<div class="flex min-w-0 items-center gap-2">
<div class="relative flex shrink-0">
<Avatar :src="friend.avatarUrl" size="1.5rem" class="!border-none" circle no-shadow />
<span
v-if="friend.presence"
class="absolute bottom-0 right-[-1px] size-2 rounded-full border border-solid border-surface-1"
:class="friend.presence === 'online' ? 'bg-brand' : 'bg-blue'"
/>
</div>
<span class="truncate text-base font-medium text-primary">{{ friend.username }}</span>
</div>
<Button
:type="friend.status === 'cancel' ? 'outlined' : 'base'"
class="!cursor-default !font-medium !h-8"
size="md"
:disabled="friend.status === 'added'"
:class="friend.status === 'added' ? '' : 'w-20'"
>
<CheckIcon v-if="friend.status === 'added'" aria-hidden="true" />
{{ friendStatusLabel(friend.status) }}
</Button>
<div
v-if="friend.showPointer"
class="absolute right-[14.25rem] top-9 z-10 flex size-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 opacity-75"
>
<img :src="pointerIcon" alt="" class="size-4" />
</div>
</div>
</div>
<div
class="absolute inset-x-0 bottom-0 border-0 border-t border-solid border-surface-3 bg-surface-2 p-4"
>
<p class="m-0 pb-2 text-base font-semibold text-contrast">
{{ formatMessage(messages.inviteLinkHeading) }}
</p>
<div
class="flex h-8 items-center justify-between gap-2 rounded-xl bg-surface-1 px-2.5 text-sm font-medium text-primary"
>
<span class="truncate">https://modrinth.com/server/abc123</span>
<ClipboardCopyIcon class="size-4 shrink-0 text-secondary" />
</div>
</div>
</div>
<div
class="pointer-events-none absolute inset-x-0 bottom-0 h-[28rem] [background:linear-gradient(to_bottom,transparent,var(--surface-1))]"
/>
<div
class="absolute left-[32%] top-[23rem] z-10 flex w-[21rem] max-w-[calc(100%-1rem)] gap-4 !overflow-hidden rounded-2xl border border-solid border-surface-4 bg-surface-2 px-4 py-3 shadow-card"
>
<div
class="absolute top-[16px] left-[16px] size-[20px] rounded-full bg-contrast opacity-75 blur-xl"
/>
<div class="relative h-max">
<Avatar :src="geometricallyAvatar" size="2.25rem" circle no-shadow />
<span
class="absolute bottom-0 right-[-1px] size-3 rounded-full border-2 border-solid border-surface-2 bg-brand"
/>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-start gap-1">
<div class="min-w-0 flex-1 text-base leading-5 text-primary">
<p class="m-0">
<span class="font-medium text-contrast">Geometrically</span>
{{ formatMessage(messages.invitedYouTo) }}
</p>
<p class="m-0 flex items-center gap-1">
<Avatar :src="modrinthSmpIcon" size="1.25rem" no-shadow />
<span class="font-medium text-contrast">Modrinth SMP</span>
<span>{{ formatMessage(messages.serverSuffix) }}</span>
</p>
</div>
<XIcon class="size-5 shrink-0 text-secondary" />
</div>
<div class="mt-2.5 flex gap-2">
<Button type="colored" color="brand" class="!cursor-default !font-medium">
{{ formatMessage(messages.acceptButton) }}
</Button>
<Button type="outlined" class="!cursor-default !font-medium">
{{ formatMessage(messages.declineButton) }}
</Button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
CheckIcon,
ClipboardCopyIcon,
PlusIcon,
SearchIcon,
UserPlusIcon,
XIcon,
} from '@modrinth/assets'
import borisAvatar from '#ui/assets/servers/server-list-empty/boris.png'
import fetchAvatar from '#ui/assets/servers/server-list-empty/fetch.png'
import geometricallyAvatar from '#ui/assets/servers/server-list-empty/geometrically.png'
import imb11Avatar from '#ui/assets/servers/server-list-empty/imb11.png'
import joshAvatar from '#ui/assets/servers/server-list-empty/josh.png'
import michaelAvatar from '#ui/assets/servers/server-list-empty/michael.png'
import modrinthSmpIcon from '#ui/assets/servers/server-list-empty/modrinth-smp.png'
import prospectorAvatar from '#ui/assets/servers/server-list-empty/prospector.png'
import sayaAvatar from '#ui/assets/servers/server-list-empty/saya.png'
import trumanAvatar from '#ui/assets/servers/server-list-empty/truman.png'
import Avatar from '#ui/components/base/Avatar.vue'
import Button from '#ui/components/base/buttons/Button.vue'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import pointerIcon from './Pointer.png'
type FriendStatus = 'added' | 'cancel' | 'invite'
type FriendPresence = 'online' | 'playing'
const { formatMessage } = useVIntl()
const messages = defineMessages({
heading: {
id: 'sharing.invite-players-modal.heading',
defaultMessage: 'Invite players',
},
searchPlaceholder: {
id: 'sharing.invite-players-modal.search-placeholder',
defaultMessage: 'Enter Modrinth username',
},
addButton: {
id: 'sharing.invite-players-modal.add',
defaultMessage: 'Add',
},
friendsHeading: {
id: 'sharing.invite-players-modal.friends-heading',
defaultMessage: 'Your friends - {count}',
},
addedButton: {
id: 'sharing.invite-players-modal.added',
defaultMessage: 'Added',
},
cancelButton: {
id: 'sharing.invite-players-modal.cancel',
defaultMessage: 'Cancel',
},
inviteButton: {
id: 'sharing.invite-players-modal.invite',
defaultMessage: 'Invite',
},
inviteLinkHeading: {
id: 'sharing.invite-players-modal.invite-link-heading',
defaultMessage: 'Or use an invite link',
},
invitedYouTo: {
id: 'sharing.invite-players-toast.invited-you-to',
defaultMessage: 'invited you to',
},
serverSuffix: {
id: 'sharing.invite-players-toast.server-suffix',
defaultMessage: 'server.',
},
acceptButton: {
id: 'sharing.invite-players-toast.accept',
defaultMessage: 'Accept',
},
declineButton: {
id: 'sharing.invite-players-toast.decline',
defaultMessage: 'Decline',
},
})
const friends: Array<{
username: string
avatarUrl: string
status: FriendStatus
presence?: FriendPresence
showPointer?: boolean
}> = [
{ username: 'Josh', avatarUrl: joshAvatar, status: 'added' },
{
username: 'Prospector',
avatarUrl: prospectorAvatar,
status: 'invite',
presence: 'online',
showPointer: true,
},
{ username: 'Fetch', avatarUrl: fetchAvatar, status: 'cancel', presence: 'playing' },
{ username: 'IMB11', avatarUrl: imb11Avatar, status: 'invite' },
{ username: 'Truman', avatarUrl: trumanAvatar, status: 'invite' },
{ username: 'Boris', avatarUrl: borisAvatar, status: 'invite' },
{ username: 'Saya', avatarUrl: sayaAvatar, status: 'invite' },
{ username: 'Michael', avatarUrl: michaelAvatar, status: 'invite' },
]
const totalFriendCount = 11
function friendStatusLabel(status: FriendStatus): string {
if (status === 'added') return formatMessage(messages.addedButton)
if (status === 'cancel') return formatMessage(messages.cancelButton)
return formatMessage(messages.inviteButton)
}
</script>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB