mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 05:48:57 +00:00
feat: move download all button in heading with compatible version
This commit is contained in:
@@ -1,25 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="downloadRows.length > 0" class="flex flex-col gap-1">
|
<div v-if="downloadRows.length > 0" class="flex flex-col gap-1">
|
||||||
<div
|
<div v-if="showTitle" class="flex flex-wrap items-center justify-between gap-2">
|
||||||
v-if="showTitle || downloadableDependencyFiles.length > 0"
|
<h3 class="m-0 text-base font-semibold text-contrast">
|
||||||
class="flex flex-wrap items-center justify-between gap-2"
|
|
||||||
>
|
|
||||||
<h3 v-if="showTitle" class="m-0 text-base font-semibold text-contrast">
|
|
||||||
{{ sectionTitle }}
|
{{ sectionTitle }}
|
||||||
</h3>
|
</h3>
|
||||||
<ButtonStyled v-if="downloadableDependencyFiles.length > 0" type="transparent">
|
|
||||||
<button :disabled="downloadingDependencies" @click="downloadAllDependencies">
|
|
||||||
<SpinnerIcon v-if="downloadingDependencies" aria-hidden="true" class="animate-spin" />
|
|
||||||
<DownloadIcon v-else aria-hidden="true" />
|
|
||||||
{{
|
|
||||||
formatMessage(
|
|
||||||
downloadingDependencies
|
|
||||||
? messages.downloadingDependencies
|
|
||||||
: messages.downloadAllDependencies,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<DownloadDependency
|
<DownloadDependency
|
||||||
@@ -34,20 +18,17 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Labrinth } from '@modrinth/api-client'
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
import { DownloadIcon, FileIcon, SpinnerIcon } from '@modrinth/assets'
|
import { FileIcon } from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
ButtonStyled,
|
|
||||||
type CdnDownloadReason,
|
type CdnDownloadReason,
|
||||||
defineMessages,
|
defineMessages,
|
||||||
fileTypeMessages,
|
fileTypeMessages,
|
||||||
injectModrinthClient,
|
injectModrinthClient,
|
||||||
injectNotificationManager,
|
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import type { DisplayProjectType } from '@modrinth/utils'
|
import type { DisplayProjectType } from '@modrinth/utils'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import JSZip from 'jszip'
|
import { type Component, computed, watch } from 'vue'
|
||||||
import { type Component, computed, ref } from 'vue'
|
|
||||||
|
|
||||||
import DownloadDependency from './DownloadDependency.vue'
|
import DownloadDependency from './DownloadDependency.vue'
|
||||||
|
|
||||||
@@ -106,12 +87,11 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
download: []
|
download: []
|
||||||
|
'update:downloadable-files': [files: DownloadableDependencyFile[]]
|
||||||
}>()
|
}>()
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
||||||
const { addNotification } = injectNotificationManager()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const downloadingDependencies = ref(false)
|
|
||||||
|
|
||||||
const shouldResolveDependencies = computed(
|
const shouldResolveDependencies = computed(
|
||||||
() => !props.dependencies && !!props.project && !!props.selectedVersion,
|
() => !props.dependencies && !!props.project && !!props.selectedVersion,
|
||||||
@@ -282,18 +262,20 @@ const downloadRows = computed<DownloadDependencyRow[]>(() => [
|
|||||||
|
|
||||||
const sectionTitle = computed(() =>
|
const sectionTitle = computed(() =>
|
||||||
formatMessage(
|
formatMessage(
|
||||||
dependencyRows.value.length > 0
|
dependencyRows.value.length > 0 ? messages.dependenciesTitle : messages.additionalFilesTitle,
|
||||||
? messages.dependenciesTitle
|
|
||||||
: messages.additionalFilesTitle,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const downloadableDependencyFiles = computed<DownloadableDependencyFile[]>(() =>
|
const downloadableDependencyFiles = computed<DownloadableDependencyFile[]>(() =>
|
||||||
collectDownloadableDependencyFiles(downloadRows.value),
|
collectDownloadableDependencyFiles(dependencyRows.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
const dependencyZipFilename = computed(
|
watch(
|
||||||
() => `${sanitizeFilename(props.project?.title || 'Project')} dependencies.zip`,
|
downloadableDependencyFiles,
|
||||||
|
(files) => {
|
||||||
|
emit('update:downloadable-files', files)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
function primaryFileForVersion(version?: Labrinth.Versions.v3.Version) {
|
function primaryFileForVersion(version?: Labrinth.Versions.v3.Version) {
|
||||||
@@ -396,59 +378,6 @@ function collectDownloadableDependencyFiles(
|
|||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadAllDependencies() {
|
|
||||||
if (downloadingDependencies.value || downloadableDependencyFiles.value.length === 0) return
|
|
||||||
|
|
||||||
downloadingDependencies.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const zip = new JSZip()
|
|
||||||
const usedFilenames = new Set<string>()
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
downloadableDependencyFiles.value.map(async (file) => {
|
|
||||||
const response = await fetch(file.href)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to download ${file.name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
zip.file(uniqueFilename(file.filename, usedFilenames), await response.blob())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
downloadBlob(
|
|
||||||
await zip.generateAsync({
|
|
||||||
type: 'blob',
|
|
||||||
mimeType: 'application/zip',
|
|
||||||
}),
|
|
||||||
dependencyZipFilename.value,
|
|
||||||
)
|
|
||||||
emit('download')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to download dependencies:', error)
|
|
||||||
addNotification({
|
|
||||||
title: formatMessage(messages.downloadDependenciesFailedTitle),
|
|
||||||
text: formatMessage(messages.downloadDependenciesFailedText),
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
downloadingDependencies.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadBlob(blob: Blob, filename: string) {
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
|
|
||||||
link.href = url
|
|
||||||
link.download = filename
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
link.remove()
|
|
||||||
window.setTimeout(() => URL.revokeObjectURL(url), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function filenameFromUrl(url: string) {
|
function filenameFromUrl(url: string) {
|
||||||
try {
|
try {
|
||||||
const filename = new URL(url).pathname.split('/').pop()
|
const filename = new URL(url).pathname.split('/').pop()
|
||||||
@@ -458,39 +387,6 @@ function filenameFromUrl(url: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeFilename(value: string) {
|
|
||||||
const sanitized = value
|
|
||||||
.replace(/[<>:"/\\|?*]/g, '')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
return sanitized || 'dependencies'
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniqueFilename(filename: string, usedFilenames: Set<string>) {
|
|
||||||
const sanitizedFilename = sanitizeFilename(filename)
|
|
||||||
|
|
||||||
if (!usedFilenames.has(sanitizedFilename)) {
|
|
||||||
usedFilenames.add(sanitizedFilename)
|
|
||||||
return sanitizedFilename
|
|
||||||
}
|
|
||||||
|
|
||||||
const extensionIndex = sanitizedFilename.lastIndexOf('.')
|
|
||||||
const basename =
|
|
||||||
extensionIndex > 0 ? sanitizedFilename.slice(0, extensionIndex) : sanitizedFilename
|
|
||||||
const extension = extensionIndex > 0 ? sanitizedFilename.slice(extensionIndex) : ''
|
|
||||||
let index = 2
|
|
||||||
let candidate = `${basename} (${index})${extension}`
|
|
||||||
|
|
||||||
while (usedFilenames.has(candidate)) {
|
|
||||||
index += 1
|
|
||||||
candidate = `${basename} (${index})${extension}`
|
|
||||||
}
|
|
||||||
|
|
||||||
usedFilenames.add(candidate)
|
|
||||||
return candidate
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
dependenciesTitle: {
|
dependenciesTitle: {
|
||||||
id: 'project.download.dependencies-title',
|
id: 'project.download.dependencies-title',
|
||||||
@@ -500,22 +396,6 @@ const messages = defineMessages({
|
|||||||
id: 'project.download.additional-files-title',
|
id: 'project.download.additional-files-title',
|
||||||
defaultMessage: 'Additional files',
|
defaultMessage: 'Additional files',
|
||||||
},
|
},
|
||||||
downloadAllDependencies: {
|
|
||||||
id: 'project.download.dependencies-download-all',
|
|
||||||
defaultMessage: 'Download all',
|
|
||||||
},
|
|
||||||
downloadingDependencies: {
|
|
||||||
id: 'project.download.dependencies-downloading',
|
|
||||||
defaultMessage: 'Downloading...',
|
|
||||||
},
|
|
||||||
downloadDependenciesFailedTitle: {
|
|
||||||
id: 'project.download.dependencies-failed-title',
|
|
||||||
defaultMessage: 'Could not download dependencies',
|
|
||||||
},
|
|
||||||
downloadDependenciesFailedText: {
|
|
||||||
id: 'project.download.dependencies-failed-text',
|
|
||||||
defaultMessage: 'One or more dependency files could not be downloaded. Please try again.',
|
|
||||||
},
|
|
||||||
alreadyInstalledDependency: {
|
alreadyInstalledDependency: {
|
||||||
id: 'project.download.dependency-already-installed',
|
id: 'project.download.dependency-already-installed',
|
||||||
defaultMessage: 'This dependency is already installed',
|
defaultMessage: 'This dependency is already installed',
|
||||||
|
|||||||
@@ -80,51 +80,70 @@
|
|||||||
</Combobox>
|
</Combobox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="selectedVersion" class="flex flex-col gap-1">
|
||||||
v-if="selectedVersion"
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
class="grid grid-cols-[1fr_min-content] items-center gap-3 rounded-2xl bg-surface-2 px-3 py-3"
|
<h3 class="relative top-0.5 m-0 text-base font-semibold text-contrast">
|
||||||
>
|
{{ formatMessage(messages.compatibleVersionTitle) }}
|
||||||
<div class="flex min-w-0 flex-col gap-1">
|
</h3>
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
<ButtonStyled v-if="downloadAllFiles.length > 1" type="transparent">
|
||||||
<nuxt-link
|
<button :disabled="downloadingSelectedVersion" @click="downloadSelectedVersionFiles">
|
||||||
v-tooltip="truncatedTooltip(versionNumberRef, selectedVersion.version_number)"
|
<SpinnerIcon v-if="downloadingSelectedVersion" aria-hidden="true" class="animate-spin" />
|
||||||
:to="`/${project.project_type}/${project.slug || project.id}/version/${selectedVersion.id}`"
|
<DownloadIcon v-else aria-hidden="true" />
|
||||||
target="_blank"
|
{{
|
||||||
rel="noopener noreferrer"
|
formatMessage(
|
||||||
class="block min-w-0 text-contrast no-underline hover:underline"
|
downloadingSelectedVersion
|
||||||
|
? messages.downloadingSelectedVersion
|
||||||
|
: messages.downloadAllSelectedVersion,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-[1fr_min-content] items-center gap-3 rounded-2xl bg-surface-2 px-3 py-3"
|
||||||
|
>
|
||||||
|
<div class="flex min-w-0 flex-col gap-1">
|
||||||
|
<div class="flex min-w-0 items-center gap-2">
|
||||||
|
<nuxt-link
|
||||||
|
v-tooltip="truncatedTooltip(versionNumberRef, selectedVersion.version_number)"
|
||||||
|
:to="`/${project.project_type}/${project.slug || project.id}/version/${selectedVersion.id}`"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="block min-w-0 text-contrast no-underline hover:underline"
|
||||||
|
>
|
||||||
|
<span ref="versionNumberRef" class="block truncate font-semibold">
|
||||||
|
{{ selectedVersion.version_number }}
|
||||||
|
</span>
|
||||||
|
</nuxt-link>
|
||||||
|
<VersionChannelTag
|
||||||
|
:channel="selectedVersion.version_type"
|
||||||
|
class="relative -top-px !py-0.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
ref="versionNameRef"
|
||||||
|
v-tooltip="truncatedTooltip(versionNameRef, selectedVersion.name)"
|
||||||
|
class="m-0 w-fit max-w-full truncate text-sm text-secondary"
|
||||||
>
|
>
|
||||||
<span ref="versionNumberRef" class="block truncate font-semibold">
|
{{ selectedVersion.name }}
|
||||||
{{ selectedVersion.version_number }}
|
</p>
|
||||||
</span>
|
|
||||||
</nuxt-link>
|
|
||||||
<VersionChannelTag
|
|
||||||
:channel="selectedVersion.version_type"
|
|
||||||
class="relative -top-px !py-0.5"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<ButtonStyled v-if="selectedPrimaryFile" color="brand" circular>
|
||||||
ref="versionNameRef"
|
<a
|
||||||
v-tooltip="truncatedTooltip(versionNameRef, selectedVersion.name)"
|
v-tooltip="'Download'"
|
||||||
class="m-0 w-fit max-w-full truncate text-sm text-secondary"
|
:href="selectedPrimaryFileDownloadUrl"
|
||||||
>
|
:download="selectedPrimaryFile.filename"
|
||||||
{{ selectedVersion.name }}
|
:aria-label="
|
||||||
</p>
|
formatMessage(messages.downloadVersion, {
|
||||||
|
version: selectedVersion.version_number,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@click="emit('download')"
|
||||||
|
>
|
||||||
|
<DownloadIcon aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<ButtonStyled v-if="selectedPrimaryFile" color="brand" circular>
|
|
||||||
<a
|
|
||||||
v-tooltip="'Download'"
|
|
||||||
:href="selectedPrimaryFileDownloadUrl"
|
|
||||||
:download="selectedPrimaryFile.filename"
|
|
||||||
:aria-label="
|
|
||||||
formatMessage(messages.downloadVersion, {
|
|
||||||
version: selectedVersion.version_number,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
@click="emit('download')"
|
|
||||||
>
|
|
||||||
<DownloadIcon aria-hidden="true" />
|
|
||||||
</a>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
</div>
|
||||||
<p v-else-if="currentPlatform && currentGameVersion && versions.length > 0">
|
<p v-else-if="currentPlatform && currentGameVersion && versions.length > 0">
|
||||||
{{
|
{{
|
||||||
@@ -138,7 +157,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Labrinth } from '@modrinth/api-client'
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
import { DownloadIcon, TriangleAlertIcon } from '@modrinth/assets'
|
import { DownloadIcon, SpinnerIcon, TriangleAlertIcon } from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
type CdnDownloadReason,
|
type CdnDownloadReason,
|
||||||
@@ -147,6 +166,7 @@ import {
|
|||||||
type ComboboxOption,
|
type ComboboxOption,
|
||||||
defineMessages,
|
defineMessages,
|
||||||
getTagMessage,
|
getTagMessage,
|
||||||
|
injectNotificationManager,
|
||||||
truncatedTooltip,
|
truncatedTooltip,
|
||||||
useDebugLogger,
|
useDebugLogger,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
@@ -154,6 +174,7 @@ import {
|
|||||||
import VersionChannelTag from '@modrinth/ui/src/components/version/VersionChannelTag.vue'
|
import VersionChannelTag from '@modrinth/ui/src/components/version/VersionChannelTag.vue'
|
||||||
import type { DisplayProjectType } from '@modrinth/utils'
|
import type { DisplayProjectType } from '@modrinth/utils'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import JSZip from 'jszip'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -172,10 +193,16 @@ type ProjectDownloadSelection = {
|
|||||||
selectedPrimaryFile: Labrinth.Versions.v3.VersionFile | null
|
selectedPrimaryFile: Labrinth.Versions.v3.VersionFile | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DownloadableFile = {
|
||||||
|
href: string
|
||||||
|
filename: string
|
||||||
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
project: DownloadModalProject
|
project: DownloadModalProject
|
||||||
versions?: Labrinth.Versions.v3.Version[]
|
versions?: Labrinth.Versions.v3.Version[]
|
||||||
|
dependencyDownloadFiles?: DownloadableFile[]
|
||||||
downloadReason?: CdnDownloadReason
|
downloadReason?: CdnDownloadReason
|
||||||
initialGameVersion?: string | null
|
initialGameVersion?: string | null
|
||||||
initialPlatform?: string | null
|
initialPlatform?: string | null
|
||||||
@@ -185,6 +212,7 @@ const props = withDefaults(
|
|||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
versions: () => [],
|
versions: () => [],
|
||||||
|
dependencyDownloadFiles: () => [],
|
||||||
downloadReason: 'standalone',
|
downloadReason: 'standalone',
|
||||||
initialGameVersion: null,
|
initialGameVersion: null,
|
||||||
initialPlatform: null,
|
initialPlatform: null,
|
||||||
@@ -202,6 +230,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
||||||
|
const { addNotification } = injectNotificationManager()
|
||||||
const debug = useDebugLogger('DownloadProject')
|
const debug = useDebugLogger('DownloadProject')
|
||||||
const tags = useGeneratedState()
|
const tags = useGeneratedState()
|
||||||
|
|
||||||
@@ -211,6 +240,7 @@ const showAllVersions = ref(defaultShowAllVersions())
|
|||||||
const versionFilter = ref('')
|
const versionFilter = ref('')
|
||||||
const versionNumberRef = ref<HTMLElement | null>(null)
|
const versionNumberRef = ref<HTMLElement | null>(null)
|
||||||
const versionNameRef = ref<HTMLElement | null>(null)
|
const versionNameRef = ref<HTMLElement | null>(null)
|
||||||
|
const downloadingSelectedVersion = ref(false)
|
||||||
|
|
||||||
const incompatibleGameVersionsSet = computed(() => new Set(props.incompatibleGameVersions))
|
const incompatibleGameVersionsSet = computed(() => new Set(props.incompatibleGameVersions))
|
||||||
const incompatibleLoadersSet = computed(() => new Set(props.incompatibleLoaders))
|
const incompatibleLoadersSet = computed(() => new Set(props.incompatibleLoaders))
|
||||||
@@ -409,6 +439,36 @@ const selectedPrimaryFileDownloadUrl = computed(() => {
|
|||||||
return getDownloadUrl(selectedPrimaryFile.value.url)
|
return getDownloadUrl(selectedPrimaryFile.value.url)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedVersionDownloadFiles = computed(() => {
|
||||||
|
if (!selectedVersion.value) return []
|
||||||
|
|
||||||
|
return selectedVersion.value.files.map((file) => ({
|
||||||
|
href: getDownloadUrl(file.url),
|
||||||
|
filename: file.filename,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadAllFiles = computed(() => {
|
||||||
|
const files: DownloadableFile[] = []
|
||||||
|
const hrefs = new Set<string>()
|
||||||
|
|
||||||
|
for (const file of [...selectedVersionDownloadFiles.value, ...props.dependencyDownloadFiles]) {
|
||||||
|
if (hrefs.has(file.href)) continue
|
||||||
|
hrefs.add(file.href)
|
||||||
|
files.push(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedVersionZipFilename = computed(() => {
|
||||||
|
if (!selectedVersion.value) return `${sanitizeFilename(props.project.title)}.zip`
|
||||||
|
|
||||||
|
return `${sanitizeFilename(props.project.title)} ${sanitizeFilename(
|
||||||
|
selectedVersion.value.version_number,
|
||||||
|
)}.zip`
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[currentGameVersion, currentPlatform, selectedVersion, selectedPrimaryFile],
|
[currentGameVersion, currentPlatform, selectedVersion, selectedPrimaryFile],
|
||||||
() => {
|
() => {
|
||||||
@@ -453,6 +513,92 @@ function getDownloadUrl(url: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadSelectedVersionFiles() {
|
||||||
|
if (downloadingSelectedVersion.value || downloadAllFiles.value.length <= 1) return
|
||||||
|
|
||||||
|
downloadingSelectedVersion.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const zip = new JSZip()
|
||||||
|
const usedFilenames = new Set<string>()
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
downloadAllFiles.value.map(async (file) => {
|
||||||
|
const response = await fetch(file.href)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to download ${file.filename}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.file(uniqueFilename(file.filename, usedFilenames), await response.blob())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
downloadBlob(
|
||||||
|
await zip.generateAsync({
|
||||||
|
type: 'blob',
|
||||||
|
mimeType: 'application/zip',
|
||||||
|
}),
|
||||||
|
selectedVersionZipFilename.value,
|
||||||
|
)
|
||||||
|
emit('download')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download selected version files:', error)
|
||||||
|
addNotification({
|
||||||
|
title: formatMessage(messages.downloadSelectedVersionFailedTitle),
|
||||||
|
text: formatMessage(messages.downloadSelectedVersionFailedText),
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
downloadingSelectedVersion.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadBlob(blob: Blob, filename: string) {
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = url
|
||||||
|
link.download = filename
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
window.setTimeout(() => URL.revokeObjectURL(url), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeFilename(value: string) {
|
||||||
|
const sanitized = value
|
||||||
|
.replace(/[<>:"/\\|?*]/g, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
return sanitized || 'download'
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueFilename(filename: string, usedFilenames: Set<string>) {
|
||||||
|
const sanitizedFilename = sanitizeFilename(filename)
|
||||||
|
|
||||||
|
if (!usedFilenames.has(sanitizedFilename)) {
|
||||||
|
usedFilenames.add(sanitizedFilename)
|
||||||
|
return sanitizedFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionIndex = sanitizedFilename.lastIndexOf('.')
|
||||||
|
const basename =
|
||||||
|
extensionIndex > 0 ? sanitizedFilename.slice(0, extensionIndex) : sanitizedFilename
|
||||||
|
const extension = extensionIndex > 0 ? sanitizedFilename.slice(extensionIndex) : ''
|
||||||
|
let index = 2
|
||||||
|
let candidate = `${basename} (${index})${extension}`
|
||||||
|
|
||||||
|
while (usedFilenames.has(candidate)) {
|
||||||
|
index += 1
|
||||||
|
candidate = `${basename} (${index})${extension}`
|
||||||
|
}
|
||||||
|
|
||||||
|
usedFilenames.add(candidate)
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
function loaderLabel(loader: string) {
|
function loaderLabel(loader: string) {
|
||||||
return formatMessage(getTagMessage(loader, 'loader') ?? messages.unknownLoader)
|
return formatMessage(getTagMessage(loader, 'loader') ?? messages.unknownLoader)
|
||||||
}
|
}
|
||||||
@@ -545,6 +691,26 @@ const messages = defineMessages({
|
|||||||
id: 'project.download.download-version',
|
id: 'project.download.download-version',
|
||||||
defaultMessage: 'Download {version}',
|
defaultMessage: 'Download {version}',
|
||||||
},
|
},
|
||||||
|
compatibleVersionTitle: {
|
||||||
|
id: 'project.download.compatible-version-title',
|
||||||
|
defaultMessage: 'Compatible version',
|
||||||
|
},
|
||||||
|
downloadAllSelectedVersion: {
|
||||||
|
id: 'project.download.selected-version-download-all',
|
||||||
|
defaultMessage: 'Download all (.zip)',
|
||||||
|
},
|
||||||
|
downloadingSelectedVersion: {
|
||||||
|
id: 'project.download.selected-version-downloading',
|
||||||
|
defaultMessage: 'Downloading...',
|
||||||
|
},
|
||||||
|
downloadSelectedVersionFailedTitle: {
|
||||||
|
id: 'project.download.selected-version-failed-title',
|
||||||
|
defaultMessage: 'Could not download version',
|
||||||
|
},
|
||||||
|
downloadSelectedVersionFailedText: {
|
||||||
|
id: 'project.download.selected-version-failed-text',
|
||||||
|
defaultMessage: 'One or more version files could not be downloaded. Please try again.',
|
||||||
|
},
|
||||||
noGameVersionsFound: {
|
noGameVersionsFound: {
|
||||||
id: 'project.download.no-game-versions-found',
|
id: 'project.download.no-game-versions-found',
|
||||||
defaultMessage: 'No game versions found',
|
defaultMessage: 'No game versions found',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<DownloadProject
|
<DownloadProject
|
||||||
:project="project"
|
:project="project"
|
||||||
:versions="versions"
|
:versions="versions"
|
||||||
|
:dependency-download-files="dependencyDownloadFiles"
|
||||||
:download-reason="downloadReason"
|
:download-reason="downloadReason"
|
||||||
:initial-game-version="initialGameVersion"
|
:initial-game-version="initialGameVersion"
|
||||||
:initial-platform="initialPlatform"
|
:initial-platform="initialPlatform"
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
:current-platform="currentPlatform"
|
:current-platform="currentPlatform"
|
||||||
:download-reason="downloadReason"
|
:download-reason="downloadReason"
|
||||||
:additional-files="additionalFiles"
|
:additional-files="additionalFiles"
|
||||||
|
@update:downloadable-files="dependencyDownloadFiles = $event"
|
||||||
@download="onDownload"
|
@download="onDownload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,6 +92,11 @@ type ProjectDownloadSelection = {
|
|||||||
selectedPrimaryFile: Labrinth.Versions.v3.VersionFile | null
|
selectedPrimaryFile: Labrinth.Versions.v3.VersionFile | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DownloadableFile = {
|
||||||
|
href: string
|
||||||
|
filename: string
|
||||||
|
}
|
||||||
|
|
||||||
type NewModalRef = {
|
type NewModalRef = {
|
||||||
show: (event?: MouseEvent) => void
|
show: (event?: MouseEvent) => void
|
||||||
hide: () => void
|
hide: () => void
|
||||||
@@ -139,6 +146,7 @@ const showProjectId = ref<string | null>(null)
|
|||||||
const showOptions = ref<ResolvedProjectDownloadModalShowOptions>(getDefaultShowOptions())
|
const showOptions = ref<ResolvedProjectDownloadModalShowOptions>(getDefaultShowOptions())
|
||||||
const downloadProjectResetKey = ref(0)
|
const downloadProjectResetKey = ref(0)
|
||||||
const projectDownloadSelection = ref<ProjectDownloadSelection>(getDefaultProjectDownloadSelection())
|
const projectDownloadSelection = ref<ProjectDownloadSelection>(getDefaultProjectDownloadSelection())
|
||||||
|
const dependencyDownloadFiles = ref<DownloadableFile[]>([])
|
||||||
const MODAL_CLOSE_STATE_RESET_MS = 350
|
const MODAL_CLOSE_STATE_RESET_MS = 350
|
||||||
let closeStateResetTimeout: ReturnType<typeof setTimeout> | null = null
|
let closeStateResetTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
@@ -416,6 +424,7 @@ watch(modal, openFromHash)
|
|||||||
watch(() => route.hash, openFromHash)
|
watch(() => route.hash, openFromHash)
|
||||||
watch(routeProjectId, () => {
|
watch(routeProjectId, () => {
|
||||||
projectDownloadSelection.value = getDefaultProjectDownloadSelection()
|
projectDownloadSelection.value = getDefaultProjectDownloadSelection()
|
||||||
|
dependencyDownloadFiles.value = []
|
||||||
downloadProjectResetKey.value += 1
|
downloadProjectResetKey.value += 1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3242,17 +3242,8 @@
|
|||||||
"project.download.base-loader-incompatible-tooltip": {
|
"project.download.base-loader-incompatible-tooltip": {
|
||||||
"message": "This loader is incompatible with the base project."
|
"message": "This loader is incompatible with the base project."
|
||||||
},
|
},
|
||||||
"project.download.dependencies-download-all": {
|
"project.download.compatible-version-title": {
|
||||||
"message": "Download all"
|
"message": "Compatible version"
|
||||||
},
|
|
||||||
"project.download.dependencies-downloading": {
|
|
||||||
"message": "Downloading..."
|
|
||||||
},
|
|
||||||
"project.download.dependencies-failed-text": {
|
|
||||||
"message": "One or more dependency files could not be downloaded. Please try again."
|
|
||||||
},
|
|
||||||
"project.download.dependencies-failed-title": {
|
|
||||||
"message": "Could not download dependencies"
|
|
||||||
},
|
},
|
||||||
"project.download.dependencies-title": {
|
"project.download.dependencies-title": {
|
||||||
"message": "Dependencies"
|
"message": "Dependencies"
|
||||||
@@ -3281,6 +3272,9 @@
|
|||||||
"project.download.download-version": {
|
"project.download.download-version": {
|
||||||
"message": "Download {version}"
|
"message": "Download {version}"
|
||||||
},
|
},
|
||||||
|
"project.download.file-unavailable": {
|
||||||
|
"message": "This file cannot be downloaded"
|
||||||
|
},
|
||||||
"project.download.game-version-unsupported-tooltip": {
|
"project.download.game-version-unsupported-tooltip": {
|
||||||
"message": "{title} does not support {gameVersion} for {platform}"
|
"message": "{title} does not support {gameVersion} for {platform}"
|
||||||
},
|
},
|
||||||
@@ -3311,6 +3305,18 @@
|
|||||||
"project.download.select-platform": {
|
"project.download.select-platform": {
|
||||||
"message": "Select platform"
|
"message": "Select platform"
|
||||||
},
|
},
|
||||||
|
"project.download.selected-version-download-all": {
|
||||||
|
"message": "Download all (.zip)"
|
||||||
|
},
|
||||||
|
"project.download.selected-version-downloading": {
|
||||||
|
"message": "Downloading..."
|
||||||
|
},
|
||||||
|
"project.download.selected-version-failed-text": {
|
||||||
|
"message": "One or more version files could not be downloaded. Please try again."
|
||||||
|
},
|
||||||
|
"project.download.selected-version-failed-title": {
|
||||||
|
"message": "Could not download version"
|
||||||
|
},
|
||||||
"project.download.show-all-versions": {
|
"project.download.show-all-versions": {
|
||||||
"message": "Show all versions"
|
"message": "Show all versions"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user