diff --git a/.gitignore b/.gitignore index 064fbf0449..9d27dcaf5e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,14 @@ apps/frontend/src/generated .turbo target generated +!apps/app-frontend/src/generated/ +!apps/app-frontend/src/generated/app-events/ +!apps/app-frontend/src/generated/app-events/*.ts +!apps/app-frontend/src/generated/app-events/README.md +!apps/app-frontend/src/generated/app-events/postcard/ +!apps/app-frontend/src/generated/app-events/postcard/index.d.ts +!apps/app-frontend/src/generated/app-events/postcard/index.js +!apps/app-frontend/src/generated/app-events/postcard/package.json .env # app testing dir diff --git a/.idea/code.iml b/.idea/code.iml index c7d7651117..3861923c60 100644 --- a/.idea/code.iml +++ b/.idea/code.iml @@ -17,6 +17,17 @@ + + + + + + + + + + + diff --git a/Cargo.lock b/Cargo.lock index 03bd3d02ee..458c159e18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2170,7 +2170,7 @@ checksum = "ff6669899e23cb87b43daf7996f0ea3b9c07d0fb933d745bb7b815b052515ae3" dependencies = [ "proc-macro2", "quote", - "serde_derive_internals", + "serde_derive_internals 0.29.1", "syn 2.0.106", ] @@ -2394,6 +2394,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.16.2" @@ -4045,6 +4054,28 @@ dependencies = [ "x11", ] +[[package]] +name = "genco" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ab846431e5d637791b3279e7242fe2b21e11c3d8b4cf6a99f645c5f16ba7c0" +dependencies = [ + "genco-macros", + "relative-path", + "smallvec", +] + +[[package]] +name = "genco-macros" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42a1fe5a699c7f1d36ea6e04ed680a5c787cabff4b610ae3b8954ea3bcefec1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "generic-array" version = "0.14.9" @@ -5401,6 +5432,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "json5" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733a844dbd6fef128e98cb4487b887cb55454d92cd9994b1bafe004fabbe670c" +dependencies = [ + "serde", + "ucd-trie", +] + [[package]] name = "jsonptr" version = "0.6.3" @@ -7569,6 +7610,45 @@ dependencies = [ "serde", ] +[[package]] +name = "postcard-bindgen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e709af61573ae2563a00e760ff7db47f5c70b9d96c6dcb28993526fb8479f68" +dependencies = [ + "postcard-bindgen-core", + "postcard-bindgen-derive", +] + +[[package]] +name = "postcard-bindgen-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f19cebcb145fdccb24b63ad410785aa7c2ff9364551646ca7a927422d2d0386" +dependencies = [ + "convert_case 0.11.0", + "genco", + "tree-ds", +] + +[[package]] +name = "postcard-bindgen-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0372f55dd7c15d73f7b0f7ca507ac3db5653f346b2b44a36537e2ad39f55441b" +dependencies = [ + "convert_case 0.11.0", + "genco", + "postcard-bindgen-core", + "proc-macro2", + "quote", + "regex", + "regex-macro", + "serde", + "serde_derive_internals 0.30.0", + "syn 3.0.3", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -8451,12 +8531,27 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +[[package]] +name = "regex-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d306632607af6ec61c0b117971d57a96381b6317cf18ae419b5558048fe016e" +dependencies = [ + "regex", +] + [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "rend" version = "0.4.2" @@ -9122,7 +9217,7 @@ checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" dependencies = [ "proc-macro2", "quote", - "serde_derive_internals", + "serde_derive_internals 0.29.1", "syn 2.0.106", ] @@ -9365,6 +9460,16 @@ dependencies = [ "uuid 1.23.3", ] +[[package]] +name = "sequential_gen" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d630b1418311e028ceb45e1e567845501a9d9c770a258b80d2edc025ffad17c" +dependencies = [ + "lazy_static", + "uuid 1.23.3", +] + [[package]] name = "serde" version = "1.0.228" @@ -9470,6 +9575,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "serde_ini" version = "0.2.0" @@ -9910,6 +10026,9 @@ name = "spin" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] [[package]] name = "spinning_top" @@ -10337,6 +10456,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -10950,6 +11080,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "testcontainers" version = "0.25.2" @@ -11029,6 +11168,7 @@ dependencies = [ "image", "indicatif", "itertools 0.14.0", + "json5", "modrinth-content-management", "notify", "notify-debouncer-mini", @@ -11038,6 +11178,8 @@ dependencies = [ "path-util", "phf 0.13.1", "png 0.18.0", + "postcard", + "postcard-bindgen", "quartz_nbt", "quick-xml 0.38.3", "rand 0.8.5", @@ -11045,6 +11187,7 @@ dependencies = [ "reqwest 0.12.24", "rgb", "serde", + "serde-binhum", "serde_ini", "serde_json", "serde_with", @@ -11058,9 +11201,11 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-util", + "toml 0.9.8", "tracing", "tracing-error", "tracing-subscriber", + "ts-rs", "url", "urlencoding", "uuid 1.23.3", @@ -11737,12 +11882,49 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tree-ds" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710c388a2bb52d1a7186ae4aa2e92c6fae0606426bf4bdfb4106fa1e8e5c602e" +dependencies = [ + "lazy_static", + "sequential_gen", + "serde", + "spin 0.10.0", + "thiserror 2.0.17", +] + [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ts-rs" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8" +dependencies = [ + "chrono", + "thiserror 2.0.17", + "ts-rs-macros", + "uuid 1.23.3", +] + +[[package]] +name = "ts-rs-macros" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "termcolor", +] + [[package]] name = "tungstenite" version = "0.27.0" @@ -11786,6 +11968,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 74e9d0fbfe..ef7a85f36b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,6 +114,7 @@ indicatif = "0.18.0" itertools = "0.14.0" jemalloc_pprof = "0.8.1" json-patch = { version = "4.1.0", default-features = false } +json5 = "1.3.1" lettre = { version = "0.11.19", default-features = false, features = [ "aws-lc-rs", "builder", @@ -149,6 +150,7 @@ path-util = { path = "packages/path-util" } phf = { version = "0.13.1", features = ["macros"] } png = "0.18.0" postcard = { version = "1.1.3", default-features = false, features = ["alloc"] } +postcard-bindgen = "0.8.0" proc-macro2 = { version = "1.0" } prometheus = "0.14.0" quartz_nbt = "0.2.9" @@ -225,12 +227,14 @@ tikv-jemallocator = "0.6.0" tokio = "1.47.1" tokio-stream = "0.1.17" tokio-util = "0.7.16" +toml = "0.9.8" totp-rs = "5.7.0" tracing = "0.1.41" tracing-actix-web = { version = "0.7.19", default-features = false } tracing-ecs = "0.5.0" tracing-error = "0.2.1" tracing-subscriber = "0.3.20" +ts-rs = "12.0.1" typed-path = "0.12.0" url = "2.5.7" urlencoding = "2.1.3" diff --git a/_typos.toml b/_typos.toml index 74efbceb9b..7775afa964 100644 --- a/_typos.toml +++ b/_typos.toml @@ -24,3 +24,7 @@ gam = "gam" consts = "consts" # short for "Copy" Cpy = "Cpy" + +[default.extend-identifiers] +# Constant from the `zip` crate +ZIP64_BYTES_THR = "ZIP64_BYTES_THR" diff --git a/apps/app-frontend/.prettierignore b/apps/app-frontend/.prettierignore index 7ea3c138a4..f27dbb49e0 100644 --- a/apps/app-frontend/.prettierignore +++ b/apps/app-frontend/.prettierignore @@ -2,3 +2,7 @@ *.gltf src/locales/ src/assets/**/*.svg + +# Generated app-event bindings +src/generated/app-events/*.ts +src/generated/app-events/postcard/** diff --git a/apps/app-frontend/eslint.config.mjs b/apps/app-frontend/eslint.config.mjs index 3d6da7fb54..b8555099f1 100644 --- a/apps/app-frontend/eslint.config.mjs +++ b/apps/app-frontend/eslint.config.mjs @@ -1,2 +1,7 @@ import config from '@modrinth/tooling-config/eslint/nuxt.mjs' -export default config + +export default config.append([ + { + ignores: ['src/generated/app-events/*.ts', 'src/generated/app-events/postcard/**'], + }, +]) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index c905b2cd07..3db155776b 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -71,6 +71,7 @@ import AppActionBar from '@/components/ui/AppActionBar.vue' import Breadcrumbs from '@/components/ui/Breadcrumbs.vue' import ErrorModal from '@/components/ui/ErrorModal.vue' import FriendsList from '@/components/ui/friends/FriendsList.vue' +import HostingUpdateRequired from '@/components/ui/HostingUpdateRequired.vue' import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue' import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue' import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue' @@ -89,19 +90,19 @@ import SplashScreen from '@/components/ui/SplashScreen.vue' import SurveyPopup from '@/components/ui/SurveyPopup.vue' import WindowControls from '@/components/ui/WindowControls.vue' import { useCheckDisableMouseover } from '@/composables/macCssFix.js' +import { useAppEvent } from '@/composables/use-app-event' import { config } from '@/config' import { - ads_consent_listener, hide_ads_window, init_ads_window, perform_ads_consent_action, + release_ads_window_hold, should_show_ads_consent_popup, - show_ads_window, + take_ads_window_hold, } from '@/helpers/ads.js' import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics' import { check_reachable } from '@/helpers/auth.js' import { get_user, get_version } from '@/helpers/cache.js' -import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js' import { install_create_modpack_instance, install_get_modpack_preview } from '@/helpers/install' import { can_current_user_use_shared_instances, get as getInstance, run } from '@/helpers/instance' import { get as getCreds, login, logout } from '@/helpers/mr_auth.ts' @@ -140,6 +141,7 @@ import { } from '@/providers/download-progress.ts' import { createServerInstall, provideServerInstall } from '@/providers/server-install' import { setupProviders } from '@/providers/setup' +import { setupAppEventsProvider } from '@/providers/setup/app-events' import { setupAuthProvider } from '@/providers/setup/auth' import { setupLoadingStateProvider } from '@/providers/setup/loading-state' import { useError } from '@/store/error.js' @@ -155,6 +157,7 @@ import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal' const themeStore = useTheming() const router = useRouter() const route = useRoute() +const { channel: appEventChannel, events: appEvents } = setupAppEventsProvider() const breadcrumbManager = createBreadcrumbManager() provideBreadcrumbManager(breadcrumbManager) const canNavigateBack = ref(false) @@ -166,6 +169,25 @@ function updateHistoryNavigationState() { canNavigateForward.value = historyState?.forward != null } +let fullscreenAdsWindowHold = false + +async function handleFullscreenChange() { + const fullscreen = document.fullscreenElement !== null + if (fullscreen === fullscreenAdsWindowHold) return + + fullscreenAdsWindowHold = fullscreen + try { + if (fullscreen) { + await take_ads_window_hold() + } else { + await release_ads_window_hold() + } + } catch (error) { + fullscreenAdsWindowHold = !fullscreen + handleError(error) + } +} + updateHistoryNavigationState() const APP_LEFT_NAV_WIDTH = '4rem' @@ -182,10 +204,19 @@ watch( }, ) const forceSidebar = computed( - () => route.path.startsWith('/browse') || route.path.startsWith('/project'), + () => + route.path.startsWith('/browse') || + route.path.startsWith('/project') || + route.path.startsWith('/user'), ) const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value) const hostingRouteActive = computed(() => route.path.startsWith('/hosting')) +const hostingUpdateRequired = computed( + () => + hostingRouteActive.value && + !!appUpdateState.availableUpdate.value && + appUpdateState.updatesEnabled.value, +) const prideFundraiserEnabled = computed( () => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE, ) @@ -196,7 +227,9 @@ const hostingIntercomIdentityKey = computed(() => { return `${userId}:${serverId ?? 'hosting'}` }) const hostingIntercom = useHostingIntercom({ - enabled: computed(() => hostingRouteActive.value && !!credentials.value?.session), + enabled: computed( + () => hostingRouteActive.value && !hostingUpdateRequired.value && !!credentials.value?.session, + ), appId: 'ykeritl9', fetchToken: fetchIntercomToken, identityKey: hostingIntercomIdentityKey, @@ -211,11 +244,22 @@ const notificationManager = new AppNotificationManager() provideNotificationManager(notificationManager) const { handleError, addNotification } = notificationManager +useAppEvent( + 'warning', + (event) => + addNotification({ + title: 'Warning', + text: event.message, + type: 'warning', + }), + appEvents, +) + const popupNotificationManager = new AppPopupNotificationManager() providePopupNotificationManager(popupNotificationManager) const { addPopupNotification } = popupNotificationManager let adsConsentPopupId = null -let unlistenAdsConsent +useAppEvent('ads_consent_required', handleAdsConsentRequired, appEvents) const appVersion = getVersion() const tauriApiClient = new TauriModrinthClient({ @@ -284,8 +328,8 @@ providePageContext({ }) provideModalBehavior({ noblur: computed(() => !themeStore.advancedRendering), - onShow: () => hide_ads_window(), - onHide: () => show_ads_window(), + onShow: () => take_ads_window_hold(), + onHide: () => release_ads_window_hold(), }) const { @@ -300,7 +344,7 @@ const { setModpackAlreadyInstalledModal, handleModpackDuplicateCreateAnyway, handleModpackDuplicateGoToInstance, -} = setupProviders(notificationManager, popupNotificationManager) +} = setupProviders(tauriApiClient, notificationManager, popupNotificationManager) const news = ref([]) const displayedServerInviteNotifications = new Set() @@ -352,7 +396,6 @@ const authUnreachable = computed(() => { onMounted(async () => { await useCheckDisableMouseover() try { - unlistenAdsConsent = await ads_consent_listener(handleAdsConsentRequired) handleAdsConsentRequired(await should_show_ads_consent_popup()) } catch (error) { handleError(error) @@ -360,6 +403,7 @@ onMounted(async () => { document.querySelector('body').addEventListener('click', handleClick) document.querySelector('body').addEventListener('auxclick', handleAuxClick) + document.addEventListener('fullscreenchange', handleFullscreenChange) checkUpdates() }) @@ -367,9 +411,13 @@ onMounted(async () => { onUnmounted(async () => { document.querySelector('body').removeEventListener('click', handleClick) document.querySelector('body').removeEventListener('auxclick', handleAuxClick) + document.removeEventListener('fullscreenchange', handleFullscreenChange) clearDelayedUpdatePopup() - await unlistenAdsConsent?.() + if (fullscreenAdsWindowHold) { + fullscreenAdsWindowHold = false + await release_ads_window_hold().catch(handleError) + } await unlistenUpdateDownload?.() }) @@ -578,14 +626,6 @@ async function setupApp() { document.getElementsByTagName('html')[0].classList.add('windows') } - await warning_listener((e) => - addNotification({ - title: 'Warning', - text: e.message, - type: 'warning', - }), - ) - fetch(`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`) .then((response) => response.json()) .then((res) => { @@ -634,7 +674,7 @@ async function setupApp() { } const stateFailed = ref(false) -initialize_state() +initialize_state(appEventChannel) .then(() => { setupApp().catch((err) => { stateFailed.value = true @@ -764,7 +804,7 @@ const errorModal = ref() const minecraftAuthErrorModal = ref() const minecraftRequiredModal = ref() -const contentInstall = createContentInstall({ router, handleError }) +const contentInstall = createContentInstall({ router, handleError, appEvents }) provideContentInstall(contentInstall) const { instances: contentInstallInstances, @@ -797,7 +837,12 @@ const { handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel, } = contentInstall -const serverInstall = createServerInstall({ router, handleError, popupNotificationManager }) +const serverInstall = createServerInstall({ + router, + handleError, + popupNotificationManager, + appEvents, +}) provideServerInstall(serverInstall) const { setInstallToPlayModal: setServerInstallToPlayModal, @@ -973,8 +1018,8 @@ onMounted(() => { const accounts = ref(null) provide('accountsCard', accounts) -command_listener(handleCommand) -notification_listener(handleLiveNotification) +useAppEvent('command', handleCommand, appEvents) +useAppEvent('notification', handleLiveNotification, appEvents) async function markLiveNotificationRead(notification) { try { @@ -1400,6 +1445,7 @@ async function downloadUpdate(versionToDownload) { handleError(e) }) unlistenUpdateDownload = await subscribeToDownloadProgress( + appEvents, appUpdateDownload, versionToDownload.version, ) @@ -1758,7 +1804,8 @@ provideAppUpdateDownloadProgress(appUpdateDownload) > {{ formatMessage(messages.authUnreachableBody) }} - + +