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/Cargo.lock b/Cargo.lock index 6a53ac4b9b..25ffa1f07c 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", ] @@ -2340,6 +2340,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" @@ -3991,6 +4000,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" @@ -7499,6 +7530,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" @@ -8381,12 +8451,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" @@ -9052,7 +9137,7 @@ checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" dependencies = [ "proc-macro2", "quote", - "serde_derive_internals", + "serde_derive_internals 0.29.1", "syn 2.0.106", ] @@ -9295,6 +9380,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" @@ -9400,6 +9495,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" @@ -9840,6 +9946,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" @@ -10267,6 +10376,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" @@ -10880,6 +11000,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" @@ -10968,6 +11097,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", @@ -10975,6 +11106,7 @@ dependencies = [ "reqwest 0.12.24", "rgb", "serde", + "serde-binhum", "serde_ini", "serde_json", "serde_with", @@ -10991,6 +11123,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "ts-rs", "url", "urlencoding", "uuid 1.23.3", @@ -11667,12 +11800,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" diff --git a/Cargo.toml b/Cargo.toml index 6670a240af..d9fadf9551 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,6 +148,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" @@ -230,6 +231,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 17f2a62fb9..e846dcbdb9 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -89,9 +89,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, @@ -102,7 +102,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' @@ -141,6 +140,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' @@ -156,6 +156,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) @@ -234,11 +235,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({ @@ -375,7 +387,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) @@ -398,7 +409,6 @@ onUnmounted(async () => { fullscreenAdsWindowHold = false await release_ads_window_hold().catch(handleError) } - await unlistenAdsConsent?.() await unlistenUpdateDownload?.() }) @@ -607,14 +617,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) => { @@ -663,7 +665,7 @@ async function setupApp() { } const stateFailed = ref(false) -initialize_state() +initialize_state(appEventChannel) .then(() => { setupApp().catch((err) => { stateFailed.value = true @@ -793,7 +795,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, @@ -826,7 +828,12 @@ const { handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel, } = contentInstall -const serverInstall = createServerInstall({ router, handleError, popupNotificationManager }) +const serverInstall = createServerInstall({ + router, + handleError, + popupNotificationManager, + appEvents, +}) provideServerInstall(serverInstall) const { setInstallToPlayModal: setServerInstallToPlayModal, @@ -1002,8 +1009,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 { @@ -1429,6 +1436,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 766f234026..f1cb5fa479 100644 --- a/apps/app-frontend/src/components/ui/AccountsCard.vue +++ b/apps/app-frontend/src/components/ui/AccountsCard.vue @@ -104,8 +104,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, @@ -114,7 +115,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' @@ -251,16 +251,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 6790167f3d..9eca5548f4 100644 --- a/apps/app-frontend/src/components/ui/AppActionBar.vue +++ b/apps/app-frontend/src/components/ui/AppActionBar.vue @@ -148,8 +148,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' @@ -294,7 +294,7 @@ onMounted(() => { window.addEventListener('online', handleOnline) }) -const unlistenProcess = await process_listener(async () => { +useAppEvent('process', async () => { await refresh() }) @@ -554,7 +554,7 @@ const installJobNotifications = await useInstallJobNotifications({ await refreshLoadingBars() -const unlistenLoading = await loading_listener(async () => { +useAppEvent('loading', async () => { await refreshLoadingBars() }) @@ -573,8 +573,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 7c82504eb1..7540fb5865 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, IconButton, 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())