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:
@@ -147,6 +147,7 @@ provideModalBehavior({
|
||||
|
||||
const {
|
||||
installationModal,
|
||||
fetchExistingInstanceNames,
|
||||
handleCreate,
|
||||
handleBrowseModpacks,
|
||||
searchModpacks,
|
||||
@@ -945,6 +946,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
ref="installationModal"
|
||||
type="instance"
|
||||
show-snapshot-toggle
|
||||
:fetch-existing-instance-names="fetchExistingInstanceNames"
|
||||
:search-modpacks="searchModpacks"
|
||||
:get-project-versions="getProjectVersions"
|
||||
@create="handleCreate"
|
||||
|
||||
@@ -109,6 +109,7 @@ watch(
|
||||
const removing = ref(false)
|
||||
async function removeProfile() {
|
||||
removing.value = true
|
||||
const path = props.instance.path
|
||||
|
||||
trackEvent('InstanceRemove', {
|
||||
loader: props.instance.loader,
|
||||
@@ -116,7 +117,7 @@ async function removeProfile() {
|
||||
})
|
||||
|
||||
await router.push({ path: '/' })
|
||||
await remove(props.instance.path).catch(handleError)
|
||||
await remove(path).catch(handleError)
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
||||
@@ -246,7 +246,6 @@
|
||||
:options="options"
|
||||
:offline="offline"
|
||||
:playing="playing"
|
||||
:versions="modrinthVersions"
|
||||
:installed="instance.install_stage !== 'installed'"
|
||||
:is-server-instance="isServerInstance"
|
||||
: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 NavTabs from '@/components/ui/NavTabs.vue'
|
||||
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 { get_by_profile_path } from '@/helpers/process'
|
||||
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
|
||||
@@ -362,7 +361,6 @@ window.addEventListener('online', () => {
|
||||
})
|
||||
|
||||
const instance = ref<GameInstance>()
|
||||
const modrinthVersions = ref<Labrinth.Versions.v2.Version[]>([])
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
const exportModal = ref<InstanceType<typeof ExportModal>>()
|
||||
@@ -385,7 +383,6 @@ const loadingServerPing = ref(false)
|
||||
async function fetchInstance() {
|
||||
isServerInstance.value = false
|
||||
linkedProjectV3.value = undefined
|
||||
modrinthVersions.value = []
|
||||
ping.value = undefined
|
||||
playersOnline.value = undefined
|
||||
loadingServerPing.value = false
|
||||
@@ -402,14 +399,6 @@ async function fetchInstance() {
|
||||
if (linkedProjectV3.value?.minecraft_server != null) {
|
||||
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) {
|
||||
handleError(error as Error)
|
||||
}
|
||||
@@ -605,18 +594,23 @@ const handleOptionsClick = async (args: { option: string; item: unknown }) => {
|
||||
|
||||
const unlistenProfiles = await profile_listener(
|
||||
async (event: { profile_path_id: string; event: string }) => {
|
||||
if (event.profile_path_id === route.params.id) {
|
||||
if (event.event === 'removed') {
|
||||
await router.push({
|
||||
path: '/',
|
||||
})
|
||||
return
|
||||
if (event.profile_path_id !== route.params.id) return
|
||||
if (event.event === 'removed' || route.path === '/') {
|
||||
if (route.path !== '/') {
|
||||
await router.push({ path: '/' })
|
||||
}
|
||||
instance.value = await get(route.params.id as string).catch(handleError)
|
||||
if (!instance.value?.linked_data?.project_id) {
|
||||
linkedProjectV3.value = undefined
|
||||
isServerInstance.value = false
|
||||
return
|
||||
}
|
||||
instance.value = await get(route.params.id as string).catch((err) => {
|
||||
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
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -150,10 +150,6 @@ const props = defineProps({
|
||||
return false
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
: (updatingProject?.version?.id ?? '')
|
||||
"
|
||||
:is-app="true"
|
||||
:is-modpack="updatingModpack"
|
||||
:project-type="updatingModpack ? 'modpack' : updatingProject?.project_type"
|
||||
:project-icon-url="
|
||||
updatingModpack ? linkedModpackProject?.icon_url : updatingProject?.project?.icon_url
|
||||
"
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
type OverflowMenuOption,
|
||||
provideAppBackup,
|
||||
provideContentManager,
|
||||
useDebugLogger,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ContentCardLayout as ContentPageLayout } from '@modrinth/ui'
|
||||
@@ -165,10 +166,10 @@ const { formatMessage } = useVIntl()
|
||||
const { handleError, addNotification } = injectNotificationManager()
|
||||
const { installingItems } = injectContentInstall()
|
||||
const router = useRouter()
|
||||
const debug = useDebugLogger('Mods:ContentUpdate')
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
versions: Labrinth.Versions.v2.Version[]
|
||||
isServerInstance?: boolean
|
||||
openSettings?: () => void
|
||||
}>()
|
||||
@@ -349,6 +350,18 @@ async function handleUpdate(id: string) {
|
||||
const item = projects.value.find((p) => p.file_name === id)
|
||||
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
|
||||
updatingProject.value = item
|
||||
updatingProjectVersions.value = []
|
||||
@@ -365,7 +378,24 @@ async function handleUpdate(id: string) {
|
||||
|
||||
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(
|
||||
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
|
||||
@@ -493,9 +523,17 @@ function handleModpackUpdateCancel() {
|
||||
pendingModpackUpdateVersion.value = null
|
||||
}
|
||||
|
||||
async function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version) {
|
||||
async function handleModalUpdate(
|
||||
selectedVersion: Labrinth.Versions.v2.Version,
|
||||
event?: MouseEvent,
|
||||
) {
|
||||
if (updatingModpack.value) {
|
||||
handleModpackUpdateRequest(selectedVersion)
|
||||
if (event?.shiftKey) {
|
||||
pendingModpackUpdateVersion.value = selectedVersion
|
||||
await handleModpackUpdateConfirm()
|
||||
} else {
|
||||
handleModpackUpdateRequest(selectedVersion)
|
||||
}
|
||||
} else if (updatingProject.value) {
|
||||
const mod = updatingProject.value
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<template>{{ instance.name }} overview</template>
|
||||
<script setup lang="ts">
|
||||
import type { Version } from '@modrinth/utils'
|
||||
|
||||
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
@@ -10,7 +8,6 @@ defineProps<{
|
||||
options: InstanceType<typeof ContextMenu>
|
||||
offline: boolean
|
||||
playing: boolean
|
||||
versions: Version[]
|
||||
installed: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -134,7 +134,6 @@ import {
|
||||
RadialHeader,
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
import type { Version } from '@modrinth/utils'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
@@ -202,7 +201,6 @@ const props = defineProps<{
|
||||
options: InstanceType<typeof ContextMenu> | null
|
||||
offline: boolean
|
||||
playing: boolean
|
||||
versions: Version[]
|
||||
installed: boolean
|
||||
}>()
|
||||
|
||||
|
||||
@@ -13,9 +13,14 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
|
||||
const router = useRouter()
|
||||
|
||||
const installationModal = useTemplateRef('installationModal')
|
||||
provide('showCreationModal', async () => {
|
||||
|
||||
async function fetchExistingInstanceNames(): Promise<string[]> {
|
||||
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) {
|
||||
@@ -57,9 +62,10 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
|
||||
? null
|
||||
: (config.selectedLoaderVersion.value ?? config.loaderVersionType.value)
|
||||
const iconPath = config.instanceIconPath.value ?? null
|
||||
const name = config.instanceName.value.trim() || config.autoInstanceName.value
|
||||
|
||||
await create(
|
||||
config.instanceName.value,
|
||||
name,
|
||||
config.selectedGameVersion.value,
|
||||
loader,
|
||||
loaderVersion,
|
||||
@@ -68,7 +74,7 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
|
||||
).catch(handleError)
|
||||
|
||||
trackEvent('InstanceCreate', {
|
||||
profile_name: config.instanceName.value,
|
||||
profile_name: name,
|
||||
game_version: config.selectedGameVersion.value,
|
||||
loader,
|
||||
loader_version: loaderVersion,
|
||||
@@ -102,6 +108,7 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
|
||||
|
||||
return {
|
||||
installationModal,
|
||||
fetchExistingInstanceNames,
|
||||
handleCreate,
|
||||
handleBrowseModpacks,
|
||||
searchModpacks,
|
||||
|
||||
@@ -11,7 +11,9 @@ import _ArchiveIcon from './icons/archive.svg?component'
|
||||
import _ArrowBigRightDashIcon from './icons/arrow-big-right-dash.svg?component'
|
||||
import _ArrowBigUpDashIcon from './icons/arrow-big-up-dash.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 _ArrowDownZAIcon from './icons/arrow-down-z-a.svg?component'
|
||||
import _ArrowLeftIcon from './icons/arrow-left.svg?component'
|
||||
import _ArrowLeftRightIcon from './icons/arrow-left-right.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 _BugIcon from './icons/bug.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 _ChangeSkinIcon from './icons/change-skin.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 _ClipboardCopyIcon from './icons/clipboard-copy.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 _CodeIcon from './icons/code.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 _TransferIcon from './icons/transfer.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 _UnderlineIcon from './icons/underline.svg?component'
|
||||
import _UndoIcon from './icons/undo.svg?component'
|
||||
@@ -390,7 +396,9 @@ export const ArchiveIcon = _ArchiveIcon
|
||||
export const ArrowBigRightDashIcon = _ArrowBigRightDashIcon
|
||||
export const ArrowBigUpDashIcon = _ArrowBigUpDashIcon
|
||||
export const ArrowDownIcon = _ArrowDownIcon
|
||||
export const ArrowDownAZIcon = _ArrowDownAZIcon
|
||||
export const ArrowDownLeftIcon = _ArrowDownLeftIcon
|
||||
export const ArrowDownZAIcon = _ArrowDownZAIcon
|
||||
export const ArrowLeftIcon = _ArrowLeftIcon
|
||||
export const ArrowLeftRightIcon = _ArrowLeftRightIcon
|
||||
export const ArrowUpIcon = _ArrowUpIcon
|
||||
@@ -417,6 +425,7 @@ export const BracesIcon = _BracesIcon
|
||||
export const BrushCleaningIcon = _BrushCleaningIcon
|
||||
export const BugIcon = _BugIcon
|
||||
export const CalendarIcon = _CalendarIcon
|
||||
export const CalendarArrowDownIcon = _CalendarArrowDownIcon
|
||||
export const CardIcon = _CardIcon
|
||||
export const ChangeSkinIcon = _ChangeSkinIcon
|
||||
export const ChartIcon = _ChartIcon
|
||||
@@ -433,6 +442,8 @@ export const ClearIcon = _ClearIcon
|
||||
export const ClientIcon = _ClientIcon
|
||||
export const ClipboardCopyIcon = _ClipboardCopyIcon
|
||||
export const ClockIcon = _ClockIcon
|
||||
export const ClockArrowDownIcon = _ClockArrowDownIcon
|
||||
export const ClockArrowUpIcon = _ClockArrowUpIcon
|
||||
export const CloudIcon = _CloudIcon
|
||||
export const CodeIcon = _CodeIcon
|
||||
export const CoffeeIcon = _CoffeeIcon
|
||||
@@ -737,6 +748,7 @@ export const ToggleLeftIcon = _ToggleLeftIcon
|
||||
export const ToggleRightIcon = _ToggleRightIcon
|
||||
export const TransferIcon = _TransferIcon
|
||||
export const TrashIcon = _TrashIcon
|
||||
export const TrashExclamationIcon = _TrashExclamationIcon
|
||||
export const TriangleAlertIcon = _TriangleAlertIcon
|
||||
export const UnderlineIcon = _UnderlineIcon
|
||||
export const UndoIcon = _UndoIcon
|
||||
|
||||
19
packages/assets/icons/arrow-down-a-z.svg
Normal file
19
packages/assets/icons/arrow-down-a-z.svg
Normal 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 |
19
packages/assets/icons/arrow-down-z-a.svg
Normal file
19
packages/assets/icons/arrow-down-z-a.svg
Normal 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 |
20
packages/assets/icons/calendar-arrow-down.svg
Normal file
20
packages/assets/icons/calendar-arrow-down.svg
Normal 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 |
18
packages/assets/icons/clock-arrow-down.svg
Normal file
18
packages/assets/icons/clock-arrow-down.svg
Normal 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 |
18
packages/assets/icons/clock-arrow-up.svg
Normal file
18
packages/assets/icons/clock-arrow-up.svg
Normal 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 |
13
packages/assets/icons/trash-exclamation.svg
Normal file
13
packages/assets/icons/trash-exclamation.svg
Normal 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 |
@@ -2,6 +2,7 @@
|
||||
--surface-1: #ebebeb;
|
||||
--surface-1-5: #ededed;
|
||||
--surface-2: #f5f5f5;
|
||||
--surface-2-5: #eef1f5;
|
||||
--surface-3: #f8f8f8;
|
||||
--surface-4: #ffffff;
|
||||
--surface-5: #dddddd;
|
||||
@@ -226,6 +227,7 @@ html {
|
||||
--surface-1: #16181c;
|
||||
--surface-1-5: #1a1c20;
|
||||
--surface-2: #1d1f23;
|
||||
--surface-2-5: #222429;
|
||||
--surface-3: #27292e;
|
||||
--surface-4: #34363c;
|
||||
--surface-5: #42444a;
|
||||
@@ -398,6 +400,7 @@ html {
|
||||
--surface-1: #000000;
|
||||
--surface-1-5: #050506;
|
||||
--surface-2: #09090a;
|
||||
--surface-2-5: #0c0d11;
|
||||
--surface-3: #101013;
|
||||
--surface-4: #1b1b20;
|
||||
--surface-5: #25262b;
|
||||
@@ -419,5 +422,6 @@ html {
|
||||
}
|
||||
|
||||
.retro-mode {
|
||||
--surface-2-5: #3a3c3e;
|
||||
--brand-gradient-strong-bg: #3a3b38;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const config: Config = {
|
||||
1: 'var(--surface-1)',
|
||||
1.5: 'var(--surface-1-5)',
|
||||
2: 'var(--surface-2)',
|
||||
2.5: 'var(--surface-2-5)',
|
||||
3: 'var(--surface-3)',
|
||||
4: 'var(--surface-4)',
|
||||
5: 'var(--surface-5)',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:disabled="disabled"
|
||||
class="relative inline-flex shrink-0 rounded-full m-0 transition-all duration-200 cursor-pointer border-none"
|
||||
: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',
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : 'btn-wrapper',
|
||||
]"
|
||||
@@ -16,11 +16,11 @@
|
||||
<span
|
||||
class="absolute rounded-full transition-all duration-200"
|
||||
: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
|
||||
? 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',
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
<!-- Instance-specific: Name field -->
|
||||
<div v-if="ctx.flowType === 'instance'" class="flex flex-col gap-2">
|
||||
<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>
|
||||
|
||||
<!-- Loader chips -->
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, type ComputedRef, type Ref, ref, type ShallowRef, watch } fro
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||
|
||||
import { createContext } from '../../../providers'
|
||||
import type { ImportableLauncher } from '../../../providers/instance-import'
|
||||
@@ -77,6 +78,7 @@ export interface CreationFlowContextValue {
|
||||
|
||||
// Instance-specific state
|
||||
instanceName: Ref<string>
|
||||
autoInstanceName: ComputedRef<string>
|
||||
instanceIcon: Ref<File | null>
|
||||
instanceIconUrl: Ref<string | null>
|
||||
instanceIconPath: Ref<string | null>
|
||||
@@ -121,7 +123,7 @@ export interface CreationFlowContextValue {
|
||||
onBack: (() => void) | null
|
||||
|
||||
// Methods
|
||||
reset: (instanceCount?: number) => void
|
||||
reset: (instanceCount?: number) => Promise<void>
|
||||
setSetupType: (type: SetupType) => void
|
||||
setImportMode: () => void
|
||||
browseModpacks: () => void
|
||||
@@ -138,7 +140,6 @@ export const [injectCreationFlowContext, provideCreationFlowContext] =
|
||||
|
||||
// TODO: replace with actual world count from the world list once available
|
||||
let worldCounter = 0
|
||||
let instanceCounter = 0
|
||||
|
||||
export interface CreationFlowOptions {
|
||||
availableLoaders?: string[]
|
||||
@@ -147,6 +148,7 @@ export interface CreationFlowOptions {
|
||||
isInitialSetup?: boolean
|
||||
initialLoader?: string
|
||||
initialGameVersion?: string
|
||||
fetchExistingInstanceNames?: () => Promise<string[]>
|
||||
onBack?: () => void
|
||||
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
|
||||
getProjectVersions?: (projectId: string) => Promise<{ id: string }[]>
|
||||
@@ -183,6 +185,8 @@ export function createCreationFlowContext(
|
||||
|
||||
// Instance-specific state
|
||||
const instanceName = ref('')
|
||||
const existingInstanceNames = ref<string[]>([])
|
||||
const fetchExistingInstanceNames = options.fetchExistingInstanceNames ?? null
|
||||
const instanceIcon = ref<File | null>(null)
|
||||
const instanceIconUrl = ref<string | null>(null)
|
||||
const instanceIconPath = ref<string | null>(null)
|
||||
@@ -200,6 +204,24 @@ export function createCreationFlowContext(
|
||||
const selectedLoaderVersion = ref<string | null>(null)
|
||||
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 modpackFile = ref<File | null>(null)
|
||||
const modpackFilePath = ref<string | null>(null)
|
||||
@@ -227,15 +249,14 @@ export function createCreationFlowContext(
|
||||
() => setupType.value === 'vanilla' || selectedLoader.value === 'vanilla',
|
||||
)
|
||||
|
||||
function reset(instanceCount?: number) {
|
||||
async function reset() {
|
||||
if (fetchExistingInstanceNames) {
|
||||
existingInstanceNames.value = await fetchExistingInstanceNames()
|
||||
}
|
||||
setupType.value = null
|
||||
isImportMode.value = false
|
||||
worldCounter++
|
||||
worldName.value = flowType === 'world' ? `World ${worldCounter}` : ''
|
||||
if (instanceCount != null) {
|
||||
instanceCounter = instanceCount
|
||||
}
|
||||
instanceCounter++
|
||||
gamemode.value = 'survival'
|
||||
difficulty.value = 'normal'
|
||||
worldSeed.value = ''
|
||||
@@ -245,7 +266,7 @@ export function createCreationFlowContext(
|
||||
generatorSettingsCustom.value = ''
|
||||
|
||||
// Instance-specific
|
||||
instanceName.value = flowType === 'instance' ? `New instance (${instanceCounter})` : ''
|
||||
instanceName.value = ''
|
||||
instanceIconUrl.value = null
|
||||
instanceIcon.value = null
|
||||
instanceIconPath.value = null
|
||||
@@ -356,6 +377,7 @@ export function createCreationFlowContext(
|
||||
generatorSettingsMode,
|
||||
generatorSettingsCustom,
|
||||
instanceName,
|
||||
autoInstanceName,
|
||||
instanceIcon,
|
||||
instanceIconUrl,
|
||||
instanceIconPath,
|
||||
|
||||
@@ -31,6 +31,7 @@ const props = withDefaults(
|
||||
isInitialSetup?: boolean
|
||||
initialLoader?: string
|
||||
initialGameVersion?: string
|
||||
fetchExistingInstanceNames?: () => Promise<string[]>
|
||||
onBack?: (() => void) | null
|
||||
fade?: 'standard' | 'warning' | 'danger'
|
||||
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
|
||||
@@ -44,6 +45,7 @@ const props = withDefaults(
|
||||
isInitialSetup: false,
|
||||
initialLoader: undefined,
|
||||
initialGameVersion: undefined,
|
||||
fetchExistingInstanceNames: undefined,
|
||||
onBack: null,
|
||||
},
|
||||
)
|
||||
@@ -69,6 +71,7 @@ const ctx = createCreationFlowContext(
|
||||
isInitialSetup: props.isInitialSetup,
|
||||
initialLoader: props.initialLoader,
|
||||
initialGameVersion: props.initialGameVersion,
|
||||
fetchExistingInstanceNames: props.fetchExistingInstanceNames,
|
||||
onBack: props.onBack ?? undefined,
|
||||
searchModpacks: props.searchModpacks,
|
||||
getProjectVersions: props.getProjectVersions,
|
||||
@@ -76,8 +79,8 @@ const ctx = createCreationFlowContext(
|
||||
)
|
||||
provideCreationFlowContext(ctx)
|
||||
|
||||
function show(instanceCount?: number) {
|
||||
ctx.reset(instanceCount)
|
||||
async function show() {
|
||||
await ctx.reset()
|
||||
modal.value?.setStage(0)
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import CustomSetupStage from '../components/CustomSetupStage.vue'
|
||||
import { type CreationFlowContextValue, flowTypeHeadings } from '../creation-flow-context'
|
||||
|
||||
function isForwardBlocked(ctx: CreationFlowContextValue): boolean {
|
||||
if (ctx.flowType === 'instance' && !ctx.instanceName.value?.trim()) return true
|
||||
if (!ctx.selectedGameVersion.value) return true
|
||||
if (!ctx.hideLoaderChips.value && !ctx.selectedLoader.value) return true
|
||||
if (
|
||||
|
||||
@@ -4,9 +4,11 @@ import {
|
||||
MoreVerticalIcon,
|
||||
OrganizationIcon,
|
||||
SpinnerIcon,
|
||||
TrashExclamationIcon,
|
||||
TrashIcon,
|
||||
TriangleAlertIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
import { computed, getCurrentInstance, ref } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
@@ -64,7 +66,7 @@ const selected = defineModel<boolean>('selected')
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:enabled': [value: boolean]
|
||||
delete: []
|
||||
delete: [event: MouseEvent]
|
||||
update: []
|
||||
}>()
|
||||
|
||||
@@ -74,6 +76,9 @@ const hasUpdateListener = computed(() => typeof instance?.vnode.props?.onUpdate
|
||||
|
||||
const versionNumberRef = ref<HTMLElement | null>(null)
|
||||
const fileNameRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const { shift: shiftHeld } = useMagicKeys()
|
||||
const deleteHovered = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -84,9 +89,10 @@ const fileNameRef = ref<HTMLElement | null>(null)
|
||||
>
|
||||
<div
|
||||
class="flex min-w-0 items-center gap-4"
|
||||
:class="
|
||||
hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[350px] @[800px]:shrink-0 @[800px]:flex-none'
|
||||
"
|
||||
:class="[
|
||||
hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[350px] @[800px]:shrink-0 @[800px]:flex-none',
|
||||
enabled === false && !disabled ? 'grayscale opacity-50' : '',
|
||||
]"
|
||||
>
|
||||
<Checkbox
|
||||
v-if="showCheckbox"
|
||||
@@ -188,7 +194,10 @@ const fileNameRef = ref<HTMLElement | null>(null)
|
||||
|
||||
<div
|
||||
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">
|
||||
<AutoLink
|
||||
@@ -221,7 +230,10 @@ const fileNameRef = ref<HTMLElement | null>(null)
|
||||
</template>
|
||||
</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" />
|
||||
|
||||
<!-- Fixed width container to reserve space for update button -->
|
||||
@@ -249,18 +261,34 @@ const fileNameRef = ref<HTMLElement | null>(null)
|
||||
:model-value="enabled"
|
||||
:disabled="disabled"
|
||||
:aria-label="project.title"
|
||||
small
|
||||
class="mr-2 my-auto"
|
||||
@update:model-value="(val) => emit('update:enabled', val as boolean)"
|
||||
/>
|
||||
|
||||
<ButtonStyled v-if="hasDeleteListener && !props.hideDelete" circular type="transparent">
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.deleteLabel)"
|
||||
v-tooltip="
|
||||
formatMessage(
|
||||
shiftHeld && deleteHovered
|
||||
? commonMessages.deleteImmediatelyLabel
|
||||
: commonMessages.deleteLabel,
|
||||
)
|
||||
"
|
||||
: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>
|
||||
</ButtonStyled>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ const selectedIds = defineModel<string[]>('selectedIds', { default: () => [] })
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:enabled': [id: string, value: boolean]
|
||||
delete: [id: string]
|
||||
delete: [id: string, event: MouseEvent]
|
||||
update: [id: string]
|
||||
sort: [column: ContentCardTableSortColumn, direction: ContentCardTableSortDirection]
|
||||
}>()
|
||||
@@ -280,7 +280,11 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
:hide-actions="!hasAnyActions"
|
||||
:selected="isItemSelected(item.id)"
|
||||
: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',
|
||||
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)
|
||||
"
|
||||
@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)"
|
||||
>
|
||||
<template #additionalButtonsLeft>
|
||||
@@ -326,13 +330,17 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
:hide-actions="!hasAnyActions"
|
||||
:selected="isItemSelected(item.id)"
|
||||
: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',
|
||||
index === items.length - 1 && !flat ? 'rounded-b-[20px]' : '',
|
||||
]"
|
||||
@update:selected="(val) => toggleItemSelection(item.id, val ?? false, index)"
|
||||
@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)"
|
||||
>
|
||||
<template #additionalButtonsLeft>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<span class="text-lg font-extrabold text-contrast">{{
|
||||
header ??
|
||||
formatMessage(
|
||||
isModpack ? messages.switchModpackVersionHeader : messages.updateVersionHeader,
|
||||
isModpack.value ? messages.switchModpackVersionHeader : messages.updateVersionHeader,
|
||||
)
|
||||
}}</span>
|
||||
</template>
|
||||
@@ -246,10 +246,12 @@ import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import StyledInput from '#ui/components/base/StyledInput.vue'
|
||||
import NewModal from '#ui/components/modal/NewModal.vue'
|
||||
import VersionChannelIndicator from '#ui/components/version/VersionChannelIndicator.vue'
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const debug = useDebugLogger('ContentUpdaterModal')
|
||||
|
||||
const messages = defineMessages({
|
||||
updateVersionHeader: {
|
||||
@@ -326,8 +328,8 @@ const props = withDefaults(
|
||||
currentLoader: string
|
||||
currentVersionId: string
|
||||
isApp: boolean
|
||||
/** Whether this is a modpack update (changes header text) */
|
||||
isModpack?: boolean
|
||||
/** The project type (e.g. mod, shader, resourcepack, datapack, modpack). */
|
||||
projectType?: string
|
||||
projectIconUrl?: string
|
||||
projectName?: string
|
||||
header?: string
|
||||
@@ -337,7 +339,7 @@ const props = withDefaults(
|
||||
loadingChangelog?: boolean
|
||||
}>(),
|
||||
{
|
||||
isModpack: false,
|
||||
projectType: undefined,
|
||||
projectIconUrl: undefined,
|
||||
projectName: undefined,
|
||||
header: undefined,
|
||||
@@ -346,8 +348,10 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const isModpack = computed(() => props.projectType === 'modpack')
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [version: Labrinth.Versions.v2.Version]
|
||||
update: [version: Labrinth.Versions.v2.Version, event: MouseEvent]
|
||||
cancel: []
|
||||
/** Emitted when user selects a version, so parent can fetch full version data with changelog */
|
||||
versionSelect: [version: Labrinth.Versions.v2.Version]
|
||||
@@ -374,8 +378,20 @@ watch(
|
||||
|
||||
// Handle initial selection when versions first arrive
|
||||
if (newVersions.length > 0 && !selectedVersion.value && pendingInitialVersionId.value) {
|
||||
const version =
|
||||
newVersions.find((v) => v.id === pendingInitialVersionId.value) ?? newVersions[0]
|
||||
const pendingFound = newVersions.find((v) => v.id === pendingInitialVersionId.value)
|
||||
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
|
||||
if (version) {
|
||||
emit('versionSelect', version)
|
||||
@@ -386,12 +402,30 @@ watch(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const NON_MOD_PROJECT_TYPES = new Set(['shader', 'shaderpack', 'resourcepack', 'datapack'])
|
||||
|
||||
function isVersionCompatible(version: Labrinth.Versions.v2.Version): boolean {
|
||||
const hasGameVersion = version.game_versions.includes(props.currentGameVersion)
|
||||
const hasLoader = version.loaders.some(
|
||||
(loader) => loader.toLowerCase() === props.currentLoader.toLowerCase(),
|
||||
)
|
||||
return hasGameVersion && hasLoader
|
||||
const skipLoaderCheck = props.projectType != null && NON_MOD_PROJECT_TYPES.has(props.projectType)
|
||||
const hasLoader =
|
||||
skipLoaderCheck ||
|
||||
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))
|
||||
@@ -413,10 +447,19 @@ const filteredVersions = computed(() => {
|
||||
)
|
||||
}
|
||||
|
||||
const beforeFilterCount = versions.length
|
||||
if (hideIncompatibleState.value) {
|
||||
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
|
||||
})
|
||||
|
||||
@@ -503,9 +546,9 @@ function handleVersionSelect(version: Labrinth.Versions.v2.Version) {
|
||||
emit('versionSelect', version)
|
||||
}
|
||||
|
||||
function handleUpdate() {
|
||||
function handleUpdate(event: MouseEvent) {
|
||||
if (selectedVersion.value) {
|
||||
emit('update', selectedVersion.value)
|
||||
emit('update', selectedVersion.value, event)
|
||||
hide()
|
||||
}
|
||||
}
|
||||
@@ -519,7 +562,23 @@ function show(initialVersionId?: string) {
|
||||
searchQuery.value = ''
|
||||
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) {
|
||||
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) {
|
||||
selectedVersion.value =
|
||||
props.versions.find((v) => v.id === initialVersionId) ?? props.versions[0]
|
||||
@@ -533,6 +592,9 @@ function show(initialVersionId?: string) {
|
||||
} else {
|
||||
selectedVersion.value = null
|
||||
pendingInitialVersionId.value = initialVersionId
|
||||
debug('show(): no versions yet, deferring selection', {
|
||||
pendingInitialVersionId: initialVersionId,
|
||||
})
|
||||
}
|
||||
|
||||
modal.value?.show()
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
class="size-5 shrink-0 text-brand-orange hover:brightness-110"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-secondary">
|
||||
{{ formatMessage(messages.shiftClickHint) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -111,5 +114,9 @@ const messages = defineMessages({
|
||||
defaultMessage:
|
||||
"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>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowUpDownIcon,
|
||||
ArrowDownAZIcon,
|
||||
ArrowDownZAIcon,
|
||||
ClockArrowDownIcon,
|
||||
ClockArrowUpIcon,
|
||||
CodeIcon,
|
||||
CompassIcon,
|
||||
DownloadIcon,
|
||||
@@ -77,9 +80,13 @@ const messages = defineMessages({
|
||||
id: 'content.page-layout.sort.alphabetical',
|
||||
defaultMessage: 'Alphabetical',
|
||||
},
|
||||
sortDateAdded: {
|
||||
id: 'content.page-layout.sort.date-added',
|
||||
defaultMessage: 'Date added',
|
||||
sortDateAddedNewest: {
|
||||
id: 'content.page-layout.sort.date-added-newest',
|
||||
defaultMessage: 'Newest first',
|
||||
},
|
||||
sortDateAddedOldest: {
|
||||
id: 'content.page-layout.sort.date-added-oldest',
|
||||
defaultMessage: 'Oldest first',
|
||||
},
|
||||
updateAll: {
|
||||
id: 'content.page-layout.update-all',
|
||||
@@ -147,34 +154,55 @@ const uploadOverallProgress = computed(() => {
|
||||
return Math.min((state.completedFiles + state.currentFileProgress) / state.totalFiles, 1)
|
||||
})
|
||||
|
||||
type SortMode = 'alphabetical' | 'date-added'
|
||||
const sortMode = ref<SortMode>('alphabetical')
|
||||
type SortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'date-added-newest' | 'date-added-oldest'
|
||||
const sortMode = ref<SortMode>('alphabetical-asc')
|
||||
|
||||
const sortLabels: Record<SortMode, () => string> = {
|
||||
alphabetical: () => formatMessage(messages.sortAlphabetical),
|
||||
'date-added': () => formatMessage(messages.sortDateAdded),
|
||||
'alphabetical-asc': () => formatMessage(messages.sortAlphabetical),
|
||||
'alphabetical-desc': () => formatMessage(messages.sortAlphabetical),
|
||||
'date-added-newest': () => formatMessage(messages.sortDateAddedNewest),
|
||||
'date-added-oldest': () => formatMessage(messages.sortDateAddedOldest),
|
||||
}
|
||||
|
||||
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)
|
||||
sortMode.value = modes[(idx + 1) % modes.length]
|
||||
}
|
||||
|
||||
const sortedItems = computed(() => {
|
||||
const items = [...ctx.items.value]
|
||||
if (sortMode.value === 'date-added') {
|
||||
return items.sort((a, b) => {
|
||||
const dateA = a.date_added ?? ''
|
||||
const dateB = b.date_added ?? ''
|
||||
return dateB.localeCompare(dateA)
|
||||
})
|
||||
switch (sortMode.value) {
|
||||
case 'alphabetical-desc':
|
||||
return items.sort((a, b) => {
|
||||
const nameA = a.project?.title ?? a.file_name
|
||||
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, [
|
||||
@@ -223,20 +251,28 @@ async function handleRefresh() {
|
||||
}
|
||||
}
|
||||
|
||||
const filteredItems = computed(() => applyFilters(search(sortedItems.value)))
|
||||
const tableItems = computed<ContentCardTableItem[]>(() =>
|
||||
filteredItems.value.map((item) => {
|
||||
const filteredItems = computed(() => {
|
||||
const sorted = sortedItems.value
|
||||
const searched = search(sorted)
|
||||
return applyFilters(searched)
|
||||
})
|
||||
const tableItems = computed<ContentCardTableItem[]>(() => {
|
||||
return filteredItems.value.map((item) => {
|
||||
const base = ctx.mapToTableItem(item)
|
||||
return {
|
||||
...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,
|
||||
hasUpdate: !ctx.isPackLocked.value && item.has_update,
|
||||
isClientOnly: isClientOnlyEnvironment(item.environment),
|
||||
overflowOptions: ctx.getOverflowOptions?.(item),
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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 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)
|
||||
if (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]
|
||||
confirmDeletionModal.value?.show()
|
||||
if (event?.shiftKey) {
|
||||
confirmDelete()
|
||||
} else {
|
||||
confirmDeletionModal.value?.show()
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
@@ -359,20 +403,28 @@ const pendingBulkUpdateItems = ref<ContentItem[]>([])
|
||||
|
||||
const hasBulkUpdateSupport = computed(() => !!(ctx.bulkUpdateItem || ctx.bulkUpdateItems))
|
||||
|
||||
function promptUpdateAll() {
|
||||
function promptUpdateAll(event?: MouseEvent) {
|
||||
if (!hasBulkUpdateSupport.value) return
|
||||
const items = ctx.items.value.filter((item) => item.has_update)
|
||||
if (items.length === 0) return
|
||||
pendingBulkUpdateItems.value = items
|
||||
confirmBulkUpdateModal.value?.show()
|
||||
if (event?.shiftKey) {
|
||||
confirmBulkUpdate()
|
||||
} else {
|
||||
confirmBulkUpdateModal.value?.show()
|
||||
}
|
||||
}
|
||||
|
||||
function promptUpdateSelected() {
|
||||
function promptUpdateSelected(event?: MouseEvent) {
|
||||
if (!hasBulkUpdateSupport.value) return
|
||||
const items = selectedItems.value.filter((item) => item.has_update)
|
||||
if (items.length === 0) return
|
||||
pendingBulkUpdateItems.value = items
|
||||
confirmBulkUpdateModal.value?.show()
|
||||
if (event?.shiftKey) {
|
||||
confirmBulkUpdate()
|
||||
} else {
|
||||
confirmBulkUpdateModal.value?.show()
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmBulkUpdate() {
|
||||
@@ -505,8 +557,8 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
clearable
|
||||
:placeholder="
|
||||
formatMessage(messages.searchPlaceholder, {
|
||||
count: ctx.items.value.length,
|
||||
contentType: `${ctx.contentTypeLabel.value}${ctx.items.value.length === 1 ? '' : 's'}`,
|
||||
count: tableItems.length,
|
||||
contentType: `${ctx.contentTypeLabel.value}${tableItems.length === 1 ? '' : 's'}`,
|
||||
})
|
||||
"
|
||||
/>
|
||||
@@ -580,7 +632,11 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
"
|
||||
@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]() }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -596,7 +652,11 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
"
|
||||
@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]() }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
@@ -88,14 +88,18 @@ const disabledPlatforms = computed(() => {
|
||||
|
||||
const showModpackVersionActions = ctx.showModpackVersionActions ?? true
|
||||
|
||||
function handleModpackUpdateRequest(version: Labrinth.Versions.v2.Version) {
|
||||
function handleModpackUpdateRequest(version: Labrinth.Versions.v2.Version, event?: MouseEvent) {
|
||||
pendingUpdateVersion.value = version
|
||||
const currentVersionId = ctx.updaterModalProps.value.currentVersionId
|
||||
const currentVersion = form.updatingProjectVersions.value.find((v) => v.id === currentVersionId)
|
||||
isUpdateDowngrade.value = currentVersion
|
||||
? new Date(version.date_published) < new Date(currentVersion.date_published)
|
||||
: false
|
||||
modpackUpdateModal.value?.show()
|
||||
if (event?.shiftKey) {
|
||||
handleModpackUpdateConfirm()
|
||||
} else {
|
||||
modpackUpdateModal.value?.show()
|
||||
}
|
||||
}
|
||||
|
||||
function handleModpackUpdateConfirm() {
|
||||
@@ -363,7 +367,7 @@ const messages = defineMessages({
|
||||
<button
|
||||
class="!shadow-none"
|
||||
:disabled="ctx.isBusy.value"
|
||||
@click="unlinkModal?.show()"
|
||||
@click="(e: MouseEvent) => (e.shiftKey ? handleUnlink() : unlinkModal?.show())"
|
||||
>
|
||||
<UnlinkIcon class="size-5" />
|
||||
{{
|
||||
@@ -395,7 +399,9 @@ const messages = defineMessages({
|
||||
<button
|
||||
class="!shadow-none"
|
||||
:disabled="ctx.isBusy.value"
|
||||
@click="reinstallModal?.show()"
|
||||
@click="
|
||||
(e: MouseEvent) => (e.shiftKey ? handleReinstall() : reinstallModal?.show())
|
||||
"
|
||||
>
|
||||
<SpinnerIcon v-if="ctx.reinstalling?.value" class="animate-spin" />
|
||||
<DownloadIcon v-else class="size-5" />
|
||||
@@ -659,7 +665,7 @@ const messages = defineMessages({
|
||||
:current-loader="ctx.updaterModalProps.value.currentLoader"
|
||||
:current-version-id="ctx.updaterModalProps.value.currentVersionId"
|
||||
:is-app="ctx.isApp"
|
||||
:is-modpack="true"
|
||||
project-type="modpack"
|
||||
:project-icon-url="ctx.updaterModalProps.value.projectIconUrl"
|
||||
:project-name="ctx.updaterModalProps.value.projectName"
|
||||
:loading="form.loadingVersions.value"
|
||||
|
||||
@@ -714,15 +714,20 @@ function resetUpdateState() {
|
||||
loadingChangelog.value = false
|
||||
}
|
||||
|
||||
function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version) {
|
||||
function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version, event?: MouseEvent) {
|
||||
if (updatingModpack.value) {
|
||||
const currentVersionId = contentQuery.data.value?.modpack?.spec.version_id
|
||||
const currentVersion = updatingProjectVersions.value.find((v) => v.id === currentVersionId)
|
||||
isModpackUpdateDowngrade.value = currentVersion
|
||||
? new Date(selectedVersion.date_published) < new Date(currentVersion.date_published)
|
||||
: false
|
||||
pendingModpackUpdateVersion.value = selectedVersion
|
||||
modpackUpdateModal.value?.show()
|
||||
if (event?.shiftKey) {
|
||||
pendingModpackUpdateVersion.value = selectedVersion
|
||||
handleModpackUpdateConfirm()
|
||||
} else {
|
||||
const currentVersionId = contentQuery.data.value?.modpack?.spec.version_id
|
||||
const currentVersion = updatingProjectVersions.value.find((v) => v.id === currentVersionId)
|
||||
isModpackUpdateDowngrade.value = currentVersion
|
||||
? new Date(selectedVersion.date_published) < new Date(currentVersion.date_published)
|
||||
: false
|
||||
pendingModpackUpdateVersion.value = selectedVersion
|
||||
modpackUpdateModal.value?.show()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -860,7 +865,7 @@ provideContentManager({
|
||||
: (updatingProject?.version?.id ?? '')
|
||||
"
|
||||
:is-app="false"
|
||||
:is-modpack="updatingModpack"
|
||||
:project-type="updatingModpack ? 'modpack' : updatingProject?.project_type"
|
||||
:project-icon-url="
|
||||
updatingModpack ? modpack?.project.icon_url : updatingProject?.project?.icon_url
|
||||
"
|
||||
|
||||
@@ -287,6 +287,9 @@
|
||||
"content.inline-backup.create-backup": {
|
||||
"defaultMessage": "Create backup"
|
||||
},
|
||||
"content.inline-backup.shift-click-hint": {
|
||||
"defaultMessage": "Hold Shift while clicking to skip confirmation."
|
||||
},
|
||||
"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."
|
||||
},
|
||||
@@ -353,8 +356,11 @@
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alphabetical"
|
||||
},
|
||||
"content.page-layout.sort.date-added": {
|
||||
"defaultMessage": "Date added"
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Newest first"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Oldest first"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sort by {mode}"
|
||||
@@ -875,6 +881,9 @@
|
||||
"label.delete": {
|
||||
"defaultMessage": "Delete"
|
||||
},
|
||||
"label.delete-immediately": {
|
||||
"defaultMessage": "Delete immediately"
|
||||
},
|
||||
"label.description": {
|
||||
"defaultMessage": "Description"
|
||||
},
|
||||
|
||||
@@ -85,6 +85,10 @@ export const commonMessages = defineMessages({
|
||||
id: 'label.delete',
|
||||
defaultMessage: 'Delete',
|
||||
},
|
||||
deleteImmediatelyLabel: {
|
||||
id: 'label.delete-immediately',
|
||||
defaultMessage: 'Delete immediately',
|
||||
},
|
||||
descriptionLabel: {
|
||||
id: 'label.description',
|
||||
defaultMessage: 'Description',
|
||||
|
||||
Reference in New Issue
Block a user