forked from didirus/AstralRinth
* 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>
419 lines
12 KiB
TypeScript
419 lines
12 KiB
TypeScript
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
|
|
}
|