Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
b97f338
feat: implement access tab with dummy data
IMB11 May 4, 2026
7ca96c3
fix: spacing
IMB11 May 4, 2026
5689c2b
feat: qa
IMB11 May 6, 2026
224befa
feat: implement backend
IMB11 May 6, 2026
33400c7
qa: qa pass
IMB11 May 8, 2026
a1ebba5
feat: fix user "search"
IMB11 May 8, 2026
773f142
fix: lint
IMB11 May 8, 2026
cf25e6e
feat: change to bitfield
IMB11 May 8, 2026
054a9c4
feat: fix fields
IMB11 May 8, 2026
c4f093b
fix: lint
IMB11 May 8, 2026
3687ade
fix: lint
IMB11 May 8, 2026
bb8e85e
feat: hook up api
IMB11 May 16, 2026
7606329
feat: fix permissions
IMB11 May 19, 2026
a94e498
feat: audit log table event start
IMB11 May 19, 2026
cdced21
feat: better mobile mode for audit log table
IMB11 May 19, 2026
96d0898
feat: i18n
IMB11 May 19, 2026
740331f
feat: qa
IMB11 May 19, 2026
c7d95c3
feat: enforce permissions
IMB11 May 19, 2026
645b632
feat: email template start
IMB11 May 20, 2026
1ea2a75
feat: qa
IMB11 May 21, 2026
6612695
fix: tooltip bug
IMB11 May 21, 2026
4db6236
feat: qa
IMB11 May 21, 2026
13eb82a
impl: sse support in api-client
IMB11 May 21, 2026
9db8c55
feat: sse impl
IMB11 May 21, 2026
39a529f
fix: desync path
IMB11 May 21, 2026
ec17c40
feat: time frame picker from analytics
IMB11 May 22, 2026
498a719
feat: QA
IMB11 May 22, 2026
747c0c6
fix: spacing
IMB11 May 22, 2026
43128d4
fix: permisison audit log entries
IMB11 May 22, 2026
aff6d1d
fix: hosting manage page shared server detection
IMB11 May 22, 2026
fb4d3f8
fix: lint
IMB11 May 22, 2026
d7b6625
feat: qa + lint
IMB11 May 22, 2026
a0b644f
feat: audit log table sort by time
IMB11 May 22, 2026
5c68463
feat: finish frontend panel stuff
IMB11 May 22, 2026
5380ae5
fix: lint
IMB11 May 22, 2026
a4d93c2
fix: backend alignment
IMB11 May 22, 2026
a305464
fix: lint
IMB11 May 22, 2026
0e0dda5
Merge branch 'main' into cal/sharing-tab-panel
IMB11 May 22, 2026
9ed9873
fix: supress friend errors
IMB11 May 22, 2026
803aa49
feat: qa
IMB11 May 25, 2026
fdecfee
Merge branch 'main' into cal/sharing-tab-panel
IMB11 May 25, 2026
34c1a22
fix: qa
IMB11 May 25, 2026
185d4e3
fix: lint
IMB11 May 25, 2026
418a35f
Merge branch 'cal/sharing-tab-panel' of https://github.com/modrinth/c…
IMB11 May 25, 2026
7b137df
Merge remote-tracking branch 'origin/main' into cal/sharing-tab-panel
IMB11 Jun 1, 2026
e6c2ddf
fix: utils barrel
IMB11 Jun 1, 2026
f4c7dfe
fix: safari cookies in dev
IMB11 Jun 1, 2026
87cfb88
fix: pin nuxt
IMB11 Jun 2, 2026
65ba962
feat: fixes + notif fix
IMB11 Jun 2, 2026
84062e3
Merge remote-tracking branch 'origin/main' into cal/sharing-tab-panel
IMB11 Jun 2, 2026
b3d0478
fix: notifications
IMB11 Jun 2, 2026
c0cce3b
Merge branch 'main' into cal/sharing-tab-panel
IMB11 Jun 2, 2026
a7fb4e6
Merge branch 'main' into cal/sharing-tab-panel
IMB11 Jun 2, 2026
d9d58ee
feat: qa
IMB11 Jun 3, 2026
03935b2
fix: notification sync not happening immediately
IMB11 Jun 3, 2026
fcddaf8
Merge branch 'main' into cal/sharing-tab-panel
IMB11 Jun 3, 2026
7275b20
fix: qa
IMB11 Jun 3, 2026
d212fa7
fix: qa
IMB11 Jun 3, 2026
56a0897
feat: qa
IMB11 Jun 3, 2026
6707a70
blog + prepr
Prospector Jun 3, 2026
e5196a6
feat: toast shit
IMB11 Jun 3, 2026
5692cd6
Merge branch 'cal/sharing-tab-panel' of https://github.com/modrinth/c…
IMB11 Jun 3, 2026
45050c0
blog images
Prospector Jun 3, 2026
968eb52
thumbnail update one last time
Prospector Jun 3, 2026
82a30bd
prepr
Prospector Jun 3, 2026
ee9674c
feat: use reinvite route
IMB11 Jun 3, 2026
c36055b
Merge branch 'cal/sharing-tab-panel' of https://github.com/modrinth/c…
IMB11 Jun 3, 2026
a99b6ba
Merge remote-tracking branch 'origin/main' into cal/sharing-tab-panel
IMB11 Jun 3, 2026
050b4c5
update images
Prospector Jun 3, 2026
b6b9a2e
fix: reinvite stuff
IMB11 Jun 3, 2026
5426f58
fix: lint
IMB11 Jun 3, 2026
b93d355
fix: alignment of save bar
IMB11 Jun 3, 2026
2a59a0b
fix: notif sizing
IMB11 Jun 3, 2026
1a98120
fix: split up access
IMB11 Jun 3, 2026
d1da60c
fix: lint
IMB11 Jun 3, 2026
ea40139
fix: lint
IMB11 Jun 3, 2026
b8f6fa9
fix: link
IMB11 Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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]": {
Expand Down
41 changes: 15 additions & 26 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
import {
ArrowBigUpDashIcon,
ChangeSkinIcon,
CheckIcon,
CompassIcon,
DownloadIcon,
ExternalIcon,
Expand Down Expand Up @@ -41,7 +40,6 @@ import {
defineMessages,
I18nDebugPanel,
LoadingBar,
ModrinthHostingLogo,
NewsArticleCard,
NotificationPanel,
OverflowMenu,
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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),
},
})
}

Expand Down
39 changes: 39 additions & 0 deletions apps/app-frontend/src/components/ui/AppActionBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -284,6 +285,7 @@ function goToTerminal(path?: string) {
}

const currentLoadingBars = ref<LoadingBar[]>([])
const currentLoadingBarIconUrls = ref<Record<string, string | null>>({})
const notificationId = ref<string | number | null>(null)
const dismissed = ref(false)

Expand All @@ -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
Expand All @@ -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,
}))
Expand Down Expand Up @@ -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 ?? ''}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
InstallationSettingsLayout,
provideAppBackup,
provideInstallationSettings,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import type { GameVersionTag, PlatformTag } from '@modrinth/utils'
Expand All @@ -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,
Expand Down Expand Up @@ -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'),
Expand All @@ -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
Expand All @@ -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}` })
},
})

Expand Down Expand Up @@ -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<string, string | undefined> = {
loader: platform,
game_version: gameVersion,
Expand All @@ -199,65 +268,84 @@ 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
trackEvent('InstanceRepair', {
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
trackEvent('InstanceRepair', {
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,
})
await queryClient.invalidateQueries({
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(() => ({
Expand Down
Loading
Loading