diff --git a/.vscode/settings.json b/.vscode/settings.json index 7522198819..af3d483835 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,7 +9,7 @@ "files.insertFinalNewline": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", - "source.organizeImports": "always" + "source.organizeImports": "never" }, "editor.defaultFormatter": "esbenp.prettier-vscode", "[vue]": { diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 4e31d5a82c..df22421aa6 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -11,7 +11,6 @@ import { import { ArrowBigUpDashIcon, ChangeSkinIcon, - CheckIcon, CompassIcon, DownloadIcon, ExternalIcon, @@ -41,7 +40,6 @@ import { defineMessages, I18nDebugPanel, LoadingBar, - ModrinthHostingLogo, NewsArticleCard, NotificationPanel, OverflowMenu, @@ -86,7 +84,6 @@ import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue' import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue' import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue' import NavButton from '@/components/ui/NavButton.vue' -import ServerInvitePopupBody from '@/components/ui/notifications/ServerInvitePopupBody.vue' import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue' import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' @@ -804,6 +801,11 @@ async function declineServerInviteNotification(notification) { } } +function openServerInviteInviterProfile(inviterName) { + if (!inviterName) return + openUrl(`${config.siteUrl}/user/${encodeURIComponent(inviterName)}`) +} + async function handleLiveNotification(notification) { if (notification?.body?.type !== 'server_invite' || notification.read) return if (displayedServerInviteNotifications.has(notification.id)) return @@ -817,30 +819,17 @@ async function handleLiveNotification(notification) { typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null addPopupNotification({ - title: 'Modrinth Hosting', - titleLogo: ModrinthHostingLogo, - bodyComponent: ServerInvitePopupBody, - bodyProps: { - inviterName: invitedBy?.username ?? null, - inviterAvatarUrl: invitedBy?.avatar_url ?? null, - serverName, - }, - type: 'info', - buttons: [ - { - label: 'Accept', - action: () => acceptServerInviteNotification(notification), - icon: CheckIcon, - color: 'brand', - }, - { - label: 'Decline', - action: () => declineServerInviteNotification(notification), - icon: XIcon, - color: 'red', - }, - ], + title: serverName, autoCloseMs: null, + toast: { + type: 'server-invite', + actorName: invitedBy?.username ?? null, + actorAvatarUrl: invitedBy?.avatar_url ?? null, + entityName: serverName, + onAccept: () => acceptServerInviteNotification(notification), + onDecline: () => declineServerInviteNotification(notification), + onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null), + }, }) } diff --git a/apps/app-frontend/src/components/ui/AppActionBar.vue b/apps/app-frontend/src/components/ui/AppActionBar.vue index cdcd3d0b1f..9b18e85951 100644 --- a/apps/app-frontend/src/components/ui/AppActionBar.vue +++ b/apps/app-frontend/src/components/ui/AppActionBar.vue @@ -133,6 +133,7 @@ import { type PopupNotificationProgressItem, useVIntl, } from '@modrinth/ui' +import { convertFileSrc } from '@tauri-apps/api/core' import { Dropdown } from 'floating-vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' @@ -284,6 +285,7 @@ function goToTerminal(path?: string) { } const currentLoadingBars = ref([]) +const currentLoadingBarIconUrls = ref>({}) const notificationId = ref(null) const dismissed = ref(false) @@ -303,6 +305,16 @@ function getLoadingText(loadingBar: LoadingBar): string { return loadingBar.message ? `${percent}% ${loadingBar.message}` : `${percent}%` } +function getDisplayIconUrl(icon: string | null | undefined): string | null { + if (!icon) { + return null + } + if (/^(https?:|data:|blob:|asset:|tauri:)/.test(icon)) { + return icon + } + return convertFileSrc(icon) +} + function getNotification(): PopupNotification | null { if (!notificationId.value) { return null @@ -326,6 +338,7 @@ function buildDownloadItems(): PopupNotificationProgressItem[] { id: getLoadingBarKey(bar), title: bar.title ?? '', text: getLoadingText(bar), + iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null, progress: getLoadingProgress(bar), waiting: !bar.total || bar.total <= 0, })) @@ -400,6 +413,32 @@ async function refreshLoadingBars() { .map(formatLoadingBars) .filter((bar) => bar?.bar_type?.type !== 'launcher_update') + const profilePaths = Array.from( + new Set( + currentLoadingBars.value + .map((bar) => bar.bar_type?.profile_path) + .filter((path): path is string => !!path), + ), + ) + const profiles = profilePaths.length + ? await getInstances(profilePaths).catch((error) => { + handleError(error) + return [] + }) + : [] + const profileIconUrls = new Map( + profiles.map((profile) => [profile.path, getDisplayIconUrl(profile.icon_path)]), + ) + currentLoadingBarIconUrls.value = Object.fromEntries( + currentLoadingBars.value.map((bar) => { + const barIconUrl = getDisplayIconUrl(bar.bar_type?.icon) + const profileIconUrl = bar.bar_type?.profile_path + ? profileIconUrls.get(bar.bar_type.profile_path) + : null + return [getLoadingBarKey(bar), barIconUrl ?? profileIconUrl ?? null] + }), + ) + currentLoadingBars.value.sort((a, b) => { const aKey = `${a.loading_bar_uuid ?? a.id ?? ''}` const bKey = `${b.loading_bar_uuid ?? b.id ?? ''}` diff --git a/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue index 4b1e5af378..a9edf12ef3 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue +++ b/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue @@ -8,6 +8,7 @@ import { InstallationSettingsLayout, provideAppBackup, provideInstallationSettings, + useDebugLogger, useVIntl, } from '@modrinth/ui' import type { GameVersionTag, PlatformTag } from '@modrinth/utils' @@ -34,9 +35,17 @@ import type { Manifest } from '../../../helpers/types' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() const queryClient = useQueryClient() +const debug = useDebugLogger('AppInstallationSettings') const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings() +debug('metadata load: start', { + instancePath: instance.value.path, + loader: instance.value.loader, + gameVersion: instance.value.game_version, + installStage: instance.value.install_stage, +}) + const [ fabric_versions, forge_versions, @@ -72,6 +81,15 @@ const [ .catch(handleError), ]) +debug('metadata load: done', { + hasFabricManifest: !!fabric_versions?.value, + hasForgeManifest: !!forge_versions?.value, + hasQuiltManifest: !!quilt_versions?.value, + hasNeoforgeManifest: !!neoforge_versions?.value, + gameVersions: all_game_versions?.value?.length ?? 0, + availablePlatforms: loaders?.value?.map((loader) => loader.name) ?? [], +}) + const { data: modpackInfo } = useQuery({ queryKey: computed(() => ['linkedModpackInfo', instance.value.path]), queryFn: () => get_linked_modpack_info(instance.value.path, 'must_revalidate'), @@ -95,11 +113,21 @@ function getManifest(loader: string) { quilt: quilt_versions, neoforge: neoforge_versions, } - return map[loader] + const manifest = map[loader] + debug('getManifest:', { + loader, + hasManifest: !!manifest?.value, + gameVersions: manifest?.value?.gameVersions?.length ?? 0, + }) + return manifest } provideAppBackup({ async createBackup() { + debug('createBackup: start', { + instancePath: instance.value.path, + instanceName: instance.value.name, + }) const allProfiles = await list() const prefix = `${instance.value.name} - Backup #` const existingNums = allProfiles @@ -109,6 +137,7 @@ provideAppBackup({ const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1 const newPath = await duplicate(instance.value.path) await edit(newPath, { name: `${prefix}${nextNum}` }) + debug('createBackup: done', { newPath, backupName: `${prefix}${nextNum}` }) }, }) @@ -165,32 +194,72 @@ provideInstallationSettings({ const manifest = getManifest(loader) return !!manifest?.value?.gameVersions?.some((x) => item.version === x.id) }) - return (showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')).map( - (x) => ({ value: x.version, label: x.version }), - ) + const result = ( + showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release') + ).map((x) => ({ value: x.version, label: x.version })) + debug('resolveGameVersions:', { + loader, + showSnapshots, + totalVersions: versions.length, + filteredVersions: filtered.length, + resultVersions: result.length, + }) + return result }, resolveLoaderVersions(loader, gameVersion) { - if (loader === 'vanilla' || !gameVersion) return [] + if (loader === 'vanilla' || !gameVersion) { + debug('resolveLoaderVersions: skipped', { loader, gameVersion }) + return [] + } const manifest = getManifest(loader) - if (!manifest?.value) return [] + if (!manifest?.value) { + debug('resolveLoaderVersions: no manifest', { loader, gameVersion }) + return [] + } if (loader === 'fabric' || loader === 'quilt') { - return manifest.value.gameVersions[0]?.loaders ?? [] + const result = manifest.value.gameVersions[0]?.loaders ?? [] + debug('resolveLoaderVersions: fabric/quilt result', { + loader, + gameVersion, + count: result.length, + }) + return result } - return manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? [] + const result = + manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? [] + debug('resolveLoaderVersions: result', { loader, gameVersion, count: result.length }) + return result }, resolveHasSnapshots(loader) { const versions = all_game_versions?.value ?? [] - if (loader === 'vanilla') return versions.some((x) => x.version_type !== 'release') + if (loader === 'vanilla') { + const result = versions.some((x) => x.version_type !== 'release') + debug('resolveHasSnapshots: vanilla', { loader, result }) + return result + } const manifest = getManifest(loader) const supported = versions.filter( (item) => !!manifest?.value?.gameVersions?.some((x) => item.version === x.id), ) - return supported.some((x) => x.version_type !== 'release') + const result = supported.some((x) => x.version_type !== 'release') + debug('resolveHasSnapshots:', { + loader, + totalVersions: versions.length, + supportedVersions: supported.length, + result, + }) + return result }, async save(platform, gameVersion, loaderVersionId) { + debug('save: called', { + instancePath: instance.value.path, + platform, + gameVersion, + loaderVersionId, + }) const editProfile: Record = { loader: platform, game_version: gameVersion, @@ -199,17 +268,21 @@ provideInstallationSettings({ editProfile.loader_version = loaderVersionId } await edit(instance.value.path, editProfile).catch(handleError) + debug('save: edit complete', { editProfile }) }, afterSave: async () => { + debug('afterSave: installing', { instancePath: instance.value.path }) await install(instance.value.path, false).catch(handleError) trackEvent('InstanceRepair', { loader: instance.value.loader, game_version: instance.value.game_version, }) + debug('afterSave: done') }, async repair() { + debug('repair: called', { instancePath: instance.value.path }) repairing.value = true await install(instance.value.path, true).catch(handleError) repairing.value = false @@ -217,9 +290,11 @@ provideInstallationSettings({ loader: instance.value.loader, game_version: instance.value.game_version, }) + debug('repair: done') }, async reinstallModpack() { + debug('reinstallModpack: called', { instancePath: instance.value.path }) reinstalling.value = true await update_repair_modrinth(instance.value.path).catch(handleError) reinstalling.value = false @@ -227,9 +302,11 @@ provideInstallationSettings({ loader: instance.value.loader, game_version: instance.value.game_version, }) + debug('reinstallModpack: done') }, async unlinkModpack() { + debug('unlinkModpack: called', { instancePath: instance.value.path }) await edit(instance.value.path, { linked_data: null as unknown as undefined, }) @@ -237,27 +314,38 @@ provideInstallationSettings({ queryKey: ['linkedModpackInfo', instance.value.path], }) onUnlinked() + debug('unlinkModpack: done') }, getCachedModpackVersions: () => null, async fetchModpackVersions() { + debug('fetchModpackVersions: called', { + projectId: instance.value.linked_data?.project_id, + }) const versions = await get_project_versions(instance.value.linked_data!.project_id!).catch( handleError, ) + debug('fetchModpackVersions: done', { count: versions?.length ?? 0 }) return (versions ?? []) as Labrinth.Versions.v2.Version[] }, async getVersionChangelog(versionId: string) { + debug('getVersionChangelog: called', { versionId }) return (await get_version(versionId, 'must_revalidate').catch( () => null, )) as Labrinth.Versions.v2.Version | null }, async onModpackVersionConfirm(version) { + debug('onModpackVersionConfirm: called', { + versionId: version.id, + instancePath: instance.value.path, + }) await update_managed_modrinth_version(instance.value.path, version.id) await queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instance.value.path], }) + debug('onModpackVersionConfirm: done') }, updaterModalProps: computed(() => ({ diff --git a/apps/app-frontend/src/components/ui/notifications/ServerInvitePopupBody.vue b/apps/app-frontend/src/components/ui/notifications/ServerInvitePopupBody.vue deleted file mode 100644 index 2899a0bdb7..0000000000 --- a/apps/app-frontend/src/components/ui/notifications/ServerInvitePopupBody.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/apps/app-frontend/src/helpers/state.ts b/apps/app-frontend/src/helpers/state.ts index 8e9bb13f54..516d657cf4 100644 --- a/apps/app-frontend/src/helpers/state.ts +++ b/apps/app-frontend/src/helpers/state.ts @@ -10,6 +10,7 @@ export interface LoadingBarType { version?: string profile_path?: string pack_name?: string + icon?: string | null } export interface LoadingBar { diff --git a/apps/app-frontend/src/pages/hosting/manage/Access.vue b/apps/app-frontend/src/pages/hosting/manage/Access.vue new file mode 100644 index 0000000000..59a3b82e36 --- /dev/null +++ b/apps/app-frontend/src/pages/hosting/manage/Access.vue @@ -0,0 +1,33 @@ + + + diff --git a/apps/app-frontend/src/pages/hosting/manage/index.js b/apps/app-frontend/src/pages/hosting/manage/index.js index 50052e3f9e..0c10b07133 100644 --- a/apps/app-frontend/src/pages/hosting/manage/index.js +++ b/apps/app-frontend/src/pages/hosting/manage/index.js @@ -1,7 +1,8 @@ +import Access from './Access.vue' import Backups from './Backups.vue' import Content from './Content.vue' import Files from './Files.vue' import Index from './Index.vue' import Overview from './Overview.vue' -export { Backups, Content, Files, Index, Overview } +export { Access, Backups, Content, Files, Index, Overview } diff --git a/apps/app-frontend/src/routes.js b/apps/app-frontend/src/routes.js index f01df0670c..da93b48e85 100644 --- a/apps/app-frontend/src/routes.js +++ b/apps/app-frontend/src/routes.js @@ -73,6 +73,14 @@ export default new createRouter({ breadcrumb: [{ name: '?Server' }], }, }, + { + path: 'access', + name: 'ServerManageAccess', + component: Hosting.Access, + meta: { + breadcrumb: [{ name: '?Server' }], + }, + }, ], }, { diff --git a/apps/frontend/CLAUDE.md b/apps/frontend/CLAUDE.md index 9d0d05c4df..03515cbb31 100644 --- a/apps/frontend/CLAUDE.md +++ b/apps/frontend/CLAUDE.md @@ -40,4 +40,3 @@ These composables are deprecated and should not be used in new code: - **`useAsyncData`** - we use tanstack, not nuxt's built in async data utility. - **`useBaseFetch`** (`src/composables/fetch.js`) — legacy Labrinth fetch wrapper. Use `client.labrinth.*` modules instead. -- **`useServersFetch`** (`src/composables/servers/servers-fetch.ts`) — legacy Archon fetch wrapper with manual retry/circuit-breaker. Use `client.archon.*` modules instead — refer to the `packages/api-client/CLAUDE.md` for more information. diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts index 45f1526eb4..f7f8a5296b 100644 --- a/apps/frontend/nuxt.config.ts +++ b/apps/frontend/nuxt.config.ts @@ -224,6 +224,7 @@ export default defineNuxtConfig({ globalThis.INTERCOM_APP_ID || 'ykeritl9', production: isProduction(), + cookieSecure: isProduction(), buildEnv: process.env.BUILD_ENV, preview: process.env.PREVIEW === 'true', featureFlagOverrides: getFeatureFlagOverrides(), diff --git a/apps/frontend/package.json b/apps/frontend/package.json index e835e9cfc9..fa2e751f4c 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -26,7 +26,7 @@ "@types/semver": "^7.7.1", "autoprefixer": "^10.4.19", "glob": "^10.2.7", - "nuxt": "^3.20.2", + "nuxt": "=3.20.2", "postcss": "^8.4.39", "prettier-plugin-tailwindcss": "^0.6.5", "sass": "^1.58.0", diff --git a/apps/frontend/src/components/ui/NotificationItem.vue b/apps/frontend/src/components/ui/NotificationItem.vue index 420e852424..1bbac8bf38 100644 --- a/apps/frontend/src/components/ui/NotificationItem.vue +++ b/apps/frontend/src/components/ui/NotificationItem.vue @@ -1,244 +1,284 @@