Skip to content

Reworked app update flow #3960

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
607c42c
Make theseus capable of logging messages from the `log` crate
Gaming32 Jul 2, 2025
e59dd08
Move update checking entirely into JS and open a modal if an update i…
Gaming32 Jul 3, 2025
36cb3f1
Fix formatjs on Windows and run formatjs
Gaming32 Jul 3, 2025
35aea3c
Add in the buttons and body
Gaming32 Jul 3, 2025
523800e
Fix lint
Gaming32 Jul 3, 2025
52d6bf3
Show update size in modal
Gaming32 Jul 7, 2025
9aab6c3
Fix update not being rechecked if the update modal was directly dismi…
Gaming32 Jul 7, 2025
3d4d0af
Slight UI tweaks
Gaming32 Jul 8, 2025
9a43d49
Fix lint
Gaming32 Jul 8, 2025
ae75292
Implement skipping the update
Gaming32 Jul 9, 2025
7b73aa2
Implement the Update Now button
Gaming32 Jul 9, 2025
9b103e0
Implement updating at next exit
Gaming32 Jul 10, 2025
5495b01
Turn download progress into an error bar on failure
Gaming32 Jul 10, 2025
8922c7a
Restore 5 minute update check instead of 30 seconds
Gaming32 Jul 10, 2025
3b74e02
Fix PendingUpdateData being seen as a unit struct
Gaming32 Jul 10, 2025
f9a4042
Fix lint
Gaming32 Jul 10, 2025
286ab6d
Make CI also lint updater code
Gaming32 Jul 10, 2025
59ab09a
feat: create AppearingProgressBar component
IMB11 Jul 10, 2025
35baa1a
feat: polish update available modal
IMB11 Jul 10, 2025
69a461d
feat: add error handling
IMB11 Jul 11, 2025
62e2e5e
Open changelog with tauri-plugin-opener
Gaming32 Jul 11, 2025
6fa0ee4
Run intl:extract
Gaming32 Jul 11, 2025
1e93431
Update completion toasts (#3978)
Gaming32 Jul 12, 2025
80e0f84
Use single LAUNCHER_USER_AGENT constant for all user agents
Gaming32 Jul 16, 2025
7cc39cb
Fix build on Mac
Gaming32 Jul 16, 2025
221c26d
Merge branch 'main' into app-updater-rework
Gaming32 Jul 16, 2025
83bd4dd
Request the update size with HEAD instead of GET
Gaming32 Jul 16, 2025
2c90f1c
UI tweaks
Prospector Jul 16, 2025
30e93e0
lint
Prospector Jul 16, 2025
071e2b5
Fix lint
Gaming32 Jul 17, 2025
0310cc5
fix: hide modal header & add "Hide update reminder" button w/ tooltip
IMB11 Jul 17, 2025
a1e0c13
Merge branch 'main' into app-updater-rework
Gaming32 Jul 21, 2025
3b6a9dd
Run intl:extract
Gaming32 Jul 21, 2025
2774cdc
Merge branch 'main' into app-updater-rework
Gaming32 Jul 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/app-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"tsc:check": "vue-tsc --noEmit",
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
"test": "vue-tsc --noEmit"
},
"dependencies": {
Expand Down
157 changes: 145 additions & 12 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
<script setup>
import { computed, onMounted, onUnmounted, ref, watch, provide } from 'vue'
import {
computed,
onMounted,
onUnmounted,
ref,
watch,
useTemplateRef,
provide,
nextTick,
} from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
import {
ArrowBigUpDashIcon,
Expand Down Expand Up @@ -33,7 +42,7 @@ import { useLoading, useTheming } from '@/store/state'
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
import AccountsCard from '@/components/ui/AccountsCard.vue'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { get } from '@/helpers/settings.ts'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
Expand All @@ -42,7 +51,7 @@ import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
import { handleError, useNotifications } from '@/store/notifications.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { type } from '@tauri-apps/plugin-os'
import { getOS, isDev, restartApp } from '@/helpers/utils.js'
import { areUpdatesEnabled, getOS, isDev } from '@/helpers/utils.js'
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { getVersion } from '@tauri-apps/api/app'
Expand All @@ -59,7 +68,6 @@ import { get_opening_command, initialize_state } from '@/helpers/state'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
import { renderString } from '@modrinth/utils'
import { useFetch } from '@/helpers/fetch.js'
import { check } from '@tauri-apps/plugin-updater'
import NavButton from '@/components/ui/NavButton.vue'
import { get as getCreds, login, logout } from '@/helpers/mr_auth.js'
import { get_user } from '@/helpers/cache.js'
Expand All @@ -69,8 +77,11 @@ import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import { openUrl } from '@tauri-apps/plugin-opener'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import UpdateModal from '@/components/ui/UpdateModal.vue'
import { get_available_capes, get_available_skins } from './helpers/skins'
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { createTooltip, destroyTooltip } from 'floating-vue'

const themeStore = useTheming()

Expand Down Expand Up @@ -109,6 +120,18 @@ onUnmounted(() => {
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
})

const { formatMessage } = useVIntl()
const messages = defineMessages({
updateInstalledToastTitle: {
id: 'app.update.complete-toast.title',
defaultMessage: 'Version {version} was successfully installed!',
},
updateInstalledToastText: {
id: 'app.update.complete-toast.text',
defaultMessage: 'Click here to view the changelog.',
},
})

async function setupApp() {
stateInitialized.value = true
const {
Expand All @@ -122,7 +145,8 @@ async function setupApp() {
toggle_sidebar,
developer_mode,
feature_flags,
} = await get()
pending_update_toast_for_version,
} = await getSettings()

if (default_page === 'Library') {
await router.push('/library')
Expand Down Expand Up @@ -209,7 +233,6 @@ async function setupApp() {
})

get_opening_command().then(handleCommand)
checkUpdates()
fetchCredentials()

try {
Expand All @@ -219,6 +242,22 @@ async function setupApp() {
} catch (error) {
console.warn('Failed to generate skin previews in app setup.', error)
}

if (pending_update_toast_for_version !== null) {
const settings = await getSettings()
settings.pending_update_toast_for_version = null
await setSettings(settings)

const version = await getVersion()
if (pending_update_toast_for_version === version) {
notifications.addNotification({
type: 'success',
title: formatMessage(messages.updateInstalledToastTitle, { version }),
text: formatMessage(messages.updateInstalledToastText),
clickAction: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
})
}
}
}

const stateFailed = ref(false)
Expand Down Expand Up @@ -346,19 +385,95 @@ async function handleCommand(e) {
}
}

const updateAvailable = ref(false)
const availableUpdate = ref(null)
const updateSkipped = ref(false)
const enqueuedUpdate = ref(null)
const updateModal = useTemplateRef('updateModal')
async function checkUpdates() {
const update = await check()
updateAvailable.value = !!update
if (!(await areUpdatesEnabled())) {
console.log('Skipping update check as updates are disabled in this build')
return
}

async function performCheck() {
if (updateModal.value.isOpen) {
console.log('Skipping update check because the update modal is already open')
return
}

const update = await invoke('plugin:updater|check')
if (!update) {
return
}

console.log(`Update ${update.version} is available.`)

if (update.version === availableUpdate.value?.version) {
console.log(
'Skipping update modal because the new version is the same as the dismissed update',
)
return
}

availableUpdate.value = update

const settings = await getSettings()
if (settings.skipped_update === update.version) {
updateSkipped.value = true
console.log('Skipping update modal because the user chose to skip this update')
return
}

updateSkipped.value = false
updateModal.value.show(update)
}

await performCheck()
setTimeout(
() => {
checkUpdates()
},
5 * 1000 * 60,
5 * 60 * 1000,
)
}

async function skipUpdate(version) {
enqueuedUpdate.value = null

updateSkipped.value = true
const settings = await getSettings()
settings.skipped_update = version
await setSettings(settings)
}

async function updateEnqueuedForLater(version) {
enqueuedUpdate.value = version
}

async function forceOpenUpdateModal() {
if (updateSkipped.value) {
updateSkipped.value = false
const settings = await getSettings()
settings.skipped_update = null
await setSettings(settings)
}
updateModal.value.show(availableUpdate.value)
}

const updateButton = useTemplateRef('updateButton')
async function showUpdateButtonTooltip() {
await nextTick()
const tooltip = createTooltip(updateButton.value.$el, {
placement: 'right',
content: 'Click here to view the update again.',
})
tooltip.show()
setTimeout(() => {
tooltip.hide()
destroyTooltip(updateButton.value.$el)
}, 3500)
}

function handleClick(e) {
let target = e.target
while (target != null) {
Expand Down Expand Up @@ -399,6 +514,14 @@ function handleAuxClick(e) {
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
<div id="teleports"></div>
<div v-if="stateInitialized" class="app-grid-layout experimental-styles-within relative">
<Suspense @resolve="checkUpdates">
<UpdateModal
ref="updateModal"
@update-skipped="skipUpdate"
@update-enqueued-for-later="updateEnqueuedForLater"
@modal-hidden="showUpdateButtonTooltip"
/>
</Suspense>
<Suspense>
<AppSettingsModal ref="settingsModal" />
</Suspense>
Expand Down Expand Up @@ -449,8 +572,18 @@ function handleAuxClick(e) {
<PlusIcon />
</NavButton>
<div class="flex flex-grow"></div>
<NavButton v-if="updateAvailable" v-tooltip.right="'Install update'" :to="() => restartApp()">
<DownloadIcon />
<NavButton
v-if="!!availableUpdate"
ref="updateButton"
v-tooltip.right="
enqueuedUpdate === availableUpdate?.version
? 'Update installation queued for next restart'
: 'Update available'
"
:to="forceOpenUpdateModal"
>
<DownloadIcon v-if="updateSkipped || enqueuedUpdate === availableUpdate?.version" />
<DownloadIcon v-else class="text-brand-green" />
</NavButton>
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
<SettingsIcon />
Expand Down
13 changes: 11 additions & 2 deletions apps/app-frontend/src/components/ui/ProgressBar.vue
Original file line number Diff line number Diff line change
@@ -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>

Expand All @@ -13,6 +19,10 @@ defineProps({
return value >= 0 && value <= 100
},
},
error: {
type: Boolean,
default: false,
},
})
</script>

Expand All @@ -27,7 +37,6 @@ defineProps({

.progress-bar__fill {
height: 100%;
background-color: var(--color-brand);
transition: width 0.3s;
}
</style>
Loading