mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
feat: migrate all headers to use PageHeader shared component.
This commit is contained in:
@@ -194,6 +194,15 @@
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "No available versions match {compatibilityLabel}. Select a version to install anyway. Dependencies will not be installed automatically."
|
||||
},
|
||||
"app.browse.server.world-fallback-name": {
|
||||
"message": "Instance"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Installing modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Enter modpack description..."
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"
|
||||
>
|
||||
<template #default="{ onReinstall, onReinstallFailed }">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<RouterView v-slot="{ Component }" :route="managedRoute">
|
||||
<template v-if="Component">
|
||||
<Suspense>
|
||||
<component
|
||||
@@ -46,7 +46,7 @@ import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import { injectAuth, injectModrinthClient, ServersManageRootLayout } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, watch } from 'vue'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { get_user } from '@/helpers/cache'
|
||||
@@ -60,10 +60,23 @@ const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const serverId = computed(() => {
|
||||
const rawId = route.params.id
|
||||
return Array.isArray(rawId) ? rawId[0] : (rawId ?? '')
|
||||
})
|
||||
const managedRoute = shallowRef(router.currentRoute.value)
|
||||
const serverId = ref(getRouteParam(managedRoute.value.params.id) ?? '')
|
||||
|
||||
watch(
|
||||
router.currentRoute,
|
||||
(nextRoute) => {
|
||||
if (!nextRoute.path.startsWith('/hosting/manage/')) return
|
||||
managedRoute.value = nextRoute
|
||||
serverId.value = getRouteParam(nextRoute.params.id) ?? ''
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function getRouteParam(param: string | string[] | undefined): string | null {
|
||||
if (Array.isArray(param)) return param[0] ?? null
|
||||
return param ?? null
|
||||
}
|
||||
|
||||
if (serverId.value) {
|
||||
try {
|
||||
|
||||
@@ -13,220 +13,57 @@
|
||||
@unlinked="fetchInstance"
|
||||
/>
|
||||
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar
|
||||
:src="icon ? icon : undefined"
|
||||
:alt="instance.name"
|
||||
size="64px"
|
||||
:tint-by="instance.id"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
{{ instance.name }}
|
||||
</template>
|
||||
<template #stats>
|
||||
<PageHeader
|
||||
:header="instance.name"
|
||||
:leading="instanceHeaderLeading"
|
||||
:metadata="instanceHeaderMetadata"
|
||||
:actions="instanceHeaderActions"
|
||||
>
|
||||
<template #metadata-server-details>
|
||||
<div class="flex items-center flex-wrap gap-2">
|
||||
<template v-if="!isServerInstance">
|
||||
<div class="flex min-w-0 items-center gap-2 font-medium text-secondary text-nowrap">
|
||||
<Gamepad2Icon class="flex size-5 shrink-0" aria-hidden="true" />
|
||||
<span class="truncate">{{ instance.game_version }}</span>
|
||||
</div>
|
||||
|
||||
<BulletDivider />
|
||||
|
||||
<div class="flex min-w-0 items-center gap-2 font-medium text-secondary text-nowrap">
|
||||
<ServerLoaderIcon
|
||||
v-if="loaderDisplayName"
|
||||
:loader="loaderDisplayName"
|
||||
class="flex size-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="truncate">{{ loaderLabel }}</span>
|
||||
</div>
|
||||
|
||||
<template v-if="showInstancePlayTime">
|
||||
<BulletDivider />
|
||||
|
||||
<div class="flex min-w-0 items-center gap-2 font-medium text-secondary text-nowrap">
|
||||
<TimerIcon class="flex size-5 shrink-0" aria-hidden="true" />
|
||||
<span class="truncate">
|
||||
{{ playtimeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<template v-if="loadingServerPing">
|
||||
<ServerOnlinePlayers
|
||||
v-if="playersOnline !== undefined"
|
||||
:online="playersOnline"
|
||||
:status-online="statusOnline"
|
||||
hide-label
|
||||
/>
|
||||
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
|
||||
<div
|
||||
v-if="
|
||||
(playersOnline !== undefined || recentPlays !== undefined) &&
|
||||
(minecraftServer?.region || ping)
|
||||
"
|
||||
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
||||
></div>
|
||||
<ServerPing v-if="ping" :ping="ping" />
|
||||
</template>
|
||||
|
||||
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
|
||||
|
||||
<template v-if="loadingServerPing">
|
||||
<ServerOnlinePlayers
|
||||
v-if="playersOnline !== undefined"
|
||||
:online="playersOnline"
|
||||
:status-online="statusOnline"
|
||||
hide-label
|
||||
/>
|
||||
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
|
||||
<div
|
||||
v-if="minecraftServer?.region || ping"
|
||||
v-if="
|
||||
(playersOnline !== undefined || recentPlays !== undefined) &&
|
||||
(minecraftServer?.region || ping)
|
||||
"
|
||||
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="linkedProjectV3"
|
||||
class="flex gap-1.5 items-center font-medium text-primary"
|
||||
>
|
||||
Linked to
|
||||
<Avatar
|
||||
:src="linkedProjectV3.icon_url"
|
||||
:alt="linkedProjectV3.name"
|
||||
:tint-by="instance.id"
|
||||
size="24px"
|
||||
/>
|
||||
<router-link
|
||||
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
|
||||
class="hover:underline text-primary truncate"
|
||||
>
|
||||
{{ linkedProjectV3.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
<ServerPing v-if="ping" :ping="ping" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-if="
|
||||
[
|
||||
'installing',
|
||||
'pack_installing',
|
||||
'pack_installed',
|
||||
'not_installed',
|
||||
'minecraft_installing',
|
||||
].includes(instance.install_stage)
|
||||
"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button disabled>Installing...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="instance.install_stage !== 'installed'"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button @click="repairInstance()">
|
||||
<DownloadIcon />
|
||||
Repair
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="playing === true" color="red" size="large">
|
||||
<button :disabled="stopping" @click="stopInstance('InstancePage')">
|
||||
<StopCircleIcon />
|
||||
{{ stopping ? 'Stopping...' : 'Stop' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="playing === false && loading === false && !isServerInstance"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button @click="startInstance('InstancePage')">
|
||||
<PlayIcon />
|
||||
Play
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div
|
||||
v-else-if="playing === false && loading === false && isServerInstance"
|
||||
class="joined-buttons"
|
||||
>
|
||||
<ButtonStyled color="brand" size="large">
|
||||
<button @click="handlePlayServer()">
|
||||
<PlayIcon />
|
||||
Play
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand" size="large">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'join_server',
|
||||
action: () => handlePlayServer(),
|
||||
},
|
||||
{
|
||||
id: 'launch_instance',
|
||||
action: () => startInstance('InstancePage'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="w-0 text-xl relative top-0.5 right-2.5">
|
||||
<DropdownIcon />
|
||||
</div>
|
||||
|
||||
<template #join_server>
|
||||
<PlayIcon />
|
||||
Join server
|
||||
</template>
|
||||
<template #launch_instance>
|
||||
<PlayIcon />
|
||||
Launch instance
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled
|
||||
v-else-if="loading === true && playing === false"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button disabled>Starting...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large">
|
||||
<button v-tooltip="'Instance settings'" @click="settingsModal?.show()">
|
||||
<SettingsIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent" circular size="large">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'open-folder',
|
||||
action: () => {
|
||||
if (instance) showInstanceInFolder(instance.id)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'export-mrpack',
|
||||
action: () => exportModal?.show(),
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
action: () => createShortcut(),
|
||||
},
|
||||
]"
|
||||
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
|
||||
|
||||
<div
|
||||
v-if="minecraftServer?.region || ping"
|
||||
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
||||
></div>
|
||||
|
||||
<div v-if="linkedProjectV3" class="flex gap-1.5 items-center font-medium text-primary">
|
||||
Linked to
|
||||
<Avatar
|
||||
:src="linkedProjectV3.icon_url"
|
||||
:alt="linkedProjectV3.name"
|
||||
:tint-by="instance.id"
|
||||
size="24px"
|
||||
/>
|
||||
<router-link
|
||||
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
|
||||
class="hover:underline text-primary truncate"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #share-instance> <UserPlusIcon /> Share instance </template>
|
||||
<template #host-a-server> <ServerIcon /> Create a server </template>
|
||||
<template #open-folder> <FolderOpenIcon /> Open folder </template>
|
||||
<template #export-mrpack> <PackageIcon /> Export modpack </template>
|
||||
<template #create-shortcut> <ExternalIcon /> Create shortcut </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
{{ linkedProjectV3.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
</PageHeader>
|
||||
</div>
|
||||
<div :class="['px-6', { 'shrink-0': isFixedRender }]">
|
||||
<NavTabs :links="tabs" />
|
||||
@@ -286,7 +123,6 @@ import {
|
||||
CheckCircleIcon,
|
||||
ClipboardCopyIcon,
|
||||
DownloadIcon,
|
||||
DropdownIcon,
|
||||
EditIcon,
|
||||
ExternalIcon,
|
||||
EyeIcon,
|
||||
@@ -297,26 +133,21 @@ import {
|
||||
PackageIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
ServerIcon,
|
||||
SettingsIcon,
|
||||
StopCircleIcon,
|
||||
TagCategoryGamepad2Icon as Gamepad2Icon,
|
||||
TerminalSquareIcon,
|
||||
TimerIcon,
|
||||
UpdatedIcon,
|
||||
UserPlusIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
BulletDivider,
|
||||
ButtonStyled,
|
||||
ContentPageHeader,
|
||||
formatLoaderLabel,
|
||||
injectNotificationManager,
|
||||
LoaderIcon as ServerLoaderIcon,
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
PageHeader,
|
||||
ServerOnlinePlayers,
|
||||
ServerPing,
|
||||
ServerRecentPlays,
|
||||
@@ -774,6 +605,191 @@ const playtimeLabel = computed(() =>
|
||||
timePlayed.value > 0 ? timePlayedHumanized.value : 'Never played',
|
||||
)
|
||||
|
||||
const instanceHeaderLeading = computed(() => ({
|
||||
type: 'avatar' as const,
|
||||
src: icon.value ? icon.value : undefined,
|
||||
alt: instance.value?.name,
|
||||
avatarSize: '64px',
|
||||
tintBy: instance.value?.id,
|
||||
}))
|
||||
|
||||
const instanceHeaderMetadata = computed(() => {
|
||||
if (!instance.value) return []
|
||||
if (isServerInstance.value) {
|
||||
return [
|
||||
{
|
||||
id: 'server-details',
|
||||
type: 'custom' as const,
|
||||
class: 'contents',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'game-version',
|
||||
label: instance.value.game_version,
|
||||
icon: Gamepad2Icon,
|
||||
},
|
||||
{
|
||||
id: 'loader',
|
||||
label: loaderLabel.value,
|
||||
icon: ServerLoaderIcon,
|
||||
iconProps: {
|
||||
loader: loaderDisplayName.value,
|
||||
},
|
||||
},
|
||||
...(showInstancePlayTime.value
|
||||
? [
|
||||
{
|
||||
id: 'playtime',
|
||||
label: playtimeLabel.value,
|
||||
icon: TimerIcon,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
})
|
||||
|
||||
const installingStages = [
|
||||
'installing',
|
||||
'pack_installing',
|
||||
'pack_installed',
|
||||
'not_installed',
|
||||
'minecraft_installing',
|
||||
]
|
||||
|
||||
const primaryInstanceAction = computed(() => {
|
||||
if (!instance.value) return null
|
||||
|
||||
if (installingStages.includes(instance.value.install_stage)) {
|
||||
return {
|
||||
id: 'installing',
|
||||
label: 'Installing...',
|
||||
color: 'brand' as const,
|
||||
disabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (instance.value.install_stage !== 'installed') {
|
||||
return {
|
||||
id: 'repair',
|
||||
label: 'Repair',
|
||||
icon: DownloadIcon,
|
||||
color: 'brand' as const,
|
||||
onClick: () => {
|
||||
void repairInstance()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (playing.value === true) {
|
||||
return {
|
||||
id: 'stop',
|
||||
label: stopping.value ? 'Stopping...' : 'Stop',
|
||||
icon: StopCircleIcon,
|
||||
color: 'red' as const,
|
||||
disabled: stopping.value,
|
||||
onClick: () => {
|
||||
void stopInstance('InstancePage')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (playing.value === false && loading.value === false && !isServerInstance.value) {
|
||||
return {
|
||||
id: 'play',
|
||||
label: 'Play',
|
||||
icon: PlayIcon,
|
||||
color: 'brand' as const,
|
||||
onClick: () => {
|
||||
void startInstance('InstancePage')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (playing.value === false && loading.value === false && isServerInstance.value) {
|
||||
return {
|
||||
id: 'play',
|
||||
label: 'Play',
|
||||
color: 'brand' as const,
|
||||
joinedActions: [
|
||||
{
|
||||
id: 'join_server',
|
||||
label: 'Play',
|
||||
icon: PlayIcon,
|
||||
action: () => {
|
||||
void handlePlayServer()
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'launch_instance',
|
||||
label: 'Launch instance',
|
||||
icon: PlayIcon,
|
||||
action: () => {
|
||||
void startInstance('InstancePage')
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (loading.value === true && playing.value === false) {
|
||||
return {
|
||||
id: 'starting',
|
||||
label: 'Starting...',
|
||||
color: 'brand' as const,
|
||||
disabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const instanceHeaderActions = computed(() => [
|
||||
...(primaryInstanceAction.value ? [primaryInstanceAction.value] : []),
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Instance settings',
|
||||
icon: SettingsIcon,
|
||||
labelHidden: true,
|
||||
tooltip: 'Instance settings',
|
||||
onClick: () => settingsModal.value?.show(),
|
||||
},
|
||||
{
|
||||
id: 'more',
|
||||
label: 'More actions',
|
||||
icon: MoreVerticalIcon,
|
||||
labelHidden: true,
|
||||
type: 'transparent' as const,
|
||||
tooltip: 'More actions',
|
||||
menuActions: [
|
||||
{
|
||||
id: 'open-folder',
|
||||
label: 'Open folder',
|
||||
icon: FolderOpenIcon,
|
||||
action: () => {
|
||||
if (instance.value) void showInstanceInFolder(instance.value.id)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'export-mrpack',
|
||||
label: 'Export modpack',
|
||||
icon: PackageIcon,
|
||||
action: () => exportModal.value?.show(),
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
label: 'Create shortcut',
|
||||
icon: ExternalIcon,
|
||||
action: () => {
|
||||
void createShortcut()
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses()
|
||||
unlistenInstances()
|
||||
|
||||
@@ -64,121 +64,9 @@
|
||||
:project="data"
|
||||
:project-v3="projectV3"
|
||||
:ping="serverPing"
|
||||
:actions="projectHeaderActions"
|
||||
@contextmenu.prevent.stop="handleRightClick"
|
||||
>
|
||||
<template v-if="isServerProject" #actions>
|
||||
<ButtonStyled v-if="serverPlaying" size="large" color="red">
|
||||
<button @click="handleStopServer">
|
||||
<StopCircleIcon />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else size="large" color="brand">
|
||||
<button
|
||||
:disabled="data && installingServerProjects.includes(data.id)"
|
||||
@click="handleClickPlay"
|
||||
>
|
||||
<PlayIcon />
|
||||
{{
|
||||
data && installingServerProjects.includes(data.id)
|
||||
? formatMessage(commonMessages.installingLabel)
|
||||
: formatMessage(commonMessages.playButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular>
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
|
||||
@click="handleAddServerToInstance"
|
||||
>
|
||||
<PlusIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:tooltip="`More options`"
|
||||
:options="[
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/project/${data.slug}`,
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
<template #report> <ReportIcon /> Report </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else #actions>
|
||||
<ButtonStyled size="large" color="brand">
|
||||
<button
|
||||
v-tooltip="installButtonTooltip"
|
||||
:disabled="installButtonDisabled"
|
||||
@click="install(null)"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="installButtonLoading && !installButtonInstalled"
|
||||
class="animate-spin"
|
||||
/>
|
||||
<DownloadIcon v-else-if="!installButtonInstalled && !serverProjectSelected" />
|
||||
<CheckIcon v-else />
|
||||
{{ installButtonLabel }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:tooltip="`More options`"
|
||||
:options="[
|
||||
{
|
||||
id: 'follow',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'save',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/${data.project_type}/${data.slug}`,
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
<template #follow> <HeartIcon /> Follow </template>
|
||||
<template #save> <BookmarkIcon /> Save </template>
|
||||
<template #report> <ReportIcon /> Report </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectHeader>
|
||||
/>
|
||||
<NavTabs
|
||||
:links="[
|
||||
{
|
||||
@@ -266,14 +154,12 @@ import {
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
BrowseInstallHeader,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
CreationFlowModal,
|
||||
defineMessages,
|
||||
getTargetInstallPreferences,
|
||||
injectNotificationManager,
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
ProjectBackgroundGradient,
|
||||
ProjectHeader,
|
||||
ProjectSidebarCompatibility,
|
||||
@@ -514,6 +400,131 @@ const installButtonTooltip = computed(() => {
|
||||
return null
|
||||
})
|
||||
|
||||
const projectHeaderActions = computed(() => {
|
||||
if (!data.value) return []
|
||||
|
||||
if (isServerProject.value) {
|
||||
return [
|
||||
serverPlaying.value
|
||||
? {
|
||||
id: 'stop',
|
||||
label: formatMessage(commonMessages.stopButton),
|
||||
icon: StopCircleIcon,
|
||||
color: 'red',
|
||||
onClick: handleStopServer,
|
||||
}
|
||||
: {
|
||||
id: 'play',
|
||||
label:
|
||||
data.value && installingServerProjects.value.includes(data.value.id)
|
||||
? formatMessage(commonMessages.installingLabel)
|
||||
: formatMessage(commonMessages.playButton),
|
||||
icon: PlayIcon,
|
||||
color: 'brand',
|
||||
disabled: data.value && installingServerProjects.value.includes(data.value.id),
|
||||
onClick: handleClickPlay,
|
||||
},
|
||||
{
|
||||
id: 'add-server-to-instance',
|
||||
label: formatMessage(commonMessages.addServerToInstanceButton),
|
||||
icon: PlusIcon,
|
||||
labelHidden: true,
|
||||
tooltip: formatMessage(commonMessages.addServerToInstanceButton),
|
||||
onClick: handleAddServerToInstance,
|
||||
},
|
||||
{
|
||||
id: 'more',
|
||||
label: 'More options',
|
||||
icon: MoreVerticalIcon,
|
||||
labelHidden: true,
|
||||
type: 'transparent',
|
||||
tooltip: 'More options',
|
||||
menuActions: [
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
label: 'Open in browser',
|
||||
icon: ExternalIcon,
|
||||
action: () => openUrl(`https://modrinth.com/project/${data.value.slug}`),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
label: 'Report',
|
||||
icon: ReportIcon,
|
||||
color: 'red',
|
||||
action: () =>
|
||||
openUrl(`https://modrinth.com/report?item=project&itemID=${data.value.id}`),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'install',
|
||||
label: installButtonLabel.value,
|
||||
icon:
|
||||
installButtonLoading.value && !installButtonInstalled.value
|
||||
? SpinnerIcon
|
||||
: !installButtonInstalled.value && !serverProjectSelected.value
|
||||
? DownloadIcon
|
||||
: CheckIcon,
|
||||
iconClass:
|
||||
installButtonLoading.value && !installButtonInstalled.value ? 'animate-spin' : undefined,
|
||||
color: 'brand',
|
||||
tooltip: installButtonTooltip.value,
|
||||
disabled: installButtonDisabled.value,
|
||||
onClick: () => install(null),
|
||||
},
|
||||
{
|
||||
id: 'more',
|
||||
label: 'More options',
|
||||
icon: MoreVerticalIcon,
|
||||
labelHidden: true,
|
||||
type: 'transparent',
|
||||
tooltip: 'More options',
|
||||
menuActions: [
|
||||
{
|
||||
id: 'follow',
|
||||
label: 'Follow',
|
||||
icon: HeartIcon,
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'save',
|
||||
label: 'Save',
|
||||
icon: BookmarkIcon,
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
label: 'Open in browser',
|
||||
icon: ExternalIcon,
|
||||
action: () =>
|
||||
openUrl(`https://modrinth.com/${data.value.project_type}/${data.value.slug}`),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
label: 'Report',
|
||||
icon: ReportIcon,
|
||||
color: 'red',
|
||||
action: () => openUrl(`https://modrinth.com/report?item=project&itemID=${data.value.id}`),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const [allLoaders, allGameVersions] = await Promise.all([
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<ButtonStyled size="large" circular>
|
||||
<PopoutMenu
|
||||
v-if="authUser"
|
||||
:tooltip="
|
||||
saved ? formatMessage(commonMessages.savedLabel) : formatMessage(commonMessages.saveButton)
|
||||
"
|
||||
from="top-right"
|
||||
:aria-label="formatMessage(commonMessages.saveButton)"
|
||||
:dropdown-id="`${baseId}-save`"
|
||||
>
|
||||
<BookmarkIcon aria-hidden="true" :fill="saved ? 'currentColor' : 'none'" />
|
||||
<template #menu>
|
||||
<StyledInput
|
||||
v-model="displayCollectionsSearch"
|
||||
:placeholder="formatMessage(commonMessages.searchPlaceholder)"
|
||||
wrapper-class="menu-search"
|
||||
/>
|
||||
<div v-if="filteredCollections.length > 0" class="collections-list text-primary">
|
||||
<Checkbox
|
||||
v-for="option in filteredCollections"
|
||||
:key="option.id"
|
||||
:model-value="option.projects.includes(projectId)"
|
||||
class="popout-checkbox"
|
||||
@update:model-value="() => collectProject(option, projectId)"
|
||||
>
|
||||
{{ option.name }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div v-else class="menu-text">
|
||||
<p class="popout-text">{{ noCollectionsLabel }}</p>
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<button class="mx-3 mb-3" @click="createCollection">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ createNewCollectionLabel }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</PopoutMenu>
|
||||
<nuxt-link
|
||||
v-else
|
||||
v-tooltip="formatMessage(commonMessages.saveButton)"
|
||||
:to="signInRoute"
|
||||
:aria-label="formatMessage(commonMessages.saveButton)"
|
||||
>
|
||||
<BookmarkIcon aria-hidden="true" />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BookmarkIcon, PlusIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
PopoutMenu,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
type CollectionOption = {
|
||||
id: string
|
||||
name: string
|
||||
projects: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
authUser?: unknown
|
||||
signInRoute: RouteLocationRaw
|
||||
projectId: string
|
||||
collections: CollectionOption[]
|
||||
saved: boolean
|
||||
baseId: string
|
||||
noCollectionsLabel: string
|
||||
createNewCollectionLabel: string
|
||||
collectProject: (option: CollectionOption, projectId: string) => void | Promise<void>
|
||||
createCollection: (event: MouseEvent) => void
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const displayCollectionsSearch = ref('')
|
||||
|
||||
const filteredCollections = computed(() =>
|
||||
props.collections
|
||||
.filter((collection) =>
|
||||
collection.name.toLowerCase().includes(displayCollectionsSearch.value.toLowerCase()),
|
||||
)
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popout-checkbox {
|
||||
padding: var(--gap-sm) var(--gap-md);
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
padding: 0 var(--gap-md);
|
||||
font-size: var(--font-size-nm);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.menu-search {
|
||||
margin: var(--gap-sm) var(--gap-md);
|
||||
width: calc(100% - var(--gap-md) * 2);
|
||||
}
|
||||
|
||||
.collections-list {
|
||||
max-height: 40rem;
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-bg);
|
||||
border-radius: var(--radius-md);
|
||||
margin: var(--gap-sm) var(--gap-md);
|
||||
padding: var(--gap-sm);
|
||||
}
|
||||
</style>
|
||||
@@ -1964,6 +1964,9 @@
|
||||
"discover.install.heading.reset-modpack": {
|
||||
"message": "Selecting modpack to install after reset"
|
||||
},
|
||||
"discover.install.world-fallback-name": {
|
||||
"message": "Instance"
|
||||
},
|
||||
"discover.seo.description": {
|
||||
"message": "Search and browse thousands of Minecraft {projectType} on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft {projectType}."
|
||||
},
|
||||
@@ -3941,6 +3944,9 @@
|
||||
"servers.manage.instances.meta.title": {
|
||||
"message": "Instances - {server} - Modrinth"
|
||||
},
|
||||
"servers.manage.instances.slot-name": {
|
||||
"message": "Instance #{index}"
|
||||
},
|
||||
"servers.notice.actions": {
|
||||
"message": "Actions"
|
||||
},
|
||||
|
||||
@@ -50,8 +50,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
|
||||
const content = await queryClient.ensureQueryData({
|
||||
queryKey: ['content', 'list', 'v1', serverId, worldId],
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(serverId, worldId, { from_modpack: false }),
|
||||
queryFn: () => client.archon.content_v1.getAddons(serverId, worldId, { from_modpack: false }),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
|
||||
@@ -459,358 +459,8 @@
|
||||
:project="project"
|
||||
:project-v3="projectV3"
|
||||
:member="!!currentMember"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled v-if="auth.user && currentMember" size="large" color="brand" circular>
|
||||
<nuxt-link
|
||||
v-tooltip="'Edit project'"
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
||||
class="!font-bold lg:!hidden"
|
||||
>
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="auth.user && currentMember" size="large" color="brand">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
||||
class="!font-bold max-lg:!hidden"
|
||||
>
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
Edit project
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
|
||||
<div class="hidden sm:contents">
|
||||
<ButtonStyled
|
||||
v-if="!isServerProject"
|
||||
size="large"
|
||||
:color="
|
||||
(auth.user && currentMember) || route.name === 'type-project-version-version'
|
||||
? `standard`
|
||||
: `brand`
|
||||
"
|
||||
:circular="!!auth.user && !!currentMember"
|
||||
>
|
||||
<button
|
||||
v-tooltip="
|
||||
auth.user && currentMember ? formatMessage(commonMessages.downloadButton) : ''
|
||||
"
|
||||
@click="(event) => downloadModal.show(event)"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
{{
|
||||
auth.user && currentMember ? '' : formatMessage(commonMessages.downloadButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else
|
||||
size="large"
|
||||
:color="
|
||||
(auth.user && currentMember) || route.name === 'type-project-version-version'
|
||||
? `standard`
|
||||
: `brand`
|
||||
"
|
||||
:circular="!!auth.user && !!currentMember"
|
||||
>
|
||||
<button
|
||||
v-tooltip="auth.user && currentMember && !openInAppModal?.open ? 'Play' : ''"
|
||||
@click="handlePlayServerProject"
|
||||
>
|
||||
<PlayIcon aria-hidden="true" />
|
||||
{{ auth.user && currentMember ? '' : 'Play' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="contents sm:hidden">
|
||||
<ButtonStyled
|
||||
v-if="!isServerProject"
|
||||
size="large"
|
||||
circular
|
||||
:color="
|
||||
route.name === 'type-project-version-version' || (auth.user && currentMember)
|
||||
? `standard`
|
||||
: `brand`
|
||||
"
|
||||
>
|
||||
<button
|
||||
:aria-label="formatMessage(commonMessages.downloadButton)"
|
||||
class="flex sm:hidden"
|
||||
@click="(event) => downloadModal.show(event)"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else
|
||||
size="large"
|
||||
circular
|
||||
:color="
|
||||
route.name === 'type-project-version-version' || (auth.user && currentMember)
|
||||
? `standard`
|
||||
: `brand`
|
||||
"
|
||||
>
|
||||
<button aria-label="Play" class="flex sm:hidden" @click="handlePlayServerProject">
|
||||
<PlayIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<Tooltip
|
||||
v-if="canCreateServerFrom && flags.showProjectPageQuickServerButton"
|
||||
theme="dismissable-prompt"
|
||||
:triggers="[]"
|
||||
:shown="flags.showProjectPageCreateServersTooltip"
|
||||
:auto-hide="false"
|
||||
placement="bottom-start"
|
||||
>
|
||||
<ButtonStyled size="large" circular>
|
||||
<nuxt-link
|
||||
v-tooltip="formatMessage(messages.createServerTooltip)"
|
||||
:to="`/hosting?project=${project.id}#plan`"
|
||||
@click="
|
||||
() => {
|
||||
flags.showProjectPageCreateServersTooltip = false
|
||||
saveFeatureFlags()
|
||||
}
|
||||
"
|
||||
>
|
||||
<ServerPlusIcon aria-hidden="true" />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<template #popper>
|
||||
<div class="grid grid-cols-[min-content] gap-1">
|
||||
<div class="flex min-w-60 items-center justify-between gap-4">
|
||||
<h3
|
||||
class="m-0 flex items-center gap-2 whitespace-nowrap text-base font-bold text-contrast"
|
||||
>
|
||||
{{ formatMessage(messages.serversPromoTitle) }}
|
||||
<TagItem
|
||||
:style="{
|
||||
'--_color': 'var(--color-brand)',
|
||||
'--_bg-color': 'var(--color-brand-highlight)',
|
||||
}"
|
||||
>{{ formatMessage(commonMessages.newBadge) }}</TagItem
|
||||
>
|
||||
</h3>
|
||||
<ButtonStyled size="small" circular>
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.dontShowAgain)"
|
||||
@click="
|
||||
() => {
|
||||
flags.showProjectPageCreateServersTooltip = false
|
||||
saveFeatureFlags()
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
|
||||
{{ formatMessage(messages.serversPromoDescription) }}
|
||||
</p>
|
||||
|
||||
<p class="m-0 text-wrap text-sm font-bold text-primary">
|
||||
<IntlFormatted
|
||||
:message-id="messages.serversPromoPricing"
|
||||
:values="{
|
||||
price: formatPrice(500, 'USD', true),
|
||||
}"
|
||||
>
|
||||
<template #small="{ children }">
|
||||
<span class="text-xs">
|
||||
<component :is="() => children" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</Tooltip>
|
||||
<ButtonStyled size="large" circular>
|
||||
<ClientOnly>
|
||||
<button
|
||||
v-if="auth.user"
|
||||
v-tooltip="
|
||||
following
|
||||
? formatMessage(commonMessages.unfollowButton)
|
||||
: formatMessage(commonMessages.followButton)
|
||||
"
|
||||
:aria-label="
|
||||
following
|
||||
? formatMessage(commonMessages.unfollowButton)
|
||||
: formatMessage(commonMessages.followButton)
|
||||
"
|
||||
@click="userFollowProject(project)"
|
||||
>
|
||||
<HeartIcon :fill="following ? 'currentColor' : 'none'" aria-hidden="true" />
|
||||
</button>
|
||||
<nuxt-link
|
||||
v-else
|
||||
v-tooltip="formatMessage(commonMessages.followButton)"
|
||||
:to="signInRouteObj"
|
||||
:aria-label="formatMessage(commonMessages.followButton)"
|
||||
>
|
||||
<HeartIcon aria-hidden="true" />
|
||||
</nuxt-link>
|
||||
<template #fallback>
|
||||
<nuxt-link
|
||||
v-tooltip="formatMessage(commonMessages.followButton)"
|
||||
:to="signInRouteObj"
|
||||
:aria-label="formatMessage(commonMessages.followButton)"
|
||||
>
|
||||
<HeartIcon aria-hidden="true" />
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular>
|
||||
<PopoutMenu
|
||||
v-if="auth.user"
|
||||
:tooltip="
|
||||
collections.some((x) => x.projects.includes(project.id))
|
||||
? formatMessage(commonMessages.savedLabel)
|
||||
: formatMessage(commonMessages.saveButton)
|
||||
"
|
||||
from="top-right"
|
||||
:aria-label="formatMessage(commonMessages.saveButton)"
|
||||
:dropdown-id="`${baseId}-save`"
|
||||
>
|
||||
<BookmarkIcon
|
||||
aria-hidden="true"
|
||||
:fill="
|
||||
collections.some((x) => x.projects.includes(project.id))
|
||||
? 'currentColor'
|
||||
: 'none'
|
||||
"
|
||||
/>
|
||||
<template #menu>
|
||||
<StyledInput
|
||||
v-model="displayCollectionsSearch"
|
||||
:placeholder="formatMessage(commonMessages.searchPlaceholder)"
|
||||
wrapper-class="menu-search"
|
||||
/>
|
||||
<div v-if="collections.length > 0" class="collections-list text-primary">
|
||||
<Checkbox
|
||||
v-for="option in collections
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))"
|
||||
:key="option.id"
|
||||
:model-value="option.projects.includes(project.id)"
|
||||
class="popout-checkbox"
|
||||
@update:model-value="() => onUserCollectProject(option, project.id)"
|
||||
>
|
||||
{{ option.name }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div v-else class="menu-text">
|
||||
<p class="popout-text">{{ formatMessage(messages.noCollectionsFound) }}</p>
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
class="mx-3 mb-3"
|
||||
@click="(event) => $refs.modal_collection.show(event)"
|
||||
>
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createNewCollection) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</PopoutMenu>
|
||||
<nuxt-link v-else v-tooltip="'Save'" :to="signInRouteObj" aria-label="Save">
|
||||
<BookmarkIcon aria-hidden="true" />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:tooltip="formatMessage(commonMessages.moreOptionsButton)"
|
||||
:options="[
|
||||
{
|
||||
id: 'analytics',
|
||||
link: `/${project.project_type}/${project.slug ? project.slug : project.id}/settings/analytics`,
|
||||
hoverOnly: true,
|
||||
shown: auth.user && !!currentMember,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: auth.user && !!currentMember,
|
||||
},
|
||||
{
|
||||
id: 'moderation-checklist',
|
||||
action: openModerationChecklistFromMenu,
|
||||
color: 'orange',
|
||||
hoverOnly: true,
|
||||
shown:
|
||||
auth.user &&
|
||||
tags.staffRoles.includes(auth.user.role) &&
|
||||
!showModerationChecklist,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown:
|
||||
auth.user &&
|
||||
tags.staffRoles.includes(auth.user.role) &&
|
||||
!showModerationChecklist,
|
||||
},
|
||||
{
|
||||
id: 'tech-review',
|
||||
link: `/moderation/technical-review/${project.id}`,
|
||||
color: 'orange',
|
||||
hoverOnly: true,
|
||||
shown: auth.user && tags.staffRoles.includes(auth.user.role),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: auth.user && tags.staffRoles.includes(auth.user.role),
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
action: () =>
|
||||
auth.user
|
||||
? reportProject(project.id)
|
||||
: navigateTo(
|
||||
getSignInRouteObj(route, getReportPath('project', project.id)),
|
||||
),
|
||||
color: 'red',
|
||||
hoverOnly: true,
|
||||
shown: !isMember,
|
||||
},
|
||||
{ id: 'copy-id', action: () => copyId() },
|
||||
{ id: 'copy-permalink', action: () => copyPermalink() },
|
||||
]"
|
||||
:aria-label="formatMessage(commonMessages.moreOptionsButton)"
|
||||
:dropdown-id="`${baseId}-more-options`"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #analytics>
|
||||
<ChartIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.analyticsButton) }}
|
||||
</template>
|
||||
<template #moderation-checklist>
|
||||
<ScaleIcon aria-hidden="true" /> {{ formatMessage(messages.reviewProject) }}
|
||||
</template>
|
||||
<template #tech-review> <ScanEyeIcon aria-hidden="true" /> Tech review </template>
|
||||
<template #report>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</template>
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
<template #copy-permalink>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyPermalinkButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectHeader>
|
||||
:actions="projectHeaderActions"
|
||||
/>
|
||||
<ProjectMemberHeader
|
||||
v-if="currentMember"
|
||||
:project="project"
|
||||
@@ -1044,7 +694,6 @@
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BookmarkIcon,
|
||||
BookTextIcon,
|
||||
CalendarIcon,
|
||||
ChartIcon,
|
||||
@@ -1060,7 +709,6 @@ import {
|
||||
ModrinthIcon,
|
||||
MoreVerticalIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
ReportIcon,
|
||||
ScaleIcon,
|
||||
ScanEyeIcon,
|
||||
@@ -1069,7 +717,6 @@ import {
|
||||
SettingsIcon,
|
||||
VersionIcon,
|
||||
WrenchIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
@@ -1081,12 +728,9 @@ import {
|
||||
getTagMessage,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
NavTabs,
|
||||
NewModal,
|
||||
OpenInAppModal,
|
||||
OverflowMenu,
|
||||
PopoutMenu,
|
||||
PROJECT_DEP_MARKER_QUERY,
|
||||
ProjectBackgroundGradient,
|
||||
ProjectEnvironmentModal,
|
||||
@@ -1101,7 +745,6 @@ import {
|
||||
ScrollablePanel,
|
||||
ServersPromo,
|
||||
StyledInput,
|
||||
TagItem,
|
||||
useDebugLogger,
|
||||
useFormatDateTime,
|
||||
useFormatPrice,
|
||||
@@ -1113,7 +756,6 @@ import { capitalizeString, formatProjectType, renderString } from '@modrinth/uti
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
import { nextTick, readonly, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import { navigateTo } from '#app'
|
||||
@@ -1124,6 +766,7 @@ import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.
|
||||
import MessageBanner from '~/components/ui/MessageBanner.vue'
|
||||
import ModerationChecklist from '~/components/ui/moderation/checklist/ModerationChecklist.vue'
|
||||
import ModerationProjectNags from '~/components/ui/moderation/ModerationProjectNags.vue'
|
||||
import ProjectCollectionSaveButton from '~/components/ui/ProjectCollectionSaveButton.vue'
|
||||
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
|
||||
@@ -1202,6 +845,7 @@ const projectV3Loaded = computed(() => !projectV3Pending.value || projectV3.valu
|
||||
const isServerProject = computed(() => projectV3.value?.minecraft_server != null)
|
||||
|
||||
const projectEnvironmentModal = useTemplateRef('projectEnvironmentModal')
|
||||
const modalCollection = useTemplateRef('modal_collection')
|
||||
|
||||
const baseId = useId()
|
||||
|
||||
@@ -1632,13 +1276,8 @@ const filteredAlpha = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const displayCollectionsSearch = ref('')
|
||||
const collections = computed(() =>
|
||||
user.value && user.value.collections
|
||||
? user.value.collections.filter((x) =>
|
||||
x.name.toLowerCase().includes(displayCollectionsSearch.value.toLowerCase()),
|
||||
)
|
||||
: [],
|
||||
user.value && user.value.collections ? user.value.collections : [],
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -2362,6 +2001,184 @@ const canCreateServerFrom = computed(() => {
|
||||
return project.value.project_type === 'modpack' && project.value.server_side !== 'unsupported'
|
||||
})
|
||||
|
||||
const projectHeaderActions = computed(() => {
|
||||
if (!project.value) return []
|
||||
|
||||
const projectPath = `/${project.value.project_type}/${project.value.slug ? project.value.slug : project.value.id}`
|
||||
const hasMember = !!currentMember.value
|
||||
const userSignedIn = !!auth.value.user
|
||||
const mutedPrimaryAction = hasMember || route.name === 'type-project-version-version'
|
||||
const primaryLabel = isServerProject.value ? 'Play' : formatMessage(commonMessages.downloadButton)
|
||||
|
||||
return [
|
||||
...(userSignedIn && hasMember
|
||||
? [
|
||||
{
|
||||
id: 'edit-project',
|
||||
label: 'Edit project',
|
||||
icon: SettingsIcon,
|
||||
color: 'brand',
|
||||
to: `${projectPath}/settings`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: isServerProject.value ? 'play' : 'download',
|
||||
label: primaryLabel,
|
||||
icon: isServerProject.value ? PlayIcon : DownloadIcon,
|
||||
color: mutedPrimaryAction ? 'standard' : 'brand',
|
||||
labelHidden: userSignedIn && hasMember,
|
||||
tooltip: userSignedIn && hasMember ? primaryLabel : undefined,
|
||||
onClick: (event) => {
|
||||
if (isServerProject.value) {
|
||||
handlePlayServerProject()
|
||||
} else {
|
||||
downloadModal.value?.show(event)
|
||||
}
|
||||
},
|
||||
},
|
||||
...(canCreateServerFrom.value && flags.value.showProjectPageQuickServerButton
|
||||
? [
|
||||
{
|
||||
id: 'create-server',
|
||||
label: formatMessage(messages.serversPromoTitle),
|
||||
icon: ServerPlusIcon,
|
||||
labelHidden: true,
|
||||
tooltip: formatMessage(messages.createServerTooltip),
|
||||
to: `/hosting?project=${project.value.id}#plan`,
|
||||
onClick: () => {
|
||||
flags.value.showProjectPageCreateServersTooltip = false
|
||||
saveFeatureFlags()
|
||||
},
|
||||
prompt: {
|
||||
title: formatMessage(messages.serversPromoTitle),
|
||||
description: formatMessage(messages.serversPromoDescription),
|
||||
badge: formatMessage(commonMessages.newBadge),
|
||||
footer: formatMessage(messages.serversPromoPricing, {
|
||||
price: formatPrice(500, 'USD', true),
|
||||
small: (children) => (Array.isArray(children) ? children.join('') : children),
|
||||
}),
|
||||
dismissLabel: formatMessage(messages.dontShowAgain),
|
||||
shown: flags.value.showProjectPageCreateServersTooltip,
|
||||
placement: 'bottom-start',
|
||||
onDismiss: () => {
|
||||
flags.value.showProjectPageCreateServersTooltip = false
|
||||
saveFeatureFlags()
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'follow',
|
||||
label: following.value
|
||||
? formatMessage(commonMessages.unfollowButton)
|
||||
: formatMessage(commonMessages.followButton),
|
||||
icon: HeartIcon,
|
||||
iconProps: {
|
||||
fill: following.value ? 'currentColor' : 'none',
|
||||
},
|
||||
labelHidden: true,
|
||||
tooltip: following.value
|
||||
? formatMessage(commonMessages.unfollowButton)
|
||||
: formatMessage(commonMessages.followButton),
|
||||
to: userSignedIn ? undefined : signInRouteObj.value,
|
||||
onClick: userSignedIn ? () => userFollowProject(project.value) : undefined,
|
||||
},
|
||||
{
|
||||
id: 'save',
|
||||
label: formatMessage(commonMessages.saveButton),
|
||||
component: ProjectCollectionSaveButton,
|
||||
componentProps: {
|
||||
authUser: auth.value.user,
|
||||
signInRoute: signInRouteObj.value,
|
||||
projectId: project.value.id,
|
||||
collections: collections.value,
|
||||
saved: collections.value.some((x) => x.projects.includes(project.value.id)),
|
||||
baseId,
|
||||
noCollectionsLabel: formatMessage(messages.noCollectionsFound),
|
||||
createNewCollectionLabel: formatMessage(messages.createNewCollection),
|
||||
collectProject: onUserCollectProject,
|
||||
createCollection: (event) => modalCollection.value?.show(event),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'more',
|
||||
label: formatMessage(commonMessages.moreOptionsButton),
|
||||
icon: MoreVerticalIcon,
|
||||
labelHidden: true,
|
||||
type: 'transparent',
|
||||
tooltip: formatMessage(commonMessages.moreOptionsButton),
|
||||
menuActions: [
|
||||
{
|
||||
id: 'analytics',
|
||||
label: formatMessage(commonMessages.analyticsButton),
|
||||
icon: ChartIcon,
|
||||
link: `${projectPath}/settings/analytics`,
|
||||
shown: userSignedIn && hasMember,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: userSignedIn && hasMember,
|
||||
},
|
||||
{
|
||||
id: 'moderation-checklist',
|
||||
label: formatMessage(messages.reviewProject),
|
||||
icon: ScaleIcon,
|
||||
action: openModerationChecklistFromMenu,
|
||||
color: 'orange',
|
||||
shown:
|
||||
userSignedIn &&
|
||||
tags.value.staffRoles.includes(auth.value.user.role) &&
|
||||
!showModerationChecklist.value,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown:
|
||||
userSignedIn &&
|
||||
tags.value.staffRoles.includes(auth.value.user.role) &&
|
||||
!showModerationChecklist.value,
|
||||
},
|
||||
{
|
||||
id: 'tech-review',
|
||||
label: 'Tech review',
|
||||
icon: ScanEyeIcon,
|
||||
link: `/moderation/technical-review/${project.value.id}`,
|
||||
color: 'orange',
|
||||
shown: userSignedIn && tags.value.staffRoles.includes(auth.value.user.role),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: userSignedIn && tags.value.staffRoles.includes(auth.value.user.role),
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
label: formatMessage(commonMessages.reportButton),
|
||||
icon: ReportIcon,
|
||||
action: () =>
|
||||
auth.value.user
|
||||
? reportProject(project.value.id)
|
||||
: navigateTo(getSignInRouteObj(route, getReportPath('project', project.value.id))),
|
||||
color: 'red',
|
||||
shown: !isMember.value,
|
||||
},
|
||||
{
|
||||
id: 'copy-id',
|
||||
label: formatMessage(commonMessages.copyIdButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => copyId(),
|
||||
},
|
||||
{
|
||||
id: 'copy-permalink',
|
||||
label: formatMessage(commonMessages.copyPermalinkButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => copyPermalink(),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const createCanonicalUrl = () =>
|
||||
project.value ? `https://modrinth.com/project/${project.value.id}` : undefined
|
||||
|
||||
|
||||
@@ -62,86 +62,14 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="normal-page__header py-4">
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar :src="organization.icon_url" :alt="organization.name" size="96px" />
|
||||
</template>
|
||||
<template #title>
|
||||
{{ organization.name }}
|
||||
</template>
|
||||
<template #title-suffix>
|
||||
<div class="ml-1 flex items-center gap-2 font-semibold">
|
||||
<OrganizationIcon />
|
||||
Organization
|
||||
</div>
|
||||
</template>
|
||||
<template #summary>
|
||||
{{ organization.description }}
|
||||
</template>
|
||||
<template #stats>
|
||||
<div
|
||||
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
|
||||
>
|
||||
<UsersIcon class="h-6 w-6 text-secondary" />
|
||||
{{ formatCompactNumber(acceptedMembers?.length || 0) }}
|
||||
members
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
|
||||
>
|
||||
<BoxIcon class="h-6 w-6 text-secondary" />
|
||||
{{ formatCompactNumber(projects?.length || 0) }}
|
||||
projects
|
||||
</div>
|
||||
<div
|
||||
v-tooltip="formatNumber(sumDownloads)"
|
||||
class="flex items-center gap-2 font-semibold"
|
||||
>
|
||||
<DownloadIcon class="h-6 w-6 text-secondary" />
|
||||
{{ formatCompactNumber(sumDownloads) }}
|
||||
downloads
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<ButtonStyled v-if="auth.user && currentMember" size="large">
|
||||
<NuxtLink :to="`/organization/${organization.slug}/settings`">
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
Manage
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'manage-projects',
|
||||
action: () =>
|
||||
router.push('/organization/' + organization?.slug + '/settings/projects'),
|
||||
hoverFilledOnly: true,
|
||||
shown: !!(auth.user && currentMember),
|
||||
},
|
||||
{ divider: true, shown: !!(auth?.user && currentMember) },
|
||||
{ id: 'copy-id', action: () => copyId() },
|
||||
{ id: 'copy-permalink', action: () => copyPermalink() },
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #manage-projects>
|
||||
<BoxIcon aria-hidden="true" />
|
||||
Manage projects
|
||||
</template>
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
<template #copy-permalink>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyPermalinkButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
<PageHeader
|
||||
:header="organization.name"
|
||||
:summary="organization.description"
|
||||
:leading="organizationHeaderLeading"
|
||||
:badges="organizationHeaderBadges"
|
||||
:metadata="organizationHeaderMetadata"
|
||||
:actions="organizationHeaderActions"
|
||||
/>
|
||||
</div>
|
||||
<div class="normal-page__sidebar">
|
||||
<AdPlaceholder v-if="!auth.user" />
|
||||
@@ -307,10 +235,9 @@ import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
ContentPageHeader,
|
||||
injectModrinthClient,
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
PageHeader,
|
||||
PROJECT_DEP_MARKER_QUERY,
|
||||
ProjectCard,
|
||||
ProjectCardList,
|
||||
@@ -541,6 +468,93 @@ provideOrganizationContext(organizationContext)
|
||||
|
||||
const canAccessSettings = computed(() => !!currentMember.value?.accepted)
|
||||
|
||||
const organizationHeaderLeading = computed(() => ({
|
||||
type: 'avatar' as const,
|
||||
src: organization.value?.icon_url,
|
||||
alt: organization.value?.name,
|
||||
avatarSize: '96px',
|
||||
}))
|
||||
|
||||
const organizationHeaderBadges = computed(() => [
|
||||
{
|
||||
id: 'organization',
|
||||
label: 'Organization',
|
||||
icon: OrganizationIcon,
|
||||
class: 'px-0 text-primary',
|
||||
},
|
||||
])
|
||||
|
||||
const organizationHeaderMetadata = computed(() => [
|
||||
{
|
||||
id: 'members',
|
||||
label: `${formatCompactNumber(acceptedMembers.value?.length || 0)} members`,
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
id: 'projects',
|
||||
label: `${formatCompactNumber(projects.value?.length || 0)} projects`,
|
||||
icon: BoxIcon,
|
||||
},
|
||||
{
|
||||
id: 'downloads',
|
||||
label: `${formatCompactNumber(sumDownloads.value)} downloads`,
|
||||
icon: DownloadIcon,
|
||||
tooltip: formatNumber(sumDownloads.value),
|
||||
},
|
||||
])
|
||||
|
||||
const organizationHeaderActions = computed(() => [
|
||||
...(auth.value.user && currentMember.value
|
||||
? [
|
||||
{
|
||||
id: 'manage',
|
||||
label: 'Manage',
|
||||
icon: SettingsIcon,
|
||||
to: `/organization/${organization.value?.slug}/settings`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'more',
|
||||
label: 'More options',
|
||||
icon: MoreVerticalIcon,
|
||||
labelHidden: true,
|
||||
type: 'transparent' as const,
|
||||
tooltip: 'More options',
|
||||
menuActions: [
|
||||
{
|
||||
id: 'manage-projects',
|
||||
label: 'Manage projects',
|
||||
icon: BoxIcon,
|
||||
action: () => {
|
||||
void router.push(`/organization/${organization.value?.slug}/settings/projects`)
|
||||
},
|
||||
shown: !!(auth.value.user && currentMember.value),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !!(auth.value.user && currentMember.value),
|
||||
},
|
||||
{
|
||||
id: 'copy-id',
|
||||
label: formatMessage(commonMessages.copyIdButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => {
|
||||
void copyId()
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'copy-permalink',
|
||||
label: formatMessage(commonMessages.copyPermalinkButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => {
|
||||
void copyPermalink()
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
watch(
|
||||
[routeHasSettings, acceptedMembers, currentMember],
|
||||
() => {
|
||||
|
||||
@@ -120,35 +120,14 @@
|
||||
</NewModal>
|
||||
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
|
||||
<div class="normal-page__header py-4">
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar
|
||||
:src="user.avatar_url"
|
||||
:alt="user.username"
|
||||
:size="isModrinthUser ? '64px' : '96px'"
|
||||
circle
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
<span class="flex items-center gap-2">
|
||||
{{ user.username }}
|
||||
<BadgeCheckIcon
|
||||
v-if="isOfficialAccount"
|
||||
v-tooltip="formatMessage(messages.officialAccount)"
|
||||
class="size-5 text-brand"
|
||||
fill="var(--color-brand-highlight)"
|
||||
/>
|
||||
<TagItem
|
||||
v-if="isAdminViewing && isAffiliate"
|
||||
:style="{
|
||||
'--_color': 'var(--color-brand)',
|
||||
'--_bg-color': 'var(--color-brand-highlight)',
|
||||
}"
|
||||
>
|
||||
<AffiliateIcon /> Affiliate
|
||||
</TagItem>
|
||||
</span>
|
||||
</template>
|
||||
<PageHeader
|
||||
:header="user.username"
|
||||
:summary="isModrinthUser ? null : profileHeaderSummary"
|
||||
:leading="profileHeaderLeading"
|
||||
:badges="profileHeaderBadges"
|
||||
:metadata="profileHeaderMetadata"
|
||||
:actions="profileHeaderActions"
|
||||
>
|
||||
<template v-if="isModrinthUser" #summary>
|
||||
<IntlFormatted :message-id="messages.officialAccountBio">
|
||||
<template #support-link>
|
||||
@@ -173,159 +152,7 @@
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</template>
|
||||
<template v-else #summary>
|
||||
{{
|
||||
user.bio
|
||||
? user.bio
|
||||
: projects?.length > 0
|
||||
? formatMessage(messages.bioFallbackCreator)
|
||||
: formatMessage(messages.bioFallbackUser)
|
||||
}}
|
||||
</template>
|
||||
<template v-if="!isModrinthUser" #stats>
|
||||
<div
|
||||
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
|
||||
>
|
||||
<BoxIcon class="h-6 w-6 text-secondary" />
|
||||
{{
|
||||
formatMessage(messages.profileProjectsLabel, {
|
||||
count: formatCompactNumber(projects?.length || 0),
|
||||
countPlural: formatCompactNumberPlural(projects?.length || 0),
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-tooltip="formatNumber(sumDownloads)"
|
||||
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
|
||||
>
|
||||
<DownloadIcon class="h-6 w-6 text-secondary" />
|
||||
{{
|
||||
formatMessage(messages.profileDownloadsLabel, {
|
||||
count: formatCompactNumber(sumDownloads),
|
||||
countPlural: formatCompactNumberPlural(sumDownloads),
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-tooltip="formatDateTime(user.created)"
|
||||
class="flex items-center gap-2 font-semibold"
|
||||
>
|
||||
<CalendarIcon class="h-6 w-6 text-secondary" />
|
||||
{{ formatMessage(messages.profileJoinedLabel) }}
|
||||
{{ formatRelativeTime(user.created) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<ButtonStyled size="large">
|
||||
<NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile">
|
||||
<EditIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.editButton) }}
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'manage-projects',
|
||||
action: () => navigateTo('/dashboard/projects'),
|
||||
hoverOnly: true,
|
||||
shown: auth.user && auth.user.id === user.id,
|
||||
},
|
||||
{ divider: true, shown: auth.user && auth.user.id === user.id },
|
||||
{
|
||||
id: 'report',
|
||||
action: () =>
|
||||
auth.user ? reportUser(user.id) : navigateTo(getSignInRouteObj(route)),
|
||||
color: 'red',
|
||||
hoverOnly: true,
|
||||
shown: auth.user?.id !== user.id,
|
||||
},
|
||||
{ id: 'copy-id', action: () => copyId() },
|
||||
{ id: 'copy-permalink', action: () => copyPermalink() },
|
||||
{
|
||||
divider: true,
|
||||
shown: auth.user && isAdmin(auth.user),
|
||||
},
|
||||
{
|
||||
id: 'open-billing',
|
||||
action: () => navigateTo(`/admin/billing/${user.id}`),
|
||||
shown: auth.user && isStaff(auth.user),
|
||||
},
|
||||
{
|
||||
id: 'toggle-affiliate',
|
||||
action: () => toggleAffiliate(user.id),
|
||||
shown: isAdminViewing,
|
||||
remainOnClick: true,
|
||||
color: isAffiliate ? 'red' : 'orange',
|
||||
},
|
||||
{
|
||||
id: 'open-info',
|
||||
action: () => $refs.userDetailsModal.show(),
|
||||
shown: auth.user && isStaff(auth.user),
|
||||
},
|
||||
{
|
||||
id: 'open-analytics',
|
||||
action: () =>
|
||||
navigateTo({
|
||||
path: '/dashboard/analytics',
|
||||
query: { user: user.username || user.id },
|
||||
}),
|
||||
shown: auth.user && isAdmin(auth.user),
|
||||
},
|
||||
{
|
||||
id: 'edit-role',
|
||||
action: () => openRoleEditModal(),
|
||||
shown: auth.user && isAdmin(auth.user),
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
:dropdown-id="`${baseId}-more-options`"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #manage-projects>
|
||||
<BoxIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.profileManageProjectsButton) }}
|
||||
</template>
|
||||
<template #report>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</template>
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
<template #copy-permalink>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyPermalinkButton) }}
|
||||
</template>
|
||||
<template #open-billing>
|
||||
<CurrencyIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.billingButton) }}
|
||||
</template>
|
||||
<template #open-info>
|
||||
<InfoIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.infoButton) }}
|
||||
</template>
|
||||
<template #open-analytics>
|
||||
<ChartIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.analyticsButton) }}
|
||||
</template>
|
||||
<template #toggle-affiliate>
|
||||
<AffiliateIcon aria-hidden="true" />
|
||||
{{
|
||||
formatMessage(
|
||||
isAffiliate ? messages.removeAffiliateButton : messages.setAffiliateButton,
|
||||
)
|
||||
}}
|
||||
</template>
|
||||
<template #edit-role>
|
||||
<EditIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.editRoleButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
</PageHeader>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
|
||||
@@ -535,17 +362,15 @@ import {
|
||||
ButtonStyled,
|
||||
Combobox,
|
||||
commonMessages,
|
||||
ContentPageHeader,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
NavTabs,
|
||||
NewModal,
|
||||
OverflowMenu,
|
||||
PageHeader,
|
||||
ProjectCard,
|
||||
ProjectCardList,
|
||||
TagItem,
|
||||
useCompactNumber,
|
||||
useFormatDateTime,
|
||||
useFormatNumber,
|
||||
@@ -585,8 +410,6 @@ const formatDateTime = useFormatDateTime({
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const baseId = useId()
|
||||
|
||||
const messages = defineMessages({
|
||||
profileProjectsLabel: {
|
||||
id: 'profile.label.projects',
|
||||
@@ -893,12 +716,194 @@ async function copyPermalink() {
|
||||
|
||||
const isAffiliate = computed(() => user.value?.badges & UserBadge.AFFILIATE)
|
||||
const isAdminViewing = computed(() => isAdmin(auth.value.user))
|
||||
const userDetailsModal = useTemplateRef('userDetailsModal')
|
||||
|
||||
async function toggleAffiliate(id) {
|
||||
await client.labrinth.users_v2.patch(id, { badges: user.value.badges ^ (1 << 7) })
|
||||
queryClient.invalidateQueries({ queryKey: ['user', userId] })
|
||||
}
|
||||
|
||||
const profileHeaderSummary = computed(() =>
|
||||
user.value?.bio
|
||||
? user.value.bio
|
||||
: (projects.value?.length ?? 0) === 0
|
||||
? formatMessage(messages.bioFallbackUser)
|
||||
: formatMessage(messages.bioFallbackCreator),
|
||||
)
|
||||
|
||||
const profileHeaderLeading = computed(() => ({
|
||||
type: 'avatar',
|
||||
src: user.value?.avatar_url,
|
||||
alt: user.value?.username,
|
||||
avatarSize: isModrinthUser.value ? '64px' : '96px',
|
||||
circle: true,
|
||||
}))
|
||||
|
||||
const profileHeaderBadges = computed(() => [
|
||||
...(isOfficialAccount.value
|
||||
? [
|
||||
{
|
||||
id: 'official',
|
||||
label: formatMessage(messages.officialAccount),
|
||||
icon: BadgeCheckIcon,
|
||||
iconProps: {
|
||||
fill: 'var(--color-brand-highlight)',
|
||||
},
|
||||
tooltip: formatMessage(messages.officialAccount),
|
||||
class: 'border-brand-highlight bg-brand-highlight text-brand',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isAdminViewing.value && isAffiliate.value
|
||||
? [
|
||||
{
|
||||
id: 'affiliate',
|
||||
label: 'Affiliate',
|
||||
icon: AffiliateIcon,
|
||||
class: 'border-brand-highlight bg-brand-highlight text-brand',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
])
|
||||
|
||||
const profileHeaderMetadata = computed(() => {
|
||||
if (isModrinthUser.value) return []
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'projects',
|
||||
label: formatMessage(messages.profileProjectsLabel, {
|
||||
count: formatCompactNumber(projects.value?.length || 0),
|
||||
countPlural: formatCompactNumberPlural(projects.value?.length || 0),
|
||||
}),
|
||||
icon: BoxIcon,
|
||||
},
|
||||
{
|
||||
id: 'downloads',
|
||||
label: formatMessage(messages.profileDownloadsLabel, {
|
||||
count: formatCompactNumber(sumDownloads.value),
|
||||
countPlural: formatCompactNumberPlural(sumDownloads.value),
|
||||
}),
|
||||
icon: DownloadIcon,
|
||||
tooltip: formatNumber(sumDownloads.value),
|
||||
},
|
||||
{
|
||||
id: 'joined',
|
||||
label: `${formatMessage(messages.profileJoinedLabel)} ${formatRelativeTime(user.value.created)}`,
|
||||
icon: CalendarIcon,
|
||||
tooltip: formatDateTime(user.value.created),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const profileHeaderActions = computed(() => {
|
||||
if (!user.value) return []
|
||||
|
||||
const viewer = auth.value.user
|
||||
const isSelf = viewer?.id === user.value.id
|
||||
|
||||
return [
|
||||
...(isSelf
|
||||
? [
|
||||
{
|
||||
id: 'edit-profile',
|
||||
label: formatMessage(commonMessages.editButton),
|
||||
icon: EditIcon,
|
||||
to: '/settings/profile',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'more',
|
||||
label: 'More options',
|
||||
icon: MoreVerticalIcon,
|
||||
labelHidden: true,
|
||||
type: 'transparent',
|
||||
tooltip: 'More options',
|
||||
menuActions: [
|
||||
{
|
||||
id: 'manage-projects',
|
||||
label: formatMessage(messages.profileManageProjectsButton),
|
||||
icon: BoxIcon,
|
||||
action: () => navigateTo('/dashboard/projects'),
|
||||
shown: isSelf,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: isSelf,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
label: formatMessage(commonMessages.reportButton),
|
||||
icon: ReportIcon,
|
||||
action: () => (viewer ? reportUser(user.value.id) : navigateTo(getSignInRouteObj(route))),
|
||||
color: 'red',
|
||||
shown: viewer?.id !== user.value.id,
|
||||
},
|
||||
{
|
||||
id: 'copy-id',
|
||||
label: formatMessage(commonMessages.copyIdButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => copyId(),
|
||||
},
|
||||
{
|
||||
id: 'copy-permalink',
|
||||
label: formatMessage(commonMessages.copyPermalinkButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => copyPermalink(),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: viewer && isAdmin(viewer),
|
||||
},
|
||||
{
|
||||
id: 'open-billing',
|
||||
label: formatMessage(messages.billingButton),
|
||||
icon: CurrencyIcon,
|
||||
action: () => navigateTo(`/admin/billing/${user.value.id}`),
|
||||
shown: viewer && isStaff(viewer),
|
||||
},
|
||||
{
|
||||
id: 'toggle-affiliate',
|
||||
label: formatMessage(
|
||||
isAffiliate.value ? messages.removeAffiliateButton : messages.setAffiliateButton,
|
||||
),
|
||||
icon: AffiliateIcon,
|
||||
action: () => toggleAffiliate(user.value.id),
|
||||
shown: isAdminViewing.value,
|
||||
remainOnClick: true,
|
||||
color: isAffiliate.value ? 'red' : 'orange',
|
||||
},
|
||||
{
|
||||
id: 'open-info',
|
||||
label: formatMessage(messages.infoButton),
|
||||
icon: InfoIcon,
|
||||
action: () => userDetailsModal.value?.show(),
|
||||
shown: viewer && isStaff(viewer),
|
||||
},
|
||||
{
|
||||
id: 'open-analytics',
|
||||
label: formatMessage(messages.analyticsButton),
|
||||
icon: ChartIcon,
|
||||
action: () =>
|
||||
navigateTo({
|
||||
path: '/dashboard/analytics',
|
||||
query: { user: user.value.username || user.value.id },
|
||||
}),
|
||||
shown: viewer && isAdmin(viewer),
|
||||
},
|
||||
{
|
||||
id: 'edit-role',
|
||||
label: formatMessage(messages.editRoleButton),
|
||||
icon: EditIcon,
|
||||
action: () => openRoleEditModal(),
|
||||
shown: viewer && isAdmin(viewer),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const navLinks = computed(() => [
|
||||
{
|
||||
label: formatMessage(commonMessages.allProjectType),
|
||||
|
||||
Reference in New Issue
Block a user