diff --git a/.gitignore b/.gitignore index 145849e1c0..9497d6bc52 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/Cargo.lock b/Cargo.lock index b3eefef689..81769584d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2116,7 +2116,7 @@ checksum = "ff6669899e23cb87b43daf7996f0ea3b9c07d0fb933d745bb7b815b052515ae3" dependencies = [ "proc-macro2", "quote", - "serde_derive_internals", + "serde_derive_internals 0.29.1", "syn 2.0.106", ] @@ -2349,6 +2349,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" @@ -4000,6 +4009,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" @@ -5515,6 +5546,7 @@ dependencies = [ "scalar_api_reference", "sentry", "serde", + "serde-binhum", "serde_json", "serde_with", "sha1 0.10.6", @@ -7573,6 +7605,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" @@ -8455,12 +8526,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" @@ -9126,7 +9212,7 @@ checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" dependencies = [ "proc-macro2", "quote", - "serde_derive_internals", + "serde_derive_internals 0.29.1", "syn 2.0.106", ] @@ -9369,6 +9455,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" @@ -9379,6 +9475,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-binhum" +version = "0.1.0" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "serde-untagged" version = "0.1.9" @@ -9464,6 +9570,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" @@ -9904,6 +10021,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" @@ -10331,6 +10451,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" @@ -10944,6 +11075,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" @@ -11032,6 +11172,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", @@ -11039,6 +11181,7 @@ dependencies = [ "reqwest 0.12.24", "rgb", "serde", + "serde-binhum", "serde_ini", "serde_json", "serde_with", @@ -11055,6 +11198,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "ts-rs", "url", "urlencoding", "uuid 1.23.3", @@ -11731,12 +11875,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" @@ -13443,12 +13624,13 @@ dependencies = [ "chrono", "dashmap", "deadpool-redis", + "eyre", "futures", "lz4_flex", + "postcard", "prometheus", "redis", "serde", - "serde_json", "thiserror 2.0.17", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 1529ce8910..55453b4dba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "packages/modrinth-util", "packages/neverbounce", "packages/path-util", + "packages/serde-binhum", "packages/xredis", ] @@ -148,6 +149,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" @@ -183,6 +185,7 @@ sentry = { version = "0.45.0", default-features = false, features = [ "rustls", ] } serde = "1.0.228" +serde-binhum = { path = "packages/serde-binhum" } serde_bytes = "0.11.19" serde_cbor = "0.11.2" serde_ini = "0.2.0" @@ -229,6 +232,7 @@ 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/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 e2f78fe965..852394b9ef 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -88,9 +88,9 @@ 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, @@ -100,7 +100,6 @@ import { 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' @@ -138,6 +137,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' @@ -153,6 +153,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) @@ -206,11 +207,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({ @@ -347,7 +359,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) @@ -365,7 +376,6 @@ onUnmounted(async () => { unsubscribeSidebarToggle() clearDelayedUpdatePopup() - await unlistenAdsConsent?.() await unlistenUpdateDownload?.() }) @@ -574,14 +584,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) => { @@ -630,7 +632,7 @@ async function setupApp() { } const stateFailed = ref(false) -initialize_state() +initialize_state(appEventChannel) .then(() => { setupApp().catch((err) => { stateFailed.value = true @@ -760,7 +762,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, @@ -793,7 +795,12 @@ const { handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel, } = contentInstall -const serverInstall = createServerInstall({ router, handleError, popupNotificationManager }) +const serverInstall = createServerInstall({ + router, + handleError, + popupNotificationManager, + appEvents, +}) provideServerInstall(serverInstall) const { setInstallToPlayModal: setServerInstallToPlayModal, @@ -969,8 +976,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 { @@ -1396,6 +1403,7 @@ async function downloadUpdate(versionToDownload) { handleError(e) }) unlistenUpdateDownload = await subscribeToDownloadProgress( + appEvents, appUpdateDownload, versionToDownload.version, ) diff --git a/apps/app-frontend/src/components/ui/AccountsCard.vue b/apps/app-frontend/src/components/ui/AccountsCard.vue index 9c0611ef63..94437be7d7 100644 --- a/apps/app-frontend/src/components/ui/AccountsCard.vue +++ b/apps/app-frontend/src/components/ui/AccountsCard.vue @@ -101,8 +101,9 @@ import { useVIntl, } from '@modrinth/ui' import type { Ref } from 'vue' -import { computed, onUnmounted, ref } from 'vue' +import { computed, ref } from 'vue' +import { useAppEvent } from '@/composables/use-app-event' import { trackEvent } from '@/helpers/analytics' import { get_default_user, @@ -111,7 +112,6 @@ import { set_default_user, users, } from '@/helpers/auth' -import { process_listener } from '@/helpers/events' import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts' import type { Skin } from '@/helpers/skins' import { get_available_skins } from '@/helpers/skins' @@ -248,16 +248,12 @@ async function logout(id: string) { trackEvent('AccountLogOut') } -const unlisten = await process_listener(async (e) => { +useAppEvent('process', async (e) => { if (e.event === 'launched') { await refreshValues() } }) -onUnmounted(() => { - unlisten() -}) - const messages = defineMessages({ notSignedIn: { id: 'minecraft-account.not-signed-in', diff --git a/apps/app-frontend/src/components/ui/AppActionBar.vue b/apps/app-frontend/src/components/ui/AppActionBar.vue index 380a78a5c5..77bc01f3af 100644 --- a/apps/app-frontend/src/components/ui/AppActionBar.vue +++ b/apps/app-frontend/src/components/ui/AppActionBar.vue @@ -141,8 +141,8 @@ import { useRouter } from 'vue-router' import AppUpdateButton from '@/components/ui/app-update-button/index.vue' import { useInstallJobNotifications } from '@/composables/browse/install-job-notifications' +import { useAppEvent } from '@/composables/use-app-event' import { trackEvent } from '@/helpers/analytics' -import { loading_listener, process_listener } from '@/helpers/events' import { get_many as getInstances } from '@/helpers/instance' import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process' import type { LoadingBar } from '@/helpers/state' @@ -260,7 +260,7 @@ onMounted(() => { window.addEventListener('online', handleOnline) }) -const unlistenProcess = await process_listener(async () => { +useAppEvent('process', async () => { await refresh() }) @@ -482,7 +482,7 @@ const installJobNotifications = await useInstallJobNotifications({ await refreshLoadingBars() -const unlistenLoading = await loading_listener(async () => { +useAppEvent('loading', async () => { await refreshLoadingBars() }) @@ -499,8 +499,6 @@ onBeforeUnmount(() => { dismissed.value = false window.removeEventListener('offline', handleOffline) window.removeEventListener('online', handleOnline) - unlistenProcess() - unlistenLoading() installJobNotifications.dispose() }) diff --git a/apps/app-frontend/src/components/ui/Instance.vue b/apps/app-frontend/src/components/ui/Instance.vue index 6845a00f2b..3d7d55bb95 100644 --- a/apps/app-frontend/src/components/ui/Instance.vue +++ b/apps/app-frontend/src/components/ui/Instance.vue @@ -10,11 +10,11 @@ import { import { Avatar, ButtonStyled, injectNotificationManager, useRelativeTime } from '@modrinth/ui' import { convertFileSrc } from '@tauri-apps/api/core' import dayjs from 'dayjs' -import { computed, onMounted, onUnmounted, ref } from 'vue' +import { computed, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' +import { useAppEvent } from '@/composables/use-app-event' import { trackEvent } from '@/helpers/analytics' -import { process_listener } from '@/helpers/events' import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install' import { kill, run } from '@/helpers/instance' import { get_by_instance_id } from '@/helpers/process' @@ -136,7 +136,7 @@ defineExpose({ const currentEvent = ref(null) -const unlisten = await process_listener((e) => { +useAppEvent('process', (e) => { if (e.instance_id === props.instance.id) { currentEvent.value = e.event if (e.event === 'finished') { @@ -148,7 +148,6 @@ const unlisten = await process_listener((e) => { onMounted(() => { checkProcess() }) -onUnmounted(() => unlisten())