fix: add download attribute to fix JAR files saving as ZIP in Chromium (#6065)

* fix: add download attribute to fix JAR files saving as ZIP in Chromium

- JAR files were downloading with a `.zip` extension in Chromium-based browsers (Chrome, Edge, Arc, Brave, Opera, Vivaldi)
- Root cause: JAR files are ZIP archives internally, so Chromium sniffs the `Content-Type` as `application/zip` and overrides the filename extension when no `download` attribute is present
- Fix: add `download="<filename>"` to all file download `<a>` tags so the browser uses the original filename from the API

* fix: add download attribute to remaining download links

Missed in initial pass: changelog page button, versions overflow
menu, settings/versions overflow menu. Also adds `download` prop
to Button and OverflowMenu to support dropdown link items.

Adds missing `getPrimaryFile` definition in changelog.vue.

---------

Co-authored-by: Mr_chank <180248271+chank-op@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Mr_chank
2026-05-16 00:58:26 +10:00
committed by GitHub
parent e9eb98f97e
commit 02a7774722
7 changed files with 26 additions and 3 deletions

View File

@@ -57,6 +57,7 @@
<a <a
class="ml-auto" class="ml-auto"
:href="createDownloadUrl(version)" :href="createDownloadUrl(version)"
:download="getPrimaryFile(version).filename"
:title="`Download ${version.name}`" :title="`Download ${version.name}`"
> >
<DownloadIcon aria-hidden="true" /> <DownloadIcon aria-hidden="true" />

View File

@@ -111,6 +111,7 @@
color: 'primary', color: 'primary',
hoverFilled: true, hoverFilled: true,
link: createDownloadUrl(version), link: createDownloadUrl(version),
download: getPrimaryFile(version).filename,
action: () => { action: () => {
emit('onDownload') emit('onDownload')
}, },

View File

@@ -140,6 +140,7 @@
<a <a
v-tooltip="primaryFile.filename + ' (' + formatBytes(primaryFile.size) + ')'" v-tooltip="primaryFile.filename + ' (' + formatBytes(primaryFile.size) + ')'"
:href="decoratedPrimaryFileUrl" :href="decoratedPrimaryFileUrl"
:download="primaryFile.filename"
@click="emit('onDownload')" @click="emit('onDownload')"
> >
<DownloadIcon aria-hidden="true" /> <DownloadIcon aria-hidden="true" />
@@ -307,6 +308,7 @@
:href="decorateDownloadUrl(file.url)" :href="decorateDownloadUrl(file.url)"
class="raised-button" class="raised-button"
:title="`Download ${file.filename}`" :title="`Download ${file.filename}`"
:download="file.filename"
tabindex="0" tabindex="0"
> >
<DownloadIcon aria-hidden="true" /> <DownloadIcon aria-hidden="true" />

View File

@@ -47,6 +47,7 @@
<a <a
v-tooltip="`Download`" v-tooltip="`Download`"
:href="createDownloadUrl(version)" :href="createDownloadUrl(version)"
:download="getPrimaryFile(version).filename"
class="hover:!bg-button-bg [&>svg]:!text-green" class="hover:!bg-button-bg [&>svg]:!text-green"
aria-label="Download" aria-label="Download"
@click="emit('onDownload')" @click="emit('onDownload')"
@@ -101,6 +102,7 @@
color: 'primary', color: 'primary',
hoverFilled: true, hoverFilled: true,
link: createDownloadUrl(version), link: createDownloadUrl(version),
download: getPrimaryFile(version).filename,
action: () => { action: () => {
emit('onDownload') emit('onDownload')
}, },

View File

@@ -11,6 +11,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
download: {
type: String,
default: null,
},
action: { action: {
type: Function, type: Function,
default: null, default: null,
@@ -106,6 +110,7 @@ const classes = computed(() => {
class="btn" class="btn"
:class="classes" :class="classes"
:href="disabled ? undefined : link" :href="disabled ? undefined : link"
:download="download || undefined"
:target="external ? '_blank' : '_self'" :target="external ? '_blank' : '_self'"
@click=" @click="
(event) => { (event) => {

View File

@@ -36,6 +36,7 @@
: undefined : undefined
" "
:link="option.link ? option.link : undefined" :link="option.link ? option.link : undefined"
:download="option.download ? option.download : undefined"
:external="option.external ? option.external : false" :external="option.external ? option.external : false"
:disabled="option.disabled" :disabled="option.disabled"
@click=" @click="
@@ -76,6 +77,7 @@ interface Item extends BaseOption {
icon?: Component icon?: Component
action?: (event?: MouseEvent) => void action?: (event?: MouseEvent) => void
link?: string link?: string
download?: string
external?: boolean external?: boolean
color?: color?:
| 'primary' | 'primary'

View File

@@ -12,7 +12,12 @@
</p> </p>
</div> </div>
<ButtonStyled color="brand"> <ButtonStyled color="brand">
<a :href="downloadUrl" class="min-w-0" @click="emit('onDownload')"> <a
:href="downloadUrl"
:download="primaryFilename"
class="min-w-0"
@click="emit('onDownload')"
>
<DownloadIcon aria-hidden="true" /> Download <DownloadIcon aria-hidden="true" /> Download
</a> </a>
</ButtonStyled> </ButtonStyled>
@@ -42,12 +47,17 @@ const props = defineProps<{
decorateDownloadUrl?: (url: string) => string decorateDownloadUrl?: (url: string) => string
}>() }>()
const primaryFile = computed<VersionFile>(
() => props.version.files.find((x) => x.primary) || props.version.files[0],
)
const downloadUrl = computed(() => { const downloadUrl = computed(() => {
const primary: VersionFile = props.version.files.find((x) => x.primary) || props.version.files[0] const raw = primaryFile.value.url
const raw = primary.url
return props.decorateDownloadUrl ? props.decorateDownloadUrl(raw) : raw return props.decorateDownloadUrl ? props.decorateDownloadUrl(raw) : raw
}) })
const primaryFilename = computed(() => primaryFile.value.filename)
const emit = defineEmits<{ const emit = defineEmits<{
onDownload: [] onDownload: []
onNavigate: [url: string] onNavigate: [url: string]