diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 1e497d9248..8acdc32340 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -36,14 +36,16 @@ import { NewsArticleCard, NotificationPanel, OverflowMenu, + PopupNotificationPanel, ProgressSpinner, provideModrinthClient, provideNotificationManager, providePageContext, + providePopupNotificationManager, useDebugLogger, useVIntl, } from '@modrinth/ui' -import { renderString } from '@modrinth/utils' +import { formatBytes, renderString } from '@modrinth/utils' import { useQuery } from '@tanstack/vue-query' import { getVersion } from '@tauri-apps/api/app' import { invoke } from '@tauri-apps/api/core' @@ -67,13 +69,13 @@ import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue' import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue' import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue' import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue' +import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue' +import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue' import NavButton from '@/components/ui/NavButton.vue' import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' import RunningAppBar from '@/components/ui/RunningAppBar.vue' import SplashScreen from '@/components/ui/SplashScreen.vue' -import UpdateAvailableToast from '@/components/ui/UpdateAvailableToast.vue' -import UpdateToast from '@/components/ui/UpdateToast.vue' import URLConfirmModal from '@/components/ui/URLConfirmModal.vue' import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js' @@ -107,6 +109,7 @@ import { create_profile_and_install_from_file } from './helpers/pack' import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer' import { get_available_capes, get_available_skins } from './helpers/skins' import { AppNotificationManager } from './providers/app-notifications' +import { AppPopupNotificationManager } from './providers/app-popup-notifications' const themeStore = useTheming() @@ -114,6 +117,10 @@ const notificationManager = new AppNotificationManager() provideNotificationManager(notificationManager) const { handleError, addNotification } = notificationManager +const popupNotificationManager = new AppPopupNotificationManager() +providePopupNotificationManager(popupNotificationManager) +const { addPopupNotification } = popupNotificationManager + const tauriApiClient = new TauriModrinthClient({ userAgent: `modrinth/theseus/${getVersion()} (support@modrinth.com)`, features: [ @@ -394,6 +401,8 @@ const install = useInstall() const modInstallModal = ref() const installConfirmModal = ref() const incompatibilityWarningModal = ref() +const installToPlayModal = ref() +const updateToPlayModal = ref() const credentials = ref() @@ -471,6 +480,9 @@ onMounted(() => { install.setIncompatibilityWarningModal(incompatibilityWarningModal) install.setInstallConfirmModal(installConfirmModal) install.setModInstallModal(modInstallModal) + install.setInstallToPlayModal(installToPlayModal) + install.setUpdateToPlayModal(updateToPlayModal) + install.setPopupNotificationManager(popupNotificationManager) }) const accounts = ref(null) @@ -506,14 +518,60 @@ const downloadPercent = computed(() => Math.trunc(appUpdateDownload.progress.val const metered = ref(true) const finishedDownloading = ref(false) const restarting = ref(false) -const updateToastDismissed = ref(false) const availableUpdate = ref(null) const updateSize = ref(null) const updatesEnabled = ref(true) + +const updatePopupMessages = defineMessages({ + updateAvailable: { + id: 'app.update-popup.title', + defaultMessage: 'Update available', + }, + downloadComplete: { + id: 'app.update-popup.download-complete', + defaultMessage: 'Download complete', + }, + body: { + id: 'app.update-popup.body', + defaultMessage: + 'Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App.', + }, + meteredBody: { + id: 'app.update-popup.body.metered', + defaultMessage: `Modrinth App v{version} is available now! Since you're on a metered network, we didn't automatically download it.`, + }, + downloadedBody: { + id: 'app.update-popup.body.download-complete', + defaultMessage: `Modrinth App v{version} has finished downloading. Reload to update now, or automatically when you close Modrinth App.`, + }, + linuxBody: { + id: 'app.update-popup.body.linux', + defaultMessage: + 'Modrinth App v{version} is available. Use your package manager to update for the latest features and fixes!', + }, + reload: { + id: 'app.update-popup.reload', + defaultMessage: 'Reload', + }, + download: { + id: 'app.update-popup.download', + defaultMessage: 'Download ({size})', + }, + changelog: { + id: 'app.update-popup.changelog', + defaultMessage: 'Changelog', + }, +}) + async function checkUpdates() { if (!(await areUpdatesEnabled())) { console.log('Skipping update check as updates are disabled in this build or environment') updatesEnabled.value = false + + if (os.value === 'Linux' && !isDevEnvironment.value) { + checkLinuxUpdates() + setInterval(checkLinuxUpdates, 5 * 60 * 1000) + } return } @@ -533,7 +591,6 @@ async function checkUpdates() { appUpdateDownload.progress.value = 0 finishedDownloading.value = false - updateToastDismissed.value = false console.log(`Update ${update.version} is available.`) @@ -543,6 +600,28 @@ async function checkUpdates() { downloadUpdate(update) } else { console.log(`Metered connection detected, not auto-downloading update.`) + getUpdateSize(update.rid).then((size) => { + updateSize.value = size + addPopupNotification({ + title: formatMessage(updatePopupMessages.updateAvailable), + text: formatMessage(updatePopupMessages.meteredBody, { version: update.version }), + type: 'info', + autoCloseMs: null, + buttons: [ + { + label: formatMessage(updatePopupMessages.download, { + size: formatBytes(updateSize.value ?? 0), + }), + action: () => downloadAvailableUpdate(), + color: 'brand', + }, + { + label: formatMessage(updatePopupMessages.changelog), + action: () => openUrl('https://modrinth.com/news/changelog?filter=app'), + }, + ], + }) + }) } getUpdateSize(update.rid).then((size) => (updateSize.value = size)) @@ -559,8 +638,26 @@ async function checkUpdates() { ) } -async function showUpdateToast() { - updateToastDismissed.value = false +async function checkLinuxUpdates() { + try { + const [response, currentVersion] = await Promise.all([ + fetch('https://launcher-files.modrinth.com/updates.json'), + getVersion(), + ]) + const updates = await response.json() + const latestVersion = updates?.version + + if (latestVersion && latestVersion !== currentVersion) { + addPopupNotification({ + title: formatMessage(updatePopupMessages.updateAvailable), + text: formatMessage(updatePopupMessages.linuxBody, { version: latestVersion }), + type: 'info', + autoCloseMs: null, + }) + } + } catch (e) { + console.error('Failed to check for updates:', e) + } } async function downloadAvailableUpdate() { @@ -586,6 +683,26 @@ async function downloadUpdate(versionToDownload) { unlistenUpdateDownload = null }) console.log('Finished downloading!') + + addPopupNotification({ + title: formatMessage(updatePopupMessages.downloadComplete), + text: formatMessage(updatePopupMessages.downloadedBody, { + version: versionToDownload.version, + }), + type: 'success', + autoCloseMs: null, + buttons: [ + { + label: formatMessage(updatePopupMessages.reload), + action: () => installUpdate(), + color: 'brand', + }, + { + label: formatMessage(updatePopupMessages.changelog), + action: () => openUrl('https://modrinth.com/news/changelog?filter=app'), + }, + ], + }) }) unlistenUpdateDownload = await subscribeToDownloadProgress( appUpdateDownload, @@ -759,25 +876,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload) class="app-grid-layout experimental-styles-within relative" :class="{ 'disable-advanced-rendering': !themeStore.advancedRendering }" > - - - - - -
-
+
- + diff --git a/apps/app-frontend/src/components/ui/RunningAppBar.vue b/apps/app-frontend/src/components/ui/RunningAppBar.vue index 53f050362c..d95f1fb298 100644 --- a/apps/app-frontend/src/components/ui/RunningAppBar.vue +++ b/apps/app-frontend/src/components/ui/RunningAppBar.vue @@ -52,10 +52,12 @@

