fix: app problems post release qa (#5554)

* fix: app problems post release qa

* fix: lint

* fix: dont prefill

* fix: toggle gap

* feat: macs thing

* fix: lint
This commit is contained in:
Calum H.
2026-03-13 20:18:11 +00:00
committed by GitHub
parent 51deba8cd1
commit 86c0937616
31 changed files with 512 additions and 139 deletions

View File

@@ -147,6 +147,7 @@ provideModalBehavior({
const { const {
installationModal, installationModal,
fetchExistingInstanceNames,
handleCreate, handleCreate,
handleBrowseModpacks, handleBrowseModpacks,
searchModpacks, searchModpacks,
@@ -945,6 +946,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
ref="installationModal" ref="installationModal"
type="instance" type="instance"
show-snapshot-toggle show-snapshot-toggle
:fetch-existing-instance-names="fetchExistingInstanceNames"
:search-modpacks="searchModpacks" :search-modpacks="searchModpacks"
:get-project-versions="getProjectVersions" :get-project-versions="getProjectVersions"
@create="handleCreate" @create="handleCreate"

View File

@@ -109,6 +109,7 @@ watch(
const removing = ref(false) const removing = ref(false)
async function removeProfile() { async function removeProfile() {
removing.value = true removing.value = true
const path = props.instance.path
trackEvent('InstanceRemove', { trackEvent('InstanceRemove', {
loader: props.instance.loader, loader: props.instance.loader,
@@ -116,7 +117,7 @@ async function removeProfile() {
}) })
await router.push({ path: '/' }) await router.push({ path: '/' })
await remove(props.instance.path).catch(handleError) await remove(path).catch(handleError)
} }
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -246,7 +246,6 @@
:options="options" :options="options"
:offline="offline" :offline="offline"
:playing="playing" :playing="playing"
:versions="modrinthVersions"
:installed="instance.install_stage !== 'installed'" :installed="instance.install_stage !== 'installed'"
:is-server-instance="isServerInstance" :is-server-instance="isServerInstance"
:open-settings="() => settingsModal?.show(1)" :open-settings="() => settingsModal?.show(1)"
@@ -332,7 +331,7 @@ import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.v
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.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_v3, get_version_many } from '@/helpers/cache.js' import { get_project_v3 } from '@/helpers/cache.js'
import { process_listener, profile_listener } from '@/helpers/events' import { process_listener, profile_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process' import { get_by_profile_path } from '@/helpers/process'
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile' import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
@@ -362,7 +361,6 @@ window.addEventListener('online', () => {
}) })
const instance = ref<GameInstance>() const instance = ref<GameInstance>()
const modrinthVersions = ref<Labrinth.Versions.v2.Version[]>([])
const playing = ref(false) const playing = ref(false)
const loading = ref(false) const loading = ref(false)
const exportModal = ref<InstanceType<typeof ExportModal>>() const exportModal = ref<InstanceType<typeof ExportModal>>()
@@ -385,7 +383,6 @@ const loadingServerPing = ref(false)
async function fetchInstance() { async function fetchInstance() {
isServerInstance.value = false isServerInstance.value = false
linkedProjectV3.value = undefined linkedProjectV3.value = undefined
modrinthVersions.value = []
ping.value = undefined ping.value = undefined
playersOnline.value = undefined playersOnline.value = undefined
loadingServerPing.value = false loadingServerPing.value = false
@@ -402,14 +399,6 @@ async function fetchInstance() {
if (linkedProjectV3.value?.minecraft_server != null) { if (linkedProjectV3.value?.minecraft_server != null) {
isServerInstance.value = true isServerInstance.value = true
} }
if (linkedProjectV3.value && linkedProjectV3.value.versions) {
const versions = await get_version_many(linkedProjectV3.value.versions, 'must_revalidate')
modrinthVersions.value = versions.sort(
(a: Labrinth.Versions.v2.Version, b: Labrinth.Versions.v2.Version) =>
dayjs(b.date_published).valueOf() - dayjs(a.date_published).valueOf(),
)
}
} catch (error) { } catch (error) {
handleError(error as Error) handleError(error as Error)
} }
@@ -605,18 +594,23 @@ const handleOptionsClick = async (args: { option: string; item: unknown }) => {
const unlistenProfiles = await profile_listener( const unlistenProfiles = await profile_listener(
async (event: { profile_path_id: string; event: string }) => { async (event: { profile_path_id: string; event: string }) => {
if (event.profile_path_id === route.params.id) { if (event.profile_path_id !== route.params.id) return
if (event.event === 'removed') { if (event.event === 'removed' || route.path === '/') {
await router.push({ if (route.path !== '/') {
path: '/', await router.push({ path: '/' })
})
return
} }
instance.value = await get(route.params.id as string).catch(handleError) return
if (!instance.value?.linked_data?.project_id) { }
linkedProjectV3.value = undefined instance.value = await get(route.params.id as string).catch((err) => {
isServerInstance.value = false if (String(err).includes('not managed')) {
router.push({ path: '/' })
return undefined
} }
return handleError(err)
})
if (!instance.value?.linked_data?.project_id) {
linkedProjectV3.value = undefined
isServerInstance.value = false
} }
}, },
) )

View File

@@ -150,10 +150,6 @@ const props = defineProps({
return false return false
}, },
}, },
versions: {
type: Array,
required: true,
},
installed: { installed: {
type: Boolean, type: Boolean,
default() { default() {

View File

@@ -36,7 +36,7 @@
: (updatingProject?.version?.id ?? '') : (updatingProject?.version?.id ?? '')
" "
:is-app="true" :is-app="true"
:is-modpack="updatingModpack" :project-type="updatingModpack ? 'modpack' : updatingProject?.project_type"
:project-icon-url=" :project-icon-url="
updatingModpack ? linkedModpackProject?.icon_url : updatingProject?.project?.icon_url updatingModpack ? linkedModpackProject?.icon_url : updatingProject?.project?.icon_url
" "
@@ -73,6 +73,7 @@ import {
type OverflowMenuOption, type OverflowMenuOption,
provideAppBackup, provideAppBackup,
provideContentManager, provideContentManager,
useDebugLogger,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { ContentCardLayout as ContentPageLayout } from '@modrinth/ui' import { ContentCardLayout as ContentPageLayout } from '@modrinth/ui'
@@ -165,10 +166,10 @@ const { formatMessage } = useVIntl()
const { handleError, addNotification } = injectNotificationManager() const { handleError, addNotification } = injectNotificationManager()
const { installingItems } = injectContentInstall() const { installingItems } = injectContentInstall()
const router = useRouter() const router = useRouter()
const debug = useDebugLogger('Mods:ContentUpdate')
const props = defineProps<{ const props = defineProps<{
instance: GameInstance instance: GameInstance
versions: Labrinth.Versions.v2.Version[]
isServerInstance?: boolean isServerInstance?: boolean
openSettings?: () => void openSettings?: () => void
}>() }>()
@@ -349,6 +350,18 @@ async function handleUpdate(id: string) {
const item = projects.value.find((p) => p.file_name === id) const item = projects.value.find((p) => p.file_name === id)
if (!item?.has_update || !item.project?.id || !item.version?.id) return if (!item?.has_update || !item.project?.id || !item.version?.id) return
debug('handleUpdate triggered', {
fileName: item.file_name,
projectType: item.project_type,
projectId: item.project.id,
projectTitle: item.project.title,
currentVersionId: item.version.id,
currentVersionNumber: item.version.version_number,
updateVersionId: item.update_version_id,
instanceGameVersion: props.instance.game_version,
instanceLoader: props.instance.loader,
})
updatingModpack.value = false updatingModpack.value = false
updatingProject.value = item updatingProject.value = item
updatingProjectVersions.value = [] updatingProjectVersions.value = []
@@ -365,7 +378,24 @@ async function handleUpdate(id: string) {
loadingVersions.value = false loadingVersions.value = false
if (!versions) return if (!versions) {
debug('handleUpdate: no versions returned', { projectId: item.project.id })
return
}
debug('handleUpdate: fetched versions', {
projectId: item.project.id,
projectType: item.project_type,
totalVersions: versions.length,
versionSample: versions.slice(0, 5).map((v) => ({
id: v.id,
number: v.version_number,
loaders: v.loaders,
gameVersions: v.game_versions,
})),
currentVersionInList: versions.some((v) => v.id === item.version?.id),
updateVersionInList: versions.some((v) => v.id === item.update_version_id),
})
versions.sort( versions.sort(
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(), (a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
@@ -493,9 +523,17 @@ function handleModpackUpdateCancel() {
pendingModpackUpdateVersion.value = null pendingModpackUpdateVersion.value = null
} }
async function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version) { async function handleModalUpdate(
selectedVersion: Labrinth.Versions.v2.Version,
event?: MouseEvent,
) {
if (updatingModpack.value) { if (updatingModpack.value) {
handleModpackUpdateRequest(selectedVersion) if (event?.shiftKey) {
pendingModpackUpdateVersion.value = selectedVersion
await handleModpackUpdateConfirm()
} else {
handleModpackUpdateRequest(selectedVersion)
}
} else if (updatingProject.value) { } else if (updatingProject.value) {
const mod = updatingProject.value const mod = updatingProject.value

View File

@@ -1,7 +1,5 @@
<template>{{ instance.name }} overview</template> <template>{{ instance.name }} overview</template>
<script setup lang="ts"> <script setup lang="ts">
import type { Version } from '@modrinth/utils'
import type ContextMenu from '@/components/ui/ContextMenu.vue' import type ContextMenu from '@/components/ui/ContextMenu.vue'
import type { GameInstance } from '@/helpers/types' import type { GameInstance } from '@/helpers/types'
@@ -10,7 +8,6 @@ defineProps<{
options: InstanceType<typeof ContextMenu> options: InstanceType<typeof ContextMenu>
offline: boolean offline: boolean
playing: boolean playing: boolean
versions: Version[]
installed: boolean installed: boolean
}>() }>()
</script> </script>

View File

@@ -134,7 +134,6 @@ import {
RadialHeader, RadialHeader,
StyledInput, StyledInput,
} from '@modrinth/ui' } from '@modrinth/ui'
import type { Version } from '@modrinth/utils'
import { platform } from '@tauri-apps/plugin-os' import { platform } from '@tauri-apps/plugin-os'
import { computed, onUnmounted, ref, watch } from 'vue' import { computed, onUnmounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
@@ -202,7 +201,6 @@ const props = defineProps<{
options: InstanceType<typeof ContextMenu> | null options: InstanceType<typeof ContextMenu> | null
offline: boolean offline: boolean
playing: boolean playing: boolean
versions: Version[]
installed: boolean installed: boolean
}>() }>()

View File

@@ -13,9 +13,14 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
const router = useRouter() const router = useRouter()
const installationModal = useTemplateRef('installationModal') const installationModal = useTemplateRef('installationModal')
provide('showCreationModal', async () => {
async function fetchExistingInstanceNames(): Promise<string[]> {
const instances = await list().catch(handleError) const instances = await list().catch(handleError)
installationModal.value?.show(instances?.length ?? 0) return instances?.map((i) => i.name) ?? []
}
provide('showCreationModal', () => {
installationModal.value?.show()
}) })
async function handleCreate(config: CreationFlowContextValue) { async function handleCreate(config: CreationFlowContextValue) {
@@ -57,9 +62,10 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
? null ? null
: (config.selectedLoaderVersion.value ?? config.loaderVersionType.value) : (config.selectedLoaderVersion.value ?? config.loaderVersionType.value)
const iconPath = config.instanceIconPath.value ?? null const iconPath = config.instanceIconPath.value ?? null
const name = config.instanceName.value.trim() || config.autoInstanceName.value
await create( await create(
config.instanceName.value, name,
config.selectedGameVersion.value, config.selectedGameVersion.value,
loader, loader,
loaderVersion, loaderVersion,
@@ -68,7 +74,7 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
).catch(handleError) ).catch(handleError)
trackEvent('InstanceCreate', { trackEvent('InstanceCreate', {
profile_name: config.instanceName.value, profile_name: name,
game_version: config.selectedGameVersion.value, game_version: config.selectedGameVersion.value,
loader, loader,
loader_version: loaderVersion, loader_version: loaderVersion,
@@ -102,6 +108,7 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
return { return {
installationModal, installationModal,
fetchExistingInstanceNames,
handleCreate, handleCreate,
handleBrowseModpacks, handleBrowseModpacks,
searchModpacks, searchModpacks,

View File

@@ -11,7 +11,9 @@ import _ArchiveIcon from './icons/archive.svg?component'
import _ArrowBigRightDashIcon from './icons/arrow-big-right-dash.svg?component' import _ArrowBigRightDashIcon from './icons/arrow-big-right-dash.svg?component'
import _ArrowBigUpDashIcon from './icons/arrow-big-up-dash.svg?component' import _ArrowBigUpDashIcon from './icons/arrow-big-up-dash.svg?component'
import _ArrowDownIcon from './icons/arrow-down.svg?component' import _ArrowDownIcon from './icons/arrow-down.svg?component'
import _ArrowDownAZIcon from './icons/arrow-down-a-z.svg?component'
import _ArrowDownLeftIcon from './icons/arrow-down-left.svg?component' import _ArrowDownLeftIcon from './icons/arrow-down-left.svg?component'
import _ArrowDownZAIcon from './icons/arrow-down-z-a.svg?component'
import _ArrowLeftIcon from './icons/arrow-left.svg?component' import _ArrowLeftIcon from './icons/arrow-left.svg?component'
import _ArrowLeftRightIcon from './icons/arrow-left-right.svg?component' import _ArrowLeftRightIcon from './icons/arrow-left-right.svg?component'
import _ArrowUpIcon from './icons/arrow-up.svg?component' import _ArrowUpIcon from './icons/arrow-up.svg?component'
@@ -38,6 +40,7 @@ import _BracesIcon from './icons/braces.svg?component'
import _BrushCleaningIcon from './icons/brush-cleaning.svg?component' import _BrushCleaningIcon from './icons/brush-cleaning.svg?component'
import _BugIcon from './icons/bug.svg?component' import _BugIcon from './icons/bug.svg?component'
import _CalendarIcon from './icons/calendar.svg?component' import _CalendarIcon from './icons/calendar.svg?component'
import _CalendarArrowDownIcon from './icons/calendar-arrow-down.svg?component'
import _CardIcon from './icons/card.svg?component' import _CardIcon from './icons/card.svg?component'
import _ChangeSkinIcon from './icons/change-skin.svg?component' import _ChangeSkinIcon from './icons/change-skin.svg?component'
import _ChartIcon from './icons/chart.svg?component' import _ChartIcon from './icons/chart.svg?component'
@@ -54,6 +57,8 @@ import _ClearIcon from './icons/clear.svg?component'
import _ClientIcon from './icons/client.svg?component' import _ClientIcon from './icons/client.svg?component'
import _ClipboardCopyIcon from './icons/clipboard-copy.svg?component' import _ClipboardCopyIcon from './icons/clipboard-copy.svg?component'
import _ClockIcon from './icons/clock.svg?component' import _ClockIcon from './icons/clock.svg?component'
import _ClockArrowDownIcon from './icons/clock-arrow-down.svg?component'
import _ClockArrowUpIcon from './icons/clock-arrow-up.svg?component'
import _CloudIcon from './icons/cloud.svg?component' import _CloudIcon from './icons/cloud.svg?component'
import _CodeIcon from './icons/code.svg?component' import _CodeIcon from './icons/code.svg?component'
import _CoffeeIcon from './icons/coffee.svg?component' import _CoffeeIcon from './icons/coffee.svg?component'
@@ -358,6 +363,7 @@ import _ToggleLeftIcon from './icons/toggle-left.svg?component'
import _ToggleRightIcon from './icons/toggle-right.svg?component' import _ToggleRightIcon from './icons/toggle-right.svg?component'
import _TransferIcon from './icons/transfer.svg?component' import _TransferIcon from './icons/transfer.svg?component'
import _TrashIcon from './icons/trash.svg?component' import _TrashIcon from './icons/trash.svg?component'
import _TrashExclamationIcon from './icons/trash-exclamation.svg?component'
import _TriangleAlertIcon from './icons/triangle-alert.svg?component' import _TriangleAlertIcon from './icons/triangle-alert.svg?component'
import _UnderlineIcon from './icons/underline.svg?component' import _UnderlineIcon from './icons/underline.svg?component'
import _UndoIcon from './icons/undo.svg?component' import _UndoIcon from './icons/undo.svg?component'
@@ -390,7 +396,9 @@ export const ArchiveIcon = _ArchiveIcon
export const ArrowBigRightDashIcon = _ArrowBigRightDashIcon export const ArrowBigRightDashIcon = _ArrowBigRightDashIcon
export const ArrowBigUpDashIcon = _ArrowBigUpDashIcon export const ArrowBigUpDashIcon = _ArrowBigUpDashIcon
export const ArrowDownIcon = _ArrowDownIcon export const ArrowDownIcon = _ArrowDownIcon
export const ArrowDownAZIcon = _ArrowDownAZIcon
export const ArrowDownLeftIcon = _ArrowDownLeftIcon export const ArrowDownLeftIcon = _ArrowDownLeftIcon
export const ArrowDownZAIcon = _ArrowDownZAIcon
export const ArrowLeftIcon = _ArrowLeftIcon export const ArrowLeftIcon = _ArrowLeftIcon
export const ArrowLeftRightIcon = _ArrowLeftRightIcon export const ArrowLeftRightIcon = _ArrowLeftRightIcon
export const ArrowUpIcon = _ArrowUpIcon export const ArrowUpIcon = _ArrowUpIcon
@@ -417,6 +425,7 @@ export const BracesIcon = _BracesIcon
export const BrushCleaningIcon = _BrushCleaningIcon export const BrushCleaningIcon = _BrushCleaningIcon
export const BugIcon = _BugIcon export const BugIcon = _BugIcon
export const CalendarIcon = _CalendarIcon export const CalendarIcon = _CalendarIcon
export const CalendarArrowDownIcon = _CalendarArrowDownIcon
export const CardIcon = _CardIcon export const CardIcon = _CardIcon
export const ChangeSkinIcon = _ChangeSkinIcon export const ChangeSkinIcon = _ChangeSkinIcon
export const ChartIcon = _ChartIcon export const ChartIcon = _ChartIcon
@@ -433,6 +442,8 @@ export const ClearIcon = _ClearIcon
export const ClientIcon = _ClientIcon export const ClientIcon = _ClientIcon
export const ClipboardCopyIcon = _ClipboardCopyIcon export const ClipboardCopyIcon = _ClipboardCopyIcon
export const ClockIcon = _ClockIcon export const ClockIcon = _ClockIcon
export const ClockArrowDownIcon = _ClockArrowDownIcon
export const ClockArrowUpIcon = _ClockArrowUpIcon
export const CloudIcon = _CloudIcon export const CloudIcon = _CloudIcon
export const CodeIcon = _CodeIcon export const CodeIcon = _CodeIcon
export const CoffeeIcon = _CoffeeIcon export const CoffeeIcon = _CoffeeIcon
@@ -737,6 +748,7 @@ export const ToggleLeftIcon = _ToggleLeftIcon
export const ToggleRightIcon = _ToggleRightIcon export const ToggleRightIcon = _ToggleRightIcon
export const TransferIcon = _TransferIcon export const TransferIcon = _TransferIcon
export const TrashIcon = _TrashIcon export const TrashIcon = _TrashIcon
export const TrashExclamationIcon = _TrashExclamationIcon
export const TriangleAlertIcon = _TriangleAlertIcon export const TriangleAlertIcon = _TriangleAlertIcon
export const UnderlineIcon = _UnderlineIcon export const UnderlineIcon = _UnderlineIcon
export const UndoIcon = _UndoIcon export const UndoIcon = _UndoIcon

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-arrow-down-a-z"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m3 16 4 4 4-4" />
<path d="M7 20V4" />
<path d="M20 8h-5" />
<path d="M15 10V6.5a2.5 2.5 0 0 1 5 0V10" />
<path d="M15 14h5l-5 6h5" />
</svg>

After

Width:  |  Height:  |  Size: 448 B

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-arrow-down-z-a"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m3 16 4 4 4-4" />
<path d="M7 4v16" />
<path d="M15 4h5l-5 6h5" />
<path d="M15 20v-3.5a2.5 2.5 0 0 1 5 0V20" />
<path d="M20 18h-5" />
</svg>

After

Width:  |  Height:  |  Size: 449 B

View File

@@ -0,0 +1,20 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-calendar-arrow-down"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m14 18 4 4 4-4" />
<path d="M16 2v4" />
<path d="M18 14v8" />
<path d="M21 11.354V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7.343" />
<path d="M3 10h18" />
<path d="M8 2v4" />
</svg>

After

Width:  |  Height:  |  Size: 503 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-clock-arrow-down"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 6v6l2 1" />
<path d="M12.337 21.994a10 10 0 1 1 9.588-8.767" />
<path d="m14 18 4 4 4-4" />
<path d="M18 14v8" />
</svg>

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-clock-arrow-up"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 6v6l1.56.78" />
<path d="M13.227 21.925a10 10 0 1 1 8.767-9.588" />
<path d="m14 18 4-4 4 4" />
<path d="M18 22v-8" />
</svg>

After

Width:  |  Height:  |  Size: 434 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide">
<g transform="matrix(1,0,0,1,0,1)">
<path d="M12,9L12,13" />
</g>
<g transform="matrix(1,0,0,1,0,1)">
<path d="M12,17L12.01,17" />
</g>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
<path d="M3 6h18" />
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>

After

Width:  |  Height:  |  Size: 480 B

View File

@@ -2,6 +2,7 @@
--surface-1: #ebebeb; --surface-1: #ebebeb;
--surface-1-5: #ededed; --surface-1-5: #ededed;
--surface-2: #f5f5f5; --surface-2: #f5f5f5;
--surface-2-5: #eef1f5;
--surface-3: #f8f8f8; --surface-3: #f8f8f8;
--surface-4: #ffffff; --surface-4: #ffffff;
--surface-5: #dddddd; --surface-5: #dddddd;
@@ -226,6 +227,7 @@ html {
--surface-1: #16181c; --surface-1: #16181c;
--surface-1-5: #1a1c20; --surface-1-5: #1a1c20;
--surface-2: #1d1f23; --surface-2: #1d1f23;
--surface-2-5: #222429;
--surface-3: #27292e; --surface-3: #27292e;
--surface-4: #34363c; --surface-4: #34363c;
--surface-5: #42444a; --surface-5: #42444a;
@@ -398,6 +400,7 @@ html {
--surface-1: #000000; --surface-1: #000000;
--surface-1-5: #050506; --surface-1-5: #050506;
--surface-2: #09090a; --surface-2: #09090a;
--surface-2-5: #0c0d11;
--surface-3: #101013; --surface-3: #101013;
--surface-4: #1b1b20; --surface-4: #1b1b20;
--surface-5: #25262b; --surface-5: #25262b;
@@ -419,5 +422,6 @@ html {
} }
.retro-mode { .retro-mode {
--surface-2-5: #3a3c3e;
--brand-gradient-strong-bg: #3a3b38; --brand-gradient-strong-bg: #3a3b38;
} }

View File

@@ -9,6 +9,7 @@ const config: Config = {
1: 'var(--surface-1)', 1: 'var(--surface-1)',
1.5: 'var(--surface-1-5)', 1.5: 'var(--surface-1-5)',
2: 'var(--surface-2)', 2: 'var(--surface-2)',
2.5: 'var(--surface-2-5)',
3: 'var(--surface-3)', 3: 'var(--surface-3)',
4: 'var(--surface-4)', 4: 'var(--surface-4)',
5: 'var(--surface-5)', 5: 'var(--surface-5)',

View File

@@ -7,7 +7,7 @@
:disabled="disabled" :disabled="disabled"
class="relative inline-flex shrink-0 rounded-full m-0 transition-all duration-200 cursor-pointer border-none" class="relative inline-flex shrink-0 rounded-full m-0 transition-all duration-200 cursor-pointer border-none"
:class="[ :class="[
small ? 'h-5 !w-[38px]' : 'h-8 !w-[52px]', small ? 'h-5 !w-[40px]' : 'h-8 !w-[60px]',
modelValue ? 'bg-brand' : 'bg-button-bg', modelValue ? 'bg-brand' : 'bg-button-bg',
disabled ? 'opacity-50 cursor-not-allowed' : 'btn-wrapper', disabled ? 'opacity-50 cursor-not-allowed' : 'btn-wrapper',
]" ]"
@@ -16,11 +16,11 @@
<span <span
class="absolute rounded-full transition-all duration-200" class="absolute rounded-full transition-all duration-200"
:class="[ :class="[
small ? 'w-4 h-4 top-0.5 left-0.5' : 'w-[18px] h-[18px] top-[7px] left-[7px]', small ? 'w-4 h-4 top-0.5 left-0.5' : 'w-[24px] h-[24px] top-1 left-1',
modelValue modelValue
? small ? small
? 'translate-x-[18px] bg-black/90' ? 'translate-x-5 bg-black/90'
: 'translate-x-5 bg-black/90' : 'translate-x-7 bg-black/90'
: 'bg-gray', : 'bg-gray',
]" ]"
/> />

View File

@@ -22,7 +22,10 @@
<!-- Instance-specific: Name field --> <!-- Instance-specific: Name field -->
<div v-if="ctx.flowType === 'instance'" class="flex flex-col gap-2"> <div v-if="ctx.flowType === 'instance'" class="flex flex-col gap-2">
<span class="font-semibold text-contrast">Name</span> <span class="font-semibold text-contrast">Name</span>
<StyledInput v-model="ctx.instanceName.value" placeholder="Enter instance name" /> <StyledInput
v-model="ctx.instanceName.value"
:placeholder="ctx.autoInstanceName.value || 'Enter instance name'"
/>
</div> </div>
<!-- Loader chips --> <!-- Loader chips -->

View File

@@ -3,6 +3,7 @@ import { computed, type ComputedRef, type Ref, ref, type ShallowRef, watch } fro
import type { ComponentExposed } from 'vue-component-type-helpers' import type { ComponentExposed } from 'vue-component-type-helpers'
import { useDebugLogger } from '#ui/composables/debug-logger' import { useDebugLogger } from '#ui/composables/debug-logger'
import { formatLoaderLabel } from '#ui/utils/loaders'
import { createContext } from '../../../providers' import { createContext } from '../../../providers'
import type { ImportableLauncher } from '../../../providers/instance-import' import type { ImportableLauncher } from '../../../providers/instance-import'
@@ -77,6 +78,7 @@ export interface CreationFlowContextValue {
// Instance-specific state // Instance-specific state
instanceName: Ref<string> instanceName: Ref<string>
autoInstanceName: ComputedRef<string>
instanceIcon: Ref<File | null> instanceIcon: Ref<File | null>
instanceIconUrl: Ref<string | null> instanceIconUrl: Ref<string | null>
instanceIconPath: Ref<string | null> instanceIconPath: Ref<string | null>
@@ -121,7 +123,7 @@ export interface CreationFlowContextValue {
onBack: (() => void) | null onBack: (() => void) | null
// Methods // Methods
reset: (instanceCount?: number) => void reset: (instanceCount?: number) => Promise<void>
setSetupType: (type: SetupType) => void setSetupType: (type: SetupType) => void
setImportMode: () => void setImportMode: () => void
browseModpacks: () => void browseModpacks: () => void
@@ -138,7 +140,6 @@ export const [injectCreationFlowContext, provideCreationFlowContext] =
// TODO: replace with actual world count from the world list once available // TODO: replace with actual world count from the world list once available
let worldCounter = 0 let worldCounter = 0
let instanceCounter = 0
export interface CreationFlowOptions { export interface CreationFlowOptions {
availableLoaders?: string[] availableLoaders?: string[]
@@ -147,6 +148,7 @@ export interface CreationFlowOptions {
isInitialSetup?: boolean isInitialSetup?: boolean
initialLoader?: string initialLoader?: string
initialGameVersion?: string initialGameVersion?: string
fetchExistingInstanceNames?: () => Promise<string[]>
onBack?: () => void onBack?: () => void
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult> searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
getProjectVersions?: (projectId: string) => Promise<{ id: string }[]> getProjectVersions?: (projectId: string) => Promise<{ id: string }[]>
@@ -183,6 +185,8 @@ export function createCreationFlowContext(
// Instance-specific state // Instance-specific state
const instanceName = ref('') const instanceName = ref('')
const existingInstanceNames = ref<string[]>([])
const fetchExistingInstanceNames = options.fetchExistingInstanceNames ?? null
const instanceIcon = ref<File | null>(null) const instanceIcon = ref<File | null>(null)
const instanceIconUrl = ref<string | null>(null) const instanceIconUrl = ref<string | null>(null)
const instanceIconPath = ref<string | null>(null) const instanceIconPath = ref<string | null>(null)
@@ -200,6 +204,24 @@ export function createCreationFlowContext(
const selectedLoaderVersion = ref<string | null>(null) const selectedLoaderVersion = ref<string | null>(null)
const showSnapshots = ref(false) const showSnapshots = ref(false)
const autoInstanceName = computed(() => {
const loader = selectedLoader.value
const version = selectedGameVersion.value
if (!version) return ''
const loaderName = loader ? formatLoaderLabel(loader) : 'Vanilla'
const baseName = `${loaderName} ${version}`
const names = new Set(existingInstanceNames.value)
if (!names.has(baseName)) return baseName
let counter = 1
while (names.has(`${baseName} (${counter})`)) {
counter++
}
return `${baseName} (${counter})`
})
const modpackSelection = ref<ModpackSelection | null>(null) const modpackSelection = ref<ModpackSelection | null>(null)
const modpackFile = ref<File | null>(null) const modpackFile = ref<File | null>(null)
const modpackFilePath = ref<string | null>(null) const modpackFilePath = ref<string | null>(null)
@@ -227,15 +249,14 @@ export function createCreationFlowContext(
() => setupType.value === 'vanilla' || selectedLoader.value === 'vanilla', () => setupType.value === 'vanilla' || selectedLoader.value === 'vanilla',
) )
function reset(instanceCount?: number) { async function reset() {
if (fetchExistingInstanceNames) {
existingInstanceNames.value = await fetchExistingInstanceNames()
}
setupType.value = null setupType.value = null
isImportMode.value = false isImportMode.value = false
worldCounter++ worldCounter++
worldName.value = flowType === 'world' ? `World ${worldCounter}` : '' worldName.value = flowType === 'world' ? `World ${worldCounter}` : ''
if (instanceCount != null) {
instanceCounter = instanceCount
}
instanceCounter++
gamemode.value = 'survival' gamemode.value = 'survival'
difficulty.value = 'normal' difficulty.value = 'normal'
worldSeed.value = '' worldSeed.value = ''
@@ -245,7 +266,7 @@ export function createCreationFlowContext(
generatorSettingsCustom.value = '' generatorSettingsCustom.value = ''
// Instance-specific // Instance-specific
instanceName.value = flowType === 'instance' ? `New instance (${instanceCounter})` : '' instanceName.value = ''
instanceIconUrl.value = null instanceIconUrl.value = null
instanceIcon.value = null instanceIcon.value = null
instanceIconPath.value = null instanceIconPath.value = null
@@ -356,6 +377,7 @@ export function createCreationFlowContext(
generatorSettingsMode, generatorSettingsMode,
generatorSettingsCustom, generatorSettingsCustom,
instanceName, instanceName,
autoInstanceName,
instanceIcon, instanceIcon,
instanceIconUrl, instanceIconUrl,
instanceIconPath, instanceIconPath,

View File

@@ -31,6 +31,7 @@ const props = withDefaults(
isInitialSetup?: boolean isInitialSetup?: boolean
initialLoader?: string initialLoader?: string
initialGameVersion?: string initialGameVersion?: string
fetchExistingInstanceNames?: () => Promise<string[]>
onBack?: (() => void) | null onBack?: (() => void) | null
fade?: 'standard' | 'warning' | 'danger' fade?: 'standard' | 'warning' | 'danger'
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult> searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
@@ -44,6 +45,7 @@ const props = withDefaults(
isInitialSetup: false, isInitialSetup: false,
initialLoader: undefined, initialLoader: undefined,
initialGameVersion: undefined, initialGameVersion: undefined,
fetchExistingInstanceNames: undefined,
onBack: null, onBack: null,
}, },
) )
@@ -69,6 +71,7 @@ const ctx = createCreationFlowContext(
isInitialSetup: props.isInitialSetup, isInitialSetup: props.isInitialSetup,
initialLoader: props.initialLoader, initialLoader: props.initialLoader,
initialGameVersion: props.initialGameVersion, initialGameVersion: props.initialGameVersion,
fetchExistingInstanceNames: props.fetchExistingInstanceNames,
onBack: props.onBack ?? undefined, onBack: props.onBack ?? undefined,
searchModpacks: props.searchModpacks, searchModpacks: props.searchModpacks,
getProjectVersions: props.getProjectVersions, getProjectVersions: props.getProjectVersions,
@@ -76,8 +79,8 @@ const ctx = createCreationFlowContext(
) )
provideCreationFlowContext(ctx) provideCreationFlowContext(ctx)
function show(instanceCount?: number) { async function show() {
ctx.reset(instanceCount) await ctx.reset()
modal.value?.setStage(0) modal.value?.setStage(0)
modal.value?.show() modal.value?.show()
} }

View File

@@ -6,7 +6,6 @@ import CustomSetupStage from '../components/CustomSetupStage.vue'
import { type CreationFlowContextValue, flowTypeHeadings } from '../creation-flow-context' import { type CreationFlowContextValue, flowTypeHeadings } from '../creation-flow-context'
function isForwardBlocked(ctx: CreationFlowContextValue): boolean { function isForwardBlocked(ctx: CreationFlowContextValue): boolean {
if (ctx.flowType === 'instance' && !ctx.instanceName.value?.trim()) return true
if (!ctx.selectedGameVersion.value) return true if (!ctx.selectedGameVersion.value) return true
if (!ctx.hideLoaderChips.value && !ctx.selectedLoader.value) return true if (!ctx.hideLoaderChips.value && !ctx.selectedLoader.value) return true
if ( if (

View File

@@ -4,9 +4,11 @@ import {
MoreVerticalIcon, MoreVerticalIcon,
OrganizationIcon, OrganizationIcon,
SpinnerIcon, SpinnerIcon,
TrashExclamationIcon,
TrashIcon, TrashIcon,
TriangleAlertIcon, TriangleAlertIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { useMagicKeys } from '@vueuse/core'
import { Tooltip } from 'floating-vue' import { Tooltip } from 'floating-vue'
import { computed, getCurrentInstance, ref } from 'vue' import { computed, getCurrentInstance, ref } from 'vue'
import type { RouteLocationRaw } from 'vue-router' import type { RouteLocationRaw } from 'vue-router'
@@ -64,7 +66,7 @@ const selected = defineModel<boolean>('selected')
const emit = defineEmits<{ const emit = defineEmits<{
'update:enabled': [value: boolean] 'update:enabled': [value: boolean]
delete: [] delete: [event: MouseEvent]
update: [] update: []
}>() }>()
@@ -74,6 +76,9 @@ const hasUpdateListener = computed(() => typeof instance?.vnode.props?.onUpdate
const versionNumberRef = ref<HTMLElement | null>(null) const versionNumberRef = ref<HTMLElement | null>(null)
const fileNameRef = ref<HTMLElement | null>(null) const fileNameRef = ref<HTMLElement | null>(null)
const { shift: shiftHeld } = useMagicKeys()
const deleteHovered = ref(false)
</script> </script>
<template> <template>
@@ -84,9 +89,10 @@ const fileNameRef = ref<HTMLElement | null>(null)
> >
<div <div
class="flex min-w-0 items-center gap-4" class="flex min-w-0 items-center gap-4"
:class=" :class="[
hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[350px] @[800px]:shrink-0 @[800px]:flex-none' hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[350px] @[800px]:shrink-0 @[800px]:flex-none',
" enabled === false && !disabled ? 'grayscale opacity-50' : '',
]"
> >
<Checkbox <Checkbox
v-if="showCheckbox" v-if="showCheckbox"
@@ -188,7 +194,10 @@ const fileNameRef = ref<HTMLElement | null>(null)
<div <div
class="hidden flex-col gap-0.5 @[800px]:flex" class="hidden flex-col gap-0.5 @[800px]:flex"
:class="hideActions ? 'flex-1' : 'flex-1 min-w-0'" :class="[
hideActions ? 'flex-1' : 'flex-1 min-w-0',
enabled === false && !disabled ? 'grayscale opacity-50' : '',
]"
> >
<template v-if="version"> <template v-if="version">
<AutoLink <AutoLink
@@ -221,7 +230,10 @@ const fileNameRef = ref<HTMLElement | null>(null)
</template> </template>
</div> </div>
<div v-if="!hideActions" class="flex min-w-[160px] shrink-0 items-center justify-end gap-2"> <div
v-if="!hideActions"
class="flex min-w-[160px] shrink-0 items-center justify-end gap-2 transition-colors duration-200"
>
<slot name="additionalButtonsLeft" /> <slot name="additionalButtonsLeft" />
<!-- Fixed width container to reserve space for update button --> <!-- Fixed width container to reserve space for update button -->
@@ -249,18 +261,34 @@ const fileNameRef = ref<HTMLElement | null>(null)
:model-value="enabled" :model-value="enabled"
:disabled="disabled" :disabled="disabled"
:aria-label="project.title" :aria-label="project.title"
small
class="mr-2 my-auto" class="mr-2 my-auto"
@update:model-value="(val) => emit('update:enabled', val as boolean)" @update:model-value="(val) => emit('update:enabled', val as boolean)"
/> />
<ButtonStyled v-if="hasDeleteListener && !props.hideDelete" circular type="transparent"> <ButtonStyled v-if="hasDeleteListener && !props.hideDelete" circular type="transparent">
<button <button
v-tooltip="formatMessage(commonMessages.deleteLabel)" v-tooltip="
formatMessage(
shiftHeld && deleteHovered
? commonMessages.deleteImmediatelyLabel
: commonMessages.deleteLabel,
)
"
:disabled="disabled" :disabled="disabled"
@click="emit('delete')" @click="emit('delete', $event)"
@mouseenter="deleteHovered = true"
@mouseleave="deleteHovered = false"
> >
<TrashIcon class="size-5 text-secondary" /> <span class="relative size-5">
<TrashIcon
class="absolute inset-0 size-5 text-secondary transition-opacity duration-200"
:class="shiftHeld && deleteHovered ? 'opacity-0' : 'opacity-100'"
/>
<TrashExclamationIcon
class="absolute inset-0 size-5 text-red transition-opacity duration-200"
:class="shiftHeld && deleteHovered ? 'opacity-100' : 'opacity-0'"
/>
</span>
</button> </button>
</ButtonStyled> </ButtonStyled>

View File

@@ -47,7 +47,7 @@ const selectedIds = defineModel<string[]>('selectedIds', { default: () => [] })
const emit = defineEmits<{ const emit = defineEmits<{
'update:enabled': [id: string, value: boolean] 'update:enabled': [id: string, value: boolean]
delete: [id: string] delete: [id: string, event: MouseEvent]
update: [id: string] update: [id: string]
sort: [column: ContentCardTableSortColumn, direction: ContentCardTableSortDirection] sort: [column: ContentCardTableSortColumn, direction: ContentCardTableSortDirection]
}>() }>()
@@ -280,7 +280,11 @@ function handleSort(column: ContentCardTableSortColumn) {
:hide-actions="!hasAnyActions" :hide-actions="!hasAnyActions"
:selected="isItemSelected(item.id)" :selected="isItemSelected(item.id)"
:class="[ :class="[
(visibleRange.start + idx) % 2 === 1 ? 'bg-surface-1.5' : 'bg-surface-2', isItemSelected(item.id)
? 'bg-surface-2.5'
: (visibleRange.start + idx) % 2 === 1
? 'bg-surface-1.5'
: 'bg-surface-2',
'border-0 border-t border-solid border-surface-4', 'border-0 border-t border-solid border-surface-4',
visibleRange.start + idx === items.length - 1 && !flat ? 'rounded-b-[20px]' : '', visibleRange.start + idx === items.length - 1 && !flat ? 'rounded-b-[20px]' : '',
]" ]"
@@ -288,7 +292,7 @@ function handleSort(column: ContentCardTableSortColumn) {
(val) => toggleItemSelection(item.id, val ?? false, visibleRange.start + idx) (val) => toggleItemSelection(item.id, val ?? false, visibleRange.start + idx)
" "
@update:enabled="(val) => emit('update:enabled', item.id, val)" @update:enabled="(val) => emit('update:enabled', item.id, val)"
@delete="emit('delete', item.id)" @delete="(e: MouseEvent) => emit('delete', item.id, e)"
@update="emit('update', item.id)" @update="emit('update', item.id)"
> >
<template #additionalButtonsLeft> <template #additionalButtonsLeft>
@@ -326,13 +330,17 @@ function handleSort(column: ContentCardTableSortColumn) {
:hide-actions="!hasAnyActions" :hide-actions="!hasAnyActions"
:selected="isItemSelected(item.id)" :selected="isItemSelected(item.id)"
:class="[ :class="[
index % 2 === 1 ? 'bg-surface-1.5' : 'bg-surface-2', isItemSelected(item.id)
? 'bg-surface-2.5'
: index % 2 === 1
? 'bg-surface-1.5'
: 'bg-surface-2',
'border-0 border-t border-solid border-surface-4', 'border-0 border-t border-solid border-surface-4',
index === items.length - 1 && !flat ? 'rounded-b-[20px]' : '', index === items.length - 1 && !flat ? 'rounded-b-[20px]' : '',
]" ]"
@update:selected="(val) => toggleItemSelection(item.id, val ?? false, index)" @update:selected="(val) => toggleItemSelection(item.id, val ?? false, index)"
@update:enabled="(val) => emit('update:enabled', item.id, val)" @update:enabled="(val) => emit('update:enabled', item.id, val)"
@delete="emit('delete', item.id)" @delete="(e: MouseEvent) => emit('delete', item.id, e)"
@update="emit('update', item.id)" @update="emit('update', item.id)"
> >
<template #additionalButtonsLeft> <template #additionalButtonsLeft>

View File

@@ -10,7 +10,7 @@
<span class="text-lg font-extrabold text-contrast">{{ <span class="text-lg font-extrabold text-contrast">{{
header ?? header ??
formatMessage( formatMessage(
isModpack ? messages.switchModpackVersionHeader : messages.updateVersionHeader, isModpack.value ? messages.switchModpackVersionHeader : messages.updateVersionHeader,
) )
}}</span> }}</span>
</template> </template>
@@ -246,10 +246,12 @@ import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import StyledInput from '#ui/components/base/StyledInput.vue' import StyledInput from '#ui/components/base/StyledInput.vue'
import NewModal from '#ui/components/modal/NewModal.vue' import NewModal from '#ui/components/modal/NewModal.vue'
import VersionChannelIndicator from '#ui/components/version/VersionChannelIndicator.vue' import VersionChannelIndicator from '#ui/components/version/VersionChannelIndicator.vue'
import { useDebugLogger } from '#ui/composables/debug-logger'
import { defineMessages, useVIntl } from '#ui/composables/i18n' import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonMessages } from '#ui/utils/common-messages' import { commonMessages } from '#ui/utils/common-messages'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const debug = useDebugLogger('ContentUpdaterModal')
const messages = defineMessages({ const messages = defineMessages({
updateVersionHeader: { updateVersionHeader: {
@@ -326,8 +328,8 @@ const props = withDefaults(
currentLoader: string currentLoader: string
currentVersionId: string currentVersionId: string
isApp: boolean isApp: boolean
/** Whether this is a modpack update (changes header text) */ /** The project type (e.g. mod, shader, resourcepack, datapack, modpack). */
isModpack?: boolean projectType?: string
projectIconUrl?: string projectIconUrl?: string
projectName?: string projectName?: string
header?: string header?: string
@@ -337,7 +339,7 @@ const props = withDefaults(
loadingChangelog?: boolean loadingChangelog?: boolean
}>(), }>(),
{ {
isModpack: false, projectType: undefined,
projectIconUrl: undefined, projectIconUrl: undefined,
projectName: undefined, projectName: undefined,
header: undefined, header: undefined,
@@ -346,8 +348,10 @@ const props = withDefaults(
}, },
) )
const isModpack = computed(() => props.projectType === 'modpack')
const emit = defineEmits<{ const emit = defineEmits<{
update: [version: Labrinth.Versions.v2.Version] update: [version: Labrinth.Versions.v2.Version, event: MouseEvent]
cancel: [] cancel: []
/** Emitted when user selects a version, so parent can fetch full version data with changelog */ /** Emitted when user selects a version, so parent can fetch full version data with changelog */
versionSelect: [version: Labrinth.Versions.v2.Version] versionSelect: [version: Labrinth.Versions.v2.Version]
@@ -374,8 +378,20 @@ watch(
// Handle initial selection when versions first arrive // Handle initial selection when versions first arrive
if (newVersions.length > 0 && !selectedVersion.value && pendingInitialVersionId.value) { if (newVersions.length > 0 && !selectedVersion.value && pendingInitialVersionId.value) {
const version = const pendingFound = newVersions.find((v) => v.id === pendingInitialVersionId.value)
newVersions.find((v) => v.id === pendingInitialVersionId.value) ?? newVersions[0] debug('versions watcher: initial selection', {
pendingInitialVersionId: pendingInitialVersionId.value,
foundPending: !!pendingFound,
currentVersionId: props.currentVersionId,
currentInList: newVersions.some((v) => v.id === props.currentVersionId),
totalVersions: newVersions.length,
loaderDistribution: [...new Set(newVersions.flatMap((v) => v.loaders))],
gameVersionDistribution: [...new Set(newVersions.flatMap((v) => v.game_versions))].slice(
0,
10,
),
})
const version = pendingFound ?? newVersions[0]
selectedVersion.value = version selectedVersion.value = version
if (version) { if (version) {
emit('versionSelect', version) emit('versionSelect', version)
@@ -386,12 +402,30 @@ watch(
{ deep: true }, { deep: true },
) )
const NON_MOD_PROJECT_TYPES = new Set(['shader', 'shaderpack', 'resourcepack', 'datapack'])
function isVersionCompatible(version: Labrinth.Versions.v2.Version): boolean { function isVersionCompatible(version: Labrinth.Versions.v2.Version): boolean {
const hasGameVersion = version.game_versions.includes(props.currentGameVersion) const hasGameVersion = version.game_versions.includes(props.currentGameVersion)
const hasLoader = version.loaders.some( const skipLoaderCheck = props.projectType != null && NON_MOD_PROJECT_TYPES.has(props.projectType)
(loader) => loader.toLowerCase() === props.currentLoader.toLowerCase(), const hasLoader =
) skipLoaderCheck ||
return hasGameVersion && hasLoader version.loaders.some((loader) => loader.toLowerCase() === props.currentLoader.toLowerCase())
const compatible = hasGameVersion && hasLoader
if (!compatible) {
debug('isVersionCompatible: INCOMPATIBLE', {
versionId: version.id,
versionNumber: version.version_number,
versionLoaders: version.loaders,
versionGameVersions: version.game_versions,
currentLoader: props.currentLoader,
currentGameVersion: props.currentGameVersion,
projectType: props.projectType,
hasGameVersion,
hasLoader,
skipLoaderCheck,
})
}
return compatible
} }
const currentVersion = computed(() => props.versions.find((v) => v.id === props.currentVersionId)) const currentVersion = computed(() => props.versions.find((v) => v.id === props.currentVersionId))
@@ -413,10 +447,19 @@ const filteredVersions = computed(() => {
) )
} }
const beforeFilterCount = versions.length
if (hideIncompatibleState.value) { if (hideIncompatibleState.value) {
versions = versions.filter(isVersionCompatible) versions = versions.filter(isVersionCompatible)
} }
debug('filteredVersions computed', {
totalVersions: props.versions.length,
afterSearchFilter: beforeFilterCount,
afterCompatibilityFilter: versions.length,
hiddenByCompatibility: beforeFilterCount - versions.length,
hideIncompatible: hideIncompatibleState.value,
})
return versions return versions
}) })
@@ -503,9 +546,9 @@ function handleVersionSelect(version: Labrinth.Versions.v2.Version) {
emit('versionSelect', version) emit('versionSelect', version)
} }
function handleUpdate() { function handleUpdate(event: MouseEvent) {
if (selectedVersion.value) { if (selectedVersion.value) {
emit('update', selectedVersion.value) emit('update', selectedVersion.value, event)
hide() hide()
} }
} }
@@ -519,7 +562,23 @@ function show(initialVersionId?: string) {
searchQuery.value = '' searchQuery.value = ''
hideIncompatibleState.value = true hideIncompatibleState.value = true
debug('show() called', {
initialVersionId,
currentVersionId: props.currentVersionId,
currentGameVersion: props.currentGameVersion,
currentLoader: props.currentLoader,
projectType: props.projectType,
versionsAvailable: props.versions.length,
})
if (props.versions.length > 0) { if (props.versions.length > 0) {
const currentInList = props.versions.find((v) => v.id === props.currentVersionId)
debug('show(): currentVersionId lookup', {
currentVersionId: props.currentVersionId,
foundInList: !!currentInList,
allVersionIds: props.versions.map((v) => v.id),
})
if (initialVersionId) { if (initialVersionId) {
selectedVersion.value = selectedVersion.value =
props.versions.find((v) => v.id === initialVersionId) ?? props.versions[0] props.versions.find((v) => v.id === initialVersionId) ?? props.versions[0]
@@ -533,6 +592,9 @@ function show(initialVersionId?: string) {
} else { } else {
selectedVersion.value = null selectedVersion.value = null
pendingInitialVersionId.value = initialVersionId pendingInitialVersionId.value = initialVersionId
debug('show(): no versions yet, deferring selection', {
pendingInitialVersionId: initialVersionId,
})
} }
modal.value?.show() modal.value?.show()

View File

@@ -43,6 +43,9 @@
class="size-5 shrink-0 text-brand-orange hover:brightness-110" class="size-5 shrink-0 text-brand-orange hover:brightness-110"
/> />
</div> </div>
<span class="text-secondary">
{{ formatMessage(messages.shiftClickHint) }}
</span>
</div> </div>
</template> </template>
@@ -111,5 +114,9 @@ const messages = defineMessages({
defaultMessage: defaultMessage:
"A backup is in progress, it's recommended to wait for it to finish before performing this action.", "A backup is in progress, it's recommended to wait for it to finish before performing this action.",
}, },
shiftClickHint: {
id: 'content.inline-backup.shift-click-hint',
defaultMessage: 'Hold Shift while clicking to skip confirmation.',
},
}) })
</script> </script>

View File

@@ -1,6 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
ArrowUpDownIcon, ArrowDownAZIcon,
ArrowDownZAIcon,
ClockArrowDownIcon,
ClockArrowUpIcon,
CodeIcon, CodeIcon,
CompassIcon, CompassIcon,
DownloadIcon, DownloadIcon,
@@ -77,9 +80,13 @@ const messages = defineMessages({
id: 'content.page-layout.sort.alphabetical', id: 'content.page-layout.sort.alphabetical',
defaultMessage: 'Alphabetical', defaultMessage: 'Alphabetical',
}, },
sortDateAdded: { sortDateAddedNewest: {
id: 'content.page-layout.sort.date-added', id: 'content.page-layout.sort.date-added-newest',
defaultMessage: 'Date added', defaultMessage: 'Newest first',
},
sortDateAddedOldest: {
id: 'content.page-layout.sort.date-added-oldest',
defaultMessage: 'Oldest first',
}, },
updateAll: { updateAll: {
id: 'content.page-layout.update-all', id: 'content.page-layout.update-all',
@@ -147,34 +154,55 @@ const uploadOverallProgress = computed(() => {
return Math.min((state.completedFiles + state.currentFileProgress) / state.totalFiles, 1) return Math.min((state.completedFiles + state.currentFileProgress) / state.totalFiles, 1)
}) })
type SortMode = 'alphabetical' | 'date-added' type SortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'date-added-newest' | 'date-added-oldest'
const sortMode = ref<SortMode>('alphabetical') const sortMode = ref<SortMode>('alphabetical-asc')
const sortLabels: Record<SortMode, () => string> = { const sortLabels: Record<SortMode, () => string> = {
alphabetical: () => formatMessage(messages.sortAlphabetical), 'alphabetical-asc': () => formatMessage(messages.sortAlphabetical),
'date-added': () => formatMessage(messages.sortDateAdded), 'alphabetical-desc': () => formatMessage(messages.sortAlphabetical),
'date-added-newest': () => formatMessage(messages.sortDateAddedNewest),
'date-added-oldest': () => formatMessage(messages.sortDateAddedOldest),
} }
function cycleSortMode() { function cycleSortMode() {
const modes: SortMode[] = ['alphabetical', 'date-added'] const modes: SortMode[] = [
'alphabetical-asc',
'date-added-newest',
'alphabetical-desc',
'date-added-oldest',
]
const idx = modes.indexOf(sortMode.value) const idx = modes.indexOf(sortMode.value)
sortMode.value = modes[(idx + 1) % modes.length] sortMode.value = modes[(idx + 1) % modes.length]
} }
const sortedItems = computed(() => { const sortedItems = computed(() => {
const items = [...ctx.items.value] const items = [...ctx.items.value]
if (sortMode.value === 'date-added') { switch (sortMode.value) {
return items.sort((a, b) => { case 'alphabetical-desc':
const dateA = a.date_added ?? '' return items.sort((a, b) => {
const dateB = b.date_added ?? '' const nameA = a.project?.title ?? a.file_name
return dateB.localeCompare(dateA) const nameB = b.project?.title ?? b.file_name
}) return nameB.toLowerCase().localeCompare(nameA.toLowerCase())
})
case 'date-added-newest':
return items.sort((a, b) => {
const dateA = a.date_added ?? ''
const dateB = b.date_added ?? ''
return dateB.localeCompare(dateA)
})
case 'date-added-oldest':
return items.sort((a, b) => {
const dateA = a.date_added ?? ''
const dateB = b.date_added ?? ''
return dateA.localeCompare(dateB)
})
default:
return items.sort((a, b) => {
const nameA = a.project?.title ?? a.file_name
const nameB = b.project?.title ?? b.file_name
return nameA.toLowerCase().localeCompare(nameB.toLowerCase())
})
} }
return items.sort((a, b) => {
const nameA = a.project?.title ?? a.file_name
const nameB = b.project?.title ?? b.file_name
return nameA.toLowerCase().localeCompare(nameB.toLowerCase())
})
}) })
const { searchQuery, search } = useContentSearch(sortedItems, [ const { searchQuery, search } = useContentSearch(sortedItems, [
@@ -223,20 +251,28 @@ async function handleRefresh() {
} }
} }
const filteredItems = computed(() => applyFilters(search(sortedItems.value))) const filteredItems = computed(() => {
const tableItems = computed<ContentCardTableItem[]>(() => const sorted = sortedItems.value
filteredItems.value.map((item) => { const searched = search(sorted)
return applyFilters(searched)
})
const tableItems = computed<ContentCardTableItem[]>(() => {
return filteredItems.value.map((item) => {
const base = ctx.mapToTableItem(item) const base = ctx.mapToTableItem(item)
return { return {
...base, ...base,
disabled: isChanging(base.id) || ctx.isBusy.value || item.installing === true, disabled:
isChanging(base.id) ||
ctx.isBusy.value ||
isBulkOperating.value ||
item.installing === true,
installing: item.installing === true, installing: item.installing === true,
hasUpdate: !ctx.isPackLocked.value && item.has_update, hasUpdate: !ctx.isPackLocked.value && item.has_update,
isClientOnly: isClientOnlyEnvironment(item.environment), isClientOnly: isClientOnlyEnvironment(item.environment),
overflowOptions: ctx.getOverflowOptions?.(item), overflowOptions: ctx.getOverflowOptions?.(item),
} }
}), })
) })
const hasOutdatedProjects = computed(() => ctx.items.value.some((p) => p.has_update)) const hasOutdatedProjects = computed(() => ctx.items.value.some((p) => p.has_update))
@@ -244,17 +280,25 @@ const hasOutdatedProjects = computed(() => ctx.items.value.some((p) => p.has_upd
const pendingDeletionItems = ref<ContentItem[]>([]) const pendingDeletionItems = ref<ContentItem[]>([])
const confirmDeletionModal = ref<InstanceType<typeof ConfirmDeletionModal>>() const confirmDeletionModal = ref<InstanceType<typeof ConfirmDeletionModal>>()
function handleDeleteById(id: string) { function handleDeleteById(id: string, event?: MouseEvent) {
const item = ctx.items.value.find((i) => ctx.getItemId(i) === id) const item = ctx.items.value.find((i) => ctx.getItemId(i) === id)
if (item) { if (item) {
pendingDeletionItems.value = [item] pendingDeletionItems.value = [item]
confirmDeletionModal.value?.show() if (event?.shiftKey) {
confirmDelete()
} else {
confirmDeletionModal.value?.show()
}
} }
} }
function showBulkDeleteModal() { function showBulkDeleteModal(event?: MouseEvent) {
pendingDeletionItems.value = [...selectedItems.value] pendingDeletionItems.value = [...selectedItems.value]
confirmDeletionModal.value?.show() if (event?.shiftKey) {
confirmDelete()
} else {
confirmDeletionModal.value?.show()
}
} }
async function confirmDelete() { async function confirmDelete() {
@@ -359,20 +403,28 @@ const pendingBulkUpdateItems = ref<ContentItem[]>([])
const hasBulkUpdateSupport = computed(() => !!(ctx.bulkUpdateItem || ctx.bulkUpdateItems)) const hasBulkUpdateSupport = computed(() => !!(ctx.bulkUpdateItem || ctx.bulkUpdateItems))
function promptUpdateAll() { function promptUpdateAll(event?: MouseEvent) {
if (!hasBulkUpdateSupport.value) return if (!hasBulkUpdateSupport.value) return
const items = ctx.items.value.filter((item) => item.has_update) const items = ctx.items.value.filter((item) => item.has_update)
if (items.length === 0) return if (items.length === 0) return
pendingBulkUpdateItems.value = items pendingBulkUpdateItems.value = items
confirmBulkUpdateModal.value?.show() if (event?.shiftKey) {
confirmBulkUpdate()
} else {
confirmBulkUpdateModal.value?.show()
}
} }
function promptUpdateSelected() { function promptUpdateSelected(event?: MouseEvent) {
if (!hasBulkUpdateSupport.value) return if (!hasBulkUpdateSupport.value) return
const items = selectedItems.value.filter((item) => item.has_update) const items = selectedItems.value.filter((item) => item.has_update)
if (items.length === 0) return if (items.length === 0) return
pendingBulkUpdateItems.value = items pendingBulkUpdateItems.value = items
confirmBulkUpdateModal.value?.show() if (event?.shiftKey) {
confirmBulkUpdate()
} else {
confirmBulkUpdateModal.value?.show()
}
} }
async function confirmBulkUpdate() { async function confirmBulkUpdate() {
@@ -505,8 +557,8 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
clearable clearable
:placeholder=" :placeholder="
formatMessage(messages.searchPlaceholder, { formatMessage(messages.searchPlaceholder, {
count: ctx.items.value.length, count: tableItems.length,
contentType: `${ctx.contentTypeLabel.value}${ctx.items.value.length === 1 ? '' : 's'}`, contentType: `${ctx.contentTypeLabel.value}${tableItems.length === 1 ? '' : 's'}`,
}) })
" "
/> />
@@ -580,7 +632,11 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
" "
@click="cycleSortMode" @click="cycleSortMode"
> >
<ArrowUpDownIcon /> <ArrowDownZAIcon v-if="sortMode === 'alphabetical-desc'" /><ClockArrowDownIcon
v-else-if="sortMode === 'date-added-newest'"
/><ClockArrowUpIcon
v-else-if="sortMode === 'date-added-oldest'"
/><ArrowDownAZIcon v-else />
{{ sortLabels[sortMode]() }} {{ sortLabels[sortMode]() }}
</button> </button>
</ButtonStyled> </ButtonStyled>
@@ -596,7 +652,11 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
" "
@click="cycleSortMode" @click="cycleSortMode"
> >
<ArrowUpDownIcon /> <ArrowDownZAIcon v-if="sortMode === 'alphabetical-desc'" /><ClockArrowDownIcon
v-else-if="sortMode === 'date-added-newest'"
/><ClockArrowUpIcon
v-else-if="sortMode === 'date-added-oldest'"
/><ArrowDownAZIcon v-else />
{{ sortLabels[sortMode]() }} {{ sortLabels[sortMode]() }}
</button> </button>
</ButtonStyled> </ButtonStyled>

View File

@@ -88,14 +88,18 @@ const disabledPlatforms = computed(() => {
const showModpackVersionActions = ctx.showModpackVersionActions ?? true const showModpackVersionActions = ctx.showModpackVersionActions ?? true
function handleModpackUpdateRequest(version: Labrinth.Versions.v2.Version) { function handleModpackUpdateRequest(version: Labrinth.Versions.v2.Version, event?: MouseEvent) {
pendingUpdateVersion.value = version pendingUpdateVersion.value = version
const currentVersionId = ctx.updaterModalProps.value.currentVersionId const currentVersionId = ctx.updaterModalProps.value.currentVersionId
const currentVersion = form.updatingProjectVersions.value.find((v) => v.id === currentVersionId) const currentVersion = form.updatingProjectVersions.value.find((v) => v.id === currentVersionId)
isUpdateDowngrade.value = currentVersion isUpdateDowngrade.value = currentVersion
? new Date(version.date_published) < new Date(currentVersion.date_published) ? new Date(version.date_published) < new Date(currentVersion.date_published)
: false : false
modpackUpdateModal.value?.show() if (event?.shiftKey) {
handleModpackUpdateConfirm()
} else {
modpackUpdateModal.value?.show()
}
} }
function handleModpackUpdateConfirm() { function handleModpackUpdateConfirm() {
@@ -363,7 +367,7 @@ const messages = defineMessages({
<button <button
class="!shadow-none" class="!shadow-none"
:disabled="ctx.isBusy.value" :disabled="ctx.isBusy.value"
@click="unlinkModal?.show()" @click="(e: MouseEvent) => (e.shiftKey ? handleUnlink() : unlinkModal?.show())"
> >
<UnlinkIcon class="size-5" /> <UnlinkIcon class="size-5" />
{{ {{
@@ -395,7 +399,9 @@ const messages = defineMessages({
<button <button
class="!shadow-none" class="!shadow-none"
:disabled="ctx.isBusy.value" :disabled="ctx.isBusy.value"
@click="reinstallModal?.show()" @click="
(e: MouseEvent) => (e.shiftKey ? handleReinstall() : reinstallModal?.show())
"
> >
<SpinnerIcon v-if="ctx.reinstalling?.value" class="animate-spin" /> <SpinnerIcon v-if="ctx.reinstalling?.value" class="animate-spin" />
<DownloadIcon v-else class="size-5" /> <DownloadIcon v-else class="size-5" />
@@ -659,7 +665,7 @@ const messages = defineMessages({
:current-loader="ctx.updaterModalProps.value.currentLoader" :current-loader="ctx.updaterModalProps.value.currentLoader"
:current-version-id="ctx.updaterModalProps.value.currentVersionId" :current-version-id="ctx.updaterModalProps.value.currentVersionId"
:is-app="ctx.isApp" :is-app="ctx.isApp"
:is-modpack="true" project-type="modpack"
:project-icon-url="ctx.updaterModalProps.value.projectIconUrl" :project-icon-url="ctx.updaterModalProps.value.projectIconUrl"
:project-name="ctx.updaterModalProps.value.projectName" :project-name="ctx.updaterModalProps.value.projectName"
:loading="form.loadingVersions.value" :loading="form.loadingVersions.value"

View File

@@ -714,15 +714,20 @@ function resetUpdateState() {
loadingChangelog.value = false loadingChangelog.value = false
} }
function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version) { function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version, event?: MouseEvent) {
if (updatingModpack.value) { if (updatingModpack.value) {
const currentVersionId = contentQuery.data.value?.modpack?.spec.version_id if (event?.shiftKey) {
const currentVersion = updatingProjectVersions.value.find((v) => v.id === currentVersionId) pendingModpackUpdateVersion.value = selectedVersion
isModpackUpdateDowngrade.value = currentVersion handleModpackUpdateConfirm()
? new Date(selectedVersion.date_published) < new Date(currentVersion.date_published) } else {
: false const currentVersionId = contentQuery.data.value?.modpack?.spec.version_id
pendingModpackUpdateVersion.value = selectedVersion const currentVersion = updatingProjectVersions.value.find((v) => v.id === currentVersionId)
modpackUpdateModal.value?.show() isModpackUpdateDowngrade.value = currentVersion
? new Date(selectedVersion.date_published) < new Date(currentVersion.date_published)
: false
pendingModpackUpdateVersion.value = selectedVersion
modpackUpdateModal.value?.show()
}
return return
} }
@@ -860,7 +865,7 @@ provideContentManager({
: (updatingProject?.version?.id ?? '') : (updatingProject?.version?.id ?? '')
" "
:is-app="false" :is-app="false"
:is-modpack="updatingModpack" :project-type="updatingModpack ? 'modpack' : updatingProject?.project_type"
:project-icon-url=" :project-icon-url="
updatingModpack ? modpack?.project.icon_url : updatingProject?.project?.icon_url updatingModpack ? modpack?.project.icon_url : updatingProject?.project?.icon_url
" "

View File

@@ -287,6 +287,9 @@
"content.inline-backup.create-backup": { "content.inline-backup.create-backup": {
"defaultMessage": "Create backup" "defaultMessage": "Create backup"
}, },
"content.inline-backup.shift-click-hint": {
"defaultMessage": "Hold Shift while clicking to skip confirmation."
},
"content.inline-backup.warning-body": { "content.inline-backup.warning-body": {
"defaultMessage": "We recommend creating a backup before proceeding so you can restore your {type, select, server {world} other {instance}} if anything breaks." "defaultMessage": "We recommend creating a backup before proceeding so you can restore your {type, select, server {world} other {instance}} if anything breaks."
}, },
@@ -353,8 +356,11 @@
"content.page-layout.sort.alphabetical": { "content.page-layout.sort.alphabetical": {
"defaultMessage": "Alphabetical" "defaultMessage": "Alphabetical"
}, },
"content.page-layout.sort.date-added": { "content.page-layout.sort.date-added-newest": {
"defaultMessage": "Date added" "defaultMessage": "Newest first"
},
"content.page-layout.sort.date-added-oldest": {
"defaultMessage": "Oldest first"
}, },
"content.page-layout.sort.label": { "content.page-layout.sort.label": {
"defaultMessage": "Sort by {mode}" "defaultMessage": "Sort by {mode}"
@@ -875,6 +881,9 @@
"label.delete": { "label.delete": {
"defaultMessage": "Delete" "defaultMessage": "Delete"
}, },
"label.delete-immediately": {
"defaultMessage": "Delete immediately"
},
"label.description": { "label.description": {
"defaultMessage": "Description" "defaultMessage": "Description"
}, },

View File

@@ -85,6 +85,10 @@ export const commonMessages = defineMessages({
id: 'label.delete', id: 'label.delete',
defaultMessage: 'Delete', defaultMessage: 'Delete',
}, },
deleteImmediatelyLabel: {
id: 'label.delete-immediately',
defaultMessage: 'Delete immediately',
},
descriptionLabel: { descriptionLabel: {
id: 'label.description', id: 'label.description',
defaultMessage: 'Description', defaultMessage: 'Description',