Reworked app update flow (#3960)

* Make theseus capable of logging messages from the `log` crate

* Move update checking entirely into JS and open a modal if an update is available

* Fix formatjs on Windows and run formatjs

* Add in the buttons and body

* Fix lint

* Show update size in modal

* Fix update not being rechecked if the update modal was directly dismissed

* Slight UI tweaks

* Fix lint

* Implement skipping the update

* Implement the Update Now button

* Implement updating at next exit

* Turn download progress into an error bar on failure

* Restore 5 minute update check instead of 30 seconds

* Fix PendingUpdateData being seen as a unit struct

* Fix lint

* Make CI also lint updater code

* feat: create AppearingProgressBar component

* feat: polish update available modal

* feat: add error handling

* Open changelog with tauri-plugin-opener

* Run intl:extract

* Update completion toasts (#3978)

* Use single LAUNCHER_USER_AGENT constant for all user agents

* Fix build on Mac

* Request the update size with HEAD instead of GET

* UI tweaks

* lint

* Fix lint

* fix: hide modal header & add "Hide update reminder" button w/ tooltip

* Run intl:extract

* fix: lint issues

* fix: merge issues

* notifications.js no longer exists

* Add metered network checking

* Add a timeout to macOS is_network_metered

* Fix tauri.conf.json

* vibe debugging

* Set a dispatch queue

* Have a popup that asks you if you'd like to disable automatic file downloads if you're on a metered network

* Move UpdateModal to modal package

* Fix lint

* Add a toggle for automatic downloads

* Fix type

Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com>
Signed-off-by: Josiah Glosson <soujournme@gmail.com>

* Redo updating UI and experience

* lint

* fix unlistener issue

* remove unneeded translation keys

* Fix expose issue

* temp disable cranelift, tweak some messages

* change version back

* Clean up App.vue

* move toast to top right

* update reload icon

* Fixed the bug!!!!!!!!!!!!

* improve messages

* intl:extract

* Add liquid glass icon file

* not you!

* use dependency injection

* lint on apple icon

* Fix imports, move download size to button

* change update check back to 5 mins

* lint + move to providers

* intl:extract

---------

Signed-off-by: Cal H. <hendersoncal117@gmail.com>
Signed-off-by: Josiah Glosson <soujournme@gmail.com>
Co-authored-by: Calum <calum@modrinth.com>
Co-authored-by: Prospector <prospectordev@gmail.com>
Co-authored-by: Cal H. <hendersoncal117@gmail.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com>
This commit is contained in:
Josiah Glosson
2025-09-29 09:28:31 -06:00
committed by GitHub
parent f6f66a313f
commit a538b99c18
49 changed files with 1487 additions and 284 deletions

View File

@@ -6,6 +6,7 @@
:class="{
'router-link-active': isPrimary && isPrimary(route),
'subpage-active': isSubpage && isSubpage(route),
disabled: disabled,
}"
class="w-12 h-12 text-primary rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
>
@@ -15,6 +16,7 @@
v-else
v-bind="$attrs"
class="button-animation border-none text-primary cursor-pointer w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
:disabled="disabled"
@click="to"
>
<slot />
@@ -29,12 +31,18 @@ const route = useRoute()
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
defineProps<{
to: (() => void) | string
isPrimary?: RouteFunction
isSubpage?: RouteFunction
highlightOverride?: boolean
}>()
withDefaults(
defineProps<{
to: (() => void) | string
isPrimary?: RouteFunction
isSubpage?: RouteFunction
highlightOverride?: boolean
disabled?: boolean
}>(),
{
disabled: false,
},
)
defineOptions({
inheritAttrs: false,

View File

@@ -1,6 +1,12 @@
<template>
<div class="progress-bar">
<div class="progress-bar__fill" :style="{ width: `${progress}%` }"></div>
<div
class="progress-bar__fill"
:style="{
width: `${progress}%`,
'background-color': error ? 'var(--color-red)' : 'var(--color-brand)',
}"
></div>
</div>
</template>
@@ -13,6 +19,10 @@ defineProps({
return value >= 0 && value <= 100
},
},
error: {
type: Boolean,
default: false,
},
})
</script>
@@ -27,7 +37,6 @@ defineProps({
.progress-bar__fill {
height: 100%;
background-color: var(--color-brand);
transition: width 0.3s;
}
</style>

View File

@@ -177,8 +177,8 @@ const currentLoadingBars = ref([])
const refreshInfo = async () => {
const currentLoadingBarCount = currentLoadingBars.value.length
currentLoadingBars.value = Object.values(await progress_bars_list().catch(handleError)).map(
(x) => {
currentLoadingBars.value = Object.values(await progress_bars_list().catch(handleError))
.map((x) => {
if (x.bar_type.type === 'java_download') {
x.title = 'Downloading Java ' + x.bar_type.version
}
@@ -190,8 +190,8 @@ const refreshInfo = async () => {
}
return x
},
)
})
.filter((bar) => bar?.bar_type?.type !== 'launcher_update')
currentLoadingBars.value.sort((a, b) => {
if (a.loading_bar_uuid < b.loading_bar_uuid) {

View File

@@ -114,7 +114,7 @@ watch(loading, (newValue) => {
setTimeout(() => {
hidden.value = true
loading.setEnabled(true)
}, 250)
}, 50)
}
}
})
@@ -135,9 +135,6 @@ loading_listener(async (e) => {
if (e.event.type === 'directory_move') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Updating app directory...'
} else if (e.event.type === 'launcher_update') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Updating Modrinth App...'
} else if (e.event.type === 'checking_for_updates') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Checking for updates...'

View File

@@ -0,0 +1,131 @@
<script setup lang="ts">
import { DownloadIcon, ExternalIcon, RefreshCwIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, commonMessages, ProgressBar } from '@modrinth/ui'
import { formatBytes } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { ref } from 'vue'
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
const { formatMessage } = useVIntl()
const emit = defineEmits<{
(e: 'close' | 'restart' | 'download'): void
}>()
defineProps<{
version: string
size: number | null
metered: boolean
}>()
const downloading = ref(false)
const { progress } = injectAppUpdateDownloadProgress()
function download() {
emit('download')
downloading.value = true
}
const messages = defineMessages({
title: {
id: 'app.update-toast.title',
defaultMessage: 'Update available',
},
body: {
id: 'app.update-toast.body',
defaultMessage:
'Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App.',
},
reload: {
id: 'app.update-toast.reload',
defaultMessage: 'Reload',
},
download: {
id: 'app.update-toast.download',
defaultMessage: 'Download ({size})',
},
downloading: {
id: 'app.update-toast.downloading',
defaultMessage: 'Downloading...',
},
changelog: {
id: 'app.update-toast.changelog',
defaultMessage: 'Changelog',
},
meteredBody: {
id: 'app.update-toast.body.metered',
defaultMessage: `Modrinth App v{version} is available now! Since you're on a metered network, we didn't automatically download it.`,
},
downloadCompleteTitle: {
id: 'app.update-toast.title.download-complete',
defaultMessage: 'Download complete',
},
downloadedBody: {
id: 'app.update-toast.body.download-complete',
defaultMessage: `Modrinth App v{version} has finished downloading. Reload to update now, or automatically when you close Modrinth App.`,
},
})
</script>
<template>
<div
class="fixed card-shadow rounded-2xl top-[--top-bar-height] mt-6 right-6 p-4 z-10 w-[25rem] bg-bg-raised border-divider border-solid border-[2px]"
:class="{
'download-complete': progress === 1,
}"
>
<div class="flex">
<h2 class="text-base text-contrast font-semibold m-0 grow">
{{
formatMessage(metered && progress === 1 ? messages.downloadCompleteTitle : messages.title)
}}
</h2>
<ButtonStyled size="small" circular>
<button v-tooltip="formatMessage(commonMessages.closeButton)" @click="emit('close')">
<XIcon />
</button>
</ButtonStyled>
</div>
<p class="text-sm mt-2 mb-0">
{{
formatMessage(
metered
? progress === 1
? messages.downloadedBody
: messages.meteredBody
: messages.body,
{ version },
)
}}
</p>
<p
v-if="metered && progress < 1"
class="text-sm text-secondary mt-2 mb-0 flex items-center gap-1"
>
<template v-if="progress > 0">
<ProgressBar :progress="progress" class="max-w-[unset]" />
</template>
</p>
<div class="flex gap-2 mt-4">
<ButtonStyled color="brand">
<button v-if="metered && progress < 1" :disabled="downloading" @click="download">
<SpinnerIcon v-if="downloading" class="animate-spin" />
<DownloadIcon v-else />
{{
formatMessage(downloading ? messages.downloading : messages.download, {
size: formatBytes(size ?? 0),
})
}}
</button>
<button v-else @click="emit('restart')">
<RefreshCwIcon /> {{ formatMessage(messages.reload) }}
</button>
</ButtonStyled>
<ButtonStyled>
<a href="https://modrinth.com/news/changelog?filter=app">
{{ formatMessage(messages.changelog) }} <ExternalIcon />
</a>
</ButtonStyled>
</div>
</div>
</template>

View File

@@ -9,10 +9,10 @@ import {
SettingsIcon,
ShieldIcon,
} from '@modrinth/assets'
import { TabbedModal } from '@modrinth/ui'
import { ProgressBar, TabbedModal } from '@modrinth/ui'
import { getVersion } from '@tauri-apps/api/app'
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
import { defineMessage, useVIntl } from '@vintl/vintl'
import { defineMessage, defineMessages, useVIntl } from '@vintl/vintl'
import { computed, ref, watch } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
@@ -23,6 +23,7 @@ import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
import { get, set } from '@/helpers/settings.ts'
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
import { useTheming } from '@/store/state'
const themeStore = useTheming()
@@ -98,6 +99,8 @@ const isOpen = computed(() => modal.value?.isOpen)
defineExpose({ show, isOpen })
const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress()
const version = await getVersion()
const osPlatform = getOsPlatform()
const osVersion = getOsVersion()
@@ -123,6 +126,13 @@ function devModeCount() {
}
}
}
const messages = defineMessages({
downloading: {
id: 'app.settings.downloading',
defaultMessage: 'Downloading v{version}',
},
})
</script>
<template>
<ModalWrapper ref="modal">
@@ -135,6 +145,14 @@ function devModeCount() {
<TabbedModal :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
<template #footer>
<div class="mt-auto text-secondary text-sm">
<div class="mb-3">
<template v-if="progress > 0 && progress < 1">
<p class="m-0 mb-2">
{{ formatMessage(messages.downloading, { downloadingVersion }) }}
</p>
<ProgressBar :progress="progress" />
</template>
</div>
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
{{ formatMessage(developerModeEnabled) }}
</p>
@@ -152,7 +170,7 @@ function devModeCount() {
<div>
<p class="m-0">Modrinth App {{ version }}</p>
<p class="m-0">
<span v-if="osPlatform === 'macos'">MacOS</span>
<span v-if="osPlatform === 'macos'">macOS</span>
<span v-else class="capitalize">{{ osPlatform }}</span>
{{ osVersion }}
</p>

View File

@@ -12,6 +12,10 @@ const props = defineProps({
type: String,
default: null,
},
hideHeader: {
type: Boolean,
default: false,
},
closable: {
type: Boolean,
default: true,
@@ -49,7 +53,14 @@ function onModalHide() {
</script>
<template>
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
<Modal
ref="modal"
:header="header"
:noblur="!themeStore.advancedRendering"
:closable="closable"
:hide-header="hideHeader"
@hide="onModalHide"
>
<template #title>
<slot name="title" />
</template>