{{ loadingBar.title }}

- -
- {{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}% - {{ loadingBar.message }} +
+ +
+ {{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}% + {{ loadingBar.message }} +
@@ -102,7 +104,12 @@ import { TerminalSquareIcon, UnplugIcon, } from '@modrinth/assets' -import { Button, ButtonStyled, Card, injectNotificationManager } from '@modrinth/ui' +import { + Button, + ButtonStyled, + Card, + injectNotificationManager, +} from '@modrinth/ui' import { onBeforeUnmount, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' @@ -176,7 +183,7 @@ const goToTerminal = (path) => { const currentLoadingBars = ref([]) const refreshInfo = async () => { - const currentLoadingBarCount = currentLoadingBars.value.length + const previousBars = [...currentLoadingBars.value] currentLoadingBars.value = Object.values(await progress_bars_list().catch(handleError)) .map((x) => { if (x.bar_type.type === 'java_download') { @@ -205,7 +212,7 @@ const refreshInfo = async () => { if (currentLoadingBars.value.length === 0) { showCard.value = false - } else if (currentLoadingBarCount < currentLoadingBars.value.length) { + } else if (previousBars.length < currentLoadingBars.value.length) { showCard.value = true } } @@ -346,7 +353,7 @@ onBeforeUnmount(() => { .info-card { position: absolute; top: 3.5rem; - right: 0.5rem; + right: 100%; z-index: 9; width: 20rem; background-color: var(--color-raised-bg); @@ -420,7 +427,7 @@ onBeforeUnmount(() => { display: flex; flex-direction: column; align-items: flex-start; - gap: 0.5rem; + gap: 0.75rem; margin: 0; padding: 0; } diff --git a/apps/app-frontend/src/components/ui/SearchCard.vue b/apps/app-frontend/src/components/ui/SearchCard.vue index f544cd52e9..fbf31b9cdb 100644 --- a/apps/app-frontend/src/components/ui/SearchCard.vue +++ b/apps/app-frontend/src/components/ui/SearchCard.vue @@ -5,7 +5,7 @@ () => { emit('open') $router.push({ - path: `/project/${project.project_id ?? project.id}`, + path: `/project/ipxQs0xE`, query: { i: props.instance ? props.instance.path : undefined }, }) } diff --git a/apps/app-frontend/src/components/ui/UpdateAvailableToast.vue b/apps/app-frontend/src/components/ui/UpdateAvailableToast.vue deleted file mode 100644 index ccae3ef5a1..0000000000 --- a/apps/app-frontend/src/components/ui/UpdateAvailableToast.vue +++ /dev/null @@ -1,84 +0,0 @@ - - diff --git a/apps/app-frontend/src/components/ui/UpdateToast.vue b/apps/app-frontend/src/components/ui/UpdateToast.vue deleted file mode 100644 index 6a7f104c6b..0000000000 --- a/apps/app-frontend/src/components/ui/UpdateToast.vue +++ /dev/null @@ -1,130 +0,0 @@ - - diff --git a/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue b/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue index 54c9521cb2..b92fb5f23d 100644 --- a/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue +++ b/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue @@ -1,7 +1,7 @@