Merge branch 'main' into boris/dev-1205-trace-rules
@@ -15,12 +15,9 @@ import { I18nDebugPanel, LoadingBar, NotificationPanel } from '@modrinth/ui'
|
||||
|
||||
import AdsConsentNotification from '~/components/ui/AdsConsentNotification.vue'
|
||||
import { setupProviders } from '~/providers/setup.ts'
|
||||
import { setupUserCountryProvider } from '~/providers/setup/user-country.ts'
|
||||
|
||||
import { useAuth } from './composables/auth'
|
||||
|
||||
setupUserCountryProvider()
|
||||
|
||||
const auth = await useAuth()
|
||||
setupProviders(auth)
|
||||
</script>
|
||||
|
||||
@@ -330,9 +330,13 @@ const sharedInstanceBanPending = ref(false)
|
||||
const sharedInstanceVersions = new Map<string, SharedInstances.Instances.v1.InstanceVersion>()
|
||||
let sharedInstanceDetailsRequest: Promise<void> | null = null
|
||||
|
||||
watch(isThreadCollapsed, (collapsed) => {
|
||||
if (!collapsed) void loadSharedInstanceDetails()
|
||||
})
|
||||
watch(
|
||||
isThreadCollapsed,
|
||||
(collapsed) => {
|
||||
if (!collapsed) void loadSharedInstanceDetails()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const didCloseReport = ref(false)
|
||||
const reportClosed = computed(() => {
|
||||
|
||||
@@ -400,10 +400,10 @@ defineExpose({ show, hide })
|
||||
</Combobox>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.deleteAllGroups)"
|
||||
type="quiet"
|
||||
color="red"
|
||||
type="base"
|
||||
:label="formatMessage(messages.deleteAllGroups)"
|
||||
:disabled="titleButtonsDisabled"
|
||||
class="[&:not(:disabled):focus-visible>svg]:!text-red [&:not(:disabled):hover>svg]:!text-red"
|
||||
@click="showConfirmClearGroups"
|
||||
>
|
||||
<TrashIcon v-if="!isClearing" aria-hidden="true" />
|
||||
|
||||
@@ -6,26 +6,25 @@ import type {
|
||||
CreationFlowContextValue,
|
||||
EnvironmentSearchOverride,
|
||||
FilterValue,
|
||||
PendingServerContentInstall,
|
||||
PendingServerContentInstallType,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
addPendingServerContentInstalls,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
flushStoredServerAddonInstallQueue,
|
||||
getServerAddonInstallPlanProjectIds,
|
||||
getStoredServerAddonInstallQueue,
|
||||
getTargetInstallPreferences,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
readPendingServerContentInstalls,
|
||||
readStoredServerInstallQueue,
|
||||
removePendingServerContentInstall,
|
||||
requestInstall,
|
||||
resolveServerAddonInstallPlans,
|
||||
stripServerRuntimeInstallFilters,
|
||||
stripServerRuntimeInstallOverrides,
|
||||
useServerContextRuntime,
|
||||
useServerPanelSync,
|
||||
useVIntl,
|
||||
writePendingServerContentInstallBaseline,
|
||||
waitForServerContextRuntimeReady,
|
||||
writeStoredServerInstallQueue,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
@@ -35,7 +34,6 @@ import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { navigateTo, useRoute } from '#app'
|
||||
import { queryAsString } from '~/utils/router'
|
||||
|
||||
type PendingServerContentInstallInput = Omit<PendingServerContentInstall, 'createdAt'>
|
||||
type ServerInstallBrowseSearchState = Pick<
|
||||
BrowseSearchState,
|
||||
'currentFilters' | 'overriddenProvidedFilterTypes'
|
||||
@@ -88,34 +86,6 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
function getQueuedInstallOwnerFallback(project: ServerInstallSearchResult) {
|
||||
if (project.organization) {
|
||||
const ownerId = project.organization_id ?? project.organization
|
||||
return {
|
||||
id: ownerId,
|
||||
name: project.organization,
|
||||
type: 'organization' as const,
|
||||
link: `/organization/${ownerId}`,
|
||||
}
|
||||
}
|
||||
|
||||
if (!project.author) return null
|
||||
|
||||
const ownerId = project.author_id ?? project.author
|
||||
return {
|
||||
id: ownerId,
|
||||
name: project.author,
|
||||
type: 'user' as const,
|
||||
link: `/user/${ownerId}`,
|
||||
}
|
||||
}
|
||||
|
||||
function getQueuedAddonInstallPlans(
|
||||
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
|
||||
) {
|
||||
return Array.from(plans.values()).filter((plan) => plan.contentType !== 'modpack')
|
||||
}
|
||||
|
||||
export function useServerInstallContent({
|
||||
projectType,
|
||||
onboardingModalRef,
|
||||
@@ -136,6 +106,11 @@ export function useServerInstallContent({
|
||||
const currentServerId = computed(() => queryAsString(route.query.sid) || null)
|
||||
const fromContext = computed(() => queryAsString(route.query.from) || null)
|
||||
const currentWorldId = computed(() => queryAsString(route.query.wid) || null)
|
||||
useServerContextRuntime(currentServerId)
|
||||
useServerPanelSync({
|
||||
serverId: currentServerId,
|
||||
worldId: currentWorldId,
|
||||
})
|
||||
|
||||
const {
|
||||
data: serverData,
|
||||
@@ -177,7 +152,12 @@ export function useServerInstallContent({
|
||||
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<ServerInstallSearchResult>>>(
|
||||
readStoredServerInstallQueue(currentServerId.value, currentWorldId.value),
|
||||
)
|
||||
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
|
||||
const queuedServerInstallRootProjectIds = computed(
|
||||
() => new Set(queuedServerInstalls.value.keys()),
|
||||
)
|
||||
const queuedServerInstallProjectIds = computed(() =>
|
||||
getServerAddonInstallPlanProjectIds(queuedServerInstalls.value.values()),
|
||||
)
|
||||
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
|
||||
const selectedServerInstallProjects = computed(() =>
|
||||
Array.from(queuedServerInstalls.value.values()).map((plan) => ({
|
||||
@@ -219,81 +199,6 @@ export function useServerInstallContent({
|
||||
writeStoredServerInstallQueue(serverId, worldId, plans)
|
||||
}
|
||||
|
||||
async function getQueuedInstallOwner(project: ServerInstallSearchResult) {
|
||||
const fallback = getQueuedInstallOwnerFallback(project)
|
||||
|
||||
try {
|
||||
if (project.organization) {
|
||||
const organization = await client.labrinth.projects_v3.getOrganization(project.project_id)
|
||||
if (organization) {
|
||||
return {
|
||||
id: organization.id,
|
||||
name: organization.name,
|
||||
type: 'organization' as const,
|
||||
avatar_url: organization.icon_url ?? undefined,
|
||||
link: `/organization/${organization.slug}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const members = await client.labrinth.projects_v3.getMembers(project.project_id)
|
||||
const owner =
|
||||
members.find((member) => member.user.id === project.author_id)?.user ??
|
||||
members.find((member) => member.is_owner || member.role === 'Owner')?.user ??
|
||||
members[0]?.user
|
||||
|
||||
if (owner) {
|
||||
return {
|
||||
id: owner.id,
|
||||
name: owner.username,
|
||||
type: 'user' as const,
|
||||
avatar_url: owner.avatar_url,
|
||||
link: `/user/${owner.username}`,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
function getQueuedInstallPlaceholder(
|
||||
plan: BrowseInstallPlan<ServerInstallSearchResult>,
|
||||
owner: PendingServerContentInstallInput['owner'],
|
||||
): PendingServerContentInstallInput {
|
||||
return {
|
||||
projectId: plan.projectId,
|
||||
versionId: plan.versionId,
|
||||
contentType: plan.contentType as PendingServerContentInstallType,
|
||||
title: getInstallProjectName(plan.project),
|
||||
versionName: plan.versionName ?? null,
|
||||
versionNumber: plan.versionNumber ?? null,
|
||||
fileName: plan.fileName ?? null,
|
||||
owner,
|
||||
slug: plan.project.slug ?? plan.projectId,
|
||||
iconUrl: plan.project.icon_url ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function getQueuedInstallPlaceholderFallbacks(
|
||||
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
|
||||
) {
|
||||
return getQueuedAddonInstallPlans(plans).map((plan) =>
|
||||
getQueuedInstallPlaceholder(plan, getQueuedInstallOwnerFallback(plan.project)),
|
||||
)
|
||||
}
|
||||
|
||||
async function getQueuedInstallPlaceholders(
|
||||
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
|
||||
) {
|
||||
return Promise.all(
|
||||
getQueuedAddonInstallPlans(plans).map(async (plan) =>
|
||||
getQueuedInstallPlaceholder(plan, await getQueuedInstallOwner(plan.project)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function setProjectInstalling(projectId: string, installing: boolean) {
|
||||
const next = new Set(installingProjectIds.value)
|
||||
if (installing) {
|
||||
@@ -319,10 +224,6 @@ export function useServerInstallContent({
|
||||
)
|
||||
}
|
||||
|
||||
function getServerInstalledContentKeys(data = serverContentData.value) {
|
||||
return new Set((data?.addons ?? []).map((addon) => addon.project_id ?? addon.filename))
|
||||
}
|
||||
|
||||
function syncHiddenInstalledProjectIds() {
|
||||
hiddenInstalledProjectIds.value = new Set([
|
||||
...getServerInstalledProjectIds(),
|
||||
@@ -437,32 +338,43 @@ export function useServerInstallContent({
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAddonPlan(
|
||||
plan: BrowseInstallPlan<ServerInstallSearchResult>,
|
||||
existingProjectIds: string[],
|
||||
) {
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(getServerInstallTargetPreferences(plan.contentType)),
|
||||
existing_project_ids: existingProjectIds,
|
||||
})
|
||||
|
||||
return [resolved.primary, ...resolved.dependencies].map((item) => ({
|
||||
projectId: item.project_id,
|
||||
versionId: item.version_id,
|
||||
}))
|
||||
}
|
||||
|
||||
async function resolveAndStoreQueuedAddonPlan(
|
||||
plan: BrowseInstallPlan<ServerInstallSearchResult>,
|
||||
) {
|
||||
const resolvedContent = await resolveAddonPlan(plan, Array.from(getServerInstalledProjectIds()))
|
||||
const storedPlan = queuedServerInstalls.value.get(plan.projectId)
|
||||
if (!storedPlan || storedPlan.versionId !== plan.versionId) return
|
||||
|
||||
const nextPlans = new Map(queuedServerInstalls.value)
|
||||
nextPlans.set(plan.projectId, { ...storedPlan, resolvedContent })
|
||||
serverInstallQueue.set(nextPlans)
|
||||
}
|
||||
|
||||
async function resolveQueuedAddonPlans(plans: BrowseInstallPlan<ServerInstallSearchResult>[]) {
|
||||
const existingProjectIds = getServerInstalledProjectIds()
|
||||
const resolvedAddons: Array<{ project_id: string; version_id: string }> = []
|
||||
|
||||
for (const plan of plans) {
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(getServerInstallTargetPreferences(plan.contentType)),
|
||||
existing_project_ids: Array.from(existingProjectIds),
|
||||
})
|
||||
const content = [resolved.primary, ...resolved.dependencies]
|
||||
|
||||
for (const item of content) {
|
||||
if (existingProjectIds.has(item.project_id)) continue
|
||||
existingProjectIds.add(item.project_id)
|
||||
resolvedAddons.push({
|
||||
project_id: item.project_id,
|
||||
version_id: item.version_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedAddons
|
||||
return await resolveServerAddonInstallPlans({
|
||||
plans,
|
||||
existingProjectIds: getServerInstalledProjectIds(),
|
||||
resolvePlan: resolveAddonPlan,
|
||||
})
|
||||
}
|
||||
|
||||
function getInstallProjectVersions(projectId: string) {
|
||||
@@ -498,6 +410,13 @@ export function useServerInstallContent({
|
||||
)
|
||||
if (queuedPlans.size === 0) return true
|
||||
|
||||
try {
|
||||
await waitForServerContextRuntimeReady(client, serverId)
|
||||
} catch (error) {
|
||||
handleError(error as Error)
|
||||
return false
|
||||
}
|
||||
|
||||
isInstallingQueuedServerInstalls.value = true
|
||||
queuedInstallProgress.value = {
|
||||
completed: 0,
|
||||
@@ -518,9 +437,6 @@ export function useServerInstallContent({
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
for (const plan of result.attemptedPlans) {
|
||||
removePendingServerContentInstall(serverId, worldId, plan.projectId)
|
||||
}
|
||||
handleError(result.error as Error)
|
||||
return false
|
||||
}
|
||||
@@ -533,10 +449,7 @@ export function useServerInstallContent({
|
||||
total: result.flushedPlans.length,
|
||||
}
|
||||
if (result.flushedPlans.length > 0) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['content', 'list'] }),
|
||||
])
|
||||
await queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -559,26 +472,10 @@ export function useServerInstallContent({
|
||||
|
||||
if (sid && wid) {
|
||||
writeStoredServerInstallQueue(sid, wid, plans)
|
||||
writePendingServerContentInstallBaseline(sid, wid, [
|
||||
...getServerInstalledContentKeys(),
|
||||
...optimisticallyInstalledProjectIds.value,
|
||||
])
|
||||
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
|
||||
void getQueuedInstallPlaceholders(plans)
|
||||
.then((items) => {
|
||||
const pendingProjectIds = new Set(
|
||||
readPendingServerContentInstalls(sid, wid).map((item) => item.projectId),
|
||||
)
|
||||
addPendingServerContentInstalls(
|
||||
sid,
|
||||
wid,
|
||||
items.filter((item) => pendingProjectIds.has(item.projectId)),
|
||||
)
|
||||
})
|
||||
.catch((err) => handleError(err as Error))
|
||||
}
|
||||
const installed = await flushQueuedServerInstalls(sid, wid)
|
||||
if (!installed) return false
|
||||
await navigateTo(backUrl)
|
||||
void flushQueuedServerInstalls(sid, wid)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -598,16 +495,17 @@ export function useServerInstallContent({
|
||||
const isModpack = contentType === 'modpack'
|
||||
|
||||
try {
|
||||
if (!isModpack && queuedServerInstallProjectIds.value.has(project.project_id)) {
|
||||
if (!isModpack && queuedServerInstallRootProjectIds.value.has(project.project_id)) {
|
||||
removeQueuedServerInstall(project.project_id)
|
||||
return
|
||||
}
|
||||
if (!isModpack && queuedServerInstallProjectIds.value.has(project.project_id)) return
|
||||
|
||||
if (isModpack || !queuedServerInstallProjectIds.value.has(project.project_id)) {
|
||||
setProjectInstalling(project.project_id, true)
|
||||
}
|
||||
|
||||
await requestInstall({
|
||||
const plan = await requestInstall({
|
||||
project,
|
||||
contentType,
|
||||
mode: isModpack ? 'immediate' : 'queue',
|
||||
@@ -646,10 +544,13 @@ export function useServerInstallContent({
|
||||
ctx.modal.value?.setStage('final-config')
|
||||
},
|
||||
})
|
||||
if (!isModpack) await resolveAndStoreQueuedAddonPlan(plan)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
if (isModpack) {
|
||||
setProjectInstalling(project.project_id, false)
|
||||
} else {
|
||||
removeQueuedServerInstall(project.project_id)
|
||||
}
|
||||
handleError(e instanceof Error ? e : new Error(`Error installing content ${e}`))
|
||||
} finally {
|
||||
@@ -804,6 +705,7 @@ export function useServerInstallContent({
|
||||
hideSelectedServerInstalls,
|
||||
installingProjectIds,
|
||||
optimisticallyInstalledProjectIds,
|
||||
queuedServerInstallRootProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
isInstallingQueuedServerInstalls,
|
||||
|
||||
@@ -97,39 +97,16 @@ import {
|
||||
LoadingBar,
|
||||
normalizeChildren,
|
||||
NotificationPanel,
|
||||
provideModrinthClient,
|
||||
provideNotificationManager,
|
||||
providePageContext,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import Logo404 from '~/assets/images/404.svg'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { logout } from '~/composables/user.js'
|
||||
|
||||
import { createModrinthClient } from './helpers/api.ts'
|
||||
import { FrontendNotificationManager } from './providers/frontend-notifications.ts'
|
||||
import { setupLoadingStateProvider } from './providers/setup/loading-state.ts'
|
||||
import { setupProviders } from '~/providers/setup.ts'
|
||||
|
||||
const auth = await useAuth()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
provideNotificationManager(new FrontendNotificationManager())
|
||||
setupLoadingStateProvider()
|
||||
|
||||
const client = createModrinthClient(auth.value, {
|
||||
apiBaseUrl: config.public.apiBaseUrl.replace('/v2/', '/'),
|
||||
archonBaseUrl: config.public.pyroBaseUrl.replace('/v2/', '/'),
|
||||
sharedInstancesBaseUrl: config.public.sharedInstancesBaseUrl,
|
||||
rateLimitKey: config.rateLimitKey,
|
||||
})
|
||||
provideModrinthClient(client)
|
||||
providePageContext({
|
||||
hierarchicalSidebarAvailable: ref(false),
|
||||
showAds: ref(false),
|
||||
adConsentAvailable: ref(false),
|
||||
openExternalUrl: (url) => window.open(url, '_blank'),
|
||||
})
|
||||
setupProviders(auth)
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { GameVersion, InferredVersionInfo, Project } from './infer'
|
||||
import {
|
||||
getGameVersionsMatchingMavenRange,
|
||||
getGameVersionsMatchingSemverRange,
|
||||
semverRangeIntersects,
|
||||
} from './version-ranges'
|
||||
import { versionType } from './version-utils'
|
||||
|
||||
@@ -132,38 +133,73 @@ export function createLoaderParsers(
|
||||
),
|
||||
}
|
||||
},
|
||||
// Fabric (or Babric for mc version beta 1.7.3)
|
||||
'fabric.mod.json': (file: string): InferredVersionInfo => {
|
||||
// Fabric (or its derivatives: Babric, Legacy Fabric, Ornithe)
|
||||
'fabric.mod.json': async (file: string, zip: JSZip): Promise<InferredVersionInfo> => {
|
||||
const metadata = JSON.parse(file) as any
|
||||
|
||||
const mcDependency = metadata.depends?.minecraft
|
||||
const mcDependencies = Array.isArray(mcDependency) ? mcDependency : [mcDependency]
|
||||
|
||||
let detectedGameVersions = metadata.depends
|
||||
? getGameVersionsMatchingSemverRange(metadata.depends.minecraft, simplifiedGameVersions)
|
||||
? getGameVersionsMatchingSemverRange(mcDependency, simplifiedGameVersions)
|
||||
: []
|
||||
const loaders: string[] = []
|
||||
|
||||
// Detect Beta 1.7.3 -> Babric
|
||||
const hasBabricVersion = mcDependencies.some(
|
||||
(version: string | undefined) => version?.includes('1.0.0-beta.7.3'), // this is fabric's normalized mc version format
|
||||
)
|
||||
// Both Legacy Fabric and Ornithe add details about their intermediary to the jar manifest
|
||||
const manifestFile = zip.file('META-INF/MANIFEST.MF')
|
||||
if (manifestFile !== null) {
|
||||
const manifestText = await manifestFile.async('text')
|
||||
if (manifestText.match(/^Legacy-Fabric-Intermediary-Version: (.*)$/)) {
|
||||
loaders.push('legacy-fabric')
|
||||
}
|
||||
if (manifestText.match(/^Calamus-Generation: (.*)$/)) {
|
||||
loaders.push('ornithe')
|
||||
}
|
||||
}
|
||||
|
||||
// Detect 1.3-1.13 -> legacy-fabric
|
||||
const hasLegacyVersions = detectedGameVersions.some((version) => {
|
||||
const match = version.match(/^1\.(\d+)/)
|
||||
return match && parseInt(match[1]) >= 3 && parseInt(match[1]) <= 13
|
||||
})
|
||||
// fall back to version comparison if nothing found in jar manifest
|
||||
if (loaders.length == 0) {
|
||||
// Fabric and its derivates all support different version ranges
|
||||
// - Fabric supports 18w43b and later
|
||||
// - Legacy Fabric supports (almost) only release versions 1.3-1.13.2
|
||||
// - Ornithe supports versions up to 1.14.4
|
||||
// - Babric supports only b1.7.3
|
||||
// These ranges overlap but given the jar manifest logic above the only likely overlap scenarios are
|
||||
// - a b1.7.3 mod that could be Ornithe but is most likely Babric
|
||||
// - a Fabric mod that is dependent on "*" (i.e. all versions)
|
||||
// In other cases, resolve as follows:
|
||||
// 1. if the dependency falls entirely in >=1.3 <=1.13.2, assume Legacy Fabric
|
||||
// 2. if the dependency falls entirely in <=18w43a, assume Ornithe
|
||||
|
||||
if (hasBabricVersion) {
|
||||
loaders.push('babric')
|
||||
detectedGameVersions = gameVersions
|
||||
.filter((version) => version.version === 'b1.7.3')
|
||||
.map((version) => version.version)
|
||||
} else if (hasLegacyVersions) {
|
||||
loaders.push('legacy-fabric')
|
||||
} else {
|
||||
loaders.push('fabric')
|
||||
// dependency version strings are normalized to be Semver 2.0.0 compliant through
|
||||
// https://github.com/FabricMC/fabric-loader/blob/master/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/McVersionLookup.java
|
||||
|
||||
// assume Babric only if dependent on b1.7.3 exactly
|
||||
const hasBabricVersion = mcDependencies.every(
|
||||
(version: string | undefined) => version == '1.0.0-beta.7.3',
|
||||
)
|
||||
|
||||
// Legacy Fabric supports (almost) only release versions which is easy to check
|
||||
const hasLegacyFabricVersions = detectedGameVersions.every((version) => {
|
||||
const match = version.match(/^1\.(\d+)/)
|
||||
return match && parseInt(match[1]) >= 3 && parseInt(match[1]) <= 13
|
||||
})
|
||||
|
||||
// Assume Ornithe only if the dependency range falls entirely into <18w43b
|
||||
const hasOrnitheVersions = !semverRangeIntersects(mcDependencies, '>=1.14.0-alpha.18.43.b')
|
||||
|
||||
if (hasBabricVersion) {
|
||||
loaders.push('babric')
|
||||
detectedGameVersions = gameVersions
|
||||
.filter((version) => version.version === 'b1.7.3')
|
||||
.map((version) => version.version)
|
||||
} else if (hasLegacyFabricVersions) {
|
||||
loaders.push('legacy-fabric')
|
||||
} else if (hasOrnitheVersions) {
|
||||
loaders.push('ornithe')
|
||||
} else {
|
||||
loaders.push('fabric')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -174,23 +210,56 @@ export function createLoaderParsers(
|
||||
game_versions: detectedGameVersions,
|
||||
}
|
||||
},
|
||||
// Quilt
|
||||
'quilt.mod.json': (file: string): InferredVersionInfo => {
|
||||
// Quilt (or its derivatives: Ornithe)
|
||||
'quilt.mod.json': async (file: string, zip: JSZip): Promise<InferredVersionInfo> => {
|
||||
const metadata = JSON.parse(file) as any
|
||||
|
||||
const mcDependency = metadata.quilt_loader.depends?.find(
|
||||
(x: any) => x.id === 'minecraft',
|
||||
)?.versions
|
||||
const mcDependencies = Array.isArray(mcDependency) ? mcDependency : [mcDependency]
|
||||
|
||||
const detectedGameVersions = metadata.quilt_loader.depends
|
||||
? getGameVersionsMatchingSemverRange(mcDependency, simplifiedGameVersions)
|
||||
: []
|
||||
const loaders: string[] = []
|
||||
|
||||
// Ornithe add details about their intermediary to the jar manifest
|
||||
const manifestFile = zip.file('META-INF/MANIFEST.MF')
|
||||
if (manifestFile !== null) {
|
||||
const manifestText = await manifestFile.async('text')
|
||||
if (manifestText.match(/^Calamus-Generation: (.*)$/)) {
|
||||
loaders.push('ornithe')
|
||||
}
|
||||
}
|
||||
|
||||
// fall back to version comparison if nothing found in jar manifest
|
||||
if (loaders.length == 0) {
|
||||
// Quilt and its derivates all support different version ranges
|
||||
// - Quilt supports 1.14.4 and later
|
||||
// - Ornithe supports versions up to 1.14.4
|
||||
// These ranges overlap but given the jar manifest logic above
|
||||
// we can simply prioritize Quilt for the small overlap range
|
||||
|
||||
// dependency version strings are normalized to be Semver 2.0.0 compliant through
|
||||
// https://github.com/QuiltMC/quilt-loader/blob/develop/minecraft/src/main/java/org/quiltmc/loader/impl/game/minecraft/McVersionLookup.java
|
||||
|
||||
// Assume Ornithe only if the dependency range falls entirely into <1.14.4`
|
||||
const hasOrnitheVersions = !semverRangeIntersects(mcDependencies, '>=1.14.4')
|
||||
|
||||
if (hasOrnitheVersions) {
|
||||
loaders.push('ornithe')
|
||||
} else {
|
||||
loaders.push('quilt')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: `${project.title} ${metadata.quilt_loader.version}`,
|
||||
version_number: metadata.quilt_loader.version,
|
||||
loaders: ['quilt'],
|
||||
loaders,
|
||||
version_type: versionType(metadata.quilt_loader.version),
|
||||
game_versions: metadata.quilt_loader.depends
|
||||
? getGameVersionsMatchingSemverRange(
|
||||
metadata.quilt_loader.depends.find((x: any) => x.id === 'minecraft')
|
||||
? metadata.quilt_loader.depends.find((x: any) => x.id === 'minecraft').versions
|
||||
: [],
|
||||
simplifiedGameVersions,
|
||||
)
|
||||
: [],
|
||||
game_versions: detectedGameVersions,
|
||||
}
|
||||
},
|
||||
// Bukkit + Other Forks
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { satisfies } from 'semver'
|
||||
import { intersects, satisfies } from 'semver'
|
||||
|
||||
/**
|
||||
* Returns game versions that match a semver range or array of ranges.
|
||||
@@ -19,6 +19,21 @@ export function getGameVersionsMatchingSemverRange(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a semver range intersects another semver range.
|
||||
*/
|
||||
export function semverRangeIntersects(
|
||||
range: string | string[] | undefined,
|
||||
other: string,
|
||||
): boolean {
|
||||
if (!range) {
|
||||
return true
|
||||
}
|
||||
|
||||
const ranges = Array.isArray(range) ? range : [range]
|
||||
return ranges.some((r) => intersects(r, other))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns game versions that match a Maven-style version range.
|
||||
*/
|
||||
|
||||
@@ -3461,6 +3461,12 @@
|
||||
"project.actions.back-to-project-page": {
|
||||
"message": "Back to project page"
|
||||
},
|
||||
"project.actions.check-modpack-archives": {
|
||||
"message": "Check modpack unzip"
|
||||
},
|
||||
"project.actions.checking-modpack-archives": {
|
||||
"message": "Checking..."
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Create a server"
|
||||
},
|
||||
@@ -3746,9 +3752,24 @@
|
||||
"project.moderation.title": {
|
||||
"message": "Moderation"
|
||||
},
|
||||
"project.modpack-archive-warning.description": {
|
||||
"message": "Importing this .mrpack might be broken."
|
||||
},
|
||||
"project.modpack-archive-warning.title": {
|
||||
"message": "This modpack was published during export bug"
|
||||
},
|
||||
"project.navigation.changelog": {
|
||||
"message": "Changelog"
|
||||
},
|
||||
"project.notification.check-modpack-archives.failed": {
|
||||
"message": "Some modpack files could not be unzipped"
|
||||
},
|
||||
"project.notification.check-modpack-archives.no-files": {
|
||||
"message": "No .mrpack files were found for this project."
|
||||
},
|
||||
"project.notification.check-modpack-archives.success": {
|
||||
"message": "{count, plural, one {The modpack file can be unzipped.} other {All # modpack files can be unzipped.}}"
|
||||
},
|
||||
"project.notification.icon-updated.message": {
|
||||
"message": "Your project's icon has been updated."
|
||||
},
|
||||
@@ -4304,12 +4325,21 @@
|
||||
"report.not-for.dmca.description": {
|
||||
"message": "See our <policy-link>Copyright Policy</policy-link>."
|
||||
},
|
||||
"report.note.ai-images.1": {
|
||||
"message": "We've just updated our rules to prohibit AI-generated images in icons, galleries, and descriptions. We're giving creators a grace period to remove AI images from their projects before we begin accepting reports for them."
|
||||
},
|
||||
"report.note.check-back-later": {
|
||||
"message": "Check back later once the grace period has ended. Reports of this type are not being accepted at this time."
|
||||
},
|
||||
"report.note.copyright.1": {
|
||||
"message": "Please note that you are *not* submitting a DMCA takedown request, but rather a report of reuploaded content."
|
||||
},
|
||||
"report.note.copyright.2": {
|
||||
"message": "If you meant to file a DMCA takedown request (which is a legal action) instead, please see our <copyright-policy-link>Copyright Policy</copyright-policy-link>."
|
||||
},
|
||||
"report.note.fully-ai-generated.1": {
|
||||
"message": "We've just updated our rules to prohibit fully AI-generated projects. We're giving creators a grace period to update or take down fully AI-generated projects before we begin accepting reports for them."
|
||||
},
|
||||
"report.note.malicious.1": {
|
||||
"message": "Reports for malicious or deceptive content must include substantial evidence of the behavior, such as code samples."
|
||||
},
|
||||
@@ -4319,9 +4349,6 @@
|
||||
"report.note.missing-disclosure.1": {
|
||||
"message": "Content disclosures are a new feature we've just added. We're giving creators a grace period to get their disclosures up-to-date before we begin accepting reports for missing or incorrect disclosures."
|
||||
},
|
||||
"report.note.missing-disclosure.2": {
|
||||
"message": "Check back later once the grace period has ended. Reports of this type are not being accepted at this time."
|
||||
},
|
||||
"report.please-report": {
|
||||
"message": "Please report:"
|
||||
},
|
||||
|
||||
@@ -405,6 +405,36 @@
|
||||
:auth="auth"
|
||||
:tags="tags"
|
||||
/>
|
||||
<Admonition
|
||||
v-if="
|
||||
auth.user &&
|
||||
tags.staffRoles.includes(auth.user.role) &&
|
||||
project.actualProjectType === 'modpack' &&
|
||||
hasModpackArchiveInWarningWindow
|
||||
"
|
||||
type="warning"
|
||||
:header="formatMessage(messages.modpackArchiveWarningTitle)"
|
||||
class="mt-3"
|
||||
>
|
||||
{{ formatMessage(messages.modpackArchiveWarningDescription) }}
|
||||
<template #actions>
|
||||
<Button
|
||||
type="colored"
|
||||
color="orange"
|
||||
:loading="isCheckingModpackArchives"
|
||||
@click="checkModpackArchives"
|
||||
>
|
||||
<FileArchiveIcon />
|
||||
{{
|
||||
formatMessage(
|
||||
isCheckingModpackArchives
|
||||
? messages.checkingModpackArchives
|
||||
: messages.checkModpackArchives,
|
||||
)
|
||||
}}
|
||||
</Button>
|
||||
</template>
|
||||
</Admonition>
|
||||
<Admonition
|
||||
v-if="
|
||||
currentMember &&
|
||||
@@ -529,6 +559,7 @@ import {
|
||||
ClipboardCopyIcon,
|
||||
CompassIcon,
|
||||
DownloadIcon,
|
||||
FileArchiveIcon,
|
||||
FolderSearchIcon,
|
||||
HeartIcon,
|
||||
LeftArrowIcon,
|
||||
@@ -663,6 +694,7 @@ const downloadModal = ref()
|
||||
const openInAppModal = ref()
|
||||
const overTheTopDownloadAnimation = ref()
|
||||
const scanModal = ref()
|
||||
const isCheckingModpackArchives = ref(false)
|
||||
|
||||
const projectV3Loaded = computed(() => !projectV3Pending.value || projectV3.value != null)
|
||||
const isServerProject = computed(() => projectV3.value?.minecraft_server != null)
|
||||
@@ -818,6 +850,35 @@ const messages = defineMessages({
|
||||
id: 'project.actions.rescan-modpack',
|
||||
defaultMessage: 'Rescan modpack',
|
||||
},
|
||||
checkModpackArchives: {
|
||||
id: 'project.actions.check-modpack-archives',
|
||||
defaultMessage: 'Check modpack unzip',
|
||||
},
|
||||
checkingModpackArchives: {
|
||||
id: 'project.actions.checking-modpack-archives',
|
||||
defaultMessage: 'Checking...',
|
||||
},
|
||||
checkModpackArchivesSuccess: {
|
||||
id: 'project.notification.check-modpack-archives.success',
|
||||
defaultMessage:
|
||||
'{count, plural, one {The modpack file can be unzipped.} other {All # modpack files can be unzipped.}}',
|
||||
},
|
||||
checkModpackArchivesFailed: {
|
||||
id: 'project.notification.check-modpack-archives.failed',
|
||||
defaultMessage: 'Some modpack files could not be unzipped',
|
||||
},
|
||||
checkModpackArchivesNoFiles: {
|
||||
id: 'project.notification.check-modpack-archives.no-files',
|
||||
defaultMessage: 'No .mrpack files were found for this project.',
|
||||
},
|
||||
modpackArchiveWarningTitle: {
|
||||
id: 'project.modpack-archive-warning.title',
|
||||
defaultMessage: 'This modpack was published during export bug',
|
||||
},
|
||||
modpackArchiveWarningDescription: {
|
||||
id: 'project.modpack-archive-warning.description',
|
||||
defaultMessage: 'Importing this .mrpack might be broken.',
|
||||
},
|
||||
serversPromoDescription: {
|
||||
id: 'project.actions.servers-promo.description',
|
||||
defaultMessage: 'Modrinth Hosting is the easiest way to play with your friends without hassle!',
|
||||
@@ -1135,7 +1196,7 @@ const {
|
||||
|
||||
const dependencies = computed(() => dependenciesRaw.value ?? null)
|
||||
|
||||
// V3 Versions - lazy loaded client-side only
|
||||
// V3 Versions - lazy loaded client-side only (except for staff, who need v3 versions for moderation)
|
||||
const versionsEnabled = ref(false)
|
||||
const {
|
||||
data: versionsV3,
|
||||
@@ -1149,7 +1210,7 @@ const {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
staleTime: STALE_TIME_LONG,
|
||||
enabled: computed(() => !!projectId.value && versionsEnabled.value),
|
||||
enabled: computed(() => !!projectId.value && (versionsEnabled.value || isStaff(auth.value.user))),
|
||||
})
|
||||
|
||||
// Organization
|
||||
@@ -1684,16 +1745,24 @@ const following = computed(() => {
|
||||
return !!user.value.follows.find((x) => x.id === project.value.id)
|
||||
})
|
||||
|
||||
const PROJECT_NOT_FOUND_DESCRIPTION =
|
||||
"There's no project here, check that you have the right link! It may still be under review or no longer publicly available on Modrinth."
|
||||
|
||||
const title = computed(() =>
|
||||
project.value ? `${project.value.title} - Minecraft ${projectTypeDisplay.value}` : '',
|
||||
)
|
||||
const description = computed(() =>
|
||||
project.value
|
||||
? `${project.value.description} - Download the Minecraft ${projectTypeDisplay.value} ${
|
||||
project.value.title
|
||||
} by ${members.value.find((x) => x.is_owner)?.user?.username || 'a creator'} on Modrinth`
|
||||
: '',
|
||||
? `${project.value.title} - Minecraft ${projectTypeDisplay.value}`
|
||||
: 'Project not found',
|
||||
)
|
||||
const description = computed(() => {
|
||||
if (!project.value) {
|
||||
return PROJECT_NOT_FOUND_DESCRIPTION
|
||||
}
|
||||
|
||||
const creator = organization.value?.name || members.value.find((x) => x.is_owner)?.user?.username
|
||||
const byLine = creator ? ` by ${creator}` : ''
|
||||
|
||||
return `${project.value.description} - Download the Minecraft ${projectTypeDisplay.value} ${project.value.title}${byLine} on Modrinth`
|
||||
})
|
||||
|
||||
const canCreateServerFrom = computed(() => {
|
||||
if (!project.value) return false
|
||||
@@ -1749,6 +1818,90 @@ const showProjectHeaderCreateServerAction = computed(
|
||||
const projectHeaderCreateServerTo = computed(() =>
|
||||
project.value ? `/hosting?project=${project.value.id}#plan` : '/hosting',
|
||||
)
|
||||
|
||||
const MRPACK_ARCHIVE_WARNING_START = new Date('2026-08-10T17:00:00.000Z').getTime()
|
||||
const MRPACK_ARCHIVE_WARNING_END = new Date('2026-08-13T20:00:00.000Z').getTime()
|
||||
const hasModpackArchiveInWarningWindow = computed(() =>
|
||||
(versionsV3.value ?? []).some((version) => {
|
||||
const publishedAt = new Date(version.date_published).getTime()
|
||||
return (
|
||||
version.files.some((file) => file.filename.toLowerCase().endsWith('.mrpack')) &&
|
||||
publishedAt >= MRPACK_ARCHIVE_WARNING_START &&
|
||||
publishedAt <= MRPACK_ARCHIVE_WARNING_END
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
async function checkModpackArchives() {
|
||||
if (!project.value || isCheckingModpackArchives.value) return
|
||||
|
||||
isCheckingModpackArchives.value = true
|
||||
startLoading()
|
||||
|
||||
try {
|
||||
const versions = await client.labrinth.versions_v2.getProjectVersions(project.value.id)
|
||||
const filesByUrl = new Map(
|
||||
versions
|
||||
.flatMap((version) => version.files)
|
||||
.filter((file) => file.filename.toLowerCase().endsWith('.mrpack'))
|
||||
.map((file) => [file.url, file]),
|
||||
)
|
||||
const files = [...filesByUrl.values()]
|
||||
|
||||
if (files.length === 0) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: formatMessage(messages.checkModpackArchivesNoFiles),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { default: JSZip } = await import('jszip')
|
||||
const failures = []
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const response = await fetch(file.url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed (${response.status} ${response.statusText})`)
|
||||
}
|
||||
|
||||
await JSZip.loadAsync(await response.blob(), { checkCRC32: true })
|
||||
} catch (error) {
|
||||
failures.push({
|
||||
filename: file.filename,
|
||||
error: error?.message ?? String(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
addNotification({
|
||||
title: formatMessage(messages.checkModpackArchivesFailed),
|
||||
text: failures.map((failure) => `${failure.filename}: ${failure.error}`).join('\n'),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.successLabel),
|
||||
text: formatMessage(messages.checkModpackArchivesSuccess, { count: files.length }),
|
||||
type: 'success',
|
||||
})
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: error?.data?.description ?? error?.message ?? String(error),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isCheckingModpackArchives.value = false
|
||||
stopLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const projectHeaderMoreActions = computed(() => {
|
||||
const isStaff = !!(auth.value.user && tags.value.staffRoles.includes(auth.value.user.role))
|
||||
|
||||
@@ -1787,6 +1940,19 @@ const projectHeaderMoreActions = computed(() => {
|
||||
tone: 'orange',
|
||||
shown: !!auth.value.user && isStaff && project.value?.actualProjectType === 'modpack',
|
||||
},
|
||||
{
|
||||
id: 'moderation-modpack-check-archives',
|
||||
label: formatMessage(
|
||||
isCheckingModpackArchives.value
|
||||
? messages.checkingModpackArchives
|
||||
: messages.checkModpackArchives,
|
||||
),
|
||||
icon: FileArchiveIcon,
|
||||
action: checkModpackArchives,
|
||||
tone: 'orange',
|
||||
disabled: isCheckingModpackArchives.value,
|
||||
shown: !!auth.value.user && isStaff && project.value?.actualProjectType === 'modpack',
|
||||
},
|
||||
{ type: 'divider', shown: !!auth.value.user && isStaff },
|
||||
{
|
||||
id: 'report',
|
||||
@@ -1828,8 +1994,11 @@ if (!route.name.startsWith('type-project-settings')) {
|
||||
title: () => title.value,
|
||||
description: () => description.value,
|
||||
ogTitle: () => title.value,
|
||||
ogDescription: () => project.value?.description ?? '',
|
||||
ogImage: () => project.value?.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
|
||||
ogDescription: () => project.value?.description ?? PROJECT_NOT_FOUND_DESCRIPTION,
|
||||
ogImage: () =>
|
||||
project.value
|
||||
? (project.value?.icon_url ?? 'https://cdn-raw.modrinth.com/placeholder-square.png')
|
||||
: 'https://cdn-raw.modrinth.com/not-found-transparent.png',
|
||||
ogUrl: createCanonicalUrl,
|
||||
robots: () => (project.value?.status === 'approved' ? 'all' : 'noindex'),
|
||||
})
|
||||
|
||||
@@ -740,7 +740,7 @@ watch(
|
||||
}),
|
||||
ogTitle: formatMessage(messages.collectionTitle, { name: col.name }),
|
||||
ogDescription: col.description,
|
||||
ogImage: col.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
|
||||
ogImage: col.icon_url ?? 'https://cdn-raw.modrinth.com/placeholder-square.png',
|
||||
ogUrl: canonicalUrl,
|
||||
robots: col.status === 'listed' ? 'all' : 'noindex',
|
||||
})
|
||||
|
||||
@@ -163,6 +163,7 @@ const {
|
||||
hideSelectedServerInstalls,
|
||||
installingProjectIds,
|
||||
optimisticallyInstalledProjectIds,
|
||||
queuedServerInstallRootProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
isInstallingQueuedServerInstalls,
|
||||
@@ -327,6 +328,7 @@ function getCardActions(
|
||||
|
||||
if (serverData.value) {
|
||||
const isQueued = queuedServerInstallProjectIds.value.has(result.project_id)
|
||||
const isQueuedRoot = queuedServerInstallRootProjectIds.value.has(result.project_id)
|
||||
const isInstalled =
|
||||
projectResult.installed ||
|
||||
optimisticallyInstalledProjectIds.value.has(result.project_id) ||
|
||||
@@ -362,7 +364,8 @@ function getCardActions(
|
||||
? CheckIcon
|
||||
: DownloadIcon,
|
||||
iconClass: isInstalling || isInstallingSelection ? 'animate-spin' : undefined,
|
||||
disabled: !!isInstalled || isInstalling || isInstallingSelection,
|
||||
disabled:
|
||||
!!isInstalled || isInstalling || isInstallingSelection || (isQueued && !isQueuedRoot),
|
||||
color: isQueued && !isInstalling && !isInstallingSelection ? 'green' : 'brand',
|
||||
type: 'outlined',
|
||||
onClick: () => serverInstall(projectResult),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="markdown-body">
|
||||
<h1>Content Rules</h1>
|
||||
<p><em>Last modified: August 12, 2026</em></p>
|
||||
<p><em>Last modified: August 13, 2026</em></p>
|
||||
|
||||
<p>
|
||||
These Content Rules are to be considered part of our
|
||||
|
||||
@@ -483,7 +483,7 @@ watch(
|
||||
description,
|
||||
ogTitle: title,
|
||||
ogDescription: org.description,
|
||||
ogImage: org.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
|
||||
ogImage: org.icon_url ?? 'https://cdn-raw.modrinth.com/placeholder-square.png',
|
||||
ogUrl: canonicalUrl,
|
||||
})
|
||||
useHead({
|
||||
|
||||
@@ -109,17 +109,7 @@
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.whatContentType) }}
|
||||
</span>
|
||||
<RadioButtons
|
||||
v-slot="{ item }"
|
||||
v-model="reportItem"
|
||||
:items="reportItems"
|
||||
@update:model-value="
|
||||
() => {
|
||||
prefilled = false
|
||||
fetchItem()
|
||||
}
|
||||
"
|
||||
>
|
||||
<RadioButtons v-slot="{ item }" v-model="reportItem" :items="reportItems">
|
||||
{{ capitalizeString(item) }}
|
||||
</RadioButtons>
|
||||
</div>
|
||||
@@ -139,13 +129,6 @@
|
||||
autocomplete="off"
|
||||
:disabled="reportItem === ''"
|
||||
wrapper-class="w-40"
|
||||
@blur="
|
||||
() => {
|
||||
prefilled = false
|
||||
reportItemID = reportItemID.trim()
|
||||
fetchItem()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div v-if="checkingId || checkedId" class="flex items-center gap-1">
|
||||
<template v-if="checkingId">
|
||||
@@ -163,8 +146,8 @@
|
||||
class="flex items-center gap-1 font-semibold text-contrast hover:underline"
|
||||
>
|
||||
<Avatar
|
||||
v-if="typeof itemIcon === 'string'"
|
||||
:src="itemIcon"
|
||||
v-if="typeof itemIcon === 'string' || !itemIcon"
|
||||
:src="itemIcon ?? null"
|
||||
:alt="itemName"
|
||||
size="24px"
|
||||
:circle="reportItem === 'user'"
|
||||
@@ -303,22 +286,22 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { Project, Report, User, Version } from '@modrinth/utils'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const tags = useGeneratedState()
|
||||
const route = useNativeRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const auth = await useAuth()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
if (!auth.value.user) {
|
||||
router.push('/auth/sign-in?redirect=' + encodeURIComponent(route.fullPath))
|
||||
}
|
||||
|
||||
const accessQuery = (id: string): string => {
|
||||
return route.query?.[id]?.toString() || ''
|
||||
}
|
||||
@@ -368,58 +351,83 @@ async function fetchExistingReports() {
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchItem() {
|
||||
if (reportItem.value && reportItemID.value) {
|
||||
checkingId.value = true
|
||||
itemIcon.value = undefined
|
||||
itemName.value = undefined
|
||||
itemLink.value = undefined
|
||||
itemId.value = undefined
|
||||
itemIssueTracker.value = undefined
|
||||
try {
|
||||
if (reportItem.value === 'project') {
|
||||
const project = (await useBaseFetch(`project/${reportItemID.value}`)) as Project
|
||||
currentProject.value = project
|
||||
const fetchItemDebounced = useDebounceFn(fetchItem, 500)
|
||||
|
||||
itemIcon.value = project.icon_url
|
||||
itemName.value = project.title
|
||||
itemLink.value = `/project/${project.id}`
|
||||
itemId.value = project.id
|
||||
itemIssueTracker.value = project.issues_url
|
||||
} else if (reportItem.value === 'version') {
|
||||
const version = (await useBaseFetch(`version/${reportItemID.value}`)) as Version
|
||||
currentVersion.value = version
|
||||
|
||||
itemIcon.value = VersionIcon
|
||||
itemName.value = version.version_number
|
||||
itemLink.value = `project/${version.project_id}/version/${version.id}`
|
||||
itemId.value = version.id
|
||||
} else if (reportItem.value === 'user') {
|
||||
const user = (await useBaseFetch(`user/${reportItemID.value}`)) as User
|
||||
currentUser.value = user
|
||||
|
||||
itemIcon.value = user.avatar_url
|
||||
itemName.value = user.username
|
||||
itemLink.value = `/user/${user.username}`
|
||||
itemId.value = user.id
|
||||
}
|
||||
} catch {
|
||||
// Ignored
|
||||
}
|
||||
checkedId.value = true
|
||||
checkingId.value = false
|
||||
watch([reportItem, reportItemID], ([type, id], [prevType]) => {
|
||||
prefilled.value = false
|
||||
if (!id.trim() || type !== prevType) {
|
||||
fetchItem()
|
||||
} else {
|
||||
fetchItemDebounced()
|
||||
}
|
||||
})
|
||||
|
||||
async function fetchItem() {
|
||||
const type = reportItem.value
|
||||
const id = reportItemID.value.trim()
|
||||
|
||||
currentProject.value = null
|
||||
currentVersion.value = null
|
||||
currentUser.value = null
|
||||
itemIcon.value = undefined
|
||||
itemName.value = undefined
|
||||
itemLink.value = undefined
|
||||
itemId.value = undefined
|
||||
itemIssueTracker.value = undefined
|
||||
|
||||
if (!type || !id) {
|
||||
checkedId.value = false
|
||||
checkingId.value = false
|
||||
return
|
||||
}
|
||||
|
||||
checkingId.value = true
|
||||
try {
|
||||
if (type === 'project') {
|
||||
const project = (await useBaseFetch(`project/${id}`)) as Project
|
||||
currentProject.value = project
|
||||
itemIcon.value = project.icon_url
|
||||
itemName.value = project.title
|
||||
itemLink.value = `/project/${project.id}`
|
||||
itemId.value = project.id
|
||||
itemIssueTracker.value = project.issues_url
|
||||
} else if (type === 'version') {
|
||||
const version = (await useBaseFetch(`version/${id}`)) as Version
|
||||
currentVersion.value = version
|
||||
itemIcon.value = VersionIcon
|
||||
itemName.value = version.version_number
|
||||
itemLink.value = `project/${version.project_id}/version/${version.id}`
|
||||
itemId.value = version.id
|
||||
} else if (type === 'user') {
|
||||
const user = (await useBaseFetch(`user/${id}`)) as User
|
||||
currentUser.value = user
|
||||
itemIcon.value = user.avatar_url
|
||||
itemName.value = user.username
|
||||
itemLink.value = `/user/${user.username}`
|
||||
itemId.value = user.id
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
checkedId.value = true
|
||||
checkingId.value = false
|
||||
}
|
||||
|
||||
const reportItems = ['project', 'version', 'user']
|
||||
const dummyProjectReportTypes = ['missing-disclosure', 'ai-images', 'fully-ai-generated'] as const
|
||||
const reportTypes = computed(() => {
|
||||
const types = [...tags.value.reportTypes]
|
||||
if (reportItem.value === 'project') {
|
||||
types.push('missing-disclosure')
|
||||
const otherIndex = types.indexOf('other')
|
||||
if (otherIndex === -1) {
|
||||
types.push(...dummyProjectReportTypes)
|
||||
} else {
|
||||
types.splice(otherIndex, 0, ...dummyProjectReportTypes)
|
||||
}
|
||||
}
|
||||
return types
|
||||
})
|
||||
const disableReporting = computed(() => reportType.value === 'missing-disclosure')
|
||||
const disableReporting = computed(() => dummyProjectReportTypes.includes(reportType.value))
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return (
|
||||
@@ -531,44 +539,6 @@ const onImageUpload = async (file: File) => {
|
||||
return item.url
|
||||
}
|
||||
|
||||
const warnings: Record<string, MessageDescriptor[]> = {
|
||||
copyright: [
|
||||
defineMessage({
|
||||
id: 'report.note.copyright.1',
|
||||
defaultMessage:
|
||||
'Please note that you are *not* submitting a DMCA takedown request, but rather a report of reuploaded content.',
|
||||
}),
|
||||
defineMessage({
|
||||
id: 'report.note.copyright.2',
|
||||
defaultMessage:
|
||||
'If you meant to file a DMCA takedown request (which is a legal action) instead, please see our <copyright-policy-link>Copyright Policy</copyright-policy-link>.',
|
||||
}),
|
||||
],
|
||||
malicious: [
|
||||
defineMessage({
|
||||
id: 'report.note.malicious.1',
|
||||
defaultMessage:
|
||||
'Reports for malicious or deceptive content must include substantial evidence of the behavior, such as code samples.',
|
||||
}),
|
||||
defineMessage({
|
||||
id: 'report.note.malicious.2',
|
||||
defaultMessage:
|
||||
'Summaries from Microsoft Defender, VirusTotal, or AI malware detection are not sufficient forms of evidence and will not be accepted.',
|
||||
}),
|
||||
],
|
||||
'missing-disclosure': [
|
||||
defineMessage({
|
||||
id: 'report.note.missing-disclosure.1',
|
||||
defaultMessage: `Content disclosures are a new feature we've just added. We're giving creators a grace period to get their disclosures up-to-date before we begin accepting reports for missing or incorrect disclosures.`,
|
||||
}),
|
||||
defineMessage({
|
||||
id: 'report.note.missing-disclosure.2',
|
||||
defaultMessage:
|
||||
'Check back later once the grace period has ended. Reports of this type are not being accepted at this time.',
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
reportContent: {
|
||||
id: 'report.report-content',
|
||||
@@ -662,7 +632,60 @@ const messages = defineMessages({
|
||||
id: 'report.submit',
|
||||
defaultMessage: 'Submit report',
|
||||
},
|
||||
checkBackLater: {
|
||||
id: 'report.note.check-back-later',
|
||||
defaultMessage:
|
||||
'Check back later once the grace period has ended. Reports of this type are not being accepted at this time.',
|
||||
},
|
||||
})
|
||||
|
||||
const warnings: Record<string, MessageDescriptor[]> = {
|
||||
copyright: [
|
||||
defineMessage({
|
||||
id: 'report.note.copyright.1',
|
||||
defaultMessage:
|
||||
'Please note that you are *not* submitting a DMCA takedown request, but rather a report of reuploaded content.',
|
||||
}),
|
||||
defineMessage({
|
||||
id: 'report.note.copyright.2',
|
||||
defaultMessage:
|
||||
'If you meant to file a DMCA takedown request (which is a legal action) instead, please see our <copyright-policy-link>Copyright Policy</copyright-policy-link>.',
|
||||
}),
|
||||
],
|
||||
malicious: [
|
||||
defineMessage({
|
||||
id: 'report.note.malicious.1',
|
||||
defaultMessage:
|
||||
'Reports for malicious or deceptive content must include substantial evidence of the behavior, such as code samples.',
|
||||
}),
|
||||
defineMessage({
|
||||
id: 'report.note.malicious.2',
|
||||
defaultMessage:
|
||||
'Summaries from Microsoft Defender, VirusTotal, or AI malware detection are not sufficient forms of evidence and will not be accepted.',
|
||||
}),
|
||||
],
|
||||
'missing-disclosure': [
|
||||
defineMessage({
|
||||
id: 'report.note.missing-disclosure.1',
|
||||
defaultMessage: `Content disclosures are a new feature we've just added. We're giving creators a grace period to get their disclosures up-to-date before we begin accepting reports for missing or incorrect disclosures.`,
|
||||
}),
|
||||
messages.checkBackLater,
|
||||
],
|
||||
'ai-images': [
|
||||
defineMessage({
|
||||
id: 'report.note.ai-images.1',
|
||||
defaultMessage: `We've just updated our rules to prohibit AI-generated images in icons, galleries, and descriptions. We're giving creators a grace period to remove AI images from their projects before we begin accepting reports for them.`,
|
||||
}),
|
||||
messages.checkBackLater,
|
||||
],
|
||||
'fully-ai-generated': [
|
||||
defineMessage({
|
||||
id: 'report.note.fully-ai-generated.1',
|
||||
defaultMessage: `We've just updated our rules to prohibit fully AI-generated projects. We're giving creators a grace period to update or take down fully AI-generated projects before we begin accepting reports for them.`,
|
||||
}),
|
||||
messages.checkBackLater,
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -85,10 +85,12 @@ if (projectsResult.status === 'fulfilled') {
|
||||
warmProjectCheckCaches(queryClient, projectsResult.value)
|
||||
}
|
||||
const title = computed(() =>
|
||||
prefetchedUser ? `${prefetchedUser.username} - Modrinth` : 'Modrinth',
|
||||
prefetchedUser ? `${prefetchedUser.username} - Modrinth` : 'User not found',
|
||||
)
|
||||
const description = computed(() => {
|
||||
if (!prefetchedUser) return ''
|
||||
if (!prefetchedUser) {
|
||||
return `There's no user here, check that you have the right link!`
|
||||
}
|
||||
return prefetchedUser.bio
|
||||
? `${prefetchedUser.bio} - Download ${prefetchedUser.username}'s projects on Modrinth`
|
||||
: `Download ${prefetchedUser.username}'s projects on Modrinth`
|
||||
@@ -99,7 +101,10 @@ useSeoMeta({
|
||||
description: () => description.value,
|
||||
ogTitle: () => title.value,
|
||||
ogDescription: () => description.value,
|
||||
ogImage: () => prefetchedUser?.avatar_url ?? 'https://cdn.modrinth.com/placeholder.png',
|
||||
ogImage: () =>
|
||||
prefetchedUser
|
||||
? (prefetchedUser?.avatar_url ?? 'https://cdn-raw.modrinth.com/placeholder-circle.png')
|
||||
: 'https://cdn-raw.modrinth.com/not-found-transparent.png',
|
||||
})
|
||||
|
||||
const projectCreateModal = ref<InstanceType<typeof ProjectCreateModal> | null>(null)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { setupLoadingStateProvider } from './setup/loading-state'
|
||||
import { setupModrinthClientProvider } from './setup/modrinth-client'
|
||||
import { setupPageContextProvider } from './setup/page-context'
|
||||
import { setupTagsProvider } from './setup/tags'
|
||||
import { setupUserCountryProvider } from './setup/user-country'
|
||||
|
||||
export function setupProviders(auth: Awaited<ReturnType<typeof useAuth>>) {
|
||||
provideNotificationManager(new FrontendNotificationManager())
|
||||
@@ -17,4 +18,5 @@ export function setupProviders(auth: Awaited<ReturnType<typeof useAuth>>) {
|
||||
setupFilePickerProvider()
|
||||
setupPageContextProvider()
|
||||
setupLoadingStateProvider()
|
||||
setupUserCountryProvider()
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ conversantmedia.com, 100141, RESELLER
|
||||
# Adagio - Equativ
|
||||
smartadserver.com, 3554, RESELLER
|
||||
loopme.com, 5679, RESELLER, 6c8d5f95897a5a3b
|
||||
sharethrough.com, OAW69Fon, RESELLER, d53b998a7bd4ecd2
|
||||
smartadserver.com,3262,RESELLER,060d053dcf45cbf3
|
||||
sharethrough.com,3262,RESELLER,d53b998a7bd4ecd2
|
||||
# Adagio - Sovrn
|
||||
lijit.com, 367236, RESELLER, fafdf38b16bf6b2b
|
||||
openx.com, 538959099, RESELLER, 6a698e2ec38604c6
|
||||
|
||||
|
After Width: | Height: | Size: 700 KiB |
|
After Width: | Height: | Size: 572 KiB |
|
After Width: | Height: | Size: 501 KiB |
|
After Width: | Height: | Size: 705 KiB |
|
After Width: | Height: | Size: 462 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 162 KiB |
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"articles": [
|
||||
{
|
||||
"title": "More ways to personalize your library",
|
||||
"summary": "The new Play page, overhauled instance groups, custom icons, and a smoother onboarding experience.",
|
||||
"thumbnail": "https://modrinth.com/news/article/app-personalization/thumbnail.webp",
|
||||
"date": "2026-08-18T00:30:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/app-personalization"
|
||||
},
|
||||
{
|
||||
"title": "New AI rules and project disclosures",
|
||||
"summary": "An update to Modrinth’s Content Rules and our new mandatory content disclosures.",
|
||||
|
||||