You've already forked pages
forked from didirus/AstralRinth
feat: managing project versions (#4811)
* start modal, working show modal * add stages and implement MultiModalStage component * add project versions context and add file button * implement add files stage * export interfaces * move MultiStageModal to /base * small update to file input * add version types to api-client * wrap version namespace under v3 * implement add details stage fields and loaders component * start create MC versions stage * implement changelog stage and bring width into a per stage concern * implement loader picker with grouping * improve grouping and sorting for loader picker * use chips component * small updaets * fix loader icon color * componentize mc version picker * initial version of shift click to select range * use newModal for markdown editor * start add dependencies stage with search * implement showing mod options in search * componentize modselect and add version/dependency relation select * hide version and dependency relation when no project selected * fix project facet search * implement api-client versions requests * fix search api request facet type to be string * fix new modal outer container scroll * implement add dependency stage * fix parse error * add placeholders * fix types * update dependency row styles * small change * fix the types on manage versions to be correct with labrinth request bodies * fix create version file parts * use draft version ref in flow and implement proper file handlling * use draft version ref for mc versions select * implement reactive modal state and conditionally disabled next buttons * ensure all data is using draftVersion ref * remove shift click to select range since it sucks * fix up add dependencies stage state/types * small fixes * implement adding dependencies connected to api calls and make adding dependencies work * add final create version button config * start create version backend call and bring versions table to project settings * set add files stage width * remove version file upload in project page * small fix * fix create version api call * implement error handling * implement mc versions search * implement showing all mc versions * small fix * implement prefill data * add success notification * add cancel button * add new dropzone file input * run pnpm run fix * add tailwind preset in ui package * polish file version row * fix modal widths * hide added versions when no versions added * implement add loaders stage * implement small chips and small fixes * implement grouping for all releases * implement new all releases grouping * implement better shift click for version select * small fixes * fix search input style * delete versions provider and start project type inferring * implement getting project type * add versions empty state, add folder up icon and pnpm run fix * implement create version in project versions table * update side nav * implement dynamic create version flow depending on project type and detected data * add id to stages and fix calling setStage not working * move added loaded out of loader picker * remove selected and detected MC versions * add loading message to dependency search and fix dependency type always being "required" * fix components in ref * fix width on dropdown * implement toggle all mc versions based on state of last in range * fix mc version text colour * do proper clean up * update loaders to use tag item * update UI to use TagItem and better match styles * handle detected data when setting primary file * add progress bar * hide progress bar for non-progress stage * add loading state on submit * properly cache dependencies projects/versions * pnpm run fix * add dragover show purple border on dropzone file input * better handle added dependencies * move versions in side nav * implement adding file type * fix api body format for file type * implement working edit existing version - working add/remove file - working edit version details * a step towards proper versions refresh * add gallery to project settings * actually figured out refresh versions * move checklist into settings page * remove editing version from version page and add button to versions table in project settings * remove edit and delete buttons from gallery in project page * add empty state messages for project page * add default scroll bar styles * implement support for new file types * remove edit from dropdown in project page versions table * redirect to settings page * move changelog to row with actions * fix overflow on added dependencies * fix redirect * update scroll styles * implement add environment stage (create and modify version not persisting environment to backend) * small style fixes * small spacing fix * small style fixes * add a flag for loading dependency projects * address PR comments * fix modrinth ui imports * use magic keys instead of window.addeventlistener * add spacing in bottom of settings page * useDebounceFn from vue * fix inconsistent stroke * persist scroll through * fix remove button * fix api fields * fix version file dropdown: hide primary option in edit mode and fix setting initial value * fix links in nags * implement skipped field for skipping steps instead of mutating stages array's elements * implement suggested dependencies components * implement suggested dependencies api call * refactor cached get project and get version calls * always hide environments * update links * set scroll in 10ms * update links * fix links pt2 * fix shadow * fix progress bar * dont include mc versions in suggested versions finder * fix text overflow styles * use tooltip * fix change version name api * implement set environment api call * delete unused vue pages * implement detected environment, edit environment step, and fix showing loaders in details for no loader projects * small fix * no loaders project wrong check * fix not having 'minecraft' loader for resource pack * implement updating existing files file type * move add minecraft loader outside try catch * add datapack to have environment * fix being able to select duplicate MC versions * remove datapack project from environment * fix version fetch * fix having detected environment not properly skipping step * only add detected data when primary file changes * fix unknown environemtn * implement gallery and versions have moved admonition * update project page for creator view * small copy update * merge fixes * pnpm run fix * fix checkmark squished * fix version type can be deselected * refactor: DI context + better typed MultistageModal * fix type import * Misc QA fixes * fix allowed file types with no project type * implement new add files stage * fix versiosn header with new pagination * hide buttons when no files for add file stage * use prettier formatter * allow signature file types * add detecting primary file * fix progress bar in firefox * fix environment not correctly being hidden/shown * remove environment missing nag * temp bring back environment page * remove delete version action from project page * replace "continue" next button label with actual next step * fix types * pnpm run fix * move supplementary files alert up and update border radius style on dropzone * copy updates * small update on version num placeholder * update placeholder * make timeout on upload routes 2 minutes * fix lint issues * run pnpm intl:extract --------- Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"[vue]": {
|
"[vue]": {
|
||||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.6",
|
||||||
|
"vue-component-type-helpers": "^3.1.8",
|
||||||
"vue-tsc": "^2.1.6"
|
"vue-tsc": "^2.1.6"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.4.0",
|
"packageManager": "pnpm@9.4.0",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"vite-svg-loader": "^5.1.0",
|
"vite-svg-loader": "^5.1.0",
|
||||||
|
"vue-component-type-helpers": "^3.1.8",
|
||||||
"vue-tsc": "^2.0.24"
|
"vue-tsc": "^2.0.24"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -20,20 +20,6 @@
|
|||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ModerationProjectNags
|
|
||||||
v-if="
|
|
||||||
(currentMember && project.status === 'draft') ||
|
|
||||||
tags.rejectedStatuses.includes(project.status)
|
|
||||||
"
|
|
||||||
:project="project"
|
|
||||||
:versions="versions"
|
|
||||||
:current-member="currentMember"
|
|
||||||
:collapsed="collapsed"
|
|
||||||
:route-name="routeName"
|
|
||||||
:tags="tags"
|
|
||||||
@toggle-collapsed="handleToggleCollapsed"
|
|
||||||
@set-processing="handleSetProcessing"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -45,8 +31,6 @@ import { computed } from 'vue'
|
|||||||
|
|
||||||
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
|
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
|
||||||
|
|
||||||
import ModerationProjectNags from './moderation/ModerationProjectNags.vue'
|
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
interface Tags {
|
interface Tags {
|
||||||
@@ -71,12 +55,9 @@ interface Props {
|
|||||||
currentMember?: Member | null
|
currentMember?: Member | null
|
||||||
allMembers?: Member[] | null
|
allMembers?: Member[] | null
|
||||||
isSettings?: boolean
|
isSettings?: boolean
|
||||||
collapsed?: boolean
|
|
||||||
routeName?: string
|
|
||||||
auth: Auth
|
auth: Auth
|
||||||
tags: Tags
|
tags: Tags
|
||||||
setProcessing?: (processing: boolean) => void
|
setProcessing?: (processing: boolean) => void
|
||||||
toggleCollapsed?: () => void
|
|
||||||
updateMembers?: () => void | Promise<void>
|
updateMembers?: () => void | Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +125,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
allMembers: null,
|
allMembers: null,
|
||||||
isSettings: false,
|
isSettings: false,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
routeName: '',
|
|
||||||
setProcessing: () => {},
|
setProcessing: () => {},
|
||||||
toggleCollapsed: () => {},
|
toggleCollapsed: () => {},
|
||||||
updateMembers: async () => {},
|
updateMembers: async () => {},
|
||||||
@@ -164,14 +144,6 @@ const showInvitation = computed<boolean>(() => {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleToggleCollapsed(): void {
|
|
||||||
if (props.toggleCollapsed) {
|
|
||||||
props.toggleCollapsed()
|
|
||||||
} else {
|
|
||||||
emit('toggleCollapsed')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUpdateMembers(): Promise<void> {
|
async function handleUpdateMembers(): Promise<void> {
|
||||||
if (props.updateMembers) {
|
if (props.updateMembers) {
|
||||||
await props.updateMembers()
|
await props.updateMembers()
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<MultiStageModal ref="modal" :stages="ctx.stageConfigs" :context="ctx" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { injectProjectPageContext, MultiStageModal } from '@modrinth/ui'
|
||||||
|
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createManageVersionContext,
|
||||||
|
provideManageVersionContext,
|
||||||
|
} from '~/providers/version/manage-version-modal'
|
||||||
|
|
||||||
|
const modal = useTemplateRef<ComponentExposed<typeof MultiStageModal>>('modal')
|
||||||
|
|
||||||
|
const ctx = createManageVersionContext(modal)
|
||||||
|
provideManageVersionContext(ctx)
|
||||||
|
|
||||||
|
const { newDraftVersion, setProjectType } = ctx
|
||||||
|
|
||||||
|
const { projectV2 } = injectProjectPageContext()
|
||||||
|
|
||||||
|
function showCreateVersionModal(version: Labrinth.Versions.v3.DraftVersion | null = null) {
|
||||||
|
newDraftVersion(projectV2.value.id, version)
|
||||||
|
setProjectType(projectV2.value)
|
||||||
|
modal.value?.setStage(0)
|
||||||
|
modal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show: showCreateVersionModal,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex 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" />
|
||||||
|
|
||||||
|
<span v-tooltip="name || projectId" class="truncate font-semibold text-contrast">
|
||||||
|
{{ name || 'Unknown Project' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<TagItem class="shrink-0 border !border-solid border-surface-5">
|
||||||
|
{{ dependencyType }}
|
||||||
|
</TagItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="versionName"
|
||||||
|
v-tooltip="versionName"
|
||||||
|
class="max-w-[35%] truncate whitespace-nowrap font-medium"
|
||||||
|
>
|
||||||
|
{{ versionName }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end gap-1">
|
||||||
|
<ButtonStyled size="standard" :circular="true">
|
||||||
|
<button aria-label="Remove file" class="!shadow-none" @click="emitRemove">
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { XIcon } from '@modrinth/assets'
|
||||||
|
import { Avatar, ButtonStyled, TagItem } from '@modrinth/ui'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'fileTypeChange', type: string): void
|
||||||
|
(e: 'remove'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { projectId, name, icon, dependencyType, versionName } = defineProps<{
|
||||||
|
projectId: string
|
||||||
|
name?: string
|
||||||
|
icon?: string
|
||||||
|
dependencyType: Labrinth.Versions.v2.DependencyType
|
||||||
|
versionName?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function emitRemove() {
|
||||||
|
emit('remove')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2.5">
|
||||||
|
<span class="font-semibold text-contrast">Loaders <span class="text-red">*</span></span>
|
||||||
|
|
||||||
|
<Chips
|
||||||
|
v-model="loaderGroup"
|
||||||
|
:items="groupLabels"
|
||||||
|
:never-empty="false"
|
||||||
|
:capitalize="true"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex min-h-[150px] flex-1 flex-col gap-4 overflow-y-auto rounded-xl border border-solid border-surface-5 p-3"
|
||||||
|
>
|
||||||
|
<div v-if="groupedLoaders[loaderGroup].length" class="flex flex-col gap-1.5">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<TagItem
|
||||||
|
v-for="loader in groupedLoaders[loaderGroup]"
|
||||||
|
:key="`loader-${loader.name}`"
|
||||||
|
:action="() => toggleLoader(loader.name)"
|
||||||
|
class="border !border-solid !transition-all hover:bg-button-bgHover hover:no-underline"
|
||||||
|
:class="
|
||||||
|
selectedLoaders.includes(loader.name)
|
||||||
|
? 'border-brand bg-highlight-green text-brand'
|
||||||
|
: 'border-surface-5'
|
||||||
|
"
|
||||||
|
:style="`--_color: var(--color-platform-${loader.name})`"
|
||||||
|
>
|
||||||
|
<div v-html="loader.icon"></div>
|
||||||
|
{{ formatCategory(loader.name) }}
|
||||||
|
</TagItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>Select one or more loaders this version supports.</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { Chips, TagItem } from '@modrinth/ui'
|
||||||
|
import { formatCategory } from '@modrinth/utils'
|
||||||
|
|
||||||
|
const selectedLoaders = defineModel<string[]>({ default: [] })
|
||||||
|
|
||||||
|
const { loaders } = defineProps<{
|
||||||
|
loaders: Labrinth.Tags.v2.Loader[]
|
||||||
|
toggleLoader: (loader: string) => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const loaderGroup = ref<GroupLabels>('mods')
|
||||||
|
|
||||||
|
type GroupLabels = 'mods' | 'plugins' | 'packs' | 'shaders' | 'other'
|
||||||
|
|
||||||
|
const groupLabels: GroupLabels[] = ['mods', 'plugins', 'packs', 'shaders']
|
||||||
|
|
||||||
|
function groupLoaders(loaders: Labrinth.Tags.v2.Loader[]) {
|
||||||
|
const groups: Record<GroupLabels, Labrinth.Tags.v2.Loader[]> = {
|
||||||
|
mods: [],
|
||||||
|
plugins: [],
|
||||||
|
packs: [],
|
||||||
|
shaders: [],
|
||||||
|
other: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const MOD_SORT = [
|
||||||
|
'fabric',
|
||||||
|
'neoforge',
|
||||||
|
'forge',
|
||||||
|
'quilt',
|
||||||
|
'liteloader',
|
||||||
|
'rift',
|
||||||
|
'ornithe',
|
||||||
|
'nilloader',
|
||||||
|
'risugami',
|
||||||
|
'legacy-fabric',
|
||||||
|
'bta-babric',
|
||||||
|
'babric',
|
||||||
|
'modloader',
|
||||||
|
'java-agent',
|
||||||
|
]
|
||||||
|
|
||||||
|
const PLUGIN_SORT = [
|
||||||
|
'paper',
|
||||||
|
'purpur',
|
||||||
|
'spigot',
|
||||||
|
'bukkit',
|
||||||
|
'sponge',
|
||||||
|
'folia',
|
||||||
|
'bungeecord',
|
||||||
|
'velocity',
|
||||||
|
'waterfall',
|
||||||
|
'geyser',
|
||||||
|
]
|
||||||
|
|
||||||
|
const SHADER_SORT = ['optifine', 'iris', 'canvas', 'vanilla']
|
||||||
|
const PACKS_SORT = ['minecraft', 'datapack']
|
||||||
|
|
||||||
|
for (const loader of loaders) {
|
||||||
|
const name = loader.name.toLowerCase()
|
||||||
|
if (PACKS_SORT.includes(name)) groups.packs.push(loader)
|
||||||
|
else if (SHADER_SORT.includes(name)) groups.shaders.push(loader)
|
||||||
|
else if (PLUGIN_SORT.includes(name)) groups.plugins.push(loader)
|
||||||
|
else if (MOD_SORT.includes(name)) groups.mods.push(loader)
|
||||||
|
else groups.other.push(loader)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortByOrder = (arr: any[], order: string[]) =>
|
||||||
|
arr.sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name))
|
||||||
|
|
||||||
|
sortByOrder(groups.mods, MOD_SORT)
|
||||||
|
sortByOrder(groups.plugins, PLUGIN_SORT)
|
||||||
|
sortByOrder(groups.shaders, SHADER_SORT)
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedLoaders = computed(() => groupLoaders(loaders))
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-2.5">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="font-semibold text-contrast">
|
||||||
|
Minecraft versions <span class="text-red">*</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Chips
|
||||||
|
v-model="versionType"
|
||||||
|
:items="['release', 'all']"
|
||||||
|
:never-empty="true"
|
||||||
|
:capitalize="true"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="iconified-input w-full">
|
||||||
|
<SearchIcon aria-hidden="true" />
|
||||||
|
<input v-model="searchQuery" type="text" placeholder="Search versions" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex h-72 select-none flex-col gap-3 overflow-y-auto rounded-xl border border-solid border-surface-5 p-3 py-4"
|
||||||
|
>
|
||||||
|
<div v-for="group in groupedGameVersions" :key="group.key" class="space-y-1.5">
|
||||||
|
<span class="font-semibold">{{ group.key }}</span>
|
||||||
|
<div class="flex flex-wrap gap-2 gap-x-1.5">
|
||||||
|
<ButtonStyled
|
||||||
|
v-for="version in group.versions"
|
||||||
|
:key="version"
|
||||||
|
:color="
|
||||||
|
holdingShift && version === anchorVersion
|
||||||
|
? 'purple'
|
||||||
|
: modelValue.includes(version)
|
||||||
|
? 'green'
|
||||||
|
: 'standard'
|
||||||
|
"
|
||||||
|
:highlighted="modelValue.includes(version)"
|
||||||
|
type="chip"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="!py-1.5 focus:outline-none"
|
||||||
|
:class="[
|
||||||
|
versionType === 'all' && !group.isReleaseGroup ? 'w-max' : 'w-16',
|
||||||
|
modelValue.includes(version) ? '!text-contrast' : '',
|
||||||
|
]"
|
||||||
|
@click="() => handleToggleVersion(version)"
|
||||||
|
@blur="
|
||||||
|
() => {
|
||||||
|
if (!holdingShift) anchorVersion = ''
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ version }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span v-if="!filteredVersions.length">No versions found.</span>
|
||||||
|
</div>
|
||||||
|
<div>Hold shift and click to select range.</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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'
|
||||||
|
|
||||||
|
type GameVersion = Labrinth.Tags.v2.GameVersion
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: string[]
|
||||||
|
gameVersions: Labrinth.Tags.v2.GameVersion[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string[]): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const keys = useMagicKeys()
|
||||||
|
const holdingShift = computed(() => keys.shift.value)
|
||||||
|
|
||||||
|
const versionType = ref<string | null>('release')
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
const filteredVersions = computed(() =>
|
||||||
|
props.gameVersions
|
||||||
|
.filter((v) => versionType.value === 'all' || v.version_type === versionType.value)
|
||||||
|
.filter(searchFilter),
|
||||||
|
)
|
||||||
|
|
||||||
|
const groupedGameVersions = computed(() => groupVersions(filteredVersions.value))
|
||||||
|
|
||||||
|
const allVersionsFlat = computed(() => groupedGameVersions.value.flatMap((group) => group.versions))
|
||||||
|
const anchorVersion = ref<string | null>(null)
|
||||||
|
|
||||||
|
const handleToggleVersion = (version: string) => {
|
||||||
|
const flat = allVersionsFlat.value
|
||||||
|
|
||||||
|
if (holdingShift.value && anchorVersion.value && flat.length) {
|
||||||
|
const anchorIdx = flat.indexOf(anchorVersion.value)
|
||||||
|
const targetIdx = flat.indexOf(version)
|
||||||
|
|
||||||
|
if (anchorIdx === -1 || targetIdx === -1) {
|
||||||
|
return toggleVersion(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Math.min(anchorIdx, targetIdx)
|
||||||
|
const end = Math.max(anchorIdx, targetIdx)
|
||||||
|
const range = flat.slice(start, end + 1)
|
||||||
|
|
||||||
|
const isTargetSelected = props.modelValue.includes(version)
|
||||||
|
if (isTargetSelected) {
|
||||||
|
emit(
|
||||||
|
'update:modelValue',
|
||||||
|
props.modelValue.filter((v) => !range.includes(v)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const newVersions = range.filter((v) => !props.modelValue.includes(v))
|
||||||
|
emit('update:modelValue', [...props.modelValue, ...newVersions])
|
||||||
|
}
|
||||||
|
|
||||||
|
anchorVersion.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleVersion(version)
|
||||||
|
anchorVersion.value = version
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleVersion = (version: string) => {
|
||||||
|
const isSelected = props.modelValue.includes(version)
|
||||||
|
const next = isSelected
|
||||||
|
? props.modelValue.filter((v) => v !== version)
|
||||||
|
: [...props.modelValue, version]
|
||||||
|
|
||||||
|
emit('update:modelValue', next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEV_RELEASE_KEY = 'Snapshots'
|
||||||
|
|
||||||
|
function groupVersions(gameVersions: GameVersion[]) {
|
||||||
|
gameVersions = [...gameVersions].sort(
|
||||||
|
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||||
|
)
|
||||||
|
|
||||||
|
const getGroupKey = (v: string) => v.split('.').slice(0, 2).join('.')
|
||||||
|
const groups: Record<string, string[]> = {}
|
||||||
|
|
||||||
|
let currentGroupKey = getGroupKey(gameVersions.find((v) => v.major)?.version || '')
|
||||||
|
|
||||||
|
gameVersions.forEach((gameVersion) => {
|
||||||
|
if (gameVersion.version_type === 'release') {
|
||||||
|
currentGroupKey = getGroupKey(gameVersion.version)
|
||||||
|
if (!groups[currentGroupKey]) groups[currentGroupKey] = []
|
||||||
|
groups[currentGroupKey].push(gameVersion.version)
|
||||||
|
} else {
|
||||||
|
const key = `${currentGroupKey} ${DEV_RELEASE_KEY}`
|
||||||
|
if (!groups[key]) groups[key] = []
|
||||||
|
groups[key].push(gameVersion.version)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortedKeys = Object.keys(groups).sort(compareGroupKeys)
|
||||||
|
return sortedKeys.map((key) => ({
|
||||||
|
key,
|
||||||
|
versions: groups[key].sort((a, b) => compareVersions(b, a)),
|
||||||
|
isReleaseGroup: !key.includes(DEV_RELEASE_KEY),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBaseVersion = (key: string) => key.split(' ')[0]
|
||||||
|
|
||||||
|
function compareVersions(a: string, b: string) {
|
||||||
|
const pa = a.split('.').map(Number)
|
||||||
|
const pb = b.split('.').map(Number)
|
||||||
|
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const na = pa[i] || 0
|
||||||
|
const nb = pb[i] || 0
|
||||||
|
if (na > nb) return 1
|
||||||
|
if (na < nb) return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareGroupKeys(a: string, b: string) {
|
||||||
|
const aBase = getBaseVersion(a)
|
||||||
|
const bBase = getBaseVersion(b)
|
||||||
|
|
||||||
|
const versionSort = compareVersions(aBase, bBase)
|
||||||
|
if (versionSort !== 0) return -versionSort // descending
|
||||||
|
|
||||||
|
const isADev = a.includes(DEV_RELEASE_KEY)
|
||||||
|
const isBDev = b.includes(DEV_RELEASE_KEY)
|
||||||
|
|
||||||
|
if (isADev && !isBDev) return 1
|
||||||
|
if (!isADev && isBDev) return -1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchFilter(gameVersion: Labrinth.Tags.v2.GameVersion) {
|
||||||
|
return gameVersion.version.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<Combobox
|
||||||
|
v-model="projectId"
|
||||||
|
placeholder="Select project"
|
||||||
|
:options="options"
|
||||||
|
:searchable="true"
|
||||||
|
search-placeholder="Search by name, slug, or paste ID..."
|
||||||
|
:no-options-message="searchLoading ? 'Loading...' : 'No results found'"
|
||||||
|
@search-input="(query) => handleSearch(query)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Combobox, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import type { DropdownOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
import { defineAsyncComponent, h } from 'vue'
|
||||||
|
|
||||||
|
const { addNotification } = injectNotificationManager()
|
||||||
|
const projectId = defineModel<string>()
|
||||||
|
|
||||||
|
const searchLoading = ref(false)
|
||||||
|
const options = ref<DropdownOption<string>[]>([])
|
||||||
|
|
||||||
|
const { labrinth } = injectModrinthClient()
|
||||||
|
|
||||||
|
const search = async (query: string) => {
|
||||||
|
if (!query.trim()) {
|
||||||
|
searchLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await labrinth.projects_v2.search({
|
||||||
|
query: query,
|
||||||
|
limit: 20,
|
||||||
|
facets: [['project_type:mod']],
|
||||||
|
})
|
||||||
|
|
||||||
|
options.value = results.hits.map((hit) => ({
|
||||||
|
label: hit.title,
|
||||||
|
value: hit.project_id,
|
||||||
|
icon: defineAsyncComponent(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
setup: () => () =>
|
||||||
|
h('img', {
|
||||||
|
src: hit.icon_url,
|
||||||
|
alt: hit.title,
|
||||||
|
class: 'h-5 w-5 rounded',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
} catch (error: any) {
|
||||||
|
addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: error.data ? error.data.description : error,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
searchLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttledSearch = useDebounceFn(search, 500)
|
||||||
|
|
||||||
|
const handleSearch = async (query: string) => {
|
||||||
|
searchLoading.value = true
|
||||||
|
await throttledSearch(query)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<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">
|
||||||
|
<SuggestedDependency
|
||||||
|
v-if="dependency"
|
||||||
|
:key="index"
|
||||||
|
:project-id="dependency.project_id"
|
||||||
|
:name="dependency.name"
|
||||||
|
:icon="dependency.icon"
|
||||||
|
:dependency-type="dependency.dependency_type"
|
||||||
|
:version-name="dependency.versionName"
|
||||||
|
@on-add-suggestion="
|
||||||
|
() =>
|
||||||
|
handleAddSuggestion({
|
||||||
|
dependency_type: dependency.dependency_type,
|
||||||
|
project_id: dependency.project_id,
|
||||||
|
version_id: dependency.version_id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
|
||||||
|
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||||
|
|
||||||
|
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 emit = defineEmits<{
|
||||||
|
(e: 'onAddSuggestion', dependency: Labrinth.Versions.v3.Dependency): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handleAddSuggestion(dependency: Labrinth.Versions.v3.Dependency) {
|
||||||
|
emit('onAddSuggestion', dependency)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex 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" />
|
||||||
|
|
||||||
|
<span v-tooltip="name || 'Unknown Project'" class="truncate font-semibold text-contrast">
|
||||||
|
{{ name || 'Unknown Project' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<TagItem class="shrink-0 border !border-solid border-surface-5">
|
||||||
|
{{ dependencyType }}
|
||||||
|
</TagItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="versionName"
|
||||||
|
v-tooltip="versionName"
|
||||||
|
class="max-w-[35%] truncate whitespace-nowrap font-medium"
|
||||||
|
>
|
||||||
|
{{ versionName }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end gap-1">
|
||||||
|
<ButtonStyled size="standard" :circular="true">
|
||||||
|
<button aria-label="Add dependency" class="!shadow-none" @click="emitAddSuggestion">
|
||||||
|
<PlusIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { PlusIcon } from '@modrinth/assets'
|
||||||
|
import { Avatar, ButtonStyled, TagItem } from '@modrinth/ui'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'onAddSuggestion'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { name, icon, dependencyType, versionName } = defineProps<{
|
||||||
|
name?: string
|
||||||
|
icon?: string
|
||||||
|
dependencyType: Labrinth.Versions.v2.DependencyType
|
||||||
|
versionName?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function emitAddSuggestion() {
|
||||||
|
emit('onAddSuggestion')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<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">
|
||||||
|
<div class="grid h-5 min-h-5 w-5 min-w-5 place-content-center rounded-full bg-green">
|
||||||
|
<CheckIcon class="text-sm text-black" />
|
||||||
|
</div>
|
||||||
|
<span v-tooltip="name" class="overflow-hidden text-ellipsis whitespace-nowrap font-medium">
|
||||||
|
{{ name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<template v-if="!isPrimary">
|
||||||
|
<div class="w-36">
|
||||||
|
<Combobox
|
||||||
|
v-model="selectedType"
|
||||||
|
:searchable="false"
|
||||||
|
class="rounded-xl border border-solid border-surface-5 text-sm"
|
||||||
|
:options="versionTypes"
|
||||||
|
:close-on-select="true"
|
||||||
|
:show-labels="false"
|
||||||
|
:allow-empty="false"
|
||||||
|
@update:model-value="emitFileTypeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ButtonStyled v-if="onRemove" size="standard" :circular="true">
|
||||||
|
<button aria-label="Remove file" class="!shadow-none" @click="onRemove">
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled v-if="isPrimary" size="standard" :circular="true">
|
||||||
|
<button
|
||||||
|
v-tooltip="
|
||||||
|
editingVersion
|
||||||
|
? 'Primary file cannot be changed after version is uploaded'
|
||||||
|
: 'Replace primary file'
|
||||||
|
"
|
||||||
|
aria-label="Change primary file"
|
||||||
|
class="!shadow-none"
|
||||||
|
:disabled="editingVersion"
|
||||||
|
@click="primaryFileInput?.click()"
|
||||||
|
>
|
||||||
|
<ArrowLeftRightIcon aria-hidden="true" />
|
||||||
|
<input
|
||||||
|
ref="primaryFileInput"
|
||||||
|
class="hidden"
|
||||||
|
type="file"
|
||||||
|
:accept="acceptFileFromProjectType(projectV2.project_type)"
|
||||||
|
:disabled="editingVersion"
|
||||||
|
@change="
|
||||||
|
(e) => {
|
||||||
|
emit('setPrimaryFile', (e.target as HTMLInputElement)?.files?.[0])
|
||||||
|
;(e.target as HTMLInputElement).value = ''
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
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 { acceptFileFromProjectType } from '@modrinth/utils'
|
||||||
|
|
||||||
|
const { projectV2 } = injectProjectPageContext()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'setPrimaryFile', file?: File): void
|
||||||
|
(e: 'setFileType', type: Labrinth.Versions.v3.FileType): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { name, isPrimary, onRemove, initialFileType, editingVersion } = defineProps<{
|
||||||
|
name: string
|
||||||
|
isPrimary: boolean
|
||||||
|
onRemove?: () => void
|
||||||
|
initialFileType?: Labrinth.Versions.v3.FileType | 'primary'
|
||||||
|
editingVersion: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
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'>[]
|
||||||
|
|
||||||
|
function emitFileTypeChange() {
|
||||||
|
if (selectedType.value === 'primary') emit('setPrimaryFile')
|
||||||
|
else emit('setFileType', selectedType.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<MarkdownEditor
|
||||||
|
v-model="draftVersion.changelog"
|
||||||
|
:on-image-upload="onImageUpload"
|
||||||
|
:max-height="500"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
<ModSelect v-model="newDependencyProjectId" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="newDependencyProjectId">
|
||||||
|
<div class="grid gap-2.5">
|
||||||
|
<span class="font-semibold text-contrast"> Version </span>
|
||||||
|
<Combobox
|
||||||
|
v-model="newDependencyVersionId"
|
||||||
|
placeholder="Select version"
|
||||||
|
:options="[{ label: 'Any version', value: null }, ...newDependencyVersions]"
|
||||||
|
:searchable="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2.5">
|
||||||
|
<span class="font-semibold text-contrast"> Dependency relation </span>
|
||||||
|
<Combobox
|
||||||
|
v-model="newDependencyType"
|
||||||
|
placeholder="Select dependency type"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Required', value: 'required' },
|
||||||
|
{ label: 'Optional', value: 'optional' },
|
||||||
|
{ label: 'Incompatible', value: 'incompatible' },
|
||||||
|
{ label: 'Embedded', value: 'embedded' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ButtonStyled>
|
||||||
|
<button
|
||||||
|
class="self-start"
|
||||||
|
:disabled="!newDependencyProjectId"
|
||||||
|
@click="
|
||||||
|
() =>
|
||||||
|
addDependency(
|
||||||
|
toRaw({
|
||||||
|
project_id: newDependencyProjectId,
|
||||||
|
version_id: newDependencyVersionId || undefined,
|
||||||
|
dependency_type: newDependencyType,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Add Dependency
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SuggestedDependencies
|
||||||
|
:suggested-dependencies="suggestedDependencies"
|
||||||
|
@on-add-suggestion="handleAddSuggestedDependency"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import {
|
||||||
|
ButtonStyled,
|
||||||
|
Combobox,
|
||||||
|
injectModrinthClient,
|
||||||
|
injectNotificationManager,
|
||||||
|
injectProjectPageContext,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import type { DropdownOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||||
|
|
||||||
|
import ModSelect from '~/components/ui/create-project-version/components/ModSelect.vue'
|
||||||
|
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||||
|
|
||||||
|
import AddedDependencyRow from '../components/AddedDependencyRow.vue'
|
||||||
|
import SuggestedDependencies from '../components/SuggestedDependencies/SuggestedDependencies.vue'
|
||||||
|
|
||||||
|
const { addNotification } = injectNotificationManager()
|
||||||
|
const { labrinth } = injectModrinthClient()
|
||||||
|
|
||||||
|
const errorNotification = (err: any) => {
|
||||||
|
addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: err.data ? err.data.description : err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }>
|
||||||
|
>([])
|
||||||
|
|
||||||
|
// reset to defaults when select different project
|
||||||
|
watch(newDependencyProjectId, async () => {
|
||||||
|
newDependencyVersionId.value = null
|
||||||
|
newDependencyType.value = 'required'
|
||||||
|
|
||||||
|
if (!newDependencyProjectId.value) {
|
||||||
|
newDependencyVersions.value = []
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const versions = await labrinth.versions_v3.getProjectVersions(newDependencyProjectId.value)
|
||||||
|
newDependencyVersions.value = versions.map((version) => ({
|
||||||
|
label: version.name,
|
||||||
|
value: version.id,
|
||||||
|
}))
|
||||||
|
} catch (error: any) {
|
||||||
|
errorNotification(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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 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,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
addNotification({
|
||||||
|
title: 'Dependency already added',
|
||||||
|
text: 'You cannot add the same dependency twice.',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectsFetchLoading.value = true
|
||||||
|
draftVersion.value.dependencies.push(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,
|
||||||
|
version_id: dependency.version_id,
|
||||||
|
dependency_type: dependency.dependency_type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex 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', 'alpha', 'beta']"
|
||||||
|
: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="32"
|
||||||
|
/>
|
||||||
|
</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 draftVersion.loaders.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 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">
|
||||||
|
<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, TagItem } 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 editLoaders = () => {
|
||||||
|
modal.value?.setStage('edit-loaders')
|
||||||
|
}
|
||||||
|
const editVersions = () => {
|
||||||
|
modal.value?.setStage('edit-mc-versions')
|
||||||
|
}
|
||||||
|
const editEnvironment = () => {
|
||||||
|
modal.value?.setStage('edit-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 environmentCopy = computed(() => {
|
||||||
|
const emptyMessage = {
|
||||||
|
title: 'No environment set',
|
||||||
|
description: 'The environment for this version has not been specified.',
|
||||||
|
}
|
||||||
|
if (!draftVersion.value.environment) return emptyMessage
|
||||||
|
|
||||||
|
const envCopy: Record<string, { title: string; description: string }> = {
|
||||||
|
client_only: {
|
||||||
|
title: 'Client-side only',
|
||||||
|
description: 'All functionality is done client-side and is compatible with vanilla servers.',
|
||||||
|
},
|
||||||
|
server_only: {
|
||||||
|
title: 'Server-side only',
|
||||||
|
description: 'All functionality is done server-side and is compatible with vanilla clients.',
|
||||||
|
},
|
||||||
|
singleplayer_only: {
|
||||||
|
title: 'Singleplayer only',
|
||||||
|
description: 'Only functions in Singleplayer or when not connected to a Multiplayer server.',
|
||||||
|
},
|
||||||
|
dedicated_server_only: {
|
||||||
|
title: 'Server-side only',
|
||||||
|
description: 'All functionality is done server-side and is compatible with vanilla clients.',
|
||||||
|
},
|
||||||
|
client_and_server: {
|
||||||
|
title: 'Client and server',
|
||||||
|
description: 'Has some functionality on both the client and server, even if only partially.',
|
||||||
|
},
|
||||||
|
client_only_server_optional: {
|
||||||
|
title: 'Client and server',
|
||||||
|
description: 'Has some functionality on both the client and server, even if only partially.',
|
||||||
|
},
|
||||||
|
server_only_client_optional: {
|
||||||
|
title: 'Client and server',
|
||||||
|
description: 'Has some functionality on both the client and server, even if only partially.',
|
||||||
|
},
|
||||||
|
client_or_server: {
|
||||||
|
title: 'Client and server',
|
||||||
|
description: 'Has some functionality on both the client and server, even if only partially.',
|
||||||
|
},
|
||||||
|
client_or_server_prefers_both: {
|
||||||
|
title: 'Client and server',
|
||||||
|
description: 'Has some functionality on both the client and server, even if only partially.',
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
title: 'Unknown environment',
|
||||||
|
description: 'The environment for this version could not be determined.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
envCopy[draftVersion.value.environment] || {
|
||||||
|
title: 'Unknown environment',
|
||||||
|
description: `The environment: "${draftVersion.value.environment}" is not recognized.`,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<ProjectSettingsEnvSelector v-model="draftVersion.environment" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ProjectSettingsEnvSelector } from '@modrinth/ui'
|
||||||
|
|
||||||
|
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||||
|
|
||||||
|
const { draftVersion } = injectManageVersionContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex w-full flex-col gap-4">
|
||||||
|
<template v-if="!(filesToAdd.length || draftVersion.existing_files?.length)">
|
||||||
|
<DropzoneFileInput
|
||||||
|
aria-label="Upload file"
|
||||||
|
multiple
|
||||||
|
:accept="acceptFileFromProjectType(projectV2.project_type)"
|
||||||
|
:max-size="524288000"
|
||||||
|
@change="handleNewFiles"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-base font-semibold text-contrast">Primary file</span>
|
||||||
|
<div class="flex flex-col gap-2.5">
|
||||||
|
<VersionFileRow
|
||||||
|
v-if="primaryFile"
|
||||||
|
:key="primaryFile.name"
|
||||||
|
:name="primaryFile.name"
|
||||||
|
:is-primary="true"
|
||||||
|
:editing-version="editingVersion"
|
||||||
|
:on-remove="undefined"
|
||||||
|
@set-primary-file="
|
||||||
|
(file) => {
|
||||||
|
if (file && !editingVersion) filesToAdd[0] = { file }
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
The primary file is the default file a user downloads when installing the project.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Admonition v-if="hasSupplementaryFiles" type="warning">
|
||||||
|
{{ formatMessage(messages.addFilesAdmonition) }}
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
|
<span class="text-base font-semibold text-contrast">Supplementary files</span>
|
||||||
|
|
||||||
|
<DropzoneFileInput
|
||||||
|
aria-label="Upload additional file"
|
||||||
|
multiple
|
||||||
|
:accept="acceptFileFromProjectType(projectV2.project_type)"
|
||||||
|
:max-size="524288000"
|
||||||
|
size="small"
|
||||||
|
:primary-prompt="null"
|
||||||
|
secondary-prompt="Drag and drop files or click to browse"
|
||||||
|
@change="handleNewFiles"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="hasSupplementaryFiles" class="flex flex-col gap-2.5">
|
||||||
|
<VersionFileRow
|
||||||
|
v-for="versionFile in supplementaryExistingFiles"
|
||||||
|
:key="versionFile.filename"
|
||||||
|
:name="versionFile.filename"
|
||||||
|
:is-primary="false"
|
||||||
|
:initial-file-type="versionFile.file_type"
|
||||||
|
:editing-version="editingVersion"
|
||||||
|
:on-remove="() => handleRemoveExistingFile(versionFile.hashes.sha1 || '')"
|
||||||
|
@set-file-type="(type) => (versionFile.file_type = type)"
|
||||||
|
/>
|
||||||
|
<VersionFileRow
|
||||||
|
v-for="(versionFile, idx) in supplementaryNewFiles"
|
||||||
|
:key="versionFile.file.name"
|
||||||
|
:name="versionFile.file.name"
|
||||||
|
:is-primary="false"
|
||||||
|
:initial-file-type="versionFile.fileType"
|
||||||
|
: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))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
You can optionally add supplementary files such as source code, documentation, or required
|
||||||
|
resource packs.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { Admonition, DropzoneFileInput, injectProjectPageContext } from '@modrinth/ui'
|
||||||
|
import { acceptFileFromProjectType } from '@modrinth/utils'
|
||||||
|
|
||||||
|
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||||
|
|
||||||
|
import VersionFileRow from '../components/VersionFileRow.vue'
|
||||||
|
|
||||||
|
const { projectV2 } = injectProjectPageContext()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const {
|
||||||
|
draftVersion,
|
||||||
|
filesToAdd,
|
||||||
|
existingFilesToDelete,
|
||||||
|
setPrimaryFile,
|
||||||
|
setInferredVersionData,
|
||||||
|
setProjectType,
|
||||||
|
editingVersion,
|
||||||
|
projectType,
|
||||||
|
} = 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
setProjectType(projectV2.value, primaryFile)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing version file data', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectType.value === 'resourcepack') {
|
||||||
|
draftVersion.value.loaders = ['minecraft']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
(file) => file.hashes.sha1 !== sha1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
} 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 hasSupplementaryFiles = computed(
|
||||||
|
() => filesToAdd.value.length + (draftVersion.value.existing_files?.length || 0) > 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
addFilesAdmonition: {
|
||||||
|
id: 'create-project-version.create-modal.stage.add-files.admonition',
|
||||||
|
defaultMessage:
|
||||||
|
'Supplementary files are for supporting resources like source code, not for alternative versions or variants.',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<LoaderPicker
|
||||||
|
v-model="draftVersion.loaders"
|
||||||
|
:loaders="generatedState.loaders"
|
||||||
|
:toggle-loader="toggleLoader"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="draftVersion.loaders.length" class="space-y-1">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="font-semibold text-contrast"> Added loaders </span>
|
||||||
|
<ButtonStyled type="transparent" size="standard">
|
||||||
|
<button @click="onClearAll()">Clear all</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 draftVersion.loaders.map((loaderName) =>
|
||||||
|
loaders.find((loader) => loaderName === loader.name),
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<TagItem
|
||||||
|
v-if="loader"
|
||||||
|
:key="`loader-${loader.name}`"
|
||||||
|
:action="() => toggleLoader(loader.name)"
|
||||||
|
class="border !border-solid border-surface-5 !transition-all hover:bg-button-bgHover hover:no-underline"
|
||||||
|
:style="`--_color: var(--color-platform-${loader.name})`"
|
||||||
|
>
|
||||||
|
<div v-html="loader.icon"></div>
|
||||||
|
{{ formatCategory(loader.name) }}
|
||||||
|
<XIcon class="text-secondary" />
|
||||||
|
</TagItem>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { XIcon } from '@modrinth/assets'
|
||||||
|
import { ButtonStyled, injectProjectPageContext, TagItem } from '@modrinth/ui'
|
||||||
|
import { formatCategory } from '@modrinth/utils'
|
||||||
|
|
||||||
|
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||||
|
|
||||||
|
import LoaderPicker from '../components/LoaderPicker.vue'
|
||||||
|
|
||||||
|
const generatedState = useGeneratedState()
|
||||||
|
|
||||||
|
const { projectV2 } = injectProjectPageContext()
|
||||||
|
const loaders = computed(() => generatedState.value.loaders)
|
||||||
|
|
||||||
|
const { draftVersion, setProjectType } = injectManageVersionContext()
|
||||||
|
|
||||||
|
const toggleLoader = (loader: string) => {
|
||||||
|
if (draftVersion.value.loaders.includes(loader)) {
|
||||||
|
draftVersion.value.loaders = draftVersion.value.loaders.filter((l) => l !== loader)
|
||||||
|
} else {
|
||||||
|
draftVersion.value.loaders = [...draftVersion.value.loaders, loader]
|
||||||
|
}
|
||||||
|
setProjectType(projectV2.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClearAll = () => {
|
||||||
|
draftVersion.value.loaders = []
|
||||||
|
setProjectType(projectV2.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<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">
|
||||||
|
<span class="font-semibold text-contrast"> Added versions </span>
|
||||||
|
<ButtonStyled type="transparent" size="standard">
|
||||||
|
<button @click="clearAllVersions()">Clear all</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">
|
||||||
|
<template v-if="draftVersion.game_versions.length">
|
||||||
|
<TagItem
|
||||||
|
v-for="version in draftVersion.game_versions"
|
||||||
|
:key="version"
|
||||||
|
:action="() => toggleVersion(version)"
|
||||||
|
class="border !border-solid border-surface-5 !transition-all hover:bg-button-bgHover hover:no-underline"
|
||||||
|
>
|
||||||
|
{{ version }}
|
||||||
|
<XIcon />
|
||||||
|
</TagItem>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span>No versions selected.</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { XIcon } from '@modrinth/assets'
|
||||||
|
import { ButtonStyled, TagItem } from '@modrinth/ui'
|
||||||
|
|
||||||
|
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||||
|
|
||||||
|
import McVersionPicker from '../components/McVersionPicker.vue'
|
||||||
|
|
||||||
|
const generatedState = useGeneratedState()
|
||||||
|
const gameVersions = generatedState.value.gameVersions
|
||||||
|
|
||||||
|
const { draftVersion } = injectManageVersionContext()
|
||||||
|
|
||||||
|
const toggleVersion = (version: string) => {
|
||||||
|
if (draftVersion.value.game_versions.includes(version)) {
|
||||||
|
draftVersion.value.game_versions = draftVersion.value.game_versions.filter((v) => v !== version)
|
||||||
|
} else {
|
||||||
|
draftVersion.value.game_versions.push(version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearAllVersions = () => {
|
||||||
|
draftVersion.value.game_versions = []
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -247,13 +247,7 @@ async function createProject() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
await router.push({
|
await router.push(`/project/${slug.value}/settings`)
|
||||||
name: 'type-id',
|
|
||||||
params: {
|
|
||||||
type: 'project',
|
|
||||||
id: slug.value,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addNotification({
|
addNotification({
|
||||||
title: formatMessage(messages.errorTitle),
|
title: formatMessage(messages.errorTitle),
|
||||||
|
|||||||
@@ -437,6 +437,9 @@
|
|||||||
"common.yes": {
|
"common.yes": {
|
||||||
"message": "Yes"
|
"message": "Yes"
|
||||||
},
|
},
|
||||||
|
"create-project-version.create-modal.stage.add-files.admonition": {
|
||||||
|
"message": "Supplementary files are for supporting resources like source code, not for alternative versions or variants."
|
||||||
|
},
|
||||||
"create.collection.cancel": {
|
"create.collection.cancel": {
|
||||||
"message": "Cancel"
|
"message": "Cancel"
|
||||||
},
|
},
|
||||||
@@ -2246,12 +2249,6 @@
|
|||||||
"project.status.archived.message": {
|
"project.status.archived.message": {
|
||||||
"message": "{title} has been archived. {title} will not receive any further updates unless the author decides to unarchive the project."
|
"message": "{title} has been archived. {title} will not receive any further updates unless the author decides to unarchive the project."
|
||||||
},
|
},
|
||||||
"project.version.all-versions": {
|
|
||||||
"message": "All versions"
|
|
||||||
},
|
|
||||||
"project.version.back-to-versions": {
|
|
||||||
"message": "Back to versions"
|
|
||||||
},
|
|
||||||
"project.versions.title": {
|
"project.versions.title": {
|
||||||
"message": "Versions"
|
"message": "Versions"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,10 +32,7 @@
|
|||||||
:versions="versions"
|
:versions="versions"
|
||||||
:current-member="currentMember"
|
:current-member="currentMember"
|
||||||
:is-settings="route.name.startsWith('type-id-settings')"
|
:is-settings="route.name.startsWith('type-id-settings')"
|
||||||
:route-name="route.name"
|
|
||||||
:set-processing="setProcessing"
|
:set-processing="setProcessing"
|
||||||
:collapsed="collapsedChecklist"
|
|
||||||
:toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
|
|
||||||
:all-members="allMembers"
|
:all-members="allMembers"
|
||||||
:update-members="updateMembers"
|
:update-members="updateMembers"
|
||||||
:auth="auth"
|
:auth="auth"
|
||||||
@@ -55,6 +52,7 @@
|
|||||||
:patch-project="patchProject"
|
:patch-project="patchProject"
|
||||||
:patch-icon="patchIcon"
|
:patch-icon="patchIcon"
|
||||||
:reset-project="resetProject"
|
:reset-project="resetProject"
|
||||||
|
:reset-versions="resetVersions"
|
||||||
:reset-organization="resetOrganization"
|
:reset-organization="resetOrganization"
|
||||||
:reset-members="resetMembers"
|
:reset-members="resetMembers"
|
||||||
:route="route"
|
:route="route"
|
||||||
@@ -447,14 +445,34 @@
|
|||||||
<div class="normal-page__header relative my-4">
|
<div class="normal-page__header relative my-4">
|
||||||
<ProjectHeader :project="project" :member="!!currentMember">
|
<ProjectHeader :project="project" :member="!!currentMember">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
|
<ButtonStyled v-if="auth.user && currentMember" size="large" color="brand">
|
||||||
|
<nuxt-link
|
||||||
|
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
||||||
|
class="!font-bold"
|
||||||
|
>
|
||||||
|
<SettingsIcon aria-hidden="true" />
|
||||||
|
Edit project
|
||||||
|
</nuxt-link>
|
||||||
|
</ButtonStyled>
|
||||||
|
|
||||||
<div class="hidden sm:contents">
|
<div class="hidden sm:contents">
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
|
v-tooltip="
|
||||||
|
auth.user && currentMember ? formatMessage(commonMessages.downloadButton) : ''
|
||||||
|
"
|
||||||
size="large"
|
size="large"
|
||||||
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
|
:color="
|
||||||
|
(auth.user && currentMember) || route.name === 'type-id-version-version'
|
||||||
|
? `standard`
|
||||||
|
: `brand`
|
||||||
|
"
|
||||||
|
:circular="auth.user && currentMember"
|
||||||
>
|
>
|
||||||
<button @click="(event) => downloadModal.show(event)">
|
<button @click="(event) => downloadModal.show(event)">
|
||||||
<DownloadIcon aria-hidden="true" />
|
<DownloadIcon aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.downloadButton) }}
|
{{
|
||||||
|
auth.user && currentMember ? '' : formatMessage(commonMessages.downloadButton)
|
||||||
|
}}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@@ -641,14 +659,7 @@
|
|||||||
<BookmarkIcon aria-hidden="true" />
|
<BookmarkIcon aria-hidden="true" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-if="auth.user && currentMember" size="large" circular>
|
|
||||||
<nuxt-link
|
|
||||||
v-tooltip="formatMessage(commonMessages.settingsLabel)"
|
|
||||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
|
||||||
>
|
|
||||||
<SettingsIcon aria-hidden="true" />
|
|
||||||
</nuxt-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled size="large" circular type="transparent">
|
<ButtonStyled size="large" circular type="transparent">
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:tooltip="formatMessage(commonMessages.moreOptionsButton)"
|
:tooltip="formatMessage(commonMessages.moreOptionsButton)"
|
||||||
@@ -903,6 +914,7 @@
|
|||||||
v-model:organization="organization"
|
v-model:organization="organization"
|
||||||
:current-member="currentMember"
|
:current-member="currentMember"
|
||||||
:reset-project="resetProject"
|
:reset-project="resetProject"
|
||||||
|
:reset-versions="resetVersions"
|
||||||
:reset-organization="resetOrganization"
|
:reset-organization="resetOrganization"
|
||||||
:reset-members="resetMembers"
|
:reset-members="resetMembers"
|
||||||
:route="route"
|
:route="route"
|
||||||
@@ -1446,6 +1458,7 @@ let project,
|
|||||||
resetMembers,
|
resetMembers,
|
||||||
dependencies,
|
dependencies,
|
||||||
versions,
|
versions,
|
||||||
|
resetVersions,
|
||||||
organization,
|
organization,
|
||||||
resetOrganization,
|
resetOrganization,
|
||||||
projectV2Error,
|
projectV2Error,
|
||||||
@@ -1459,7 +1472,7 @@ try {
|
|||||||
{ data: projectV3, error: projectV3Error, refresh: resetProjectV3 },
|
{ data: projectV3, error: projectV3Error, refresh: resetProjectV3 },
|
||||||
{ data: allMembers, error: membersError, refresh: resetMembers },
|
{ data: allMembers, error: membersError, refresh: resetMembers },
|
||||||
{ data: dependencies, error: dependenciesError },
|
{ data: dependencies, error: dependenciesError },
|
||||||
{ data: versions, error: versionsError },
|
{ data: versions, error: versionsError, refresh: resetVersions },
|
||||||
{ data: organization, refresh: resetOrganization },
|
{ data: organization, refresh: resetOrganization },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
useAsyncData(`project/${projectId.value}`, () => useBaseFetch(`project/${projectId.value}`), {
|
useAsyncData(`project/${projectId.value}`, () => useBaseFetch(`project/${projectId.value}`), {
|
||||||
@@ -1917,6 +1930,7 @@ provideProjectPageContext({
|
|||||||
projectV2: project,
|
projectV2: project,
|
||||||
projectV3,
|
projectV3,
|
||||||
refreshProject: resetProject,
|
refreshProject: resetProject,
|
||||||
|
refreshVersions: resetVersions,
|
||||||
currentMember,
|
currentMember,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -195,28 +195,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentMember" class="card header-buttons">
|
<Admonition v-if="!hideGalleryAdmonition" type="info" class="mb-4">
|
||||||
<FileInput
|
Managing gallery has moved! You can now add and edit gallery images in the
|
||||||
:max-size="5242880"
|
<NuxtLink to="settings/gallery" class="font-medium text-blue hover:underline"
|
||||||
:accept="acceptFileTypes"
|
>project settings</NuxtLink
|
||||||
prompt="Upload an image"
|
>.
|
||||||
aria-label="Upload an image"
|
<template #actions>
|
||||||
class="iconified-button brand-button"
|
<div class="flex gap-2">
|
||||||
:disabled="!isPermission(currentMember?.permissions, 1 << 2)"
|
<ButtonStyled color="blue">
|
||||||
@change="handleFiles"
|
<button
|
||||||
>
|
aria-label="Project Settings"
|
||||||
<UploadIcon aria-hidden="true" />
|
class="!shadow-none"
|
||||||
</FileInput>
|
@click="() => $router.push('settings/gallery')"
|
||||||
<span class="indicator">
|
>
|
||||||
<InfoIcon aria-hidden="true" /> Click to choose an image or drag one onto this page
|
<SettingsIcon />
|
||||||
</span>
|
Edit gallery
|
||||||
<DropArea
|
</button>
|
||||||
:accept="acceptFileTypes"
|
</ButtonStyled>
|
||||||
:disabled="!isPermission(currentMember?.permissions, 1 << 2)"
|
<ButtonStyled type="transparent">
|
||||||
@change="handleFiles"
|
<button
|
||||||
/>
|
aria-label="Dismiss"
|
||||||
</div>
|
class="!shadow-none"
|
||||||
<div class="items">
|
@click="() => (hideGalleryAdmonition = true)"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Admonition>
|
||||||
|
<div v-if="project.gallery.length" class="items">
|
||||||
<div v-for="(item, index) in project.gallery" :key="index" class="card gallery-item">
|
<div v-for="(item, index) in project.gallery" :key="index" class="card gallery-item">
|
||||||
<a class="gallery-thumbnail" @click="expandImage(item, index)">
|
<a class="gallery-thumbnail" @click="expandImage(item, index)">
|
||||||
<img
|
<img
|
||||||
@@ -239,40 +247,18 @@
|
|||||||
<CalendarIcon aria-hidden="true" aria-label="Date created" />
|
<CalendarIcon aria-hidden="true" aria-label="Date created" />
|
||||||
{{ $dayjs(item.created).format('MMMM D, YYYY') }}
|
{{ $dayjs(item.created).format('MMMM D, YYYY') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentMember" class="gallery-buttons input-group">
|
|
||||||
<button
|
|
||||||
class="iconified-button"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
resetEdit()
|
|
||||||
editIndex = index
|
|
||||||
editTitle = item.title
|
|
||||||
editDescription = item.description
|
|
||||||
editFeatured = item.featured
|
|
||||||
editOrder = item.ordering
|
|
||||||
$refs.modal_edit_item.show()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<EditIcon aria-hidden="true" />
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="iconified-button"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
deleteIndex = index
|
|
||||||
$refs.modal_confirm.show()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<TrashIcon aria-hidden="true" />
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<p class="ml-2">
|
||||||
|
No images in gallery. Visit
|
||||||
|
<NuxtLink to="settings/gallery">
|
||||||
|
<span class="font-medium text-green hover:underline">project settings</span> to
|
||||||
|
</NuxtLink>
|
||||||
|
upload images.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -280,30 +266,27 @@
|
|||||||
import {
|
import {
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
ContractIcon,
|
ContractIcon,
|
||||||
EditIcon,
|
|
||||||
ExpandIcon,
|
ExpandIcon,
|
||||||
ExternalIcon,
|
ExternalIcon,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
InfoIcon,
|
|
||||||
LeftArrowIcon,
|
LeftArrowIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
RightArrowIcon,
|
RightArrowIcon,
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
|
SettingsIcon,
|
||||||
StarIcon,
|
StarIcon,
|
||||||
TransferIcon,
|
TransferIcon,
|
||||||
TrashIcon,
|
|
||||||
UploadIcon,
|
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
|
Admonition,
|
||||||
|
ButtonStyled,
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
DropArea,
|
|
||||||
FileInput,
|
FileInput,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
NewModal as Modal,
|
NewModal as Modal,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
|
import { useLocalStorage } from '@vueuse/core'
|
||||||
import { isPermission } from '~/utils/permissions.ts'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
project: {
|
project: {
|
||||||
@@ -334,6 +317,11 @@ useSeoMeta({
|
|||||||
ogTitle: title,
|
ogTitle: title,
|
||||||
ogDescription: description,
|
ogDescription: description,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hideGalleryAdmonition = useLocalStorage(
|
||||||
|
'hideGalleryHasMovedAdmonition',
|
||||||
|
!props.project.gallery.length,
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -3,12 +3,21 @@
|
|||||||
<div v-if="project.body" class="card">
|
<div v-if="project.body" class="card">
|
||||||
<ProjectPageDescription :description="project.body" />
|
<ProjectPageDescription :description="project.body" />
|
||||||
</div>
|
</div>
|
||||||
|
<p v-else class="ml-2">
|
||||||
|
No description provided. Visit
|
||||||
|
<NuxtLink :to="`${route.fullPath}/settings/description`">
|
||||||
|
<span class="font-medium text-green hover:underline">project settings</span> to
|
||||||
|
</NuxtLink>
|
||||||
|
add your description.
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ProjectPageDescription } from '@modrinth/ui'
|
import { ProjectPageDescription } from '@modrinth/ui'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
project: {
|
project: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
AlignLeftIcon,
|
AlignLeftIcon,
|
||||||
BookTextIcon,
|
BookTextIcon,
|
||||||
ChartIcon,
|
ChartIcon,
|
||||||
GlobeIcon,
|
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
@@ -11,11 +10,17 @@ import {
|
|||||||
UsersIcon,
|
UsersIcon,
|
||||||
VersionIcon,
|
VersionIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { commonMessages, commonProjectSettingsMessages } from '@modrinth/ui'
|
import {
|
||||||
|
commonMessages,
|
||||||
|
commonProjectSettingsMessages,
|
||||||
|
injectNotificationManager,
|
||||||
|
} from '@modrinth/ui'
|
||||||
import type { Project, ProjectV3Partial } from '@modrinth/utils'
|
import type { Project, ProjectV3Partial } from '@modrinth/utils'
|
||||||
import { useVIntl } from '@vintl/vintl'
|
import { useVIntl } from '@vintl/vintl'
|
||||||
|
import { useLocalStorage, useScroll } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import ModerationProjectNags from '~/components/ui/moderation/ModerationProjectNags.vue'
|
||||||
import NavStack from '~/components/ui/NavStack.vue'
|
import NavStack from '~/components/ui/NavStack.vue'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
@@ -25,6 +30,7 @@ defineProps<{
|
|||||||
patchProject: any
|
patchProject: any
|
||||||
patchIcon: any
|
patchIcon: any
|
||||||
resetProject: any
|
resetProject: any
|
||||||
|
resetVersions: any
|
||||||
resetOrganization: any
|
resetOrganization: any
|
||||||
resetMembers: any
|
resetMembers: any
|
||||||
}>()
|
}>()
|
||||||
@@ -55,15 +61,6 @@ const navItems = computed(() => {
|
|||||||
icon: InfoIcon,
|
icon: InfoIcon,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
flags.value.newProjectEnvironmentSettings &&
|
|
||||||
projectV3.value.project_types.some((type: string) => ['mod', 'modpack'].includes(type))
|
|
||||||
? {
|
|
||||||
link: `/${base}/settings/environment`,
|
|
||||||
label: formatMessage(commonProjectSettingsMessages.environment),
|
|
||||||
badge: formatMessage(commonMessages.newBadge),
|
|
||||||
icon: GlobeIcon,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
{
|
{
|
||||||
link: `/${base}/settings/tags`,
|
link: `/${base}/settings/tags`,
|
||||||
label: formatMessage(commonProjectSettingsMessages.tags),
|
label: formatMessage(commonProjectSettingsMessages.tags),
|
||||||
@@ -74,11 +71,21 @@ const navItems = computed(() => {
|
|||||||
label: formatMessage(commonProjectSettingsMessages.description),
|
label: formatMessage(commonProjectSettingsMessages.description),
|
||||||
icon: AlignLeftIcon,
|
icon: AlignLeftIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
link: `/${base}/settings/versions`,
|
||||||
|
label: formatMessage(commonProjectSettingsMessages.versions),
|
||||||
|
icon: VersionIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
link: `/${base}/settings/license`,
|
link: `/${base}/settings/license`,
|
||||||
label: formatMessage(commonProjectSettingsMessages.license),
|
label: formatMessage(commonProjectSettingsMessages.license),
|
||||||
icon: BookTextIcon,
|
icon: BookTextIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
link: `/${base}/settings/gallery`,
|
||||||
|
label: formatMessage(commonProjectSettingsMessages.gallery),
|
||||||
|
icon: ImageIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
link: `/${base}/settings/links`,
|
link: `/${base}/settings/links`,
|
||||||
label: formatMessage(commonProjectSettingsMessages.links),
|
label: formatMessage(commonProjectSettingsMessages.links),
|
||||||
@@ -89,51 +96,91 @@ const navItems = computed(() => {
|
|||||||
label: formatMessage(commonProjectSettingsMessages.members),
|
label: formatMessage(commonProjectSettingsMessages.members),
|
||||||
icon: UsersIcon,
|
icon: UsersIcon,
|
||||||
},
|
},
|
||||||
{ type: 'heading', label: formatMessage(commonProjectSettingsMessages.view) },
|
|
||||||
{
|
{
|
||||||
link: `/${base}/settings/analytics`,
|
link: `/${base}/settings/analytics`,
|
||||||
label: formatMessage(commonProjectSettingsMessages.analytics),
|
label: formatMessage(commonProjectSettingsMessages.analytics),
|
||||||
icon: ChartIcon,
|
icon: ChartIcon,
|
||||||
chevron: true,
|
|
||||||
},
|
|
||||||
{ type: 'heading', label: formatMessage(commonProjectSettingsMessages.upload) },
|
|
||||||
{
|
|
||||||
link: `/${base}/gallery`,
|
|
||||||
label: formatMessage(commonProjectSettingsMessages.gallery),
|
|
||||||
icon: ImageIcon,
|
|
||||||
chevron: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: `/${base}/versions`,
|
|
||||||
label: formatMessage(commonProjectSettingsMessages.versions),
|
|
||||||
icon: VersionIcon,
|
|
||||||
chevron: true,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
return items.filter(Boolean) as any[]
|
return items.filter(Boolean) as any[]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
|
const tags = useGeneratedState()
|
||||||
|
const route = useRoute()
|
||||||
|
const collapsedChecklist = useLocalStorage(`project-checklist-collapsed-${project.value.id}`, false)
|
||||||
|
|
||||||
|
async function setProcessing() {
|
||||||
|
startLoading()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await useBaseFetch(`project/${project.value.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
status: 'processing',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
project.value.status = 'processing'
|
||||||
|
} catch (err: any) {
|
||||||
|
addNotification({
|
||||||
|
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||||
|
text: err.data ? err.data.description : err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stopLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
// To persist scroll position through settings pages
|
||||||
|
// This scroll code is jank asf, if anyone has a better way please do suggest it
|
||||||
|
const scroll = useScroll(window)
|
||||||
|
watch(route, () => {
|
||||||
|
const scrollY = scroll.y.value
|
||||||
|
setTimeout(() => window.scrollTo(0, scrollY), 10)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="experimental-styles-within grid gap-4 lg:grid-cols-[1fr_3fr]">
|
<div class="mb-8 flex w-full flex-col gap-4">
|
||||||
<div>
|
<ModerationProjectNags
|
||||||
<NavStack :items="navItems" />
|
v-if="
|
||||||
</div>
|
(currentMember && project.status === 'draft') ||
|
||||||
<div class="min-w-0">
|
tags.rejectedStatuses.includes(project.status)
|
||||||
<NuxtPage
|
"
|
||||||
v-model:project="project"
|
:project="project"
|
||||||
v-model:project-v3="projectV3"
|
:versions="versions"
|
||||||
v-model:versions="versions"
|
:current-member="currentMember"
|
||||||
v-model:members="members"
|
:collapsed="collapsedChecklist"
|
||||||
v-model:all-members="allMembers"
|
:route-name="route.name as string"
|
||||||
v-model:dependencies="dependencies"
|
:tags="tags"
|
||||||
v-model:organization="organization"
|
@toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
|
||||||
:current-member="currentMember"
|
@set-processing="setProcessing"
|
||||||
:patch-project="patchProject"
|
/>
|
||||||
:patch-icon="patchIcon"
|
<div class="experimental-styles-within grid gap-4 lg:grid-cols-[1fr_3fr]">
|
||||||
:reset-project="resetProject"
|
<div>
|
||||||
:reset-organization="resetOrganization"
|
<NavStack :items="navItems" />
|
||||||
:reset-members="resetMembers"
|
</div>
|
||||||
/>
|
<div class="min-w-0">
|
||||||
|
<NuxtPage
|
||||||
|
v-model:project="project"
|
||||||
|
v-model:project-v3="projectV3"
|
||||||
|
v-model:versions="versions"
|
||||||
|
v-model:members="members"
|
||||||
|
v-model:all-members="allMembers"
|
||||||
|
v-model:dependencies="dependencies"
|
||||||
|
v-model:organization="organization"
|
||||||
|
:current-member="currentMember"
|
||||||
|
:patch-project="patchProject"
|
||||||
|
:patch-icon="patchIcon"
|
||||||
|
:reset-project="resetProject"
|
||||||
|
:reset-versions="resetVersions"
|
||||||
|
:reset-organization="resetOrganization"
|
||||||
|
:reset-members="resetMembers"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
810
apps/frontend/src/pages/[type]/[id]/settings/gallery.vue
Normal file
810
apps/frontend/src/pages/[type]/[id]/settings/gallery.vue
Normal file
@@ -0,0 +1,810 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Modal
|
||||||
|
v-if="currentMember"
|
||||||
|
ref="modal_edit_item"
|
||||||
|
:header="editIndex === -1 ? 'Upload gallery image' : 'Edit gallery item'"
|
||||||
|
>
|
||||||
|
<div class="modal-gallery universal-labels">
|
||||||
|
<div class="gallery-file-input">
|
||||||
|
<div class="file-header">
|
||||||
|
<ImageIcon aria-hidden="true" />
|
||||||
|
<strong>{{ editFile ? editFile.name : 'Current image' }}</strong>
|
||||||
|
<FileInput
|
||||||
|
v-if="editIndex === -1"
|
||||||
|
class="iconified-button raised-button"
|
||||||
|
prompt="Replace"
|
||||||
|
:accept="acceptFileTypes"
|
||||||
|
:max-size="5242880"
|
||||||
|
should-always-reset
|
||||||
|
aria-label="Replace image"
|
||||||
|
@change="
|
||||||
|
(x) => {
|
||||||
|
editFile = x[0]
|
||||||
|
showPreviewImage()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<TransferIcon aria-hidden="true" />
|
||||||
|
</FileInput>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
:src="
|
||||||
|
previewImage
|
||||||
|
? previewImage
|
||||||
|
: project.gallery[editIndex] && project.gallery[editIndex].url
|
||||||
|
? project.gallery[editIndex].url
|
||||||
|
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||||
|
"
|
||||||
|
alt="gallery-preview"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label for="gallery-image-title">
|
||||||
|
<span class="label__title">Title</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="gallery-image-title"
|
||||||
|
v-model="editTitle"
|
||||||
|
type="text"
|
||||||
|
maxlength="64"
|
||||||
|
placeholder="Enter title..."
|
||||||
|
/>
|
||||||
|
<label for="gallery-image-desc">
|
||||||
|
<span class="label__title">Description</span>
|
||||||
|
</label>
|
||||||
|
<div class="textarea-wrapper">
|
||||||
|
<textarea
|
||||||
|
id="gallery-image-desc"
|
||||||
|
v-model="editDescription"
|
||||||
|
maxlength="255"
|
||||||
|
placeholder="Enter description..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label for="gallery-image-ordering">
|
||||||
|
<span class="label__title">Order Index</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="gallery-image-ordering"
|
||||||
|
v-model="editOrder"
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter order index..."
|
||||||
|
/>
|
||||||
|
<label for="gallery-image-featured">
|
||||||
|
<span class="label__title">Featured</span>
|
||||||
|
<span class="label__description">
|
||||||
|
A featured gallery image shows up in search and your project card. Only one gallery
|
||||||
|
image can be featured.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
v-if="!editFeatured"
|
||||||
|
id="gallery-image-featured"
|
||||||
|
class="iconified-button"
|
||||||
|
@click="editFeatured = true"
|
||||||
|
>
|
||||||
|
<StarIcon aria-hidden="true" />
|
||||||
|
Feature image
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
id="gallery-image-featured"
|
||||||
|
class="iconified-button"
|
||||||
|
@click="editFeatured = false"
|
||||||
|
>
|
||||||
|
<StarIcon fill="currentColor" aria-hidden="true" />
|
||||||
|
Unfeature image
|
||||||
|
</button>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="iconified-button" @click="$refs.modal_edit_item.hide()">
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="editIndex === -1"
|
||||||
|
class="iconified-button brand-button"
|
||||||
|
:disabled="shouldPreventActions"
|
||||||
|
@click="createGalleryItem"
|
||||||
|
>
|
||||||
|
<PlusIcon aria-hidden="true" />
|
||||||
|
Add gallery image
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="iconified-button brand-button"
|
||||||
|
:disabled="shouldPreventActions"
|
||||||
|
@click="editGalleryItem"
|
||||||
|
>
|
||||||
|
<SaveIcon aria-hidden="true" />
|
||||||
|
Save changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<ConfirmModal
|
||||||
|
v-if="currentMember"
|
||||||
|
ref="modal_confirm"
|
||||||
|
title="Are you sure you want to delete this gallery image?"
|
||||||
|
description="This will remove this gallery image forever (like really forever)."
|
||||||
|
:has-to-type="false"
|
||||||
|
proceed-label="Delete"
|
||||||
|
@proceed="deleteGalleryImage"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="expandedGalleryItem != null"
|
||||||
|
class="expanded-image-modal"
|
||||||
|
@click="expandedGalleryItem = null"
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<img
|
||||||
|
class="image"
|
||||||
|
:class="{ 'zoomed-in': zoomedIn }"
|
||||||
|
:src="
|
||||||
|
expandedGalleryItem.raw_url
|
||||||
|
? expandedGalleryItem.raw_url
|
||||||
|
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||||
|
"
|
||||||
|
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="floating" @click.stop>
|
||||||
|
<div class="text">
|
||||||
|
<h2 v-if="expandedGalleryItem.title">
|
||||||
|
{{ expandedGalleryItem.title }}
|
||||||
|
</h2>
|
||||||
|
<p v-if="expandedGalleryItem.description">
|
||||||
|
{{ expandedGalleryItem.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="close circle-button" @click="expandedGalleryItem = null">
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
class="open circle-button"
|
||||||
|
target="_blank"
|
||||||
|
:href="
|
||||||
|
expandedGalleryItem.raw_url
|
||||||
|
? expandedGalleryItem.raw_url
|
||||||
|
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ExternalIcon aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
<button class="circle-button" @click="zoomedIn = !zoomedIn">
|
||||||
|
<ExpandIcon v-if="!zoomedIn" aria-hidden="true" />
|
||||||
|
<ContractIcon v-else aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="project.gallery.length > 1"
|
||||||
|
class="previous circle-button"
|
||||||
|
@click="previousImage()"
|
||||||
|
>
|
||||||
|
<LeftArrowIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="project.gallery.length > 1"
|
||||||
|
class="next circle-button"
|
||||||
|
@click="nextImage()"
|
||||||
|
>
|
||||||
|
<RightArrowIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="currentMember" class="card header-buttons">
|
||||||
|
<FileInput
|
||||||
|
:max-size="5242880"
|
||||||
|
:accept="acceptFileTypes"
|
||||||
|
prompt="Upload an image"
|
||||||
|
aria-label="Upload an image"
|
||||||
|
class="iconified-button brand-button"
|
||||||
|
:disabled="!isPermission(currentMember?.permissions, 1 << 2)"
|
||||||
|
@change="handleFiles"
|
||||||
|
>
|
||||||
|
<UploadIcon aria-hidden="true" />
|
||||||
|
</FileInput>
|
||||||
|
<span class="indicator">
|
||||||
|
<InfoIcon aria-hidden="true" /> Click to choose an image or drag one onto this page
|
||||||
|
</span>
|
||||||
|
<DropArea
|
||||||
|
:accept="acceptFileTypes"
|
||||||
|
:disabled="!isPermission(currentMember?.permissions, 1 << 2)"
|
||||||
|
@change="handleFiles"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="items">
|
||||||
|
<div v-for="(item, index) in project.gallery" :key="index" class="card gallery-item">
|
||||||
|
<a class="gallery-thumbnail" @click="expandImage(item, index)">
|
||||||
|
<img
|
||||||
|
:src="item.url ? item.url : 'https://cdn.modrinth.com/placeholder-banner.svg'"
|
||||||
|
:alt="item.title ? item.title : 'gallery-image'"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<div class="gallery-body">
|
||||||
|
<div class="gallery-info">
|
||||||
|
<h2 v-if="item.title">
|
||||||
|
{{ item.title }}
|
||||||
|
</h2>
|
||||||
|
<p v-if="item.description">
|
||||||
|
{{ item.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gallery-bottom">
|
||||||
|
<div class="gallery-created">
|
||||||
|
<CalendarIcon aria-hidden="true" aria-label="Date created" />
|
||||||
|
{{ $dayjs(item.created).format('MMMM D, YYYY') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="currentMember" class="gallery-buttons input-group">
|
||||||
|
<button
|
||||||
|
class="iconified-button"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
resetEdit()
|
||||||
|
editIndex = index
|
||||||
|
editTitle = item.title
|
||||||
|
editDescription = item.description
|
||||||
|
editFeatured = item.featured
|
||||||
|
editOrder = item.ordering
|
||||||
|
$refs.modal_edit_item.show()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<EditIcon aria-hidden="true" />
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="iconified-button"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
deleteIndex = index
|
||||||
|
$refs.modal_confirm.show()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<TrashIcon aria-hidden="true" />
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
CalendarIcon,
|
||||||
|
ContractIcon,
|
||||||
|
EditIcon,
|
||||||
|
ExpandIcon,
|
||||||
|
ExternalIcon,
|
||||||
|
ImageIcon,
|
||||||
|
InfoIcon,
|
||||||
|
LeftArrowIcon,
|
||||||
|
PlusIcon,
|
||||||
|
RightArrowIcon,
|
||||||
|
SaveIcon,
|
||||||
|
StarIcon,
|
||||||
|
TransferIcon,
|
||||||
|
TrashIcon,
|
||||||
|
UploadIcon,
|
||||||
|
XIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
ConfirmModal,
|
||||||
|
DropArea,
|
||||||
|
FileInput,
|
||||||
|
injectNotificationManager,
|
||||||
|
NewModal as Modal,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
|
||||||
|
import { isPermission } from '~/utils/permissions.ts'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
project: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentMember: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resetProject: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = `${props.project.title} - Gallery`
|
||||||
|
const description = `View ${props.project.gallery.length} images of ${props.project.title} on Modrinth.`
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
ogTitle: title,
|
||||||
|
ogDescription: description,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default defineNuxtComponent({
|
||||||
|
setup() {
|
||||||
|
const { addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
|
return {
|
||||||
|
addNotification,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
expandedGalleryItem: null,
|
||||||
|
expandedGalleryIndex: 0,
|
||||||
|
zoomedIn: false,
|
||||||
|
|
||||||
|
deleteIndex: -1,
|
||||||
|
|
||||||
|
editIndex: -1,
|
||||||
|
editTitle: '',
|
||||||
|
editDescription: '',
|
||||||
|
editFeatured: false,
|
||||||
|
editOrder: null,
|
||||||
|
editFile: null,
|
||||||
|
previewImage: null,
|
||||||
|
shouldPreventActions: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
acceptFileTypes() {
|
||||||
|
return 'image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this._keyListener = function (e) {
|
||||||
|
if (this.expandedGalleryItem) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
this.expandedGalleryItem = null
|
||||||
|
} else if (e.key === 'ArrowLeft') {
|
||||||
|
e.stopPropagation()
|
||||||
|
this.previousImage()
|
||||||
|
} else if (e.key === 'ArrowRight') {
|
||||||
|
e.stopPropagation()
|
||||||
|
this.nextImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', this._keyListener.bind(this))
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
nextImage() {
|
||||||
|
this.expandedGalleryIndex++
|
||||||
|
if (this.expandedGalleryIndex >= this.project.gallery.length) {
|
||||||
|
this.expandedGalleryIndex = 0
|
||||||
|
}
|
||||||
|
this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]
|
||||||
|
},
|
||||||
|
previousImage() {
|
||||||
|
this.expandedGalleryIndex--
|
||||||
|
if (this.expandedGalleryIndex < 0) {
|
||||||
|
this.expandedGalleryIndex = this.project.gallery.length - 1
|
||||||
|
}
|
||||||
|
this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]
|
||||||
|
},
|
||||||
|
expandImage(item, index) {
|
||||||
|
this.expandedGalleryItem = item
|
||||||
|
this.expandedGalleryIndex = index
|
||||||
|
this.zoomedIn = false
|
||||||
|
},
|
||||||
|
resetEdit() {
|
||||||
|
this.editIndex = -1
|
||||||
|
this.editTitle = ''
|
||||||
|
this.editDescription = ''
|
||||||
|
this.editFeatured = false
|
||||||
|
this.editOrder = null
|
||||||
|
this.editFile = null
|
||||||
|
this.previewImage = null
|
||||||
|
},
|
||||||
|
handleFiles(files) {
|
||||||
|
this.resetEdit()
|
||||||
|
this.editFile = files[0]
|
||||||
|
|
||||||
|
this.showPreviewImage()
|
||||||
|
this.$refs.modal_edit_item.show()
|
||||||
|
},
|
||||||
|
showPreviewImage() {
|
||||||
|
const reader = new FileReader()
|
||||||
|
if (this.editFile instanceof Blob) {
|
||||||
|
reader.readAsDataURL(this.editFile)
|
||||||
|
reader.onload = (event) => {
|
||||||
|
this.previewImage = event.target.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async createGalleryItem() {
|
||||||
|
this.shouldPreventActions = true
|
||||||
|
startLoading()
|
||||||
|
|
||||||
|
try {
|
||||||
|
let url = `project/${this.project.id}/gallery?ext=${
|
||||||
|
this.editFile
|
||||||
|
? this.editFile.type.split('/')[this.editFile.type.split('/').length - 1]
|
||||||
|
: null
|
||||||
|
}&featured=${this.editFeatured}`
|
||||||
|
|
||||||
|
if (this.editTitle) {
|
||||||
|
url += `&title=${encodeURIComponent(this.editTitle)}`
|
||||||
|
}
|
||||||
|
if (this.editDescription) {
|
||||||
|
url += `&description=${encodeURIComponent(this.editDescription)}`
|
||||||
|
}
|
||||||
|
if (this.editOrder) {
|
||||||
|
url += `&ordering=${this.editOrder}`
|
||||||
|
}
|
||||||
|
|
||||||
|
await useBaseFetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: this.editFile,
|
||||||
|
})
|
||||||
|
await this.resetProject()
|
||||||
|
|
||||||
|
this.$refs.modal_edit_item.hide()
|
||||||
|
} catch (err) {
|
||||||
|
this.addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: err.data ? err.data.description : err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stopLoading()
|
||||||
|
this.shouldPreventActions = false
|
||||||
|
},
|
||||||
|
async editGalleryItem() {
|
||||||
|
this.shouldPreventActions = true
|
||||||
|
startLoading()
|
||||||
|
try {
|
||||||
|
let url = `project/${this.project.id}/gallery?url=${encodeURIComponent(
|
||||||
|
this.project.gallery[this.editIndex].url,
|
||||||
|
)}&featured=${this.editFeatured}`
|
||||||
|
|
||||||
|
if (this.editTitle) {
|
||||||
|
url += `&title=${encodeURIComponent(this.editTitle)}`
|
||||||
|
}
|
||||||
|
if (this.editDescription) {
|
||||||
|
url += `&description=${encodeURIComponent(this.editDescription)}`
|
||||||
|
}
|
||||||
|
if (this.editOrder) {
|
||||||
|
url += `&ordering=${this.editOrder}`
|
||||||
|
}
|
||||||
|
|
||||||
|
await useBaseFetch(url, {
|
||||||
|
method: 'PATCH',
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.resetProject()
|
||||||
|
this.$refs.modal_edit_item.hide()
|
||||||
|
} catch (err) {
|
||||||
|
this.addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: err.data ? err.data.description : err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stopLoading()
|
||||||
|
this.shouldPreventActions = false
|
||||||
|
},
|
||||||
|
async deleteGalleryImage() {
|
||||||
|
startLoading()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await useBaseFetch(
|
||||||
|
`project/${this.project.id}/gallery?url=${encodeURIComponent(
|
||||||
|
this.project.gallery[this.deleteIndex].url,
|
||||||
|
)}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.resetProject()
|
||||||
|
} catch (err) {
|
||||||
|
this.addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: err.data ? err.data.description : err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stopLoading()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5ch;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text-inactive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-image-modal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20;
|
||||||
|
overflow: auto;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #000000;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
width: calc(100vw - 2 * var(--spacing-card-lg));
|
||||||
|
height: calc(100vh - 2 * var(--spacing-card-lg));
|
||||||
|
|
||||||
|
.circle-button {
|
||||||
|
padding: 0.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
max-width: 2rem;
|
||||||
|
color: var(--color-button-text);
|
||||||
|
background-color: var(--color-button-bg);
|
||||||
|
border-radius: var(--size-rounded-max);
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: inset 0px -1px 1px rgb(17 24 39 / 10%);
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-button-bg-hover) !important;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--color-button-text-hover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-button-bg-active) !important;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--color-button-text-active) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
max-width: calc(100vw - 2 * var(--spacing-card-lg));
|
||||||
|
max-height: calc(100vh - 2 * var(--spacing-card-lg));
|
||||||
|
border-radius: var(--size-rounded-card);
|
||||||
|
|
||||||
|
&.zoomed-in {
|
||||||
|
object-fit: cover;
|
||||||
|
width: auto;
|
||||||
|
height: calc(100vh - 2 * var(--spacing-card-lg));
|
||||||
|
max-width: calc(100vw - 2 * var(--spacing-card-lg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.floating {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
bottom: var(--spacing-card-md);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-card-sm);
|
||||||
|
transition: opacity 0.25s ease-in-out;
|
||||||
|
opacity: 1;
|
||||||
|
padding: 2rem 2rem 0 2rem;
|
||||||
|
|
||||||
|
&:not(&:hover) {
|
||||||
|
opacity: 0.4;
|
||||||
|
.text {
|
||||||
|
transform: translateY(2.5rem) scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
transform: translateY(0.25rem) scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 40rem;
|
||||||
|
transition:
|
||||||
|
opacity 0.25s ease-in-out,
|
||||||
|
transform 0.25s ease-in-out;
|
||||||
|
text-shadow: 1px 1px 10px #000000d4;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: var(--dark-color-text-dark);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--dark-color-text);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
background-color: var(--color-raised-bg);
|
||||||
|
padding: var(--spacing-card-md);
|
||||||
|
border-radius: var(--size-rounded-card);
|
||||||
|
transition:
|
||||||
|
opacity 0.25s ease-in-out,
|
||||||
|
transform 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-gap: var(--spacing-card-md);
|
||||||
|
|
||||||
|
@media screen and (min-width: 1024px) {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-radius: var(--size-rounded-card) var(--size-rounded-card) 0 0;
|
||||||
|
|
||||||
|
min-height: 10rem;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-body {
|
||||||
|
width: calc(100% - 2 * var(--spacing-card-md));
|
||||||
|
padding: var(--spacing-card-sm) var(--spacing-card-md);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
|
||||||
|
.gallery-info {
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-thumbnail {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
transition: filter 0.25s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-bottom {
|
||||||
|
width: calc(100% - 2 * var(--spacing-card-md));
|
||||||
|
padding: 0 var(--spacing-card-md) var(--spacing-card-sm) var(--spacing-card-md);
|
||||||
|
|
||||||
|
.gallery-created {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--color-icon);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-gallery {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.gallery-file-input {
|
||||||
|
.file-header {
|
||||||
|
border-radius: var(--size-rounded-card) var(--size-rounded-card) 0 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background-color: var(--color-button-bg);
|
||||||
|
padding: var(--spacing-card-md);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
min-width: 1rem;
|
||||||
|
}
|
||||||
|
strong {
|
||||||
|
word-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconified-button {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 0 0 var(--size-rounded-card) var(--size-rounded-card);
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 15rem;
|
||||||
|
object-fit: contain;
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-button {
|
||||||
|
color: var(--color-accent-contrast);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
366
apps/frontend/src/pages/[type]/[id]/settings/versions.vue
Normal file
366
apps/frontend/src/pages/[type]/[id]/settings/versions.vue
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<CreateProjectVersionModal ref="modal"></CreateProjectVersionModal>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
v-if="currentMember"
|
||||||
|
ref="deleteVersionModal"
|
||||||
|
title="Are you sure you want to delete this version?"
|
||||||
|
description="This will remove this version forever (like really forever)."
|
||||||
|
:has-to-type="false"
|
||||||
|
proceed-label="Delete"
|
||||||
|
@proceed="deleteVersion()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProjectPageVersions
|
||||||
|
v-if="versions.length > 0"
|
||||||
|
:project="project"
|
||||||
|
:versions="versionsWithDisplayUrl"
|
||||||
|
:show-files="flags.showVersionFilesInTable"
|
||||||
|
:current-member="!!currentMember"
|
||||||
|
:loaders="tags.loaders"
|
||||||
|
:game-versions="tags.gameVersions"
|
||||||
|
:base-id="baseDropdownId"
|
||||||
|
:version-link="
|
||||||
|
(version: any) =>
|
||||||
|
`/${project.project_type}/${
|
||||||
|
project.slug ? project.slug : project.id
|
||||||
|
}/version/${encodeURI(version.displayUrlEnding)}`
|
||||||
|
"
|
||||||
|
:open-modal="openModal"
|
||||||
|
>
|
||||||
|
<template #actions="{ version }">
|
||||||
|
<ButtonStyled circular type="transparent">
|
||||||
|
<a
|
||||||
|
v-tooltip="`Download`"
|
||||||
|
:href="getPrimaryFile(version).url"
|
||||||
|
class="group-hover:!bg-brand group-hover:[&>svg]:!text-brand-inverted"
|
||||||
|
aria-label="Download"
|
||||||
|
@click="emit('onDownload')"
|
||||||
|
>
|
||||||
|
<DownloadIcon aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled circular type="transparent">
|
||||||
|
<a
|
||||||
|
v-tooltip="`Edit version`"
|
||||||
|
aria-label="Edit"
|
||||||
|
@click="() => handleOpenEditVersionModal(version)"
|
||||||
|
>
|
||||||
|
<EditIcon aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled circular type="transparent">
|
||||||
|
<OverflowMenu
|
||||||
|
class="group-hover:!bg-button-bg"
|
||||||
|
:dropdown-id="`${baseDropdownId}-${version.id}`"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'download',
|
||||||
|
color: 'primary',
|
||||||
|
hoverFilled: true,
|
||||||
|
link: getPrimaryFile(version).url,
|
||||||
|
action: () => {
|
||||||
|
emit('onDownload')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new-tab',
|
||||||
|
action: () => {},
|
||||||
|
link: `/${project.project_type}/${
|
||||||
|
project.slug ? project.slug : project.id
|
||||||
|
}/version/${encodeURI(version.displayUrlEnding)}`,
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'copy-link',
|
||||||
|
action: () =>
|
||||||
|
copyToClipboard(
|
||||||
|
`https://modrinth.com/${project.project_type}/${
|
||||||
|
project.slug ? project.slug : project.id
|
||||||
|
}/version/${encodeURI(version.displayUrlEnding)}`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'share',
|
||||||
|
action: () => {},
|
||||||
|
shown: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'report',
|
||||||
|
color: 'red',
|
||||||
|
hoverFilled: true,
|
||||||
|
action: () => (auth.user ? reportVersion(version.id) : navigateTo('/auth/sign-in')),
|
||||||
|
shown: !currentMember,
|
||||||
|
},
|
||||||
|
{ divider: true, shown: !!currentMember || flags.developerMode },
|
||||||
|
{
|
||||||
|
id: 'copy-id',
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(version.id)
|
||||||
|
},
|
||||||
|
shown: !!currentMember || flags.developerMode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'copy-maven',
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(`maven.modrinth:${project.slug}:${version.id}`)
|
||||||
|
},
|
||||||
|
shown: flags.developerMode,
|
||||||
|
},
|
||||||
|
{ divider: true, shown: !!currentMember },
|
||||||
|
{
|
||||||
|
id: 'edit',
|
||||||
|
action: () => handleOpenEditVersionModal(version),
|
||||||
|
shown: !!currentMember,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delete',
|
||||||
|
color: 'red',
|
||||||
|
hoverFilled: true,
|
||||||
|
action: () => {
|
||||||
|
selectedVersion = version.id
|
||||||
|
deleteVersionModal?.show()
|
||||||
|
},
|
||||||
|
shown: !!currentMember,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
aria-label="More options"
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
|
<template #download>
|
||||||
|
<DownloadIcon aria-hidden="true" />
|
||||||
|
Download
|
||||||
|
</template>
|
||||||
|
<template #new-tab>
|
||||||
|
<ExternalIcon aria-hidden="true" />
|
||||||
|
Open in new tab
|
||||||
|
</template>
|
||||||
|
<template #copy-link>
|
||||||
|
<LinkIcon aria-hidden="true" />
|
||||||
|
Copy link
|
||||||
|
</template>
|
||||||
|
<template #share>
|
||||||
|
<ShareIcon aria-hidden="true" />
|
||||||
|
Share
|
||||||
|
</template>
|
||||||
|
<template #report>
|
||||||
|
<ReportIcon aria-hidden="true" />
|
||||||
|
Report
|
||||||
|
</template>
|
||||||
|
<template #edit>
|
||||||
|
<EditIcon aria-hidden="true" />
|
||||||
|
Edit
|
||||||
|
</template>
|
||||||
|
<template #delete>
|
||||||
|
<TrashIcon aria-hidden="true" />
|
||||||
|
Delete
|
||||||
|
</template>
|
||||||
|
<template #copy-id>
|
||||||
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
|
Copy ID
|
||||||
|
</template>
|
||||||
|
<template #copy-maven>
|
||||||
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
|
Copy Maven coordinates
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
</ProjectPageVersions>
|
||||||
|
|
||||||
|
<template v-if="!versions.length">
|
||||||
|
<div class="grid place-content-center py-10">
|
||||||
|
<svg
|
||||||
|
width="250"
|
||||||
|
height="200"
|
||||||
|
viewBox="0 0 250 200"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-[200px] w-[250px]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M136 64C139.866 64 143 67.134 143 71C143 74.866 139.866 78 136 78H200C203.866 78 207 81.134 207 85C207 88.866 203.866 92 200 92H222C225.866 92 229 95.134 229 99C229 102.866 225.866 106 222 106H203C199.134 106 196 109.134 196 113C196 116.866 199.134 120 203 120H209C212.866 120 216 123.134 216 127C216 130.866 212.866 134 209 134H157C156.485 134 155.983 133.944 155.5 133.839C155.017 133.944 154.515 134 154 134H63C59.134 134 56 130.866 56 127C56 123.134 59.134 120 63 120H24C20.134 120 17 116.866 17 113C17 109.134 20.134 106 24 106H64C67.866 106 71 102.866 71 99C71 95.134 67.866 92 64 92H39C35.134 92 32 88.866 32 85C32 81.134 35.134 78 39 78H79C75.134 78 72 74.866 72 71C72 67.134 75.134 64 79 64H136ZM226 120C229.866 120 233 123.134 233 127C233 130.866 229.866 134 226 134C222.134 134 219 130.866 219 127C219 123.134 222.134 120 226 120Z"
|
||||||
|
class="fill-surface-2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M113.119 112.307C113.04 112.86 113 113.425 113 114C113 120.627 118.373 126 125 126C131.627 126 137 120.627 137 114C137 113.425 136.96 112.86 136.881 112.307H166V139C166 140.657 164.657 142 163 142H87C85.3431 142 84 140.657 84 139V112.307H113.119Z"
|
||||||
|
class="fill-surface-1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M138 112C138 119.18 132.18 125 125 125C117.82 125 112 119.18 112 112C112 111.767 112.006 111.536 112.018 111.307H84L93.5604 83.0389C93.9726 81.8202 95.1159 81 96.4023 81H153.598C154.884 81 156.027 81.8202 156.44 83.0389L166 111.307H137.982C137.994 111.536 138 111.767 138 112Z"
|
||||||
|
class="fill-surface-1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M136.098 112.955C136.098 118.502 131.129 124 125 124C118.871 124 113.902 118.502 113.902 112.955C113.902 112.775 113.908 111.596 113.918 111.419H93L101.161 91.5755C101.513 90.6338 102.489 90 103.587 90H146.413C147.511 90 148.487 90.6338 148.839 91.5755L157 111.419H136.082C136.092 111.596 136.098 112.775 136.098 112.955Z"
|
||||||
|
fill="#27292E"
|
||||||
|
class="fill-surface-3"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M85.25 111.512V138C85.25 138.966 86.0335 139.75 87 139.75H163C163.966 139.75 164.75 138.966 164.75 138V111.512L155.255 83.4393C155.015 82.7285 154.348 82.25 153.598 82.25H96.4023C95.6519 82.25 94.985 82.7285 94.7446 83.4393L85.25 111.512Z"
|
||||||
|
stroke-width="2.5"
|
||||||
|
class="stroke-surface-4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M98 111C101.937 111 106.185 111 110.745 111C112.621 111 112.621 112.319 112.621 113C112.621 119.627 118.117 125 124.897 125C131.677 125 137.173 119.627 137.173 113C137.173 112.319 137.173 111 139.05 111H164M90.5737 111H93H90.5737Z"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="stroke-surface-4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M150.1 58.3027L139 70.7559M124.1 54V70.7559V54ZM98 58.3027L109.1 70.7559L98 58.3027Z"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="stroke-surface-4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-2 text-center">
|
||||||
|
<div class="text-2xl font-semibold text-contrast">No versions created</div>
|
||||||
|
<div>Create your first project version.</div>
|
||||||
|
<br />
|
||||||
|
<ButtonStyled color="green">
|
||||||
|
<button @click="openModal"><PlusIcon /> Create version</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import {
|
||||||
|
ClipboardCopyIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
EditIcon,
|
||||||
|
ExternalIcon,
|
||||||
|
LinkIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
PlusIcon,
|
||||||
|
ReportIcon,
|
||||||
|
ShareIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
ButtonStyled,
|
||||||
|
ConfirmModal,
|
||||||
|
injectModrinthClient,
|
||||||
|
injectNotificationManager,
|
||||||
|
injectProjectPageContext,
|
||||||
|
OverflowMenu,
|
||||||
|
ProjectPageVersions,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
|
||||||
|
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||||
|
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
project: Labrinth.Projects.v2.Project
|
||||||
|
currentMember?: object
|
||||||
|
}
|
||||||
|
|
||||||
|
const { project, currentMember } = defineProps<Props>()
|
||||||
|
|
||||||
|
const versions = defineModel<Labrinth.Versions.v3.Version[]>('versions', { required: true })
|
||||||
|
|
||||||
|
const client = injectModrinthClient()
|
||||||
|
const { addNotification } = injectNotificationManager()
|
||||||
|
const { refreshVersions } = injectProjectPageContext()
|
||||||
|
|
||||||
|
const modal = ref<InstanceType<typeof CreateProjectVersionModal>>()
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
modal.value?.show?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = useGeneratedState()
|
||||||
|
const flags = useFeatureFlags()
|
||||||
|
const auth = await useAuth()
|
||||||
|
|
||||||
|
const deleteVersionModal = ref<InstanceType<typeof ConfirmModal>>()
|
||||||
|
const selectedVersion = ref<string | null>(null)
|
||||||
|
|
||||||
|
const versionsWithDisplayUrl = computed(() =>
|
||||||
|
versions.value.map((v) => ({
|
||||||
|
...v,
|
||||||
|
displayUrlEnding: v.version_number,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits(['onDownload'])
|
||||||
|
|
||||||
|
const baseDropdownId = useId()
|
||||||
|
|
||||||
|
function getPrimaryFile(version: Labrinth.Versions.v3.Version) {
|
||||||
|
return version.files.find((x) => x.primary) || version.files[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyToClipboard(text: string) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteVersion() {
|
||||||
|
const id = selectedVersion.value
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
startLoading()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.labrinth.versions_v3.deleteVersion(id)
|
||||||
|
|
||||||
|
addNotification({
|
||||||
|
title: 'Version deleted',
|
||||||
|
text: 'The version has been successfully deleted.',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: err.data ? err.data.description : err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshVersions()
|
||||||
|
selectedVersion.value = null
|
||||||
|
|
||||||
|
stopLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOpenEditVersionModal(version: Labrinth.Versions.v3.Version) {
|
||||||
|
selectedVersion.value = version.id
|
||||||
|
try {
|
||||||
|
const versionData = await client.labrinth.versions_v3.getVersion(version.id)
|
||||||
|
modal.value?.show({
|
||||||
|
project_id: project.id,
|
||||||
|
version_id: version.id,
|
||||||
|
name: versionData.name ?? '',
|
||||||
|
version_number: versionData.version_number ?? '',
|
||||||
|
changelog: versionData.changelog ?? '',
|
||||||
|
game_versions: versionData.game_versions ?? [],
|
||||||
|
version_type: versionData.version_type ?? 'release',
|
||||||
|
loaders: versionData.loaders ?? [],
|
||||||
|
dependencies: versionData.dependencies ?? [],
|
||||||
|
existing_files: versionData.files ?? [],
|
||||||
|
environment: versionData.environment,
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: err.data ? err.data.description : err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="normal-page__content flex flex-col gap-4">
|
|
||||||
<nuxt-link
|
|
||||||
:to="versionsListLink"
|
|
||||||
class="flex w-fit items-center gap-1 text-brand-blue hover:underline"
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon />
|
|
||||||
{{
|
|
||||||
hasBackLink ? formatMessage(messages.backToVersions) : formatMessage(messages.allVersions)
|
|
||||||
}}
|
|
||||||
</nuxt-link>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<VersionChannelIndicator :channel="version.version_type" large />
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h1 class="m-0 text-xl font-extrabold leading-none text-contrast">
|
|
||||||
{{ version.version_number }}
|
|
||||||
</h1>
|
|
||||||
<span class="text-sm font-semibold text-secondary"> {{ version.name }} </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<ButtonStyled color="brand">
|
|
||||||
<button><DownloadIcon /> Download</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled>
|
|
||||||
<button><ShareIcon /> Share</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled circular type="transparent">
|
|
||||||
<button>
|
|
||||||
<MoreVerticalIcon />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-lg font-extrabold text-contrast">Files</h2>
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
<div
|
|
||||||
v-for="(file, index) in version.files"
|
|
||||||
:key="index"
|
|
||||||
class="flex gap-2 rounded-2xl bg-bg-raised p-4"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
:class="`flex h-9 w-9 items-center justify-center rounded-full ${file.primary ? 'bg-brand-highlight text-brand' : 'bg-button-bg text-secondary'}`"
|
|
||||||
>
|
|
||||||
<FileIcon />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-grow flex-col">
|
|
||||||
<span class="font-extrabold text-contrast">{{
|
|
||||||
file.primary ? 'Primary file' : 'Supplementary resource'
|
|
||||||
}}</span>
|
|
||||||
<span class="text-sm font-semibold text-secondary"
|
|
||||||
>{{ file.filename }} • {{ formatBytes(file.size) }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ButtonStyled circular type="transparent">
|
|
||||||
<button>
|
|
||||||
<DownloadIcon />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-lg font-extrabold text-contrast">Dependencies</h2>
|
|
||||||
<h2 class="text-lg font-extrabold text-contrast">Changes</h2>
|
|
||||||
<div class="rounded-2xl bg-bg-raised px-6 py-4">
|
|
||||||
<div
|
|
||||||
class="markdown-body"
|
|
||||||
v-html="renderHighlightedString(version.changelog ?? 'No changelog provided')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="normal-page__sidebar">
|
|
||||||
<div class="padding-lg h-[250px] rounded-2xl bg-bg-raised"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
ChevronLeftIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
FileIcon,
|
|
||||||
MoreVerticalIcon,
|
|
||||||
ShareIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import { ButtonStyled, VersionChannelIndicator } from '@modrinth/ui'
|
|
||||||
import { formatBytes, renderHighlightedString } from '@modrinth/utils'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
project: Project
|
|
||||||
versions: Version[]
|
|
||||||
members: User[]
|
|
||||||
currentMember: User
|
|
||||||
dependencies: Dependency[]
|
|
||||||
resetProject: (opts?: { dedupe?: 'cancel' | 'defer' }) => Promise<void>
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const version = computed(() => {
|
|
||||||
let version: Version | undefined
|
|
||||||
|
|
||||||
if (route.params.version === 'latest') {
|
|
||||||
let versionList = props.versions
|
|
||||||
if (route.query.loader) {
|
|
||||||
versionList = versionList.filter((x) => x.loaders.includes(route.query.loader))
|
|
||||||
}
|
|
||||||
if (route.query.version) {
|
|
||||||
versionList = versionList.filter((x) => x.game_versions.includes(route.query.version))
|
|
||||||
}
|
|
||||||
version = versionList.reduce((a, b) => (a.date_published > b.date_published ? a : b))
|
|
||||||
} else {
|
|
||||||
version = props.versions.find(
|
|
||||||
(x) => x.id === route.params.version || x.displayUrlEnding === route.params.version,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!version) {
|
|
||||||
throw createError({
|
|
||||||
fatal: true,
|
|
||||||
statusCode: 404,
|
|
||||||
message: 'Version not found',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return version
|
|
||||||
})
|
|
||||||
|
|
||||||
// const data = useNuxtApp();
|
|
||||||
const route = useNativeRoute()
|
|
||||||
|
|
||||||
// const auth = await useAuth();
|
|
||||||
// const tags = useGeneratedState();
|
|
||||||
|
|
||||||
const versionsListLink = computed(() => {
|
|
||||||
if (router.options.history.state.back) {
|
|
||||||
if (router.options.history.state.back.includes('/versions')) {
|
|
||||||
return router.options.history.state.back
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return `/${props.project.project_type}/${
|
|
||||||
props.project.slug ? props.project.slug : props.project.id
|
|
||||||
}/versions`
|
|
||||||
})
|
|
||||||
|
|
||||||
const hasBackLink = computed(
|
|
||||||
() =>
|
|
||||||
router.options.history.state.back && router.options.history.state.back.endsWith('/versions'),
|
|
||||||
)
|
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const messages = defineMessages({
|
|
||||||
backToVersions: {
|
|
||||||
id: 'project.version.back-to-versions',
|
|
||||||
defaultMessage: 'Back to versions',
|
|
||||||
},
|
|
||||||
allVersions: {
|
|
||||||
id: 'project.version.all-versions',
|
|
||||||
defaultMessage: 'All versions',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="input-group">
|
<div v-else class="input-group">
|
||||||
<ButtonStyled v-if="primaryFile" color="brand">
|
<ButtonStyled v-if="primaryFile && !currentMember" color="brand">
|
||||||
<a
|
<a
|
||||||
v-tooltip="primaryFile.filename + ' (' + formatBytes(primaryFile.size) + ')'"
|
v-tooltip="primaryFile.filename + ' (' + formatBytes(primaryFile.size) + ')'"
|
||||||
:href="primaryFile.url"
|
:href="primaryFile.url"
|
||||||
@@ -163,18 +163,6 @@
|
|||||||
Report
|
Report
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
|
||||||
<nuxt-link
|
|
||||||
v-if="currentMember"
|
|
||||||
class="action"
|
|
||||||
:to="`/${project.project_type}/${
|
|
||||||
project.slug ? project.slug : project.id
|
|
||||||
}/version/${encodeURI(version.displayUrlEnding)}/edit`"
|
|
||||||
>
|
|
||||||
<EditIcon aria-hidden="true" />
|
|
||||||
Edit
|
|
||||||
</nuxt-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button
|
<button
|
||||||
v-if="
|
v-if="
|
||||||
@@ -187,12 +175,6 @@
|
|||||||
Package as mod
|
Package as mod
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
|
||||||
<button v-if="currentMember" @click="$refs.modal_confirm.show()">
|
|
||||||
<TrashIcon aria-hidden="true" />
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-page__changelog universal-card">
|
<div class="version-page__changelog universal-card">
|
||||||
@@ -1353,7 +1335,6 @@ export default defineNuxtComponent({
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 1rem;
|
|
||||||
gap: var(--spacing-card-md);
|
gap: var(--spacing-card-md);
|
||||||
|
|
||||||
h2,
|
h2,
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div />
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
definePageMeta({
|
|
||||||
middleware: 'auth',
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,34 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<ConfirmModal
|
|
||||||
v-if="currentMember"
|
|
||||||
ref="deleteVersionModal"
|
|
||||||
title="Are you sure you want to delete this version?"
|
|
||||||
description="This will remove this version forever (like really forever)."
|
|
||||||
:has-to-type="false"
|
|
||||||
proceed-label="Delete"
|
|
||||||
@proceed="deleteVersion()"
|
|
||||||
/>
|
|
||||||
<section class="experimental-styles-within overflow-visible">
|
<section class="experimental-styles-within overflow-visible">
|
||||||
<div
|
<Admonition v-if="!hideVersionsAdmonition" type="info" class="mb-4">
|
||||||
v-if="currentMember && isPermission(currentMember?.permissions, 1 << 0)"
|
Managing project versions has moved! You can now add and edit versions in the
|
||||||
class="card flex items-center gap-4"
|
<NuxtLink to="settings/versions" class="font-medium text-blue hover:underline"
|
||||||
>
|
>project settings</NuxtLink
|
||||||
<FileInput
|
>.
|
||||||
:max-size="524288000"
|
<template #actions>
|
||||||
:accept="acceptFileFromProjectType(project.project_type)"
|
<div class="flex gap-2">
|
||||||
prompt="Upload a version"
|
<ButtonStyled color="blue">
|
||||||
class="btn btn-primary"
|
<button
|
||||||
aria-label="Upload a version"
|
aria-label="Project Settings"
|
||||||
@change="handleFiles"
|
class="!shadow-none"
|
||||||
>
|
@click="() => router.push('settings/versions')"
|
||||||
<UploadIcon aria-hidden="true" />
|
>
|
||||||
</FileInput>
|
<SettingsIcon />
|
||||||
<span class="flex items-center gap-2">
|
Edit versions
|
||||||
<InfoIcon aria-hidden="true" /> Click to choose a file or drag one onto this page
|
</button>
|
||||||
</span>
|
</ButtonStyled>
|
||||||
<DropArea :accept="acceptFileFromProjectType(project.project_type)" @change="handleFiles" />
|
<ButtonStyled type="transparent">
|
||||||
</div>
|
<button
|
||||||
|
aria-label="Dismiss"
|
||||||
|
class="!shadow-none"
|
||||||
|
@click="() => (hideVersionsAdmonition = true)"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
<ProjectPageVersions
|
<ProjectPageVersions
|
||||||
|
v-if="versions.length"
|
||||||
:project="project"
|
:project="project"
|
||||||
:versions="versions"
|
:versions="versions"
|
||||||
:show-files="flags.showVersionFilesInTable"
|
:show-files="flags.showVersionFilesInTable"
|
||||||
@@ -113,24 +116,6 @@
|
|||||||
},
|
},
|
||||||
shown: flags.developerMode,
|
shown: flags.developerMode,
|
||||||
},
|
},
|
||||||
{ divider: true, shown: currentMember },
|
|
||||||
{
|
|
||||||
id: 'edit',
|
|
||||||
link: `/${project.project_type}/${
|
|
||||||
project.slug ? project.slug : project.id
|
|
||||||
}/version/${encodeURI(version.displayUrlEnding)}/edit`,
|
|
||||||
shown: currentMember,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'delete',
|
|
||||||
color: 'red',
|
|
||||||
hoverFilled: true,
|
|
||||||
action: () => {
|
|
||||||
selectedVersion = version.id
|
|
||||||
deleteVersionModal.show()
|
|
||||||
},
|
|
||||||
shown: currentMember,
|
|
||||||
},
|
|
||||||
]"
|
]"
|
||||||
aria-label="More options"
|
aria-label="More options"
|
||||||
>
|
>
|
||||||
@@ -155,14 +140,6 @@
|
|||||||
<ReportIcon aria-hidden="true" />
|
<ReportIcon aria-hidden="true" />
|
||||||
Report
|
Report
|
||||||
</template>
|
</template>
|
||||||
<template #edit>
|
|
||||||
<EditIcon aria-hidden="true" />
|
|
||||||
Edit
|
|
||||||
</template>
|
|
||||||
<template #delete>
|
|
||||||
<TrashIcon aria-hidden="true" />
|
|
||||||
Delete
|
|
||||||
</template>
|
|
||||||
<template #copy-id>
|
<template #copy-id>
|
||||||
<ClipboardCopyIcon aria-hidden="true" />
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
Copy ID
|
Copy ID
|
||||||
@@ -175,6 +152,15 @@
|
|||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
</ProjectPageVersions>
|
</ProjectPageVersions>
|
||||||
|
<template v-else>
|
||||||
|
<p class="ml-2">
|
||||||
|
No versions in project. Visit
|
||||||
|
<NuxtLink to="settings/versions">
|
||||||
|
<span class="font-medium text-green hover:underline">project settings</span> to
|
||||||
|
</NuxtLink>
|
||||||
|
upload your first version.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -182,27 +168,16 @@
|
|||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
EditIcon,
|
|
||||||
ExternalIcon,
|
ExternalIcon,
|
||||||
InfoIcon,
|
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
ReportIcon,
|
ReportIcon,
|
||||||
|
SettingsIcon,
|
||||||
ShareIcon,
|
ShareIcon,
|
||||||
TrashIcon,
|
|
||||||
UploadIcon,
|
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import { Admonition, ButtonStyled, OverflowMenu, ProjectPageVersions } from '@modrinth/ui'
|
||||||
ButtonStyled,
|
import { useLocalStorage } from '@vueuse/core'
|
||||||
ConfirmModal,
|
|
||||||
DropArea,
|
|
||||||
FileInput,
|
|
||||||
OverflowMenu,
|
|
||||||
ProjectPageVersions,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
|
|
||||||
import { acceptFileFromProjectType } from '~/helpers/fileUtils.js'
|
|
||||||
import { isPermission } from '~/utils/permissions.ts'
|
|
||||||
import { reportVersion } from '~/utils/report-helpers.ts'
|
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -230,8 +205,10 @@ const tags = useGeneratedState()
|
|||||||
const flags = useFeatureFlags()
|
const flags = useFeatureFlags()
|
||||||
const auth = await useAuth()
|
const auth = await useAuth()
|
||||||
|
|
||||||
const deleteVersionModal = ref()
|
const hideVersionsAdmonition = useLocalStorage(
|
||||||
const selectedVersion = ref(null)
|
'hideVersionsHasMovedAdmonition',
|
||||||
|
!props.versions.length,
|
||||||
|
)
|
||||||
|
|
||||||
const emit = defineEmits(['onDownload', 'deleteVersion'])
|
const emit = defineEmits(['onDownload', 'deleteVersion'])
|
||||||
|
|
||||||
@@ -243,26 +220,7 @@ function getPrimaryFile(version) {
|
|||||||
return version.files.find((x) => x.primary) || version.files[0]
|
return version.files.find((x) => x.primary) || version.files[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFiles(files) {
|
|
||||||
await router.push({
|
|
||||||
name: 'type-id-version-version',
|
|
||||||
params: {
|
|
||||||
type: props.project.project_type,
|
|
||||||
id: props.project.slug ? props.project.slug : props.project.id,
|
|
||||||
version: 'create',
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
newPrimaryFile: files[0],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyToClipboard(text) {
|
async function copyToClipboard(text) {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteVersion() {
|
|
||||||
emit('deleteVersion', selectedVersion.value)
|
|
||||||
selectedVersion.value = null
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
418
apps/frontend/src/providers/version/manage-version-modal.ts
Normal file
418
apps/frontend/src/providers/version/manage-version-modal.ts
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
injectModrinthClient,
|
||||||
|
injectNotificationManager,
|
||||||
|
injectProjectPageContext,
|
||||||
|
type MultiStageModal,
|
||||||
|
resolveCtxFn,
|
||||||
|
type StageConfigInput,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import JSZip from 'jszip'
|
||||||
|
import type { ComputedRef, Ref, ShallowRef } from 'vue'
|
||||||
|
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||||
|
|
||||||
|
import { useGeneratedState } from '~/composables/generated'
|
||||||
|
import { inferVersionInfo } from '~/helpers/infer'
|
||||||
|
|
||||||
|
import { stageConfigs } from './stages'
|
||||||
|
|
||||||
|
// this interface should be in infer.js, but gotta refactor that to ts first
|
||||||
|
export interface InferredVersionInfo {
|
||||||
|
name?: string
|
||||||
|
version_number?: string
|
||||||
|
version_type?: 'alpha' | 'beta' | 'release'
|
||||||
|
loaders?: string[]
|
||||||
|
game_versions?: string[]
|
||||||
|
project_type?: Labrinth.Projects.v2.ProjectType
|
||||||
|
environment?: Labrinth.Projects.v3.Environment
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_DRAFT_VERSION: Labrinth.Versions.v3.DraftVersion = {
|
||||||
|
project_id: '',
|
||||||
|
name: '',
|
||||||
|
version_number: '',
|
||||||
|
version_type: 'release',
|
||||||
|
loaders: [],
|
||||||
|
game_versions: [],
|
||||||
|
featured: false,
|
||||||
|
status: 'draft',
|
||||||
|
changelog: '',
|
||||||
|
dependencies: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VersionStage =
|
||||||
|
| 'add-files'
|
||||||
|
| 'add-details'
|
||||||
|
| 'add-loaders'
|
||||||
|
| 'add-mc-versions'
|
||||||
|
| 'add-environment'
|
||||||
|
| 'add-dependencies'
|
||||||
|
| 'add-changelog'
|
||||||
|
| 'edit-loaders'
|
||||||
|
| 'edit-mc-versions'
|
||||||
|
| 'edit-environment'
|
||||||
|
|
||||||
|
export interface ManageVersionContextValue {
|
||||||
|
// State
|
||||||
|
draftVersion: Ref<Labrinth.Versions.v3.DraftVersion>
|
||||||
|
filesToAdd: Ref<Labrinth.Versions.v3.DraftVersionFile[]>
|
||||||
|
existingFilesToDelete: Ref<Labrinth.Versions.v3.VersionFileHash['sha1'][]>
|
||||||
|
inferredVersionData: Ref<InferredVersionInfo | undefined>
|
||||||
|
projectType: Ref<Labrinth.Projects.v2.ProjectType | undefined>
|
||||||
|
dependencyProjects: Ref<Record<string, Labrinth.Projects.v3.Project>>
|
||||||
|
dependencyVersions: Ref<Record<string, Labrinth.Versions.v3.Version>>
|
||||||
|
|
||||||
|
// Stage management
|
||||||
|
stageConfigs: StageConfigInput<ManageVersionContextValue>[]
|
||||||
|
isSubmitting: Ref<boolean>
|
||||||
|
modal: ShallowRef<ComponentExposed<typeof MultiStageModal> | null>
|
||||||
|
|
||||||
|
// Computed state
|
||||||
|
editingVersion: ComputedRef<boolean>
|
||||||
|
noLoadersProject: ComputedRef<boolean>
|
||||||
|
noEnvironmentProject: ComputedRef<boolean>
|
||||||
|
|
||||||
|
// Stage helpers
|
||||||
|
getNextLabel: (currentIndex?: number | null) => string
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
setProjectType: (
|
||||||
|
project: Labrinth.Projects.v2.Project,
|
||||||
|
file?: File | null,
|
||||||
|
) => Promise<Labrinth.Projects.v2.ProjectType | undefined>
|
||||||
|
getProject: (projectId: string) => Promise<Labrinth.Projects.v3.Project>
|
||||||
|
getVersion: (versionId: string) => Promise<Labrinth.Versions.v3.Version>
|
||||||
|
|
||||||
|
// Submission methods
|
||||||
|
handleCreateVersion: () => Promise<void>
|
||||||
|
handleSaveVersionEdits: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const [injectManageVersionContext, provideManageVersionContext] =
|
||||||
|
createContext<ManageVersionContextValue>('CreateProjectVersionModal')
|
||||||
|
|
||||||
|
export function createManageVersionContext(
|
||||||
|
modal: ShallowRef<ComponentExposed<typeof MultiStageModal> | null>,
|
||||||
|
): ManageVersionContextValue {
|
||||||
|
const { labrinth } = injectModrinthClient()
|
||||||
|
const { addNotification } = injectNotificationManager()
|
||||||
|
const { refreshVersions } = 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 inferredVersionData = ref<InferredVersionInfo>()
|
||||||
|
const projectType = ref<Labrinth.Projects.v2.ProjectType>()
|
||||||
|
const dependencyProjects = ref<Record<string, Labrinth.Projects.v3.Project>>({})
|
||||||
|
const dependencyVersions = ref<Record<string, Labrinth.Versions.v3.Version>>({})
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
// Computed state
|
||||||
|
const editingVersion = computed(() => Boolean(draftVersion.value.version_id))
|
||||||
|
|
||||||
|
// Helper functions for project type detection
|
||||||
|
// TODO: move to infer.js
|
||||||
|
async function setProjectType(
|
||||||
|
project: Labrinth.Projects.v2.Project,
|
||||||
|
file: File | null = null,
|
||||||
|
): Promise<Labrinth.Projects.v2.ProjectType | undefined> {
|
||||||
|
if (project.project_type && project.project_type !== 'project') {
|
||||||
|
projectType.value = project.project_type
|
||||||
|
return projectType.value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(file && file.name.toLowerCase().endsWith('.mrpack')) ||
|
||||||
|
(file && file.name.toLowerCase().endsWith('.mrpack-primary'))
|
||||||
|
) {
|
||||||
|
projectType.value = 'modpack'
|
||||||
|
return projectType.value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
draftVersion.value.loaders?.some((loader) =>
|
||||||
|
[
|
||||||
|
'fabric',
|
||||||
|
'neoforge',
|
||||||
|
'forge',
|
||||||
|
'quilt',
|
||||||
|
'liteloader',
|
||||||
|
'rift',
|
||||||
|
'ornithe',
|
||||||
|
'nilloader',
|
||||||
|
'legacy-fabric',
|
||||||
|
'bta-babric',
|
||||||
|
'babric',
|
||||||
|
'modloader',
|
||||||
|
'java-agent',
|
||||||
|
].includes(loader),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
projectType.value = 'mod'
|
||||||
|
return projectType.value
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (file) {
|
||||||
|
const jszip = await JSZip.loadAsync(file)
|
||||||
|
|
||||||
|
const hasMcmeta = Object.keys(jszip.files).some(
|
||||||
|
(f) => f.toLowerCase() === 'pack.mcmeta' || f.toLowerCase().endsWith('/pack.mcmeta'),
|
||||||
|
)
|
||||||
|
const hasAssetsDir = Object.keys(jszip.files).some(
|
||||||
|
(f) => f.toLowerCase() === 'assets/' || f.toLowerCase().startsWith('assets/'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasMcmeta && hasAssetsDir) {
|
||||||
|
projectType.value = 'resourcepack'
|
||||||
|
return projectType.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// not a zip
|
||||||
|
}
|
||||||
|
|
||||||
|
projectType.value = undefined
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version management methods
|
||||||
|
function newDraftVersion(
|
||||||
|
projectId: string,
|
||||||
|
version: Labrinth.Versions.v3.DraftVersion | null = null,
|
||||||
|
) {
|
||||||
|
draftVersion.value = structuredClone(version ?? EMPTY_DRAFT_VERSION)
|
||||||
|
draftVersion.value.project_id = projectId
|
||||||
|
filesToAdd.value = []
|
||||||
|
existingFilesToDelete.value = []
|
||||||
|
inferredVersionData.value = undefined
|
||||||
|
projectType.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPrimaryFile(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]]
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = useGeneratedState()
|
||||||
|
|
||||||
|
async function setInferredVersionData(
|
||||||
|
file: File,
|
||||||
|
project: Labrinth.Projects.v2.Project,
|
||||||
|
): Promise<InferredVersionInfo> {
|
||||||
|
const inferred = (await inferVersionInfo(
|
||||||
|
file,
|
||||||
|
project,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
inferredVersionData.value = inferred
|
||||||
|
projectType.value = await setProjectType(project, file)
|
||||||
|
|
||||||
|
return inferred
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProject = async (projectId: string) => {
|
||||||
|
if (dependencyProjects.value[projectId]) {
|
||||||
|
return dependencyProjects.value[projectId]
|
||||||
|
}
|
||||||
|
const proj = await labrinth.projects_v3.get(projectId)
|
||||||
|
dependencyProjects.value[projectId] = proj
|
||||||
|
return proj
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVersion = async (versionId: string) => {
|
||||||
|
if (dependencyVersions.value[versionId]) {
|
||||||
|
return dependencyVersions.value[versionId]
|
||||||
|
}
|
||||||
|
const version = await labrinth.versions_v3.getVersion(versionId)
|
||||||
|
dependencyVersions.value[versionId] = version
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submission handlers
|
||||||
|
async function handleCreateVersion() {
|
||||||
|
const version = toRaw(draftVersion.value)
|
||||||
|
const files = toRaw(filesToAdd.value)
|
||||||
|
isSubmitting.value = true
|
||||||
|
|
||||||
|
if (noEnvironmentProject.value) version.environment = undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
await labrinth.versions_v3.createVersion(version, files)
|
||||||
|
modal.value?.hide()
|
||||||
|
addNotification({
|
||||||
|
title: 'Project version created',
|
||||||
|
text: 'The version has been successfully added to your project.',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
await refreshVersions()
|
||||||
|
} catch (err: any) {
|
||||||
|
addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: err.data ? err.data.description : err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveVersionEdits() {
|
||||||
|
const version = toRaw(draftVersion.value)
|
||||||
|
const files = toRaw(filesToAdd.value)
|
||||||
|
const filesToDelete = toRaw(existingFilesToDelete.value)
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
|
||||||
|
if (noEnvironmentProject.value) version.environment = undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!version.version_id) throw new Error('Version ID is required to save edits.')
|
||||||
|
|
||||||
|
await labrinth.versions_v3.modifyVersion(version.version_id, {
|
||||||
|
name: version.name || version.version_number,
|
||||||
|
version_number: version.version_number,
|
||||||
|
changelog: version.changelog,
|
||||||
|
version_type: version.version_type,
|
||||||
|
dependencies: version.dependencies || [],
|
||||||
|
game_versions: version.game_versions,
|
||||||
|
loaders: version.loaders,
|
||||||
|
environment: version.environment,
|
||||||
|
file_types: version.existing_files
|
||||||
|
?.filter((file) => file.file_type)
|
||||||
|
.map((file) => ({
|
||||||
|
algorithm: 'sha1',
|
||||||
|
hash: file.hashes.sha1,
|
||||||
|
file_type: file.file_type ?? null,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
await labrinth.versions_v3.addFilesToVersion(version.version_id, files)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete files that were marked for deletion
|
||||||
|
for (const hash of filesToDelete) {
|
||||||
|
await useBaseFetch(`version_file/${hash}?version_id=${version.version_id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.value?.hide()
|
||||||
|
addNotification({
|
||||||
|
title: 'Project version saved',
|
||||||
|
text: 'The version has been successfully saved to your project.',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
await refreshVersions()
|
||||||
|
} catch (err: any) {
|
||||||
|
addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: err.data ? err.data.description : err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
let nextIndex = currentStageIndex + 1
|
||||||
|
while (nextIndex < stageConfigs.length) {
|
||||||
|
const skip = stageConfigs[nextIndex]?.skip
|
||||||
|
if (!skip || !resolveCtxFn(skip, contextValue)) break
|
||||||
|
nextIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = stageConfigs[nextIndex]
|
||||||
|
if (!next) return 'Done'
|
||||||
|
|
||||||
|
switch (next.id) {
|
||||||
|
case 'add-details':
|
||||||
|
return editingVersion.value ? 'Edit details' : 'Add details'
|
||||||
|
case 'add-files':
|
||||||
|
return editingVersion.value ? 'Edit files' : 'Add files'
|
||||||
|
case 'add-loaders':
|
||||||
|
return editingVersion.value ? 'Edit loaders' : 'Set loaders'
|
||||||
|
case 'add-mc-versions':
|
||||||
|
return editingVersion.value ? 'Edit game versions' : 'Set game versions'
|
||||||
|
case 'add-dependencies':
|
||||||
|
return editingVersion.value ? 'Edit dependencies' : 'Set dependencies'
|
||||||
|
case 'add-environment':
|
||||||
|
return editingVersion.value ? 'Edit environment' : 'Add environment'
|
||||||
|
case 'add-changelog':
|
||||||
|
return editingVersion.value ? 'Edit changelog' : 'Add changelog'
|
||||||
|
default:
|
||||||
|
return 'Next'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextValue: ManageVersionContextValue = {
|
||||||
|
// State
|
||||||
|
draftVersion,
|
||||||
|
filesToAdd,
|
||||||
|
existingFilesToDelete,
|
||||||
|
inferredVersionData,
|
||||||
|
projectType,
|
||||||
|
dependencyProjects,
|
||||||
|
dependencyVersions,
|
||||||
|
|
||||||
|
// Stage management
|
||||||
|
stageConfigs,
|
||||||
|
isSubmitting,
|
||||||
|
modal,
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
editingVersion,
|
||||||
|
noLoadersProject,
|
||||||
|
noEnvironmentProject,
|
||||||
|
|
||||||
|
// Stage helpers
|
||||||
|
getNextLabel,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
newDraftVersion,
|
||||||
|
setPrimaryFile,
|
||||||
|
setInferredVersionData,
|
||||||
|
setProjectType,
|
||||||
|
getProject,
|
||||||
|
getVersion,
|
||||||
|
handleCreateVersion,
|
||||||
|
handleSaveVersionEdits,
|
||||||
|
}
|
||||||
|
|
||||||
|
return contextValue
|
||||||
|
}
|
||||||
28
apps/frontend/src/providers/version/stages/add-changelog.ts
Normal file
28
apps/frontend/src/providers/version/stages/add-changelog.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { LeftArrowIcon, PlusIcon, SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import type { StageConfigInput } from '@modrinth/ui'
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
|
||||||
|
import AddChangelogStage from '~/components/ui/create-project-version/stages/AddChangelogStage.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'),
|
||||||
|
leftButtonConfig: (ctx) => ({
|
||||||
|
label: 'Back',
|
||||||
|
icon: LeftArrowIcon,
|
||||||
|
onClick: () => ctx.modal.value?.prevStage(),
|
||||||
|
}),
|
||||||
|
rightButtonConfig: (ctx) => ({
|
||||||
|
label: ctx.editingVersion.value ? 'Save changes' : 'Create version',
|
||||||
|
icon: ctx.isSubmitting.value ? SpinnerIcon : PlusIcon,
|
||||||
|
iconPosition: 'before',
|
||||||
|
iconClass: ctx.isSubmitting.value ? 'animate-spin' : undefined,
|
||||||
|
color: 'green',
|
||||||
|
disabled: ctx.isSubmitting.value,
|
||||||
|
onClick: () =>
|
||||||
|
ctx.editingVersion.value ? ctx.handleSaveVersionEdits() : ctx.handleCreateVersion(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { LeftArrowIcon, RightArrowIcon } 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 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',
|
||||||
|
leftButtonConfig: (ctx) => ({
|
||||||
|
label: 'Back',
|
||||||
|
icon: LeftArrowIcon,
|
||||||
|
onClick: () => ctx.modal.value?.prevStage(),
|
||||||
|
}),
|
||||||
|
rightButtonConfig: (ctx) => ({
|
||||||
|
label: ctx.getNextLabel(),
|
||||||
|
icon: RightArrowIcon,
|
||||||
|
iconPosition: 'after',
|
||||||
|
onClick: () => ctx.modal.value?.nextStage(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
25
apps/frontend/src/providers/version/stages/add-details.ts
Normal file
25
apps/frontend/src/providers/version/stages/add-details.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { LeftArrowIcon, RightArrowIcon } 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 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'),
|
||||||
|
leftButtonConfig: (ctx) => ({
|
||||||
|
label: 'Back',
|
||||||
|
icon: LeftArrowIcon,
|
||||||
|
onClick: () => ctx.modal.value?.prevStage(),
|
||||||
|
}),
|
||||||
|
rightButtonConfig: (ctx) => ({
|
||||||
|
label: ctx.getNextLabel(),
|
||||||
|
icon: RightArrowIcon,
|
||||||
|
iconPosition: 'after',
|
||||||
|
disabled: ctx.draftVersion.value.version_number.trim().length === 0,
|
||||||
|
onClick: () => ctx.modal.value?.nextStage(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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 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'),
|
||||||
|
skip: (ctx) =>
|
||||||
|
ctx.noEnvironmentProject.value ||
|
||||||
|
(!ctx.editingVersion.value && !!ctx.inferredVersionData.value?.environment) ||
|
||||||
|
(ctx.editingVersion.value && !!ctx.draftVersion.value.environment),
|
||||||
|
leftButtonConfig: (ctx) => ({
|
||||||
|
label: 'Back',
|
||||||
|
icon: LeftArrowIcon,
|
||||||
|
onClick: () => ctx.modal.value?.prevStage(),
|
||||||
|
}),
|
||||||
|
rightButtonConfig: (ctx) => ({
|
||||||
|
label: ctx.getNextLabel(),
|
||||||
|
icon: RightArrowIcon,
|
||||||
|
iconPosition: 'after',
|
||||||
|
disabled: !ctx.draftVersion.value.environment,
|
||||||
|
onClick: () => ctx.modal.value?.nextStage(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const editStageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||||
|
id: 'edit-environment',
|
||||||
|
stageContent: markRaw(AddEnvironmentStage),
|
||||||
|
title: 'Edit environment',
|
||||||
|
nonProgressStage: true,
|
||||||
|
leftButtonConfig: (ctx) => ({
|
||||||
|
label: 'Back',
|
||||||
|
icon: LeftArrowIcon,
|
||||||
|
disabled: !ctx.draftVersion.value.environment,
|
||||||
|
onClick: () => ctx.modal.value?.setStage('add-details'),
|
||||||
|
}),
|
||||||
|
rightButtonConfig: (ctx) => ({
|
||||||
|
label: ctx.getNextLabel(2),
|
||||||
|
icon: RightArrowIcon,
|
||||||
|
iconPosition: 'after',
|
||||||
|
disabled: !ctx.draftVersion.value.environment,
|
||||||
|
onClick: () => ctx.modal.value?.setStage(2),
|
||||||
|
}),
|
||||||
|
}
|
||||||
41
apps/frontend/src/providers/version/stages/add-files.ts
Normal file
41
apps/frontend/src/providers/version/stages/add-files.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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 (!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 (!hasFiles) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: ctx.getNextLabel(),
|
||||||
|
icon: RightArrowIcon,
|
||||||
|
iconPosition: 'after',
|
||||||
|
disabled: !hasFiles,
|
||||||
|
onClick: () => ctx.modal.value?.nextStage(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
49
apps/frontend/src/providers/version/stages/add-loaders.ts
Normal file
49
apps/frontend/src/providers/version/stages/add-loaders.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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 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'),
|
||||||
|
skip: (ctx) =>
|
||||||
|
ctx.noLoadersProject.value ||
|
||||||
|
(ctx.inferredVersionData.value?.loaders?.length ?? 0) > 0 ||
|
||||||
|
ctx.editingVersion.value,
|
||||||
|
leftButtonConfig: (ctx) => ({
|
||||||
|
label: 'Back',
|
||||||
|
icon: LeftArrowIcon,
|
||||||
|
onClick: () => ctx.modal.value?.prevStage(),
|
||||||
|
}),
|
||||||
|
rightButtonConfig: (ctx) => ({
|
||||||
|
label: ctx.getNextLabel(),
|
||||||
|
icon: RightArrowIcon,
|
||||||
|
iconPosition: 'after',
|
||||||
|
disabled: ctx.draftVersion.value.loaders.length === 0,
|
||||||
|
onClick: () => ctx.modal.value?.nextStage(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const editStageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||||
|
id: 'edit-loaders',
|
||||||
|
stageContent: markRaw(AddLoadersStage),
|
||||||
|
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'),
|
||||||
|
}),
|
||||||
|
rightButtonConfig: (ctx) => ({
|
||||||
|
label: ctx.getNextLabel(2),
|
||||||
|
icon: RightArrowIcon,
|
||||||
|
iconPosition: 'after',
|
||||||
|
disabled: ctx.draftVersion.value.loaders.length === 0,
|
||||||
|
onClick: () => ctx.modal.value?.setStage(2),
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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 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'),
|
||||||
|
skip: (ctx) =>
|
||||||
|
(ctx.inferredVersionData.value?.game_versions?.length ?? 0) > 0 || ctx.editingVersion.value,
|
||||||
|
leftButtonConfig: (ctx) => ({
|
||||||
|
label: 'Back',
|
||||||
|
icon: LeftArrowIcon,
|
||||||
|
onClick: () => ctx.modal.value?.prevStage(),
|
||||||
|
}),
|
||||||
|
rightButtonConfig: (ctx) => ({
|
||||||
|
label: ctx.getNextLabel(),
|
||||||
|
icon: RightArrowIcon,
|
||||||
|
iconPosition: 'after',
|
||||||
|
disabled: ctx.draftVersion.value.game_versions.length === 0,
|
||||||
|
onClick: () => ctx.modal.value?.nextStage(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const editStageConfig: StageConfigInput<ManageVersionContextValue> = {
|
||||||
|
id: 'edit-mc-versions',
|
||||||
|
stageContent: markRaw(AddMcVersionsStage),
|
||||||
|
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'),
|
||||||
|
}),
|
||||||
|
rightButtonConfig: (ctx) => ({
|
||||||
|
label: ctx.getNextLabel(2),
|
||||||
|
icon: RightArrowIcon,
|
||||||
|
iconPosition: 'after',
|
||||||
|
disabled: ctx.draftVersion.value.game_versions.length === 0,
|
||||||
|
onClick: () => ctx.modal.value?.setStage(2),
|
||||||
|
}),
|
||||||
|
}
|
||||||
30
apps/frontend/src/providers/version/stages/index.ts
Normal file
30
apps/frontend/src/providers/version/stages/index.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { stageConfig as addChangelogStageConfig } from './add-changelog'
|
||||||
|
import { stageConfig as addDependenciesStageConfig } from './add-dependencies'
|
||||||
|
import { stageConfig as addDetailsStageConfig } from './add-details'
|
||||||
|
import {
|
||||||
|
editStageConfig as editEnvironmentStageConfig,
|
||||||
|
stageConfig as addEnvironmentStageConfig,
|
||||||
|
} from './add-environment'
|
||||||
|
import { stageConfig as addFilesStageConfig } from './add-files'
|
||||||
|
import {
|
||||||
|
editStageConfig as editLoadersStageConfig,
|
||||||
|
stageConfig as addLoadersStageConfig,
|
||||||
|
} from './add-loaders'
|
||||||
|
import {
|
||||||
|
editStageConfig as editMcVersionsStageConfig,
|
||||||
|
stageConfig as addMcVersionsStageConfig,
|
||||||
|
} from './add-mc-versions'
|
||||||
|
|
||||||
|
export const stageConfigs = [
|
||||||
|
addFilesStageConfig,
|
||||||
|
addDetailsStageConfig,
|
||||||
|
addLoadersStageConfig,
|
||||||
|
addMcVersionsStageConfig,
|
||||||
|
addEnvironmentStageConfig,
|
||||||
|
addDependenciesStageConfig,
|
||||||
|
addChangelogStageConfig,
|
||||||
|
// Non-progress stages for editing from details page
|
||||||
|
editLoadersStageConfig,
|
||||||
|
editMcVersionsStageConfig,
|
||||||
|
editEnvironmentStageConfig,
|
||||||
|
]
|
||||||
@@ -124,6 +124,11 @@ export abstract class AbstractModrinthClient {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const headers = mergedOptions.headers
|
||||||
|
if (headers && 'Content-Type' in headers && headers['Content-Type'] === '') {
|
||||||
|
delete headers['Content-Type']
|
||||||
|
}
|
||||||
|
|
||||||
const context = this.buildContext(url, path, mergedOptions)
|
const context = this.buildContext(url, path, mergedOptions)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ArchonServersV0Module } from './archon/servers/v0'
|
|||||||
import { ArchonServersV1Module } from './archon/servers/v1'
|
import { ArchonServersV1Module } from './archon/servers/v1'
|
||||||
import { ISO3166Module } from './iso3166'
|
import { ISO3166Module } from './iso3166'
|
||||||
import { KyrosFilesV0Module } from './kyros/files/v0'
|
import { KyrosFilesV0Module } from './kyros/files/v0'
|
||||||
|
import { LabrinthVersionsV3Module } from './labrinth'
|
||||||
import { LabrinthBillingInternalModule } from './labrinth/billing/internal'
|
import { LabrinthBillingInternalModule } from './labrinth/billing/internal'
|
||||||
import { LabrinthCollectionsModule } from './labrinth/collections'
|
import { LabrinthCollectionsModule } from './labrinth/collections'
|
||||||
import { LabrinthProjectsV2Module } from './labrinth/projects/v2'
|
import { LabrinthProjectsV2Module } from './labrinth/projects/v2'
|
||||||
@@ -35,6 +36,7 @@ export const MODULE_REGISTRY = {
|
|||||||
labrinth_projects_v2: LabrinthProjectsV2Module,
|
labrinth_projects_v2: LabrinthProjectsV2Module,
|
||||||
labrinth_projects_v3: LabrinthProjectsV3Module,
|
labrinth_projects_v3: LabrinthProjectsV3Module,
|
||||||
labrinth_state: LabrinthStateModule,
|
labrinth_state: LabrinthStateModule,
|
||||||
|
labrinth_versions_v3: LabrinthVersionsV3Module,
|
||||||
} as const satisfies Record<string, ModuleConstructor>
|
} as const satisfies Record<string, ModuleConstructor>
|
||||||
|
|
||||||
export type ModuleID = keyof typeof MODULE_REGISTRY
|
export type ModuleID = keyof typeof MODULE_REGISTRY
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * from './collections'
|
|||||||
export * from './projects/v2'
|
export * from './projects/v2'
|
||||||
export * from './projects/v3'
|
export * from './projects/v3'
|
||||||
export * from './state'
|
export * from './state'
|
||||||
|
export * from './versions/v3'
|
||||||
|
|||||||
@@ -68,7 +68,10 @@ export class LabrinthProjectsV2Module extends AbstractModule {
|
|||||||
api: 'labrinth',
|
api: 'labrinth',
|
||||||
version: 2,
|
version: 2,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: params as Record<string, unknown>,
|
params: {
|
||||||
|
...params,
|
||||||
|
facets: params.facets ? JSON.stringify(params.facets) : undefined,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ export namespace Labrinth {
|
|||||||
| 'shader'
|
| 'shader'
|
||||||
| 'plugin'
|
| 'plugin'
|
||||||
| 'datapack'
|
| 'datapack'
|
||||||
|
| 'project'
|
||||||
|
|
||||||
export type GalleryImage = {
|
export type GalleryImage = {
|
||||||
url: string
|
url: string
|
||||||
@@ -264,7 +265,7 @@ export namespace Labrinth {
|
|||||||
|
|
||||||
export type ProjectSearchParams = {
|
export type ProjectSearchParams = {
|
||||||
query?: string
|
query?: string
|
||||||
facets?: string[][]
|
facets?: string[][] // in the format of [["categories:forge"],["versions:1.17.1"]]
|
||||||
filters?: string
|
filters?: string
|
||||||
index?: 'relevance' | 'downloads' | 'follows' | 'newest' | 'updated'
|
index?: 'relevance' | 'downloads' | 'follows' | 'newest' | 'updated'
|
||||||
offset?: number
|
offset?: number
|
||||||
@@ -420,23 +421,38 @@ export namespace Labrinth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: consolidate duplicated types between v2 and v3 versions
|
||||||
export namespace v3 {
|
export namespace v3 {
|
||||||
export type VersionType = 'release' | 'beta' | 'alpha'
|
export interface Dependency {
|
||||||
|
dependency_type: Labrinth.Versions.v2.DependencyType
|
||||||
|
project_id?: string
|
||||||
|
file_name?: string
|
||||||
|
version_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type VersionStatus =
|
export interface GetProjectVersionsParams {
|
||||||
| 'listed'
|
game_versions?: string[]
|
||||||
| 'archived'
|
loaders?: string[]
|
||||||
| 'draft'
|
}
|
||||||
| 'unlisted'
|
|
||||||
| 'scheduled'
|
export type VersionChannel = 'release' | 'beta' | 'alpha'
|
||||||
|
|
||||||
|
export type FileType =
|
||||||
|
| 'required-resource-pack'
|
||||||
|
| 'optional-resource-pack'
|
||||||
|
| 'sources-jar'
|
||||||
|
| 'dev-jar'
|
||||||
|
| 'javadoc-jar'
|
||||||
|
| 'signature'
|
||||||
| 'unknown'
|
| 'unknown'
|
||||||
|
|
||||||
export type DependencyType = 'required' | 'optional' | 'incompatible' | 'embedded'
|
export interface VersionFileHash {
|
||||||
|
sha512: string
|
||||||
|
sha1: string
|
||||||
|
}
|
||||||
|
|
||||||
export type FileType = 'required-resource-pack' | 'optional-resource-pack' | 'unknown'
|
interface VersionFile {
|
||||||
|
hashes: VersionFileHash
|
||||||
export type VersionFile = {
|
|
||||||
hashes: Record<string, string>
|
|
||||||
url: string
|
url: string
|
||||||
filename: string
|
filename: string
|
||||||
primary: boolean
|
primary: boolean
|
||||||
@@ -444,35 +460,75 @@ export namespace Labrinth {
|
|||||||
file_type?: FileType
|
file_type?: FileType
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Dependency = {
|
export interface Version {
|
||||||
version_id?: string
|
name: string
|
||||||
project_id?: string
|
version_number: string
|
||||||
file_name?: string
|
changelog?: string
|
||||||
dependency_type: DependencyType
|
dependencies: Dependency[]
|
||||||
}
|
game_versions: string[]
|
||||||
|
version_type: VersionChannel
|
||||||
export type Version = {
|
loaders: string[]
|
||||||
|
featured: boolean
|
||||||
|
status: Labrinth.Versions.v2.VersionStatus
|
||||||
id: string
|
id: string
|
||||||
project_id: string
|
project_id: string
|
||||||
author_id: string
|
author_id: string
|
||||||
featured: boolean
|
|
||||||
name: string
|
|
||||||
version_number: string
|
|
||||||
project_types: string[]
|
|
||||||
games: string[]
|
|
||||||
changelog: string
|
|
||||||
date_published: string
|
date_published: string
|
||||||
downloads: number
|
downloads: number
|
||||||
version_type: VersionType
|
|
||||||
status: VersionStatus
|
|
||||||
requested_status?: VersionStatus | null
|
|
||||||
files: VersionFile[]
|
files: VersionFile[]
|
||||||
dependencies: Dependency[]
|
environment?: Labrinth.Projects.v3.Environment
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DraftVersionFile {
|
||||||
|
fileType?: FileType
|
||||||
|
file: File
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DraftVersion = Omit<
|
||||||
|
Labrinth.Versions.v3.CreateVersionRequest,
|
||||||
|
'file_parts' | 'primary_file' | 'file_types'
|
||||||
|
> & {
|
||||||
|
existing_files?: VersionFile[]
|
||||||
|
version_id?: string
|
||||||
|
environment?: Labrinth.Projects.v3.Environment
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateVersionRequest {
|
||||||
|
name: string
|
||||||
|
version_number: string
|
||||||
|
changelog: string
|
||||||
|
dependencies?: Array<{
|
||||||
|
version_id?: string
|
||||||
|
project_id?: string
|
||||||
|
file_name?: string
|
||||||
|
dependency_type: Labrinth.Versions.v2.DependencyType
|
||||||
|
}>
|
||||||
|
game_versions: string[]
|
||||||
|
version_type: 'release' | 'beta' | 'alpha'
|
||||||
loaders: string[]
|
loaders: string[]
|
||||||
ordering?: number | null
|
featured?: boolean
|
||||||
game_versions?: string[]
|
status?: 'listed' | 'archived' | 'draft' | 'unlisted' | 'scheduled' | 'unknown'
|
||||||
mrpack_loaders?: string[]
|
requested_status?: 'listed' | 'archived' | 'draft' | 'unlisted' | null
|
||||||
environment?: string
|
project_id: string
|
||||||
|
file_parts: string[]
|
||||||
|
primary_file?: string
|
||||||
|
file_types?: Record<string, Labrinth.Versions.v3.FileType | null>
|
||||||
|
environment?: Labrinth.Projects.v3.Environment
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModifyVersionRequest = Partial<
|
||||||
|
Omit<CreateVersionRequest, 'project_id' | 'file_parts' | 'primary_file' | 'file_types'>
|
||||||
|
> & {
|
||||||
|
file_types?: {
|
||||||
|
algorithm: string
|
||||||
|
hash: string
|
||||||
|
file_type: Labrinth.Versions.v3.FileType | null
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddFilesToVersionRequest = {
|
||||||
|
file_parts: string[]
|
||||||
|
file_types?: Record<string, Labrinth.Versions.v3.FileType | null>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,7 +622,7 @@ export namespace Labrinth {
|
|||||||
export interface GameVersion {
|
export interface GameVersion {
|
||||||
version: string
|
version: string
|
||||||
version_type: string
|
version_type: string
|
||||||
date: string // RFC 3339 DateTime
|
date: string
|
||||||
major: boolean
|
major: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
282
packages/api-client/src/modules/labrinth/versions/v3.ts
Normal file
282
packages/api-client/src/modules/labrinth/versions/v3.ts
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import { AbstractModule } from '../../../core/abstract-module'
|
||||||
|
import type { Labrinth } from '../types'
|
||||||
|
|
||||||
|
export class LabrinthVersionsV3Module extends AbstractModule {
|
||||||
|
public getModuleID(): string {
|
||||||
|
return 'labrinth_versions_v3'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get versions for a project (v3)
|
||||||
|
*
|
||||||
|
* @param id - Project ID or slug (e.g., 'sodium' or 'AANobbMI')
|
||||||
|
* @param options - Optional query parameters to filter versions
|
||||||
|
* @returns Promise resolving to an array of v3 versions
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const versions = await client.labrinth.versions_v3.getProjectVersions('sodium')
|
||||||
|
* const filteredVersions = await client.labrinth.versions_v3.getProjectVersions('sodium', {
|
||||||
|
* game_versions: ['1.20.1'],
|
||||||
|
* loaders: ['fabric']
|
||||||
|
* })
|
||||||
|
* console.log(versions[0].version_number)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public async getProjectVersions(
|
||||||
|
id: string,
|
||||||
|
options?: Labrinth.Versions.v3.GetProjectVersionsParams,
|
||||||
|
): Promise<Labrinth.Versions.v3.Version[]> {
|
||||||
|
const params: Record<string, string> = {}
|
||||||
|
if (options?.game_versions?.length) {
|
||||||
|
params.game_versions = JSON.stringify(options.game_versions)
|
||||||
|
}
|
||||||
|
if (options?.loaders?.length) {
|
||||||
|
params.loaders = JSON.stringify(options.loaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.client.request<Labrinth.Versions.v3.Version[]>(`/project/${id}/version`, {
|
||||||
|
api: 'labrinth',
|
||||||
|
version: 2, // TODO: move this to a versions v2 module to keep api-client clean and organized
|
||||||
|
method: 'GET',
|
||||||
|
params: Object.keys(params).length > 0 ? params : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific version by ID (v3)
|
||||||
|
*
|
||||||
|
* @param id - Version ID
|
||||||
|
* @returns Promise resolving to the v3 version data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const version = await client.labrinth.versions_v3.getVersion('DXtmvS8i')
|
||||||
|
* console.log(version.version_number)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public async getVersion(id: string): Promise<Labrinth.Versions.v3.Version> {
|
||||||
|
return this.client.request<Labrinth.Versions.v3.Version>(`/version/${id}`, {
|
||||||
|
api: 'labrinth',
|
||||||
|
version: 3,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get multiple versions by IDs (v3)
|
||||||
|
*
|
||||||
|
* @param ids - Array of version IDs
|
||||||
|
* @returns Promise resolving to an array of v3 versions
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const versions = await client.labrinth.versions_v3.getVersions(['DXtmvS8i', 'abc123'])
|
||||||
|
* console.log(versions[0].version_number)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public async getVersions(ids: string[]): Promise<Labrinth.Versions.v3.Version[]> {
|
||||||
|
return this.client.request<Labrinth.Versions.v3.Version[]>(`/versions`, {
|
||||||
|
api: 'labrinth',
|
||||||
|
version: 3,
|
||||||
|
method: 'GET',
|
||||||
|
params: { ids: JSON.stringify(ids) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a version from a project by version ID or number (v3)
|
||||||
|
*
|
||||||
|
* @param projectId - Project ID or slug
|
||||||
|
* @param versionId - Version ID or version number
|
||||||
|
* @returns Promise resolving to the v3 version data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const version = await client.labrinth.versions_v3.getVersionFromIdOrNumber('sodium', 'DXtmvS8i')
|
||||||
|
* const versionByNumber = await client.labrinth.versions_v3.getVersionFromIdOrNumber('sodium', '0.4.12')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public async getVersionFromIdOrNumber(
|
||||||
|
projectId: string,
|
||||||
|
versionId: string,
|
||||||
|
): Promise<Labrinth.Versions.v3.Version> {
|
||||||
|
return this.client.request<Labrinth.Versions.v3.Version>(
|
||||||
|
`/project/${projectId}/version/${versionId}`,
|
||||||
|
{
|
||||||
|
api: 'labrinth',
|
||||||
|
version: 3,
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new version for a project (v3)
|
||||||
|
*
|
||||||
|
* Creates a new version on an existing project. At least one file must be
|
||||||
|
* attached unless the version is created as a draft.
|
||||||
|
*
|
||||||
|
* @param data - JSON metadata payload for the version (must include file_parts)
|
||||||
|
* @param files - Array of uploaded files, in the same order as `data.file_parts`
|
||||||
|
*
|
||||||
|
* @returns A promise resolving to the newly created version data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const version = await client.labrinth.versions_v3.createVersion('sodium', {
|
||||||
|
* name: 'v0.5.0',
|
||||||
|
* version_number: '0.5.0',
|
||||||
|
* version_type: 'release',
|
||||||
|
* loaders: ['fabric'],
|
||||||
|
* game_versions: ['1.20.1'],
|
||||||
|
* project_id: 'sodium',
|
||||||
|
* file_parts: ['primary']
|
||||||
|
* }, [fileObject])
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
public async createVersion(
|
||||||
|
draftVersion: Labrinth.Versions.v3.DraftVersion,
|
||||||
|
versionFiles: Labrinth.Versions.v3.DraftVersionFile[],
|
||||||
|
): Promise<Labrinth.Versions.v3.Version> {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
const files = versionFiles.map((vf) => vf.file)
|
||||||
|
const fileTypes = versionFiles.map((vf) => vf.fileType || null)
|
||||||
|
|
||||||
|
const fileParts = files.map((file, i) => {
|
||||||
|
return `${file.name}-${i === 0 ? 'primary' : i}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileTypeMap = fileParts.reduce<Record<string, Labrinth.Versions.v3.FileType | null>>(
|
||||||
|
(acc, key, i) => {
|
||||||
|
acc[key] = fileTypes[i]
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
const data: Labrinth.Versions.v3.CreateVersionRequest = {
|
||||||
|
project_id: draftVersion.project_id,
|
||||||
|
version_number: draftVersion.version_number,
|
||||||
|
name: draftVersion.name || draftVersion.version_number,
|
||||||
|
changelog: draftVersion.changelog,
|
||||||
|
dependencies: draftVersion.dependencies || [],
|
||||||
|
game_versions: draftVersion.game_versions,
|
||||||
|
loaders: draftVersion.loaders,
|
||||||
|
version_type: draftVersion.version_type,
|
||||||
|
featured: !!draftVersion.featured,
|
||||||
|
file_parts: fileParts,
|
||||||
|
file_types: fileTypeMap,
|
||||||
|
primary_file: fileParts[0],
|
||||||
|
environment: draftVersion.environment,
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append('data', JSON.stringify(data))
|
||||||
|
|
||||||
|
files.forEach((file, i) => {
|
||||||
|
formData.append(fileParts[i], new Blob([file]), file.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.client.request<Labrinth.Versions.v3.Version>(`/version`, {
|
||||||
|
api: 'labrinth',
|
||||||
|
version: 3,
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
timeout: 120000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify an existing version by ID (v3)
|
||||||
|
*
|
||||||
|
* Partially updates a version’s metadata. Only JSON fields may be modified.
|
||||||
|
* To update files, use the separate "Add files to version" endpoint.
|
||||||
|
*
|
||||||
|
* @param versionId - The version ID to update
|
||||||
|
* @param data - PATCH metadata for this version (all fields optional)
|
||||||
|
*
|
||||||
|
* @returns A promise resolving to the updated version data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const updated = await client.labrinth.versions_v3.modifyVersion('DXtmvS8i', {
|
||||||
|
* name: 'v1.0.1',
|
||||||
|
* changelog: 'Updated changelog',
|
||||||
|
* featured: true,
|
||||||
|
* status: 'listed'
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
public async modifyVersion(
|
||||||
|
versionId: string,
|
||||||
|
data: Labrinth.Versions.v3.ModifyVersionRequest,
|
||||||
|
): Promise<Labrinth.Versions.v3.Version> {
|
||||||
|
return this.client.request<Labrinth.Versions.v3.Version>(`/version/${versionId}`, {
|
||||||
|
api: 'labrinth',
|
||||||
|
version: 3,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a version by ID (v3)
|
||||||
|
*
|
||||||
|
* @param versionId - Version ID
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* await client.labrinth.versions_v3.deleteVersion('DXtmvS8i')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public async deleteVersion(versionId: string): Promise<void> {
|
||||||
|
return this.client.request(`/version/${versionId}`, {
|
||||||
|
api: 'labrinth',
|
||||||
|
version: 2,
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addFilesToVersion(
|
||||||
|
versionId: string,
|
||||||
|
versionFiles: Labrinth.Versions.v3.DraftVersionFile[],
|
||||||
|
): Promise<Labrinth.Versions.v3.Version> {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
const files = versionFiles.map((vf) => vf.file)
|
||||||
|
const fileTypes = versionFiles.map((vf) => vf.fileType || null)
|
||||||
|
|
||||||
|
const fileParts = files.map((file, i) => `${file.name}-${i}`)
|
||||||
|
|
||||||
|
const fileTypeMap = fileParts.reduce<Record<string, Labrinth.Versions.v3.FileType | null>>(
|
||||||
|
(acc, key, i) => {
|
||||||
|
acc[key] = fileTypes[i]
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
formData.append('data', JSON.stringify({ file_types: fileTypeMap }))
|
||||||
|
|
||||||
|
files.forEach((file, i) => {
|
||||||
|
formData.append(fileParts[i], new Blob([file]), file.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.client.request<Labrinth.Versions.v3.Version>(`/version/${versionId}/file`, {
|
||||||
|
api: 'labrinth',
|
||||||
|
version: 2,
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
timeout: 120000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,6 +75,7 @@ import _FilterXIcon from './icons/filter-x.svg?component'
|
|||||||
import _FolderArchiveIcon from './icons/folder-archive.svg?component'
|
import _FolderArchiveIcon from './icons/folder-archive.svg?component'
|
||||||
import _FolderOpenIcon from './icons/folder-open.svg?component'
|
import _FolderOpenIcon from './icons/folder-open.svg?component'
|
||||||
import _FolderSearchIcon from './icons/folder-search.svg?component'
|
import _FolderSearchIcon from './icons/folder-search.svg?component'
|
||||||
|
import _FolderUpIcon from './icons/folder-up.svg?component'
|
||||||
import _GameIcon from './icons/game.svg?component'
|
import _GameIcon from './icons/game.svg?component'
|
||||||
import _GapIcon from './icons/gap.svg?component'
|
import _GapIcon from './icons/gap.svg?component'
|
||||||
import _GaugeIcon from './icons/gauge.svg?component'
|
import _GaugeIcon from './icons/gauge.svg?component'
|
||||||
@@ -292,6 +293,7 @@ export const FilterIcon = _FilterIcon
|
|||||||
export const FolderArchiveIcon = _FolderArchiveIcon
|
export const FolderArchiveIcon = _FolderArchiveIcon
|
||||||
export const FolderOpenIcon = _FolderOpenIcon
|
export const FolderOpenIcon = _FolderOpenIcon
|
||||||
export const FolderSearchIcon = _FolderSearchIcon
|
export const FolderSearchIcon = _FolderSearchIcon
|
||||||
|
export const FolderUpIcon = _FolderUpIcon
|
||||||
export const GameIcon = _GameIcon
|
export const GameIcon = _GameIcon
|
||||||
export const GapIcon = _GapIcon
|
export const GapIcon = _GapIcon
|
||||||
export const GaugeIcon = _GaugeIcon
|
export const GaugeIcon = _GaugeIcon
|
||||||
|
|||||||
1
packages/assets/icons/folder-up.svg
Normal file
1
packages/assets/icons/folder-up.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder-up-icon lucide-folder-up"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/><path d="M12 10v6"/><path d="m9 13 3-3 3 3"/></svg>
|
||||||
|
After Width: | Height: | Size: 416 B |
@@ -235,3 +235,23 @@ h3 {
|
|||||||
margin-block: var(--gap-md) var(--gap-md);
|
margin-block: var(--gap-md) var(--gap-md);
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scrollbar styles
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-button-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firefox scrollbar
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--color-button-bg) transparent;
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const coreNags: Nag[] = [
|
|||||||
status: 'required',
|
status: 'required',
|
||||||
shouldShow: (context: NagContext) => context.versions.length < 1,
|
shouldShow: (context: NagContext) => context.versions.length < 1,
|
||||||
link: {
|
link: {
|
||||||
path: 'versions',
|
path: 'settings/versions',
|
||||||
title: defineMessage({
|
title: defineMessage({
|
||||||
id: 'nags.versions.title',
|
id: 'nags.versions.title',
|
||||||
defaultMessage: 'Visit versions page',
|
defaultMessage: 'Visit versions page',
|
||||||
@@ -126,7 +126,7 @@ export const coreNags: Nag[] = [
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
path: 'gallery',
|
path: 'settings/gallery',
|
||||||
title: defineMessage({
|
title: defineMessage({
|
||||||
id: 'nags.gallery.title',
|
id: 'nags.gallery.title',
|
||||||
defaultMessage: 'Visit gallery page',
|
defaultMessage: 'Visit gallery page',
|
||||||
@@ -151,7 +151,7 @@ export const coreNags: Nag[] = [
|
|||||||
return context.project?.gallery?.length === 0 || !featuredGalleryImage
|
return context.project?.gallery?.length === 0 || !featuredGalleryImage
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
path: 'gallery',
|
path: 'settings/gallery',
|
||||||
title: defineMessage({
|
title: defineMessage({
|
||||||
id: 'nags.gallery.title',
|
id: 'nags.gallery.title',
|
||||||
defaultMessage: 'Visit gallery page',
|
defaultMessage: 'Visit gallery page',
|
||||||
@@ -211,46 +211,6 @@ export const coreNags: Nag[] = [
|
|||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'select-environments',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.select-environments.title',
|
|
||||||
defaultMessage: 'Select environments',
|
|
||||||
}),
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.select-environments.description',
|
|
||||||
defaultMessage: `Select the environments your {type, select, mod {mod} modpack {modpack} other {project}} functions on.`,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
type: context.project.project_type,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const excludedTypes = ['resourcepack', 'plugin', 'shader', 'datapack']
|
|
||||||
return (
|
|
||||||
context.project.versions.length > 0 &&
|
|
||||||
!excludedTypes.includes(context.project.project_type) &&
|
|
||||||
(context.project.client_side === 'unknown' ||
|
|
||||||
context.project.server_side === 'unknown' ||
|
|
||||||
(context.project.client_side === 'unsupported' &&
|
|
||||||
context.project.server_side === 'unsupported'))
|
|
||||||
)
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/environment',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.settings.environments.title',
|
|
||||||
defaultMessage: 'Visit environment settings',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-environment',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'select-license',
|
id: 'select-license',
|
||||||
title: defineMessage({
|
title: defineMessage({
|
||||||
|
|||||||
@@ -122,12 +122,6 @@
|
|||||||
"nags.multiple-resolution-tags.title": {
|
"nags.multiple-resolution-tags.title": {
|
||||||
"defaultMessage": "Select correct resolution"
|
"defaultMessage": "Select correct resolution"
|
||||||
},
|
},
|
||||||
"nags.select-environments.description": {
|
|
||||||
"defaultMessage": "Select the environments your {type, select, mod {mod} modpack {modpack} other {project}} functions on."
|
|
||||||
},
|
|
||||||
"nags.select-environments.title": {
|
|
||||||
"defaultMessage": "Select environments"
|
|
||||||
},
|
|
||||||
"nags.select-license.description": {
|
"nags.select-license.description": {
|
||||||
"defaultMessage": "Select the license your {type, select, mod {mod} modpack {modpack} resourcepack {resource pack} shader {shader} plugin {plugin} datapack {data pack} other {project}} is distributed under."
|
"defaultMessage": "Select the license your {type, select, mod {mod} modpack {modpack} resourcepack {resource pack} shader {shader} plugin {plugin} datapack {data pack} other {project}} is distributed under."
|
||||||
},
|
},
|
||||||
@@ -143,9 +137,6 @@
|
|||||||
"nags.settings.description.title": {
|
"nags.settings.description.title": {
|
||||||
"defaultMessage": "Visit description settings"
|
"defaultMessage": "Visit description settings"
|
||||||
},
|
},
|
||||||
"nags.settings.environments.title": {
|
|
||||||
"defaultMessage": "Visit environment settings"
|
|
||||||
},
|
|
||||||
"nags.settings.license.title": {
|
"nags.settings.license.title": {
|
||||||
"defaultMessage": "Visit license settings"
|
"defaultMessage": "Visit license settings"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"stripe": "^18.1.1",
|
"stripe": "^18.1.1",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-component-type-helpers": "^3.1.8",
|
||||||
"vue-router": "4.3.0"
|
"vue-router": "4.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
<Button
|
<Button
|
||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
:key="formatLabel(item)"
|
:key="formatLabel(item)"
|
||||||
class="btn"
|
class="btn !brightness-100 hover:!brightness-125"
|
||||||
:class="{ selected: selected === item, capitalize: capitalize }"
|
:class="{
|
||||||
|
selected: selected === item,
|
||||||
|
capitalize: capitalize,
|
||||||
|
'!px-2.5 !py-1.5': size === 'small',
|
||||||
|
}"
|
||||||
@click="toggleItem(item)"
|
@click="toggleItem(item)"
|
||||||
>
|
>
|
||||||
<CheckIcon v-if="selected === item" />
|
<CheckIcon v-if="selected === item" />
|
||||||
@@ -24,14 +28,17 @@ const props = withDefaults(
|
|||||||
formatLabel?: (item: T) => string
|
formatLabel?: (item: T) => string
|
||||||
neverEmpty?: boolean
|
neverEmpty?: boolean
|
||||||
capitalize?: boolean
|
capitalize?: boolean
|
||||||
|
size?: 'standard' | 'small'
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
neverEmpty: true,
|
neverEmpty: true,
|
||||||
// Intentional any type, as this default should only be used for primitives (string or number)
|
// Intentional any type, as this default should only be used for primitives (string or number)
|
||||||
formatLabel: (item) => item.toString(),
|
formatLabel: (item) => item.toString(),
|
||||||
capitalize: true,
|
capitalize: true,
|
||||||
|
size: 'standard',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const selected = defineModel<T | null>()
|
const selected = defineModel<T | null>()
|
||||||
|
|
||||||
// If one always has to be selected, default to the first one
|
// If one always has to be selected, default to the first one
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
:placeholder="searchPlaceholder"
|
:placeholder="searchPlaceholder"
|
||||||
class=""
|
class=""
|
||||||
@keydown.stop="handleSearchKeydown"
|
@keydown.stop="handleSearchKeydown"
|
||||||
|
@input="emit('searchInput', searchQuery)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,7 +108,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="searchQuery" class="p-4 mb-2 text-center text-sm text-secondary">
|
<div v-else-if="searchQuery" class="p-4 mb-2 text-center text-sm text-secondary">
|
||||||
No results found
|
{{ noOptionsMessage }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -168,6 +169,7 @@ const props = withDefaults(
|
|||||||
extraPosition?: 'top' | 'bottom'
|
extraPosition?: 'top' | 'bottom'
|
||||||
triggerClass?: string
|
triggerClass?: string
|
||||||
forceDirection?: 'up' | 'down'
|
forceDirection?: 'up' | 'down'
|
||||||
|
noOptionsMessage?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
placeholder: 'Select an option',
|
placeholder: 'Select an option',
|
||||||
@@ -178,6 +180,7 @@ const props = withDefaults(
|
|||||||
showChevron: true,
|
showChevron: true,
|
||||||
maxHeight: DEFAULT_MAX_HEIGHT,
|
maxHeight: DEFAULT_MAX_HEIGHT,
|
||||||
extraPosition: 'bottom',
|
extraPosition: 'bottom',
|
||||||
|
noOptionsMessage: 'No results found',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -186,6 +189,7 @@ const emit = defineEmits<{
|
|||||||
select: [option: DropdownOption<T>]
|
select: [option: DropdownOption<T>]
|
||||||
open: []
|
open: []
|
||||||
close: []
|
close: []
|
||||||
|
searchInput: [query: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|||||||
122
packages/ui/src/components/base/DropzoneFileInput.vue
Normal file
122
packages/ui/src/components/base/DropzoneFileInput.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
:class="[
|
||||||
|
'flex flex-col items-center justify-center cursor-pointer border-2 border-dashed bg-surface-4 text-contrast transition-colors',
|
||||||
|
size === 'small' ? 'p-5' : 'p-12',
|
||||||
|
size === 'small' ? 'gap-2' : 'gap-4',
|
||||||
|
size === 'small' ? 'rounded-2xl' : 'rounded-3xl',
|
||||||
|
isDragOver ? 'border-purple' : 'border-surface-5',
|
||||||
|
]"
|
||||||
|
@dragover.prevent="onDragOver"
|
||||||
|
@dragleave.prevent="onDragLeave"
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'grid place-content-center text-brand border-brand border-solid border bg-highlight-green',
|
||||||
|
size === 'small' ? 'w-10 h-10' : 'h-14 w-14',
|
||||||
|
size === 'small' ? 'rounded-xl' : 'rounded-2xl',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<FolderUpIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
:class="['text-brand', size === 'small' ? 'w-6 h-6' : 'w-8 h-8']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center gap-1 text-contrast text-center">
|
||||||
|
<div class="text-contrast font-medium text-pretty">
|
||||||
|
{{ primaryPrompt }}
|
||||||
|
</div>
|
||||||
|
<span class="text-primary text-sm text-pretty">
|
||||||
|
{{ secondaryPrompt }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
:multiple="multiple"
|
||||||
|
:accept="accept"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="hidden"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FolderUpIcon } from '@modrinth/assets'
|
||||||
|
import { fileIsValid } from '@modrinth/utils'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'change', files: File[]): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
prompt?: string
|
||||||
|
primaryPrompt?: string | null
|
||||||
|
secondaryPrompt?: string | null
|
||||||
|
multiple?: boolean
|
||||||
|
accept?: string
|
||||||
|
maxSize?: number | null
|
||||||
|
shouldAlwaysReset?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
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',
|
||||||
|
size: 'standard',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const files = ref<File[]>([])
|
||||||
|
|
||||||
|
function addFiles(incoming: FileList, shouldNotReset = false) {
|
||||||
|
if (!shouldNotReset || props.shouldAlwaysReset) {
|
||||||
|
files.value = Array.from(incoming)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationOptions = {
|
||||||
|
maxSize: props.maxSize ?? undefined,
|
||||||
|
alertOnInvalid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
files.value = files.value.filter((file) => fileIsValid(file, validationOptions))
|
||||||
|
|
||||||
|
if (files.value.length > 0) {
|
||||||
|
emit('change', files.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInput.value) fileInput.value.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDragOver = ref(false)
|
||||||
|
|
||||||
|
function onDragOver() {
|
||||||
|
isDragOver.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragLeave() {
|
||||||
|
isDragOver.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(e: DragEvent) {
|
||||||
|
isDragOver.value = false
|
||||||
|
|
||||||
|
if (!e.dataTransfer) return
|
||||||
|
addFiles(e.dataTransfer.files)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChange(e: Event) {
|
||||||
|
const input = e.target as HTMLInputElement
|
||||||
|
if (!input.files) return
|
||||||
|
addFiles(input.files)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -107,7 +107,7 @@ label {
|
|||||||
grid-gap: 0.5rem;
|
grid-gap: 0.5rem;
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
border: dashed 0.3rem var(--color-contrast);
|
border: dashed 2px var(--color-contrast);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="linkModal" header="Insert link">
|
<NewModal ref="linkModal" header="Insert link">
|
||||||
<div class="modal-insert">
|
<div class="modal-insert">
|
||||||
<label class="label" for="insert-link-label">
|
<label class="label" for="insert-link-label">
|
||||||
<span class="label__title">Label</span>
|
<span class="label__title">Label</span>
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</NewModal>
|
||||||
<Modal ref="imageModal" header="Insert image">
|
<NewModal ref="imageModal" header="Insert image">
|
||||||
<div class="modal-insert">
|
<div class="modal-insert">
|
||||||
<label class="label" for="insert-image-alt">
|
<label class="label" for="insert-image-alt">
|
||||||
<span class="label__title">Description (alt text)<span class="required">*</span></span>
|
<span class="label__title">Description (alt text)<span class="required">*</span></span>
|
||||||
@@ -147,8 +147,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</NewModal>
|
||||||
<Modal ref="videoModal" header="Insert YouTube video">
|
<NewModal ref="videoModal" header="Insert YouTube video">
|
||||||
<div class="modal-insert">
|
<div class="modal-insert">
|
||||||
<label class="label" for="insert-video-url">
|
<label class="label" for="insert-video-url">
|
||||||
<span class="label__title">YouTube video URL<span class="required">*</span></span>
|
<span class="label__title">YouTube video URL<span class="required">*</span></span>
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</NewModal>
|
||||||
<div class="resizable-textarea-wrapper">
|
<div class="resizable-textarea-wrapper">
|
||||||
<div class="editor-action-row">
|
<div class="editor-action-row">
|
||||||
<div class="editor-actions">
|
<div class="editor-actions">
|
||||||
@@ -223,10 +223,10 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
<div class="preview">
|
||||||
<div class="preview">
|
<Toggle id="preview" v-model="previewMode" />
|
||||||
<Toggle id="preview" v-model="previewMode" />
|
<label class="label" for="preview"> Preview </label>
|
||||||
<label class="label" for="preview"> Preview </label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="editorRef" :class="{ hide: previewMode }" />
|
<div ref="editorRef" :class="{ hide: previewMode }" />
|
||||||
@@ -292,11 +292,11 @@ import {
|
|||||||
XIcon,
|
XIcon,
|
||||||
YouTubeIcon,
|
YouTubeIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
|
import NewModal from '@modrinth/ui/src/components/modal/NewModal.vue'
|
||||||
import { markdownCommands, modrinthMarkdownEditorKeymap } from '@modrinth/utils/codemirror'
|
import { markdownCommands, modrinthMarkdownEditorKeymap } from '@modrinth/utils/codemirror'
|
||||||
import { renderHighlightedString } from '@modrinth/utils/highlightjs'
|
import { renderHighlightedString } from '@modrinth/utils/highlightjs'
|
||||||
import { type Component, computed, onBeforeUnmount, onMounted, ref, toRef, watch } from 'vue'
|
import { type Component, computed, onBeforeUnmount, onMounted, ref, toRef, watch } from 'vue'
|
||||||
|
|
||||||
import Modal from '../modal/Modal.vue'
|
|
||||||
import Button from './Button.vue'
|
import Button from './Button.vue'
|
||||||
import Chips from './Chips.vue'
|
import Chips from './Chips.vue'
|
||||||
import FileInput from './FileInput.vue'
|
import FileInput from './FileInput.vue'
|
||||||
@@ -756,9 +756,9 @@ const videoMarkdown = computed(() => {
|
|||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const linkModal = ref<InstanceType<typeof Modal> | null>(null)
|
const linkModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||||
const imageModal = ref<InstanceType<typeof Modal> | null>(null)
|
const imageModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||||
const videoModal = ref<InstanceType<typeof Modal> | null>(null)
|
const videoModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||||
|
|
||||||
function resetModalStates() {
|
function resetModalStates() {
|
||||||
linkText.value = ''
|
linkText.value = ''
|
||||||
|
|||||||
227
packages/ui/src/components/base/MultiStageModal.vue
Normal file
227
packages/ui/src/components/base/MultiStageModal.vue
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<template>
|
||||||
|
<NewModal
|
||||||
|
ref="modal"
|
||||||
|
:scrollable="true"
|
||||||
|
max-content-height="72vh"
|
||||||
|
:on-hide="onModalHide"
|
||||||
|
:closable="true"
|
||||||
|
:close-on-click-outside="false"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<progress
|
||||||
|
v-if="currentStage?.nonProgressStage !== true"
|
||||||
|
:value="progressValue"
|
||||||
|
max="100"
|
||||||
|
class="w-full h-1 appearance-none border-none absolute top-0 left-0"
|
||||||
|
></progress>
|
||||||
|
|
||||||
|
<div class="sm:w-[512px]">
|
||||||
|
<component :is="currentStage?.stageContent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div
|
||||||
|
class="flex flex-col justify-end gap-2 sm:flex-row"
|
||||||
|
:class="leftButtonConfig || rightButtonConfig ? 'mt-4' : ''"
|
||||||
|
>
|
||||||
|
<ButtonStyled v-if="leftButtonConfig" type="outlined">
|
||||||
|
<button
|
||||||
|
class="!border-surface-5"
|
||||||
|
:disabled="leftButtonConfig.disabled"
|
||||||
|
@click="leftButtonConfig.onClick"
|
||||||
|
>
|
||||||
|
<component :is="leftButtonConfig.icon" />
|
||||||
|
{{ leftButtonConfig.label }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled v-if="rightButtonConfig" :color="rightButtonConfig.color">
|
||||||
|
<button :disabled="rightButtonConfig.disabled" @click="rightButtonConfig.onClick">
|
||||||
|
<component
|
||||||
|
:is="rightButtonConfig.icon"
|
||||||
|
v-if="rightButtonConfig.iconPosition === 'before'"
|
||||||
|
:class="rightButtonConfig.iconClass"
|
||||||
|
/>
|
||||||
|
{{ rightButtonConfig.label }}
|
||||||
|
<component
|
||||||
|
:is="rightButtonConfig.icon"
|
||||||
|
v-if="rightButtonConfig.iconPosition === 'after'"
|
||||||
|
:class="rightButtonConfig.iconClass"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { computed, ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
|
export interface StageButtonConfig {
|
||||||
|
label?: string
|
||||||
|
icon?: Component | null
|
||||||
|
iconPosition?: 'before' | 'after'
|
||||||
|
color?: InstanceType<typeof ButtonStyled>['$props']['color']
|
||||||
|
disabled?: boolean
|
||||||
|
iconClass?: string | null
|
||||||
|
onClick?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MaybeCtxFn<T, R> = R | ((ctx: T) => R)
|
||||||
|
|
||||||
|
export interface StageConfigInput<T> {
|
||||||
|
id: string
|
||||||
|
stageContent: Component
|
||||||
|
title: MaybeCtxFn<T, string>
|
||||||
|
skip?: MaybeCtxFn<T, boolean>
|
||||||
|
nonProgressStage?: boolean
|
||||||
|
leftButtonConfig: MaybeCtxFn<T, StageButtonConfig | null>
|
||||||
|
rightButtonConfig: MaybeCtxFn<T, StageButtonConfig | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveCtxFn<T, R>(value: MaybeCtxFn<T, R>, ctx: T): R {
|
||||||
|
return typeof value === 'function' ? (value as (ctx: T) => R)(ctx) : value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T">
|
||||||
|
const props = defineProps<{
|
||||||
|
stages: StageConfigInput<T>[]
|
||||||
|
context: T
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modal = useTemplateRef<InstanceType<typeof NewModal>>('modal')
|
||||||
|
const currentStageIndex = ref<number>(0)
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
modal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setStage = (indexOrId: number | string) => {
|
||||||
|
let index: number = 0
|
||||||
|
if (typeof indexOrId === 'number') {
|
||||||
|
index = indexOrId
|
||||||
|
if (index < 0 || index >= props.stages.length) return
|
||||||
|
} else {
|
||||||
|
index = props.stages.findIndex((stage) => stage.id === indexOrId)
|
||||||
|
if (index === -1) return
|
||||||
|
}
|
||||||
|
while (index < props.stages.length) {
|
||||||
|
const skip = props.stages[index]?.skip
|
||||||
|
if (!skip || !resolveCtxFn(skip, props.context)) break
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
if (index < props.stages.length) {
|
||||||
|
currentStageIndex.value = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStage = () => {
|
||||||
|
if (currentStageIndex.value === -1) return
|
||||||
|
if (currentStageIndex.value >= props.stages.length - 1) return
|
||||||
|
let nextIndex = currentStageIndex.value + 1
|
||||||
|
while (nextIndex < props.stages.length) {
|
||||||
|
const skip = props.stages[nextIndex]?.skip
|
||||||
|
if (!skip || !resolveCtxFn(skip, props.context)) break
|
||||||
|
nextIndex++
|
||||||
|
}
|
||||||
|
if (nextIndex < props.stages.length) {
|
||||||
|
currentStageIndex.value = nextIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevStage = () => {
|
||||||
|
if (currentStageIndex.value <= 0) return
|
||||||
|
let prevIndex = currentStageIndex.value - 1
|
||||||
|
while (prevIndex >= 0) {
|
||||||
|
const skip = props.stages[prevIndex]?.skip
|
||||||
|
if (!skip || !resolveCtxFn(skip, props.context)) break
|
||||||
|
prevIndex--
|
||||||
|
}
|
||||||
|
if (prevIndex >= 0) {
|
||||||
|
currentStageIndex.value = prevIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStage = computed(() => props.stages[currentStageIndex.value])
|
||||||
|
|
||||||
|
const resolvedTitle = computed(() => {
|
||||||
|
const stage = currentStage.value
|
||||||
|
if (!stage) return ''
|
||||||
|
return resolveCtxFn(stage.title, props.context)
|
||||||
|
})
|
||||||
|
|
||||||
|
const leftButtonConfig = computed(() => {
|
||||||
|
const stage = currentStage.value
|
||||||
|
if (!stage) return null
|
||||||
|
return resolveCtxFn(stage.leftButtonConfig, props.context)
|
||||||
|
})
|
||||||
|
|
||||||
|
const rightButtonConfig = computed(() => {
|
||||||
|
const stage = currentStage.value
|
||||||
|
if (!stage) return null
|
||||||
|
return resolveCtxFn(stage.rightButtonConfig, props.context)
|
||||||
|
})
|
||||||
|
|
||||||
|
const progressValue = computed(() => {
|
||||||
|
const isProgressStage = (stage: StageConfigInput<T>) => {
|
||||||
|
if (stage.nonProgressStage) return false
|
||||||
|
const skip = stage.skip ? resolveCtxFn(stage.skip, props.context) : false
|
||||||
|
return !skip
|
||||||
|
}
|
||||||
|
|
||||||
|
const completedCount = props.stages
|
||||||
|
.slice(0, currentStageIndex.value + 1)
|
||||||
|
.filter(isProgressStage).length
|
||||||
|
const totalCount = props.stages.filter(isProgressStage).length
|
||||||
|
|
||||||
|
return totalCount > 0 ? (completedCount / totalCount) * 100 : 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'refresh-data' | 'hide'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function onModalHide() {
|
||||||
|
emit('hide')
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
setStage,
|
||||||
|
nextStage,
|
||||||
|
prevStage,
|
||||||
|
currentStageIndex,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
progress {
|
||||||
|
@apply bg-surface-3;
|
||||||
|
background-color: var(--surface-3, rgb(30, 30, 30));
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-webkit-progress-bar {
|
||||||
|
@apply bg-surface-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-webkit-progress-value {
|
||||||
|
@apply bg-contrast;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-moz-progress-bar {
|
||||||
|
@apply bg-contrast;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -22,23 +22,30 @@
|
|||||||
}"
|
}"
|
||||||
class="page-number-container"
|
class="page-number-container"
|
||||||
>
|
>
|
||||||
<div v-if="item === '-'">
|
<div v-if="item === '-'" class="rotate-90 grid place-content-center">
|
||||||
<GapIcon />
|
<EllipsisVerticalIcon />
|
||||||
</div>
|
</div>
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
v-else
|
v-else
|
||||||
circular
|
circular
|
||||||
:color="page === item ? 'brand' : 'standard'"
|
:color="page === item ? 'brand' : 'standard'"
|
||||||
:type="page === item ? 'standard' : 'transparent'"
|
:type="page === item ? 'highlight' : 'transparent'"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
v-if="linkFunction"
|
v-if="linkFunction"
|
||||||
:href="linkFunction(item)"
|
:href="linkFunction(item)"
|
||||||
|
:class="page === item ? '!text-brand' : ''"
|
||||||
@click.prevent="page !== item ? switchPage(item) : null"
|
@click.prevent="page !== item ? switchPage(item) : null"
|
||||||
>
|
>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</a>
|
</a>
|
||||||
<button v-else @click="page !== item ? switchPage(item) : null">{{ item }}</button>
|
<button
|
||||||
|
v-else
|
||||||
|
:class="page === item ? '!text-brand' : ''"
|
||||||
|
@click="page !== item ? switchPage(item) : null"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -58,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronLeftIcon, ChevronRightIcon, GapIcon } from '@modrinth/assets'
|
import { ChevronLeftIcon, ChevronRightIcon, EllipsisVerticalIcon } from '@modrinth/assets'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import ButtonStyled from './ButtonStyled.vue'
|
import ButtonStyled from './ButtonStyled.vue'
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export { default as CopyCode } from './CopyCode.vue'
|
|||||||
export { default as DoubleIcon } from './DoubleIcon.vue'
|
export { default as DoubleIcon } from './DoubleIcon.vue'
|
||||||
export { default as DropArea } from './DropArea.vue'
|
export { default as DropArea } from './DropArea.vue'
|
||||||
export { default as DropdownSelect } from './DropdownSelect.vue'
|
export { default as DropdownSelect } from './DropdownSelect.vue'
|
||||||
|
export { default as DropzoneFileInput } from './DropzoneFileInput.vue'
|
||||||
export { default as EnvironmentIndicator } from './EnvironmentIndicator.vue'
|
export { default as EnvironmentIndicator } from './EnvironmentIndicator.vue'
|
||||||
export { default as ErrorInformationCard } from './ErrorInformationCard.vue'
|
export { default as ErrorInformationCard } from './ErrorInformationCard.vue'
|
||||||
export { default as FileInput } from './FileInput.vue'
|
export { default as FileInput } from './FileInput.vue'
|
||||||
@@ -32,6 +33,9 @@ export { default as JoinedButtons } from './JoinedButtons.vue'
|
|||||||
export { default as LoadingIndicator } from './LoadingIndicator.vue'
|
export { default as LoadingIndicator } from './LoadingIndicator.vue'
|
||||||
export { default as ManySelect } from './ManySelect.vue'
|
export { default as ManySelect } from './ManySelect.vue'
|
||||||
export { default as MarkdownEditor } from './MarkdownEditor.vue'
|
export { default as MarkdownEditor } from './MarkdownEditor.vue'
|
||||||
|
export type { MaybeCtxFn, StageButtonConfig, StageConfigInput } from './MultiStageModal.vue'
|
||||||
|
export { default as MultiStageModal } from './MultiStageModal.vue'
|
||||||
|
export { resolveCtxFn } from './MultiStageModal.vue'
|
||||||
export { default as OptionGroup } from './OptionGroup.vue'
|
export { default as OptionGroup } from './OptionGroup.vue'
|
||||||
export type { Option as OverflowMenuOption } from './OverflowMenu.vue'
|
export type { Option as OverflowMenuOption } from './OverflowMenu.vue'
|
||||||
export { default as OverflowMenu } from './OverflowMenu.vue'
|
export { default as OverflowMenu } from './OverflowMenu.vue'
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ function handleKeyDown(event: KeyboardEvent) {
|
|||||||
box-shadow: 4px 4px 26px 10px rgba(0, 0, 0, 0.08);
|
box-shadow: 4px 4px 26px 10px rgba(0, 0, 0, 0.08);
|
||||||
max-height: calc(100% - 2 * var(--gap-lg));
|
max-height: calc(100% - 2 * var(--gap-lg));
|
||||||
max-width: min(var(--_max-width, 60rem), calc(100% - 2 * var(--gap-lg)));
|
max-width: min(var(--_max-width, 60rem), calc(100% - 2 * var(--gap-lg)));
|
||||||
overflow-y: auto;
|
overflow-y: hidden;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|||||||
@@ -1,18 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3 flex flex-wrap gap-2">
|
<div class="flex flex-col gap-3 mb-3">
|
||||||
<VersionFilterControl
|
<div class="flex flex-wrap justify-between gap-2">
|
||||||
ref="versionFilters"
|
<VersionFilterControl
|
||||||
:versions="versions"
|
ref="versionFilters"
|
||||||
:game-versions="gameVersions"
|
:versions="versions"
|
||||||
:base-id="`${baseId}-filter`"
|
:game-versions="gameVersions"
|
||||||
@update:query="updateQuery"
|
:base-id="`${baseId}-filter`"
|
||||||
/>
|
@update:query="updateQuery"
|
||||||
<Pagination
|
/>
|
||||||
:page="currentPage"
|
|
||||||
class="ml-auto mt-auto"
|
<ButtonStyled v-if="openModal" color="green">
|
||||||
:count="Math.ceil(filteredVersions.length / pageSize)"
|
<button @click="openModal"><PlusIcon /> Create version</button>
|
||||||
@switch-page="switchPage"
|
</ButtonStyled>
|
||||||
/>
|
|
||||||
|
<Pagination
|
||||||
|
v-if="!openModal"
|
||||||
|
:page="currentPage"
|
||||||
|
class="mt-auto"
|
||||||
|
:count="Math.ceil(filteredVersions.length / pageSize)"
|
||||||
|
@switch-page="switchPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="openModal && filteredVersions.length > pageSize"
|
||||||
|
class="flex flex-wrap justify-between items-center gap-2"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Showing {{ (currentPage - 1) * pageSize + 1 }} to
|
||||||
|
{{ Math.min(currentPage * pageSize, filteredVersions.length) }} of
|
||||||
|
{{ filteredVersions.length }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
:page="currentPage"
|
||||||
|
class="mt-auto"
|
||||||
|
:count="Math.ceil(filteredVersions.length / pageSize)"
|
||||||
|
@switch-page="switchPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="versions.length > 0"
|
v-if="versions.length > 0"
|
||||||
@@ -169,14 +195,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CalendarIcon, DownloadIcon, StarIcon } from '@modrinth/assets'
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { CalendarIcon, DownloadIcon, PlusIcon, StarIcon } from '@modrinth/assets'
|
||||||
|
import { ButtonStyled } from '@modrinth/ui'
|
||||||
import {
|
import {
|
||||||
formatBytes,
|
formatBytes,
|
||||||
formatCategory,
|
formatCategory,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
formatVersionsForDisplay,
|
formatVersionsForDisplay,
|
||||||
type GameVersionTag,
|
type GameVersionTag,
|
||||||
type PlatformTag,
|
|
||||||
type Version,
|
type Version,
|
||||||
} from '@modrinth/utils'
|
} from '@modrinth/utils'
|
||||||
import { useVIntl } from '@vintl/vintl'
|
import { useVIntl } from '@vintl/vintl'
|
||||||
@@ -207,9 +234,10 @@ const props = withDefaults(
|
|||||||
versions: VersionWithDisplayUrlEnding[]
|
versions: VersionWithDisplayUrlEnding[]
|
||||||
showFiles?: boolean
|
showFiles?: boolean
|
||||||
currentMember?: boolean
|
currentMember?: boolean
|
||||||
loaders: PlatformTag[]
|
loaders: Labrinth.Tags.v2.Loader[]
|
||||||
gameVersions: GameVersionTag[]
|
gameVersions: GameVersionTag[]
|
||||||
versionLink?: (version: Version) => string
|
versionLink?: (version: Version) => string
|
||||||
|
openModal?: () => void
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
baseId: undefined,
|
baseId: undefined,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
import { defineMessage, type MessageDescriptor, useVIntl } from '@vintl/vintl'
|
import { defineMessage, type MessageDescriptor, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ import LargeRadioButton from '../../../base/LargeRadioButton.vue'
|
|||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const value = defineModel<string | undefined>({ required: true })
|
const value = defineModel<Labrinth.Projects.v3.Environment | undefined>({ required: true })
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -134,7 +135,7 @@ type SubOptionKey = ValidKeys<(typeof OUTER_OPTIONS)[keyof typeof OUTER_OPTIONS]
|
|||||||
const currentOuterOption = ref<OuterOptionKey>()
|
const currentOuterOption = ref<OuterOptionKey>()
|
||||||
const currentSubOption = ref<SubOptionKey>()
|
const currentSubOption = ref<SubOptionKey>()
|
||||||
|
|
||||||
const computedOption = computed<string>(() => {
|
const computedOption = computed<Labrinth.Projects.v3.Environment>(() => {
|
||||||
switch (currentOuterOption.value) {
|
switch (currentOuterOption.value) {
|
||||||
case 'client':
|
case 'client':
|
||||||
return 'client_only'
|
return 'client_only'
|
||||||
@@ -169,7 +170,7 @@ const computedOption = computed<string>(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function loadEnvironmentValues(env?: EnvironmentV3) {
|
function loadEnvironmentValues(env?: Labrinth.Projects.v3.Environment) {
|
||||||
switch (env) {
|
switch (env) {
|
||||||
case 'client_and_server':
|
case 'client_and_server':
|
||||||
currentOuterOption.value = 'client_and_server'
|
currentOuterOption.value = 'client_and_server'
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface ProjectPageContext {
|
|||||||
projectV2: Ref<Labrinth.Projects.v2.Project>
|
projectV2: Ref<Labrinth.Projects.v2.Project>
|
||||||
projectV3: Ref<Labrinth.Projects.v3.Project>
|
projectV3: Ref<Labrinth.Projects.v3.Project>
|
||||||
refreshProject: () => Promise<void>
|
refreshProject: () => Promise<void>
|
||||||
|
refreshVersions: () => Promise<void>
|
||||||
currentMember: Ref<TeamMember>
|
currentMember: Ref<TeamMember>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
267
packages/ui/tailwind-preset.js
Normal file
267
packages/ui/tailwind-preset.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./src/components/**/*.{js,vue,ts}',
|
||||||
|
'./src/layouts/**/*.vue',
|
||||||
|
'./src/pages/**/*.vue',
|
||||||
|
'./src/plugins/**/*.{js,ts}',
|
||||||
|
'./src/app.vue',
|
||||||
|
'./src/error.vue',
|
||||||
|
// monorepo - TODO: migrate this to its own package
|
||||||
|
'../../packages/**/*.{js,vue,ts}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
surface: {
|
||||||
|
1: 'var(--surface-1)',
|
||||||
|
2: 'var(--surface-2)',
|
||||||
|
3: 'var(--surface-3)',
|
||||||
|
4: 'var(--surface-4)',
|
||||||
|
5: 'var(--surface-5)',
|
||||||
|
},
|
||||||
|
|
||||||
|
/// TODO: Clean up these aliases within codebase to use default, primary, tertiary.
|
||||||
|
// text-default
|
||||||
|
primary: 'var(--color-text-default)',
|
||||||
|
|
||||||
|
// text-primary
|
||||||
|
contrast: 'var(--color-text-primary)',
|
||||||
|
|
||||||
|
// text-tertiary
|
||||||
|
secondary: 'var(--color-text-tertiary)',
|
||||||
|
|
||||||
|
red: {
|
||||||
|
DEFAULT: 'var(--color-red)',
|
||||||
|
50: 'var(--color-red-50)',
|
||||||
|
100: 'var(--color-red-100)',
|
||||||
|
200: 'var(--color-red-200)',
|
||||||
|
300: 'var(--color-red-300)',
|
||||||
|
400: 'var(--color-red-400)',
|
||||||
|
500: 'var(--color-red-500)',
|
||||||
|
600: 'var(--color-red-600)',
|
||||||
|
700: 'var(--color-red-700)',
|
||||||
|
800: 'var(--color-red-800)',
|
||||||
|
900: 'var(--color-red-900)',
|
||||||
|
950: 'var(--color-red-950)',
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
DEFAULT: 'var(--color-orange)',
|
||||||
|
50: 'var(--color-orange-50)',
|
||||||
|
100: 'var(--color-orange-100)',
|
||||||
|
200: 'var(--color-orange-200)',
|
||||||
|
300: 'var(--color-orange-300)',
|
||||||
|
400: 'var(--color-orange-400)',
|
||||||
|
500: 'var(--color-orange-500)',
|
||||||
|
600: 'var(--color-orange-600)',
|
||||||
|
700: 'var(--color-orange-700)',
|
||||||
|
800: 'var(--color-orange-800)',
|
||||||
|
900: 'var(--color-orange-900)',
|
||||||
|
950: 'var(--color-orange-950)',
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
DEFAULT: 'var(--color-green)',
|
||||||
|
50: 'var(--color-green-50)',
|
||||||
|
100: 'var(--color-green-100)',
|
||||||
|
200: 'var(--color-green-200)',
|
||||||
|
300: 'var(--color-green-300)',
|
||||||
|
400: 'var(--color-green-400)',
|
||||||
|
500: 'var(--color-green-500)',
|
||||||
|
600: 'var(--color-green-600)',
|
||||||
|
700: 'var(--color-green-700)',
|
||||||
|
800: 'var(--color-green-800)',
|
||||||
|
900: 'var(--color-green-900)',
|
||||||
|
950: 'var(--color-green-950)',
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
DEFAULT: 'var(--color-blue)',
|
||||||
|
50: 'var(--color-blue-50)',
|
||||||
|
100: 'var(--color-blue-100)',
|
||||||
|
200: 'var(--color-blue-200)',
|
||||||
|
300: 'var(--color-blue-300)',
|
||||||
|
400: 'var(--color-blue-400)',
|
||||||
|
500: 'var(--color-blue-500)',
|
||||||
|
600: 'var(--color-blue-600)',
|
||||||
|
700: 'var(--color-blue-700)',
|
||||||
|
800: 'var(--color-blue-800)',
|
||||||
|
900: 'var(--color-blue-900)',
|
||||||
|
950: 'var(--color-blue-950)',
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
DEFAULT: 'var(--color-purple)',
|
||||||
|
50: 'var(--color-purple-50)',
|
||||||
|
100: 'var(--color-purple-100)',
|
||||||
|
200: 'var(--color-purple-200)',
|
||||||
|
300: 'var(--color-purple-300)',
|
||||||
|
400: 'var(--color-purple-400)',
|
||||||
|
500: 'var(--color-purple-500)',
|
||||||
|
600: 'var(--color-purple-600)',
|
||||||
|
700: 'var(--color-purple-700)',
|
||||||
|
800: 'var(--color-purple-800)',
|
||||||
|
900: 'var(--color-purple-900)',
|
||||||
|
950: 'var(--color-purple-950)',
|
||||||
|
},
|
||||||
|
gray: {
|
||||||
|
DEFAULT: 'var(--color-gray)',
|
||||||
|
50: 'var(--color-gray-50)',
|
||||||
|
100: 'var(--color-gray-100)',
|
||||||
|
200: 'var(--color-gray-200)',
|
||||||
|
300: 'var(--color-gray-300)',
|
||||||
|
400: 'var(--color-gray-400)',
|
||||||
|
500: 'var(--color-gray-500)',
|
||||||
|
600: 'var(--color-gray-600)',
|
||||||
|
700: 'var(--color-gray-700)',
|
||||||
|
800: 'var(--color-gray-800)',
|
||||||
|
900: 'var(--color-gray-900)',
|
||||||
|
950: 'var(--color-gray-950)',
|
||||||
|
},
|
||||||
|
|
||||||
|
/// === LEGACY ===
|
||||||
|
icon: 'var(--color-base)',
|
||||||
|
// Text
|
||||||
|
inactive: 'var(--color-text-inactive)',
|
||||||
|
dark: 'var(--color-text-dark)',
|
||||||
|
inverted: 'var(--color-text-inverted)',
|
||||||
|
heading: 'var(--color-heading)',
|
||||||
|
bg: {
|
||||||
|
DEFAULT: 'var(--surface-1)', // var(--color-bg)
|
||||||
|
red: 'var(--color-red-bg)',
|
||||||
|
orange: 'var(--color-orange-bg)',
|
||||||
|
green: 'var(--color-green-bg)',
|
||||||
|
blue: 'var(--color-blue-bg)',
|
||||||
|
purple: 'var(--color-purple-bg)',
|
||||||
|
raised: 'var(--surface-3)', // var(--color-raised-bg)
|
||||||
|
},
|
||||||
|
banners: {
|
||||||
|
error: {
|
||||||
|
bg: 'var(--banner-error-bg)',
|
||||||
|
text: 'var(--banner-error-text)',
|
||||||
|
border: 'var(--banner-error-border)',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
bg: 'var(--banner-warning-bg)',
|
||||||
|
text: 'var(--banner-warning-text)',
|
||||||
|
border: 'var(--banner-warning-border)',
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
bg: 'var(--banner-info-bg)',
|
||||||
|
text: 'var(--banner-info-text)',
|
||||||
|
border: 'var(--banner-info-border)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
DEFAULT: 'var(--color-brand-highlight)',
|
||||||
|
red: 'var(--color-red-highlight)',
|
||||||
|
orange: 'var(--color-orange-highlight)',
|
||||||
|
green: 'var(--color-green-highlight)',
|
||||||
|
blue: 'var(--color-blue-highlight)',
|
||||||
|
purple: 'var(--color-purple-highlight)',
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
DEFAULT: 'var(--color-divider)',
|
||||||
|
dark: 'var(--color-divider-dark)',
|
||||||
|
},
|
||||||
|
brand: {
|
||||||
|
DEFAULT: 'var(--color-brand)',
|
||||||
|
red: 'var(--color-red)',
|
||||||
|
orange: 'var(--color-orange)',
|
||||||
|
green: 'var(--color-green)',
|
||||||
|
blue: 'var(--color-blue)',
|
||||||
|
purple: 'var(--color-purple)',
|
||||||
|
highlight: 'var(--color-brand-highlight)',
|
||||||
|
shadow: 'var(--color-brand-shadow)',
|
||||||
|
inverted: 'var(--color-accent-contrast)',
|
||||||
|
},
|
||||||
|
tabUnderlineHovered: 'var(--tab-underline-hovered)',
|
||||||
|
button: {
|
||||||
|
bg: 'var(--color-button-bg)',
|
||||||
|
text: 'var(--color-button-text)',
|
||||||
|
bgHover: 'var(--color-button-bg-hover)',
|
||||||
|
textHover: 'var(--color-button-text-hover)',
|
||||||
|
bgActive: 'var(--color-button-bg-active)',
|
||||||
|
textActive: 'var(--color-button-text-active)',
|
||||||
|
border: 'var(--color-button-border)',
|
||||||
|
bgSelected: 'var(--color-button-bg-selected)',
|
||||||
|
textSelected: 'var(--color-button-text-selected)',
|
||||||
|
},
|
||||||
|
toggleHandle: 'var(--color-toggle-handle)',
|
||||||
|
dropdown: {
|
||||||
|
bg: 'var(--color-dropdown-bg)',
|
||||||
|
text: 'var(--color-dropdown-text)',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
bg: 'var(--color-tooltip-bg)',
|
||||||
|
text: 'var(--color-tooltip-text)',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
bg: 'var(--color-code-bg)',
|
||||||
|
text: 'var(--color-code-text)',
|
||||||
|
},
|
||||||
|
kbdShadow: 'var(--color-kbd-shadow)',
|
||||||
|
ad: {
|
||||||
|
DEFAULT: 'var(--color-ad)',
|
||||||
|
raised: 'var(--color-ad-raised)',
|
||||||
|
contrast: 'var(--color-ad-contrast)',
|
||||||
|
highlight: 'var(--color-ad-highlight)',
|
||||||
|
},
|
||||||
|
greyLink: {
|
||||||
|
DEFAULT: 'var(--color-grey-link)',
|
||||||
|
hover: 'var(--color-grey-link-hover)',
|
||||||
|
active: 'var(--color-grey-link-active)',
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
DEFAULT: 'var(--color-link)',
|
||||||
|
hover: 'var(--color-link-hover)',
|
||||||
|
active: 'var(--color-link-active)',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
bg: 'var(--color-warning-bg)',
|
||||||
|
text: 'var(--color-warning-text)',
|
||||||
|
banner: {
|
||||||
|
text: 'var(--color-warning-banner-text)',
|
||||||
|
bg: 'var(--color-warning-banner-bg)',
|
||||||
|
side: 'var(--color-warning-banner-side)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
infoBanner: {
|
||||||
|
text: 'var(--color-info-banner-text)',
|
||||||
|
bg: 'var(--color-info-banner-bg)',
|
||||||
|
side: 'var(--color-info-banner-side)',
|
||||||
|
},
|
||||||
|
blockQuote: 'var(--color-block-quote)',
|
||||||
|
headerUnderline: 'var(--color-header-underline)',
|
||||||
|
hr: 'var(--color-hr)',
|
||||||
|
table: {
|
||||||
|
border: 'var(--color-table-border)',
|
||||||
|
alternateRow: ' var(--color-table-alternate-row)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
mazeBg: 'var(--landing-maze-bg)',
|
||||||
|
mazeGradientBg: 'var(--landing-maze-gradient-bg)',
|
||||||
|
// @ts-ignore
|
||||||
|
landing: {
|
||||||
|
mazeOuterBg: 'var(--landing-maze-outer-bg)',
|
||||||
|
colorHeading: 'var(--landing-color-heading)',
|
||||||
|
colorSubheading: 'var(--landing-color-subheading)',
|
||||||
|
transitionGradientStart: 'var(--landing-transition-gradient-start)',
|
||||||
|
transitionGradientEnd: 'var(--landing-transition-gradient-end)',
|
||||||
|
hoverCardGradient: 'var(--landing-hover-card-gradient)',
|
||||||
|
borderGradient: 'var(--landing-border-gradient)',
|
||||||
|
borderColor: 'var(--landing-border-color)',
|
||||||
|
creatorGradient: 'var(--landing-creator-gradient)',
|
||||||
|
blobGradient: 'var(--landing-blob-gradient)',
|
||||||
|
cardBg: 'var(--landing-card-bg)',
|
||||||
|
blueLabel: 'var(--landing-blue-label)',
|
||||||
|
blueLabelBg: 'var(--landing-blue-label-bg)',
|
||||||
|
greenLabel: 'var(--landing-green-label)',
|
||||||
|
greenLabelBg: 'var(--landing-green-label-bg)',
|
||||||
|
rawBg: 'var(--landing-raw-bg)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
corePlugins: {
|
||||||
|
preflight: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
5
packages/ui/tailwind.config.js
Normal file
5
packages/ui/tailwind.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ['./tailwind-preset.js'],
|
||||||
|
presets: [require('./tailwind-preset.js')],
|
||||||
|
}
|
||||||
@@ -283,7 +283,14 @@ export interface FileDependency {
|
|||||||
export type Dependency = VersionDependency | ProjectDependency | FileDependency
|
export type Dependency = VersionDependency | ProjectDependency | FileDependency
|
||||||
export type VersionChannel = 'release' | 'beta' | 'alpha'
|
export type VersionChannel = 'release' | 'beta' | 'alpha'
|
||||||
export type VersionStatus = 'listed' | 'archived' | 'draft' | 'unlisted' | 'scheduled' | 'unknown'
|
export type VersionStatus = 'listed' | 'archived' | 'draft' | 'unlisted' | 'scheduled' | 'unknown'
|
||||||
export type FileType = 'required-resource-pack' | 'optional-resource-pack'
|
export type FileType =
|
||||||
|
| 'required-resource-pack'
|
||||||
|
| 'optional-resource-pack'
|
||||||
|
| 'sources-jar'
|
||||||
|
| 'dev-jar'
|
||||||
|
| 'javadoc-jar'
|
||||||
|
| 'signature'
|
||||||
|
| 'unknown'
|
||||||
|
|
||||||
export interface VersionFileHash {
|
export interface VersionFileHash {
|
||||||
sha512: string
|
sha512: string
|
||||||
@@ -291,7 +298,7 @@ export interface VersionFileHash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface VersionFile {
|
export interface VersionFile {
|
||||||
hashes: VersionFileHash[]
|
hashes: VersionFileHash
|
||||||
url: string
|
url: string
|
||||||
filename: string
|
filename: string
|
||||||
primary: boolean
|
primary: boolean
|
||||||
|
|||||||
@@ -305,21 +305,23 @@ export const fileIsValid = (file, validationOptions) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const acceptFileFromProjectType = (projectType) => {
|
export const acceptFileFromProjectType = (projectType) => {
|
||||||
|
const commonTypes = '.sig,.asc,.gpg,application/pgp-signature,application/pgp-keys'
|
||||||
switch (projectType) {
|
switch (projectType) {
|
||||||
case 'mod':
|
case 'mod':
|
||||||
return '.jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip'
|
return `.jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip,${commonTypes}`
|
||||||
case 'plugin':
|
case 'plugin':
|
||||||
return '.jar,.zip,application/java-archive,application/x-java-archive,application/zip'
|
return `.jar,.zip,application/java-archive,application/x-java-archive,application/zip,${commonTypes}`
|
||||||
case 'resourcepack':
|
case 'resourcepack':
|
||||||
return '.zip,application/zip'
|
return `.zip,application/zip,${commonTypes}`
|
||||||
case 'shader':
|
case 'shader':
|
||||||
return '.zip,application/zip'
|
return `.zip,application/zip,${commonTypes}`
|
||||||
case 'datapack':
|
case 'datapack':
|
||||||
return '.zip,application/zip'
|
return `.zip,application/zip,${commonTypes}`
|
||||||
case 'modpack':
|
case 'modpack':
|
||||||
return '.mrpack,application/x-modrinth-modpack+zip,application/zip'
|
return `.mrpack,application/x-modrinth-modpack+zip,application/zip,${commonTypes}`
|
||||||
default:
|
default:
|
||||||
return '*'
|
// all of the above
|
||||||
|
return `.jar,.zip,.litemod,.mrpack,application/java-archive,application/x-java-archive,application/zip,application/x-modrinth-modpack+zip,${commonTypes}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -177,6 +177,9 @@ importers:
|
|||||||
vite:
|
vite:
|
||||||
specifier: ^5.4.6
|
specifier: ^5.4.6
|
||||||
version: 5.4.11(@types/node@22.4.1)(sass@1.77.6)(terser@5.44.1)
|
version: 5.4.11(@types/node@22.4.1)(sass@1.77.6)(terser@5.44.1)
|
||||||
|
vue-component-type-helpers:
|
||||||
|
specifier: ^3.1.8
|
||||||
|
version: 3.1.8
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: ^2.1.6
|
specifier: ^2.1.6
|
||||||
version: 2.1.6(typescript@5.5.4)
|
version: 2.1.6(typescript@5.5.4)
|
||||||
@@ -393,6 +396,9 @@ importers:
|
|||||||
vite-svg-loader:
|
vite-svg-loader:
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.1.0(vue@3.5.13(typescript@5.5.4))
|
version: 5.1.0(vue@3.5.13(typescript@5.5.4))
|
||||||
|
vue-component-type-helpers:
|
||||||
|
specifier: ^3.1.8
|
||||||
|
version: 3.1.8
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: ^2.0.24
|
specifier: ^2.0.24
|
||||||
version: 2.1.6(typescript@5.5.4)
|
version: 2.1.6(typescript@5.5.4)
|
||||||
@@ -656,6 +662,9 @@ importers:
|
|||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.13
|
specifier: ^3.5.13
|
||||||
version: 3.5.13(typescript@5.5.4)
|
version: 3.5.13(typescript@5.5.4)
|
||||||
|
vue-component-type-helpers:
|
||||||
|
specifier: ^3.1.8
|
||||||
|
version: 3.1.8
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: 4.3.0
|
specifier: 4.3.0
|
||||||
version: 4.3.0(vue@3.5.13(typescript@5.5.4))
|
version: 4.3.0(vue@3.5.13(typescript@5.5.4))
|
||||||
@@ -8250,6 +8259,9 @@ packages:
|
|||||||
vue-bundle-renderer@2.1.1:
|
vue-bundle-renderer@2.1.1:
|
||||||
resolution: {integrity: sha512-+qALLI5cQncuetYOXp4yScwYvqh8c6SMXee3B+M7oTZxOgtESP0l4j/fXdEJoZ+EdMxkGWIj+aSEyjXkOdmd7g==}
|
resolution: {integrity: sha512-+qALLI5cQncuetYOXp4yScwYvqh8c6SMXee3B+M7oTZxOgtESP0l4j/fXdEJoZ+EdMxkGWIj+aSEyjXkOdmd7g==}
|
||||||
|
|
||||||
|
vue-component-type-helpers@3.1.8:
|
||||||
|
resolution: {integrity: sha512-oaowlmEM6BaYY+8o+9D9cuzxpWQWHqHTMKakMxXu0E+UCIOMTljyIPO15jcnaCwJtZu/zWDotK7mOIHvWD9mcw==}
|
||||||
|
|
||||||
vue-confetti-explosion@1.0.2:
|
vue-confetti-explosion@1.0.2:
|
||||||
resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==}
|
resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -14084,7 +14096,7 @@ snapshots:
|
|||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.4.1
|
'@types/node': 20.14.11
|
||||||
merge-stream: 2.0.0
|
merge-stream: 2.0.0
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
optional: true
|
optional: true
|
||||||
@@ -17522,6 +17534,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ufo: 1.5.4
|
ufo: 1.5.4
|
||||||
|
|
||||||
|
vue-component-type-helpers@3.1.8: {}
|
||||||
|
|
||||||
vue-confetti-explosion@1.0.2(vue@3.5.13(typescript@5.5.4)):
|
vue-confetti-explosion@1.0.2(vue@3.5.13(typescript@5.5.4)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.13(typescript@5.5.4)
|
vue: 3.5.13(typescript@5.5.4)
|
||||||
|
|||||||
Reference in New Issue
Block a user