You've already forked AstralRinth
forked from xxxOFFxxx/AstralRinth
feat: manage project versions v2 (#5049)
* update add files copy and go to next step on just one file * rename and reorder stages * add metadata stage and update details stage * implement files inside metadata stage * use regular prettier instead of prettier eslint * remove changelog stage config * save button on details stage * update edit buttons in versions table * add collapse environment selector * implement dependencies list in metadata step * move dependencies into provider * add suggested dependencies to metadata stage * pnpm prepr * fix unused var * Revert "add collapse environment selector" This reverts commit f90fabc7a57ff201f26e1b628eeced8e6ef75865. * hide resource pack loader only when its the only loader * fix no dependencies for modpack * add breadcrumbs with hide breadcrumb option * wider stages * add proper horizonal scroll breadcrumbs * fix titles * handle save version in version page * remove box shadow * add notification provider to storybook * add drop area for versions to drop file right into page * fix mobile versions table buttons overflowing * pnpm prepr * fix drop file opening modal in wrong stage * implement invalid file for dropping files * allow horizontal scroll on breadcrumbs * update infer.js as best as possible * add create version button uploading version state * add extractVersionFromFilename for resource pack and datapack * allow jars for datapack project * detect multiple loaders when possible * iris means compatible with optifine too * infer environment on loader change as well * add tooltip * prevent navigate forward when cannot go to next step * larger breadcrumb click targets * hide loaders and mc versions stage until files added * fix max width in header * fix add files from metadata step jumping steps * define width in NewModal instead * disable remove dependency in metadata stage * switch metadata and details buttons positions * fix remove button spacing * do not allow duplicate suggested dependencies * fix version detection for fabric minecraft version semvar * better verion number detection based on filename * show resource pack loader but uneditable * remove vanilla shader detection * refactor: break up large infer.js into ts and modules * remove duplicated types * add fill missing from file name step * pnpm prepr * fix neoforge loader parse failing and not adding neoforge loader * add missing pack formats * handle new pack format * pnpm prepr * add another regex where it is version in anywhere in filename * only show resource pack or data pack options for filetype on datapack project * add redundant zip folder check * reject RP and DP if has redundant folder * fix hide stage in breadcrumb * add snapshot group key in case no release version. brings out 26.1 snapshots * pnpm prepr * open in group if has something selected * fix resource pack loader uneditable if accidentally selected on different project type * add new environment tags * add unknown and not applicable environment tags * pnpm prepr * use shared constant on labels * use ref for timeout * remove console logs * remove box shadow only for cm-content * feat: xhr upload + fix wrangler prettierignore * fix: upload content type fix * fix dependencies version width * fix already added dependencies logic * add changelog minheight * set progress percentage on button * add legacy fabric detection logic * lint * small update on create version button label --------- Co-authored-by: Calum H. (IMB11) <contact@cal.engineer> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@@ -11,21 +11,21 @@
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "always"
|
||||
},
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
**/dist
|
||||
**/.output
|
||||
**/.data
|
||||
**/.wrangler
|
||||
src/generated/**
|
||||
src/locales/**
|
||||
src/public/news/feed
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"@nuxtjs/i18n": "^9.0.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/iso-3166-2": "^1.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.1.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"glob": "^10.2.7",
|
||||
"nuxt": "^3.20.2",
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
<template>
|
||||
<MultiStageModal ref="modal" :stages="ctx.stageConfigs" :context="ctx" />
|
||||
<MultiStageModal
|
||||
ref="modal"
|
||||
:stages="ctx.stageConfigs"
|
||||
:context="ctx"
|
||||
:breadcrumbs="!editingVersion"
|
||||
@hide="() => (modalOpen = false)"
|
||||
/>
|
||||
<DropArea
|
||||
v-if="!modalOpen"
|
||||
:accept="acceptFileFromProjectType(projectV2.project_type)"
|
||||
@change="handleDropArea"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
DropArea,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
injectProjectPageContext,
|
||||
MultiStageModal,
|
||||
} from '@modrinth/ui'
|
||||
import { acceptFileFromProjectType } from '@modrinth/utils'
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||
|
||||
import {
|
||||
@@ -17,12 +30,17 @@ import {
|
||||
provideManageVersionContext,
|
||||
} from '~/providers/version/manage-version-modal'
|
||||
|
||||
const modal = useTemplateRef<ComponentExposed<typeof MultiStageModal>>('modal')
|
||||
const emit = defineEmits<{
|
||||
(e: 'save'): void
|
||||
}>()
|
||||
|
||||
const ctx = createManageVersionContext(modal)
|
||||
const modal = useTemplateRef<ComponentExposed<typeof MultiStageModal>>('modal')
|
||||
const modalOpen = ref(false)
|
||||
|
||||
const ctx = createManageVersionContext(modal, () => emit('save'))
|
||||
provideManageVersionContext(ctx)
|
||||
|
||||
const { newDraftVersion } = ctx
|
||||
const { newDraftVersion, editingVersion, handleNewFiles } = ctx
|
||||
|
||||
const { projectV2 } = injectProjectPageContext()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
@@ -64,6 +82,15 @@ function openCreateVersionModal(
|
||||
newDraftVersion(projectV2.value.id, version)
|
||||
modal.value?.setStage(stageId ?? 0)
|
||||
modal.value?.show()
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDropArea(files: FileList) {
|
||||
newDraftVersion(projectV2.value.id, null)
|
||||
modal.value?.setStage(0)
|
||||
await handleNewFiles(Array.from(files))
|
||||
modal.value?.show()
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 rounded-xl bg-button-bg px-4 py-1 text-button-text"
|
||||
class="flex h-11 items-center justify-between gap-2 rounded-xl bg-button-bg px-4 py-1 text-button-text"
|
||||
>
|
||||
<div class="grid max-w-[75%] grid-cols-[auto_1fr_auto] items-center gap-2">
|
||||
<Avatar v-if="icon" :src="icon" alt="dependency-icon" size="20px" :no-shadow="true" />
|
||||
@@ -9,7 +9,7 @@
|
||||
{{ name || 'Unknown Project' }}
|
||||
</span>
|
||||
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5">
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5 capitalize">
|
||||
{{ dependencyType }}
|
||||
</TagItem>
|
||||
</div>
|
||||
@@ -17,14 +17,15 @@
|
||||
<span
|
||||
v-if="versionName"
|
||||
v-tooltip="versionName"
|
||||
class="max-w-[35%] truncate whitespace-nowrap font-medium"
|
||||
class="truncate whitespace-nowrap font-medium"
|
||||
:class="!hideRemove ? 'max-w-[35%]' : 'max-w-[50%]'"
|
||||
>
|
||||
{{ versionName }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<div v-if="!hideRemove" class="flex items-center justify-end gap-1">
|
||||
<ButtonStyled size="standard" :circular="true">
|
||||
<button aria-label="Remove file" class="!shadow-none" @click="emitRemove">
|
||||
<button aria-label="Remove file" class="-mr-2 !shadow-none" @click="emitRemove">
|
||||
<XIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -42,12 +43,13 @@ const emit = defineEmits<{
|
||||
(e: 'remove'): void
|
||||
}>()
|
||||
|
||||
const { projectId, name, icon, dependencyType, versionName } = defineProps<{
|
||||
const { projectId, name, icon, dependencyType, versionName, hideRemove } = defineProps<{
|
||||
projectId: string
|
||||
name?: string
|
||||
icon?: string
|
||||
dependencyType: Labrinth.Versions.v2.DependencyType
|
||||
versionName?: string
|
||||
hideRemove?: boolean
|
||||
}>()
|
||||
|
||||
function emitRemove() {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div v-if="addedDependencies.length" class="5 flex flex-col gap-2">
|
||||
<template v-for="(dependency, index) in addedDependencies">
|
||||
<AddedDependencyRow
|
||||
v-if="dependency"
|
||||
:key="index"
|
||||
:project-id="dependency.projectId"
|
||||
:name="dependency.name"
|
||||
:icon="dependency.icon"
|
||||
:dependency-type="dependency.dependencyType"
|
||||
:version-name="dependency.versionName"
|
||||
:hide-remove="disableRemove"
|
||||
@remove="() => removeDependency(index)"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="!addedDependencies.length"> No dependencies added. </span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
import AddedDependencyRow from './AddedDependencyRow.vue'
|
||||
|
||||
const { disableRemove } = defineProps<{
|
||||
disableRemove?: boolean
|
||||
}>()
|
||||
|
||||
const { draftVersion, dependencyProjects, dependencyVersions, projectsFetchLoading } =
|
||||
injectManageVersionContext()
|
||||
|
||||
const addedDependencies = computed(() =>
|
||||
(draftVersion.value.dependencies || [])
|
||||
.map((dep) => {
|
||||
if (!dep.project_id) return null
|
||||
|
||||
const dependencyProject = dependencyProjects.value[dep.project_id]
|
||||
const versionName = dependencyVersions.value[dep.version_id || '']?.name ?? ''
|
||||
|
||||
if (!dependencyProject && projectsFetchLoading.value) return null
|
||||
|
||||
return {
|
||||
projectId: dep.project_id,
|
||||
name: dependencyProject?.name,
|
||||
icon: dependencyProject?.icon_url,
|
||||
dependencyType: dep.dependency_type,
|
||||
versionName,
|
||||
}
|
||||
})
|
||||
.filter(Boolean),
|
||||
)
|
||||
|
||||
const removeDependency = (index: number) => {
|
||||
if (!draftVersion.value.dependencies) return
|
||||
draftVersion.value.dependencies.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
@@ -118,4 +118,17 @@ function groupLoaders(loaders: Labrinth.Tags.v2.Loader[]) {
|
||||
}
|
||||
|
||||
const groupedLoaders = computed(() => groupLoaders(loaders))
|
||||
|
||||
onMounted(() => {
|
||||
if (selectedLoaders.value.length === 0) return
|
||||
|
||||
// Find the first group that contains any of the selected loaders
|
||||
const groups = groupedLoaders.value
|
||||
for (const [groupName, loadersInGroup] of Object.entries(groups)) {
|
||||
if (loadersInGroup.some((loader) => selectedLoaders.value.includes(loader.name))) {
|
||||
loaderGroup.value = groupName as GroupLabels
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -66,7 +66,7 @@ import type { Labrinth } from '@modrinth/api-client'
|
||||
import { SearchIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, Chips } from '@modrinth/ui'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
type GameVersion = Labrinth.Tags.v2.GameVersion
|
||||
|
||||
@@ -147,9 +147,15 @@ function groupVersions(gameVersions: GameVersion[]) {
|
||||
)
|
||||
|
||||
const getGroupKey = (v: string) => v.split('.').slice(0, 2).join('.')
|
||||
|
||||
const getSnapshotGroupKey = (v: string) => {
|
||||
const cleanVersion = v.split('-')[0]
|
||||
return cleanVersion.split('.').slice(0, 2).join('.')
|
||||
}
|
||||
|
||||
const groups: Record<string, string[]> = {}
|
||||
|
||||
let currentGroupKey = getGroupKey(gameVersions.find((v) => v.major)?.version || '')
|
||||
let currentGroupKey = getSnapshotGroupKey(gameVersions.find((v) => v.major)?.version || '')
|
||||
|
||||
gameVersions.forEach((gameVersion) => {
|
||||
if (gameVersion.version_type === 'release') {
|
||||
@@ -157,6 +163,8 @@ function groupVersions(gameVersions: GameVersion[]) {
|
||||
if (!groups[currentGroupKey]) groups[currentGroupKey] = []
|
||||
groups[currentGroupKey].push(gameVersion.version)
|
||||
} else {
|
||||
if (!currentGroupKey) currentGroupKey = getSnapshotGroupKey(gameVersion.version)
|
||||
|
||||
const key = `${currentGroupKey} ${DEV_RELEASE_KEY}`
|
||||
if (!groups[key]) groups[key] = []
|
||||
groups[key].push(gameVersion.version)
|
||||
@@ -205,4 +213,27 @@ function compareGroupKeys(a: string, b: string) {
|
||||
function searchFilter(gameVersion: Labrinth.Tags.v2.GameVersion) {
|
||||
return gameVersion.version.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.modelValue.length === 0) return
|
||||
|
||||
// Open non-release tab if any non-release versions are selected
|
||||
const hasNonReleaseVersions = props.gameVersions.some(
|
||||
(v) => props.modelValue.includes(v.version) && v.version_type !== 'release',
|
||||
)
|
||||
|
||||
if (hasNonReleaseVersions) {
|
||||
versionType.value = 'all'
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
const firstSelectedVersion = allVersionsFlat.value.find((v) => props.modelValue.includes(v))
|
||||
if (firstSelectedVersion) {
|
||||
const buttons = Array.from(document.querySelectorAll('button'))
|
||||
const element = buttons.find((btn) => btn.textContent?.trim() === firstSelectedVersion)
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<div v-if="visibleDependencies.length" class="flex flex-col gap-4">
|
||||
<span class="font-semibold text-contrast">Suggested dependencies</span>
|
||||
<div class="flex flex-col gap-2">
|
||||
<template v-for="(dependency, index) in visibleDependencies">
|
||||
<div v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-2">
|
||||
<template v-for="(dependency, index) in visibleSuggestedDependencies">
|
||||
<SuggestedDependency
|
||||
v-if="dependency"
|
||||
:key="index"
|
||||
@@ -22,7 +20,6 @@
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -32,28 +29,7 @@ import { injectManageVersionContext } from '~/providers/version/manage-version-m
|
||||
|
||||
import SuggestedDependency from './SuggestedDependency.vue'
|
||||
|
||||
export interface SuggestedDependency extends Labrinth.Versions.v3.Dependency {
|
||||
icon?: string
|
||||
name?: string
|
||||
versionName?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
suggestedDependencies: SuggestedDependency[]
|
||||
}>()
|
||||
|
||||
const { draftVersion } = injectManageVersionContext()
|
||||
|
||||
const visibleDependencies = computed<SuggestedDependency[]>(() =>
|
||||
props.suggestedDependencies
|
||||
.filter(
|
||||
(dep) =>
|
||||
!draftVersion.value.dependencies?.some(
|
||||
(d) => d.project_id === dep.project_id && d.version_id === dep.version_id,
|
||||
),
|
||||
)
|
||||
.sort((a, b) => (a.name || '').localeCompare(b.name || '')),
|
||||
)
|
||||
const { visibleSuggestedDependencies } = injectManageVersionContext()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'onAddSuggestion', dependency: Labrinth.Versions.v3.Dependency): void
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 rounded-xl bg-button-bg px-4 py-1 text-button-text"
|
||||
class="flex items-center justify-between gap-2 rounded-xl border-2 border-dashed border-surface-5 px-4 py-1 text-button-text"
|
||||
>
|
||||
<div class="grid max-w-[75%] grid-cols-[auto_1fr_auto] items-center gap-2">
|
||||
<Avatar v-if="icon" :src="icon" alt="dependency-icon" size="20px" :no-shadow="true" />
|
||||
@@ -9,7 +9,7 @@
|
||||
{{ name || 'Unknown Project' }}
|
||||
</span>
|
||||
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5">
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5 capitalize">
|
||||
{{ dependencyType }}
|
||||
</TagItem>
|
||||
</div>
|
||||
@@ -23,7 +23,7 @@
|
||||
</span>
|
||||
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<ButtonStyled size="standard" :circular="true">
|
||||
<ButtonStyled size="standard" :circular="true" type="transparent">
|
||||
<button aria-label="Add dependency" class="!shadow-none" @click="emitAddSuggestion">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -68,10 +68,16 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ArrowLeftRightIcon, CheckIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, Combobox, injectProjectPageContext } from '@modrinth/ui'
|
||||
import type { DropdownOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||
import type { ComboboxOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||
import { acceptFileFromProjectType } from '@modrinth/utils'
|
||||
|
||||
import {
|
||||
fileTypeLabels,
|
||||
injectManageVersionContext,
|
||||
} from '~/providers/version/manage-version-modal'
|
||||
|
||||
const { projectV2 } = injectProjectPageContext()
|
||||
const { projectType } = injectManageVersionContext()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'setPrimaryFile', file?: File): void
|
||||
@@ -89,16 +95,29 @@ const { name, isPrimary, onRemove, initialFileType, editingVersion } = definePro
|
||||
const selectedType = ref<Labrinth.Versions.v3.FileType | 'primary'>(initialFileType || 'unknown')
|
||||
const primaryFileInput = ref<HTMLInputElement>()
|
||||
|
||||
const versionTypes = [
|
||||
!editingVersion && { class: 'text-sm', value: 'primary', label: 'Primary' },
|
||||
{ class: 'text-sm', value: 'unknown', label: 'Other' },
|
||||
{ class: 'text-sm', value: 'required-resource-pack', label: 'Required RP' },
|
||||
{ class: 'text-sm', value: 'optional-resource-pack', label: 'Optional RP' },
|
||||
{ class: 'text-sm', value: 'sources-jar', label: 'Sources JAR' },
|
||||
{ class: 'text-sm', value: 'dev-jar', label: 'Dev JAR' },
|
||||
{ class: 'text-sm', value: 'javadoc-jar', label: 'Javadoc JAR' },
|
||||
{ class: 'text-sm', value: 'signature', label: 'Signature' },
|
||||
].filter(Boolean) as DropdownOption<Labrinth.Versions.v3.FileType | 'primary'>[]
|
||||
const isDatapackProject = computed(() => projectType.value === 'datapack')
|
||||
|
||||
const versionTypes = computed(
|
||||
() =>
|
||||
[
|
||||
!editingVersion && { class: 'text-sm', value: 'primary', label: fileTypeLabels.primary },
|
||||
{ class: 'text-sm', value: 'unknown', label: fileTypeLabels.unknown },
|
||||
isDatapackProject.value && {
|
||||
class: 'text-sm',
|
||||
value: 'required-resource-pack',
|
||||
label: fileTypeLabels['required-resource-pack'],
|
||||
},
|
||||
isDatapackProject.value && {
|
||||
class: 'text-sm',
|
||||
value: 'optional-resource-pack',
|
||||
label: fileTypeLabels['optional-resource-pack'],
|
||||
},
|
||||
{ class: 'text-sm', value: 'sources-jar', label: fileTypeLabels['sources-jar'] },
|
||||
{ class: 'text-sm', value: 'dev-jar', label: fileTypeLabels['dev-jar'] },
|
||||
{ class: 'text-sm', value: 'javadoc-jar', label: fileTypeLabels['javadoc-jar'] },
|
||||
{ class: 'text-sm', value: 'signature', label: fileTypeLabels.signature },
|
||||
].filter(Boolean) as ComboboxOption<Labrinth.Versions.v3.FileType | 'primary'>[],
|
||||
)
|
||||
|
||||
function emitFileTypeChange() {
|
||||
if (selectedType.value === 'primary') emit('setPrimaryFile')
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 rounded-xl bg-button-bg px-4 py-2 text-button-text"
|
||||
>
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<FileIcon v-if="isPrimary" class="text-lg" />
|
||||
<FilePlusIcon v-else class="text-lg" />
|
||||
|
||||
<span v-tooltip="name" class="overflow-hidden text-ellipsis whitespace-nowrap font-medium">
|
||||
{{ name }}
|
||||
</span>
|
||||
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5">
|
||||
{{ isPrimary ? 'Primary' : fileTypeLabels[fileType ?? 'unknown'] }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { FileIcon, FilePlusIcon } from '@modrinth/assets'
|
||||
import { TagItem } from '@modrinth/ui'
|
||||
|
||||
import { fileTypeLabels } from '~/providers/version/manage-version-modal'
|
||||
|
||||
const { name, isPrimary, fileType } = defineProps<{
|
||||
name: string
|
||||
isPrimary?: boolean
|
||||
fileType?: Labrinth.Versions.v3.FileType | 'primary'
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<MarkdownEditor
|
||||
v-model="draftVersion.changelog"
|
||||
:on-image-upload="onImageUpload"
|
||||
:max-height="500"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { MarkdownEditor } from '@modrinth/ui'
|
||||
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
const { draftVersion } = injectManageVersionContext()
|
||||
|
||||
async function onImageUpload(file: File) {
|
||||
const response = await useImageUpload(file, { context: 'version' })
|
||||
return response.url
|
||||
}
|
||||
</script>
|
||||
@@ -1,272 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6 sm:w-[512px]">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">
|
||||
Version type <span class="text-red">*</span>
|
||||
</span>
|
||||
<Chips
|
||||
v-model="draftVersion.version_type"
|
||||
:items="['release', 'beta', 'alpha']"
|
||||
:never-empty="true"
|
||||
:capitalize="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">
|
||||
Version number <span class="text-red">*</span>
|
||||
</span>
|
||||
<input
|
||||
id="version-number"
|
||||
v-model="draftVersion.version_number"
|
||||
placeholder="Enter version number, e.g. 1.2.3-alpha.1"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
maxlength="32"
|
||||
/>
|
||||
<span> The version number differentiates this specific version from others. </span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast"> Version subtitle </span>
|
||||
<input
|
||||
id="version-number"
|
||||
v-model="draftVersion.name"
|
||||
placeholder="Enter subtitle..."
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
maxlength="256"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="!noLoadersProject && (inferredVersionData?.loaders?.length || editingVersion)">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast">
|
||||
{{ usingDetectedLoaders ? 'Detected loaders' : 'Loaders' }}
|
||||
</span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button
|
||||
v-tooltip="isModpack ? 'Modpack versions cannot be edited' : undefined"
|
||||
:disabled="isModpack"
|
||||
@click="editLoaders"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-1.5 gap-y-4 rounded-xl border border-solid border-surface-5 p-3 py-4"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template
|
||||
v-for="loader in draftVersionLoaders.map((selectedLoader) =>
|
||||
loaders.find((loader) => selectedLoader === loader.name),
|
||||
)"
|
||||
>
|
||||
<TagItem
|
||||
v-if="loader"
|
||||
:key="`loader-${loader.name}`"
|
||||
class="border !border-solid border-surface-5 hover:no-underline"
|
||||
:style="`--_color: var(--color-platform-${loader.name})`"
|
||||
>
|
||||
<div v-html="loader.icon"></div>
|
||||
{{ formatCategory(loader.name) }}
|
||||
</TagItem>
|
||||
</template>
|
||||
|
||||
<span v-if="!draftVersion.loaders.length">No loaders selected.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="inferredVersionData?.game_versions?.length || editingVersion">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast">
|
||||
{{ usingDetectedVersions ? 'Detected versions' : 'Versions' }}
|
||||
</span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button
|
||||
v-tooltip="isModpack ? 'Modpack versions cannot be edited' : undefined"
|
||||
:disabled="isModpack"
|
||||
@click="editVersions"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex max-h-56 flex-col gap-1.5 gap-y-4 overflow-y-auto rounded-xl border border-solid border-surface-5 p-3 py-4"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<TagItem
|
||||
v-for="version in draftVersion.game_versions"
|
||||
:key="version"
|
||||
class="border !border-solid border-surface-5 hover:no-underline"
|
||||
>
|
||||
{{ version }}
|
||||
</TagItem>
|
||||
|
||||
<span v-if="!draftVersion.game_versions.length">No versions selected.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
!noEnvironmentProject &&
|
||||
((!editingVersion && inferredVersionData?.environment) ||
|
||||
(editingVersion && draftVersion.environment))
|
||||
"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast"> Environment </span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button @click="editEnvironment">
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5 gap-y-4 rounded-xl bg-surface-2 p-3 py-4">
|
||||
<div v-if="draftVersion.environment" class="flex flex-col gap-1">
|
||||
<div class="font-semibold text-contrast">
|
||||
{{ environmentCopy.title }}
|
||||
</div>
|
||||
<div class="text-sm font-medium">{{ environmentCopy.description }}</div>
|
||||
</div>
|
||||
|
||||
<span v-else class="text-sm font-medium">No environment has been set.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { EditIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Chips,
|
||||
defineMessages,
|
||||
ENVIRONMENTS_COPY,
|
||||
TagItem,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
import { useGeneratedState } from '~/composables/generated'
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
const {
|
||||
draftVersion,
|
||||
inferredVersionData,
|
||||
projectType,
|
||||
editingVersion,
|
||||
noLoadersProject,
|
||||
noEnvironmentProject,
|
||||
modal,
|
||||
} = injectManageVersionContext()
|
||||
|
||||
const generatedState = useGeneratedState()
|
||||
const loaders = computed(() => generatedState.value.loaders)
|
||||
const isModpack = computed(() => projectType.value === 'modpack')
|
||||
|
||||
const draftVersionLoaders = computed(() =>
|
||||
[
|
||||
...new Set([...draftVersion.value.loaders, ...(draftVersion.value.mrpack_loaders ?? [])]),
|
||||
].filter((loader) => loader !== 'mrpack'),
|
||||
)
|
||||
|
||||
const editLoaders = () => {
|
||||
modal.value?.setStage('from-details-loaders')
|
||||
}
|
||||
const editVersions = () => {
|
||||
modal.value?.setStage('from-details-mc-versions')
|
||||
}
|
||||
const editEnvironment = () => {
|
||||
modal.value?.setStage('from-details-environment')
|
||||
}
|
||||
|
||||
const usingDetectedVersions = computed(() => {
|
||||
if (!inferredVersionData.value?.game_versions) return false
|
||||
|
||||
const versionsMatch =
|
||||
draftVersion.value.game_versions.length === inferredVersionData.value.game_versions.length &&
|
||||
draftVersion.value.game_versions.every((version) =>
|
||||
inferredVersionData.value?.game_versions?.includes(version),
|
||||
)
|
||||
|
||||
return versionsMatch
|
||||
})
|
||||
|
||||
const usingDetectedLoaders = computed(() => {
|
||||
if (!inferredVersionData.value?.loaders) return false
|
||||
|
||||
const loadersMatch =
|
||||
draftVersion.value.loaders.length === inferredVersionData.value.loaders.length &&
|
||||
draftVersion.value.loaders.every((loader) =>
|
||||
inferredVersionData.value?.loaders?.includes(loader),
|
||||
)
|
||||
|
||||
return loadersMatch
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const noEnvironmentMessage = defineMessages({
|
||||
title: {
|
||||
id: 'version.environment.none.title',
|
||||
defaultMessage: 'No environment set',
|
||||
},
|
||||
description: {
|
||||
id: 'version.environment.none.description',
|
||||
defaultMessage: 'The environment for this version has not been specified.',
|
||||
},
|
||||
})
|
||||
|
||||
const unknownEnvironmentMessage = defineMessages({
|
||||
title: {
|
||||
id: 'version.environment.unknown.title',
|
||||
defaultMessage: 'Unknown environment',
|
||||
},
|
||||
description: {
|
||||
id: 'version.environment.unknown.description',
|
||||
defaultMessage: 'The environment: "{environment}" is not recognized.',
|
||||
},
|
||||
})
|
||||
|
||||
const environmentCopy = computed(() => {
|
||||
if (!draftVersion.value.environment) {
|
||||
return {
|
||||
title: formatMessage(noEnvironmentMessage.title),
|
||||
description: formatMessage(noEnvironmentMessage.description),
|
||||
}
|
||||
}
|
||||
|
||||
const envCopy = ENVIRONMENTS_COPY[draftVersion.value.environment]
|
||||
if (envCopy) {
|
||||
return {
|
||||
title: formatMessage(envCopy.title),
|
||||
description: formatMessage(envCopy.description),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: formatMessage(unknownEnvironmentMessage.title),
|
||||
description: formatMessage(unknownEnvironmentMessage.description, {
|
||||
environment: draftVersion.value.environment,
|
||||
}),
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,11 +1,15 @@
|
||||
<template>
|
||||
<div class="flex w-full flex-col gap-4 sm:w-[512px]">
|
||||
<template v-if="!(filesToAdd.length || draftVersion.existing_files?.length)">
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<template
|
||||
v-if="handlingNewFiles || !(filesToAdd.length || draftVersion.existing_files?.length)"
|
||||
>
|
||||
<DropzoneFileInput
|
||||
aria-label="Upload file"
|
||||
multiple
|
||||
:accept="acceptFileFromProjectType(projectV2.project_type)"
|
||||
:max-size="524288000"
|
||||
primary-prompt="Upload primary and supporting files"
|
||||
secondary-prompt="Drag and drop files or click to browse"
|
||||
@change="handleNewFiles"
|
||||
/>
|
||||
</template>
|
||||
@@ -21,11 +25,7 @@
|
||||
:is-primary="true"
|
||||
:editing-version="editingVersion"
|
||||
:on-remove="undefined"
|
||||
@set-primary-file="
|
||||
(file) => {
|
||||
if (file && !editingVersion) filesToAdd[0] = { file }
|
||||
}
|
||||
"
|
||||
@set-primary-file="(file) => file && replacePrimaryFile(file)"
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
@@ -72,7 +72,7 @@
|
||||
:editing-version="editingVersion"
|
||||
:on-remove="() => handleRemoveFile(idx + (primaryFile?.existing ? 0 : 1))"
|
||||
@set-file-type="(type) => (versionFile.fileType = type)"
|
||||
@set-primary-file="handleSetPrimaryFile(idx + (primaryFile?.existing ? 0 : 1))"
|
||||
@set-primary-file="() => swapPrimaryFile(idx + (primaryFile?.existing ? 0 : 1))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,7 +86,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
Admonition,
|
||||
defineMessages,
|
||||
@@ -107,68 +106,18 @@ const {
|
||||
draftVersion,
|
||||
filesToAdd,
|
||||
existingFilesToDelete,
|
||||
setPrimaryFile,
|
||||
setInferredVersionData,
|
||||
handlingNewFiles,
|
||||
swapPrimaryFile,
|
||||
replacePrimaryFile,
|
||||
editingVersion,
|
||||
primaryFile,
|
||||
handleNewFiles,
|
||||
} = injectManageVersionContext()
|
||||
|
||||
const addDetectedData = async () => {
|
||||
if (editingVersion.value) return
|
||||
|
||||
const primaryFile = filesToAdd.value[0]?.file
|
||||
if (!primaryFile) return
|
||||
|
||||
try {
|
||||
const inferredData = await setInferredVersionData(primaryFile, projectV2.value)
|
||||
const mappedInferredData: Partial<Labrinth.Versions.v3.DraftVersion> = {
|
||||
...inferredData,
|
||||
name: inferredData.name || '',
|
||||
}
|
||||
|
||||
draftVersion.value = {
|
||||
...draftVersion.value,
|
||||
...mappedInferredData,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing version file data', err)
|
||||
}
|
||||
}
|
||||
|
||||
// add detected data when the primary file changes
|
||||
watch(
|
||||
() => filesToAdd.value[0]?.file,
|
||||
() => addDetectedData(),
|
||||
)
|
||||
|
||||
function handleNewFiles(newFiles: File[]) {
|
||||
// detect primary file if no primary file is set
|
||||
const primaryFileIndex = primaryFile.value ? null : detectPrimaryFileIndex(newFiles)
|
||||
|
||||
newFiles.forEach((file) => filesToAdd.value.push({ file }))
|
||||
|
||||
if (primaryFileIndex !== null) {
|
||||
if (primaryFileIndex) setPrimaryFile(primaryFileIndex)
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemoveFile(index: number) {
|
||||
filesToAdd.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function detectPrimaryFileIndex(files: File[]): number {
|
||||
const extensionPriority = ['.jar', '.zip', '.litemod', '.mrpack', '.mrpack-primary']
|
||||
|
||||
for (const ext of extensionPriority) {
|
||||
const matches = files.filter((file) => file.name.toLowerCase().endsWith(ext))
|
||||
if (matches.length > 0) {
|
||||
const shortest = matches.reduce((a, b) => (a.name.length < b.name.length ? a : b))
|
||||
return files.indexOf(shortest)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function handleRemoveExistingFile(sha1: string) {
|
||||
existingFilesToDelete.value.push(sha1)
|
||||
draftVersion.value.existing_files = draftVersion.value.existing_files?.filter(
|
||||
@@ -176,38 +125,6 @@ function handleRemoveExistingFile(sha1: string) {
|
||||
)
|
||||
}
|
||||
|
||||
function handleSetPrimaryFile(index: number) {
|
||||
setPrimaryFile(index)
|
||||
}
|
||||
|
||||
interface PrimaryFile {
|
||||
name: string
|
||||
fileType?: string
|
||||
existing?: boolean
|
||||
}
|
||||
|
||||
const primaryFile = computed<PrimaryFile | null>(() => {
|
||||
const existingPrimaryFile = draftVersion.value.existing_files?.[0]
|
||||
if (existingPrimaryFile) {
|
||||
return {
|
||||
name: existingPrimaryFile.filename,
|
||||
fileType: existingPrimaryFile.file_type,
|
||||
existing: true,
|
||||
}
|
||||
}
|
||||
|
||||
const addedPrimaryFile = filesToAdd.value[0]
|
||||
if (addedPrimaryFile) {
|
||||
return {
|
||||
name: addedPrimaryFile.file.name,
|
||||
fileType: addedPrimaryFile.fileType,
|
||||
existing: false,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const supplementaryNewFiles = computed(() => {
|
||||
if (primaryFile.value?.existing) {
|
||||
return filesToAdd.value
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="flex w-full max-w-full flex-col gap-6 sm:w-[512px]">
|
||||
<div class="flex w-full max-w-full flex-col gap-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<span class="font-semibold text-contrast">Add dependency</span>
|
||||
<div class="flex flex-col gap-3 rounded-2xl border border-solid border-surface-5 p-4">
|
||||
<div class="grid gap-2.5">
|
||||
<span class="font-semibold text-contrast">Project <span class="text-red">*</span></span>
|
||||
<span class="font-semibold text-contrast">Project</span>
|
||||
<DependencySelect v-model="newDependencyProjectId" />
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ButtonStyled>
|
||||
<ButtonStyled color="green">
|
||||
<button
|
||||
class="self-start"
|
||||
:disabled="!newDependencyProjectId"
|
||||
@@ -55,28 +55,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SuggestedDependencies
|
||||
:suggested-dependencies="suggestedDependencies"
|
||||
@on-add-suggestion="handleAddSuggestedDependency"
|
||||
/>
|
||||
<div v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-4">
|
||||
<span class="font-semibold text-contrast">Suggested dependencies</span>
|
||||
<SuggestedDependencies @on-add-suggestion="handleAddSuggestedDependency" />
|
||||
</div>
|
||||
|
||||
<div v-if="addedDependencies.length" class="flex flex-col gap-4">
|
||||
<span class="font-semibold text-contrast">Added dependencies</span>
|
||||
<div class="5 flex flex-col gap-2">
|
||||
<template v-for="(dependency, index) in addedDependencies">
|
||||
<AddedDependencyRow
|
||||
v-if="dependency"
|
||||
:key="index"
|
||||
:project-id="dependency.projectId"
|
||||
:name="dependency.name"
|
||||
:icon="dependency.icon"
|
||||
:dependency-type="dependency.dependencyType"
|
||||
:version-name="dependency.versionName"
|
||||
@remove="() => removeDependency(index)"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="!addedDependencies.length"> No dependencies added. </span>
|
||||
</div>
|
||||
<DependenciesList />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -88,19 +74,26 @@ import {
|
||||
Combobox,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
injectProjectPageContext,
|
||||
} from '@modrinth/ui'
|
||||
import type { DropdownOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||
import type { ComboboxOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||
|
||||
import DependencySelect from '~/components/ui/create-project-version/components/DependencySelect.vue'
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
import AddedDependencyRow from '../components/AddedDependencyRow.vue'
|
||||
import DependenciesList from '../components/DependenciesList.vue'
|
||||
import SuggestedDependencies from '../components/SuggestedDependencies/SuggestedDependencies.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { labrinth } = injectModrinthClient()
|
||||
|
||||
const {
|
||||
draftVersion,
|
||||
dependencyProjects,
|
||||
dependencyVersions,
|
||||
projectsFetchLoading,
|
||||
visibleSuggestedDependencies,
|
||||
} = injectManageVersionContext()
|
||||
|
||||
const errorNotification = (err: any) => {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
@@ -113,12 +106,7 @@ const newDependencyProjectId = ref<string>()
|
||||
const newDependencyType = ref<Labrinth.Versions.v2.DependencyType>('required')
|
||||
const newDependencyVersionId = ref<string | null>(null)
|
||||
|
||||
const newDependencyVersions = ref<DropdownOption<string>[]>([])
|
||||
|
||||
const projectsFetchLoading = ref(false)
|
||||
const suggestedDependencies = ref<
|
||||
Array<Labrinth.Versions.v3.Dependency & { name?: string; icon?: string; versionName?: string }>
|
||||
>([])
|
||||
const newDependencyVersions = ref<ComboboxOption<string>[]>([])
|
||||
|
||||
// reset to defaults when select different project
|
||||
watch(newDependencyProjectId, async () => {
|
||||
@@ -140,91 +128,6 @@ watch(newDependencyProjectId, async () => {
|
||||
}
|
||||
})
|
||||
|
||||
const { draftVersion, dependencyProjects, dependencyVersions, getProject, getVersion } =
|
||||
injectManageVersionContext()
|
||||
const { projectV2: project } = injectProjectPageContext()
|
||||
|
||||
const getSuggestedDependencies = async () => {
|
||||
try {
|
||||
suggestedDependencies.value = []
|
||||
|
||||
if (!draftVersion.value.game_versions?.length || !draftVersion.value.loaders?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const versions = await labrinth.versions_v3.getProjectVersions(project.value.id, {
|
||||
loaders: draftVersion.value.loaders,
|
||||
})
|
||||
|
||||
// Get the most recent matching version and extract its dependencies
|
||||
if (versions.length > 0) {
|
||||
const mostRecentVersion = versions[0]
|
||||
for (const dep of mostRecentVersion.dependencies) {
|
||||
suggestedDependencies.value.push({
|
||||
project_id: dep.project_id,
|
||||
version_id: dep.version_id,
|
||||
dependency_type: dep.dependency_type,
|
||||
file_name: dep.file_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to get versions for project ${project.value.id}:`, error)
|
||||
}
|
||||
|
||||
for (const dep of suggestedDependencies.value) {
|
||||
try {
|
||||
if (dep.project_id) {
|
||||
const proj = await getProject(dep.project_id)
|
||||
dep.name = proj.name
|
||||
dep.icon = proj.icon_url
|
||||
}
|
||||
|
||||
if (dep.version_id) {
|
||||
const version = await getVersion(dep.version_id)
|
||||
dep.versionName = version.name
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to fetch project/version data for dependency:`, error)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
errorNotification(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSuggestedDependencies()
|
||||
})
|
||||
|
||||
watch(
|
||||
draftVersion,
|
||||
async (draftVersion) => {
|
||||
const deps = draftVersion.dependencies || []
|
||||
|
||||
for (const dep of deps) {
|
||||
if (dep?.project_id) {
|
||||
try {
|
||||
await getProject(dep.project_id)
|
||||
} catch (error: any) {
|
||||
errorNotification(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (dep?.version_id) {
|
||||
try {
|
||||
await getVersion(dep.version_id)
|
||||
} catch (error: any) {
|
||||
errorNotification(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
projectsFetchLoading.value = false
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
const addedDependencies = computed(() =>
|
||||
(draftVersion.value.dependencies || [])
|
||||
.map((dep) => {
|
||||
@@ -249,12 +152,13 @@ const addedDependencies = computed(() =>
|
||||
const addDependency = (dependency: Labrinth.Versions.v3.Dependency) => {
|
||||
if (!draftVersion.value.dependencies) draftVersion.value.dependencies = []
|
||||
|
||||
// already added
|
||||
if (
|
||||
draftVersion.value.dependencies.find(
|
||||
(d) => d.project_id === dependency.project_id && d.version_id === dependency.version_id,
|
||||
)
|
||||
) {
|
||||
const alreadyAdded = draftVersion.value.dependencies.some((existing) => {
|
||||
if (existing.project_id !== dependency.project_id) return false
|
||||
if (!existing.version_id && !dependency.version_id) return true
|
||||
return existing.version_id === dependency.version_id
|
||||
})
|
||||
|
||||
if (alreadyAdded) {
|
||||
addNotification({
|
||||
title: 'Dependency already added',
|
||||
text: 'You cannot add the same dependency twice.',
|
||||
@@ -268,11 +172,6 @@ const addDependency = (dependency: Labrinth.Versions.v3.Dependency) => {
|
||||
newDependencyProjectId.value = undefined
|
||||
}
|
||||
|
||||
const removeDependency = (index: number) => {
|
||||
if (!draftVersion.value.dependencies) return
|
||||
draftVersion.value.dependencies.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleAddSuggestedDependency = (dependency: Labrinth.Versions.v3.Dependency) => {
|
||||
draftVersion.value.dependencies?.push({
|
||||
project_id: dependency.project_id,
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="flex w-full flex-col gap-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">
|
||||
Version type <span class="text-red">*</span>
|
||||
</span>
|
||||
<Chips
|
||||
v-model="draftVersion.version_type"
|
||||
:items="['release', 'beta', 'alpha']"
|
||||
:never-empty="true"
|
||||
:capitalize="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">
|
||||
Version number <span class="text-red">*</span>
|
||||
</span>
|
||||
<input
|
||||
id="version-number"
|
||||
v-model="draftVersion.version_number"
|
||||
placeholder="Enter version number, e.g. 1.2.3-alpha.1"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
maxlength="32"
|
||||
/>
|
||||
<span> The version number differentiates this specific version from others. </span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast"> Version subtitle </span>
|
||||
<input
|
||||
id="version-number"
|
||||
v-model="draftVersion.name"
|
||||
placeholder="Enter subtitle..."
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
maxlength="256"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast"> Version changlog </span>
|
||||
|
||||
<div class="w-full">
|
||||
<MarkdownEditor
|
||||
v-model="draftVersion.changelog"
|
||||
:on-image-upload="onImageUpload"
|
||||
:min-height="150"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Chips, MarkdownEditor } from '@modrinth/ui'
|
||||
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
const { draftVersion } = injectManageVersionContext()
|
||||
|
||||
async function onImageUpload(file: File) {
|
||||
const response = await useImageUpload(file, { context: 'version' })
|
||||
return response.url
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="space-y-6 sm:w-[512px]">
|
||||
<div class="space-y-6">
|
||||
<LoaderPicker
|
||||
v-model="draftVersion.loaders"
|
||||
:loaders="generatedState.loaders"
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6 sm:w-[512px]">
|
||||
<div class="flex flex-col gap-6">
|
||||
<McVersionPicker v-model="draftVersion.game_versions" :game-versions="gameVersions" />
|
||||
<div v-if="draftVersion.game_versions.length" class="space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div v-if="!editingVersion" class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast"> Uploaded files </span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button @click="editFiles">
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<ViewOnlyFileRow
|
||||
v-if="primaryFile"
|
||||
:key="primaryFile.name"
|
||||
:name="primaryFile.name"
|
||||
:is-primary="true"
|
||||
/>
|
||||
<ViewOnlyFileRow
|
||||
v-for="file in supplementaryNewFiles"
|
||||
:key="file.file.name"
|
||||
:name="file.file.name"
|
||||
:file-type="file.fileType"
|
||||
/>
|
||||
<ViewOnlyFileRow
|
||||
v-for="file in supplementaryExistingFiles"
|
||||
:key="file.filename"
|
||||
:name="file.filename"
|
||||
:file-type="file.file_type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast">
|
||||
{{ usingDetectedLoaders ? 'Detected loaders' : 'Loaders' }}
|
||||
</span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button
|
||||
v-tooltip="
|
||||
isModpack
|
||||
? 'Modpack loaders cannot be edited'
|
||||
: isResourcePack
|
||||
? 'Resource pack loaders cannot be edited'
|
||||
: undefined
|
||||
"
|
||||
:disabled="isModpack || isResourcePack"
|
||||
@click="editLoaders"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-1.5 gap-y-4 rounded-xl border border-solid border-surface-5 p-3 py-4"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template
|
||||
v-for="loader in draftVersionLoaders.map((selectedLoader) =>
|
||||
loaders.find((loader) => selectedLoader === loader.name),
|
||||
)"
|
||||
>
|
||||
<TagItem
|
||||
v-if="loader"
|
||||
:key="`loader-${loader.name}`"
|
||||
class="border !border-solid border-surface-5 hover:no-underline"
|
||||
:style="`--_color: var(--color-platform-${loader.name})`"
|
||||
>
|
||||
<div v-html="loader.icon"></div>
|
||||
{{ formatCategory(loader.name) }}
|
||||
</TagItem>
|
||||
</template>
|
||||
|
||||
<span v-if="!draftVersion.loaders.length">No loaders selected.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast">
|
||||
{{ usingDetectedVersions ? 'Detected versions' : 'Versions' }}
|
||||
</span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button
|
||||
v-tooltip="isModpack ? 'Modpack versions cannot be edited' : undefined"
|
||||
:disabled="isModpack"
|
||||
@click="editVersions"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex max-h-56 flex-col gap-1.5 gap-y-4 overflow-y-auto rounded-xl border border-solid border-surface-5 p-3 py-4"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<TagItem
|
||||
v-for="version in draftVersion.game_versions"
|
||||
:key="version"
|
||||
class="border !border-solid border-surface-5 hover:no-underline"
|
||||
>
|
||||
{{ version }}
|
||||
</TagItem>
|
||||
|
||||
<span v-if="!draftVersion.game_versions.length">No versions selected.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!noEnvironmentProject">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold text-contrast"> Environment </span>
|
||||
<UnknownIcon v-tooltip="'Pre-filled from a previous similar version'" />
|
||||
</div>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button @click="editEnvironment">
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5 gap-y-4 rounded-xl bg-surface-2 p-3 py-4">
|
||||
<div v-if="draftVersion.environment" class="flex flex-col gap-1">
|
||||
<div class="font-semibold text-contrast">
|
||||
{{ environmentCopy.title }}
|
||||
</div>
|
||||
<div class="text-sm font-medium">{{ environmentCopy.description }}</div>
|
||||
</div>
|
||||
|
||||
<span v-else class="text-sm font-medium">No environment has been set.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="!noDependenciesProject">
|
||||
<div v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast"> Suggested dependencies </span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button @click="editDependencies">
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<SuggestedDependencies @on-add-suggestion="handleAddSuggestedDependency" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!visibleSuggestedDependencies.length || draftVersion.dependencies?.length"
|
||||
class="flex flex-col gap-1"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast"> Dependencies </span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button @click="editDependencies">
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div v-if="draftVersion.dependencies?.length" class="flex flex-col gap-4">
|
||||
<DependenciesList disable-remove />
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-1.5 gap-y-4 rounded-xl bg-surface-2 p-3 py-4">
|
||||
<span class="text-sm font-medium">No dependencies added.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { EditIcon, UnknownIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
ENVIRONMENTS_COPY,
|
||||
injectProjectPageContext,
|
||||
TagItem,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
import { useGeneratedState } from '~/composables/generated'
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
import DependenciesList from '../components/DependenciesList.vue'
|
||||
import SuggestedDependencies from '../components/SuggestedDependencies/SuggestedDependencies.vue'
|
||||
import ViewOnlyFileRow from '../components/ViewOnlyFileRow.vue'
|
||||
|
||||
const {
|
||||
draftVersion,
|
||||
inferredVersionData,
|
||||
projectType,
|
||||
noEnvironmentProject,
|
||||
noDependenciesProject,
|
||||
modal,
|
||||
filesToAdd,
|
||||
editingVersion,
|
||||
visibleSuggestedDependencies,
|
||||
} = injectManageVersionContext()
|
||||
|
||||
const { projectV2 } = injectProjectPageContext()
|
||||
|
||||
const generatedState = useGeneratedState()
|
||||
const loaders = computed(() => generatedState.value.loaders)
|
||||
const isModpack = computed(() => projectType.value === 'modpack')
|
||||
const isResourcePack = computed(
|
||||
() =>
|
||||
projectType.value === 'resourcepack' &&
|
||||
(projectV2.value?.project_type === 'resourcepack' ||
|
||||
projectV2.value?.project_type === 'project'),
|
||||
)
|
||||
|
||||
const draftVersionLoaders = computed(() =>
|
||||
[
|
||||
...new Set([...draftVersion.value.loaders, ...(draftVersion.value.mrpack_loaders ?? [])]),
|
||||
].filter((loader) => loader !== 'mrpack'),
|
||||
)
|
||||
|
||||
const editLoaders = () => {
|
||||
modal.value?.setStage('from-details-loaders')
|
||||
}
|
||||
const editVersions = () => {
|
||||
modal.value?.setStage('from-details-mc-versions')
|
||||
}
|
||||
const editEnvironment = () => {
|
||||
modal.value?.setStage('from-details-environment')
|
||||
}
|
||||
const editFiles = () => {
|
||||
modal.value?.setStage('from-details-files')
|
||||
}
|
||||
const editDependencies = () => {
|
||||
modal.value?.setStage('from-details-dependencies')
|
||||
}
|
||||
|
||||
const usingDetectedVersions = computed(() => {
|
||||
if (!inferredVersionData.value?.game_versions) return false
|
||||
|
||||
const versionsMatch =
|
||||
draftVersion.value.game_versions.length === inferredVersionData.value.game_versions.length &&
|
||||
draftVersion.value.game_versions.every((version) =>
|
||||
inferredVersionData.value?.game_versions?.includes(version),
|
||||
)
|
||||
|
||||
return versionsMatch
|
||||
})
|
||||
|
||||
const usingDetectedLoaders = computed(() => {
|
||||
if (!inferredVersionData.value?.loaders) return false
|
||||
|
||||
const loadersMatch =
|
||||
draftVersion.value.loaders.length === inferredVersionData.value.loaders.length &&
|
||||
draftVersion.value.loaders.every((loader) =>
|
||||
inferredVersionData.value?.loaders?.includes(loader),
|
||||
)
|
||||
|
||||
return loadersMatch
|
||||
})
|
||||
|
||||
interface PrimaryFile {
|
||||
name: string
|
||||
fileType?: string
|
||||
existing?: boolean
|
||||
}
|
||||
|
||||
const primaryFile = computed<PrimaryFile | null>(() => {
|
||||
const existingPrimaryFile = draftVersion.value.existing_files?.[0]
|
||||
if (existingPrimaryFile) {
|
||||
return {
|
||||
name: existingPrimaryFile.filename,
|
||||
fileType: existingPrimaryFile.file_type,
|
||||
existing: true,
|
||||
}
|
||||
}
|
||||
|
||||
const addedPrimaryFile = filesToAdd.value[0]
|
||||
if (addedPrimaryFile) {
|
||||
return {
|
||||
name: addedPrimaryFile.file.name,
|
||||
fileType: addedPrimaryFile.fileType,
|
||||
existing: false,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const supplementaryNewFiles = computed(() => {
|
||||
if (primaryFile.value?.existing) {
|
||||
return filesToAdd.value
|
||||
} else {
|
||||
return filesToAdd.value.slice(1)
|
||||
}
|
||||
})
|
||||
|
||||
const supplementaryExistingFiles = computed(() => {
|
||||
if (primaryFile.value?.existing) {
|
||||
return draftVersion.value.existing_files?.slice(1)
|
||||
} else {
|
||||
return draftVersion.value.existing_files
|
||||
}
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const noEnvironmentMessage = defineMessages({
|
||||
title: {
|
||||
id: 'version.environment.none.title',
|
||||
defaultMessage: 'No environment set',
|
||||
},
|
||||
description: {
|
||||
id: 'version.environment.none.description',
|
||||
defaultMessage: 'The environment for this version has not been specified.',
|
||||
},
|
||||
})
|
||||
|
||||
const unknownEnvironmentMessage = defineMessages({
|
||||
title: {
|
||||
id: 'version.environment.unknown.title',
|
||||
defaultMessage: 'Unknown environment',
|
||||
},
|
||||
description: {
|
||||
id: 'version.environment.unknown.description',
|
||||
defaultMessage: 'The environment: "{environment}" is not recognized.',
|
||||
},
|
||||
})
|
||||
|
||||
const environmentCopy = computed(() => {
|
||||
if (!draftVersion.value.environment) {
|
||||
return {
|
||||
title: formatMessage(noEnvironmentMessage.title),
|
||||
description: formatMessage(noEnvironmentMessage.description),
|
||||
}
|
||||
}
|
||||
|
||||
const envCopy = ENVIRONMENTS_COPY[draftVersion.value.environment]
|
||||
if (envCopy) {
|
||||
return {
|
||||
title: formatMessage(envCopy.title),
|
||||
description: formatMessage(envCopy.description),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: formatMessage(unknownEnvironmentMessage.title),
|
||||
description: formatMessage(unknownEnvironmentMessage.description, {
|
||||
environment: draftVersion.value.environment,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const handleAddSuggestedDependency = (dependency: Labrinth.Versions.v3.Dependency) => {
|
||||
if (!draftVersion.value.dependencies) draftVersion.value.dependencies = []
|
||||
draftVersion.value.dependencies.push({
|
||||
project_id: dependency.project_id,
|
||||
version_id: dependency.version_id,
|
||||
dependency_type: dependency.dependency_type,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,514 +0,0 @@
|
||||
import { parse as parseTOML } from '@ltd/j-toml'
|
||||
import yaml from 'js-yaml'
|
||||
import JSZip from 'jszip'
|
||||
import { satisfies } from 'semver'
|
||||
|
||||
export const inferVersionInfo = async function (rawFile, project, gameVersions) {
|
||||
function versionType(number) {
|
||||
if (number.includes('alpha')) {
|
||||
return 'alpha'
|
||||
} else if (
|
||||
number.includes('beta') ||
|
||||
number.match(/[^A-z](rc)[^A-z]/) || // includes `rc`
|
||||
number.match(/[^A-z](pre)[^A-z]/) // includes `pre`
|
||||
) {
|
||||
return 'beta'
|
||||
} else {
|
||||
return 'release'
|
||||
}
|
||||
}
|
||||
|
||||
function getGameVersionsMatchingSemverRange(range, gameVersions) {
|
||||
if (!range) {
|
||||
return []
|
||||
}
|
||||
const ranges = Array.isArray(range) ? range : [range]
|
||||
return gameVersions.filter((version) => {
|
||||
const semverVersion = version.split('.').length === 2 ? `${version}.0` : version // add patch version if missing (e.g. 1.16 -> 1.16.0)
|
||||
return ranges.some((v) => satisfies(semverVersion, v))
|
||||
})
|
||||
}
|
||||
|
||||
function getGameVersionsMatchingMavenRange(range, gameVersions) {
|
||||
if (!range) {
|
||||
return []
|
||||
}
|
||||
const ranges = []
|
||||
|
||||
while (range.startsWith('[') || range.startsWith('(')) {
|
||||
let index = range.indexOf(')')
|
||||
const index2 = range.indexOf(']')
|
||||
if (index === -1 || (index2 !== -1 && index2 < index)) {
|
||||
index = index2
|
||||
}
|
||||
if (index === -1) break
|
||||
ranges.push(range.substring(0, index + 1))
|
||||
range = range.substring(index + 1).trim()
|
||||
if (range.startsWith(',')) {
|
||||
range = range.substring(1).trim()
|
||||
}
|
||||
}
|
||||
|
||||
if (range) {
|
||||
ranges.push(range)
|
||||
}
|
||||
|
||||
const LESS_THAN_EQUAL = /^\(,(.*)]$/
|
||||
const LESS_THAN = /^\(,(.*)\)$/
|
||||
const EQUAL = /^\[(.*)]$/
|
||||
const GREATER_THAN_EQUAL = /^\[(.*),\)$/
|
||||
const GREATER_THAN = /^\((.*),\)$/
|
||||
const BETWEEN = /^\((.*),(.*)\)$/
|
||||
const BETWEEN_EQUAL = /^\[(.*),(.*)]$/
|
||||
const BETWEEN_LESS_THAN_EQUAL = /^\((.*),(.*)]$/
|
||||
const BETWEEN_GREATER_THAN_EQUAL = /^\[(.*),(.*)\)$/
|
||||
|
||||
const semverRanges = []
|
||||
|
||||
for (const range of ranges) {
|
||||
let result
|
||||
if ((result = range.match(LESS_THAN_EQUAL))) {
|
||||
semverRanges.push(`<=${result[1]}`)
|
||||
} else if ((result = range.match(LESS_THAN))) {
|
||||
semverRanges.push(`<${result[1]}`)
|
||||
} else if ((result = range.match(EQUAL))) {
|
||||
semverRanges.push(`${result[1]}`)
|
||||
} else if ((result = range.match(GREATER_THAN_EQUAL))) {
|
||||
semverRanges.push(`>=${result[1]}`)
|
||||
} else if ((result = range.match(GREATER_THAN))) {
|
||||
semverRanges.push(`>${result[1]}`)
|
||||
} else if ((result = range.match(BETWEEN))) {
|
||||
semverRanges.push(`>${result[1]} <${result[2]}`)
|
||||
} else if ((result = range.match(BETWEEN_EQUAL))) {
|
||||
semverRanges.push(`>=${result[1]} <=${result[2]}`)
|
||||
} else if ((result = range.match(BETWEEN_LESS_THAN_EQUAL))) {
|
||||
semverRanges.push(`>${result[1]} <=${result[2]}`)
|
||||
} else if ((result = range.match(BETWEEN_GREATER_THAN_EQUAL))) {
|
||||
semverRanges.push(`>=${result[1]} <${result[2]}`)
|
||||
}
|
||||
}
|
||||
return getGameVersionsMatchingSemverRange(semverRanges, gameVersions)
|
||||
}
|
||||
|
||||
const simplifiedGameVersions = gameVersions
|
||||
.filter((it) => it.version_type === 'release')
|
||||
.map((it) => it.version)
|
||||
|
||||
const inferFunctions = {
|
||||
// NeoForge
|
||||
'META-INF/neoforge.mods.toml': (file) => {
|
||||
const metadata = parseTOML(file, { joiner: '\n' })
|
||||
if (!metadata.mods || metadata.mods.length === 0) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const neoForgeDependency = Object.values(metadata.dependencies)
|
||||
.flat()
|
||||
.find((dependency) => dependency.modId === 'neoforge')
|
||||
if (!neoForgeDependency) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// https://docs.neoforged.net/docs/gettingstarted/versioning/#neoforge
|
||||
const mcVersionRange = neoForgeDependency.versionRange
|
||||
.replace('-beta', '')
|
||||
.replace(/(\d+)(?:\.(\d+))?(?:\.(\d+)?)?/g, (_match, major, minor) => {
|
||||
return `1.${major}${minor ? '.' + minor : ''}`
|
||||
})
|
||||
const gameVersions = getGameVersionsMatchingMavenRange(mcVersionRange, simplifiedGameVersions)
|
||||
|
||||
const versionNum = metadata.mods[0].version
|
||||
return {
|
||||
name: `${project.title} ${versionNum}`,
|
||||
version_number: versionNum,
|
||||
loaders: ['neoforge'],
|
||||
version_type: versionType(versionNum),
|
||||
game_versions: gameVersions,
|
||||
}
|
||||
},
|
||||
// Forge 1.13+
|
||||
'META-INF/mods.toml': async (file, zip) => {
|
||||
const metadata = parseTOML(file, { joiner: '\n' })
|
||||
|
||||
if (metadata.mods && metadata.mods.length > 0) {
|
||||
let versionNum = metadata.mods[0].version
|
||||
|
||||
// ${file.jarVersion} -> Implementation-Version from manifest
|
||||
const manifestFile = zip.file('META-INF/MANIFEST.MF')
|
||||
if (metadata.mods[0].version.includes('${file.jarVersion}') && manifestFile !== null) {
|
||||
const manifestText = await manifestFile.async('text')
|
||||
const regex = /Implementation-Version: (.*)$/m
|
||||
const match = manifestText.match(regex)
|
||||
if (match) {
|
||||
versionNum = versionNum.replace('${file.jarVersion}', match[1])
|
||||
}
|
||||
}
|
||||
|
||||
let gameVersions = []
|
||||
const mcDependencies = Object.values(metadata.dependencies)
|
||||
.flat()
|
||||
.filter((dependency) => dependency.modId === 'minecraft')
|
||||
|
||||
if (mcDependencies.length > 0) {
|
||||
gameVersions = getGameVersionsMatchingMavenRange(
|
||||
mcDependencies[0].versionRange,
|
||||
simplifiedGameVersions,
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
name: `${project.title} ${versionNum}`,
|
||||
version_number: versionNum,
|
||||
version_type: versionType(versionNum),
|
||||
loaders: ['forge'],
|
||||
game_versions: gameVersions,
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// Old Forge
|
||||
'mcmod.info': (file) => {
|
||||
const metadata = JSON.parse(file)
|
||||
|
||||
return {
|
||||
name: metadata.version ? `${project.title} ${metadata.version}` : '',
|
||||
version_number: metadata.version,
|
||||
version_type: versionType(metadata.version),
|
||||
loaders: ['forge'],
|
||||
game_versions: simplifiedGameVersions.filter((version) =>
|
||||
version.startsWith(metadata.mcversion),
|
||||
),
|
||||
}
|
||||
},
|
||||
// Fabric
|
||||
'fabric.mod.json': (file) => {
|
||||
const metadata = JSON.parse(file)
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.version}`,
|
||||
version_number: metadata.version,
|
||||
loaders: ['fabric'],
|
||||
version_type: versionType(metadata.version),
|
||||
game_versions: metadata.depends
|
||||
? getGameVersionsMatchingSemverRange(metadata.depends.minecraft, simplifiedGameVersions)
|
||||
: [],
|
||||
}
|
||||
},
|
||||
// Quilt
|
||||
'quilt.mod.json': (file) => {
|
||||
const metadata = JSON.parse(file)
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.quilt_loader.version}`,
|
||||
version_number: metadata.quilt_loader.version,
|
||||
loaders: ['quilt'],
|
||||
version_type: versionType(metadata.quilt_loader.version),
|
||||
game_versions: metadata.quilt_loader.depends
|
||||
? getGameVersionsMatchingSemverRange(
|
||||
metadata.quilt_loader.depends.find((x) => x.id === 'minecraft')
|
||||
? metadata.quilt_loader.depends.find((x) => x.id === 'minecraft').versions
|
||||
: [],
|
||||
simplifiedGameVersions,
|
||||
)
|
||||
: [],
|
||||
}
|
||||
},
|
||||
// Bukkit + Other Forks
|
||||
'plugin.yml': (file) => {
|
||||
const metadata = yaml.load(file)
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.version}`,
|
||||
version_number: metadata.version,
|
||||
version_type: versionType(metadata.version),
|
||||
// We don't know which fork of Bukkit users are using
|
||||
loaders: [],
|
||||
game_versions: gameVersions
|
||||
.filter(
|
||||
(x) => x.version.startsWith(metadata['api-version']) && x.version_type === 'release',
|
||||
)
|
||||
.map((x) => x.version),
|
||||
}
|
||||
},
|
||||
// Paper 1.19.3+
|
||||
'paper-plugin.yml': (file) => {
|
||||
const metadata = yaml.load(file)
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.version}`,
|
||||
version_number: metadata.version,
|
||||
version_type: versionType(metadata.version),
|
||||
loaders: ['paper'],
|
||||
game_versions: gameVersions
|
||||
.filter(
|
||||
(x) => x.version.startsWith(metadata['api-version']) && x.version_type === 'release',
|
||||
)
|
||||
.map((x) => x.version),
|
||||
}
|
||||
},
|
||||
// Bungeecord + Waterfall
|
||||
'bungee.yml': (file) => {
|
||||
const metadata = yaml.load(file)
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.version}`,
|
||||
version_number: metadata.version,
|
||||
version_type: versionType(metadata.version),
|
||||
loaders: ['bungeecord'],
|
||||
}
|
||||
},
|
||||
// Velocity
|
||||
'velocity-plugin.json': (file) => {
|
||||
const metadata = JSON.parse(file)
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.version}`,
|
||||
version_number: metadata.version,
|
||||
version_type: versionType(metadata.version),
|
||||
loaders: ['velocity'],
|
||||
}
|
||||
},
|
||||
// Modpacks
|
||||
'modrinth.index.json': (file) => {
|
||||
const metadata = JSON.parse(file)
|
||||
|
||||
const loaders = []
|
||||
if ('forge' in metadata.dependencies) {
|
||||
loaders.push('forge')
|
||||
}
|
||||
if ('neoforge' in metadata.dependencies) {
|
||||
loaders.push('neoforge')
|
||||
}
|
||||
if ('fabric-loader' in metadata.dependencies) {
|
||||
loaders.push('fabric')
|
||||
}
|
||||
if ('quilt-loader' in metadata.dependencies) {
|
||||
loaders.push('quilt')
|
||||
}
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.versionId}`,
|
||||
version_number: metadata.versionId,
|
||||
version_type: versionType(metadata.versionId),
|
||||
loaders,
|
||||
game_versions: gameVersions
|
||||
.filter((x) => x.version === metadata.dependencies.minecraft)
|
||||
.map((x) => x.version),
|
||||
}
|
||||
},
|
||||
// Resource Packs + Data Packs
|
||||
'pack.mcmeta': (file) => {
|
||||
const metadata = JSON.parse(file)
|
||||
|
||||
function getRange(versionA, versionB) {
|
||||
const startingIndex = gameVersions.findIndex((x) => x.version === versionA)
|
||||
const endingIndex = gameVersions.findIndex((x) => x.version === versionB)
|
||||
|
||||
const final = []
|
||||
const filterOnlyRelease = gameVersions[startingIndex].version_type === 'release'
|
||||
|
||||
for (let i = startingIndex; i >= endingIndex; i--) {
|
||||
if (gameVersions[i].version_type === 'release' || !filterOnlyRelease) {
|
||||
final.push(gameVersions[i].version)
|
||||
}
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
const loaders = []
|
||||
let newGameVersions = []
|
||||
|
||||
if (project.actualProjectType === 'mod') {
|
||||
loaders.push('datapack')
|
||||
|
||||
switch (metadata.pack.pack_format) {
|
||||
case 4:
|
||||
newGameVersions = getRange('1.13', '1.14.4')
|
||||
break
|
||||
case 5:
|
||||
newGameVersions = getRange('1.15', '1.16.1')
|
||||
break
|
||||
case 6:
|
||||
newGameVersions = getRange('1.16.2', '1.16.5')
|
||||
break
|
||||
case 7:
|
||||
newGameVersions = getRange('1.17', '1.17.1')
|
||||
break
|
||||
case 8:
|
||||
newGameVersions = getRange('1.18', '1.18.1')
|
||||
break
|
||||
case 9:
|
||||
newGameVersions.push('1.18.2')
|
||||
break
|
||||
case 10:
|
||||
newGameVersions = getRange('1.19', '1.19.3')
|
||||
break
|
||||
case 11:
|
||||
newGameVersions = getRange('23w03a', '23w05a')
|
||||
break
|
||||
case 12:
|
||||
newGameVersions.push('1.19.4')
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if (project.actualProjectType === 'resourcepack') {
|
||||
loaders.push('minecraft')
|
||||
|
||||
switch (metadata.pack.pack_format) {
|
||||
case 1:
|
||||
newGameVersions = getRange('1.6.1', '1.8.9')
|
||||
break
|
||||
case 2:
|
||||
newGameVersions = getRange('1.9', '1.10.2')
|
||||
break
|
||||
case 3:
|
||||
newGameVersions = getRange('1.11', '1.12.2')
|
||||
break
|
||||
case 4:
|
||||
newGameVersions = getRange('1.13', '1.14.4')
|
||||
break
|
||||
case 5:
|
||||
newGameVersions = getRange('1.15', '1.16.1')
|
||||
break
|
||||
case 6:
|
||||
newGameVersions = getRange('1.16.2', '1.16.5')
|
||||
break
|
||||
case 7:
|
||||
newGameVersions = getRange('1.17', '1.17.1')
|
||||
break
|
||||
case 8:
|
||||
newGameVersions = getRange('1.18', '1.18.2')
|
||||
break
|
||||
case 9:
|
||||
newGameVersions = getRange('1.19', '1.19.2')
|
||||
break
|
||||
case 11:
|
||||
newGameVersions = getRange('22w42a', '22w44a')
|
||||
break
|
||||
case 12:
|
||||
newGameVersions.push('1.19.3')
|
||||
break
|
||||
case 13:
|
||||
newGameVersions.push('1.19.4')
|
||||
break
|
||||
case 14:
|
||||
newGameVersions = getRange('23w14a', '23w16a')
|
||||
break
|
||||
case 15:
|
||||
newGameVersions = getRange('1.20', '1.20.1')
|
||||
break
|
||||
case 16:
|
||||
newGameVersions.push('23w31a')
|
||||
break
|
||||
case 17:
|
||||
newGameVersions = getRange('23w32a', '1.20.2-pre1')
|
||||
break
|
||||
case 18:
|
||||
newGameVersions.push('1.20.2')
|
||||
break
|
||||
case 19:
|
||||
newGameVersions.push('23w42a')
|
||||
break
|
||||
case 20:
|
||||
newGameVersions = getRange('23w43a', '23w44a')
|
||||
break
|
||||
case 21:
|
||||
newGameVersions = getRange('23w45a', '23w46a')
|
||||
break
|
||||
case 22:
|
||||
newGameVersions = getRange('1.20.3', '1.20.4')
|
||||
break
|
||||
case 24:
|
||||
newGameVersions = getRange('24w03a', '24w04a')
|
||||
break
|
||||
case 25:
|
||||
newGameVersions = getRange('24w05a', '24w05b')
|
||||
break
|
||||
case 26:
|
||||
newGameVersions = getRange('24w06a', '24w07a')
|
||||
break
|
||||
case 28:
|
||||
newGameVersions = getRange('24w09a', '24w10a')
|
||||
break
|
||||
case 29:
|
||||
newGameVersions.push('24w11a')
|
||||
break
|
||||
case 30:
|
||||
newGameVersions.push('24w12a')
|
||||
break
|
||||
case 31:
|
||||
newGameVersions = getRange('24w13a', '1.20.5-pre3')
|
||||
break
|
||||
case 32:
|
||||
newGameVersions = getRange('1.20.5', '1.20.6')
|
||||
break
|
||||
case 33:
|
||||
newGameVersions = getRange('24w18a', '24w20a')
|
||||
break
|
||||
case 34:
|
||||
newGameVersions = getRange('1.21', '1.21.1')
|
||||
break
|
||||
case 35:
|
||||
newGameVersions.push('24w33a')
|
||||
break
|
||||
case 36:
|
||||
newGameVersions = getRange('24w34a', '24w35a')
|
||||
break
|
||||
case 37:
|
||||
newGameVersions.push('24w36a')
|
||||
break
|
||||
case 38:
|
||||
newGameVersions.push('24w37a')
|
||||
break
|
||||
case 39:
|
||||
newGameVersions = getRange('24w38a', '24w39a')
|
||||
break
|
||||
case 40:
|
||||
newGameVersions.push('24w40a')
|
||||
break
|
||||
case 41:
|
||||
newGameVersions = getRange('1.21.2-pre1', '1.21.2-pre2')
|
||||
break
|
||||
case 42:
|
||||
newGameVersions = getRange('1.21.2', '1.21.3')
|
||||
break
|
||||
case 43:
|
||||
newGameVersions.push('24w44a')
|
||||
break
|
||||
case 44:
|
||||
newGameVersions.push('24w45a')
|
||||
break
|
||||
case 45:
|
||||
newGameVersions.push('24w46a')
|
||||
break
|
||||
case 46:
|
||||
newGameVersions.push('1.21.4')
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loaders,
|
||||
game_versions: newGameVersions,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const zipReader = new JSZip()
|
||||
|
||||
const zip = await zipReader.loadAsync(rawFile)
|
||||
|
||||
for (const fileName in inferFunctions) {
|
||||
const file = zip.file(fileName)
|
||||
|
||||
if (file !== null) {
|
||||
const text = await file.async('text')
|
||||
return inferFunctions[fileName](text, zip)
|
||||
}
|
||||
}
|
||||
}
|
||||
123
apps/frontend/src/helpers/infer/constants.ts
Normal file
123
apps/frontend/src/helpers/infer/constants.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// Pack format to Minecraft version mappings
|
||||
// See: https://minecraft.wiki/w/Pack_format
|
||||
|
||||
// NOTE: This needs to be continuously updated as new versions are released.
|
||||
|
||||
// Resource pack format history (full table including development versions)
|
||||
export const RESOURCE_PACK_FORMATS = {
|
||||
1: { min: '1.6.1', max: '1.8.9' },
|
||||
2: { min: '1.9', max: '1.10.2' },
|
||||
3: { min: '1.11', max: '1.12.2' },
|
||||
4: { min: '1.13', max: '1.14.4' },
|
||||
5: { min: '1.15', max: '1.16.1' },
|
||||
6: { min: '1.16.2', max: '1.16.5' },
|
||||
7: { min: '1.17', max: '1.17.1' },
|
||||
8: { min: '1.18', max: '1.18.2' },
|
||||
9: { min: '1.19', max: '1.19.2' },
|
||||
11: { min: '22w42a', max: '22w44a' },
|
||||
12: { min: '1.19.3', max: '1.19.3' },
|
||||
13: { min: '1.19.4', max: '1.19.4' },
|
||||
14: { min: '23w14a', max: '23w16a' },
|
||||
15: { min: '1.20', max: '1.20.1' },
|
||||
16: { min: '23w31a', max: '23w31a' },
|
||||
17: { min: '23w32a', max: '1.20.2-pre1' },
|
||||
18: { min: '1.20.2', max: '1.20.2' },
|
||||
19: { min: '23w42a', max: '23w42a' },
|
||||
20: { min: '23w43a', max: '23w44a' },
|
||||
21: { min: '23w45a', max: '23w46a' },
|
||||
22: { min: '1.20.3', max: '1.20.4' },
|
||||
24: { min: '24w03a', max: '24w04a' },
|
||||
25: { min: '24w05a', max: '24w05b' },
|
||||
26: { min: '24w06a', max: '24w07a' },
|
||||
28: { min: '24w09a', max: '24w10a' },
|
||||
29: { min: '24w11a', max: '24w11a' },
|
||||
30: { min: '24w12a', max: '24w12a' },
|
||||
31: { min: '24w13a', max: '1.20.5-pre3' },
|
||||
32: { min: '1.20.5', max: '1.20.6' },
|
||||
33: { min: '24w18a', max: '24w20a' },
|
||||
34: { min: '1.21', max: '1.21.1' },
|
||||
35: { min: '24w33a', max: '24w33a' },
|
||||
36: { min: '24w34a', max: '24w35a' },
|
||||
37: { min: '24w36a', max: '24w36a' },
|
||||
38: { min: '24w37a', max: '24w37a' },
|
||||
39: { min: '24w38a', max: '24w39a' },
|
||||
40: { min: '24w40a', max: '24w40a' },
|
||||
41: { min: '1.21.2-pre1', max: '1.21.2-pre2' },
|
||||
42: { min: '1.21.2', max: '1.21.3' },
|
||||
43: { min: '24w44a', max: '24w44a' },
|
||||
44: { min: '24w45a', max: '24w45a' },
|
||||
45: { min: '24w46a', max: '24w46a' },
|
||||
46: { min: '1.21.4', max: '1.21.4' },
|
||||
55: { min: '1.21.5', max: '1.21.5' },
|
||||
63: { min: '1.21.6', max: '1.21.6' },
|
||||
64: { min: '1.21.7', max: '1.21.8' },
|
||||
69.0: { min: '1.21.9', max: '1.21.10' },
|
||||
75: { min: '1.21.11', max: '1.21.11' },
|
||||
} as const
|
||||
|
||||
// Data pack format history (full table including development versions)
|
||||
export const DATA_PACK_FORMATS = {
|
||||
4: { min: '1.13', max: '1.14.4' },
|
||||
5: { min: '1.15', max: '1.16.1' },
|
||||
6: { min: '1.16.2', max: '1.16.5' },
|
||||
7: { min: '1.17', max: '1.17.1' },
|
||||
8: { min: '1.18', max: '1.18.1' },
|
||||
9: { min: '1.18.2', max: '1.18.2' },
|
||||
10: { min: '1.19', max: '1.19.3' },
|
||||
11: { min: '23w03a', max: '23w05a' },
|
||||
12: { min: '1.19.4', max: '1.19.4' },
|
||||
13: { min: '23w12a', max: '23w14a' },
|
||||
14: { min: '23w16a', max: '23w17a' },
|
||||
15: { min: '1.20', max: '1.20.1' },
|
||||
16: { min: '23w31a', max: '23w31a' },
|
||||
17: { min: '23w32a', max: '1.20.2-pre1' },
|
||||
18: { min: '1.20.2', max: '1.20.2' },
|
||||
19: { min: '23w40a', max: '23w40a' },
|
||||
20: { min: '23w41a', max: '23w41a' },
|
||||
21: { min: '23w42a', max: '23w42a' },
|
||||
22: { min: '23w43a', max: '23w44a' },
|
||||
23: { min: '23w45a', max: '23w46a' },
|
||||
24: { min: '1.20.3-pre1', max: '1.20.3-pre1' },
|
||||
25: { min: '1.20.3-pre2', max: '1.20.3-pre4' },
|
||||
26: { min: '1.20.3', max: '1.20.4' },
|
||||
27: { min: '23w51a', max: '23w51b' },
|
||||
28: { min: '24w03a', max: '24w04a' },
|
||||
29: { min: '24w05a', max: '24w05b' },
|
||||
30: { min: '24w06a', max: '24w06a' },
|
||||
31: { min: '24w07a', max: '24w07a' },
|
||||
32: { min: '24w09a', max: '24w10a' },
|
||||
33: { min: '24w11a', max: '24w11a' },
|
||||
34: { min: '24w12a', max: '24w12a' },
|
||||
35: { min: '24w13a', max: '24w13a' },
|
||||
36: { min: '24w14a', max: '24w14a' },
|
||||
37: { min: '1.20.5-pre1', max: '1.20.5-pre1' },
|
||||
38: { min: '1.20.5-pre2', max: '1.20.5-pre3' },
|
||||
39: { min: '1.20.5-pre4', max: '1.20.5-rc3' },
|
||||
40: { min: '1.20.5-rc4', max: '1.20.5-rc4' },
|
||||
41: { min: '1.20.5', max: '1.20.6' },
|
||||
42: { min: '24w18a', max: '24w19b' },
|
||||
43: { min: '24w20a', max: '24w20a' },
|
||||
44: { min: '24w21a', max: '24w21b' },
|
||||
45: { min: '1.21-pre1', max: '1.21-pre1' },
|
||||
46: { min: '1.21-pre2', max: '1.21-pre4' },
|
||||
47: { min: '1.21-rc1', max: '1.21-rc1' },
|
||||
48: { min: '1.21', max: '1.21.1' },
|
||||
49: { min: '24w33a', max: '24w33a' },
|
||||
50: { min: '24w34a', max: '24w35a' },
|
||||
51: { min: '24w36a', max: '24w36a' },
|
||||
52: { min: '24w37a', max: '24w37a' },
|
||||
53: { min: '24w38a', max: '24w38a' },
|
||||
54: { min: '24w39a', max: '24w39a' },
|
||||
55: { min: '24w40a', max: '24w40a' },
|
||||
56: { min: '1.21.2-pre1', max: '1.21.2-pre2' },
|
||||
57: { min: '1.21.2', max: '1.21.3' },
|
||||
58: { min: '24w44a', max: '24w44a' },
|
||||
59: { min: '24w45a', max: '24w45a' },
|
||||
60: { min: '24w46a', max: '24w46a' },
|
||||
61: { min: '1.21.4', max: '1.21.4' },
|
||||
71: { min: '1.21.5', max: '1.21.5' },
|
||||
80: { min: '1.21.6', max: '1.21.6' },
|
||||
81: { min: '1.21.7', max: '1.21.8' },
|
||||
88.0: { min: '1.21.9', max: '1.21.10' },
|
||||
94.1: { min: '1.21.11', max: '1.21.11' },
|
||||
} as const
|
||||
3
apps/frontend/src/helpers/infer/index.ts
Normal file
3
apps/frontend/src/helpers/infer/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type { InferredVersionInfo } from './infer'
|
||||
export { inferVersionInfo } from './infer'
|
||||
export { extractVersionDetailsFromFilename } from './version-utils'
|
||||
132
apps/frontend/src/helpers/infer/infer.ts
Normal file
132
apps/frontend/src/helpers/infer/infer.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import JSZip from 'jszip'
|
||||
|
||||
import { createLoaderParsers } from './loader-parsers'
|
||||
import { createMultiFileDetectors } from './multi-file-detectors'
|
||||
import { createPackParser } from './pack-parsers'
|
||||
import { extractVersionDetailsFromFilename } from './version-utils'
|
||||
|
||||
export type GameVersion = { version: string; version_type: string }
|
||||
|
||||
export type Project = { title: string; actualProjectType?: string }
|
||||
|
||||
export type RawFile = File | (Blob & { name: string })
|
||||
|
||||
export interface InferredVersionInfo {
|
||||
name?: string
|
||||
version_number?: string
|
||||
version_type?: 'alpha' | 'beta' | 'release'
|
||||
loaders?: string[]
|
||||
game_versions?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills in missing version information from the filename if not already present.
|
||||
*/
|
||||
function fillMissingFromFilename(
|
||||
result: InferredVersionInfo,
|
||||
filename: string,
|
||||
projectTitle: string,
|
||||
): InferredVersionInfo {
|
||||
const filenameDetails = extractVersionDetailsFromFilename(filename)
|
||||
|
||||
if (!result.version_number && filenameDetails.versionNumber) {
|
||||
result.version_number = filenameDetails.versionNumber
|
||||
}
|
||||
|
||||
if (!result.version_type) {
|
||||
result.version_type = filenameDetails.versionType
|
||||
}
|
||||
|
||||
if (!result.name && result.version_number) {
|
||||
result.name = `${projectTitle} ${result.version_number}`
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to infer version information from a file.
|
||||
* Analyzes mod loaders, packs, and other Minecraft-related file formats.
|
||||
*/
|
||||
export const inferVersionInfo = async function (
|
||||
rawFile: RawFile,
|
||||
project: Project,
|
||||
gameVersions: GameVersion[],
|
||||
): Promise<InferredVersionInfo> {
|
||||
const simplifiedGameVersions = gameVersions
|
||||
.filter((it) => it.version_type === 'release')
|
||||
.map((it) => it.version)
|
||||
|
||||
const zipReader = new JSZip()
|
||||
const zip = await zipReader.loadAsync(rawFile)
|
||||
|
||||
const loaderParsers = createLoaderParsers(project, gameVersions, simplifiedGameVersions)
|
||||
const packParser = createPackParser(project, gameVersions, rawFile)
|
||||
const multiFileDetectors = createMultiFileDetectors(project, gameVersions, rawFile)
|
||||
|
||||
const inferFunctions = {
|
||||
...loaderParsers,
|
||||
'pack.mcmeta': packParser,
|
||||
}
|
||||
|
||||
// Multi-loader detection
|
||||
const multiLoaderFiles = [
|
||||
'META-INF/neoforge.mods.toml',
|
||||
'META-INF/mods.toml',
|
||||
'fabric.mod.json',
|
||||
'quilt.mod.json',
|
||||
]
|
||||
const detectedLoaderFiles = multiLoaderFiles.filter((fileName) => zip.file(fileName) !== null)
|
||||
if (detectedLoaderFiles.length > 1) {
|
||||
const results: InferredVersionInfo[] = []
|
||||
for (const fileName of detectedLoaderFiles) {
|
||||
const file = zip.file(fileName)
|
||||
if (file !== null) {
|
||||
const text = await file.async('text')
|
||||
const parser = inferFunctions[fileName as keyof typeof inferFunctions]
|
||||
if (parser) {
|
||||
const result = await parser(text, zip)
|
||||
if (result && Object.keys(result).length > 0) results.push(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (results.length > 0) {
|
||||
const combinedLoaders = [...new Set(results.flatMap((r) => r.loaders || []))]
|
||||
const allGameVersions = [...new Set(results.flatMap((r) => r.game_versions || []))]
|
||||
const primaryResult = results.find((r) => r.version_number) || results[0]
|
||||
|
||||
const mergedResult = {
|
||||
name: primaryResult.name,
|
||||
version_number: primaryResult.version_number,
|
||||
version_type: primaryResult.version_type,
|
||||
loaders: combinedLoaders,
|
||||
game_versions: allGameVersions,
|
||||
}
|
||||
return fillMissingFromFilename(mergedResult, rawFile.name, project.title)
|
||||
}
|
||||
}
|
||||
|
||||
// Standard single-loader detection
|
||||
for (const fileName in inferFunctions) {
|
||||
const file = zip.file(fileName)
|
||||
|
||||
if (file !== null) {
|
||||
const text = await file.async('text')
|
||||
const parser = inferFunctions[fileName as keyof typeof inferFunctions]
|
||||
if (parser) {
|
||||
const result = await parser(text, zip)
|
||||
return fillMissingFromFilename(result, rawFile.name, project.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-file detection functions
|
||||
for (const detector of Object.values(multiFileDetectors)) {
|
||||
const result = await detector(zip)
|
||||
if (result !== null) {
|
||||
return fillMissingFromFilename(result, rawFile.name, project.title)
|
||||
}
|
||||
}
|
||||
|
||||
return fillMissingFromFilename({}, rawFile.name, project.title)
|
||||
}
|
||||
268
apps/frontend/src/helpers/infer/loader-parsers.ts
Normal file
268
apps/frontend/src/helpers/infer/loader-parsers.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { parse as parseTOML } from '@ltd/j-toml'
|
||||
import yaml from 'js-yaml'
|
||||
import type JSZip from 'jszip'
|
||||
|
||||
import type { GameVersion, InferredVersionInfo, Project } from './infer'
|
||||
import {
|
||||
getGameVersionsMatchingMavenRange,
|
||||
getGameVersionsMatchingSemverRange,
|
||||
} from './version-ranges'
|
||||
import { versionType } from './version-utils'
|
||||
|
||||
/**
|
||||
* Creates the inferFunctions object containing all mod loader parsers.
|
||||
*/
|
||||
export function createLoaderParsers(
|
||||
project: Project,
|
||||
gameVersions: GameVersion[],
|
||||
simplifiedGameVersions: string[],
|
||||
) {
|
||||
return {
|
||||
// NeoForge
|
||||
'META-INF/neoforge.mods.toml': (file: string): InferredVersionInfo => {
|
||||
const metadata = parseTOML(file, { joiner: '\n' }) as any
|
||||
|
||||
const versionNum = metadata.mods?.[0]?.version || ''
|
||||
let newGameVersions: string[] = []
|
||||
|
||||
if (metadata.dependencies) {
|
||||
const neoForgeDependency = Object.values(metadata.dependencies)
|
||||
.flat()
|
||||
.find((dependency: any) => dependency.modId === 'neoforge')
|
||||
|
||||
if (neoForgeDependency) {
|
||||
try {
|
||||
// https://docs.neoforged.net/docs/gettingstarted/versioning/#neoforge
|
||||
const mcVersionRange = (neoForgeDependency as any).versionRange
|
||||
.replace('-beta', '')
|
||||
.replace(
|
||||
/(\d+)(?:\.(\d+))?(?:\.(\d+)?)?/g,
|
||||
(_match: string, major: string, minor: string) => {
|
||||
return `1.${major}${minor ? '.' + minor : ''}`
|
||||
},
|
||||
)
|
||||
newGameVersions = getGameVersionsMatchingMavenRange(
|
||||
mcVersionRange,
|
||||
simplifiedGameVersions,
|
||||
)
|
||||
} catch {
|
||||
// Ignore parsing errors, just leave game_versions empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: versionNum ? `${project.title} ${versionNum}` : '',
|
||||
version_number: versionNum,
|
||||
loaders: ['neoforge'],
|
||||
version_type: versionType(versionNum),
|
||||
game_versions: newGameVersions,
|
||||
}
|
||||
},
|
||||
// Forge 1.13+
|
||||
'META-INF/mods.toml': async (file: string, zip: JSZip): Promise<InferredVersionInfo> => {
|
||||
const metadata = parseTOML(file, { joiner: '\n' }) as any
|
||||
|
||||
if (metadata.mods && metadata.mods.length > 0) {
|
||||
let versionNum = metadata.mods[0].version
|
||||
|
||||
// ${file.jarVersion} -> Implementation-Version from manifest
|
||||
const manifestFile = zip.file('META-INF/MANIFEST.MF')
|
||||
if (metadata.mods[0].version.includes('${file.jarVersion}') && manifestFile !== null) {
|
||||
const manifestText = await manifestFile.async('text')
|
||||
const regex = /Implementation-Version: (.*)$/m
|
||||
const match = manifestText.match(regex)
|
||||
if (match) {
|
||||
versionNum = versionNum.replace('${file.jarVersion}', match[1])
|
||||
}
|
||||
}
|
||||
|
||||
let newGameVersions: string[] = []
|
||||
const mcDependencies = Object.values(metadata.dependencies)
|
||||
.flat()
|
||||
.filter((dependency: any) => dependency.modId === 'minecraft')
|
||||
|
||||
if (mcDependencies.length > 0) {
|
||||
newGameVersions = getGameVersionsMatchingMavenRange(
|
||||
(mcDependencies[0] as any).versionRange,
|
||||
simplifiedGameVersions,
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
name: `${project.title} ${versionNum}`,
|
||||
version_number: versionNum,
|
||||
version_type: versionType(versionNum),
|
||||
loaders: ['forge'],
|
||||
game_versions: newGameVersions,
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// Old Forge
|
||||
'mcmod.info': (file: string): InferredVersionInfo => {
|
||||
const metadata = JSON.parse(file) as any
|
||||
|
||||
return {
|
||||
name: metadata.version ? `${project.title} ${metadata.version}` : '',
|
||||
version_number: metadata.version,
|
||||
version_type: versionType(metadata.version),
|
||||
loaders: ['forge'],
|
||||
game_versions: simplifiedGameVersions.filter((version) =>
|
||||
version.startsWith(metadata.mcversion),
|
||||
),
|
||||
}
|
||||
},
|
||||
// Fabric
|
||||
'fabric.mod.json': (file: string): InferredVersionInfo => {
|
||||
const metadata = JSON.parse(file) as any
|
||||
|
||||
const detectedGameVersions = metadata.depends
|
||||
? getGameVersionsMatchingSemverRange(metadata.depends.minecraft, simplifiedGameVersions)
|
||||
: []
|
||||
const loaders: string[] = []
|
||||
|
||||
// Detect 1.3-1.13 -> legacy-fabric
|
||||
const hasLegacyVersions = detectedGameVersions.some((version) => {
|
||||
const match = version.match(/^1\.(\d+)/)
|
||||
return match && parseInt(match[1]) >= 3 && parseInt(match[1]) <= 13
|
||||
})
|
||||
|
||||
if (hasLegacyVersions) loaders.push('legacy-fabric')
|
||||
else loaders.push('fabric')
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.version}`,
|
||||
version_number: metadata.version,
|
||||
loaders,
|
||||
version_type: versionType(metadata.version),
|
||||
game_versions: detectedGameVersions,
|
||||
}
|
||||
},
|
||||
// Quilt
|
||||
'quilt.mod.json': (file: string): InferredVersionInfo => {
|
||||
const metadata = JSON.parse(file) as any
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.quilt_loader.version}`,
|
||||
version_number: metadata.quilt_loader.version,
|
||||
loaders: ['quilt'],
|
||||
version_type: versionType(metadata.quilt_loader.version),
|
||||
game_versions: metadata.quilt_loader.depends
|
||||
? getGameVersionsMatchingSemverRange(
|
||||
metadata.quilt_loader.depends.find((x: any) => x.id === 'minecraft')
|
||||
? metadata.quilt_loader.depends.find((x: any) => x.id === 'minecraft').versions
|
||||
: [],
|
||||
simplifiedGameVersions,
|
||||
)
|
||||
: [],
|
||||
}
|
||||
},
|
||||
// Bukkit + Other Forks
|
||||
'plugin.yml': (file: string): InferredVersionInfo => {
|
||||
const metadata = yaml.load(file) as any
|
||||
|
||||
// Check for Folia support
|
||||
const loaders = []
|
||||
if (metadata['folia-supported'] === true) {
|
||||
loaders.push('folia')
|
||||
}
|
||||
// We don't know which fork of Bukkit users are using otherwise
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.version}`,
|
||||
version_number: metadata.version,
|
||||
version_type: versionType(metadata.version),
|
||||
loaders,
|
||||
game_versions: gameVersions
|
||||
.filter(
|
||||
(x) => x.version.startsWith(metadata['api-version']) && x.version_type === 'release',
|
||||
)
|
||||
.map((x) => x.version),
|
||||
}
|
||||
},
|
||||
// Paper 1.19.3+
|
||||
'paper-plugin.yml': (file: string): InferredVersionInfo => {
|
||||
const metadata = yaml.load(file) as any
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.version}`,
|
||||
version_number: metadata.version,
|
||||
version_type: versionType(metadata.version),
|
||||
loaders: ['paper'],
|
||||
game_versions: gameVersions
|
||||
.filter(
|
||||
(x) => x.version.startsWith(metadata['api-version']) && x.version_type === 'release',
|
||||
)
|
||||
.map((x) => x.version),
|
||||
}
|
||||
},
|
||||
// Bungeecord + Waterfall
|
||||
'bungee.yml': (file: string): InferredVersionInfo => {
|
||||
const metadata = yaml.load(file) as any
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.version}`,
|
||||
version_number: metadata.version,
|
||||
version_type: versionType(metadata.version),
|
||||
loaders: ['bungeecord'],
|
||||
}
|
||||
},
|
||||
// Velocity
|
||||
'velocity-plugin.json': (file: string): InferredVersionInfo => {
|
||||
const metadata = JSON.parse(file) as any
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.version}`,
|
||||
version_number: metadata.version,
|
||||
version_type: versionType(metadata.version),
|
||||
loaders: ['velocity'],
|
||||
}
|
||||
},
|
||||
// Sponge plugin (8+)
|
||||
'META-INF/sponge_plugins.json': (file: string): InferredVersionInfo => {
|
||||
const metadata = JSON.parse(file) as any
|
||||
const plugin = metadata.plugins?.[0]
|
||||
|
||||
if (!plugin) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
name: plugin.version ? `${project.title} ${plugin.version}` : '',
|
||||
version_number: plugin.version,
|
||||
version_type: versionType(plugin.version),
|
||||
loaders: ['sponge'],
|
||||
}
|
||||
},
|
||||
// Modpacks
|
||||
'modrinth.index.json': (file: string): InferredVersionInfo => {
|
||||
const metadata = JSON.parse(file) as any
|
||||
|
||||
const loaders = []
|
||||
if ('forge' in metadata.dependencies) {
|
||||
loaders.push('forge')
|
||||
}
|
||||
if ('neoforge' in metadata.dependencies) {
|
||||
loaders.push('neoforge')
|
||||
}
|
||||
if ('fabric-loader' in metadata.dependencies) {
|
||||
loaders.push('fabric')
|
||||
}
|
||||
if ('quilt-loader' in metadata.dependencies) {
|
||||
loaders.push('quilt')
|
||||
}
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.versionId}`,
|
||||
version_number: metadata.versionId,
|
||||
version_type: versionType(metadata.versionId),
|
||||
loaders,
|
||||
game_versions: gameVersions
|
||||
.filter((x) => x.version === metadata.dependencies.minecraft)
|
||||
.map((x) => x.version),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
131
apps/frontend/src/helpers/infer/multi-file-detectors.ts
Normal file
131
apps/frontend/src/helpers/infer/multi-file-detectors.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type JSZip from 'jszip'
|
||||
|
||||
import type { GameVersion, InferredVersionInfo, Project, RawFile } from './infer'
|
||||
import { extractVersionFromFilename, versionType } from './version-utils'
|
||||
|
||||
/**
|
||||
* Creates multi-file detection functions that scan multiple files in a zip.
|
||||
*/
|
||||
export function createMultiFileDetectors(
|
||||
project: Project,
|
||||
gameVersions: GameVersion[],
|
||||
rawFile: RawFile,
|
||||
) {
|
||||
return {
|
||||
// Legacy texture pack (pre-1.6.1)
|
||||
legacyTexturePack: async (zip: JSZip): Promise<InferredVersionInfo | null> => {
|
||||
const packTxt = zip.file('pack.txt')
|
||||
if (!packTxt) return null
|
||||
|
||||
// Check for legacy texture pack files/directories
|
||||
const legacyIndicators = [
|
||||
'font.txt',
|
||||
'particles.png',
|
||||
'achievement/',
|
||||
'armor/',
|
||||
'art/',
|
||||
'environment/',
|
||||
'font/',
|
||||
'gui/',
|
||||
'item/',
|
||||
'lang/',
|
||||
'misc/',
|
||||
'mob/',
|
||||
'textures/',
|
||||
'title/',
|
||||
]
|
||||
|
||||
const hasLegacyContent = legacyIndicators.some((indicator) => {
|
||||
if (indicator.endsWith('/')) {
|
||||
return zip.file(new RegExp(`^${indicator}`))?.length > 0
|
||||
}
|
||||
return zip.file(indicator) !== null
|
||||
})
|
||||
|
||||
if (!hasLegacyContent) return null
|
||||
|
||||
// Legacy texture packs are compatible with a1.2.2 to 1.5.2
|
||||
// We'll return versions from 1.0 to 1.5.2 (as older alpha/beta versions may not be in gameVersions)
|
||||
const legacyVersions = gameVersions
|
||||
.filter((v) => {
|
||||
const version = v.version
|
||||
// Match 1.0 through 1.5.2
|
||||
if (version.match(/^1\.[0-4](\.\d+)?$/) || version.match(/^1\.5(\.[0-2])?$/)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
.map((v) => v.version)
|
||||
|
||||
const versionNum = extractVersionFromFilename(rawFile.name)
|
||||
|
||||
return {
|
||||
name: versionNum ? `${project.title} ${versionNum}` : undefined,
|
||||
version_number: versionNum || undefined,
|
||||
version_type: versionType(versionNum),
|
||||
loaders: ['minecraft'],
|
||||
game_versions: legacyVersions,
|
||||
}
|
||||
},
|
||||
|
||||
// Shader pack (OptiFine/Iris)
|
||||
shaderPack: async (zip: JSZip): Promise<InferredVersionInfo | null> => {
|
||||
const shadersDir = zip.file(/^shaders\//)
|
||||
if (!shadersDir || shadersDir.length === 0) return null
|
||||
|
||||
const loaders: string[] = []
|
||||
|
||||
// Check for Iris-specific features in shaders.properties
|
||||
const shaderProps = zip.file('shaders/shaders.properties')
|
||||
if (shaderProps) {
|
||||
const propsText = await shaderProps.async('text')
|
||||
if (
|
||||
propsText.includes('iris.features.required') ||
|
||||
propsText.includes('iris.features.optional')
|
||||
) {
|
||||
loaders.push('iris', 'optifine')
|
||||
}
|
||||
}
|
||||
|
||||
// If no specific loader detected, it could be OptiFine or Iris
|
||||
if (loaders.length === 0) {
|
||||
loaders.push('optifine', 'iris')
|
||||
}
|
||||
|
||||
const versionNum = extractVersionFromFilename(rawFile.name)
|
||||
|
||||
return {
|
||||
name: versionNum ? `${project.title} ${versionNum}` : undefined,
|
||||
version_number: versionNum || undefined,
|
||||
version_type: versionType(versionNum),
|
||||
loaders,
|
||||
game_versions: [],
|
||||
}
|
||||
},
|
||||
|
||||
// NilLoader mod
|
||||
nilLoaderMod: async (zip: JSZip): Promise<InferredVersionInfo | null> => {
|
||||
const nilModFiles = zip.file(/\.nilmod\.css$/)
|
||||
if (!nilModFiles || nilModFiles.length === 0) return null
|
||||
|
||||
return {
|
||||
loaders: ['nilloader'],
|
||||
game_versions: [],
|
||||
}
|
||||
},
|
||||
|
||||
// Java Agent
|
||||
javaAgent: async (zip: JSZip): Promise<InferredVersionInfo | null> => {
|
||||
const manifest = zip.file('META-INF/MANIFEST.MF')
|
||||
if (!manifest) return null
|
||||
|
||||
const manifestText = await manifest.async('text')
|
||||
if (!manifestText.includes('Premain-Class:')) return null
|
||||
|
||||
return {
|
||||
loaders: ['java-agent'],
|
||||
game_versions: [],
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
266
apps/frontend/src/helpers/infer/pack-parsers.ts
Normal file
266
apps/frontend/src/helpers/infer/pack-parsers.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import type JSZip from 'jszip'
|
||||
|
||||
import { DATA_PACK_FORMATS, RESOURCE_PACK_FORMATS } from './constants'
|
||||
import type { GameVersion, InferredVersionInfo, Project, RawFile } from './infer'
|
||||
import { extractVersionFromFilename, versionType } from './version-utils'
|
||||
|
||||
type PackFormat = number | [number] | [number, number]
|
||||
|
||||
/**
|
||||
* Normalizes a pack format to [major, minor] tuple. See https://minecraft.wiki/w/Pack.mcmeta
|
||||
* - Single integer: [major, 0] for min, [major, Infinity] for max
|
||||
* - Array [major]: [major, 0] for min, [major, Infinity] for max
|
||||
* - Array [major, minor]: returns as-is
|
||||
*/
|
||||
function normalizePackFormat(format: PackFormat, isMax: boolean): [number, number] {
|
||||
if (Array.isArray(format)) {
|
||||
if (format.length === 1) {
|
||||
return isMax ? [format[0], Infinity] : [format[0], 0]
|
||||
}
|
||||
return [format[0], format[1]]
|
||||
}
|
||||
return isMax ? [format, Infinity] : [format, 0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two pack formats [major, minor].
|
||||
* Returns: -1 if a < b, 0 if equal, 1 if a > b
|
||||
*/
|
||||
function comparePackFormats(a: [number, number], b: [number, number]): number {
|
||||
if (a[0] !== b[0]) return a[0] - b[0]
|
||||
return a[1] - b[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a format number falls within the min/max range.
|
||||
*/
|
||||
function isFormatInRange(
|
||||
format: number,
|
||||
minFormat: [number, number],
|
||||
maxFormat: [number, number],
|
||||
): boolean {
|
||||
// Check if the major version matches
|
||||
if (format < minFormat[0] || format > maxFormat[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If major version is exactly min or max, we need to check minor version
|
||||
// For entries in our map, we treat them as [major, 0]
|
||||
const formatTuple: [number, number] = [format, 0]
|
||||
|
||||
// If the format has a decimal (like 69.0, 88.0), extract it
|
||||
const formatStr = format.toString()
|
||||
if (formatStr.includes('.')) {
|
||||
const [maj, min] = formatStr.split('.').map(Number)
|
||||
formatTuple[0] = maj
|
||||
formatTuple[1] = min
|
||||
}
|
||||
|
||||
return (
|
||||
comparePackFormats(formatTuple, minFormat) >= 0 &&
|
||||
comparePackFormats(formatTuple, maxFormat) <= 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get a range of game versions between two versions.
|
||||
*/
|
||||
function getRange(versionA: string, versionB: string, gameVersions: GameVersion[]): string[] {
|
||||
const startingIndex = gameVersions.findIndex((x) => x.version === versionA)
|
||||
const endingIndex = gameVersions.findIndex((x) => x.version === versionB)
|
||||
|
||||
if (startingIndex === -1 || endingIndex === -1) {
|
||||
return []
|
||||
}
|
||||
|
||||
const final = []
|
||||
const filterOnlyRelease = gameVersions[startingIndex]?.version_type === 'release'
|
||||
|
||||
for (let i = startingIndex; i >= endingIndex; i--) {
|
||||
if (gameVersions[i].version_type === 'release' || !filterOnlyRelease) {
|
||||
final.push(gameVersions[i].version)
|
||||
}
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets game versions from a single pack format number.
|
||||
*/
|
||||
function getVersionsFromPackFormat(
|
||||
packFormat: number,
|
||||
formatMap: Record<number, { min: string; max: string }>,
|
||||
gameVersions: GameVersion[],
|
||||
): string[] {
|
||||
const mapping = formatMap[packFormat]
|
||||
if (!mapping) {
|
||||
return []
|
||||
}
|
||||
return getRange(mapping.min, mapping.max, gameVersions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets game versions from a pack format range (min to max inclusive).
|
||||
* Supports both integer and [major, minor] format specifications.
|
||||
*/
|
||||
function getVersionsFromFormatRange(
|
||||
minFormat: PackFormat,
|
||||
maxFormat: PackFormat,
|
||||
formatMap: Record<number, { min: string; max: string }>,
|
||||
gameVersions: GameVersion[],
|
||||
): string[] {
|
||||
const normalizedMin = normalizePackFormat(minFormat, false)
|
||||
const normalizedMax = normalizePackFormat(maxFormat, true)
|
||||
|
||||
// Get all format numbers from the map that fall within the range
|
||||
const allVersions: string[] = []
|
||||
const formatNumbers = Object.keys(formatMap)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
|
||||
for (const format of formatNumbers) {
|
||||
if (isFormatInRange(format, normalizedMin, normalizedMax)) {
|
||||
const versions = getVersionsFromPackFormat(format, formatMap, gameVersions)
|
||||
for (const version of versions) {
|
||||
if (!allVersions.includes(version)) {
|
||||
allVersions.push(version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allVersions
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets game versions from pack.mcmeta metadata.
|
||||
* Supports multiple format specifications:
|
||||
* - min_format + max_format: Can be integers or [major, minor] arrays (since 25w31a)
|
||||
* - supported_formats: Single int, array of ints, or { min_inclusive, max_inclusive }
|
||||
* - pack_format: Single format number (legacy)
|
||||
*/
|
||||
function getGameVersionsFromPackMeta(
|
||||
packMeta: any,
|
||||
formatMap: Record<number, { min: string; max: string }>,
|
||||
gameVersions: GameVersion[],
|
||||
): string[] {
|
||||
const pack = packMeta.pack
|
||||
if (!pack) return []
|
||||
|
||||
// Check for min_format and max_format (25w31a+ format)
|
||||
// These can be: int (e.g., 82), [int] (e.g., [82]), or [major, minor] (e.g., [88, 0])
|
||||
if (pack.min_format !== undefined && pack.max_format !== undefined) {
|
||||
return getVersionsFromFormatRange(pack.min_format, pack.max_format, formatMap, gameVersions)
|
||||
}
|
||||
|
||||
// Check for supported_formats
|
||||
if (pack.supported_formats !== undefined) {
|
||||
const formats = pack.supported_formats
|
||||
|
||||
// Single integer: major version
|
||||
if (typeof formats === 'number') {
|
||||
return getVersionsFromPackFormat(formats, formatMap, gameVersions)
|
||||
}
|
||||
|
||||
// Array of integers or [min, max] range
|
||||
if (Array.isArray(formats)) {
|
||||
if (
|
||||
formats.length === 2 &&
|
||||
typeof formats[0] === 'number' &&
|
||||
typeof formats[1] === 'number'
|
||||
) {
|
||||
// Could be [major, minor] or [minMajor, maxMajor]
|
||||
// Based on context, if both are close (within ~50), treat as major version range
|
||||
// Otherwise, treat as [major, minor]
|
||||
if (Math.abs(formats[1] - formats[0]) < 50) {
|
||||
// Likely a major version range like [42, 45]
|
||||
return getVersionsFromFormatRange(formats[0], formats[1], formatMap, gameVersions)
|
||||
}
|
||||
}
|
||||
|
||||
// Array of major versions
|
||||
const allVersions: string[] = []
|
||||
for (const format of formats) {
|
||||
if (typeof format === 'number') {
|
||||
const versions = getVersionsFromPackFormat(format, formatMap, gameVersions)
|
||||
for (const version of versions) {
|
||||
if (!allVersions.includes(version)) {
|
||||
allVersions.push(version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return allVersions
|
||||
}
|
||||
|
||||
// Object format: { min_inclusive, max_inclusive }
|
||||
if (
|
||||
typeof formats === 'object' &&
|
||||
formats.min_inclusive !== undefined &&
|
||||
formats.max_inclusive !== undefined
|
||||
) {
|
||||
return getVersionsFromFormatRange(
|
||||
formats.min_inclusive,
|
||||
formats.max_inclusive,
|
||||
formatMap,
|
||||
gameVersions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to pack_format (legacy single format)
|
||||
if (pack.pack_format !== undefined) {
|
||||
return getVersionsFromPackFormat(pack.pack_format, formatMap, gameVersions)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the pack.mcmeta parser function.
|
||||
*/
|
||||
export function createPackParser(project: Project, gameVersions: GameVersion[], rawFile: RawFile) {
|
||||
return async (file: string, zip: JSZip): Promise<InferredVersionInfo> => {
|
||||
const metadata = JSON.parse(file) as any
|
||||
|
||||
// Check for assets/ directory (resource pack) or data/ directory (data pack)
|
||||
const hasAssetsDir = zip.file(/^assets\//)?.[0] !== undefined
|
||||
const hasDataDir = zip.file(/^data\//)?.[0] !== undefined
|
||||
const hasZipExtension = rawFile.name.toLowerCase().endsWith('.zip')
|
||||
|
||||
const loaders: string[] = []
|
||||
let newGameVersions: string[] = []
|
||||
|
||||
// Data pack detection: has data/ directory
|
||||
if (hasDataDir && hasZipExtension) {
|
||||
loaders.push('datapack')
|
||||
newGameVersions = getGameVersionsFromPackMeta(metadata, DATA_PACK_FORMATS, gameVersions)
|
||||
}
|
||||
// Resource pack detection: has assets/ directory
|
||||
else if (hasAssetsDir && hasZipExtension) {
|
||||
loaders.push('minecraft')
|
||||
newGameVersions = getGameVersionsFromPackMeta(metadata, RESOURCE_PACK_FORMATS, gameVersions)
|
||||
}
|
||||
|
||||
// Fallback to old behavior based on project type
|
||||
else if (project.actualProjectType === 'mod') {
|
||||
loaders.push('datapack')
|
||||
newGameVersions = getGameVersionsFromPackMeta(metadata, DATA_PACK_FORMATS, gameVersions)
|
||||
} else if (project.actualProjectType === 'resourcepack') {
|
||||
loaders.push('minecraft')
|
||||
newGameVersions = getGameVersionsFromPackMeta(metadata, RESOURCE_PACK_FORMATS, gameVersions)
|
||||
}
|
||||
|
||||
// Try to extract version from filename
|
||||
const versionNum = extractVersionFromFilename(rawFile.name)
|
||||
|
||||
return {
|
||||
name: versionNum ? `${project.title} ${versionNum}` : undefined,
|
||||
version_number: versionNum || undefined,
|
||||
version_type: versionType(versionNum),
|
||||
loaders,
|
||||
game_versions: newGameVersions,
|
||||
}
|
||||
}
|
||||
}
|
||||
87
apps/frontend/src/helpers/infer/version-ranges.ts
Normal file
87
apps/frontend/src/helpers/infer/version-ranges.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { satisfies } from 'semver'
|
||||
|
||||
/**
|
||||
* Returns game versions that match a semver range or array of ranges.
|
||||
*/
|
||||
export function getGameVersionsMatchingSemverRange(
|
||||
range: string | string[] | undefined,
|
||||
gameVersions: string[],
|
||||
): string[] {
|
||||
if (!range) {
|
||||
return []
|
||||
}
|
||||
const ranges = Array.isArray(range) ? range : [range]
|
||||
// Normalize ranges: strip trailing hyphens from version numbers used by Fabric for prerelease matching (e.g., ">=1.21.11-" -> ">=1.21.11")
|
||||
const normalizedRanges = ranges.map((r) => r.replace(/(\d)-(\s|$)/g, '$1$2'))
|
||||
return gameVersions.filter((version) => {
|
||||
const semverVersion = version.split('.').length === 2 ? `${version}.0` : version // add patch version if missing (e.g. 1.16 -> 1.16.0)
|
||||
return normalizedRanges.some((v) => satisfies(semverVersion, v))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns game versions that match a Maven-style version range.
|
||||
*/
|
||||
export function getGameVersionsMatchingMavenRange(
|
||||
range: string | undefined,
|
||||
gameVersions: string[],
|
||||
): string[] {
|
||||
if (!range) {
|
||||
return []
|
||||
}
|
||||
const ranges = []
|
||||
|
||||
while (range.startsWith('[') || range.startsWith('(')) {
|
||||
let index = range.indexOf(')')
|
||||
const index2 = range.indexOf(']')
|
||||
if (index === -1 || (index2 !== -1 && index2 < index)) {
|
||||
index = index2
|
||||
}
|
||||
if (index === -1) break
|
||||
ranges.push(range.substring(0, index + 1))
|
||||
range = range.substring(index + 1).trim()
|
||||
if (range.startsWith(',')) {
|
||||
range = range.substring(1).trim()
|
||||
}
|
||||
}
|
||||
|
||||
if (range) {
|
||||
ranges.push(range)
|
||||
}
|
||||
|
||||
const LESS_THAN_EQUAL = /^\(,(.*)]$/
|
||||
const LESS_THAN = /^\(,(.*)\)$/
|
||||
const EQUAL = /^\[(.*)]$/
|
||||
const GREATER_THAN_EQUAL = /^\[(.*),\)$/
|
||||
const GREATER_THAN = /^\((.*),\)$/
|
||||
const BETWEEN = /^\((.*),(.*)\)$/
|
||||
const BETWEEN_EQUAL = /^\[(.*),(.*)]$/
|
||||
const BETWEEN_LESS_THAN_EQUAL = /^\((.*),(.*)]$/
|
||||
const BETWEEN_GREATER_THAN_EQUAL = /^\[(.*),(.*)\)$/
|
||||
|
||||
const semverRanges = []
|
||||
|
||||
for (const range of ranges) {
|
||||
let result
|
||||
if ((result = range.match(LESS_THAN_EQUAL))) {
|
||||
semverRanges.push(`<=${result[1]}`)
|
||||
} else if ((result = range.match(LESS_THAN))) {
|
||||
semverRanges.push(`<${result[1]}`)
|
||||
} else if ((result = range.match(EQUAL))) {
|
||||
semverRanges.push(`${result[1]}`)
|
||||
} else if ((result = range.match(GREATER_THAN_EQUAL))) {
|
||||
semverRanges.push(`>=${result[1]}`)
|
||||
} else if ((result = range.match(GREATER_THAN))) {
|
||||
semverRanges.push(`>${result[1]}`)
|
||||
} else if ((result = range.match(BETWEEN))) {
|
||||
semverRanges.push(`>${result[1]} <${result[2]}`)
|
||||
} else if ((result = range.match(BETWEEN_EQUAL))) {
|
||||
semverRanges.push(`>=${result[1]} <=${result[2]}`)
|
||||
} else if ((result = range.match(BETWEEN_LESS_THAN_EQUAL))) {
|
||||
semverRanges.push(`>${result[1]} <=${result[2]}`)
|
||||
} else if ((result = range.match(BETWEEN_GREATER_THAN_EQUAL))) {
|
||||
semverRanges.push(`>=${result[1]} <${result[2]}`)
|
||||
}
|
||||
}
|
||||
return getGameVersionsMatchingSemverRange(semverRanges, gameVersions)
|
||||
}
|
||||
57
apps/frontend/src/helpers/infer/version-utils.ts
Normal file
57
apps/frontend/src/helpers/infer/version-utils.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Determines the version type based on the version string.
|
||||
*/
|
||||
export function versionType(number: string | null | undefined): 'alpha' | 'beta' | 'release' {
|
||||
if (!number) return 'release'
|
||||
if (number.includes('alpha')) {
|
||||
return 'alpha'
|
||||
} else if (
|
||||
number.includes('beta') ||
|
||||
number.match(/[^A-z](rc)[^A-z]/) || // includes `rc`
|
||||
number.match(/[^A-z](pre)[^A-z]/) // includes `pre`
|
||||
) {
|
||||
return 'beta'
|
||||
} else {
|
||||
return 'release'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts version number from a filename.
|
||||
*/
|
||||
export function extractVersionFromFilename(filename: string | null | undefined): string | null {
|
||||
if (!filename) return null
|
||||
|
||||
// Remove file extension
|
||||
let baseName = filename.replace(/\.(zip|jar)$/i, '')
|
||||
|
||||
// Remove explicit MC version markers: mc followed by version (e.g., +mc1.21.11, -mc1.21, _mc1.21.4)
|
||||
baseName = baseName.replace(/[+_-]mc\d+\.\d+(?:\.\d+)?/gi, '')
|
||||
|
||||
const versionPatterns = [
|
||||
/[_\-\s]v(\d+(?:\.\d+)*)/i, // Match version with 'v' anywhere: "Name-v1.2.3-extra" (less strict)
|
||||
/[_\-\s]r(\d+(?:\.\d+)*)/i, // Match version with 'r' anywhere: "Name-r1.2.3-extra" (less strict)
|
||||
/[_\-\s](\d+(?:\.\d+)+)$/, // Match version at end after space/separator: "Name 1.2.3"
|
||||
/(\d+\.\d+(?:\.\d+)*)/, // Match any version pattern x.x or x.x.x.x...: "Name1.2.3extra"
|
||||
]
|
||||
|
||||
for (const pattern of versionPatterns) {
|
||||
const match = baseName.match(pattern)
|
||||
if (match && match[1]) {
|
||||
return match[1]
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts version details from a filename (public API).
|
||||
*/
|
||||
export function extractVersionDetailsFromFilename(filename: string | null | undefined) {
|
||||
const versionNum = extractVersionFromFilename(filename)
|
||||
return {
|
||||
versionNumber: versionNum || undefined,
|
||||
versionType: versionType(versionNum),
|
||||
}
|
||||
}
|
||||
@@ -39,20 +39,14 @@
|
||||
class="hover:!bg-button-bg [&>svg]:!text-green"
|
||||
:dropdown-id="`${baseDropdownId}-edit-${version.id}`"
|
||||
:options="[
|
||||
{
|
||||
id: 'edit-metadata',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'metadata'),
|
||||
},
|
||||
{
|
||||
id: 'edit-details',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-details'),
|
||||
},
|
||||
{
|
||||
id: 'edit-changelog',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-changelog'),
|
||||
},
|
||||
{
|
||||
id: 'edit-dependencies',
|
||||
action: () =>
|
||||
handleOpenEditVersionModal(version.id, project.id, 'add-dependencies'),
|
||||
shown: project.project_type !== 'modpack',
|
||||
},
|
||||
{
|
||||
id: 'edit-files',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-files'),
|
||||
@@ -69,13 +63,9 @@
|
||||
<InfoIcon aria-hidden="true" />
|
||||
Edit details
|
||||
</template>
|
||||
<template #edit-dependencies>
|
||||
<template #edit-metadata>
|
||||
<BoxIcon aria-hidden="true" />
|
||||
Edit dependencies
|
||||
</template>
|
||||
<template #edit-changelog>
|
||||
<AlignLeftIcon aria-hidden="true" />
|
||||
Edit changelog
|
||||
Edit metadata
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
@@ -145,16 +135,10 @@
|
||||
shown: !!currentMember,
|
||||
},
|
||||
{
|
||||
id: 'edit-changelog',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-changelog'),
|
||||
id: 'edit-metadata',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'metadata'),
|
||||
shown: !!currentMember,
|
||||
},
|
||||
{
|
||||
id: 'edit-dependencies',
|
||||
action: () =>
|
||||
handleOpenEditVersionModal(version.id, project.id, 'add-dependencies'),
|
||||
shown: !!currentMember && project.project_type !== 'modpack',
|
||||
},
|
||||
{
|
||||
id: 'edit-files',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-files'),
|
||||
@@ -202,13 +186,9 @@
|
||||
<InfoIcon aria-hidden="true" />
|
||||
Edit details
|
||||
</template>
|
||||
<template #edit-dependencies>
|
||||
<template #edit-metadata>
|
||||
<BoxIcon aria-hidden="true" />
|
||||
Edit dependencies
|
||||
</template>
|
||||
<template #edit-changelog>
|
||||
<AlignLeftIcon aria-hidden="true" />
|
||||
Edit changelog
|
||||
Edit metadata
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
@@ -301,7 +281,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
AlignLeftIcon,
|
||||
BoxIcon,
|
||||
ClipboardCopyIcon,
|
||||
DownloadIcon,
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<!-- TODO: Remove this^after converting to composition API. -->
|
||||
<template>
|
||||
<div v-if="version" class="version-page">
|
||||
<CreateProjectVersionModal
|
||||
v-if="currentMember"
|
||||
ref="createProjectVersionModal"
|
||||
@save="handleVersionSaved"
|
||||
/>
|
||||
<ConfirmModal
|
||||
v-if="currentMember"
|
||||
ref="modal_confirm"
|
||||
@@ -140,7 +145,7 @@
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-else class="input-group">
|
||||
<div v-else class="input-group mt-2">
|
||||
<ButtonStyled v-if="primaryFile && !currentMember" color="brand">
|
||||
<a
|
||||
v-tooltip="primaryFile.filename + ' (' + formatBytes(primaryFile.size) + ')'"
|
||||
@@ -163,6 +168,24 @@
|
||||
Report
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="currentMember">
|
||||
<button @click="handleOpenEditVersionModal(version.id, project.id, 'metadata')">
|
||||
<BoxIcon aria-hidden="true" />
|
||||
Edit metadata
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="currentMember">
|
||||
<button @click="handleOpenEditVersionModal(version.id, project.id, 'add-details')">
|
||||
<InfoIcon aria-hidden="true" />
|
||||
Edit details
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="currentMember">
|
||||
<button @click="handleOpenEditVersionModal(version.id, project.id, 'add-files')">
|
||||
<FileIcon aria-hidden="true" />
|
||||
Edit files
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
v-if="
|
||||
@@ -629,6 +652,7 @@ import {
|
||||
EditIcon,
|
||||
FileIcon,
|
||||
HashIcon,
|
||||
InfoIcon,
|
||||
PlusIcon,
|
||||
ReportIcon,
|
||||
RightArrowIcon,
|
||||
@@ -655,12 +679,13 @@ import { Multiselect } from 'vue-multiselect'
|
||||
|
||||
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||
import FileInput from '~/components/ui/FileInput.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import Categories from '~/components/ui/search/Categories.vue'
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
import { acceptFileFromProjectType } from '~/helpers/fileUtils.js'
|
||||
import { inferVersionInfo } from '~/helpers/infer.js'
|
||||
import { inferVersionInfo } from '~/helpers/infer'
|
||||
import { createDataPackVersion } from '~/helpers/package.js'
|
||||
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||
|
||||
@@ -672,11 +697,13 @@ export default defineNuxtComponent({
|
||||
Checkbox,
|
||||
ChevronRightIcon,
|
||||
Categories,
|
||||
CreateProjectVersionModal,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
StarIcon,
|
||||
FileIcon,
|
||||
InfoIcon,
|
||||
ReportIcon,
|
||||
SaveIcon,
|
||||
XIcon,
|
||||
@@ -966,6 +993,13 @@ export default defineNuxtComponent({
|
||||
methods: {
|
||||
formatBytes,
|
||||
formatCategory,
|
||||
handleOpenEditVersionModal(versionId, projectId, stageId) {
|
||||
if (!this.currentMember) return
|
||||
this.$refs.createProjectVersionModal?.openEditVersionModal(versionId, projectId, stageId)
|
||||
},
|
||||
async handleVersionSaved() {
|
||||
this.$router.go(0) // reload page for new data
|
||||
},
|
||||
async onImageUpload(file) {
|
||||
const response = await useImageUpload(file, { context: 'version' })
|
||||
|
||||
|
||||
@@ -80,20 +80,14 @@
|
||||
class="hover:!bg-button-bg"
|
||||
:dropdown-id="`${baseDropdownId}-edit-${version.id}`"
|
||||
:options="[
|
||||
{
|
||||
id: 'edit-metadata',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'metadata'),
|
||||
},
|
||||
{
|
||||
id: 'edit-details',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-details'),
|
||||
},
|
||||
{
|
||||
id: 'edit-changelog',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-changelog'),
|
||||
},
|
||||
{
|
||||
id: 'edit-dependencies',
|
||||
action: () =>
|
||||
handleOpenEditVersionModal(version.id, project.id, 'add-dependencies'),
|
||||
shown: project.project_type !== 'modpack',
|
||||
},
|
||||
{
|
||||
id: 'edit-files',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-files'),
|
||||
@@ -110,13 +104,9 @@
|
||||
<InfoIcon aria-hidden="true" />
|
||||
Edit details
|
||||
</template>
|
||||
<template #edit-dependencies>
|
||||
<template #edit-metadata>
|
||||
<BoxIcon aria-hidden="true" />
|
||||
Edit dependencies
|
||||
</template>
|
||||
<template #edit-changelog>
|
||||
<AlignLeftIcon aria-hidden="true" />
|
||||
Edit changelog
|
||||
Edit metadata
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
@@ -180,22 +170,16 @@
|
||||
shown: flags.developerMode,
|
||||
},
|
||||
{ divider: true, shown: !!currentMember },
|
||||
{
|
||||
id: 'edit-metadata',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'metadata'),
|
||||
shown: !!currentMember,
|
||||
},
|
||||
{
|
||||
id: 'edit-details',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-details'),
|
||||
shown: !!currentMember,
|
||||
},
|
||||
{
|
||||
id: 'edit-changelog',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-changelog'),
|
||||
shown: !!currentMember,
|
||||
},
|
||||
{
|
||||
id: 'edit-dependencies',
|
||||
action: () =>
|
||||
handleOpenEditVersionModal(version.id, project.id, 'add-dependencies'),
|
||||
shown: !!currentMember && project.project_type !== 'modpack',
|
||||
},
|
||||
{
|
||||
id: 'edit-files',
|
||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-files'),
|
||||
@@ -243,13 +227,9 @@
|
||||
<InfoIcon aria-hidden="true" />
|
||||
Edit details
|
||||
</template>
|
||||
<template #edit-dependencies>
|
||||
<template #edit-metadata>
|
||||
<BoxIcon aria-hidden="true" />
|
||||
Edit dependencies
|
||||
</template>
|
||||
<template #edit-changelog>
|
||||
<AlignLeftIcon aria-hidden="true" />
|
||||
Edit changelog
|
||||
Edit metadata
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
@@ -281,7 +261,6 @@
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
AlignLeftIcon,
|
||||
BoxIcon,
|
||||
ClipboardCopyIcon,
|
||||
DownloadIcon,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import type { Labrinth, UploadProgress } from '@modrinth/api-client'
|
||||
import { SaveIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import {
|
||||
createContext,
|
||||
@@ -55,6 +55,18 @@ export type VersionStage =
|
||||
| 'from-details-mc-versions'
|
||||
| 'from-details-environment'
|
||||
|
||||
export type SuggestedDependency = Labrinth.Versions.v3.Dependency & {
|
||||
name?: string
|
||||
icon?: string
|
||||
versionName?: string
|
||||
}
|
||||
|
||||
export interface PrimaryFile {
|
||||
name: string
|
||||
fileType?: string
|
||||
existing?: boolean
|
||||
}
|
||||
|
||||
export interface ManageVersionContextValue {
|
||||
// State
|
||||
draftVersion: Ref<Labrinth.Versions.v3.DraftVersion>
|
||||
@@ -64,16 +76,23 @@ export interface ManageVersionContextValue {
|
||||
projectType: Ref<Labrinth.Projects.v2.ProjectType | undefined>
|
||||
dependencyProjects: Ref<Record<string, Labrinth.Projects.v3.Project>>
|
||||
dependencyVersions: Ref<Record<string, Labrinth.Versions.v3.Version>>
|
||||
projectsFetchLoading: Ref<boolean>
|
||||
handlingNewFiles: Ref<boolean>
|
||||
suggestedDependencies: Ref<SuggestedDependency[]>
|
||||
visibleSuggestedDependencies: ComputedRef<SuggestedDependency[]>
|
||||
primaryFile: ComputedRef<PrimaryFile | null>
|
||||
|
||||
// Stage management
|
||||
stageConfigs: StageConfigInput<ManageVersionContextValue>[]
|
||||
isSubmitting: Ref<boolean>
|
||||
isUploading: Ref<boolean>
|
||||
uploadProgress: Ref<UploadProgress>
|
||||
modal: ShallowRef<ComponentExposed<typeof MultiStageModal> | null>
|
||||
|
||||
// Computed state
|
||||
editingVersion: ComputedRef<boolean>
|
||||
noLoadersProject: ComputedRef<boolean>
|
||||
noEnvironmentProject: ComputedRef<boolean>
|
||||
noDependenciesProject: ComputedRef<boolean>
|
||||
|
||||
// Stage helpers
|
||||
getNextLabel: (currentIndex?: number | null) => string
|
||||
@@ -81,11 +100,9 @@ export interface ManageVersionContextValue {
|
||||
|
||||
// Version methods
|
||||
newDraftVersion: (projectId: string, version?: Labrinth.Versions.v3.DraftVersion | null) => void
|
||||
setPrimaryFile: (index: number) => void
|
||||
setInferredVersionData: (
|
||||
file: File,
|
||||
project: Labrinth.Projects.v2.Project,
|
||||
) => Promise<InferredVersionInfo>
|
||||
handleNewFiles: (newFiles: File[]) => Promise<void>
|
||||
swapPrimaryFile: (index: number) => void
|
||||
replacePrimaryFile: (file: File) => Promise<void>
|
||||
getProject: (projectId: string) => Promise<Labrinth.Projects.v3.Project>
|
||||
getVersion: (versionId: string) => Promise<Labrinth.Versions.v3.Version>
|
||||
|
||||
@@ -129,24 +146,42 @@ const PROJECT_TYPE_LOADERS: Record<string, readonly string[]> = {
|
||||
modpack: ['mrpack'],
|
||||
} as const
|
||||
|
||||
export const fileTypeLabels: Record<Labrinth.Versions.v3.FileType | 'primary', string> = {
|
||||
primary: 'Primary',
|
||||
unknown: 'Other',
|
||||
'required-resource-pack': 'Required RP',
|
||||
'optional-resource-pack': 'Optional RP',
|
||||
'sources-jar': 'Sources JAR',
|
||||
'dev-jar': 'Dev JAR',
|
||||
'javadoc-jar': 'Javadoc JAR',
|
||||
signature: 'Signature',
|
||||
}
|
||||
|
||||
export const [injectManageVersionContext, provideManageVersionContext] =
|
||||
createContext<ManageVersionContextValue>('CreateProjectVersionModal')
|
||||
|
||||
export function createManageVersionContext(
|
||||
modal: ShallowRef<ComponentExposed<typeof MultiStageModal> | null>,
|
||||
onSave?: () => void,
|
||||
): ManageVersionContextValue {
|
||||
const { labrinth } = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { refreshVersions } = injectProjectPageContext()
|
||||
const { refreshVersions, projectV2 } = injectProjectPageContext()
|
||||
|
||||
// State
|
||||
const draftVersion = ref<Labrinth.Versions.v3.DraftVersion>(structuredClone(EMPTY_DRAFT_VERSION))
|
||||
const filesToAdd = ref<Labrinth.Versions.v3.DraftVersionFile[]>([])
|
||||
const existingFilesToDelete = ref<Labrinth.Versions.v3.VersionFileHash['sha1'][]>([])
|
||||
const handlingNewFiles = ref(false)
|
||||
const inferredVersionData = ref<InferredVersionInfo>()
|
||||
const dependencyProjects = ref<Record<string, Labrinth.Projects.v3.Project>>({})
|
||||
const dependencyVersions = ref<Record<string, Labrinth.Versions.v3.Version>>({})
|
||||
const projectsFetchLoading = ref(false)
|
||||
const suggestedDependencies = ref<SuggestedDependency[]>([])
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
const isUploading = ref(false)
|
||||
const uploadProgress = ref<UploadProgress>({ loaded: 0, total: 0, progress: 0 })
|
||||
|
||||
const projectType = computed<Labrinth.Projects.v2.ProjectType>(() => {
|
||||
const primaryFile = filesToAdd.value[0]?.file
|
||||
@@ -166,7 +201,7 @@ export function createManageVersionContext(
|
||||
if (loaders.some((loader) => PROJECT_TYPE_LOADERS.datapack.includes(loader))) {
|
||||
return 'datapack'
|
||||
}
|
||||
if (loaders.some((loader) => PROJECT_TYPE_LOADERS.resourcepack.includes(loader))) {
|
||||
if (loaders.length === 1 && loaders[0] === 'minecraft') {
|
||||
return 'resourcepack'
|
||||
}
|
||||
if (loaders.some((loader) => PROJECT_TYPE_LOADERS.shader.includes(loader))) {
|
||||
@@ -185,6 +220,30 @@ export function createManageVersionContext(
|
||||
// Computed state
|
||||
const editingVersion = computed(() => Boolean(draftVersion.value.version_id))
|
||||
|
||||
const visibleSuggestedDependencies = computed<SuggestedDependency[]>(() => {
|
||||
const existingDeps = draftVersion.value.dependencies ?? []
|
||||
const seenKeys = new Set<string>()
|
||||
|
||||
const isDuplicateSuggestion = (dep: SuggestedDependency) => {
|
||||
const key = `${dep.project_id ?? ''}:${dep.version_id ?? ''}`
|
||||
if (seenKeys.has(key)) return true
|
||||
seenKeys.add(key)
|
||||
return false
|
||||
}
|
||||
|
||||
const isAlreadyAdded = (dep: SuggestedDependency) =>
|
||||
existingDeps.some((existing) => {
|
||||
if (existing.project_id !== dep.project_id) return false
|
||||
if (!existing.version_id && !dep.version_id) return true
|
||||
return existing.version_id === dep.version_id
|
||||
})
|
||||
|
||||
return suggestedDependencies.value
|
||||
.filter((dep) => !isDuplicateSuggestion(dep))
|
||||
.filter((dep) => !isAlreadyAdded(dep))
|
||||
.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
|
||||
})
|
||||
|
||||
// Version management methods
|
||||
function newDraftVersion(
|
||||
projectId: string,
|
||||
@@ -195,15 +254,50 @@ export function createManageVersionContext(
|
||||
filesToAdd.value = []
|
||||
existingFilesToDelete.value = []
|
||||
inferredVersionData.value = undefined
|
||||
// projectType.value = undefined
|
||||
}
|
||||
|
||||
function setPrimaryFile(index: number) {
|
||||
async function handleNewFiles(newFiles: File[]) {
|
||||
handlingNewFiles.value = true
|
||||
// detect primary file if no primary file is set
|
||||
const primaryFileIndex = primaryFile.value ? null : detectPrimaryFileIndex(newFiles)
|
||||
|
||||
newFiles.forEach((file) => filesToAdd.value.push({ file }))
|
||||
|
||||
if (primaryFileIndex !== null) {
|
||||
if (primaryFileIndex) swapPrimaryFile(primaryFileIndex)
|
||||
}
|
||||
|
||||
if (
|
||||
filesToAdd.value.length === 1 &&
|
||||
!editingVersion.value &&
|
||||
modal.value?.currentStageIndex === 0
|
||||
) {
|
||||
if (await rejectOnRedundantWrappedZip(filesToAdd.value[0].file)) return
|
||||
|
||||
await addDetectedData()
|
||||
modal.value?.nextStage()
|
||||
}
|
||||
|
||||
handlingNewFiles.value = false
|
||||
}
|
||||
|
||||
async function replacePrimaryFile(file: File) {
|
||||
if (file && !editingVersion.value) {
|
||||
filesToAdd.value[0] = { file }
|
||||
}
|
||||
if (await rejectOnRedundantWrappedZip(file)) return
|
||||
await addDetectedData()
|
||||
}
|
||||
|
||||
async function swapPrimaryFile(index: number) {
|
||||
const files = filesToAdd.value
|
||||
if (index <= 0 || index >= files.length) return
|
||||
files[0].fileType = 'unknown'
|
||||
files[index].fileType = 'unknown'
|
||||
;[files[0], files[index]] = [files[index], files[0]]
|
||||
|
||||
if (await rejectOnRedundantWrappedZip(files[0].file)) return
|
||||
await addDetectedData()
|
||||
}
|
||||
|
||||
const tags = useGeneratedState()
|
||||
@@ -241,6 +335,65 @@ export function createManageVersionContext(
|
||||
}
|
||||
}
|
||||
|
||||
async function checkRedundantWrappedZip(file: File): Promise<boolean> {
|
||||
const fileName = file.name.toLowerCase()
|
||||
if (!fileName.endsWith('.zip')) return false
|
||||
|
||||
const zip = await JSZip.loadAsync(file)
|
||||
const entries = Object.keys(zip.files).map((e) => e.toLowerCase())
|
||||
const filtered = entries.filter((e) => !e.startsWith('__macosx/') && !e.endsWith('.ds_store'))
|
||||
|
||||
const hasRootEntries = filtered.some((e) => !e.includes('/'))
|
||||
if (hasRootEntries) return false
|
||||
|
||||
const topLevelFolders = new Set(filtered.map((e) => e.split('/')[0]).filter(Boolean))
|
||||
if (topLevelFolders.size !== 1) return false
|
||||
|
||||
const [folderName] = [...topLevelFolders]
|
||||
|
||||
// Check if the inner folder contents indicate a datapack or resource pack
|
||||
const innerEntries = filtered.map((e) => e.substring(folderName.length + 1))
|
||||
const hasPackMcmeta = hasFile(innerEntries, 'pack.mcmeta')
|
||||
const hasAssets = hasDir(innerEntries, 'assets')
|
||||
const hasData = hasDir(innerEntries, 'data')
|
||||
|
||||
return hasPackMcmeta && (hasAssets || hasData)
|
||||
}
|
||||
|
||||
async function rejectOnRedundantWrappedZip(file: File): Promise<boolean> {
|
||||
if (await checkRedundantWrappedZip(file)) {
|
||||
newDraftVersion(projectV2.value.id)
|
||||
modal.value?.setStage('add-files')
|
||||
addNotification({
|
||||
title: 'Invalid ZIP structure',
|
||||
text: `The uploaded ZIP file "${file.name}" contains a redundant top-level folder. Please re-zip the contents directly without the extra folder layer.`,
|
||||
type: 'error',
|
||||
})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function inferEnvironmentFromVersions(
|
||||
projectId: string,
|
||||
loaders: string[],
|
||||
): Promise<Labrinth.Projects.v3.Environment | undefined> {
|
||||
try {
|
||||
const versions = await labrinth.versions_v3.getProjectVersions(projectId, {
|
||||
loaders,
|
||||
})
|
||||
|
||||
if (versions.length > 0) {
|
||||
const mostRecentVersion = versions[0]
|
||||
const version = await labrinth.versions_v3.getVersion(mostRecentVersion.id)
|
||||
return version.environment !== 'unknown' ? version.environment : undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching versions for environment inference:', error)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function setInferredVersionData(
|
||||
file: File,
|
||||
project: Labrinth.Projects.v2.Project,
|
||||
@@ -251,19 +404,7 @@ export function createManageVersionContext(
|
||||
tags.value.gameVersions,
|
||||
)) as InferredVersionInfo
|
||||
|
||||
try {
|
||||
const versions = await labrinth.versions_v3.getProjectVersions(project.id, {
|
||||
loaders: inferred.loaders ?? [],
|
||||
})
|
||||
|
||||
if (versions.length > 0) {
|
||||
const mostRecentVersion = versions[0]
|
||||
const version = await labrinth.versions_v3.getVersion(mostRecentVersion.id)
|
||||
inferred.environment = version.environment !== 'unknown' ? version.environment : undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching versions for environment inference:', error)
|
||||
}
|
||||
inferred.environment = await inferEnvironmentFromVersions(project.id, inferred.loaders ?? [])
|
||||
|
||||
const noLoaders = !inferred.loaders?.length
|
||||
|
||||
@@ -284,6 +425,12 @@ export function createManageVersionContext(
|
||||
return inferred
|
||||
}
|
||||
|
||||
// Stage visibility computeds (inlined)
|
||||
const noEnvironmentProject = computed(
|
||||
() => projectType.value !== 'mod' && projectType.value !== 'modpack',
|
||||
)
|
||||
const noDependenciesProject = computed(() => projectType.value === 'modpack')
|
||||
|
||||
const getProject = async (projectId: string) => {
|
||||
if (dependencyProjects.value[projectId]) {
|
||||
return dependencyProjects.value[projectId]
|
||||
@@ -302,16 +449,198 @@ export function createManageVersionContext(
|
||||
return version
|
||||
}
|
||||
|
||||
// Primary file computed
|
||||
const primaryFile = computed<PrimaryFile | null>(() => {
|
||||
const existingPrimaryFile = draftVersion.value.existing_files?.[0]
|
||||
if (existingPrimaryFile) {
|
||||
return {
|
||||
name: existingPrimaryFile.filename,
|
||||
fileType: existingPrimaryFile.file_type,
|
||||
existing: true,
|
||||
}
|
||||
}
|
||||
|
||||
const addedPrimaryFile = filesToAdd.value[0]
|
||||
if (addedPrimaryFile) {
|
||||
return {
|
||||
name: addedPrimaryFile.file.name,
|
||||
fileType: addedPrimaryFile.fileType,
|
||||
existing: false,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
// File handling helpers
|
||||
function detectPrimaryFileIndex(files: File[]): number {
|
||||
const extensionPriority = ['.jar', '.zip', '.litemod', '.mrpack', '.mrpack-primary']
|
||||
|
||||
for (const ext of extensionPriority) {
|
||||
const matches = files.filter((file) => file.name.toLowerCase().endsWith(ext))
|
||||
if (matches.length > 0) {
|
||||
const shortest = matches.reduce((a, b) => (a.name.length < b.name.length ? a : b))
|
||||
return files.indexOf(shortest)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
const addDetectedData = async () => {
|
||||
if (editingVersion.value) return
|
||||
|
||||
const primaryFileData = filesToAdd.value[0]?.file
|
||||
if (!primaryFileData) return
|
||||
|
||||
try {
|
||||
const inferredData = await setInferredVersionData(primaryFileData, projectV2.value)
|
||||
const mappedInferredData: Partial<Labrinth.Versions.v3.DraftVersion> = {
|
||||
...inferredData,
|
||||
name: inferredData.name || '',
|
||||
}
|
||||
|
||||
draftVersion.value = {
|
||||
...draftVersion.value,
|
||||
...mappedInferredData,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing version file data', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch draft version dependencies to fetch project/version data
|
||||
watch(
|
||||
draftVersion,
|
||||
async (version) => {
|
||||
if (noDependenciesProject.value) return
|
||||
const deps = version.dependencies || []
|
||||
|
||||
for (const dep of deps) {
|
||||
try {
|
||||
if (dep?.project_id) await getProject(dep.project_id)
|
||||
if (dep?.version_id) await getVersion(dep.version_id)
|
||||
} catch (error: any) {
|
||||
addNotification({
|
||||
title: 'Could not fetch dependency data',
|
||||
text: error.data ? error.data.description : error,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
projectsFetchLoading.value = false
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
// Watch loaders to infer environment if not set
|
||||
watch(
|
||||
() => draftVersion.value.loaders,
|
||||
async (loaders) => {
|
||||
if (noEnvironmentProject.value) return
|
||||
if (draftVersion.value.environment) return
|
||||
if (!loaders?.length) return
|
||||
|
||||
const projectId = draftVersion.value.project_id
|
||||
if (!projectId) return
|
||||
|
||||
const environment = await inferEnvironmentFromVersions(projectId, loaders)
|
||||
if (environment && !draftVersion.value.environment) {
|
||||
draftVersion.value.environment = environment
|
||||
inferredVersionData.value = { ...inferredVersionData.value, environment }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Watch loaders to fetch suggested dependencies
|
||||
// Gets the most recent version that matches loaders and suggests its dependencies
|
||||
watch(
|
||||
() => draftVersion.value.loaders,
|
||||
async (loaders) => {
|
||||
if (noDependenciesProject.value) return
|
||||
try {
|
||||
suggestedDependencies.value = []
|
||||
|
||||
if (!loaders?.length) return
|
||||
|
||||
const projectId = draftVersion.value.project_id
|
||||
if (!projectId) return
|
||||
|
||||
try {
|
||||
const versions = await labrinth.versions_v3.getProjectVersions(projectId, {
|
||||
loaders,
|
||||
})
|
||||
|
||||
// Get the most recent matching version and extract its dependencies
|
||||
if (versions.length > 0) {
|
||||
const mostRecentVersion = versions[0]
|
||||
for (const dep of mostRecentVersion.dependencies) {
|
||||
suggestedDependencies.value.push({
|
||||
project_id: dep.project_id,
|
||||
version_id: dep.version_id,
|
||||
dependency_type: dep.dependency_type,
|
||||
file_name: dep.file_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to get versions for project ${projectId}:`, error)
|
||||
}
|
||||
|
||||
for (const dep of suggestedDependencies.value) {
|
||||
try {
|
||||
if (dep.project_id) {
|
||||
const proj = await getProject(dep.project_id)
|
||||
dep.name = proj.name
|
||||
dep.icon = proj.icon_url
|
||||
}
|
||||
|
||||
if (dep.version_id) {
|
||||
const version = await getVersion(dep.version_id)
|
||||
dep.versionName = version.name
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to fetch project/version data for dependency:`, error)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
addNotification({
|
||||
title: 'Could not fetch suggested dependencies',
|
||||
text: error.data ? error.data.description : error,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// Submission handlers
|
||||
async function handleCreateVersion() {
|
||||
const version = toRaw(draftVersion.value)
|
||||
const files = toRaw(filesToAdd.value)
|
||||
isSubmitting.value = true
|
||||
isUploading.value = true
|
||||
|
||||
// Reset progress and navigate to uploading stage
|
||||
uploadProgress.value = { loaded: 0, total: 0, progress: 0 }
|
||||
|
||||
if (noEnvironmentProject.value) version.environment = undefined
|
||||
|
||||
try {
|
||||
await labrinth.versions_v3.createVersion(version, files, projectType.value ?? null)
|
||||
const uploadHandle = labrinth.versions_v3.createVersion(
|
||||
version,
|
||||
files,
|
||||
projectType.value ?? null,
|
||||
)
|
||||
|
||||
// Subscribe to progress updates
|
||||
uploadHandle.onProgress((progress) => {
|
||||
uploadProgress.value = progress
|
||||
})
|
||||
|
||||
// Wait for upload to complete
|
||||
await uploadHandle.promise
|
||||
|
||||
modal.value?.hide()
|
||||
addNotification({
|
||||
title: 'Project version created',
|
||||
@@ -319,13 +648,15 @@ export function createManageVersionContext(
|
||||
type: 'success',
|
||||
})
|
||||
await refreshVersions()
|
||||
onSave?.()
|
||||
} catch (err: any) {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
title: 'Could not create project version',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
isUploading.value = false
|
||||
isSubmitting.value = false
|
||||
}
|
||||
|
||||
@@ -336,6 +667,12 @@ export function createManageVersionContext(
|
||||
|
||||
isSubmitting.value = true
|
||||
|
||||
// Reset progress if we have files to upload
|
||||
if (files.length > 0) {
|
||||
isUploading.value = true
|
||||
uploadProgress.value = { loaded: 0, total: 0, progress: 0 }
|
||||
}
|
||||
|
||||
if (noEnvironmentProject.value) version.environment = undefined
|
||||
|
||||
try {
|
||||
@@ -362,7 +699,13 @@ export function createManageVersionContext(
|
||||
await labrinth.versions_v3.modifyVersion(version.version_id, data)
|
||||
|
||||
if (files.length > 0) {
|
||||
await labrinth.versions_v3.addFilesToVersion(version.version_id, files)
|
||||
const uploadHandle = labrinth.versions_v3.addFilesToVersion(version.version_id, files)
|
||||
|
||||
uploadHandle.onProgress((progress) => {
|
||||
uploadProgress.value = progress
|
||||
})
|
||||
|
||||
await uploadHandle.promise
|
||||
}
|
||||
|
||||
// Delete files that were marked for deletion
|
||||
@@ -379,6 +722,7 @@ export function createManageVersionContext(
|
||||
type: 'success',
|
||||
})
|
||||
await refreshVersions()
|
||||
onSave?.()
|
||||
} catch (err: any) {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
@@ -386,15 +730,10 @@ export function createManageVersionContext(
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
isUploading.value = false
|
||||
isSubmitting.value = false
|
||||
}
|
||||
|
||||
// Stage visibility computeds (inlined)
|
||||
const noLoadersProject = computed(() => projectType.value === 'resourcepack')
|
||||
const noEnvironmentProject = computed(
|
||||
() => projectType.value !== 'mod' && projectType.value !== 'modpack',
|
||||
)
|
||||
|
||||
// Dynamic next button label
|
||||
function getNextLabel(currentIndex: number | null = null) {
|
||||
const currentStageIndex = currentIndex ? currentIndex : modal.value?.currentStageIndex || 0
|
||||
@@ -424,6 +763,8 @@ export function createManageVersionContext(
|
||||
return editingVersion.value ? 'Edit environment' : 'Add environment'
|
||||
case 'add-changelog':
|
||||
return editingVersion.value ? 'Edit changelog' : 'Add changelog'
|
||||
case 'metadata':
|
||||
return 'Edit metadata'
|
||||
default:
|
||||
return 'Next'
|
||||
}
|
||||
@@ -448,16 +789,23 @@ export function createManageVersionContext(
|
||||
projectType,
|
||||
dependencyProjects,
|
||||
dependencyVersions,
|
||||
handlingNewFiles,
|
||||
projectsFetchLoading,
|
||||
suggestedDependencies,
|
||||
visibleSuggestedDependencies,
|
||||
primaryFile,
|
||||
|
||||
// Stage management
|
||||
stageConfigs,
|
||||
isSubmitting,
|
||||
isUploading,
|
||||
uploadProgress,
|
||||
modal,
|
||||
|
||||
// Computed
|
||||
editingVersion,
|
||||
noLoadersProject,
|
||||
noEnvironmentProject,
|
||||
noDependenciesProject,
|
||||
|
||||
// Stage helpers
|
||||
getNextLabel,
|
||||
@@ -465,10 +813,11 @@ export function createManageVersionContext(
|
||||
|
||||
// Methods
|
||||
newDraftVersion,
|
||||
setPrimaryFile,
|
||||
setInferredVersionData,
|
||||
swapPrimaryFile,
|
||||
replacePrimaryFile,
|
||||
getProject,
|
||||
getVersion,
|
||||
handleNewFiles,
|
||||
handleCreateVersion,
|
||||
handleSaveVersionEdits,
|
||||
}
|
||||
|
||||
100
apps/frontend/src/providers/version/stages/add-files-stage.ts
Normal file
100
apps/frontend/src/providers/version/stages/add-files-stage.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { LeftArrowIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
|
||||
import type { StageConfigInput } from '@modrinth/ui'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import AddFilesStage from '~/components/ui/create-project-version/stages/AddFilesStage.vue'
|
||||
|
||||
import type { ManageVersionContextValue } from '../manage-version-modal'
|
||||
|
||||
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'add-files',
|
||||
stageContent: markRaw(AddFilesStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit files' : 'Files'),
|
||||
nonProgressStage: (ctx) => ctx.editingVersion.value,
|
||||
cannotNavigateForward: (ctx) => {
|
||||
const hasFiles =
|
||||
ctx.filesToAdd.value.length !== 0 ||
|
||||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
|
||||
return !hasFiles || ctx.handlingNewFiles.value
|
||||
},
|
||||
leftButtonConfig: (ctx) => {
|
||||
const hasFiles =
|
||||
ctx.filesToAdd.value.length !== 0 ||
|
||||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
|
||||
|
||||
if (ctx.editingVersion.value)
|
||||
return {
|
||||
label: 'Cancel',
|
||||
icon: XIcon,
|
||||
onClick: () => ctx.modal.value?.hide(),
|
||||
}
|
||||
|
||||
if (!hasFiles || ctx.handlingNewFiles.value) return null
|
||||
|
||||
return {
|
||||
label: 'Cancel',
|
||||
icon: XIcon,
|
||||
onClick: () => ctx.modal.value?.hide(),
|
||||
}
|
||||
},
|
||||
rightButtonConfig: (ctx) => {
|
||||
const hasFiles =
|
||||
ctx.filesToAdd.value.length !== 0 ||
|
||||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
|
||||
|
||||
if (ctx.editingVersion.value)
|
||||
return {
|
||||
...ctx.saveButtonConfig(),
|
||||
label: 'Save files',
|
||||
disabled: ctx.isSubmitting.value,
|
||||
}
|
||||
|
||||
if (!hasFiles || ctx.handlingNewFiles.value) return null
|
||||
|
||||
return {
|
||||
label: ctx.getNextLabel(),
|
||||
icon: RightArrowIcon,
|
||||
iconPosition: 'after',
|
||||
disabled: !hasFiles,
|
||||
onClick: () => ctx.modal.value?.nextStage(),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'from-details-files',
|
||||
stageContent: markRaw(AddFilesStage),
|
||||
title: 'Edit files',
|
||||
nonProgressStage: true,
|
||||
leftButtonConfig: (ctx) => {
|
||||
const hasFiles =
|
||||
ctx.filesToAdd.value.length !== 0 ||
|
||||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
|
||||
|
||||
return {
|
||||
label: 'Back',
|
||||
icon: LeftArrowIcon,
|
||||
disabled: !hasFiles,
|
||||
onClick: () => ctx.modal.value?.setStage('metadata'),
|
||||
}
|
||||
},
|
||||
rightButtonConfig: (ctx) => {
|
||||
const hasFiles =
|
||||
ctx.filesToAdd.value.length !== 0 ||
|
||||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
|
||||
|
||||
return ctx.editingVersion.value
|
||||
? {
|
||||
...ctx.saveButtonConfig(),
|
||||
label: 'Save files',
|
||||
disabled: !hasFiles || ctx.isSubmitting.value,
|
||||
}
|
||||
: {
|
||||
label: 'Add details',
|
||||
icon: RightArrowIcon,
|
||||
iconPosition: 'after',
|
||||
disabled: !hasFiles,
|
||||
onClick: () => ctx.modal.value?.setStage('add-details'),
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { RightArrowIcon, XIcon } from '@modrinth/assets'
|
||||
import type { StageConfigInput } from '@modrinth/ui'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import AddFilesStage from '~/components/ui/create-project-version/stages/AddFilesStage.vue'
|
||||
|
||||
import type { ManageVersionContextValue } from '../manage-version-modal'
|
||||
|
||||
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'add-files',
|
||||
stageContent: markRaw(AddFilesStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit files' : 'Add files'),
|
||||
leftButtonConfig: (ctx) => {
|
||||
const hasFiles =
|
||||
ctx.filesToAdd.value.length !== 0 ||
|
||||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
|
||||
|
||||
if (ctx.editingVersion.value)
|
||||
return {
|
||||
label: 'Cancel',
|
||||
icon: XIcon,
|
||||
onClick: () => ctx.modal.value?.hide(),
|
||||
}
|
||||
|
||||
if (!hasFiles) return null
|
||||
|
||||
return {
|
||||
label: 'Cancel',
|
||||
icon: XIcon,
|
||||
onClick: () => ctx.modal.value?.hide(),
|
||||
}
|
||||
},
|
||||
rightButtonConfig: (ctx) => {
|
||||
const hasFiles =
|
||||
ctx.filesToAdd.value.length !== 0 ||
|
||||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
|
||||
|
||||
if (ctx.editingVersion.value)
|
||||
return {
|
||||
...ctx.saveButtonConfig(),
|
||||
label: 'Save files',
|
||||
disabled: ctx.isSubmitting.value,
|
||||
}
|
||||
|
||||
if (!hasFiles) return null
|
||||
|
||||
return {
|
||||
label: ctx.getNextLabel(),
|
||||
icon: RightArrowIcon,
|
||||
iconPosition: 'after',
|
||||
disabled: !hasFiles,
|
||||
onClick: () => ctx.modal.value?.nextStage(),
|
||||
}
|
||||
},
|
||||
nonProgressStage: (ctx) => ctx.editingVersion.value,
|
||||
}
|
||||
@@ -2,14 +2,15 @@ import { LeftArrowIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
|
||||
import type { StageConfigInput } from '@modrinth/ui'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import AddDetailsStage from '~/components/ui/create-project-version/stages/AddDetailsStage.vue'
|
||||
import DependenciesStage from '~/components/ui/create-project-version/stages/DependenciesStage.vue'
|
||||
|
||||
import type { ManageVersionContextValue } from '../manage-version-modal'
|
||||
|
||||
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'add-details',
|
||||
stageContent: markRaw(AddDetailsStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit details' : 'Add details'),
|
||||
id: 'add-dependencies',
|
||||
stageContent: markRaw(DependenciesStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit dependencies' : 'Dependencies'),
|
||||
skip: true,
|
||||
leftButtonConfig: (ctx) =>
|
||||
ctx.editingVersion.value
|
||||
? {
|
||||
@@ -24,17 +25,35 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
},
|
||||
rightButtonConfig: (ctx) =>
|
||||
ctx.editingVersion.value
|
||||
? {
|
||||
...ctx.saveButtonConfig(),
|
||||
disabled:
|
||||
ctx.draftVersion.value.version_number.trim().length === 0 || ctx.isSubmitting.value,
|
||||
}
|
||||
? ctx.saveButtonConfig()
|
||||
: {
|
||||
label: ctx.getNextLabel(),
|
||||
icon: RightArrowIcon,
|
||||
iconPosition: 'after',
|
||||
disabled: ctx.draftVersion.value.version_number.trim().length === 0,
|
||||
onClick: () => ctx.modal.value?.nextStage(),
|
||||
},
|
||||
nonProgressStage: (ctx) => ctx.editingVersion.value,
|
||||
}
|
||||
|
||||
export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'from-details-dependencies',
|
||||
stageContent: markRaw(DependenciesStage),
|
||||
title: 'Edit dependencies',
|
||||
nonProgressStage: true,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
icon: LeftArrowIcon,
|
||||
onClick: () => ctx.modal.value?.setStage('metadata'),
|
||||
}),
|
||||
rightButtonConfig: (ctx) =>
|
||||
ctx.editingVersion.value
|
||||
? {
|
||||
...ctx.saveButtonConfig(),
|
||||
}
|
||||
: {
|
||||
label: 'Add details',
|
||||
icon: RightArrowIcon,
|
||||
iconPosition: 'after',
|
||||
onClick: () => ctx.modal.value?.setStage('add-details'),
|
||||
},
|
||||
}
|
||||
@@ -2,14 +2,15 @@ import { LeftArrowIcon, PlusIcon, SaveIcon, SpinnerIcon, XIcon } from '@modrinth
|
||||
import type { StageConfigInput } from '@modrinth/ui'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import AddChangelogStage from '~/components/ui/create-project-version/stages/AddChangelogStage.vue'
|
||||
import DetailsStage from '~/components/ui/create-project-version/stages/DetailsStage.vue'
|
||||
|
||||
import type { ManageVersionContextValue } from '../manage-version-modal'
|
||||
|
||||
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'add-changelog',
|
||||
stageContent: markRaw(AddChangelogStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit changelog' : 'Add changelog'),
|
||||
id: 'add-details',
|
||||
stageContent: markRaw(DetailsStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit details' : 'Details'),
|
||||
maxWidth: '744px',
|
||||
leftButtonConfig: (ctx) =>
|
||||
ctx.editingVersion.value
|
||||
? {
|
||||
@@ -23,7 +24,13 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
onClick: () => ctx.modal.value?.prevStage(),
|
||||
},
|
||||
rightButtonConfig: (ctx) => ({
|
||||
label: ctx.editingVersion.value ? 'Save changes' : 'Create version',
|
||||
label: ctx.editingVersion.value
|
||||
? 'Save changes'
|
||||
: ctx.isUploading.value
|
||||
? ctx.uploadProgress.value.progress >= 1
|
||||
? 'Creating version'
|
||||
: `Uploading version ${Math.round(ctx.uploadProgress.value.progress * 100)}%`
|
||||
: 'Create version',
|
||||
icon: ctx.isSubmitting.value ? SpinnerIcon : ctx.editingVersion.value ? SaveIcon : PlusIcon,
|
||||
iconPosition: 'before',
|
||||
iconClass: ctx.isSubmitting.value ? 'animate-spin' : undefined,
|
||||
@@ -2,18 +2,20 @@ import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets'
|
||||
import type { StageConfigInput } from '@modrinth/ui'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import AddEnvironmentStage from '~/components/ui/create-project-version/stages/AddEnvironmentStage.vue'
|
||||
import EnvironmentStage from '~/components/ui/create-project-version/stages/EnvironmentStage.vue'
|
||||
|
||||
import type { ManageVersionContextValue } from '../manage-version-modal'
|
||||
|
||||
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'add-environment',
|
||||
stageContent: markRaw(AddEnvironmentStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit environment' : 'Add environment'),
|
||||
stageContent: markRaw(EnvironmentStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit environment' : 'Environment'),
|
||||
skip: (ctx) =>
|
||||
ctx.noEnvironmentProject.value ||
|
||||
(!ctx.editingVersion.value && !!ctx.inferredVersionData.value?.environment) ||
|
||||
(ctx.editingVersion.value && !!ctx.draftVersion.value.environment),
|
||||
hideStageInBreadcrumb: (ctx) => !ctx.primaryFile.value || ctx.handlingNewFiles.value,
|
||||
cannotNavigateForward: (ctx) => !ctx.draftVersion.value.environment,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
icon: LeftArrowIcon,
|
||||
@@ -30,14 +32,14 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
|
||||
export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'from-details-environment',
|
||||
stageContent: markRaw(AddEnvironmentStage),
|
||||
stageContent: markRaw(EnvironmentStage),
|
||||
title: 'Edit environment',
|
||||
nonProgressStage: true,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
icon: LeftArrowIcon,
|
||||
disabled: !ctx.draftVersion.value.environment,
|
||||
onClick: () => ctx.modal.value?.setStage('add-details'),
|
||||
onClick: () => ctx.modal.value?.setStage('metadata'),
|
||||
}),
|
||||
rightButtonConfig: (ctx) =>
|
||||
ctx.editingVersion.value
|
||||
@@ -46,10 +48,10 @@ export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue>
|
||||
disabled: !ctx.draftVersion.value.environment,
|
||||
}
|
||||
: {
|
||||
label: ctx.getNextLabel(2),
|
||||
label: 'Add details',
|
||||
icon: RightArrowIcon,
|
||||
iconPosition: 'after',
|
||||
disabled: !ctx.draftVersion.value.environment,
|
||||
onClick: () => ctx.modal.value?.setStage(2),
|
||||
onClick: () => ctx.modal.value?.setStage('add-details'),
|
||||
},
|
||||
}
|
||||
@@ -1,30 +1,39 @@
|
||||
import { stageConfig as addChangelogStageConfig } from './add-changelog'
|
||||
import { stageConfig as addDependenciesStageConfig } from './add-dependencies'
|
||||
import { stageConfig as addDetailsStageConfig } from './add-details'
|
||||
import {
|
||||
fromDetailsStageConfig as editEnvironmentStageConfig,
|
||||
stageConfig as addEnvironmentStageConfig,
|
||||
} from './add-environment'
|
||||
import { stageConfig as addFilesStageConfig } from './add-files'
|
||||
fromDetailsStageConfig as fromDetailsFilesStageConfig,
|
||||
stageConfig as addFilesStageConfig,
|
||||
} from './add-files-stage'
|
||||
import {
|
||||
fromDetailsStageConfig as editLoadersStageConfig,
|
||||
stageConfig as addLoadersStageConfig,
|
||||
} from './add-loaders'
|
||||
fromDetailsStageConfig as fromDetailsDependenciesStageConfig,
|
||||
stageConfig as dependenciesStageConfig,
|
||||
} from './dependencies-stage'
|
||||
import { stageConfig as detailsStageConfig } from './details-stage'
|
||||
import {
|
||||
fromDetailsStageConfig as editMcVersionsStageConfig,
|
||||
stageConfig as addMcVersionsStageConfig,
|
||||
} from './add-mc-versions'
|
||||
fromDetailsStageConfig as fromDetailsEnvironmentStageConfig,
|
||||
stageConfig as environmentStageConfig,
|
||||
} from './environment-stage'
|
||||
import {
|
||||
fromDetailsStageConfig as fromDetailsLoadersStageConfig,
|
||||
stageConfig as loadersStageConfig,
|
||||
} from './loaders-stage'
|
||||
import {
|
||||
fromDetailsStageConfig as fromDetailsMcVersionsStageConfig,
|
||||
stageConfig as mcVersionsStageConfig,
|
||||
} from './mc-versions-stage'
|
||||
import { stageConfig as metadataStageConfig } from './metadata-stage'
|
||||
|
||||
export const stageConfigs = [
|
||||
addFilesStageConfig,
|
||||
addDetailsStageConfig,
|
||||
addLoadersStageConfig,
|
||||
addMcVersionsStageConfig,
|
||||
addEnvironmentStageConfig,
|
||||
addDependenciesStageConfig,
|
||||
addChangelogStageConfig,
|
||||
loadersStageConfig,
|
||||
mcVersionsStageConfig,
|
||||
environmentStageConfig,
|
||||
dependenciesStageConfig,
|
||||
metadataStageConfig,
|
||||
detailsStageConfig,
|
||||
|
||||
// Non-progress stages for editing from details page
|
||||
editLoadersStageConfig,
|
||||
editMcVersionsStageConfig,
|
||||
editEnvironmentStageConfig,
|
||||
fromDetailsLoadersStageConfig,
|
||||
fromDetailsMcVersionsStageConfig,
|
||||
fromDetailsEnvironmentStageConfig,
|
||||
fromDetailsFilesStageConfig,
|
||||
fromDetailsDependenciesStageConfig,
|
||||
]
|
||||
|
||||
@@ -2,18 +2,18 @@ import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets'
|
||||
import type { StageConfigInput } from '@modrinth/ui'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import AddLoadersStage from '~/components/ui/create-project-version/stages/AddLoadersStage.vue'
|
||||
import LoadersStage from '~/components/ui/create-project-version/stages/LoadersStage.vue'
|
||||
|
||||
import type { ManageVersionContextValue } from '../manage-version-modal'
|
||||
|
||||
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'add-loaders',
|
||||
stageContent: markRaw(AddLoadersStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit loaders' : 'Add loaders'),
|
||||
stageContent: markRaw(LoadersStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit loaders' : 'Loaders'),
|
||||
skip: (ctx) =>
|
||||
ctx.noLoadersProject.value ||
|
||||
(ctx.inferredVersionData.value?.loaders?.length ?? 0) > 0 ||
|
||||
ctx.editingVersion.value,
|
||||
(ctx.inferredVersionData.value?.loaders?.length ?? 0) > 0 || ctx.editingVersion.value,
|
||||
hideStageInBreadcrumb: (ctx) => !ctx.primaryFile.value || ctx.handlingNewFiles.value,
|
||||
cannotNavigateForward: (ctx) => ctx.draftVersion.value.loaders.length === 0,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
icon: LeftArrowIcon,
|
||||
@@ -30,14 +30,14 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
|
||||
export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'from-details-loaders',
|
||||
stageContent: markRaw(AddLoadersStage),
|
||||
stageContent: markRaw(LoadersStage),
|
||||
title: 'Edit loaders',
|
||||
nonProgressStage: true,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
icon: LeftArrowIcon,
|
||||
disabled: ctx.draftVersion.value.loaders.length === 0,
|
||||
onClick: () => ctx.modal.value?.setStage('add-details'),
|
||||
onClick: () => ctx.modal.value?.setStage('metadata'),
|
||||
}),
|
||||
rightButtonConfig: (ctx) =>
|
||||
ctx.editingVersion.value
|
||||
@@ -46,10 +46,10 @@ export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue>
|
||||
disabled: ctx.draftVersion.value.loaders.length === 0,
|
||||
}
|
||||
: {
|
||||
label: ctx.getNextLabel(2),
|
||||
label: 'Add details',
|
||||
icon: RightArrowIcon,
|
||||
iconPosition: 'after',
|
||||
disabled: ctx.draftVersion.value.loaders.length === 0,
|
||||
onClick: () => ctx.modal.value?.setStage(2),
|
||||
onClick: () => ctx.modal.value?.setStage('add-details'),
|
||||
},
|
||||
}
|
||||
@@ -2,16 +2,19 @@ import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets'
|
||||
import type { StageConfigInput } from '@modrinth/ui'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import AddMcVersionsStage from '~/components/ui/create-project-version/stages/AddMcVersionsStage.vue'
|
||||
import McVersionsStage from '~/components/ui/create-project-version/stages/McVersionsStage.vue'
|
||||
|
||||
import type { ManageVersionContextValue } from '../manage-version-modal'
|
||||
|
||||
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'add-mc-versions',
|
||||
stageContent: markRaw(AddMcVersionsStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit game versions' : 'Add game versions'),
|
||||
stageContent: markRaw(McVersionsStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit game versions' : 'Game versions'),
|
||||
skip: (ctx) =>
|
||||
(ctx.inferredVersionData.value?.game_versions?.length ?? 0) > 0 || ctx.editingVersion.value,
|
||||
(ctx.inferredVersionData.value?.game_versions?.length ?? 0) > 0 || !ctx.primaryFile.value,
|
||||
hideStageInBreadcrumb: (ctx) => !ctx.primaryFile.value || ctx.handlingNewFiles.value,
|
||||
|
||||
cannotNavigateForward: (ctx) => ctx.draftVersion.value.game_versions.length === 0,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
icon: LeftArrowIcon,
|
||||
@@ -28,14 +31,14 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
|
||||
export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'from-details-mc-versions',
|
||||
stageContent: markRaw(AddMcVersionsStage),
|
||||
stageContent: markRaw(McVersionsStage),
|
||||
title: 'Edit game versions',
|
||||
nonProgressStage: true,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
icon: LeftArrowIcon,
|
||||
disabled: ctx.draftVersion.value.game_versions.length === 0,
|
||||
onClick: () => ctx.modal.value?.setStage('add-details'),
|
||||
onClick: () => ctx.modal.value?.setStage('metadata'),
|
||||
}),
|
||||
rightButtonConfig: (ctx) =>
|
||||
ctx.editingVersion.value
|
||||
@@ -44,10 +47,10 @@ export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue>
|
||||
disabled: ctx.draftVersion.value.game_versions.length === 0,
|
||||
}
|
||||
: {
|
||||
label: ctx.getNextLabel(2),
|
||||
label: 'Add details',
|
||||
icon: RightArrowIcon,
|
||||
iconPosition: 'after',
|
||||
disabled: ctx.draftVersion.value.game_versions.length === 0,
|
||||
onClick: () => ctx.modal.value?.setStage(2),
|
||||
onClick: () => ctx.modal.value?.setStage('add-details'),
|
||||
},
|
||||
}
|
||||
@@ -2,15 +2,14 @@ import { LeftArrowIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
|
||||
import type { StageConfigInput } from '@modrinth/ui'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import AddDependenciesStage from '~/components/ui/create-project-version/stages/AddDependenciesStage.vue'
|
||||
import MetadataStage from '~/components/ui/create-project-version/stages/MetadataStage.vue'
|
||||
|
||||
import type { ManageVersionContextValue } from '../manage-version-modal'
|
||||
|
||||
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
id: 'add-dependencies',
|
||||
stageContent: markRaw(AddDependenciesStage),
|
||||
title: (ctx) => (ctx.editingVersion.value ? 'Edit dependencies' : 'Add dependencies'),
|
||||
skip: (ctx) => ctx.projectType.value === 'modpack',
|
||||
id: 'metadata',
|
||||
stageContent: markRaw(MetadataStage),
|
||||
title: 'Metadata',
|
||||
leftButtonConfig: (ctx) =>
|
||||
ctx.editingVersion.value
|
||||
? {
|
||||
@@ -25,12 +24,13 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||
},
|
||||
rightButtonConfig: (ctx) =>
|
||||
ctx.editingVersion.value
|
||||
? ctx.saveButtonConfig()
|
||||
? {
|
||||
...ctx.saveButtonConfig(),
|
||||
}
|
||||
: {
|
||||
label: ctx.getNextLabel(),
|
||||
icon: RightArrowIcon,
|
||||
iconPosition: 'after',
|
||||
onClick: () => ctx.modal.value?.nextStage(),
|
||||
},
|
||||
nonProgressStage: (ctx) => ctx.editingVersion.value,
|
||||
}
|
||||
@@ -248,17 +248,33 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
* Build context for an upload request
|
||||
*
|
||||
* Sets metadata.isUpload = true so features can detect uploads.
|
||||
* Supports both single file uploads and FormData uploads.
|
||||
*/
|
||||
protected buildUploadContext(
|
||||
url: string,
|
||||
path: string,
|
||||
options: UploadRequestOptions,
|
||||
): RequestContext {
|
||||
const metadata: UploadMetadata = {
|
||||
let metadata: UploadMetadata
|
||||
let body: File | Blob | FormData
|
||||
|
||||
if ('formData' in options && options.formData) {
|
||||
metadata = {
|
||||
isUpload: true,
|
||||
formData: options.formData,
|
||||
onProgress: options.onProgress,
|
||||
}
|
||||
body = options.formData
|
||||
} else if ('file' in options && options.file) {
|
||||
metadata = {
|
||||
isUpload: true,
|
||||
file: options.file,
|
||||
onProgress: options.onProgress,
|
||||
}
|
||||
body = options.file
|
||||
} else {
|
||||
throw new Error('Upload options must include either file or formData')
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
@@ -266,7 +282,7 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
options: {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: options.file,
|
||||
body,
|
||||
},
|
||||
attempt: 1,
|
||||
startTime: Date.now(),
|
||||
|
||||
@@ -12,9 +12,9 @@ import type { UploadHandle, UploadRequestOptions } from '../types/upload'
|
||||
*/
|
||||
export abstract class AbstractUploadClient {
|
||||
/**
|
||||
* Upload a file with progress tracking
|
||||
* Upload a file or FormData with progress tracking
|
||||
* @param path - API path (e.g., '/fs/create')
|
||||
* @param options - Upload options including file, api, version
|
||||
* @param options - Upload options including file or formData, api, version
|
||||
* @returns UploadHandle with promise, onProgress chain, and cancel method
|
||||
*/
|
||||
abstract upload<T = void>(path: string, options: UploadRequestOptions): UploadHandle<T>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { UploadHandle } from '../../../types/upload'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthVersionsV3Module extends AbstractModule {
|
||||
@@ -136,11 +137,11 @@ export class LabrinthVersionsV3Module extends AbstractModule {
|
||||
* ```
|
||||
*/
|
||||
|
||||
public async createVersion(
|
||||
public createVersion(
|
||||
draftVersion: Labrinth.Versions.v3.DraftVersion,
|
||||
versionFiles: Labrinth.Versions.v3.DraftVersionFile[],
|
||||
projectType: Labrinth.Projects.v2.ProjectType | null = null,
|
||||
): Promise<Labrinth.Versions.v3.Version> {
|
||||
): UploadHandle<Labrinth.Versions.v3.Version> {
|
||||
const formData = new FormData()
|
||||
|
||||
const files = versionFiles.map((vf) => vf.file)
|
||||
@@ -182,21 +183,15 @@ export class LabrinthVersionsV3Module extends AbstractModule {
|
||||
formData.append('data', JSON.stringify(data))
|
||||
|
||||
files.forEach((file, i) => {
|
||||
formData.append(fileParts[i], new Blob([file]), file.name)
|
||||
formData.append(fileParts[i], file, file.name)
|
||||
})
|
||||
|
||||
const newVersion = await this.client.request<Labrinth.Versions.v3.Version>(`/version`, {
|
||||
return this.client.upload<Labrinth.Versions.v3.Version>(`/version`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
formData,
|
||||
timeout: 60 * 5 * 1000,
|
||||
headers: {
|
||||
'Content-Type': '',
|
||||
},
|
||||
})
|
||||
|
||||
return newVersion
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,10 +246,10 @@ export class LabrinthVersionsV3Module extends AbstractModule {
|
||||
})
|
||||
}
|
||||
|
||||
public async addFilesToVersion(
|
||||
public addFilesToVersion(
|
||||
versionId: string,
|
||||
versionFiles: Labrinth.Versions.v3.DraftVersionFile[],
|
||||
): Promise<Labrinth.Versions.v3.Version> {
|
||||
): UploadHandle<Labrinth.Versions.v3.Version> {
|
||||
const formData = new FormData()
|
||||
|
||||
const files = versionFiles.map((vf) => vf.file)
|
||||
@@ -273,18 +268,14 @@ export class LabrinthVersionsV3Module extends AbstractModule {
|
||||
formData.append('data', JSON.stringify({ file_types: fileTypeMap }))
|
||||
|
||||
files.forEach((file, i) => {
|
||||
formData.append(fileParts[i], new Blob([file]), file.name)
|
||||
formData.append(fileParts[i], file, file.name)
|
||||
})
|
||||
|
||||
return this.client.request<Labrinth.Versions.v3.Version>(`/version/${versionId}/file`, {
|
||||
return this.client.upload<Labrinth.Versions.v3.Version>(`/version/${versionId}/file`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
formData,
|
||||
timeout: 60 * 5 * 1000,
|
||||
headers: {
|
||||
'Content-Type': '',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,22 @@ export abstract class XHRUploadClient extends AbstractModrinthClient {
|
||||
|
||||
const url = this.buildUrl(path, baseUrl, options.version)
|
||||
|
||||
// For FormData uploads, don't set Content-Type (let browser set multipart boundary)
|
||||
// For file uploads, use application/octet-stream
|
||||
const isFormData = 'formData' in options && options.formData instanceof FormData
|
||||
const baseHeaders = this.buildDefaultHeaders()
|
||||
// Remove Content-Type for FormData so browser can set multipart/form-data with boundary
|
||||
if (isFormData) {
|
||||
delete baseHeaders['Content-Type']
|
||||
} else {
|
||||
baseHeaders['Content-Type'] = 'application/octet-stream'
|
||||
}
|
||||
|
||||
const mergedOptions: UploadRequestOptions = {
|
||||
retry: false, // default: don't retry uploads
|
||||
...options,
|
||||
headers: {
|
||||
...this.buildDefaultHeaders(),
|
||||
'Content-Type': 'application/octet-stream',
|
||||
...baseHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
}
|
||||
@@ -121,7 +131,9 @@ export abstract class XHRUploadClient extends AbstractModrinthClient {
|
||||
xhr.setRequestHeader(key, value)
|
||||
}
|
||||
|
||||
xhr.send(metadata.file)
|
||||
// Send either FormData or file depending on what was provided
|
||||
const data = 'formData' in metadata ? metadata.formData : metadata.file
|
||||
xhr.send(data)
|
||||
abortController.signal.addEventListener('abort', () => xhr.abort())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,29 +13,66 @@ export interface UploadProgress {
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for upload requests (matches request() style)
|
||||
*
|
||||
* Extends RequestOptions but excludes body and method since those
|
||||
* are determined by the upload itself.
|
||||
* Base options for upload requests
|
||||
*/
|
||||
export interface UploadRequestOptions extends Omit<RequestOptions, 'body' | 'method'> {
|
||||
/** File or Blob to upload */
|
||||
file: File | Blob
|
||||
interface BaseUploadRequestOptions extends Omit<RequestOptions, 'body' | 'method'> {
|
||||
/** Callback for progress updates */
|
||||
onProgress?: (progress: UploadProgress) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata attached to upload contexts
|
||||
* Options for single file upload requests
|
||||
*/
|
||||
export interface FileUploadRequestOptions extends BaseUploadRequestOptions {
|
||||
/** File or Blob to upload */
|
||||
file: File | Blob
|
||||
formData?: never
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for FormData upload requests
|
||||
*
|
||||
* Used for multipart uploads (e.g., version file uploads) that need
|
||||
* to send metadata alongside files.
|
||||
*/
|
||||
export interface FormDataUploadRequestOptions extends BaseUploadRequestOptions {
|
||||
/** FormData containing files and metadata */
|
||||
formData: FormData
|
||||
file?: never
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for upload requests - either a single file or FormData
|
||||
*/
|
||||
export type UploadRequestOptions = FileUploadRequestOptions | FormDataUploadRequestOptions
|
||||
|
||||
/**
|
||||
* Metadata attached to file upload contexts
|
||||
*
|
||||
* Features can check `context.metadata?.isUpload` to detect uploads.
|
||||
*/
|
||||
export interface UploadMetadata extends Record<string, unknown> {
|
||||
export interface FileUploadMetadata extends Record<string, unknown> {
|
||||
isUpload: true
|
||||
file: File | Blob
|
||||
formData?: never
|
||||
onProgress?: (progress: UploadProgress) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata attached to FormData upload contexts
|
||||
*/
|
||||
export interface FormDataUploadMetadata extends Record<string, unknown> {
|
||||
isUpload: true
|
||||
formData: FormData
|
||||
file?: never
|
||||
onProgress?: (progress: UploadProgress) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata attached to upload contexts - either file or FormData
|
||||
*/
|
||||
export type UploadMetadata = FileUploadMetadata | FormDataUploadMetadata
|
||||
|
||||
/**
|
||||
* Handle returned from upload operations
|
||||
*
|
||||
|
||||
@@ -76,6 +76,7 @@ import _FileIcon from './icons/file.svg?component'
|
||||
import _FileArchiveIcon from './icons/file-archive.svg?component'
|
||||
import _FileCodeIcon from './icons/file-code.svg?component'
|
||||
import _FileImageIcon from './icons/file-image.svg?component'
|
||||
import _FilePlusIcon from './icons/file-plus.svg?component'
|
||||
import _FileTextIcon from './icons/file-text.svg?component'
|
||||
import _FilterIcon from './icons/filter.svg?component'
|
||||
import _FilterXIcon from './icons/filter-x.svg?component'
|
||||
@@ -308,6 +309,7 @@ export const FileIcon = _FileIcon
|
||||
export const FileArchiveIcon = _FileArchiveIcon
|
||||
export const FileCodeIcon = _FileCodeIcon
|
||||
export const FileImageIcon = _FileImageIcon
|
||||
export const FilePlusIcon = _FilePlusIcon
|
||||
export const FileTextIcon = _FileTextIcon
|
||||
export const FilterIcon = _FilterIcon
|
||||
export const FilterXIcon = _FilterXIcon
|
||||
|
||||
18
packages/assets/icons/file-plus.svg
Normal file
18
packages/assets/icons/file-plus.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<!-- @license lucide-static v0.562.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-file-plus"
|
||||
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="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||
<path d="M9 15h6" />
|
||||
<path d="M12 18v-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 502 B |
@@ -86,6 +86,7 @@ textarea,
|
||||
|
||||
.cm-content {
|
||||
white-space: pre-wrap !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
|
||||
@@ -6,13 +6,21 @@ import { withThemeByClassName } from '@storybook/addon-themes'
|
||||
import type { Preview } from '@storybook/vue3-vite'
|
||||
import { setup } from '@storybook/vue3-vite'
|
||||
import FloatingVue from 'floating-vue'
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import NotificationPanel from '../src/components/nav/NotificationPanel.vue'
|
||||
import {
|
||||
buildLocaleMessages,
|
||||
createMessageCompiler,
|
||||
type CrowdinMessages,
|
||||
} from '../src/composables/i18n'
|
||||
import {
|
||||
AbstractWebNotificationManager,
|
||||
type NotificationPanelLocation,
|
||||
provideNotificationManager,
|
||||
type WebNotification,
|
||||
} from '../src/providers'
|
||||
|
||||
// Load locale messages from the UI package's locales
|
||||
// @ts-ignore
|
||||
@@ -31,6 +39,42 @@ const i18n = createI18n({
|
||||
messages: buildLocaleMessages(localeModules),
|
||||
})
|
||||
|
||||
class StorybookNotificationManager extends AbstractWebNotificationManager {
|
||||
private readonly state = ref<WebNotification[]>([])
|
||||
private readonly locationState = ref<NotificationPanelLocation>('right')
|
||||
|
||||
public getNotificationLocation(): NotificationPanelLocation {
|
||||
return this.locationState.value
|
||||
}
|
||||
|
||||
public setNotificationLocation(location: NotificationPanelLocation): void {
|
||||
this.locationState.value = location
|
||||
}
|
||||
|
||||
public getNotifications(): WebNotification[] {
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
protected addNotificationToStorage(notification: WebNotification): void {
|
||||
this.state.value.push(notification)
|
||||
}
|
||||
|
||||
protected removeNotificationFromStorage(id: string | number): void {
|
||||
const index = this.state.value.findIndex((n) => n.id === id)
|
||||
if (index > -1) {
|
||||
this.state.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
protected removeNotificationFromStorageByIndex(index: number): void {
|
||||
this.state.value.splice(index, 1)
|
||||
}
|
||||
|
||||
protected clearAllNotificationsFromStorage(): void {
|
||||
this.state.value.splice(0)
|
||||
}
|
||||
}
|
||||
|
||||
setup((app) => {
|
||||
app.use(i18n)
|
||||
app.use(FloatingVue, {
|
||||
@@ -56,6 +100,14 @@ setup((app) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Wrapper component that provides notification manager context
|
||||
const NotificationManagerProvider = defineComponent({
|
||||
setup(_, { slots }) {
|
||||
provideNotificationManager(new StorybookNotificationManager())
|
||||
return () => slots.default?.()
|
||||
},
|
||||
})
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
@@ -74,6 +126,16 @@ const preview: Preview = {
|
||||
},
|
||||
defaultTheme: 'dark',
|
||||
}),
|
||||
// Wrap stories with notification manager provider
|
||||
(story) => ({
|
||||
components: { story, NotificationManagerProvider, NotificationPanel },
|
||||
template: /*html*/ `
|
||||
<NotificationManagerProvider>
|
||||
<NotificationPanel />
|
||||
<story />
|
||||
</NotificationManagerProvider>
|
||||
`,
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { injectNotificationManager } from '../../providers'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
accept: string
|
||||
@@ -27,7 +31,6 @@ const props = withDefaults(
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const dropAreaRef = ref<HTMLDivElement>()
|
||||
const fileAllowed = ref(false)
|
||||
|
||||
const hideDropArea = () => {
|
||||
if (dropAreaRef.value) {
|
||||
@@ -36,30 +39,62 @@ const hideDropArea = () => {
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
hideDropArea()
|
||||
if (event.dataTransfer && event.dataTransfer.files && fileAllowed.value) {
|
||||
emit('change', event.dataTransfer.files)
|
||||
|
||||
const files = event.dataTransfer?.files
|
||||
if (!files || files.length === 0) return
|
||||
|
||||
const file = files[0]
|
||||
|
||||
if (!matchesAccept({ getAsFile: () => file } as DataTransferItem, props.accept)) {
|
||||
addNotification({
|
||||
title: 'Invalid file',
|
||||
text: `The file "${file.name}" is not a valid file type for this project.`,
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
emit('change', files)
|
||||
}
|
||||
|
||||
function matchesAccept(file: DataTransferItem, accept?: string): boolean {
|
||||
if (!accept || accept.trim() === '') return true
|
||||
|
||||
const fileType = file.type // e.g. "image/png"
|
||||
const fileName = file.getAsFile()?.name.toLowerCase() ?? ''
|
||||
|
||||
return accept
|
||||
.split(',')
|
||||
.map((t) => t.trim().toLowerCase())
|
||||
.some((token) => {
|
||||
// .png, .jpg
|
||||
if (token.startsWith('.')) {
|
||||
return fileName.endsWith(token)
|
||||
}
|
||||
|
||||
// image/*
|
||||
if (token.endsWith('/*')) {
|
||||
const base = token.slice(0, -1) // "image/"
|
||||
return fileType.startsWith(base)
|
||||
}
|
||||
|
||||
// image/png
|
||||
return fileType === token
|
||||
})
|
||||
}
|
||||
|
||||
const allowDrag = (event: DragEvent) => {
|
||||
const file = event.dataTransfer?.items[0]
|
||||
if (
|
||||
file &&
|
||||
props.accept
|
||||
.split(',')
|
||||
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === '*', false)
|
||||
) {
|
||||
fileAllowed.value = true
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
const item = event.dataTransfer?.items?.[0]
|
||||
if (!item || item.kind !== 'file') return
|
||||
|
||||
event.preventDefault()
|
||||
event.dataTransfer!.dropEffect = 'copy'
|
||||
|
||||
if (dropAreaRef.value) {
|
||||
dropAreaRef.value.style.visibility = 'visible'
|
||||
}
|
||||
} else {
|
||||
fileAllowed.value = false
|
||||
hideDropArea()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -50,6 +50,10 @@ import { FolderUpIcon } from '@modrinth/assets'
|
||||
import { fileIsValid } from '@modrinth/utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { injectNotificationManager } from '../../providers'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -58,7 +62,6 @@ const emit = defineEmits<{
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
prompt?: string
|
||||
primaryPrompt?: string | null
|
||||
secondaryPrompt?: string | null
|
||||
multiple?: boolean
|
||||
@@ -69,20 +72,58 @@ const props = withDefaults(
|
||||
size?: 'small' | 'standard'
|
||||
}>(),
|
||||
{
|
||||
prompt: 'Drag and drop files or click to browse',
|
||||
primaryPrompt: 'Drag and drop files or click to browse',
|
||||
secondaryPrompt: 'You can try to drag files or folder or click this area to select it',
|
||||
primaryPrompt: 'Drop files here or click to upload',
|
||||
secondaryPrompt: 'Only supported file types will be accepted',
|
||||
size: 'standard',
|
||||
},
|
||||
)
|
||||
|
||||
const files = ref<File[]>([])
|
||||
|
||||
function matchesAccept(file: File, accept?: string): boolean {
|
||||
if (!accept || accept.trim() === '') return true
|
||||
|
||||
const fileType = file.type // e.g. "image/png"
|
||||
const fileName = file.name.toLowerCase()
|
||||
|
||||
return accept
|
||||
.split(',')
|
||||
.map((t) => t.trim().toLowerCase())
|
||||
.some((token) => {
|
||||
// .png, .jpg
|
||||
if (token.startsWith('.')) {
|
||||
return fileName.endsWith(token)
|
||||
}
|
||||
|
||||
// image/*
|
||||
if (token.endsWith('/*')) {
|
||||
const base = token.slice(0, -1) // "image/"
|
||||
return fileType.startsWith(base)
|
||||
}
|
||||
|
||||
// image/png
|
||||
return fileType === token
|
||||
})
|
||||
}
|
||||
|
||||
function addFiles(incoming: FileList, shouldNotReset = false) {
|
||||
if (!shouldNotReset || props.shouldAlwaysReset) {
|
||||
files.value = Array.from(incoming)
|
||||
}
|
||||
|
||||
// Filter out files that don't match the accept prop
|
||||
const invalidFiles = files.value.filter((file) => !matchesAccept(file, props.accept))
|
||||
if (invalidFiles.length > 0) {
|
||||
for (const file of invalidFiles) {
|
||||
addNotification({
|
||||
title: 'Invalid file',
|
||||
text: `The file "${file.name}" is not a valid file type for this project.`,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
files.value = files.value.filter((file) => matchesAccept(file, props.accept))
|
||||
}
|
||||
|
||||
const validationOptions = {
|
||||
maxSize: props.maxSize ?? undefined,
|
||||
alertOnInvalid: true,
|
||||
|
||||
@@ -315,6 +315,7 @@ const props = withDefaults(
|
||||
placeholder?: string
|
||||
maxLength?: number
|
||||
maxHeight?: number
|
||||
minHeight?: number
|
||||
}>(),
|
||||
{
|
||||
modelValue: '',
|
||||
@@ -324,6 +325,7 @@ const props = withDefaults(
|
||||
placeholder: 'Write something...',
|
||||
maxLength: undefined,
|
||||
maxHeight: undefined,
|
||||
minHeight: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -360,9 +362,9 @@ onMounted(() => {
|
||||
border: 'none',
|
||||
},
|
||||
'.cm-content': {
|
||||
minHeight: props.minHeight ? `${props.minHeight}px` : '200px',
|
||||
marginBlockEnd: '0.5rem',
|
||||
padding: '0.5rem',
|
||||
minHeight: '200px',
|
||||
caretColor: 'var(--color-contrast)',
|
||||
width: '100%',
|
||||
},
|
||||
@@ -609,9 +611,9 @@ watch(
|
||||
border: 'none',
|
||||
},
|
||||
'.cm-content': {
|
||||
minHeight: props.minHeight ? `${props.minHeight}px` : '200px',
|
||||
marginBlockEnd: '0.5rem',
|
||||
padding: '0.5rem',
|
||||
minHeight: '200px',
|
||||
caretColor: 'var(--color-contrast)',
|
||||
width: '100%',
|
||||
opacity: newValue ? 0.6 : 1,
|
||||
|
||||
@@ -6,12 +6,55 @@
|
||||
:on-hide="onModalHide"
|
||||
:closable="true"
|
||||
:close-on-click-outside="false"
|
||||
:width="resolvedMaxWidth"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex flex-wrap items-center gap-1 text-secondary">
|
||||
<span class="text-lg font-bold text-contrast sm:text-xl">{{ resolvedTitle }}</span>
|
||||
<div
|
||||
v-if="breadcrumbs && !resolveCtxFn(currentStage.nonProgressStage, context)"
|
||||
class="relative w-full"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-bg-raised to-transparent z-10 transition-opacity duration-200"
|
||||
:class="showLeftShadow ? 'opacity-100' : 'opacity-0'"
|
||||
/>
|
||||
<div
|
||||
ref="breadcrumbScroller"
|
||||
class="flex w-full overflow-x-auto overflow-y-hidden scrollbar-hide pr-6"
|
||||
@wheel.prevent="onBreadcrumbWheel"
|
||||
@scroll="updateScrollShadows"
|
||||
>
|
||||
<template v-for="(stage, index) in breadcrumbStages" :key="stage.id">
|
||||
<div
|
||||
:ref="(el) => setBreadcrumbRef(stage.id, el as HTMLElement | null)"
|
||||
class="flex w-max items-center"
|
||||
>
|
||||
<button
|
||||
class="bg-transparent active:scale-95 font-bold text-secondary p-0 w-max py-3 px-1"
|
||||
:class="{
|
||||
'!text-contrast font-bold': resolveCtxFn(currentStage.id, context) === stage.id,
|
||||
'font-bold': resolveCtxFn(currentStage.id, context) !== stage.id,
|
||||
'opacity-50 cursor-not-allowed': cannotNavigateToStage(index),
|
||||
}"
|
||||
:disabled="cannotNavigateToStage(index)"
|
||||
@click="setStage(stage.id)"
|
||||
>
|
||||
{{ resolveCtxFn(stage.title, context) }}
|
||||
</button>
|
||||
<ChevronRightIcon
|
||||
v-if="index < breadcrumbStages.length - 1"
|
||||
class="h-5 w-5 text-secondary"
|
||||
stroke-width="3"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
class="pointer-events-none absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-bg-raised to-transparent z-10 transition-opacity duration-200"
|
||||
:class="showRightShadow ? 'opacity-100' : 'opacity-0'"
|
||||
/>
|
||||
</div>
|
||||
<span v-else class="text-lg font-bold text-contrast sm:text-xl">{{ resolvedTitle }}</span>
|
||||
</template>
|
||||
|
||||
<progress
|
||||
v-if="nonProgressStage !== true"
|
||||
@@ -58,9 +101,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import type { Component } from 'vue'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
export interface StageButtonConfig {
|
||||
label?: string
|
||||
@@ -79,9 +123,13 @@ export interface StageConfigInput<T> {
|
||||
stageContent: Component
|
||||
title: MaybeCtxFn<T, string>
|
||||
skip?: MaybeCtxFn<T, boolean>
|
||||
hideStageInBreadcrumb?: MaybeCtxFn<T, boolean>
|
||||
nonProgressStage?: MaybeCtxFn<T, boolean>
|
||||
cannotNavigateForward?: MaybeCtxFn<T, boolean>
|
||||
leftButtonConfig: MaybeCtxFn<T, StageButtonConfig | null>
|
||||
rightButtonConfig: MaybeCtxFn<T, StageButtonConfig | null>
|
||||
/** Max width for the modal content and header defined in px (e.g., '460px', '600px'). Defaults to '460px'. */
|
||||
maxWidth?: MaybeCtxFn<T, string>
|
||||
}
|
||||
|
||||
export function resolveCtxFn<T, R>(value: MaybeCtxFn<T, R>, ctx: T): R {
|
||||
@@ -93,6 +141,8 @@ export function resolveCtxFn<T, R>(value: MaybeCtxFn<T, R>, ctx: T): R {
|
||||
const props = defineProps<{
|
||||
stages: StageConfigInput<T>[]
|
||||
context: T
|
||||
breadcrumbs?: boolean
|
||||
fitContent?: boolean
|
||||
}>()
|
||||
|
||||
const modal = useTemplateRef<InstanceType<typeof NewModal>>('modal')
|
||||
@@ -178,6 +228,12 @@ const nonProgressStage = computed(() => {
|
||||
return resolveCtxFn(stage.nonProgressStage, props.context)
|
||||
})
|
||||
|
||||
const resolvedMaxWidth = computed(() => {
|
||||
const stage = currentStage.value
|
||||
if (!stage?.maxWidth) return '560px'
|
||||
return resolveCtxFn(stage.maxWidth, props.context)
|
||||
})
|
||||
|
||||
const progressValue = computed(() => {
|
||||
const isProgressStage = (stage: StageConfigInput<T>) => {
|
||||
if (resolveCtxFn(stage.nonProgressStage, props.context)) return false
|
||||
@@ -193,6 +249,99 @@ const progressValue = computed(() => {
|
||||
return totalCount > 0 ? (completedCount / totalCount) * 100 : 0
|
||||
})
|
||||
|
||||
const breadcrumbScroller = ref<HTMLElement | null>(null)
|
||||
const breadcrumbRefs = ref<Map<string, HTMLElement>>(new Map())
|
||||
const showLeftShadow = ref(false)
|
||||
const showRightShadow = ref(false)
|
||||
|
||||
function setBreadcrumbRef(stageId: string, el: HTMLElement | null) {
|
||||
if (el) breadcrumbRefs.value.set(stageId, el)
|
||||
else breadcrumbRefs.value.delete(stageId)
|
||||
}
|
||||
|
||||
function scrollToCurrentBreadcrumb() {
|
||||
const stage = currentStage.value
|
||||
if (!stage || !breadcrumbScroller.value) return
|
||||
|
||||
const el = breadcrumbRefs.value.get(stage.id)
|
||||
if (!el) return
|
||||
|
||||
nextTick(() => {
|
||||
breadcrumbScroller.value?.scrollTo({
|
||||
left: el.offsetLeft - 50,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function updateScrollShadows() {
|
||||
const el = breadcrumbScroller.value
|
||||
if (!el) {
|
||||
showLeftShadow.value = false
|
||||
showRightShadow.value = false
|
||||
return
|
||||
}
|
||||
|
||||
showLeftShadow.value = el.scrollLeft > 0
|
||||
showRightShadow.value = el.scrollLeft < el.scrollWidth - el.clientWidth - 1
|
||||
}
|
||||
|
||||
function onBreadcrumbWheel(e: WheelEvent) {
|
||||
if (!breadcrumbScroller.value) return
|
||||
|
||||
const el = breadcrumbScroller.value
|
||||
const canScrollHorizontally = el.scrollWidth > el.clientWidth
|
||||
|
||||
if (canScrollHorizontally) {
|
||||
// Support both horizontal and vertical scroll input
|
||||
const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY
|
||||
el.scrollLeft += delta
|
||||
}
|
||||
}
|
||||
|
||||
// Stages that are not skipped (visible in breadcrumbs)
|
||||
const breadcrumbStages = computed(() => {
|
||||
return props.stages.filter((stage) => {
|
||||
const visibleStep =
|
||||
!resolveCtxFn(stage.skip, props.context) &&
|
||||
!resolveCtxFn(stage.nonProgressStage, props.context) &&
|
||||
!resolveCtxFn(stage.hideStageInBreadcrumb, props.context)
|
||||
return visibleStep
|
||||
})
|
||||
})
|
||||
|
||||
// Check if navigation to a breadcrumb stage is allowed
|
||||
// Navigation backwards is always allowed, but forward navigation requires all intermediate stages to allow it
|
||||
function cannotNavigateToStage(breadcrumbIndex: number): boolean {
|
||||
const targetStage = breadcrumbStages.value[breadcrumbIndex]
|
||||
if (!targetStage) return false
|
||||
|
||||
const targetStageIndex = props.stages.findIndex((s) => s.id === targetStage.id)
|
||||
if (targetStageIndex === -1) return false
|
||||
|
||||
// Always allow navigating to current or previous stages
|
||||
if (targetStageIndex <= currentStageIndex.value) return false
|
||||
|
||||
// For forward navigation, check all stages between current and target
|
||||
for (let i = currentStageIndex.value; i < targetStageIndex; i++) {
|
||||
const stage = props.stages[i]
|
||||
if (stage.skip && resolveCtxFn(stage.skip, props.context)) continue
|
||||
if (resolveCtxFn(stage.cannotNavigateForward, props.context)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
watch([breadcrumbStages, currentStageIndex], () => nextTick(() => updateScrollShadows()), {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
watch(currentStageIndex, () => {
|
||||
scrollToCurrentBreadcrumb()
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'refresh-data' | 'hide'): void
|
||||
}>()
|
||||
@@ -228,4 +377,13 @@ progress::-webkit-progress-value {
|
||||
progress::-moz-progress-bar {
|
||||
@apply bg-contrast;
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,12 +20,19 @@
|
||||
]"
|
||||
@click="() => (closeOnClickOutside && closable ? hide() : {})"
|
||||
/>
|
||||
<div class="modal-container experimental-styles-within" :class="{ shown: visible }">
|
||||
<div
|
||||
class="modal-container experimental-styles-within"
|
||||
:class="{ shown: visible }"
|
||||
:style="{
|
||||
'--_max-width': maxWidth,
|
||||
'--_width': width,
|
||||
}"
|
||||
>
|
||||
<div class="modal-body flex flex-col bg-bg-raised rounded-2xl">
|
||||
<div
|
||||
v-if="!hideHeader"
|
||||
data-tauri-drag-region
|
||||
class="grid grid-cols-[auto_min-content] items-center gap-12 p-6 border-solid border-0 border-b-[1px] border-divider max-w-full"
|
||||
class="grid grid-cols-[auto_min-content] items-center gap-4 p-6 border-solid border-0 border-b-[1px] border-divider max-w-full"
|
||||
>
|
||||
<div class="flex text-wrap break-words items-center gap-3 min-w-0">
|
||||
<slot name="title">
|
||||
@@ -130,6 +137,10 @@ const props = withDefaults(
|
||||
mergeHeader?: boolean
|
||||
scrollable?: boolean
|
||||
maxContentHeight?: string
|
||||
/** Max width for the modal (e.g., '460px', '600px'). Defaults to '60rem'. */
|
||||
maxWidth?: string
|
||||
/** Width for the modal body (e.g., '460px', '600px'). */
|
||||
width?: string
|
||||
}>(),
|
||||
{
|
||||
type: true,
|
||||
@@ -147,6 +158,8 @@ const props = withDefaults(
|
||||
// TODO: migrate all modals to use scrollable and remove this prop
|
||||
scrollable: false,
|
||||
maxContentHeight: '70vh',
|
||||
maxWidth: undefined,
|
||||
width: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -315,7 +328,7 @@ function handleKeyDown(event: KeyboardEvent) {
|
||||
max-width: min(var(--_max-width, 60rem), calc(100% - 2 * var(--gap-lg)));
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
width: fit-content;
|
||||
width: var(--_width, fit-content);
|
||||
pointer-events: auto;
|
||||
scale: 0.97;
|
||||
|
||||
|
||||
@@ -155,25 +155,19 @@
|
||||
</TagItem>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasMultipleEnvironments"
|
||||
v-tooltip="
|
||||
ENVIRONMENTS_COPY[version.environment || 'unknown']?.description
|
||||
? formatMessage(ENVIRONMENTS_COPY[version.environment || 'unknown'].description)
|
||||
: undefined
|
||||
"
|
||||
class="flex items-center"
|
||||
<div v-if="hasMultipleEnvironments" class="flex items-center">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<TagItem
|
||||
v-for="(tag, tagIdx) in getEnvironmentTags(version.environment)"
|
||||
:key="`env-tag-${tagIdx}`"
|
||||
class="z-[1] text-center"
|
||||
>
|
||||
<TagItem class="z-[1] text-center">
|
||||
<component :is="ENVIRONMENTS_COPY[version.environment || 'unknown']?.icon" />
|
||||
{{
|
||||
ENVIRONMENTS_COPY[version.environment || 'unknown']?.title
|
||||
? formatMessage(ENVIRONMENTS_COPY[version.environment || 'unknown'].title)
|
||||
: ''
|
||||
}}
|
||||
<component :is="tag.icon" />
|
||||
{{ formatMessage(tag.label) }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col justify-center gap-1 max-sm:flex-row max-sm:justify-start max-sm:gap-3 xl:contents"
|
||||
>
|
||||
@@ -198,7 +192,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start justify-end gap-1 sm:items-center z-[1]">
|
||||
<div
|
||||
class="flex items-start justify-end gap-1 sm:items-center z-[1] max-[400px]:flex-col max-[400px]:justify-start"
|
||||
>
|
||||
<slot name="actions" :version="version"></slot>
|
||||
</div>
|
||||
<div v-if="showFiles" class="tag-list pointer-events-none relative z-[1] col-span-full">
|
||||
@@ -244,7 +240,7 @@ import { commonMessages } from '../../utils/common-messages'
|
||||
import AutoLink from '../base/AutoLink.vue'
|
||||
import TagItem from '../base/TagItem.vue'
|
||||
import { Pagination, VersionChannelIndicator, VersionFilterControl } from '../index'
|
||||
import { ENVIRONMENTS_COPY } from './settings/environment/environments'
|
||||
import { getEnvironmentTags } from './settings/environment/environments'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
<section v-if="showEnvironments" class="flex flex-col gap-2">
|
||||
<h3 class="text-primary text-base m-0">{{ formatMessage(messages.environments) }}</h3>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<TagItem v-for="tag in primaryEnvironmentTags" :key="`environment-tag-${tag.message.id}`">
|
||||
<TagItem v-for="(tag, tagIdx) in primaryEnvironmentTags" :key="`environment-tag-${tagIdx}`">
|
||||
<component :is="tag.icon" />
|
||||
{{ formatMessage(tag.message) }}
|
||||
{{ formatMessage(tag.label) }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</section>
|
||||
@@ -88,16 +88,12 @@
|
||||
import { ClientIcon, MonitorSmartphoneIcon, ServerIcon, UserIcon } from '@modrinth/assets'
|
||||
import type { EnvironmentV3, GameVersionTag, PlatformTag, ProjectV3Partial } from '@modrinth/utils'
|
||||
import { formatCategory, getVersionsToDisplay } from '@modrinth/utils'
|
||||
import { type Component, computed } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
defineMessage,
|
||||
defineMessages,
|
||||
type MessageDescriptor,
|
||||
useVIntl,
|
||||
} from '../../composables/i18n'
|
||||
import { defineMessages, useVIntl } from '../../composables/i18n'
|
||||
import TagItem from '../base/TagItem.vue'
|
||||
import { getEnvironmentTags } from './settings/environment/environments'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const router = useRouter()
|
||||
@@ -133,82 +129,8 @@ const primaryEnvironment = computed<EnvironmentV3 | undefined>(() =>
|
||||
props.v3Metadata?.environment?.find((x) => x !== 'unknown'),
|
||||
)
|
||||
|
||||
type EnvironmentTag = {
|
||||
icon: Component
|
||||
message: MessageDescriptor
|
||||
environments: EnvironmentV3[]
|
||||
}
|
||||
|
||||
const environmentTags: EnvironmentTag[] = [
|
||||
{
|
||||
icon: ClientIcon,
|
||||
message: defineMessage({
|
||||
id: `project.about.compatibility.environments.client-side`,
|
||||
defaultMessage: 'Client-side',
|
||||
}),
|
||||
environments: [
|
||||
'client_only',
|
||||
'client_only_server_optional',
|
||||
'client_or_server',
|
||||
'client_or_server_prefers_both',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: ServerIcon,
|
||||
message: defineMessage({
|
||||
id: `project.about.compatibility.environments.server-side`,
|
||||
defaultMessage: 'Server-side',
|
||||
}),
|
||||
environments: [
|
||||
'server_only',
|
||||
'server_only_client_optional',
|
||||
'client_or_server',
|
||||
'client_or_server_prefers_both',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: ServerIcon,
|
||||
message: defineMessage({
|
||||
id: `project.about.compatibility.environments.dedicated-servers-only`,
|
||||
defaultMessage: 'Dedicated servers only',
|
||||
}),
|
||||
environments: ['dedicated_server_only'],
|
||||
},
|
||||
{
|
||||
icon: UserIcon,
|
||||
message: defineMessage({
|
||||
id: `project.about.compatibility.environments.singleplayer-only`,
|
||||
defaultMessage: 'Singleplayer only',
|
||||
}),
|
||||
environments: ['singleplayer_only'],
|
||||
},
|
||||
{
|
||||
icon: UserIcon,
|
||||
message: defineMessage({
|
||||
id: `project.about.compatibility.environments.singleplayer`,
|
||||
defaultMessage: 'Singleplayer',
|
||||
}),
|
||||
environments: ['server_only'],
|
||||
},
|
||||
{
|
||||
icon: MonitorSmartphoneIcon,
|
||||
message: defineMessage({
|
||||
id: `project.about.compatibility.environments.client-and-server`,
|
||||
defaultMessage: 'Client and server',
|
||||
}),
|
||||
environments: [
|
||||
'client_and_server',
|
||||
'client_only_server_optional',
|
||||
'server_only_client_optional',
|
||||
'client_or_server_prefers_both',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const primaryEnvironmentTags = computed(() => {
|
||||
return primaryEnvironment.value
|
||||
? environmentTags.filter((x) => x.environments.includes(primaryEnvironment.value ?? 'unknown'))
|
||||
: []
|
||||
return getEnvironmentTags(primaryEnvironment.value)
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ClientIcon, MonitorSmartphoneIcon, ServerIcon, UserIcon } from '@modrinth/assets'
|
||||
import { ClientIcon, ServerIcon, UserIcon } from '@modrinth/assets'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import { defineMessage, type MessageDescriptor } from '../../../../composables/i18n'
|
||||
|
||||
export const ENVIRONMENTS_COPY: Record<
|
||||
Labrinth.Projects.v3.Environment,
|
||||
{ title: MessageDescriptor; description: MessageDescriptor; icon?: Component }
|
||||
{
|
||||
title: MessageDescriptor
|
||||
description: MessageDescriptor
|
||||
}
|
||||
> = {
|
||||
client_only: {
|
||||
title: defineMessage({
|
||||
@@ -18,19 +21,17 @@ export const ENVIRONMENTS_COPY: Record<
|
||||
defaultMessage:
|
||||
'All functionality is done client-side and is compatible with vanilla servers.',
|
||||
}),
|
||||
icon: ClientIcon,
|
||||
},
|
||||
server_only: {
|
||||
title: defineMessage({
|
||||
id: 'project.environment.server-only.title',
|
||||
defaultMessage: 'Server-side only',
|
||||
defaultMessage: 'Server-side only, works in singleplayer too',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'project.environment.server-only.description',
|
||||
defaultMessage:
|
||||
'All functionality is done server-side and is compatible with vanilla clients.',
|
||||
}),
|
||||
icon: ServerIcon,
|
||||
},
|
||||
singleplayer_only: {
|
||||
title: defineMessage({
|
||||
@@ -42,7 +43,6 @@ export const ENVIRONMENTS_COPY: Record<
|
||||
defaultMessage:
|
||||
'Only functions in Singleplayer or when not connected to a Multiplayer server.',
|
||||
}),
|
||||
icon: UserIcon,
|
||||
},
|
||||
dedicated_server_only: {
|
||||
title: defineMessage({
|
||||
@@ -54,67 +54,61 @@ export const ENVIRONMENTS_COPY: Record<
|
||||
defaultMessage:
|
||||
'All functionality is done server-side and is compatible with vanilla clients.',
|
||||
}),
|
||||
icon: ServerIcon,
|
||||
},
|
||||
client_and_server: {
|
||||
title: defineMessage({
|
||||
id: 'project.environment.client-and-server.title',
|
||||
defaultMessage: 'Client and server',
|
||||
defaultMessage: 'Client and server, required on both',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'project.environment.client-and-server.description',
|
||||
defaultMessage:
|
||||
'Has some functionality on both the client and server, even if only partially.',
|
||||
}),
|
||||
icon: MonitorSmartphoneIcon,
|
||||
},
|
||||
client_only_server_optional: {
|
||||
title: defineMessage({
|
||||
id: 'project.environment.client-only-server-optional.title',
|
||||
defaultMessage: 'Client and server',
|
||||
defaultMessage: 'Client and server, optional on server',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'project.environment.client-only-server-optional.description',
|
||||
defaultMessage:
|
||||
'Has some functionality on both the client and server, even if only partially.',
|
||||
}),
|
||||
icon: MonitorSmartphoneIcon,
|
||||
},
|
||||
server_only_client_optional: {
|
||||
title: defineMessage({
|
||||
id: 'project.environment.server-only-client-optional.title',
|
||||
defaultMessage: 'Client and server',
|
||||
defaultMessage: 'Client and server, optional on client',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'project.environment.server-only-client-optional.description',
|
||||
defaultMessage:
|
||||
'Has some functionality on both the client and server, even if only partially.',
|
||||
}),
|
||||
icon: MonitorSmartphoneIcon,
|
||||
},
|
||||
client_or_server: {
|
||||
title: defineMessage({
|
||||
id: 'project.environment.client-or-server.title',
|
||||
defaultMessage: 'Client and server',
|
||||
defaultMessage: 'Client and server, optional on both',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'project.environment.client-or-server.description',
|
||||
defaultMessage:
|
||||
'Has some functionality on both the client and server, even if only partially.',
|
||||
}),
|
||||
icon: MonitorSmartphoneIcon,
|
||||
},
|
||||
client_or_server_prefers_both: {
|
||||
title: defineMessage({
|
||||
id: 'project.environment.client-or-server-prefers-both.title',
|
||||
defaultMessage: 'Client and server',
|
||||
defaultMessage: 'Client and server, best when installed on both',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: 'project.environment.client-or-server-prefers-both.description',
|
||||
defaultMessage:
|
||||
'Has some functionality on both the client and server, even if only partially.',
|
||||
}),
|
||||
icon: MonitorSmartphoneIcon,
|
||||
},
|
||||
unknown: {
|
||||
title: defineMessage({
|
||||
@@ -127,3 +121,91 @@ export const ENVIRONMENTS_COPY: Record<
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const ENVIRONMENT_TAG_LABELS = {
|
||||
client: defineMessage({
|
||||
id: 'project.environment.tag.client',
|
||||
defaultMessage: 'Client',
|
||||
}),
|
||||
server: defineMessage({
|
||||
id: 'project.environment.tag.server',
|
||||
defaultMessage: 'Server',
|
||||
}),
|
||||
singleplayer: defineMessage({
|
||||
id: 'project.environment.tag.singleplayer',
|
||||
defaultMessage: 'Singleplayer',
|
||||
}),
|
||||
clientOptional: defineMessage({
|
||||
id: 'project.environment.tag.client-optional',
|
||||
defaultMessage: 'Client optional',
|
||||
}),
|
||||
serverOptional: defineMessage({
|
||||
id: 'project.environment.tag.server-optional',
|
||||
defaultMessage: 'Server optional',
|
||||
}),
|
||||
unknown: defineMessage({
|
||||
id: 'project.environment.tag.unknown',
|
||||
defaultMessage: 'Unknown',
|
||||
}),
|
||||
notApplicable: defineMessage({
|
||||
id: 'project.environment.tag.not-applicable',
|
||||
defaultMessage: 'N/A',
|
||||
}),
|
||||
} as const
|
||||
|
||||
export function getEnvironmentTags(
|
||||
environment?: Labrinth.Projects.v3.Environment,
|
||||
): Array<{ icon: Component | null; label: MessageDescriptor }> {
|
||||
switch (environment) {
|
||||
case 'client_only':
|
||||
return [{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.client }]
|
||||
|
||||
case 'server_only':
|
||||
return [
|
||||
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.server },
|
||||
{ icon: UserIcon, label: ENVIRONMENT_TAG_LABELS.singleplayer },
|
||||
]
|
||||
|
||||
case 'singleplayer_only':
|
||||
return [{ icon: UserIcon, label: ENVIRONMENT_TAG_LABELS.singleplayer }]
|
||||
|
||||
case 'dedicated_server_only':
|
||||
return [{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.server }]
|
||||
|
||||
case 'client_and_server':
|
||||
return [
|
||||
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.client },
|
||||
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.server },
|
||||
]
|
||||
|
||||
case 'client_only_server_optional':
|
||||
return [
|
||||
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.client },
|
||||
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.serverOptional },
|
||||
]
|
||||
|
||||
case 'server_only_client_optional':
|
||||
return [
|
||||
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.server },
|
||||
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientOptional },
|
||||
]
|
||||
|
||||
case 'client_or_server':
|
||||
return [
|
||||
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientOptional },
|
||||
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.serverOptional },
|
||||
]
|
||||
|
||||
case 'client_or_server_prefers_both':
|
||||
return [
|
||||
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientOptional },
|
||||
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.serverOptional },
|
||||
]
|
||||
|
||||
case 'unknown':
|
||||
return [{ label: ENVIRONMENT_TAG_LABELS.unknown, icon: null }]
|
||||
|
||||
default:
|
||||
return [{ label: ENVIRONMENT_TAG_LABELS.notApplicable, icon: null }]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,24 +593,6 @@
|
||||
"project.about.compatibility.environments": {
|
||||
"defaultMessage": "Supported environments"
|
||||
},
|
||||
"project.about.compatibility.environments.client-and-server": {
|
||||
"defaultMessage": "Client and server"
|
||||
},
|
||||
"project.about.compatibility.environments.client-side": {
|
||||
"defaultMessage": "Client-side"
|
||||
},
|
||||
"project.about.compatibility.environments.dedicated-servers-only": {
|
||||
"defaultMessage": "Dedicated servers only"
|
||||
},
|
||||
"project.about.compatibility.environments.server-side": {
|
||||
"defaultMessage": "Server-side"
|
||||
},
|
||||
"project.about.compatibility.environments.singleplayer": {
|
||||
"defaultMessage": "Singleplayer"
|
||||
},
|
||||
"project.about.compatibility.environments.singleplayer-only": {
|
||||
"defaultMessage": "Singleplayer only"
|
||||
},
|
||||
"project.about.compatibility.game.minecraftJava": {
|
||||
"defaultMessage": "Minecraft: Java Edition"
|
||||
},
|
||||
@@ -681,13 +663,13 @@
|
||||
"defaultMessage": "Has some functionality on both the client and server, even if only partially."
|
||||
},
|
||||
"project.environment.client-and-server.title": {
|
||||
"defaultMessage": "Client and server"
|
||||
"defaultMessage": "Client and server, required on both"
|
||||
},
|
||||
"project.environment.client-only-server-optional.description": {
|
||||
"defaultMessage": "Has some functionality on both the client and server, even if only partially."
|
||||
},
|
||||
"project.environment.client-only-server-optional.title": {
|
||||
"defaultMessage": "Client and server"
|
||||
"defaultMessage": "Client and server, optional on server"
|
||||
},
|
||||
"project.environment.client-only.description": {
|
||||
"defaultMessage": "All functionality is done client-side and is compatible with vanilla servers."
|
||||
@@ -699,13 +681,13 @@
|
||||
"defaultMessage": "Has some functionality on both the client and server, even if only partially."
|
||||
},
|
||||
"project.environment.client-or-server-prefers-both.title": {
|
||||
"defaultMessage": "Client and server"
|
||||
"defaultMessage": "Client and server, best when installed on both"
|
||||
},
|
||||
"project.environment.client-or-server.description": {
|
||||
"defaultMessage": "Has some functionality on both the client and server, even if only partially."
|
||||
},
|
||||
"project.environment.client-or-server.title": {
|
||||
"defaultMessage": "Client and server"
|
||||
"defaultMessage": "Client and server, optional on both"
|
||||
},
|
||||
"project.environment.dedicated-server-only.description": {
|
||||
"defaultMessage": "All functionality is done server-side and is compatible with vanilla clients."
|
||||
@@ -717,13 +699,13 @@
|
||||
"defaultMessage": "Has some functionality on both the client and server, even if only partially."
|
||||
},
|
||||
"project.environment.server-only-client-optional.title": {
|
||||
"defaultMessage": "Client and server"
|
||||
"defaultMessage": "Client and server, optional on client"
|
||||
},
|
||||
"project.environment.server-only.description": {
|
||||
"defaultMessage": "All functionality is done server-side and is compatible with vanilla clients."
|
||||
},
|
||||
"project.environment.server-only.title": {
|
||||
"defaultMessage": "Server-side only"
|
||||
"defaultMessage": "Server-side only, works in singleplayer too"
|
||||
},
|
||||
"project.environment.singleplayer-only.description": {
|
||||
"defaultMessage": "Only functions in Singleplayer or when not connected to a Multiplayer server."
|
||||
@@ -731,6 +713,27 @@
|
||||
"project.environment.singleplayer-only.title": {
|
||||
"defaultMessage": "Singleplayer only"
|
||||
},
|
||||
"project.environment.tag.client": {
|
||||
"defaultMessage": "Client"
|
||||
},
|
||||
"project.environment.tag.client-optional": {
|
||||
"defaultMessage": "Client optional"
|
||||
},
|
||||
"project.environment.tag.not-applicable": {
|
||||
"defaultMessage": "N/A"
|
||||
},
|
||||
"project.environment.tag.server": {
|
||||
"defaultMessage": "Server"
|
||||
},
|
||||
"project.environment.tag.server-optional": {
|
||||
"defaultMessage": "Server optional"
|
||||
},
|
||||
"project.environment.tag.singleplayer": {
|
||||
"defaultMessage": "Singleplayer"
|
||||
},
|
||||
"project.environment.tag.unknown": {
|
||||
"defaultMessage": "Unknown"
|
||||
},
|
||||
"project.environment.unknown.description": {
|
||||
"defaultMessage": "The environment for this version could not be determined."
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ export default meta
|
||||
export const Default: StoryObj = {
|
||||
render: () => ({
|
||||
components: { DropArea },
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<DropArea accept="*" @change="(files) => console.log('Files dropped:', files)">
|
||||
<div class="p-8 border-2 border-dashed border-divider rounded-lg text-center">
|
||||
<p class="text-secondary">Drag and drop files anywhere on the page</p>
|
||||
@@ -26,7 +26,7 @@ export const Default: StoryObj = {
|
||||
export const ImagesOnly: StoryObj = {
|
||||
render: () => ({
|
||||
components: { DropArea },
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<DropArea accept="image/*" @change="(files) => console.log('Images dropped:', files)">
|
||||
<div class="p-8 border-2 border-dashed border-divider rounded-lg text-center">
|
||||
<p class="text-secondary">Drop images here</p>
|
||||
@@ -36,3 +36,37 @@ export const ImagesOnly: StoryObj = {
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AcceptMods: StoryObj = {
|
||||
render: () => ({
|
||||
components: { DropArea },
|
||||
template: /*html*/ `
|
||||
<DropArea
|
||||
accept=".jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip,.sig,.asc,.gpg,application/pgp-signature,application/pgp-keys"
|
||||
@change="(files) => console.log('Mod files dropped:', files)"
|
||||
>
|
||||
<div class="p-8 border-2 border-dashed border-divider rounded-lg text-center">
|
||||
<p class="text-secondary">Drop mod files here</p>
|
||||
<p class="text-sm text-secondary mt-2">Accepts .jar, .zip, .litemod, and signature files (.sig, .asc, .gpg)</p>
|
||||
</div>
|
||||
</DropArea>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AcceptImages: StoryObj = {
|
||||
render: () => ({
|
||||
components: { DropArea },
|
||||
template: /*html*/ `
|
||||
<DropArea
|
||||
accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml"
|
||||
@change="(files) => console.log('Image files dropped:', files)"
|
||||
>
|
||||
<div class="p-8 border-2 border-dashed border-divider rounded-lg text-center">
|
||||
<p class="text-secondary">Drop image files here</p>
|
||||
<p class="text-sm text-secondary mt-2">Accepts PNG, JPEG, GIF, WebP, and SVG images</p>
|
||||
</div>
|
||||
</DropArea>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ export const acceptFileFromProjectType = (projectType) => {
|
||||
case 'shader':
|
||||
return `.zip,application/zip,${commonTypes}`
|
||||
case 'datapack':
|
||||
return `.zip,application/zip,${commonTypes}`
|
||||
return `.jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip,${commonTypes}`
|
||||
case 'modpack':
|
||||
return `.mrpack,application/x-modrinth-modpack+zip,application/zip,${commonTypes}`
|
||||
default:
|
||||
|
||||
253
pnpm-lock.yaml
generated
253
pnpm-lock.yaml
generated
@@ -152,7 +152,7 @@ importers:
|
||||
devDependencies:
|
||||
'@eslint/compat':
|
||||
specifier: ^1.1.1
|
||||
version: 1.4.1(eslint@9.39.2(jiti@1.21.7))
|
||||
version: 1.4.1(eslint@9.39.2(jiti@2.6.1))
|
||||
'@formatjs/cli':
|
||||
specifier: ^6.2.12
|
||||
version: 6.8.8(@vue/compiler-core@3.5.26)(vue@3.5.26(typescript@5.9.3))
|
||||
@@ -161,22 +161,22 @@ importers:
|
||||
version: link:../../packages/tooling-config
|
||||
'@nuxt/eslint-config':
|
||||
specifier: ^0.5.6
|
||||
version: 0.5.7(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
version: 0.5.7(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@taijased/vue-render-tracker':
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(vue@3.5.26(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3(vite@6.4.1(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
||||
version: 6.0.3(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
||||
autoprefixer:
|
||||
specifier: ^10.4.19
|
||||
version: 10.4.23(postcss@8.5.6)
|
||||
eslint:
|
||||
specifier: ^9.9.1
|
||||
version: 9.39.2(jiti@1.21.7)
|
||||
version: 9.39.2(jiti@2.6.1)
|
||||
eslint-plugin-turbo:
|
||||
specifier: ^2.5.4
|
||||
version: 2.7.2(eslint@9.39.2(jiti@1.21.7))(turbo@2.7.2)
|
||||
version: 2.7.2(eslint@9.39.2(jiti@2.6.1))(turbo@2.7.2)
|
||||
postcss:
|
||||
specifier: ^8.4.39
|
||||
version: 8.5.6
|
||||
@@ -194,7 +194,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^6.0.0
|
||||
version: 6.4.1(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
version: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vue-component-type-helpers:
|
||||
specifier: ^3.1.8
|
||||
version: 3.2.1
|
||||
@@ -375,9 +375,15 @@ importers:
|
||||
'@types/iso-3166-2':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
'@types/js-yaml':
|
||||
specifier: ^4.0.9
|
||||
version: 4.0.9
|
||||
'@types/node':
|
||||
specifier: ^20.1.0
|
||||
version: 20.19.27
|
||||
'@types/semver':
|
||||
specifier: ^7.7.1
|
||||
version: 7.7.1
|
||||
autoprefixer:
|
||||
specifier: ^10.4.19
|
||||
version: 10.4.23(postcss@8.5.6)
|
||||
@@ -386,7 +392,7 @@ importers:
|
||||
version: 10.5.0
|
||||
nuxt:
|
||||
specifier: ^3.20.2
|
||||
version: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2)
|
||||
version: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2)
|
||||
postcss:
|
||||
specifier: ^8.4.39
|
||||
version: 8.5.6
|
||||
@@ -3731,6 +3737,9 @@ packages:
|
||||
'@types/sax@1.2.7':
|
||||
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
|
||||
|
||||
'@types/semver@7.7.1':
|
||||
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
||||
|
||||
'@types/stats.js@0.17.4':
|
||||
resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
|
||||
|
||||
@@ -10192,11 +10201,11 @@ snapshots:
|
||||
|
||||
'@eslint-community/regexpp@4.12.2': {}
|
||||
|
||||
'@eslint/compat@1.4.1(eslint@9.39.2(jiti@1.21.7))':
|
||||
'@eslint/compat@1.4.1(eslint@9.39.2(jiti@2.6.1))':
|
||||
dependencies:
|
||||
'@eslint/core': 0.17.0
|
||||
optionalDependencies:
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
|
||||
'@eslint/config-array@0.21.1':
|
||||
dependencies:
|
||||
@@ -10586,9 +10595,9 @@ snapshots:
|
||||
|
||||
'@intlify/shared@11.2.8': {}
|
||||
|
||||
'@intlify/unplugin-vue-i18n@6.0.8(@vue/compiler-dom@3.5.26)(eslint@9.39.2(jiti@2.6.1))(rollup@4.54.0)(typescript@5.9.3)(vue-i18n@10.0.8(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))':
|
||||
'@intlify/unplugin-vue-i18n@6.0.8(@vue/compiler-dom@3.5.26)(eslint@9.39.2(jiti@1.21.7))(rollup@4.54.0)(typescript@5.9.3)(vue-i18n@10.0.8(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
|
||||
'@intlify/bundle-utils': 10.0.1(vue-i18n@10.0.8(vue@3.5.26(typescript@5.9.3)))
|
||||
'@intlify/shared': 11.2.8
|
||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.2.8)(@vue/compiler-dom@3.5.26)(vue-i18n@10.0.8(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
|
||||
@@ -10843,7 +10852,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@nuxt/kit': 4.2.2(magicast@0.5.1)
|
||||
execa: 8.0.1
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
@@ -10888,7 +10897,7 @@ snapshots:
|
||||
sirv: 3.0.2
|
||||
structured-clone-es: 1.0.0
|
||||
tinyglobby: 0.2.15
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite-plugin-inspect: 11.3.3(@nuxt/kit@4.2.2(magicast@0.5.1))(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))
|
||||
vite-plugin-vue-tracer: 1.2.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
||||
which: 5.0.0
|
||||
@@ -10899,36 +10908,36 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@nuxt/eslint-config@0.5.7(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||
'@nuxt/eslint-config@0.5.7(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint/js': 9.39.2
|
||||
'@nuxt/eslint-plugin': 0.5.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
'@stylistic/eslint-plugin': 2.13.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint-config-flat-gitignore: 0.3.0(eslint@9.39.2(jiti@1.21.7))
|
||||
'@nuxt/eslint-plugin': 0.5.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@stylistic/eslint-plugin': 2.13.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-config-flat-gitignore: 0.3.0(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-flat-config-utils: 0.4.0
|
||||
eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-jsdoc: 50.8.0(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-regexp: 2.10.0(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-unicorn: 55.0.0(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-vue: 9.33.0(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-jsdoc: 50.8.0(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-regexp: 2.10.0(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-unicorn: 55.0.0(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-vue: 9.33.0(eslint@9.39.2(jiti@2.6.1))
|
||||
globals: 15.15.0
|
||||
local-pkg: 0.5.1
|
||||
pathe: 1.1.2
|
||||
vue-eslint-parser: 9.4.3(eslint@9.39.2(jiti@1.21.7))
|
||||
vue-eslint-parser: 9.4.3(eslint@9.39.2(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- '@typescript-eslint/utils'
|
||||
- eslint-import-resolver-node
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@nuxt/eslint-plugin@0.5.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||
'@nuxt/eslint-plugin@0.5.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.51.0
|
||||
'@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
'@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -10984,7 +10993,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@nuxt/nitro-server@3.20.2(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(typescript@5.9.3)(xml2js@0.6.2)':
|
||||
'@nuxt/nitro-server@3.20.2(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(typescript@5.9.3)(xml2js@0.6.2)':
|
||||
dependencies:
|
||||
'@nuxt/devalue': 2.0.2
|
||||
'@nuxt/kit': 3.20.2(magicast@0.5.1)
|
||||
@@ -11002,7 +11011,7 @@ snapshots:
|
||||
klona: 2.0.6
|
||||
mocked-exports: 0.1.1
|
||||
nitropack: 2.12.9(xml2js@0.6.2)
|
||||
nuxt: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2)
|
||||
nuxt: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2)
|
||||
pathe: 2.0.3
|
||||
pkg-types: 2.3.0
|
||||
radix3: 1.1.2
|
||||
@@ -11073,7 +11082,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@nuxt/vite-builder@3.20.2(@types/node@20.19.27)(eslint@9.39.2(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)':
|
||||
'@nuxt/vite-builder@3.20.2(@types/node@20.19.27)(eslint@9.39.2(jiti@1.21.7))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.20.2(magicast@0.5.1)
|
||||
'@rollup/plugin-replace': 6.0.3(rollup@4.54.0)
|
||||
@@ -11094,7 +11103,7 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
mlly: 1.8.0
|
||||
mocked-exports: 0.1.1
|
||||
nuxt: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2)
|
||||
nuxt: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2)
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
perfect-debounce: 2.0.0
|
||||
@@ -11105,9 +11114,9 @@ snapshots:
|
||||
std-env: 3.10.0
|
||||
ufo: 1.6.1
|
||||
unenv: 2.0.0-rc.24
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite-node: 5.2.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite-plugin-checker: 0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))
|
||||
vite-plugin-checker: 0.12.0(eslint@9.39.2(jiti@1.21.7))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
vue-bundle-renderer: 2.2.0
|
||||
transitivePeerDependencies:
|
||||
@@ -11139,7 +11148,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@intlify/h3': 0.6.1
|
||||
'@intlify/shared': 10.0.8
|
||||
'@intlify/unplugin-vue-i18n': 6.0.8(@vue/compiler-dom@3.5.26)(eslint@9.39.2(jiti@2.6.1))(rollup@4.54.0)(typescript@5.9.3)(vue-i18n@10.0.8(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
|
||||
'@intlify/unplugin-vue-i18n': 6.0.8(@vue/compiler-dom@3.5.26)(eslint@9.39.2(jiti@1.21.7))(rollup@4.54.0)(typescript@5.9.3)(vue-i18n@10.0.8(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
|
||||
'@intlify/utils': 0.13.0
|
||||
'@miyaneee/rollup-plugin-json5': 1.2.0(rollup@4.54.0)
|
||||
'@nuxt/kit': 3.20.2(magicast@0.5.1)
|
||||
@@ -11887,18 +11896,6 @@ snapshots:
|
||||
|
||||
'@stripe/stripe-js@7.9.0': {}
|
||||
|
||||
'@stylistic/eslint-plugin@2.13.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint-visitor-keys: 4.2.1
|
||||
espree: 10.4.0
|
||||
estraverse: 5.3.0
|
||||
picomatch: 4.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@stylistic/eslint-plugin@2.13.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
@@ -11910,7 +11907,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
optional: true
|
||||
|
||||
'@svgdotjs/svg.draggable.js@3.0.6(@svgdotjs/svg.js@3.2.5)':
|
||||
dependencies:
|
||||
@@ -12262,6 +12258,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.19.27
|
||||
|
||||
'@types/semver@7.7.1': {}
|
||||
|
||||
'@types/stats.js@0.17.4': {}
|
||||
|
||||
'@types/three@0.172.0':
|
||||
@@ -12292,22 +12290,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.19.27
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.51.0
|
||||
'@typescript-eslint/type-utils': 8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.51.0
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
ignore: 7.0.5
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.3.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@@ -12324,18 +12306,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.51.0
|
||||
'@typescript-eslint/types': 8.51.0
|
||||
'@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.51.0
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.51.0
|
||||
@@ -12366,18 +12336,6 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.51.0
|
||||
'@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
ts-api-utils: 2.3.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/type-utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.51.0
|
||||
@@ -12407,17 +12365,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
|
||||
'@typescript-eslint/scope-manager': 8.51.0
|
||||
'@typescript-eslint/types': 8.51.0
|
||||
'@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||
@@ -12532,7 +12479,7 @@ snapshots:
|
||||
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
|
||||
'@rolldown/pluginutils': 1.0.0-beta.58
|
||||
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.28.5)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -12542,16 +12489,16 @@ snapshots:
|
||||
vite: 5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(vite@6.4.1(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue@6.0.3(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
vite: 6.4.1(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
'@vitest/browser-playwright@4.0.16(playwright@1.57.0)(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))(vitest@4.0.16)':
|
||||
@@ -14177,10 +14124,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
source-map: 0.6.1
|
||||
|
||||
eslint-config-flat-gitignore@0.3.0(eslint@9.39.2(jiti@1.21.7)):
|
||||
eslint-config-flat-gitignore@0.3.0(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@eslint/compat': 1.4.1(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
'@eslint/compat': 1.4.1(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
find-up-simple: 1.0.1
|
||||
|
||||
eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)):
|
||||
@@ -14198,12 +14145,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
unrs-resolver: 1.11.1
|
||||
|
||||
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)):
|
||||
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.51.0
|
||||
comment-parser: 1.4.1
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
|
||||
is-glob: 4.0.3
|
||||
minimatch: 10.1.1
|
||||
@@ -14211,18 +14158,18 @@ snapshots:
|
||||
stable-hash-x: 0.2.0
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-jsdoc@50.8.0(eslint@9.39.2(jiti@1.21.7)):
|
||||
eslint-plugin-jsdoc@50.8.0(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@es-joy/jsdoccomment': 0.50.2
|
||||
are-docs-informative: 0.0.2
|
||||
comment-parser: 1.4.1
|
||||
debug: 4.4.3
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
espree: 10.4.0
|
||||
esquery: 1.7.0
|
||||
parse-imports-exports: 0.2.4
|
||||
@@ -14240,12 +14187,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1))
|
||||
|
||||
eslint-plugin-regexp@2.10.0(eslint@9.39.2(jiti@1.21.7)):
|
||||
eslint-plugin-regexp@2.10.0(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
comment-parser: 1.4.1
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
jsdoc-type-pratt-parser: 4.8.0
|
||||
refa: 0.12.1
|
||||
regexp-ast-analysis: 0.7.1
|
||||
@@ -14264,20 +14211,20 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
eslint-plugin-turbo@2.7.2(eslint@9.39.2(jiti@1.21.7))(turbo@2.7.2):
|
||||
eslint-plugin-turbo@2.7.2(eslint@9.39.2(jiti@2.6.1))(turbo@2.7.2):
|
||||
dependencies:
|
||||
dotenv: 16.0.3
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
turbo: 2.7.2
|
||||
|
||||
eslint-plugin-unicorn@55.0.0(eslint@9.39.2(jiti@1.21.7)):
|
||||
eslint-plugin-unicorn@55.0.0(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||
ci-info: 4.3.1
|
||||
clean-regexp: 1.0.0
|
||||
core-js-compat: 3.47.0
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
esquery: 1.7.0
|
||||
globals: 15.15.0
|
||||
indent-string: 4.0.0
|
||||
@@ -14304,16 +14251,16 @@ snapshots:
|
||||
'@stylistic/eslint-plugin': 2.13.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
|
||||
eslint-plugin-vue@9.33.0(eslint@9.39.2(jiti@1.21.7)):
|
||||
eslint-plugin-vue@9.33.0(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
globals: 13.24.0
|
||||
natural-compare: 1.4.0
|
||||
nth-check: 2.1.1
|
||||
postcss-selector-parser: 6.1.2
|
||||
semver: 7.7.3
|
||||
vue-eslint-parser: 9.4.3(eslint@9.39.2(jiti@1.21.7))
|
||||
vue-eslint-parser: 9.4.3(eslint@9.39.2(jiti@2.6.1))
|
||||
xml-name-validator: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -16321,16 +16268,16 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2):
|
||||
nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@dxup/nuxt': 0.2.2(magicast@0.5.1)
|
||||
'@nuxt/cli': 3.31.3(cac@6.7.14)(magicast@0.5.1)
|
||||
'@nuxt/devtools': 3.1.1(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
||||
'@nuxt/kit': 3.20.2(magicast@0.5.1)
|
||||
'@nuxt/nitro-server': 3.20.2(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(typescript@5.9.3)(xml2js@0.6.2)
|
||||
'@nuxt/nitro-server': 3.20.2(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(typescript@5.9.3)(xml2js@0.6.2)
|
||||
'@nuxt/schema': 3.20.2
|
||||
'@nuxt/telemetry': 2.6.6(magicast@0.5.1)
|
||||
'@nuxt/vite-builder': 3.20.2(@types/node@20.19.27)(eslint@9.39.2(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)
|
||||
'@nuxt/vite-builder': 3.20.2(@types/node@20.19.27)(eslint@9.39.2(jiti@1.21.7))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)
|
||||
'@unhead/vue': 2.1.1(vue@3.5.26(typescript@5.9.3))
|
||||
'@vue/shared': 3.5.26
|
||||
c12: 3.3.3(magicast@0.5.1)
|
||||
@@ -18498,12 +18445,12 @@ snapshots:
|
||||
vite-dev-rpc@1.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite-hot-client: 2.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))
|
||||
|
||||
vite-hot-client@2.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
|
||||
vite-node@5.2.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2):
|
||||
dependencies:
|
||||
@@ -18525,7 +18472,7 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-plugin-checker@0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3)):
|
||||
vite-plugin-checker@0.12.0(eslint@9.39.2(jiti@1.21.7))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
chokidar: 4.0.3
|
||||
@@ -18534,10 +18481,10 @@ snapshots:
|
||||
picomatch: 4.0.3
|
||||
tiny-invariant: 1.3.3
|
||||
tinyglobby: 0.2.15
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vscode-uri: 3.1.0
|
||||
optionalDependencies:
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
optionator: 0.9.4
|
||||
typescript: 5.9.3
|
||||
vue-tsc: 2.2.12(typescript@5.9.3)
|
||||
@@ -18552,7 +18499,7 @@ snapshots:
|
||||
perfect-debounce: 2.0.0
|
||||
sirv: 3.0.2
|
||||
unplugin-utils: 0.3.1
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite-dev-rpc: 1.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 4.2.2(magicast@0.5.1)
|
||||
@@ -18566,7 +18513,7 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
source-map-js: 1.2.1
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
vite-svg-loader@5.1.0(vue@3.5.26(typescript@5.9.3)):
|
||||
@@ -18586,23 +18533,6 @@ snapshots:
|
||||
sass: 1.97.1
|
||||
terser: 5.44.1
|
||||
|
||||
vite@6.4.1(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.54.0
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.27
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
lightningcss: 1.30.2
|
||||
sass: 1.97.1
|
||||
terser: 5.44.1
|
||||
yaml: 2.8.2
|
||||
|
||||
vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
@@ -18620,6 +18550,23 @@ snapshots:
|
||||
terser: 5.44.1
|
||||
yaml: 2.8.2
|
||||
|
||||
vite@7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2):
|
||||
dependencies:
|
||||
esbuild: 0.27.2
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.54.0
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.27
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
lightningcss: 1.30.2
|
||||
sass: 1.97.1
|
||||
terser: 5.44.1
|
||||
yaml: 2.8.2
|
||||
|
||||
vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2):
|
||||
dependencies:
|
||||
esbuild: 0.27.2
|
||||
@@ -18835,10 +18782,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vue-eslint-parser@9.4.3(eslint@9.39.2(jiti@1.21.7)):
|
||||
vue-eslint-parser@9.4.3(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-scope: 7.2.2
|
||||
eslint-visitor-keys: 3.4.3
|
||||
espree: 9.6.1
|
||||
|
||||
Reference in New Issue
Block a user