feat: app server projects modals + modal borders (#5256)
* feat: add modals * NewModal add stroke * update diff type sorting * update icon to match figma * fix lint ci issues * remove formatCategory * feature flag on buttons * prepr * consistent modal borders * intl --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -1002,7 +1002,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
|||||||
<transition name="popup-survey">
|
<transition name="popup-survey">
|
||||||
<div
|
<div
|
||||||
v-if="availableSurvey"
|
v-if="availableSurvey"
|
||||||
class="w-[400px] z-20 fixed -bottom-12 pb-16 right-[--right-bar-width] mr-4 rounded-t-2xl card-shadow bg-bg-raised border-divider border-[1px] border-solid border-b-0 p-4"
|
class="w-[400px] z-20 fixed -bottom-12 pb-16 right-[--right-bar-width] mr-4 rounded-t-2xl card-shadow bg-bg-raised border-surface-5 border-[1px] border-solid border-b-0 p-4"
|
||||||
>
|
>
|
||||||
<h2 class="text-lg font-extrabold mt-0 mb-2">Hey there Modrinth user!</h2>
|
<h2 class="text-lg font-extrabold mt-0 mb-2">Hey there Modrinth user!</h2>
|
||||||
<p class="m-0 leading-tight">
|
<p class="m-0 leading-tight">
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const messages = defineMessages({
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="availableUpdate && !dismissed"
|
v-if="availableUpdate && !dismissed"
|
||||||
class="grid grid-cols-[min-content] fixed card-shadow rounded-2xl top-[--top-bar-height] mt-6 right-6 p-4 z-10 bg-bg-raised border-divider border-solid border-[2px]"
|
class="grid grid-cols-[min-content] fixed card-shadow rounded-2xl top-[--top-bar-height] mt-6 right-6 p-4 z-10 bg-bg-raised border-surface-5 border-solid border-[2px]"
|
||||||
>
|
>
|
||||||
<div class="flex min-w-[25rem] gap-4">
|
<div class="flex min-w-[25rem] gap-4">
|
||||||
<h2 class="whitespace-nowrap text-base text-contrast font-semibold m-0 grow">
|
<h2 class="whitespace-nowrap text-base text-contrast font-semibold m-0 grow">
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const messages = defineMessages({
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-[min-content] fixed card-shadow rounded-2xl top-[--top-bar-height] mt-6 right-6 p-4 z-10 bg-bg-raised border-divider border-solid border-[2px]"
|
class="grid grid-cols-[min-content] fixed card-shadow rounded-2xl top-[--top-bar-height] mt-6 right-6 p-4 z-10 bg-bg-raised border-surface-5 border-solid border-[2px]"
|
||||||
:class="{
|
:class="{
|
||||||
'download-complete': progress === 1,
|
'download-complete': progress === 1,
|
||||||
}"
|
}"
|
||||||
|
|||||||
189
apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue
Normal file
189
apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<NewModal ref="modal" :header="formatMessage(messages.installToPlay)" :closable="true">
|
||||||
|
<div class="flex flex-col gap-6 max-w-[500px]">
|
||||||
|
<Admonition type="info" :header="formatMessage(messages.sharedServerInstance)">
|
||||||
|
{{ formatMessage(messages.serverRequiresMods) }}
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
|
<div v-if="sharedBy?.name" class="flex items-center gap-2 text-sm text-secondary">
|
||||||
|
<Avatar
|
||||||
|
v-if="sharedBy?.icon_url"
|
||||||
|
:src="sharedBy.icon_url"
|
||||||
|
:alt="sharedBy.name"
|
||||||
|
size="24px"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<IntlFormatted :message-id="messages.sharedByToday">
|
||||||
|
<template #~name>
|
||||||
|
<span class="font-semibold text-contrast">{{ sharedBy.name }}</span>
|
||||||
|
</template>
|
||||||
|
</IntlFormatted>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="font-semibold text-contrast">
|
||||||
|
{{ formatMessage(messages.sharedInstance) }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-3 rounded-xl bg-surface-4 p-3">
|
||||||
|
<Avatar :src="project.icon_url" :alt="project.title" size="48px" />
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<span class="font-semibold text-contrast">{{ project.title }}</span>
|
||||||
|
<span class="text-sm text-secondary">
|
||||||
|
{{ loaderDisplay }} {{ project.game_versions?.[0] }}
|
||||||
|
<template v-if="modCount">
|
||||||
|
· {{ formatMessage(messages.modCount, { count: modCount }) }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="handleDecline">
|
||||||
|
<XIcon />
|
||||||
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="handleAccept">
|
||||||
|
<DownloadIcon />
|
||||||
|
{{ formatMessage(messages.installButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Admonition,
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
commonMessages,
|
||||||
|
defineMessages,
|
||||||
|
formatLoader,
|
||||||
|
IntlFormatted,
|
||||||
|
NewModal,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { get_organization, get_team, get_version } from '@/helpers/cache.js'
|
||||||
|
import { install } from '@/store/install.js'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
project: Labrinth.Projects.v2.Project
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const { data: organization } = useQuery({
|
||||||
|
queryKey: computed(() => ['organization', props.project.organization]),
|
||||||
|
queryFn: () => get_organization(props.project.organization!, 'must_revalidate'),
|
||||||
|
enabled: computed(() => !!props.project.organization),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: teamMembers } = useQuery({
|
||||||
|
queryKey: computed(() => ['team', props.project.team]),
|
||||||
|
queryFn: () => get_team(props.project.team, 'must_revalidate'),
|
||||||
|
enabled: computed(() => !!props.project.team && !props.project.organization),
|
||||||
|
})
|
||||||
|
|
||||||
|
const sharedBy = computed(() => {
|
||||||
|
if (organization.value) {
|
||||||
|
return {
|
||||||
|
name: organization.value.name,
|
||||||
|
icon_url: organization.value.icon_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (teamMembers.value) {
|
||||||
|
const owner = teamMembers.value.find((member: { is_owner: boolean }) => member.is_owner)
|
||||||
|
if (owner) {
|
||||||
|
return {
|
||||||
|
name: owner.user.username,
|
||||||
|
icon_url: owner.user.avatar_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const loaderDisplay = computed(() => {
|
||||||
|
const loader = props.project.loaders?.[0]
|
||||||
|
if (!loader) return ''
|
||||||
|
return formatLoader(formatMessage, loader)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch the most recent version to get mod count from dependencies
|
||||||
|
const latestVersionId = computed(() => props.project.versions?.[0] ?? null)
|
||||||
|
const { data: latestVersion } = useQuery({
|
||||||
|
queryKey: computed(() => ['version', latestVersionId.value]),
|
||||||
|
queryFn: () => get_version(latestVersionId.value, 'must_revalidate'),
|
||||||
|
enabled: computed(() => !!latestVersionId.value),
|
||||||
|
})
|
||||||
|
const modCount = computed(() => latestVersion.value?.dependencies?.length)
|
||||||
|
|
||||||
|
async function handleAccept() {
|
||||||
|
hide()
|
||||||
|
try {
|
||||||
|
await install(props.project.id, null, null, 'ProjectPageInstallToPlayModal')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to install project from InstallToPlayModal:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDecline() {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(e?: MouseEvent) {
|
||||||
|
modal.value?.show(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
installToPlay: {
|
||||||
|
id: 'app.modal.install-to-play.header',
|
||||||
|
defaultMessage: 'Install to play',
|
||||||
|
},
|
||||||
|
sharedServerInstance: {
|
||||||
|
id: 'app.modal.install-to-play.shared-server-instance',
|
||||||
|
defaultMessage: 'Shared server instance',
|
||||||
|
},
|
||||||
|
serverRequiresMods: {
|
||||||
|
id: 'app.modal.install-to-play.server-requires-mods',
|
||||||
|
defaultMessage:
|
||||||
|
'This server requires mods to play. Click install to set up the required files from Modrinth.',
|
||||||
|
},
|
||||||
|
sharedByToday: {
|
||||||
|
id: 'app.modal.install-to-play.shared-by-today',
|
||||||
|
defaultMessage: '{name} shared this instance with you today.',
|
||||||
|
},
|
||||||
|
sharedInstance: {
|
||||||
|
id: 'app.modal.install-to-play.shared-instance',
|
||||||
|
defaultMessage: 'Shared instance',
|
||||||
|
},
|
||||||
|
modCount: {
|
||||||
|
id: 'app.modal.install-to-play.mod-count',
|
||||||
|
defaultMessage: '{count, plural, one {# mod} other {# mods}}',
|
||||||
|
},
|
||||||
|
installButton: {
|
||||||
|
id: 'app.modal.install-to-play.install-button',
|
||||||
|
defaultMessage: 'Install',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({ show, hide })
|
||||||
|
</script>
|
||||||
382
apps/app-frontend/src/components/ui/modal/UpdateToPlayModal.vue
Normal file
382
apps/app-frontend/src/components/ui/modal/UpdateToPlayModal.vue
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
<template>
|
||||||
|
<NewModal ref="modal" :header="formatMessage(messages.updateToPlay)" :closable="true" no-padding>
|
||||||
|
<div class="max-w-[500px]">
|
||||||
|
<div class="flex flex-col gap-4 p-4">
|
||||||
|
<Admonition type="warning" :header="formatMessage(messages.updateRequired)">
|
||||||
|
{{ formatMessage(messages.updateRequiredDescription, { name: instance.name }) }}
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
|
<div v-if="diffs.length" class="flex flex-col gap-2">
|
||||||
|
<span v-if="publishedDate" class="text-contrast font-semibold">{{
|
||||||
|
formatMessage(messages.publishedDate, { date: publishedDate })
|
||||||
|
}}</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div v-if="removedCount" class="flex gap-1 items-center">
|
||||||
|
<MinusIcon />
|
||||||
|
{{ formatMessage(messages.removedCount, { count: removedCount }) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="addedCount" class="flex gap-1 items-center">
|
||||||
|
<PlusIcon />
|
||||||
|
{{ formatMessage(messages.addedCount, { count: addedCount }) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="updatedCount" class="flex gap-1 items-center">
|
||||||
|
<RefreshCwIcon />
|
||||||
|
{{ formatMessage(messages.updatedCount, { count: updatedCount }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="diffs.length" class="flex flex-col bg-surface-2 p-4 max-h-[272px] overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="diff in diffs"
|
||||||
|
:key="diff.project_id"
|
||||||
|
class="grid grid-cols-[auto_1fr_1fr_1fr] items-center min-h-10 h-10 gap-2"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col justify-between items-center">
|
||||||
|
<div class="w-[1px] h-2"></div>
|
||||||
|
<PlusIcon v-if="diff.type === 'added'" />
|
||||||
|
<MinusIcon v-else-if="diff.type === 'removed'" />
|
||||||
|
<RefreshCwIcon v-else />
|
||||||
|
<div class="bg-surface-5 w-[1px] h-2 relative top-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-1 col-span-2">
|
||||||
|
<span class="text-sm">{{ formatMessage(diffTypeMessages[diff.type]) }}</span>
|
||||||
|
<span
|
||||||
|
v-if="diff.project"
|
||||||
|
v-tooltip="diff.project.title"
|
||||||
|
class="text-sm text-contrast font-medium truncate"
|
||||||
|
>
|
||||||
|
{{ diff.project.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="getFilename(diff.newVersion) || getFilename(diff.currentVersion)"
|
||||||
|
v-tooltip="getFilename(diff.newVersion) || getFilename(diff.currentVersion)"
|
||||||
|
class="text-xs truncate text-right"
|
||||||
|
>
|
||||||
|
{{ getFilename(diff.newVersion) || getFilename(diff.currentVersion) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex justify-between gap-2">
|
||||||
|
<ButtonStyled color="red" type="transparent">
|
||||||
|
<button @click="handleReport">
|
||||||
|
<ReportIcon />
|
||||||
|
{{ formatMessage(commonMessages.reportButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="handleDecline">
|
||||||
|
<XIcon />
|
||||||
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="handleUpdate">
|
||||||
|
<DownloadIcon />
|
||||||
|
{{ formatMessage(commonMessages.updateButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import {
|
||||||
|
DownloadIcon,
|
||||||
|
MinusIcon,
|
||||||
|
PlusIcon,
|
||||||
|
RefreshCwIcon,
|
||||||
|
ReportIcon,
|
||||||
|
XIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Admonition,
|
||||||
|
ButtonStyled,
|
||||||
|
commonMessages,
|
||||||
|
defineMessages,
|
||||||
|
NewModal,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { get_project, get_project_many, get_version_many } from '@/helpers/cache.js'
|
||||||
|
import { update_managed_modrinth_version } from '@/helpers/profile'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
|
||||||
|
type Dependency = Labrinth.Versions.v3.Dependency
|
||||||
|
type Version = Labrinth.Versions.v2.Version
|
||||||
|
|
||||||
|
interface BaseDiff {
|
||||||
|
project_id: string
|
||||||
|
project?: {
|
||||||
|
title: string
|
||||||
|
icon_url?: string
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
currentVersionId?: string
|
||||||
|
newVersionId?: string
|
||||||
|
currentVersion?: Version
|
||||||
|
newVersion?: Version
|
||||||
|
}
|
||||||
|
interface AddedDiff extends BaseDiff {
|
||||||
|
type: 'added'
|
||||||
|
newVersionId: string
|
||||||
|
}
|
||||||
|
interface RemovedDiff extends BaseDiff {
|
||||||
|
type: 'removed'
|
||||||
|
}
|
||||||
|
interface UpdatedDiff extends BaseDiff {
|
||||||
|
type: 'updated'
|
||||||
|
currentVersionId: string
|
||||||
|
newVersionId: string
|
||||||
|
}
|
||||||
|
type DependencyDiff = AddedDiff | RemovedDiff | UpdatedDiff
|
||||||
|
|
||||||
|
type ProjectInfo = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
icon_url?: string
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { instance } = defineProps<{
|
||||||
|
instance: GameInstance
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
const diffs = ref<DependencyDiff[]>([])
|
||||||
|
const latestVersionId = ref<string | null>(null)
|
||||||
|
const latestVersion = ref<Version | null>(null)
|
||||||
|
|
||||||
|
const removedCount = computed(() => diffs.value.filter((d) => d.type === 'removed').length)
|
||||||
|
const addedCount = computed(() => diffs.value.filter((d) => d.type === 'added').length)
|
||||||
|
const updatedCount = computed(() => diffs.value.filter((d) => d.type === 'updated').length)
|
||||||
|
const publishedDate = computed(() =>
|
||||||
|
latestVersion.value?.date_published ? new Date(latestVersion.value.date_published) : null,
|
||||||
|
)
|
||||||
|
|
||||||
|
function getFilename(version?: Version): string | undefined {
|
||||||
|
return version?.files.find((f) => f.primary)?.filename
|
||||||
|
}
|
||||||
|
|
||||||
|
async function computeDependencyDiffs(
|
||||||
|
currentDeps: Dependency[],
|
||||||
|
latestDeps: Dependency[],
|
||||||
|
): Promise<DependencyDiff[]> {
|
||||||
|
const currentByProject = new Map<string, Dependency>(
|
||||||
|
currentDeps.map((d) => [d.project_id || '', d]),
|
||||||
|
)
|
||||||
|
const latestByProject = new Map<string, Dependency>(
|
||||||
|
latestDeps.map((d) => [d.project_id || '', d]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const diffs: DependencyDiff[] = []
|
||||||
|
|
||||||
|
// Find added and updated dependencies
|
||||||
|
latestByProject.forEach((latestDep, projectId) => {
|
||||||
|
if (!projectId) return
|
||||||
|
const currentDep = currentByProject.get(projectId)
|
||||||
|
if (!currentDep && latestDep.version_id) {
|
||||||
|
diffs.push({ type: 'added', project_id: projectId, newVersionId: latestDep.version_id })
|
||||||
|
} else if (
|
||||||
|
currentDep?.version_id &&
|
||||||
|
latestDep?.version_id &&
|
||||||
|
currentDep?.version_id !== latestDep.version_id
|
||||||
|
) {
|
||||||
|
diffs.push({
|
||||||
|
type: 'updated',
|
||||||
|
project_id: projectId,
|
||||||
|
currentVersionId: currentDep.version_id,
|
||||||
|
newVersionId: latestDep.version_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find removed dependencies
|
||||||
|
currentByProject.forEach((currentDep, projectId) => {
|
||||||
|
if (!projectId) return
|
||||||
|
if (!latestByProject.has(projectId)) {
|
||||||
|
diffs.push({
|
||||||
|
type: 'removed',
|
||||||
|
project_id: projectId,
|
||||||
|
currentVersionId: currentDep.version_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch projects and versions of diffs
|
||||||
|
const allProjectIds = [...new Set(diffs.map((d) => d.project_id).filter(Boolean))]
|
||||||
|
const allVersionIds = [
|
||||||
|
...new Set(
|
||||||
|
[...diffs.map((d) => d.newVersionId), ...diffs.map((d) => d.currentVersionId)].filter(
|
||||||
|
Boolean,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] as string[]
|
||||||
|
const [projects, versions] = await Promise.all([
|
||||||
|
get_project_many(allProjectIds, 'must_revalidate'),
|
||||||
|
get_version_many(allVersionIds, 'must_revalidate'),
|
||||||
|
])
|
||||||
|
|
||||||
|
const projectMap = new Map<string, ProjectInfo>(projects.map((p: ProjectInfo) => [p.id, p]))
|
||||||
|
const versionMap = new Map<string, Version>(versions.map((v: Version) => [v.id, v]))
|
||||||
|
|
||||||
|
return diffs
|
||||||
|
.map((diff) => {
|
||||||
|
const project = projectMap.get(diff.project_id)
|
||||||
|
return {
|
||||||
|
...diff,
|
||||||
|
project: project
|
||||||
|
? { title: project.title, icon_url: project.icon_url, slug: project.slug }
|
||||||
|
: undefined,
|
||||||
|
currentVersion: diff.currentVersionId ? versionMap.get(diff.currentVersionId) : undefined,
|
||||||
|
newVersion: diff.newVersionId ? versionMap.get(diff.newVersionId) : undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const typeOrder = { removed: 0, added: 1, updated: 2 }
|
||||||
|
const typeCompare = typeOrder[a.type] - typeOrder[b.type]
|
||||||
|
if (typeCompare !== 0) return typeCompare
|
||||||
|
|
||||||
|
const aDate = a.newVersion?.date_published || a.currentVersion?.date_published || ''
|
||||||
|
const bDate = b.newVersion?.date_published || b.currentVersion?.date_published || ''
|
||||||
|
return dayjs(bDate).valueOf() - dayjs(aDate).valueOf()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkUpdateAvailable(instance: GameInstance): Promise<DependencyDiff[] | null> {
|
||||||
|
if (!instance.linked_data) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const project = await get_project(instance.linked_data.project_id, 'must_revalidate')
|
||||||
|
if (!project || !project.versions || project.versions.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = await get_version_many(project.versions, 'must_revalidate')
|
||||||
|
const sortedVersions = versions.sort(
|
||||||
|
(a: { date_published: string }, b: { date_published: string }) =>
|
||||||
|
dayjs(b.date_published).valueOf() - dayjs(a.date_published).valueOf(),
|
||||||
|
)
|
||||||
|
|
||||||
|
latestVersion.value = sortedVersions[0]
|
||||||
|
latestVersionId.value = latestVersion.value?.id || null
|
||||||
|
|
||||||
|
const currentVersionId = instance.linked_data.version_id
|
||||||
|
const currentVersion = versions.find((v: { id: string }) => v.id === currentVersionId)
|
||||||
|
|
||||||
|
// Compute dependency diffs between current and latest version
|
||||||
|
if (currentVersion && latestVersion.value) {
|
||||||
|
return await computeDependencyDiffs(
|
||||||
|
currentVersion.dependencies || [],
|
||||||
|
latestVersion.value.dependencies || [],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking for updates:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => instance,
|
||||||
|
async () => {
|
||||||
|
const result = await checkUpdateAvailable(instance)
|
||||||
|
diffs.value = result || []
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
async function handleUpdate() {
|
||||||
|
hide()
|
||||||
|
try {
|
||||||
|
if (latestVersionId.value) {
|
||||||
|
await update_managed_modrinth_version(instance.path, latestVersionId.value)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating instance:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReport() {
|
||||||
|
if (instance.linked_data?.project_id) {
|
||||||
|
openUrl(`https://modrinth.com/report?item=project&itemID=${instance.linked_data.project_id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDecline() {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(e?: MouseEvent) {
|
||||||
|
modal.value?.show(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
updateToPlay: {
|
||||||
|
id: 'app.modal.update-to-play.header',
|
||||||
|
defaultMessage: 'Update to play',
|
||||||
|
},
|
||||||
|
updateRequired: {
|
||||||
|
id: 'app.modal.update-to-play.update-required',
|
||||||
|
defaultMessage: 'Update required',
|
||||||
|
},
|
||||||
|
updateRequiredDescription: {
|
||||||
|
id: 'app.modal.update-to-play.update-required-description',
|
||||||
|
defaultMessage:
|
||||||
|
'An update is required to play {name}. Please update to the latest version to launch the game.',
|
||||||
|
},
|
||||||
|
publishedDate: {
|
||||||
|
id: 'app.modal.update-to-play.published-date',
|
||||||
|
defaultMessage: '{date, date, long}',
|
||||||
|
},
|
||||||
|
removedCount: {
|
||||||
|
id: 'app.modal.update-to-play.removed-count',
|
||||||
|
defaultMessage: '{count} removed',
|
||||||
|
},
|
||||||
|
addedCount: {
|
||||||
|
id: 'app.modal.update-to-play.added-count',
|
||||||
|
defaultMessage: '{count} added',
|
||||||
|
},
|
||||||
|
updatedCount: {
|
||||||
|
id: 'app.modal.update-to-play.updated-count',
|
||||||
|
defaultMessage: '{count} updated',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const diffTypeMessages = defineMessages({
|
||||||
|
added: {
|
||||||
|
id: 'app.modal.update-to-play.diff-type.added',
|
||||||
|
defaultMessage: 'Added',
|
||||||
|
},
|
||||||
|
removed: {
|
||||||
|
id: 'app.modal.update-to-play.diff-type.removed',
|
||||||
|
defaultMessage: 'Removed',
|
||||||
|
},
|
||||||
|
updated: {
|
||||||
|
id: 'app.modal.update-to-play.diff-type.updated',
|
||||||
|
defaultMessage: 'Updated',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({ show, hide })
|
||||||
|
</script>
|
||||||
@@ -5,6 +5,57 @@
|
|||||||
"app.auth-servers.unreachable.header": {
|
"app.auth-servers.unreachable.header": {
|
||||||
"message": "Cannot reach authentication servers"
|
"message": "Cannot reach authentication servers"
|
||||||
},
|
},
|
||||||
|
"app.modal.install-to-play.header": {
|
||||||
|
"message": "Install to play"
|
||||||
|
},
|
||||||
|
"app.modal.install-to-play.install-button": {
|
||||||
|
"message": "Install"
|
||||||
|
},
|
||||||
|
"app.modal.install-to-play.mod-count": {
|
||||||
|
"message": "{count, plural, one {# mod} other {# mods}}"
|
||||||
|
},
|
||||||
|
"app.modal.install-to-play.server-requires-mods": {
|
||||||
|
"message": "This server requires mods to play. Click install to set up the required files from Modrinth."
|
||||||
|
},
|
||||||
|
"app.modal.install-to-play.shared-by-today": {
|
||||||
|
"message": "{name} shared this instance with you today."
|
||||||
|
},
|
||||||
|
"app.modal.install-to-play.shared-instance": {
|
||||||
|
"message": "Shared instance"
|
||||||
|
},
|
||||||
|
"app.modal.install-to-play.shared-server-instance": {
|
||||||
|
"message": "Shared server instance"
|
||||||
|
},
|
||||||
|
"app.modal.update-to-play.added-count": {
|
||||||
|
"message": "{count} added"
|
||||||
|
},
|
||||||
|
"app.modal.update-to-play.diff-type.added": {
|
||||||
|
"message": "Added"
|
||||||
|
},
|
||||||
|
"app.modal.update-to-play.diff-type.removed": {
|
||||||
|
"message": "Removed"
|
||||||
|
},
|
||||||
|
"app.modal.update-to-play.diff-type.updated": {
|
||||||
|
"message": "Updated"
|
||||||
|
},
|
||||||
|
"app.modal.update-to-play.header": {
|
||||||
|
"message": "Update to play"
|
||||||
|
},
|
||||||
|
"app.modal.update-to-play.published-date": {
|
||||||
|
"message": "{date, date, long}"
|
||||||
|
},
|
||||||
|
"app.modal.update-to-play.removed-count": {
|
||||||
|
"message": "{count} removed"
|
||||||
|
},
|
||||||
|
"app.modal.update-to-play.update-required": {
|
||||||
|
"message": "Update required"
|
||||||
|
},
|
||||||
|
"app.modal.update-to-play.update-required-description": {
|
||||||
|
"message": "An update is required to play {name}. Please update to the latest version to launch the game."
|
||||||
|
},
|
||||||
|
"app.modal.update-to-play.updated-count": {
|
||||||
|
"message": "{count} updated"
|
||||||
|
},
|
||||||
"app.settings.developer-mode-enabled": {
|
"app.settings.developer-mode-enabled": {
|
||||||
"message": "Developer mode enabled."
|
"message": "Developer mode enabled."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
>
|
>
|
||||||
<ExportModal ref="exportModal" :instance="instance" />
|
<ExportModal ref="exportModal" :instance="instance" />
|
||||||
<InstanceSettingsModal ref="settingsModal" :instance="instance" :offline="offline" />
|
<InstanceSettingsModal ref="settingsModal" :instance="instance" :offline="offline" />
|
||||||
|
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
|
||||||
|
<ButtonStyled v-if="themeStore.featureFlags.server_project_qa">
|
||||||
|
<button @click="updateToPlayModal.show()">Update to play modal</button>
|
||||||
|
</ButtonStyled>
|
||||||
<ContentPageHeader>
|
<ContentPageHeader>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Avatar :src="icon" :alt="instance.name" size="96px" :tint-by="instance.path" />
|
<Avatar :src="icon" :alt="instance.name" size="96px" :tint-by="instance.path" />
|
||||||
@@ -198,6 +202,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||||
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
||||||
|
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
||||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { get_project, get_version_many } from '@/helpers/cache.js'
|
import { get_project, get_version_many } from '@/helpers/cache.js'
|
||||||
@@ -229,6 +234,7 @@ const instance = ref()
|
|||||||
const modrinthVersions = ref([])
|
const modrinthVersions = ref([])
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const updateToPlayModal = ref()
|
||||||
|
|
||||||
async function fetchInstance() {
|
async function fetchInstance() {
|
||||||
instance.value = await get(route.params.id).catch(handleError)
|
instance.value = await get(route.params.id).catch(handleError)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<InstallToPlayModal ref="installToPlayModal" :project="data" />
|
||||||
<Teleport to="#sidebar-teleport-target">
|
<Teleport to="#sidebar-teleport-target">
|
||||||
<ProjectSidebarCompatibility
|
<ProjectSidebarCompatibility
|
||||||
:project="data"
|
:project="data"
|
||||||
@@ -23,6 +24,9 @@
|
|||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<div class="flex flex-col gap-4 p-6">
|
<div class="flex flex-col gap-4 p-6">
|
||||||
|
<ButtonStyled v-if="themeStore.featureFlags.server_project_qa">
|
||||||
|
<button @click="installToPlayModal.show()">Install to play modal</button>
|
||||||
|
</ButtonStyled>
|
||||||
<InstanceIndicator v-if="instance" :instance="instance" />
|
<InstanceIndicator v-if="instance" :instance="instance" />
|
||||||
<template v-if="data">
|
<template v-if="data">
|
||||||
<Teleport
|
<Teleport
|
||||||
@@ -159,6 +163,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||||
|
import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue'
|
||||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||||
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
|
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
|
||||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
||||||
@@ -186,6 +191,8 @@ const instanceProjects = ref(null)
|
|||||||
const installed = ref(false)
|
const installed = ref(false)
|
||||||
const installedVersion = ref(null)
|
const installedVersion = ref(null)
|
||||||
|
|
||||||
|
const installToPlayModal = ref()
|
||||||
|
|
||||||
const instanceFilters = computed(() => {
|
const instanceFilters = computed(() => {
|
||||||
if (!instance.value) {
|
if (!instance.value) {
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const DEFAULT_FEATURE_FLAGS = {
|
|||||||
worlds_tab: false,
|
worlds_tab: false,
|
||||||
worlds_in_home: true,
|
worlds_in_home: true,
|
||||||
servers_in_app: false,
|
servers_in_app: false,
|
||||||
|
server_project_qa: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const
|
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const
|
||||||
|
|||||||
@@ -90,7 +90,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
class="text-muted flex flex-col gap-2 rounded-lg border border-divider bg-button-bg p-4"
|
class="text-muted flex flex-col gap-2 rounded-lg border border-surface-5 bg-button-bg p-4"
|
||||||
>
|
>
|
||||||
<span>Hi {user.name},</span>
|
<span>Hi {user.name},</span>
|
||||||
<div class="textarea-wrapper">
|
<div class="textarea-wrapper">
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
{{ formatMoney(result?.fee || 0) }}
|
{{ formatMoney(result?.fee || 0) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-b-1 h-0 w-full rounded-full border-b border-solid border-divider" />
|
<div class="border-b-1 h-0 w-full rounded-full border-b border-solid border-surface-5" />
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
|
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-auto">
|
<div class="mt-auto">
|
||||||
<div
|
<div
|
||||||
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-divider pt-4"
|
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-surface-5 pt-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ButtonStyled v-if="lockStatus.expired" @click="retryAcquireLock">
|
<ButtonStyled v-if="lockStatus.expired" @click="retryAcquireLock">
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-auto">
|
<div class="mt-auto">
|
||||||
<div
|
<div
|
||||||
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-divider pt-4"
|
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-surface-5 pt-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ButtonStyled @click="reviewAnyway">
|
<ButtonStyled @click="reviewAnyway">
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
v-else
|
v-else
|
||||||
v-model="message"
|
v-model="message"
|
||||||
type="text"
|
type="text"
|
||||||
class="bg-bg-input h-[400px] w-full rounded-lg border border-solid border-divider px-3 py-2 font-mono text-base"
|
class="bg-bg-input h-[400px] w-full rounded-lg border border-solid border-surface-5 px-3 py-2 font-mono text-base"
|
||||||
placeholder="No message generated."
|
placeholder="No message generated."
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@input="persistState"
|
@input="persistState"
|
||||||
@@ -317,7 +317,7 @@
|
|||||||
<!-- Stage control buttons -->
|
<!-- Stage control buttons -->
|
||||||
<div class="mt-auto">
|
<div class="mt-auto">
|
||||||
<div
|
<div
|
||||||
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-divider pt-4"
|
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-surface-5 pt-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ButtonStyled v-if="!done && !generatedMessage && moderationStore.hasItems">
|
<ButtonStyled v-if="!done && !generatedMessage && moderationStore.hasItems">
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
:open-by-default="!versionFilter"
|
:open-by-default="!versionFilter"
|
||||||
:class="[
|
:class="[
|
||||||
versionFilter ? '' : '!border-solid border-orange bg-bg-orange !text-contrast',
|
versionFilter ? '' : '!border-solid border-orange bg-bg-orange !text-contrast',
|
||||||
'flex flex-col gap-2 rounded-2xl border-2 border-dashed border-divider p-3 transition-all',
|
'flex flex-col gap-2 rounded-2xl border-2 border-dashed border-surface-5 p-3 transition-all',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<p class="m-0 items-center font-bold">
|
<p class="m-0 items-center font-bold">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
ref="menuRef"
|
ref="menuRef"
|
||||||
data-pyro-telepopover-root
|
data-pyro-telepopover-root
|
||||||
class="experimental-styles-within fixed isolate z-[9999] flex w-fit flex-col gap-2 overflow-hidden rounded-2xl border-[1px] border-solid border-divider bg-bg-raised p-2 shadow-lg"
|
class="experimental-styles-within fixed isolate z-[9999] flex w-fit flex-col gap-2 overflow-hidden rounded-2xl border-[1px] border-solid border-surface-5 bg-bg-raised p-2 shadow-lg"
|
||||||
:style="menuStyle"
|
:style="menuStyle"
|
||||||
role="menu"
|
role="menu"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
|||||||
@@ -28,11 +28,13 @@
|
|||||||
'--_width': width,
|
'--_width': width,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="modal-body flex flex-col bg-bg-raised rounded-2xl">
|
<div
|
||||||
|
class="modal-body flex flex-col bg-bg-raised rounded-2xl border border-solid border-surface-5"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!hideHeader"
|
v-if="!hideHeader"
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
class="grid grid-cols-[auto_min-content] items-center gap-4 p-6 border-solid border-0 border-b-[1px] border-divider max-w-full"
|
class="grid grid-cols-[auto_min-content] items-center gap-4 p-6 border-solid border-0 border-b-[1px] border-surface-5 max-w-full"
|
||||||
>
|
>
|
||||||
<div class="flex text-wrap break-words items-center gap-3 min-w-0">
|
<div class="flex text-wrap break-words items-center gap-3 min-w-0">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
ref="menuRef"
|
ref="menuRef"
|
||||||
data-pyro-telepopover-root
|
data-pyro-telepopover-root
|
||||||
class="experimental-styles-within fixed isolate z-[9999] flex w-fit flex-col gap-2 overflow-hidden rounded-2xl border-[1px] border-solid border-divider bg-bg-raised p-2 shadow-lg"
|
class="experimental-styles-within fixed isolate z-[9999] flex w-fit flex-col gap-2 overflow-hidden rounded-2xl border-[1px] border-solid border-surface-5 bg-bg-raised p-2 shadow-lg"
|
||||||
:style="menuStyle"
|
:style="menuStyle"
|
||||||
role="menu"
|
role="menu"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
|||||||
Reference in New Issue
Block a user