refactor: content tab styling (#7091)

* refactor: managed content card

* fix: update filter pill styling to be chips-like

* fix: modpack exporting

* fixed: vanilla server projs + header

* refactor: filters

* refactor: detect external file icons + other metadata inside

* refactor: support json5 metadata

* fix: qa

* fix: qa

* fix: fmt + prepr

* fix: fmt

* fix: qa

* fix: headers

* feat: locking

* fix: clippy

* fix: qa

* fix: frozen loc

* fix: shared instances diff calc break

* fix: env filter + remove categories
This commit is contained in:
Calum H.
2026-08-13 14:55:35 +00:00
committed by GitHub
parent e087e571f3
commit 06b7c44dcc
126 changed files with 3654 additions and 3473 deletions
@@ -140,10 +140,11 @@
</div>
</NewModal>
<ModpackContentModal
ref="modpackContentModal"
:modpack-name="project?.name ?? ''"
:modpack-icon-url="project?.icon_url ?? undefined"
<ManagedContentModal
ref="managedContentModal"
:header="formatMessage(messages.modpackContent)"
:source-name="project?.name ?? ''"
:source-icon-url="project?.icon_url ?? undefined"
/>
</template>
@@ -158,7 +159,7 @@ import {
type ContentItem,
defineMessages,
formatLoader,
ModpackContentModal,
ManagedContentModal,
NewModal,
Table,
type TableColumn,
@@ -267,10 +268,10 @@ function handleReport() {
}
}
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal>>()
const managedContentModal = ref<InstanceType<typeof ManagedContentModal>>()
async function openViewContents() {
modpackContentModal.value?.showLoading()
managedContentModal.value?.showLoading()
try {
// Ensure version data is available — the useQuery may not have resolved yet
const versionId = modpackVersionId.value
@@ -328,10 +329,10 @@ async function openViewContents() {
}
},
)
modpackContentModal.value?.show(contentItems)
managedContentModal.value?.show(contentItems)
} catch (err) {
console.error('Failed to load modpack contents:', err)
modpackContentModal.value?.show([])
managedContentModal.value?.show([])
}
}
@@ -360,6 +361,10 @@ function hide() {
}
const messages = defineMessages({
modpackContent: {
id: 'app.modal.install-to-play.managed-content.modpack-header',
defaultMessage: 'Modpack content',
},
installToPlay: {
id: 'app.modal.install-to-play.header',
defaultMessage: 'Install to play',
@@ -229,11 +229,11 @@
</div>
</template>
</NewModal>
<ModpackContentModal
<ManagedContentModal
ref="contentModal"
:header="formatMessage(messages.sharedInstanceContent)"
:modpack-name="preview?.name ?? ''"
:modpack-icon-url="preview?.iconUrl ?? undefined"
:source-name="preview?.name ?? ''"
:source-icon-url="preview?.iconUrl ?? undefined"
/>
</template>
@@ -255,8 +255,8 @@ import {
injectModrinthClient,
injectNotificationManager,
IntlFormatted,
ManagedContentModal,
MarkdownEditor,
ModpackContentModal,
NewModal,
Table,
type TableColumn,
@@ -288,7 +288,7 @@ type SharedInstanceCreator = {
}
const modal = ref<InstanceType<typeof NewModal>>()
const contentModal = ref<InstanceType<typeof ModpackContentModal>>()
const contentModal = ref<InstanceType<typeof ManagedContentModal>>()
const externalFileTable = ref<HTMLElement | null>(null)
const preview = ref<SharedInstanceInstallPreview | null>(null)
const creator = ref<SharedInstanceCreator | null>(null)
@@ -1,17 +1,10 @@
import type {
ContentItem,
ContentModpackCardCategory,
ContentModpackCardProject,
ContentModpackCardVersion,
ContentOwner,
} from '@modrinth/ui'
import type { ContentItem, ManagedContentProject, ManagedContentVersion } from '@modrinth/ui'
import {
get_content_items,
get_linked_modpack_info,
type LinkedModpackInfo,
} from '@/helpers/instance'
import { get_categories } from '@/helpers/tags.js'
import type { CacheBehaviour } from '@/helpers/types'
export type InstanceContentData = {
@@ -21,11 +14,8 @@ export type InstanceContentData = {
}
export type InstanceContentModpackData = {
project: ContentModpackCardProject
version: ContentModpackCardVersion
owner: ContentOwner | null
categories: ContentModpackCardCategory[]
hasUpdate: boolean
project: ManagedContentProject
version: ManagedContentVersion
updateVersionId: string | null
}
@@ -34,19 +24,15 @@ export async function loadInstanceContentData(
cacheBehaviour?: CacheBehaviour,
onError?: (error: Error) => unknown,
): Promise<InstanceContentData> {
const [contentItems, modpackInfo, allCategories] = await Promise.all([
const [contentItems, modpackInfo] = await Promise.all([
get_content_items(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
get_linked_modpack_info(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
get_categories().catch((error) => handleLoadError(error, onError)),
])
return {
path,
contentItems: (contentItems as ContentItem[] | null | undefined) ?? null,
modpack: normalizeLinkedModpackInfo(
modpackInfo as LinkedModpackInfo | null | undefined,
allCategories as ContentModpackCardCategory[] | null | undefined,
),
modpack: normalizeLinkedModpackInfo(modpackInfo as LinkedModpackInfo | null | undefined),
}
}
@@ -58,7 +44,6 @@ function handleLoadError(error: unknown, onError?: (error: Error) => unknown) {
function normalizeLinkedModpackInfo(
modpackInfo: LinkedModpackInfo | null | undefined,
allCategories: ContentModpackCardCategory[] | null | undefined,
): InstanceContentModpackData | null {
if (!modpackInfo) return null
@@ -72,30 +57,6 @@ function normalizeLinkedModpackInfo(
...modpackInfo.version,
date_published: modpackInfo.version.date_published.toString(),
},
owner: modpackInfo.owner
? {
...modpackInfo.owner,
avatar_url: modpackInfo.owner.avatar_url ?? undefined,
}
: null,
categories: resolveLinkedModpackCategories(modpackInfo, allCategories),
hasUpdate: modpackInfo.has_update,
updateVersionId: modpackInfo.update_version_id,
}
}
function resolveLinkedModpackCategories(
modpackInfo: LinkedModpackInfo,
allCategories: ContentModpackCardCategory[] | null | undefined,
) {
if (!allCategories || !modpackInfo.project.categories) return []
const seen = new Set<string>()
return allCategories.filter((category) => {
if (modpackInfo.project.categories.includes(category.name) && !seen.has(category.name)) {
seen.add(category.name)
return true
}
return false
})
}
+42 -6
View File
@@ -5,7 +5,7 @@
*/
import type { Labrinth } from '@modrinth/api-client'
import type { ContentItem, ContentOwner } from '@modrinth/ui'
import { invoke } from '@tauri-apps/api/core'
import { convertFileSrc, invoke } from '@tauri-apps/api/core'
import type { InstallJobSnapshot, SharedInstanceUpdateDiff } from './install'
import type {
@@ -74,7 +74,11 @@ export async function get_content_items(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke('plugin:instance|instance_get_content_items', { instanceId, cacheBehaviour })
const items = await invoke<ContentItem[]>('plugin:instance|instance_get_content_items', {
instanceId,
cacheBehaviour,
})
return adaptContentItems(items)
}
export async function refresh_content_updates(instanceId: string): Promise<void> {
@@ -111,10 +115,11 @@ export async function get_linked_modpack_content(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke('plugin:instance|instance_get_linked_modpack_content', {
const items = await invoke<ContentItem[]>('plugin:instance|instance_get_linked_modpack_content', {
instanceId,
cacheBehaviour,
})
return adaptContentItems(items)
}
// Convert a list of dependencies into ContentItems with rich metadata
@@ -122,9 +127,28 @@ export async function get_dependencies_as_content_items(
dependencies: Labrinth.Versions.v3.Dependency[],
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke('plugin:instance|instance_get_dependencies_as_content_items', {
dependencies,
cacheBehaviour,
const items = await invoke<ContentItem[]>(
'plugin:instance|instance_get_dependencies_as_content_items',
{
dependencies,
cacheBehaviour,
},
)
return adaptContentItems(items)
}
function adaptContentItems(items: ContentItem[]): ContentItem[] {
return items.map((item) => {
const embeddedMetadata = item.embedded_metadata
if (!embeddedMetadata?.icon_path) return item
return {
...item,
embedded_metadata: {
...embeddedMetadata,
icon_url: convertFileSrc(embeddedMetadata.icon_path),
},
}
})
}
@@ -264,6 +288,18 @@ export async function toggle_disable_project(
})
}
export async function set_project_locked(
instanceId: string,
projectPath: string,
locked: boolean,
): Promise<void> {
return await invoke('plugin:instance|instance_set_project_locked', {
instanceId,
projectPath,
locked,
})
}
// Remove a project
export async function remove_project(instanceId: string, projectPath: string): Promise<void> {
return await invoke('plugin:instance|instance_remove_project', { instanceId, projectPath })
+1
View File
@@ -128,6 +128,7 @@ export type ContentSourceKind =
type ContentFile = {
enabled: boolean
locked: boolean
source_kind?: ContentSourceKind | null
metadata?: {
project_id: string
@@ -1073,16 +1073,7 @@
"search.filter.locked.instance.sync": {
"message": "مزامنة مع النسخة"
},
"search.filter.locked.server": {
"message": "يقدمها الخادم"
},
"search.filter.locked.server-environment.title": {
"message": "يمكن إضافة التعديلات **المحليه** فقط إلى نموذج الخادم"
},
"search.filter.locked.server-game-version.title": {
"message": "يتم توفير نسخة اللعبة من قبل الخادم"
},
"search.filter.locked.server-loader.title": {
"message": "يتم توفير المحمّل من قبل الخادم"
}
}
@@ -965,16 +965,7 @@
"search.filter.locked.instance.sync": {
"message": "Synchronizováno s instancí"
},
"search.filter.locked.server": {
"message": "Poskytováno serverem"
},
"search.filter.locked.server-environment.title": {
"message": "Pouze módy ze strany klientu mohou být přidány na server"
},
"search.filter.locked.server-game-version.title": {
"message": "Verze hry je poskytována serverem"
},
"search.filter.locked.server-loader.title": {
"message": "Loader zprostředkovává server"
}
}
@@ -296,18 +296,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Gennemså ændringer"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Gennemgå opdatering"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Gennemgår..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "En opdatering er krævet for at spille {name}. Venligst opdater til den seneste version for at køre spillet."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "En opdatering er tilgængelig"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Al' data for din instance vil blive permanent slettet, dette inkludere dine verdener, konfigurationer, og alt installeret indhold."
},
@@ -1181,16 +1172,7 @@
"search.filter.locked.instance.sync": {
"message": "Synkroniser med instance"
},
"search.filter.locked.server": {
"message": "Givet af serveren"
},
"search.filter.locked.server-environment.title": {
"message": "Kun klient-sided mods kan blive tilføjet til denne server instance"
},
"search.filter.locked.server-game-version.title": {
"message": "Spille version er givet af serveren"
},
"search.filter.locked.server-loader.title": {
"message": "Loader er givet af serveren"
}
}
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Änderungen überprüfen"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Update überprüfen"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Wird überprüft..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Ein Update ist erforderlich, um {name} zu spielen. Bitte aktualisiere auf die neueste Version, um das Spiel zu starten."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Ein Update ist verfügbar"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Alle Daten deiner Instanz werden permanent gelöscht, inlusive deiner Welten, Konfigurationen und allen installierten Inhalten."
},
@@ -1922,18 +1913,9 @@
"search.filter.locked.instance.sync": {
"message": "Mit Instanz synchronisieren"
},
"search.filter.locked.server": {
"message": "Vom Server bereitgestellt"
},
"search.filter.locked.server-environment.title": {
"message": "Nur Clientseitige Mods können der Serverinstanz hinzugefügt werden"
},
"search.filter.locked.server-game-version.title": {
"message": "Spielversion wird vom Server bereitgestellt"
},
"search.filter.locked.server-loader.title": {
"message": "Loader wird vom Server bereitgestellt"
},
"settings.sidebar.label.account": {
"message": "Konto"
},
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Änderungen überprüfen"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Update überprüfen"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Wird überprüft..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Ein Update ist erforderlich, um {name} zu spielen. Bitte aktualisiere auf die neueste Version, um das Spiel zu starten."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Ein Update ist verfügbar"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Alle Daten deiner Instanz werden permanent gelöscht, einschließlich deiner Welten, Konfigurationen und allen installierten Inhalten."
},
@@ -1922,18 +1913,9 @@
"search.filter.locked.instance.sync": {
"message": "Mit Instanz synchronisieren"
},
"search.filter.locked.server": {
"message": "Vom Server vorgegeben"
},
"search.filter.locked.server-environment.title": {
"message": "Nur clientseitige Mods können der Serverinstanz hinzugefügt werden"
},
"search.filter.locked.server-game-version.title": {
"message": "Spielversion vom Server vorgegeben"
},
"search.filter.locked.server-loader.title": {
"message": "Loader vom Server vorgegeben"
},
"settings.sidebar.label.account": {
"message": "Konto"
},
+19 -19
View File
@@ -273,7 +273,7 @@
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Adding content can break compatibility when joining the server. Any added content will also be lost when you update the server instance content."
"message": "Adding content may prevent you from joining this server. Any content you add will be removed when the managed server content is updated."
},
"app.browse.server.installing": {
"message": "Installing"
@@ -395,18 +395,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Review changes"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Review update"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Reviewing..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "An update is required to play {name}. Please update to latest version to launch the game."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "An update is available"
},
"app.instance.confirm-delete.admonition-body": {
"message": "All data for your instance will be permanently deleted, including your worlds, configs, and all installed content."
},
@@ -419,6 +410,12 @@
"app.instance.confirm-delete.header": {
"message": "Delete instance"
},
"app.instance.content.managed-content.modpack-header": {
"message": "Modpack content"
},
"app.instance.content.managed-content.shared-header": {
"message": "Shared content"
},
"app.instance.modpack-already-installed.body": {
"message": "This modpack is already installed in the <bold>{instanceName}</bold> instance. Are you sure you want to duplicate it?"
},
@@ -443,6 +440,9 @@
"app.instance.mods.content-type-project": {
"message": "project"
},
"app.instance.mods.freeze-content": {
"message": "Freeze version"
},
"app.instance.mods.locked-content": {
"message": "Content in locked instances cannot be changed."
},
@@ -461,6 +461,9 @@
"app.instance.mods.successfully-uploaded": {
"message": "Successfully uploaded"
},
"app.instance.mods.unfreeze-content": {
"message": "Unfreeze version"
},
"app.instance.share.empty.description": {
"message": "You can share this instance with your friends!"
},
@@ -704,6 +707,9 @@
"app.modal.install-to-play.invite-warning-with-creator": {
"message": "This invite was created by <creator>{username}</creator>, not Modrinth. Only accept invites from people you trust."
},
"app.modal.install-to-play.managed-content.modpack-header": {
"message": "Modpack content"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# mods}}"
},
@@ -1475,6 +1481,9 @@
"instance.files.save-as": {
"message": "Save as..."
},
"instance.last-played": {
"message": "Last played"
},
"instance.locked.delete-button": {
"message": "Delete instance"
},
@@ -1940,18 +1949,9 @@
"search.filter.locked.instance.sync": {
"message": "Sync with instance"
},
"search.filter.locked.server": {
"message": "Provided by the server"
},
"search.filter.locked.server-environment.title": {
"message": "Only client-side mods can be added to the server instance"
},
"search.filter.locked.server-game-version.title": {
"message": "Game version is provided by the server"
},
"search.filter.locked.server-loader.title": {
"message": "Loader is provided by the server"
},
"settings.sidebar.label.account": {
"message": "Account"
},
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Revisar cambios"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Actualizar"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Actualizando..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Se necesita una actualización para jugar a {name}. Por favor actualiza a la versión más reciente para iniciar el juego."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Hay una actualización disponible"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Todos los datos de tu instancia se eliminarán permanentemente, incluidos tus mundos, configuraciones y todo el contenido instalado."
},
@@ -1922,18 +1913,9 @@
"search.filter.locked.instance.sync": {
"message": "Sincronizar con la instancia"
},
"search.filter.locked.server": {
"message": "Proporcionado por el servidor"
},
"search.filter.locked.server-environment.title": {
"message": "Solo se pueden añadir mods que sean del lado del cliente a la instancia del servidor"
},
"search.filter.locked.server-game-version.title": {
"message": "La versión del juego es proporcionada por el servidor"
},
"search.filter.locked.server-loader.title": {
"message": "El loader es proporcionado por el servidor"
},
"settings.sidebar.label.account": {
"message": "Cuenta"
},
@@ -878,16 +878,7 @@
"search.filter.locked.instance.sync": {
"message": "Sincronizar con la instancia"
},
"search.filter.locked.server": {
"message": "Proporcionado por el servidor"
},
"search.filter.locked.server-environment.title": {
"message": "Solo se pueden añadir a la instancia del servidor modificaciones del lado del cliente"
},
"search.filter.locked.server-game-version.title": {
"message": "La versión del juego es proporcionada por el servidor"
},
"search.filter.locked.server-loader.title": {
"message": "Loader proporcionado por el servidor"
}
}
@@ -959,16 +959,7 @@
"search.filter.locked.instance.sync": {
"message": "Synkronoi instanssin kanssa"
},
"search.filter.locked.server": {
"message": "Palvelimen tarjoama"
},
"search.filter.locked.server-environment.title": {
"message": "Voit lisätä vain paikallisia modeja palvelin instanssiin"
},
"search.filter.locked.server-game-version.title": {
"message": "Peliversio on palvelimen tarjoama"
},
"search.filter.locked.server-loader.title": {
"message": "Modialusta on palvelimen tarjoama"
}
}
@@ -659,16 +659,7 @@
"search.filter.locked.instance.sync": {
"message": "Maki-sync sa instansiya"
},
"search.filter.locked.server": {
"message": "Handog ng server"
},
"search.filter.locked.server-environment.title": {
"message": "Mga mod sa panig ng client lamang ang maidadaragdag sa instansiyang server"
},
"search.filter.locked.server-game-version.title": {
"message": "Ang bersiyon ng laro ay handog ng server"
},
"search.filter.locked.server-loader.title": {
"message": "Ang loader ay handog na ng server"
}
}
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Examiner les modifications"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Télécharger la mise à jour"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Examination..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Une mise à jour est nécessaire pour jouer à {name}. Veuillez mettre à jour vers la dernière version pour lancer le jeu."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Une mise à jour est disponible"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Toutes les données pour votre instance seront supprimées à jamais, y compris vos mondes, vos configurations, et le contenu installé."
},
@@ -1919,18 +1910,9 @@
"search.filter.locked.instance.sync": {
"message": "Synchroniser avec l'instance"
},
"search.filter.locked.server": {
"message": "Fournis par le serveur"
},
"search.filter.locked.server-environment.title": {
"message": "Seuls les mods client peuvent être ajoutés à l'instance du serveur"
},
"search.filter.locked.server-game-version.title": {
"message": "Version du jeu est procurée par le serveur"
},
"search.filter.locked.server-loader.title": {
"message": "Le loader est procuré par le serveur"
},
"settings.sidebar.label.account": {
"message": "Compte"
},
@@ -533,16 +533,7 @@
"search.filter.locked.instance.sync": {
"message": "סנכרן עם התקנה"
},
"search.filter.locked.server": {
"message": "מסופק על ידי השרת"
},
"search.filter.locked.server-environment.title": {
"message": "ניתן להוסיף רק מודים בצד הלקוח לשרת"
},
"search.filter.locked.server-game-version.title": {
"message": "גרסת המשחק מסופקת על ידי השרת"
},
"search.filter.locked.server-loader.title": {
"message": "הטוען מסופק על ידי השרת"
}
}
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Változtatások áttekintése"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Frissítés áttekintése"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Áttekintés..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Egy frssítés szükséges a(z) {name} példányhoz. Kérjük, frissítsd a játékot a legújabb verzióra az indításhoz."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Frissítés érhető el"
},
"app.instance.confirm-delete.admonition-body": {
"message": "A példányodhoz tartozó összes adat véglegesen törlődik, beleértve a világjaidat, a beállításaidat és az összes telepített tartalmat."
},
@@ -1907,18 +1898,9 @@
"search.filter.locked.instance.sync": {
"message": "Szinkronizálás a példánnyal"
},
"search.filter.locked.server": {
"message": "A szerver által van megadva"
},
"search.filter.locked.server-environment.title": {
"message": "Szerverpéldányokhoz csak szerveroldali modok adhatók hozzá"
},
"search.filter.locked.server-game-version.title": {
"message": "A játékverzió a szerver által van megadva"
},
"search.filter.locked.server-loader.title": {
"message": "A betöltőt az adott szerver biztosítja"
},
"settings.sidebar.label.account": {
"message": "Fiók"
},
@@ -821,16 +821,7 @@
"search.filter.locked.instance.sync": {
"message": "Sinkronkan dengan instans"
},
"search.filter.locked.server": {
"message": "Disediakan oleh server"
},
"search.filter.locked.server-environment.title": {
"message": "Anda hanya dapat menambahkan mod sisi klien pada instans server"
},
"search.filter.locked.server-game-version.title": {
"message": "Versi permainan disediakan oleh server"
},
"search.filter.locked.server-loader.title": {
"message": "Pemuat disediakan oleh server"
}
}
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Revisiona modifiche"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Revisiona aggiornamento"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Revisione..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "{name} richiede degli aggiornamenti. Installa l'ultima versione per poter giocare."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Aggiornamento disponibile"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Tutti i dati della tua istanza verranno eliminati permanentemente, inclusi i tuoi mondi, configurazioni e tutti i contenuti installati."
},
@@ -1919,18 +1910,9 @@
"search.filter.locked.instance.sync": {
"message": "Sincronizza con l'istanza"
},
"search.filter.locked.server": {
"message": "Determinato dal server"
},
"search.filter.locked.server-environment.title": {
"message": "Solo mod lato client possono essere aggiunte all'istanza del server"
},
"search.filter.locked.server-game-version.title": {
"message": "La versione del gioco è determinata dal server"
},
"search.filter.locked.server-loader.title": {
"message": "Il loader è determinato dal server"
},
"settings.sidebar.label.account": {
"message": "Account"
},
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "変更点を確認"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "アップデート内容を確認"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "審査中…"
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "{name}をプレイするにはアップデートが必要です。ゲームを起動するには最新バージョンにアップデートしてください。"
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "アップデートが利用可能です"
},
"app.instance.confirm-delete.admonition-body": {
"message": "インスタンス内のすべてのデータは、ワールド、設定、インストール済みのコンテンツを含め、完全に削除されます。"
},
@@ -1922,18 +1913,9 @@
"search.filter.locked.instance.sync": {
"message": "インスタンスと同期"
},
"search.filter.locked.server": {
"message": "サーバーによる条件"
},
"search.filter.locked.server-environment.title": {
"message": "サーバーインスタンスにはクライアント側Modのみ追加可能"
},
"search.filter.locked.server-game-version.title": {
"message": "ゲームバージョンはサーバーによる条件です"
},
"search.filter.locked.server-loader.title": {
"message": "ローダーはサーバーによる条件です"
},
"settings.sidebar.label.account": {
"message": "アカウント"
},
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Review changes"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "업데이트 검토"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "검토중..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "{name} 플레이를 위해 업데이트가 필요합니다. 게임을 실행하려면 최신 버전 업데이트를 진행하세요."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "업데이트가 있습니다"
},
"app.instance.confirm-delete.admonition-body": {
"message": "인스턴스의 모든 데이터가 삭제됩니다. 세계 폴더, 설정 폴더 등 설치된 모든 컨텐츠가 포함됩니다."
},
@@ -1895,18 +1886,9 @@
"search.filter.locked.instance.sync": {
"message": "인스턴스와 동기화"
},
"search.filter.locked.server": {
"message": "서버에 의해 관리됨"
},
"search.filter.locked.server-environment.title": {
"message": "서버 인스턴스에는 클라이언트 전용 모드만 추가할 수 있습니다"
},
"search.filter.locked.server-game-version.title": {
"message": "게임 버전이 서버에 의해 제공됩니다"
},
"search.filter.locked.server-loader.title": {
"message": "로더가 서버에 의해 제공됩니다"
},
"settings.sidebar.label.account": {
"message": "계정"
},
@@ -818,16 +818,7 @@
"search.filter.locked.instance.sync": {
"message": "Selaraskan dengan pemasangan"
},
"search.filter.locked.server": {
"message": "Disediakan oleh pelayan"
},
"search.filter.locked.server-environment.title": {
"message": "Hanya mod bahagian pelanggan sahaja yang boleh ditambah pada pemasangan pelayan"
},
"search.filter.locked.server-game-version.title": {
"message": "Versi permainan adalah disediakan oleh pelayan"
},
"search.filter.locked.server-loader.title": {
"message": "Pemuat adalah disediakan oleh pelayan"
}
}
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Wijzigingen bekijken"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Update bekijken"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Controleren..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Er is een update nodig om {name} te kunnen spelen. Werk het spel bij naar de nieuwste versie om het te kunnen starten."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Update is beschikbaar"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Alle gegevens van je instantie worden definitief verwijderd, inclusief je werelden, configuraties en alle geïnstalleerde inhoud."
},
@@ -1922,18 +1913,9 @@
"search.filter.locked.instance.sync": {
"message": "Synchroniseer installatie"
},
"search.filter.locked.server": {
"message": "Aangeleverd door de server"
},
"search.filter.locked.server-environment.title": {
"message": "Alleen client-side mods kunnen toegevoegd worden aan de server instantie"
},
"search.filter.locked.server-game-version.title": {
"message": "Spel versie is gegeven door de server"
},
"search.filter.locked.server-loader.title": {
"message": "Loader is gegeven door de server"
},
"settings.sidebar.label.account": {
"message": "Account"
},
@@ -371,15 +371,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Sprawdź zmiany"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Sprawdź aktualizację"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Trwa przegląd..."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Dostępna aktualizacja"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Wszystkie dane z Twojej instancji zostaną trwale usunięte, w tym Twoje światy, pliki konfiguracji i jakakolwiek dodana zawartość."
},
@@ -1703,18 +1697,9 @@
"search.filter.locked.instance.sync": {
"message": "Synchronizuj z instancją"
},
"search.filter.locked.server": {
"message": "Dostarczone przez serwer"
},
"search.filter.locked.server-environment.title": {
"message": "Tylko mody po stronie klienta mogą być dodane do instancji serwera"
},
"search.filter.locked.server-game-version.title": {
"message": "Wersja gry jest dostarczona przez serwer"
},
"search.filter.locked.server-loader.title": {
"message": "Loader jest dostarczony przez serwer"
},
"settings.sidebar.label.account": {
"message": "Konto"
},
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Revisar alterações"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Revisar atualização"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Revisando..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Uma atualização é necessária para jogar {name}. Atualize para a versão mais recente para iniciar o jogo."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Uma atualização está disponível"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Todos os dados para sua instância serão excluídos permanentemente, incluindo seus mundos, configs e todo o conteúdo instalado."
},
@@ -1922,18 +1913,9 @@
"search.filter.locked.instance.sync": {
"message": "Sincronizar com a instância"
},
"search.filter.locked.server": {
"message": "Fornecido pelo servidor"
},
"search.filter.locked.server-environment.title": {
"message": "Somente mods do cliente podem ser adicionados à instância do servidor"
},
"search.filter.locked.server-game-version.title": {
"message": "A versão do jogo é fornecida pelo servidor"
},
"search.filter.locked.server-loader.title": {
"message": "O loader é fornecido pelo servidor"
},
"settings.sidebar.label.account": {
"message": "Conta"
},
@@ -872,16 +872,7 @@
"search.filter.locked.instance.sync": {
"message": "Sincronizar com a instância"
},
"search.filter.locked.server": {
"message": "Fornecido pelo servidor"
},
"search.filter.locked.server-environment.title": {
"message": "Apenas mods no cliente podem ser adicionados à instância de servidor"
},
"search.filter.locked.server-game-version.title": {
"message": "Versão do jogo é fornecida pelo servidor"
},
"search.filter.locked.server-loader.title": {
"message": "Carregador é fornecido pelo servidor"
}
}
@@ -506,16 +506,7 @@
"search.filter.locked.instance.sync": {
"message": "Sincronizează cu instanța"
},
"search.filter.locked.server": {
"message": "Furnizat de server"
},
"search.filter.locked.server-environment.title": {
"message": "Numai modurile de pe partea clientului pot fi adăugate la instanța serverului"
},
"search.filter.locked.server-game-version.title": {
"message": "Versiunea jocului este furnizată de server"
},
"search.filter.locked.server-loader.title": {
"message": "Încărcătorul este furnizat de server"
}
}
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Проверьте изменения"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Обзор обновления"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Проверка..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Необходимо обновление для игры в {name}. Обновитесь до последней версии и запустите игру."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Доступно обновление"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Все данные сборки будут удалены навсегда, в том числе миры, настройки и весь установленный контент."
},
@@ -1910,18 +1901,9 @@
"search.filter.locked.instance.sync": {
"message": "Использовать из сборки"
},
"search.filter.locked.server": {
"message": "Управляется сервером"
},
"search.filter.locked.server-environment.title": {
"message": "В серверной сборке доступны только клиентские моды"
},
"search.filter.locked.server-game-version.title": {
"message": "Версия игры управляется сервером"
},
"search.filter.locked.server-loader.title": {
"message": "Загрузчик управляется сервером"
},
"settings.sidebar.label.account": {
"message": "Аккаунт"
},
@@ -1100,16 +1100,7 @@
"search.filter.locked.instance.sync": {
"message": "Sinhroniziraj sa instancom"
},
"search.filter.locked.server": {
"message": "Obezbeđeno od servera"
},
"search.filter.locked.server-environment.title": {
"message": "Samo modovi za kliente mogu biti dodani na instancu servera"
},
"search.filter.locked.server-game-version.title": {
"message": "Verzije igre je obezbeđena od servera"
},
"search.filter.locked.server-loader.title": {
"message": "Učitavač je obezbeđen od servera"
}
}
@@ -359,18 +359,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Granskar ändringar"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Granska uppdatering"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Granskar..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "En uppdatering krävs för att spela {name}. Vänligen uppdatera till den senaste verisoner för att köra spelet."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "En uppdatering finns tillgänglig"
},
"app.instance.confirm-delete.admonition-body": {
"message": "All data från din instans kommer permanent raderas, däribland dina världar, konfigurationer samt allt installerat innehåll."
},
@@ -1718,18 +1709,9 @@
"search.filter.locked.instance.sync": {
"message": "Synkronisera med instansen"
},
"search.filter.locked.server": {
"message": "Tillhandahållet av servern"
},
"search.filter.locked.server-environment.title": {
"message": "Endast klient-sido moddar kan läggas till i serverinstansen"
},
"search.filter.locked.server-game-version.title": {
"message": "Spelversion tillhandahålls av servern"
},
"search.filter.locked.server-loader.title": {
"message": "Loader tillhandahålls av servern"
},
"settings.sidebar.label.account": {
"message": "Konto"
},
@@ -785,16 +785,7 @@
"search.filter.locked.instance.sync": {
"message": "เชื่อมต่อกับโปรแกรม"
},
"search.filter.locked.server": {
"message": "เซิร์ฟเวอร์เป็นผู้กำหนด"
},
"search.filter.locked.server-environment.title": {
"message": "มีเพียงม็อดสำหรับฝั่งเครื่องของผู้เล่นเท่านั้นที่สามารถเพิ่มลงในโปรแกรมเซิร์ฟเวอร์ดังกล่าวได้"
},
"search.filter.locked.server-game-version.title": {
"message": "เวอร์ชันเกมถูกกำหนดโดยเซิร์ฟเวอร์แล้ว"
},
"search.filter.locked.server-loader.title": {
"message": "ตัวรันถูกกำหนดโดยเซิร์ฟเวอร์แล้ว"
}
}
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Değişimleri inceleniyor"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Güncellemeyi gözden geçir"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "İnceleniyor..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "{name}'i oynamak için bir güncelleme gerekli. Oyunu açabilmek için lütfen son sürüme güncelleyin."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Bir güncelleme mevcut"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Dünyalarınız, yapılandırmalarınız ve yüklü tüm içerikler ve tüm veriler kalıcı olarak silinecek."
},
@@ -1508,18 +1499,9 @@
"search.filter.locked.instance.sync": {
"message": "Kurulum ile eşitle"
},
"search.filter.locked.server": {
"message": "Sunucu tarafından sağlanmıştır"
},
"search.filter.locked.server-environment.title": {
"message": "Sunucu örneğine yalnızca istemci tarafında çalışan modlar eklenebilir"
},
"search.filter.locked.server-game-version.title": {
"message": "Oyun sürümü sunucu tarafından sağlanıyor"
},
"search.filter.locked.server-loader.title": {
"message": "Yükleyici sunucu tarafından sağlanıyor"
},
"settings.sidebar.label.account": {
"message": "Hesap"
},
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Огляд змін"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Переглянути оновлення"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Оглядаємо…"
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Для гри в {name} потрібне оновлення. Будь ласка, оновіть до найновішої версії, щоб запустити гру."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Доступне оновлення"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Усі дані вашого профілю будуть видалені назавжди, включно з вашими світами, налаштуваннями та всім установленим умістом."
},
@@ -1922,18 +1913,9 @@
"search.filter.locked.instance.sync": {
"message": "Синхронізувати з профілем"
},
"search.filter.locked.server": {
"message": "Надано сервером"
},
"search.filter.locked.server-environment.title": {
"message": "До профілю сервера можна додавати лише клієнтські моди"
},
"search.filter.locked.server-game-version.title": {
"message": "Версія гри надана сервером"
},
"search.filter.locked.server-loader.title": {
"message": "Завантажувач наданий сервером"
},
"settings.sidebar.label.account": {
"message": "Обліковий запис"
},
@@ -998,16 +998,7 @@
"search.filter.locked.instance.sync": {
"message": "Đồng bộ với phiên bản"
},
"search.filter.locked.server": {
"message": "Do máy chủ cung cấp"
},
"search.filter.locked.server-environment.title": {
"message": "Chỉ các mod chạy phía client mới có thể thêm hồ sơ máy chủ"
},
"search.filter.locked.server-game-version.title": {
"message": "Phiên bản trò chơi được cung cấp bởi máy chủ"
},
"search.filter.locked.server-loader.title": {
"message": "Loader được cung cấp bởi máy chủ"
}
}
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "查看变更"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "查看更新"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "正在审查……"
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "{name}需要更新。请更新到最新版本以启动游戏。"
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "有更新可用"
},
"app.instance.confirm-delete.admonition-body": {
"message": "你实例的所有数据将被永久删除,包括你的世界、配置和所有已安装的内容。"
},
@@ -1922,18 +1913,9 @@
"search.filter.locked.instance.sync": {
"message": "与实例同步"
},
"search.filter.locked.server": {
"message": "由该服务器提供"
},
"search.filter.locked.server-environment.title": {
"message": "只能将客户端模组添加到服务器实例中"
},
"search.filter.locked.server-game-version.title": {
"message": "游戏版本由服务器提供"
},
"search.filter.locked.server-loader.title": {
"message": "加载器由服务器提供"
},
"settings.sidebar.label.account": {
"message": "账户"
},
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "檢視變更"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "檢視更新"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "正在檢視..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "需要更新才能遊玩「{name}」。請更新至最新版本以啟動遊戲。"
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "有可用的更新"
},
"app.instance.confirm-delete.admonition-body": {
"message": "你實例中的所有資料將被永久刪除,包含你的世界、設定檔以及所有已安裝的內容。"
},
@@ -1922,18 +1913,9 @@
"search.filter.locked.instance.sync": {
"message": "與實例同步"
},
"search.filter.locked.server": {
"message": "由伺服器提供"
},
"search.filter.locked.server-environment.title": {
"message": "只有用戶端模組可以被加到伺服器實例"
},
"search.filter.locked.server-game-version.title": {
"message": "遊戲版本由伺服器提供"
},
"search.filter.locked.server-loader.title": {
"message": "載入器由伺服器提供"
},
"settings.sidebar.label.account": {
"message": "帳號"
},
+11 -31
View File
@@ -399,9 +399,10 @@ const hideInstalledModpacks = computed({
const instanceFilters = computed(() => {
const filters = []
if (instance.value) {
if (instance.value && projectType.value !== 'resourcepack') {
const isVanillaShader = projectType.value === 'shader' && instance.value.loader === 'vanilla'
const gameVersion = instance.value.game_version
if (gameVersion) {
if (gameVersion && !isVanillaShader) {
filters.push({ type: 'game_version', option: gameVersion })
}
@@ -411,6 +412,9 @@ const instanceFilters = computed(() => {
if (platform && projectType.value === 'mod' && supportedModLoaders.includes(platform)) {
filters.push({ type: 'mod_loader', option: platform })
}
if (isVanillaShader) {
filters.push({ type: 'shader_loader', option: 'vanilla' })
}
if (isServerInstance.value) {
filters.push({ type: 'environment', option: 'client' })
@@ -550,10 +554,6 @@ const messages = defineMessages({
id: 'search.filter.locked.instance-game-version.title',
defaultMessage: 'Game version is provided by the instance',
},
gameVersionProvidedByServer: {
id: 'search.filter.locked.server-game-version.title',
defaultMessage: 'Game version is provided by the server',
},
hideAddedServers: {
id: 'app.browse.hide-added-servers',
defaultMessage: 'Hide servers already added',
@@ -573,7 +573,7 @@ const messages = defineMessages({
serverInstanceContentWarning: {
id: 'app.browse.server-instance-content-warning',
defaultMessage:
'Adding content can break compatibility when joining the server. Any added content will also be lost when you update the server instance content.',
'Adding content may prevent you from joining this server. Any content you add will be removed when the managed server content is updated.',
},
modLoaderProvidedByInstance: {
id: 'search.filter.locked.instance-loader.title',
@@ -583,18 +583,10 @@ const messages = defineMessages({
id: 'app.browse.project-type.modpacks',
defaultMessage: 'Modpacks',
},
modLoaderProvidedByServer: {
id: 'search.filter.locked.server-loader.title',
defaultMessage: 'Loader is provided by the server',
},
providedByInstance: {
id: 'search.filter.locked.instance',
defaultMessage: 'Provided by the instance',
},
providedByServer: {
id: 'search.filter.locked.server',
defaultMessage: 'Provided by the server',
},
syncFilterButton: {
id: 'search.filter.locked.instance.sync',
defaultMessage: 'Sync with instance',
@@ -743,7 +735,7 @@ const installContext = computed(() => {
isFromWorlds.value ? messages.addServersToInstance : commonMessages.installingContentLabel,
),
warning:
isServerInstance.value && !isFromWorlds.value
isServerInstance.value && instance.value.loader !== 'vanilla' && !isFromWorlds.value
? formatMessage(messages.serverInstanceContentWarning)
: undefined,
}
@@ -1106,24 +1098,12 @@ async function search(requestParams: string) {
}
}
const isServerFilterContext = computed(() => isServerContext.value || isServerInstance.value)
const lockedFilterMessages = computed(() => ({
gameVersion: formatMessage(
isServerFilterContext.value
? messages.gameVersionProvidedByServer
: messages.gameVersionProvidedByInstance,
),
modLoader: formatMessage(
isServerFilterContext.value
? messages.modLoaderProvidedByServer
: messages.modLoaderProvidedByInstance,
),
gameVersion: formatMessage(messages.gameVersionProvidedByInstance),
modLoader: formatMessage(messages.modLoaderProvidedByInstance),
environment: formatMessage(messages.environmentProvidedByServer),
syncButton: formatMessage(messages.syncFilterButton),
providedBy: formatMessage(
isServerFilterContext.value ? messages.providedByServer : messages.providedByInstance,
),
providedBy: formatMessage(messages.providedByInstance),
}))
const searchState = useBrowseSearch({
@@ -6,11 +6,6 @@
:instance="instance"
@published="emit('published')"
/>
<InstanceAdmonitionsSharedInstanceUpdateAvailable
v-else-if="item.kind === 'shared-instance-update-available'"
:instance-name="instance.name"
@review="emit('review-update', $event)"
/>
<InstanceAdmonitionsSharedInstanceWrongAccount
v-else-if="item.kind === 'shared-instance-wrong-account'"
:expected-user-id="sharedInstanceExpectedUserId"
@@ -38,7 +33,6 @@ import type { GameInstance } from '@/helpers/types'
import InstanceAdmonitionsSharedInstanceStale from './shared-instance-stale.vue'
import InstanceAdmonitionsSharedInstanceUnavailable from './shared-instance-unavailable.vue'
import InstanceAdmonitionsSharedInstanceUpdateAvailable from './shared-instance-update-available.vue'
import InstanceAdmonitionsSharedInstanceWrongAccount from './shared-instance-wrong-account.vue'
import type { InstanceAdmonitionItem, SharedInstanceRole } from './types.ts'
@@ -54,13 +48,11 @@ const props = defineProps<{
sharedInstanceExpectedUserId?: string | null
sharedInstanceRole?: SharedInstanceRole | null
sharedInstanceSignedOut?: boolean
sharedInstanceUpdateAvailable?: boolean
}>()
const emit = defineEmits<{
published: []
delete: []
'review-update': [event: MouseEvent]
}>()
const sharedInstanceWrongAccount = computed(() => props.sharedInstanceWrongAccount ?? false)
@@ -76,15 +68,6 @@ const showSharedInstancePublishAdmonition = computed(
props.instance.shared_instance?.role === 'owner' &&
props.instance.shared_instance.status === 'stale',
)
const showSharedInstanceUpdateAdmonition = computed(
() =>
!sharedInstanceWrongAccount.value &&
!displayedSharedInstanceUnavailableReason.value &&
props.instance.install_stage === 'installed' &&
props.sharedInstanceRole === 'member' &&
props.sharedInstanceUpdateAvailable === true,
)
const stackItems = computed<InstanceAdmonitionItem[]>(() => {
const items: InstanceAdmonitionItem[] = []
@@ -120,15 +103,6 @@ const stackItems = computed<InstanceAdmonitionItem[]>(() => {
})
}
if (showSharedInstanceUpdateAdmonition.value) {
items.push({
id: 'shared-instance-update-available',
type: 'info',
dismissible: false,
kind: 'shared-instance-update-available',
})
}
return items
})
@@ -21,19 +21,6 @@ export const instanceAdmonitionsMessages = defineMessages({
id: 'app.instance.admonitions.shared-instance.reviewing-button',
defaultMessage: 'Reviewing...',
},
sharedInstanceUpdateAvailableHeader: {
id: 'app.instance.admonitions.shared-instance.update-available-header',
defaultMessage: 'An update is available',
},
sharedInstanceUpdateAvailableBody: {
id: 'app.instance.admonitions.shared-instance.update-available-body',
defaultMessage:
'An update is required to play {name}. Please update to latest version to launch the game.',
},
sharedInstanceReviewUpdateButton: {
id: 'app.instance.admonitions.shared-instance.review-update-button',
defaultMessage: 'Review update',
},
sharedInstanceReviewHeader: {
id: 'app.instance.admonitions.shared-instance.review-header',
defaultMessage: 'Review changes',
@@ -1,32 +0,0 @@
<template>
<Admonition
type="info"
inline-actions
:header="formatMessage(messages.sharedInstanceUpdateAvailableHeader)"
>
{{ formatMessage(messages.sharedInstanceUpdateAvailableBody, { name: instanceName }) }}
<template #actions>
<Button type="colored" color="blue" size="lg" @click="emit('review', $event)">
<DownloadIcon aria-hidden="true" />
{{ formatMessage(messages.sharedInstanceReviewUpdateButton) }}
</Button>
</template>
</Admonition>
</template>
<script setup lang="ts">
import { DownloadIcon } from '@modrinth/assets'
import { Admonition, Button, useVIntl } from '@modrinth/ui'
import { instanceAdmonitionsMessages as messages } from './messages'
defineProps<{
instanceName: string
}>()
const emit = defineEmits<{
review: [event: MouseEvent]
}>()
const { formatMessage } = useVIntl()
</script>
@@ -2,7 +2,6 @@ import type { StackedAdmonitionItem } from '@modrinth/ui'
export type InstanceAdmonitionKind =
| 'shared-instance-stale'
| 'shared-instance-update-available'
| 'shared-instance-unavailable'
| 'shared-instance-wrong-account'
@@ -25,60 +25,39 @@
</template>
<template #metadata>
<div v-if="isServerInstance" class="flex flex-wrap items-center gap-2">
<InstanceHeaderServerMetadata
:loading-server-ping="loadingServerPing"
:players-online="playersOnline"
:status-online="statusOnline"
:recent-plays="recentPlays"
:ping="ping"
:minecraft-server="minecraftServer"
:linked-project-v3="linkedProjectV3"
:instance-id="instance.id"
/>
<PageHeaderMetadataItem v-if="sharedInstanceManager" :action="sharedInstanceManagerAction">
{{ sharedInstanceManagerLabel }}
<Avatar
:src="sharedInstanceManager.avatarUrl"
:alt="sharedInstanceManager.name"
:tint-by="sharedInstanceManager.tintBy"
size="24px"
:circle="sharedInstanceManager.type === 'user'"
no-shadow
/>
<span class="min-w-0 truncate">{{ sharedInstanceManager.name }}</span>
</PageHeaderMetadataItem>
</div>
<InstanceHeaderServerMetadata
v-if="isServerInstance"
:loading-server-ping="loadingServerPing"
:players-online="playersOnline"
:status-online="statusOnline"
:ping="ping"
:minecraft-server="minecraftServer"
:show-instance-play-time="showInstancePlayTime"
:playtime-label="playtimeLabel"
/>
<PageHeaderMetadata v-else>
<PageHeaderMetadataItem :icon="Gamepad2Icon" tooltip="Minecraft version">
Minecraft {{ instance.game_version }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="sharedInstanceManager?.type !== 'user'"
:icon="TagIcon"
:icon-props="{ tag: loaderDisplayName, enforceType: 'loader' }"
tooltip="Mod loader"
tooltip="Mod loader and Minecraft version"
>
{{ loaderLabel }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="showInstancePlayTime"
v-if="showInstancePlayTime && playtimeLabel"
:icon="TimerIcon"
tooltip="Total playtime"
>
{{ playtimeLabel }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem v-if="sharedInstanceManager" :action="sharedInstanceManagerAction">
{{ sharedInstanceManagerLabel }}
<Avatar
:src="sharedInstanceManager.avatarUrl"
:alt="sharedInstanceManager.name"
:tint-by="sharedInstanceManager.tintBy"
size="24px"
:circle="sharedInstanceManager.type === 'user'"
no-shadow
/>
<span class="min-w-0 truncate">{{ sharedInstanceManager.name }}</span>
<PageHeaderMetadataTimeItem
v-if="instance.last_played"
:icon="ClockIcon"
:date="instance.last_played"
:label="formatMessage(messages.lastPlayed)"
/>
<PageHeaderMetadataItem v-else :icon="ClockIcon" tooltip="Last played">
{{ formatMessage(messages.neverPlayed) }}
</PageHeaderMetadataItem>
</PageHeaderMetadata>
</template>
@@ -185,6 +164,7 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
ClockIcon,
DownloadIcon,
ExternalIcon,
FolderOpenIcon,
@@ -195,7 +175,6 @@ import {
ReportIcon,
SettingsIcon,
StopCircleIcon,
TagCategoryGamepad2Icon as Gamepad2Icon,
TimerIcon,
UnknownIcon,
} from '@modrinth/assets'
@@ -211,12 +190,12 @@ import {
PageHeaderBadgeItem,
PageHeaderMetadata,
PageHeaderMetadataItem,
PageHeaderMetadataTimeItem,
type ServerLoader,
TagIcon,
useVIntl,
} from '@modrinth/ui'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import type { GameInstance } from '@/helpers/types'
@@ -247,6 +226,10 @@ const messages = defineMessages({
id: 'instance.playtime.never-played',
defaultMessage: 'Never played',
},
lastPlayed: {
id: 'instance.last-played',
defaultMessage: 'Last played',
},
openFolder: {
id: 'instance.action.open-folder',
defaultMessage: 'Open folder',
@@ -276,7 +259,6 @@ const messages = defineMessages({
defaultMessage: "This instance's content is being shared to other users.",
},
})
const router = useRouter()
const props = withDefaults(
defineProps<{
@@ -291,16 +273,8 @@ const props = withDefaults(
loadingServerPing?: boolean
playersOnline?: number
statusOnline?: boolean
recentPlays?: number
ping?: number
minecraftServer?: Labrinth.Projects.v3.Project['minecraft_server']
linkedProjectV3?: Labrinth.Projects.v3.Project
sharedInstanceManager?: {
type: 'user' | 'server'
name: string
avatarUrl?: string
tintBy: string
} | null
}>(),
{
iconSrc: null,
@@ -313,11 +287,8 @@ const props = withDefaults(
loadingServerPing: false,
playersOnline: undefined,
statusOnline: false,
recentPlays: undefined,
ping: undefined,
minecraftServer: undefined,
linkedProjectV3: undefined,
sharedInstanceManager: null,
},
)
@@ -346,7 +317,7 @@ const { formatMessage } = useVIntl()
const isInstalling = computed(() => installingStages.includes(props.instance.install_stage))
const loaderDisplayName = computed(() => formatLoaderLabel(props.instance.loader) as ServerLoader)
const loaderLabel = computed(() =>
[loaderDisplayName.value, props.instance.loader_version].filter(Boolean).join(' '),
[loaderDisplayName.value, props.instance.game_version].filter(Boolean).join(' '),
)
const sharedInstanceTooltip = computed(() =>
formatMessage(
@@ -355,29 +326,23 @@ const sharedInstanceTooltip = computed(() =>
: messages.sharedInstanceTooltip,
),
)
const sharedInstanceManagerLabel = computed(() =>
props.sharedInstanceManager?.type === 'server' ? 'Linked to' : 'Managed by',
)
const sharedInstanceManagerAction = computed(() => {
const manager = props.sharedInstanceManager
if (manager?.type !== 'user') return undefined
return () => router.push(`/user/${encodeURIComponent(manager.name)}`)
})
const playtimeLabel = computed(() => {
if (props.timePlayed <= 0) return formatMessage(messages.neverPlayed)
const seconds = Math.floor(props.timePlayed)
if (seconds <= 0) {
return undefined
}
const hours = Math.floor(props.timePlayed / 3600)
const hours = Math.floor(seconds / 3600)
if (hours >= 1) {
return `${hours} hour${hours > 1 ? 's' : ''}`
}
const minutes = Math.floor(props.timePlayed / 60)
const minutes = Math.floor(seconds / 60)
if (minutes >= 1) {
return `${minutes} minute${minutes > 1 ? 's' : ''}`
}
const seconds = Math.floor(props.timePlayed)
return `${seconds} second${seconds > 1 ? 's' : ''}`
return `${seconds} second${seconds === 1 ? '' : 's'}`
})
const serverPlayOptions = computed<OverflowMenuOption[]>(() => [
{
@@ -1,52 +1,36 @@
<template>
<div class="flex items-center flex-wrap gap-2">
<template v-if="loadingServerPing">
<ServerOnlinePlayers
v-if="playersOnline !== undefined"
:online="playersOnline"
<PageHeaderMetadata>
<PageHeaderMetadataItem v-if="loadingServerPing && playersOnline !== undefined">
<ServerOnlinePlayers :online="playersOnline" :status-online="statusOnline" hide-label />
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="minecraftServer?.region || (loadingServerPing && ping !== undefined)"
>
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer.region" />
<ServerPing
v-if="loadingServerPing && ping !== undefined"
:ping="ping"
:status-online="statusOnline"
hide-label
/>
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
<div
v-if="
(playersOnline !== undefined || recentPlays !== undefined) &&
(minecraftServer?.region || ping)
"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<ServerPing v-if="ping" :ping="ping" />
</template>
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
<div v-if="minecraftServer?.region || ping" class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
<div v-if="linkedProjectV3" class="flex gap-1.5 items-center font-medium text-primary">
Linked to
<Avatar
:src="linkedProjectV3.icon_url"
:alt="linkedProjectV3.name"
:tint-by="instanceId"
size="24px"
/>
<router-link
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
class="hover:underline text-primary truncate"
>
{{ linkedProjectV3.name }}
</router-link>
</div>
</div>
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="showInstancePlayTime && playtimeLabel"
:icon="TimerIcon"
tooltip="Total playtime"
>
{{ playtimeLabel }}
</PageHeaderMetadataItem>
</PageHeaderMetadata>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { TimerIcon } from '@modrinth/assets'
import {
Avatar,
PageHeaderMetadata,
PageHeaderMetadataItem,
ServerOnlinePlayers,
ServerPing,
ServerRecentPlays,
ServerRegion,
} from '@modrinth/ui'
@@ -54,10 +38,9 @@ defineProps<{
loadingServerPing?: boolean
playersOnline?: number
statusOnline?: boolean
recentPlays?: number
ping?: number
minecraftServer?: Labrinth.Projects.v3.Project['minecraft_server']
linkedProjectV3?: Labrinth.Projects.v3.Project
instanceId?: string
showInstancePlayTime?: boolean
playtimeLabel?: string
}>()
</script>
@@ -232,6 +232,7 @@ provideInstallationSettings({
() =>
isModrinthLinkedModpack.value ||
isImportedModpack.value ||
instance.value.link?.type === 'server_project' ||
isSharedInstanceManagedModpack.value,
),
isBusy: installationSettingsBusy,
@@ -15,27 +15,28 @@
:share-text="formatMessage(messages.shareText)"
:open-in-new-tab="false"
/>
<ModpackContentModal
ref="modpackContentModal"
:modpack-name="displayedModpackProject?.title"
:modpack-icon-url="displayedModpackProject?.icon_url ?? undefined"
<ManagedContentModal
ref="managedContentModal"
:header="managedContentModalHeader"
:source-name="managedContent?.card.manager.name"
:source-icon-url="managedContent?.card.manager.iconUrl"
:enable-toggle="!isServerInstance && !isSharedMember && !isQuarantined"
:busy="isBulkOperating"
:action-disabled="isBulkOperating || isInstanceBusy"
:get-overflow-options="getOverflowOptions"
:switch-version="
isServerInstance || isSharedMember || isQuarantined ? undefined : handleSwitchVersion
"
@update:enabled="handleModpackContentToggle"
@bulk:enable="(items) => handleModpackContentBulkToggle(items, true)"
@bulk:disable="(items) => handleModpackContentBulkToggle(items, false)"
@update:enabled="handleManagedContentToggle"
@bulk:enable="(items) => handleManagedContentBulkToggle(items, true)"
@bulk:disable="(items) => handleManagedContentBulkToggle(items, false)"
/>
<ConfirmDisableModal
ref="sharedDisableConfirmModal"
:count="pendingModpackDisableItems.length"
:count="pendingManagedContentDisableItems.length"
:item-type="formatMessage(messages.contentTypeProject)"
:warning="managedContentPolicy.disableWarning(pendingModpackDisableItems)"
:warning="managedContentPolicy.disableWarning(pendingManagedContentDisableItems)"
:action-disabled="isInstanceBusy"
@disable="confirmPendingModpackContentDisable"
@disable="confirmPendingManagedContentDisable"
/>
<ConfirmModpackUpdateModal
ref="modpackUpdateConfirmModal"
@@ -88,7 +89,7 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { ClipboardCopyIcon, FolderOpenIcon } from '@modrinth/assets'
import { ClipboardCopyIcon, FolderOpenIcon, LockIcon, LockOpenIcon } from '@modrinth/assets'
import {
type BulkOperationStatus,
commonMessages,
@@ -96,18 +97,20 @@ import {
ConfirmModpackUpdateModal,
ContentCardLayout as ContentPageLayout,
type ContentItem,
type ContentModpackCardCategory,
type ContentModpackCardProject,
type ContentModpackCardVersion,
type ContentOwner,
ContentUpdaterModal,
dedupeManagedContentItems,
defineMessages,
injectNotificationManager,
ModpackContentModal,
type ModpackContentModalState,
type ManagedContentData,
ManagedContentModal,
type ManagedContentModalState,
type ManagedContentProject,
type ManagedContentVersion,
type OverflowMenuOption,
provideContentManager,
ReadyTransition,
summarizeManagedContent,
UnknownFileWarningModal,
useDebugLogger,
useVIntl,
@@ -135,8 +138,10 @@ import {
add_project_from_path,
edit,
get_linked_modpack_content,
get_shared_instance_publish_preview,
is_file_on_modrinth,
remove_project,
set_project_locked,
switch_project_version_with_dependencies,
toggle_disable_project,
update_all,
@@ -152,8 +157,17 @@ import type { FeatureFlag } from '@/store/theme'
import { injectInstancePage } from '../instance-context'
import { instanceContentQueryOptions, instanceKeys } from '../query-options'
import { injectSharedInstance } from '../shared-instance-context'
const messages = defineMessages({
modpackContentHeader: {
id: 'app.instance.content.managed-content.modpack-header',
defaultMessage: 'Modpack content',
},
sharedContentHeader: {
id: 'app.instance.content.managed-content.shared-header',
defaultMessage: 'Shared content',
},
shareTitle: {
id: 'app.instance.mods.share-title',
defaultMessage: 'Sharing modpack content',
@@ -178,6 +192,14 @@ const messages = defineMessages({
id: 'app.instance.mods.locked-content',
defaultMessage: 'Content in locked instances cannot be changed.',
},
freezeContent: {
id: 'app.instance.mods.freeze-content',
defaultMessage: 'Freeze version',
},
unfreezeContent: {
id: 'app.instance.mods.unfreeze-content',
defaultMessage: 'Unfreeze version',
},
contentTypeProject: {
id: 'app.instance.mods.content-type-project',
defaultMessage: 'project',
@@ -196,7 +218,7 @@ const messages = defineMessages({
},
})
let savedModalState: ModpackContentModalState | null = null
let savedModalState: ManagedContentModalState | null = null
function contentOwnerLink(owner: ContentOwner): NonNullable<ContentOwner['link']> {
if (owner.type === 'user') return `/user/${encodeURIComponent(owner.id)}`
@@ -219,6 +241,7 @@ const skipNonEssentialWarnings = computed(() =>
)
const instancePage = injectInstancePage()
const sharedInstanceState = injectSharedInstance()
const instance = instancePage.instance
const isServerInstance = instancePage.isServerInstance
const openSettings = () => instancePage.openSettings(1)
@@ -283,15 +306,12 @@ watch(
},
)
const linkedModpackProject = ref<ContentModpackCardProject | null>(null)
const linkedModpackVersion = ref<ContentModpackCardVersion | null>(null)
const linkedModpackOwner = ref<ContentOwner | null>(null)
const linkedModpackCategories = ref<ContentModpackCardCategory[]>([])
const linkedModpackHasUpdate = ref(false)
const linkedModpackProject = ref<ManagedContentProject | null>(null)
const linkedModpackVersion = ref<ManagedContentVersion | null>(null)
const linkedModpackUpdateVersionId = ref<string | null>(null)
const localImportedModpackUnlinked = ref(false)
const localImportedModpackProject = computed<ContentModpackCardProject | null>(() => {
const localImportedModpackProject = computed<ManagedContentProject | null>(() => {
const link = instance.value.link
if (localImportedModpackUnlinked.value || link?.type !== 'imported_modpack') return null
@@ -300,7 +320,6 @@ const localImportedModpackProject = computed<ContentModpackCardProject | null>((
slug: link.filename ?? instance.value.id,
title: link.name ?? instance.value.name,
icon_url: instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : undefined,
description: '',
filename: link.filename ?? undefined,
}
})
@@ -319,6 +338,7 @@ watch(
const isModpackUpdating = ref(false)
const isBulkOperating = ref(false)
const isInstanceBusy = computed(() => instance.value?.install_stage !== 'installed')
const showSharedContentFilter = computed(() => instance.value.shared_instance?.role === 'member')
const isPackLocked = computed(
() =>
instance.value.quarantined ||
@@ -329,10 +349,10 @@ const isPackLocked = computed(
const shareModal = ref<InstanceType<typeof ShareModalWrapper> | null>()
const exportModal = ref(null)
const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal> | null>()
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal> | null>()
const managedContentModal = ref<InstanceType<typeof ManagedContentModal> | null>()
const modpackUpdateConfirmModal = ref<InstanceType<typeof ConfirmModpackUpdateModal> | null>()
const sharedDisableConfirmModal = ref<InstanceType<typeof ConfirmDisableModal> | null>()
const pendingModpackDisableItems = ref<ContentItem[]>([])
const pendingManagedContentDisableItems = ref<ContentItem[]>([])
const unknownFileWarningModal = ref<InstanceType<typeof UnknownFileWarningModal> | null>()
const unknownFileName = ref('')
let resolveUnknownFileConfirmation: ((confirmed: boolean) => void) | null = null
@@ -349,6 +369,134 @@ const modpackContentQuery = useQuery({
),
})
const hasSharedManagedContent = computed(() => {
if (instance.value.shared_instance?.role === 'owner') return false
const linkType = instance.value.link?.type
return (
!!instance.value.shared_instance ||
linkType === 'server_project' ||
linkType === 'server_project_modpack'
)
})
const managedContentItems = computed(() => {
const linkedContent = modpackContentQuery.data.value ?? []
const sourcedContent = hasSharedManagedContent.value
? projects.value.filter((item) =>
['server_project', 'shared_instance'].includes(item.source_kind ?? ''),
)
: []
return dedupeManagedContentItems([...linkedContent, ...sourcedContent])
})
const managedContentSummary = computed(() =>
modpackContentQuery.isLoading.value && modpackContentQuery.data.value === undefined
? undefined
: summarizeManagedContent(managedContentItems.value),
)
const managedContent = computed<ManagedContentData | null>(() => {
const attachment = instance.value.shared_instance
const sharedManager = sharedInstanceState.manager.value
const linkedProject = instancePage.linkedProject.value
const linkType = instance.value.link?.type
const isSharedOwner = attachment?.role === 'owner'
if (
!isSharedOwner &&
(attachment || linkType === 'server_project' || linkType === 'server_project_modpack')
) {
const serverManaged =
sharedManager?.type === 'server' ||
!!attachment?.server_manager_name ||
linkType === 'server_project' ||
linkType === 'server_project_modpack' ||
(!attachment && isServerInstance.value)
const managerName = serverManaged
? (sharedManager?.name ??
attachment?.server_manager_name ??
linkedProject?.name ??
instance.value.name)
: (sharedManager?.name ?? instance.value.name)
const managerIcon = serverManaged
? (sharedManager?.avatarUrl ??
attachment?.server_manager_icon_url ??
linkedProject?.icon_url ??
undefined)
: (sharedManager?.avatarUrl ??
(instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : undefined))
const managerLink = serverManaged
? linkedProject
? {
path: `/project/${linkedProject.slug ?? linkedProject.id}`,
query: { i: instancePage.instanceId.value },
}
: undefined
: sharedManager?.type === 'user'
? `/user/${encodeURIComponent(sharedManager.name)}`
: undefined
return {
card: {
kind: serverManaged ? 'server' : 'shared-instance',
installing: isInstanceBusy.value,
manager: {
name: managerName,
iconUrl: managerIcon,
link: managerLink,
},
summary: managedContentSummary.value,
syncedAt: sharedInstanceState.lastUpdateCheckAt.value,
updateAvailable: instancePage.sharedInstanceUpdateAvailable.value,
},
disabled: attachment?.status === 'applying' || isInstanceBusy.value,
disabledText: formatMessage(commonMessages.updatingLabel),
}
}
const project = displayedModpackProject.value
if (!project) return null
return {
card: {
kind: 'modpack',
installing: isInstanceBusy.value,
manager: {
name: project.title,
iconUrl: project.icon_url ?? undefined,
link: linkedModpackProject.value
? {
path: `/project/${project.slug ?? project.id}`,
query: { i: instancePage.instanceId.value },
}
: undefined,
},
summary: managedContentSummary.value,
versionNumber: linkedModpackVersion.value?.version_number,
versionLink:
linkedModpackProject.value && linkedModpackVersion.value
? {
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`,
query: { i: instancePage.instanceId.value },
}
: undefined,
updatedAt: linkedModpackVersion.value?.date_published,
},
disabled: isModpackUpdating.value || isInstanceBusy.value,
disabledText: formatMessage(commonMessages.updatingLabel),
}
})
const managedContentModalHeader = computed(() =>
formatMessage(
managedContent.value?.card.kind === 'modpack'
? messages.modpackContentHeader
: messages.sharedContentHeader,
),
)
// TODO: Extract content operation and updater modal state into composables; this page currently owns file mutations, dependency installs, busy flags, and version selection flow.
const updatingProject = ref<ContentItem | null>(null)
const updatingProjectVersions = ref<Labrinth.Versions.v2.Version[]>([])
@@ -418,14 +566,30 @@ function canDeleteContent(item: ContentItem) {
return canMutateContent(item)
}
function canToggleContent(item: ContentItem) {
return canMutateContent(item)
}
function canChangeContentVersion(item: ContentItem) {
return canMutateContent(item) && !item.locked
}
async function reconcileSharedInstancePublishState() {
if (instance.value.shared_instance?.role !== 'owner') return
await get_shared_instance_publish_preview(instance.value.id).catch((error) => {
debug('Failed to reconcile shared instance publish state', { error })
})
}
function setContentItemBusy(item: ContentItem, busy: boolean, originalFileName = item.file_name) {
item.installing = busy
modpackContentModal.value?.updateItem(originalFileName, {
managedContentModal.value?.updateItem(originalFileName, {
installing: busy,
disabled: busy,
})
if (item.file_name !== originalFileName) {
modpackContentModal.value?.updateItem(item.file_name, {
managedContentModal.value?.updateItem(item.file_name, {
installing: busy,
disabled: busy,
})
@@ -627,8 +791,12 @@ async function handleUnknownFileContinue(dontShowAgain: boolean) {
resolveUnknownFileWarning(true)
}
async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) {
if (!mod.file_path) return
async function toggleDisableMod(
mod: ContentItem,
desiredEnabled?: boolean,
reconcileSharedState = true,
) {
if (!mod.file_path || !canToggleContent(mod)) return
const operation = beginContentOperation(mod)
if (!operation) return
const originalFilePath = mod.file_path
@@ -640,7 +808,7 @@ async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) {
mod.file_path = newPath
mod.file_name = newFileName
mod.enabled = enabled
modpackContentModal.value?.updateItem(operation.originalFileName, {
managedContentModal.value?.updateItem(operation.originalFileName, {
file_path: newPath,
file_name: newFileName,
enabled,
@@ -659,6 +827,10 @@ async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) {
project_type: mod.project_type,
disabled: !enabled,
})
if (reconcileSharedState) {
await reconcileSharedInstancePublishState()
}
} catch (err) {
handleError(err as Error)
} finally {
@@ -669,7 +841,7 @@ async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) {
const toggleDisableDebounced = toggleDisableMod
async function removeMod(mod: ContentItem) {
if (!mod.file_path) return
if (!mod.file_path || !canDeleteContent(mod)) return
const operation = beginContentOperation(mod)
if (!operation) return
@@ -800,7 +972,7 @@ async function bulkUpdateAllProjects(onProgress?: (status: BulkOperationStatus)
}
async function updateProject(mod: ContentItem) {
if (!canUpdateProject(mod)) return
if (!canUpdateProject(mod) || mod.locked) return
const operation = beginContentOperation(mod)
if (!operation) return
@@ -829,7 +1001,7 @@ async function updateProject(mod: ContentItem) {
}
async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions.v2.Version) {
if (!canMutateContent(mod)) return
if (!canChangeContentVersion(mod)) return
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
@@ -856,7 +1028,8 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions
async function handleUpdate(id: string) {
const item = projects.value.find((p) => getContentItemId(p) === id)
if (!item || !canUpdateProject(item) || !item.project?.id || !item.version?.id) return
if (!item || item.locked || !canUpdateProject(item) || !item.project?.id || !item.version?.id)
return
const requestId = beginUpdateRequest()
const itemId = getContentItemId(item)
@@ -966,7 +1139,7 @@ async function handleUpdate(id: string) {
}
async function handleSwitchVersion(item: ContentItem) {
if (!canMutateContent(item)) return
if (!canChangeContentVersion(item)) return
if (!item.project?.id || !item.version?.id) return
const requestId = beginUpdateRequest()
@@ -993,9 +1166,9 @@ async function handleSwitchVersion(item: ContentItem) {
updatingProjectVersions.value = versions
}
async function handleModpackContentToggle(item: ContentItem, enabled: boolean) {
async function handleManagedContentToggle(item: ContentItem, enabled: boolean) {
if (!enabled && managedContentPolicy.disableWarning([item])) {
pendingModpackDisableItems.value = [item]
pendingManagedContentDisableItems.value = [item]
sharedDisableConfirmModal.value?.show()
return
}
@@ -1003,47 +1176,48 @@ async function handleModpackContentToggle(item: ContentItem, enabled: boolean) {
await toggleDisableDebounced(item, enabled)
}
async function handleModpackContentBulkToggle(items: ContentItem[], enabled: boolean) {
async function handleManagedContentBulkToggle(items: ContentItem[], enabled: boolean) {
if (!enabled && managedContentPolicy.disableWarning(items)) {
pendingModpackDisableItems.value = items
pendingManagedContentDisableItems.value = items
sharedDisableConfirmModal.value?.show()
return
}
await setModpackContentEnabled(items, enabled)
await setManagedContentEnabled(items, enabled)
}
async function confirmPendingModpackContentDisable() {
const items = [...pendingModpackDisableItems.value]
pendingModpackDisableItems.value = []
await setModpackContentEnabled(items, false)
async function confirmPendingManagedContentDisable() {
const items = [...pendingManagedContentDisableItems.value]
pendingManagedContentDisableItems.value = []
await setManagedContentEnabled(items, false)
}
async function setModpackContentEnabled(items: ContentItem[], enabled: boolean) {
await Promise.all(items.map((item) => toggleDisableMod(item, enabled)))
async function setManagedContentEnabled(items: ContentItem[], enabled: boolean) {
await Promise.all(items.map((item) => toggleDisableMod(item, enabled, false)))
await reconcileSharedInstancePublishState()
}
async function handleModpackContent() {
async function handleManagedContent() {
if (!instance.value?.id) return
if (modpackContentQuery.data.value?.length) {
modpackContentModal.value?.show(modpackContentQuery.data.value)
if (modpackContentQuery.data.value !== undefined) {
managedContentModal.value?.show(managedContentItems.value)
return
}
modpackContentModal.value?.showLoading()
managedContentModal.value?.showLoading()
const { data, error } = await modpackContentQuery.refetch()
if (data !== undefined) {
modpackContentModal.value?.show(data)
managedContentModal.value?.show(managedContentItems.value)
} else {
if (error) handleError(error)
modpackContentModal.value?.hide()
managedContentModal.value?.hide()
}
}
async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) {
async function refreshManagedContentItems(cacheBehaviour?: CacheBehaviour) {
if (!instance.value?.id) return
const contentItems = await queryClient
@@ -1054,13 +1228,13 @@ async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) {
.catch(handleError)
if (contentItems) {
modpackContentModal.value?.setItems(contentItems)
managedContentModal.value?.setItems(managedContentItems.value)
}
}
async function refreshContentState(cacheBehaviour?: CacheBehaviour) {
await initProjects(cacheBehaviour)
await refreshModpackContentItems(cacheBehaviour)
await refreshManagedContentItems(cacheBehaviour)
}
watch(
@@ -1093,7 +1267,6 @@ async function handleModpackUpdate() {
linkedModpackUpdateVersionId: linkedModpackUpdateVersionId.value,
linkedModpackProject: linkedModpackProject.value,
linkedModpackVersion: linkedModpackVersion.value,
linkedModpackHasUpdate: linkedModpackHasUpdate.value,
instance: {
path: instance.value.id,
name: instance.value.name,
@@ -1262,8 +1435,6 @@ async function unpairInstance() {
})
linkedModpackProject.value = null
linkedModpackVersion.value = null
linkedModpackOwner.value = null
linkedModpackHasUpdate.value = false
linkedModpackUpdateVersionId.value = null
localImportedModpackUnlinked.value = true
await initProjects()
@@ -1326,9 +1497,41 @@ function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
})
}
if (canMutateContent(item)) {
options.push(
{ type: 'divider' },
{
id: item.locked ? 'unfreeze-content' : 'freeze-content',
label: formatMessage(item.locked ? messages.unfreezeContent : messages.freezeContent),
icon: item.locked ? LockOpenIcon : LockIcon,
action: () => handleContentFreeze(item, !item.locked),
},
)
}
return options
}
async function handleContentFreeze(item: ContentItem, frozen: boolean) {
if (!item.file_path || !canMutateContent(item)) return
const operation = beginContentOperation(item)
if (!operation) return
const originalFilePath = item.file_path
try {
await set_project_locked(instance.value.id, item.file_path, frozen)
item.locked = frozen
managedContentModal.value?.updateItem(operation.originalFileName, { locked: frozen })
updateLinkedModpackContentCache(item, operation.originalFileName, originalFilePath, {
locked: frozen,
})
} catch (err) {
handleError(err as Error)
} finally {
finishContentOperation(item, operation)
}
}
async function initProjects(cacheBehaviour?: CacheBehaviour, staleTime = 0) {
if (!instance.value) return
@@ -1358,16 +1561,10 @@ function applyContentData(contentData: InstanceContentData) {
if (contentData.modpack) {
linkedModpackProject.value = contentData.modpack.project
linkedModpackVersion.value = contentData.modpack.version
linkedModpackOwner.value = contentData.modpack.owner
linkedModpackCategories.value = contentData.modpack.categories
linkedModpackHasUpdate.value = contentData.modpack.hasUpdate
linkedModpackUpdateVersionId.value = contentData.modpack.updateVersionId
} else {
linkedModpackProject.value = null
linkedModpackVersion.value = null
linkedModpackOwner.value = null
linkedModpackCategories.value = []
linkedModpackHasUpdate.value = false
linkedModpackUpdateVersionId.value = null
}
@@ -1375,55 +1572,16 @@ function applyContentData(contentData: InstanceContentData) {
return true
}
function contentVersionLabel(item: ContentItem): string {
if (item.embedded_metadata?.version) return item.embedded_metadata.version
return formatMessage(commonMessages.unknownLabel)
}
provideContentManager({
items: mergedProjects,
loading,
error: ref(null),
modpack: computed(() => {
if (linkedModpackProject.value) {
return {
project: linkedModpackProject.value,
projectLink: {
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}`,
query: { i: instancePage.instanceId.value },
},
version: linkedModpackVersion.value ?? undefined,
versionLink:
linkedModpackProject.value && linkedModpackVersion.value
? {
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`,
query: { i: instancePage.instanceId.value },
}
: undefined,
owner: linkedModpackOwner.value
? {
...linkedModpackOwner.value,
link: contentOwnerLink(linkedModpackOwner.value),
}
: undefined,
categories: linkedModpackCategories.value,
hasUpdate: linkedModpackHasUpdate.value,
disabled: isModpackUpdating.value,
disabledText: isModpackUpdating.value
? formatMessage(commonMessages.updatingLabel)
: formatMessage(commonMessages.installingLabel),
}
}
if (localImportedModpackProject.value) {
return {
project: localImportedModpackProject.value,
categories: [],
hasUpdate: false,
disabled: isModpackUpdating.value,
disabledText: isModpackUpdating.value
? formatMessage(commonMessages.updatingLabel)
: formatMessage(commonMessages.installingLabel),
}
}
return null
}),
managedContent,
isPackLocked,
isBusy: isInstanceBusy,
disableAddContent: isQuarantined,
@@ -1432,23 +1590,27 @@ provideContentManager({
skipNonEssentialWarnings,
contentTypeLabel: ref(formatMessage(messages.contentTypeProject)),
toggleEnabled: toggleDisableDebounced,
bulkEnableItems: (items: ContentItem[]) =>
Promise.all(
bulkEnableItems: async (items: ContentItem[]) => {
await Promise.all(
items
.filter((item) => canMutateContent(item) && !item.enabled)
.map((item) => toggleDisableMod(item, true)),
).then(() => {}),
bulkDisableItems: (items: ContentItem[]) =>
Promise.all(
.filter((item) => canToggleContent(item) && !item.enabled)
.map((item) => toggleDisableMod(item, true, false)),
)
await reconcileSharedInstancePublishState()
},
bulkDisableItems: async (items: ContentItem[]) => {
await Promise.all(
items
.filter((item) => canMutateContent(item) && item.enabled)
.map((item) => toggleDisableMod(item, false)),
).then(() => {}),
.filter((item) => canToggleContent(item) && item.enabled)
.map((item) => toggleDisableMod(item, false, false)),
)
await reconcileSharedInstancePublishState()
},
deleteItem: removeMod,
bulkDeleteItems: (items: ContentItem[]) =>
Promise.all(items.filter(canMutateContent).map((item) => removeMod(item))).then(() => {}),
Promise.all(items.filter(canDeleteContent).map((item) => removeMod(item))).then(() => {}),
canDeleteItem: canDeleteContent,
canToggleItem: canMutateContent,
canToggleItem: canToggleContent,
getDeleteWarning: managedContentPolicy.deleteWarning,
getDisableWarning: managedContentPolicy.disableWarning,
getDeleteDependencyWarning,
@@ -1459,13 +1621,15 @@ provideContentManager({
updateItem: handleUpdate,
bulkUpdateAll: bulkUpdateAllProjects,
bulkUpdateItem: updateProject,
updateModpack:
isServerInstance.value || isSharedMember.value || isQuarantined.value
? undefined
: handleModpackUpdate,
viewModpackContent: handleModpackContent,
runManagedContentPrimaryAction:
instance.value.shared_instance?.role === 'member'
? instancePage.reviewSharedInstanceUpdate
: instance.value.link?.type === 'modrinth_modpack' && !isQuarantined.value
? handleModpackUpdate
: undefined,
viewManagedContent: handleManagedContent,
unlinkModpack: unpairInstance,
openSettings: openSettings,
openManagedContentSettings: openSettings,
switchVersion: handleSwitchVersion,
getOverflowOptions,
shareItems: handleShareItems,
@@ -1475,15 +1639,15 @@ provideContentManager({
project: item.project ?? {
id: item.file_name,
slug: null,
title: item.file_name.replace('.disabled', ''),
icon_url: null,
title: item.embedded_metadata?.name ?? item.file_name.replace('.disabled', ''),
icon_url: item.embedded_metadata?.icon_url ?? null,
},
projectLink: item.project?.id
? { path: `/project/${item.project.id}`, query: { i: instancePage.instanceId.value } }
: undefined,
version: item.version ?? {
id: item.file_name,
version_number: formatMessage(commonMessages.unknownLabel),
version_number: contentVersionLabel(item),
file_name: item.file_name,
},
versionLink:
@@ -1499,19 +1663,22 @@ provideContentManager({
link: contentOwnerLink(item.owner),
}
: undefined,
external: item.external ?? !item.project,
enabled: canMutateContent(item) ? item.enabled : undefined,
locked: item.locked,
installing: item.installing,
hideDelete: !canDeleteContent(item),
hideSwitchVersion: !canMutateContent(item) || !item.project?.id || !item.version?.id,
hasUpdate: canUpdateProject(item),
hideSwitchVersion: !canChangeContentVersion(item) || !item.project?.id || !item.version?.id,
hasUpdate: canUpdateProject(item) && !item.locked,
}),
showSharedContentFilter,
filterPersistKey: instance.value.id,
})
type UnlistenFn = () => void
const initialContentReady = loadInitialContent()
void initialContentReady.then(restoreModpackContentModalState).catch(handleError)
void initialContentReady.then(restoreManagedContentModalState).catch(handleError)
function getInstallRevision() {
return installRevisionByInstance.value.get(instance.value.id) ?? 0
@@ -1541,18 +1708,18 @@ watch(contentQuery.error, (error) => {
}
})
async function restoreModpackContentModalState() {
async function restoreManagedContentModalState() {
if (!savedModalState) return
const stateToRestore = savedModalState
savedModalState = null
await nextTick()
modpackContentModal.value?.restore(stateToRestore)
managedContentModal.value?.restore(stateToRestore)
}
// Save modal state when navigating away so it can be restored on back
const removeBeforeEach = router.beforeEach(() => {
const state = modpackContentModal.value?.getState()
const state = managedContentModal.value?.getState()
savedModalState = state ?? null
})
@@ -9,6 +9,7 @@ export interface InstancePageContext {
readonly instance: ComputedRef<GameInstance>
readonly linkedProject: ComputedRef<Labrinth.Projects.v3.Project | undefined>
readonly isServerInstance: ComputedRef<boolean>
readonly sharedInstanceUpdateAvailable: ComputedRef<boolean>
readonly offline: Readonly<Ref<boolean>>
readonly playing: ComputedRef<boolean>
readonly loading: Readonly<Ref<boolean>>
@@ -21,6 +22,7 @@ export interface InstancePageContext {
openSettings: (tab?: number) => void
browseContent: (projectType?: string) => Promise<void>
browseServers: () => Promise<void>
reviewSharedInstanceUpdate: (event?: MouseEvent) => void
}
export const [injectInstancePage, provideInstancePage] =
@@ -37,11 +37,8 @@
:loading-server-ping="loadingServerPing"
:players-online="playersOnline"
:status-online="statusOnline"
:recent-plays="recentPlays"
:ping="ping"
:minecraft-server="minecraftServer"
:linked-project-v3="linkedProjectV3"
:shared-instance-manager="sharedInstanceManager"
@repair="() => repairInstance()"
@stop="() => stopInstance('InstancePage')"
@play="() => startInstance('InstancePage')"
@@ -64,10 +61,8 @@
:shared-instance-expected-user-id="sharedInstanceExpectedUserId"
:shared-instance-role="instance.shared_instance?.role"
:shared-instance-signed-out="sharedInstanceSignedOut"
:shared-instance-update-available="showSharedInstanceUpdateAdmonition"
@published="refreshInstance"
@delete="requestInstanceDeletion"
@review-update="reviewSharedInstanceUpdate"
/>
</div>
<div :class="['p-6 pt-4', { 'min-h-0 flex-1 overflow-y-auto': isFixedRender }]">
@@ -211,7 +206,9 @@ useQuery(
retry: false,
})),
)
const linkedProjectId = computed(() => instance.value?.link?.project_id ?? '')
const linkedProjectId = computed(
() => instance.value?.link?.server_project_id ?? instance.value?.link?.project_id ?? '',
)
const linkedProjectQuery = useQuery(
computed(() => ({
...instanceLinkedProjectQueryOptions(linkedProjectId.value),
@@ -302,9 +299,6 @@ const minecraftServer = computed(() => linkedProjectV3.value?.minecraft_server)
const javaServerPingData = computed(() => linkedProjectV3.value?.minecraft_java_server?.ping?.data)
const liveServerStatusOnline = ref(false)
const statusOnline = computed(() => liveServerStatusOnline.value || !!javaServerPingData.value)
const recentPlays = computed(
() => linkedProjectV3.value?.minecraft_java_server?.verified_plays_2w ?? undefined,
)
const playersOnline = ref<number | undefined>(undefined)
const ping = ref<number | undefined>(undefined)
const loadingServerPing = ref(false)
@@ -317,7 +311,6 @@ provideSharedInstance(sharedInstanceState)
const {
actionsLocked: sharedInstanceActionsLocked,
expectedUserId: sharedInstanceExpectedUserId,
manager: sharedInstanceManager,
refreshUpdatePreview: refreshSharedInstanceUpdatePreview,
setUnavailable: setSharedInstanceUnavailable,
signedOut: sharedInstanceSignedOut,
@@ -331,7 +324,7 @@ const sharedInstanceUpdateKey = computed(() => {
const latestVersion = sharedInstanceUpdatePreview.value?.latestVersion
return instanceId && latestVersion !== undefined ? `${instanceId}:${latestVersion}` : null
})
const showSharedInstanceUpdateAdmonition = computed(
const sharedInstanceUpdateAvailable = computed(
() =>
sharedInstanceUpdatePreview.value?.updateAvailable === true &&
sharedInstanceUpdateKey.value !== hiddenSharedInstanceUpdateKey.value,
@@ -515,7 +508,7 @@ async function handleSharedInstanceUnavailable(
setSharedInstanceUnavailable(reason)
}
function reviewSharedInstanceUpdate(event: MouseEvent) {
function reviewSharedInstanceUpdate(event?: MouseEvent) {
const currentInstance = instance.value
const preview = sharedInstanceUpdatePreview.value
if (
@@ -792,6 +785,7 @@ provideInstancePage({
instance: instance as ComputedRef<GameInstance>,
linkedProject: linkedProjectV3,
isServerInstance,
sharedInstanceUpdateAvailable,
offline,
playing,
loading,
@@ -804,6 +798,7 @@ provideInstancePage({
openSettings,
browseContent,
browseServers,
reviewSharedInstanceUpdate,
})
provideInstanceBackup(() => instance.value!)
@@ -99,6 +99,7 @@ export function createSharedInstanceContext(
enabled: computed(
() =>
!!instance.value?.id &&
instance.value.install_stage === 'installed' &&
!!instance.value.shared_instance &&
!actionsLocked.value &&
!offline.value &&
@@ -128,6 +129,7 @@ export function createSharedInstanceContext(
const updatePreview = computed(() =>
unavailableReason.value ? null : (updatePreviewQuery.data.value ?? null),
)
const lastUpdateCheckAt = computed(() => updatePreviewQuery.dataUpdatedAt.value || undefined)
watch(
() => instance.value?.id,
@@ -162,6 +164,7 @@ export function createSharedInstanceContext(
unavailableManager,
manager,
updatePreview,
lastUpdateCheckAt,
expectedUserId,
wrongAccount,
signedOut,