From 41fa0767f61312f2a97bcac879a9269de674c4f1 Mon Sep 17 00:00:00 2001
From: "Calum H. (IMB11)"
Date: Mon, 3 Aug 2026 07:56:03 +0100
Subject: [PATCH 01/67] feat: server play frontend draft
---
.../src/composables/use-app-settings.ts | 1 +
.../src/pages/hosting/manage/Index.vue | 1 +
.../src/pages/hosting/manage/Play.vue | 18 ++
.../src/pages/hosting/manage/index.js | 3 +-
apps/app-frontend/src/routes.js | 5 +
apps/frontend/src/composables/featureFlags.ts | 1 +
.../src/pages/hosting/manage/[id].vue | 1 +
.../src/pages/hosting/manage/[id]/play.vue | 19 ++
.../manage/[id]/play/ServerPlayCard.vue | 175 +++++++++++++
.../manage/[id]/play/ServerPlayersTable.vue | 200 ++++++++++++++
.../wrapped/hosting/manage/[id]/play/play.vue | 245 ++++++++++++++++++
.../wrapped/hosting/manage/[id]/play/types.ts | 11 +
.../layouts/wrapped/hosting/manage/root.vue | 13 +
packages/ui/src/layouts/wrapped/index.ts | 1 +
14 files changed, 693 insertions(+), 1 deletion(-)
create mode 100644 apps/app-frontend/src/pages/hosting/manage/Play.vue
create mode 100644 apps/frontend/src/pages/hosting/manage/[id]/play.vue
create mode 100644 packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
create mode 100644 packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue
create mode 100644 packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
create mode 100644 packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/types.ts
diff --git a/apps/app-frontend/src/composables/use-app-settings.ts b/apps/app-frontend/src/composables/use-app-settings.ts
index dceec83997a..f1a9b5f33e1 100644
--- a/apps/app-frontend/src/composables/use-app-settings.ts
+++ b/apps/app-frontend/src/composables/use-app-settings.ts
@@ -10,6 +10,7 @@ export const DEFAULT_FEATURE_FLAGS = {
show_sync_instances_update_modal: false,
pride_fundraiser: true,
i18n_debug: false,
+ ServerPlayFrontend: false,
localhost_sign_in: false,
}
diff --git a/apps/app-frontend/src/pages/hosting/manage/Index.vue b/apps/app-frontend/src/pages/hosting/manage/Index.vue
index 358ea3f7bdf..cfcdf7f4ec2 100644
--- a/apps/app-frontend/src/pages/hosting/manage/Index.vue
+++ b/apps/app-frontend/src/pages/hosting/manage/Index.vue
@@ -9,6 +9,7 @@
:reload-page="() => router.go(0)"
:resolve-viewer="resolveViewer"
:show-copy-id-action="appSettings.devMode"
+ :show-play-tab="appSettings.getFeatureFlag('ServerPlayFrontend')"
:auth-user="authUser"
:navigate-to-billing="() => openUrl('https://modrinth.com/settings/billing')"
:navigate-to-servers="() => router.push('/hosting/manage')"
diff --git a/apps/app-frontend/src/pages/hosting/manage/Play.vue b/apps/app-frontend/src/pages/hosting/manage/Play.vue
new file mode 100644
index 00000000000..3728c9ceb07
--- /dev/null
+++ b/apps/app-frontend/src/pages/hosting/manage/Play.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/apps/app-frontend/src/pages/hosting/manage/index.js b/apps/app-frontend/src/pages/hosting/manage/index.js
index 0c10b07133d..073c5492961 100644
--- a/apps/app-frontend/src/pages/hosting/manage/index.js
+++ b/apps/app-frontend/src/pages/hosting/manage/index.js
@@ -4,5 +4,6 @@ import Content from './Content.vue'
import Files from './Files.vue'
import Index from './Index.vue'
import Overview from './Overview.vue'
+import Play from './Play.vue'
-export { Access, Backups, Content, Files, Index, Overview }
+export { Access, Backups, Content, Files, Index, Overview, Play }
diff --git a/apps/app-frontend/src/routes.js b/apps/app-frontend/src/routes.js
index 33da4d34f38..5e2b388d3f0 100644
--- a/apps/app-frontend/src/routes.js
+++ b/apps/app-frontend/src/routes.js
@@ -23,6 +23,11 @@ export default new createRouter({
name: 'ServerManage',
component: () => import('@/pages/hosting/manage/Index.vue'),
children: [
+ {
+ path: 'play',
+ name: 'ServerManagePlay',
+ component: Hosting.Play,
+ },
{
path: '',
name: 'ServerManageOverview',
diff --git a/apps/frontend/src/composables/featureFlags.ts b/apps/frontend/src/composables/featureFlags.ts
index d1d3d3b610b..0e30be1e8a2 100644
--- a/apps/frontend/src/composables/featureFlags.ts
+++ b/apps/frontend/src/composables/featureFlags.ts
@@ -59,6 +59,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
alwaysShowVersionDevInfo: false,
advancedFiltersCollapsed: true,
dismissedPhotosensitivityFilterWarning: false,
+ ServerPlayFrontend: false,
} as const)
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
diff --git a/apps/frontend/src/pages/hosting/manage/[id].vue b/apps/frontend/src/pages/hosting/manage/[id].vue
index 5f6d5245f42..9e218386abf 100644
--- a/apps/frontend/src/pages/hosting/manage/[id].vue
+++ b/apps/frontend/src/pages/hosting/manage/[id].vue
@@ -5,6 +5,7 @@
:resolve-viewer="resolveViewer"
:show-copy-id-action="flags.developerMode"
:show-advanced-debug-info="flags.advancedDebugInfo"
+ :show-play-tab="flags.ServerPlayFrontend"
:stripe-publishable-key="config.public.stripePublishableKey as string"
:site-url="config.public.siteUrl as string"
:products="products"
diff --git a/apps/frontend/src/pages/hosting/manage/[id]/play.vue b/apps/frontend/src/pages/hosting/manage/[id]/play.vue
new file mode 100644
index 00000000000..b4866d98f76
--- /dev/null
+++ b/apps/frontend/src/pages/hosting/manage/[id]/play.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
new file mode 100644
index 00000000000..b5159719929
--- /dev/null
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+ {{ formatMessage(messages.playWithAppTitle) }}
+
+
+ {{ formatMessage(messages.playWithAppDescription) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.differentLauncherTitle) }}
+
+
+ {{ formatMessage(messages.differentLauncherDescription) }}
+
+
+
+
+
+ {{ address }}
+
+
+
+
+
+
+
+
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue
new file mode 100644
index 00000000000..f2359226557
--- /dev/null
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No users match your filters.
+
+
+
+
+
+
+
+ {{ formatPlayerDate(row.lastPlayedAt) }}
+
+ Never
+
+
+
+ Pending
+
+
+ {{ formatPlayerDate(row.joinedAt) }}
+
+
+
+
+
+
+ {{ methodLabel(row.method) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
new file mode 100644
index 00000000000..0c9fd04c2da
--- /dev/null
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+ {{ formatMessage(messages.invitedPlayersTitle) }}
+
+ props.onOpenPlayerActions?.(player)"
+ />
+
+
+
+
+
+
+
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/types.ts b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/types.ts
new file mode 100644
index 00000000000..63f2dbb0550
--- /dev/null
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/types.ts
@@ -0,0 +1,11 @@
+export type ServerPlayerMethod = 'direct' | 'link'
+
+export type ServerPlayerRow = {
+ id: string
+ username: string
+ avatarUrl?: string
+ lastPlayedAt: Date | null
+ joinedAt: Date | null
+ method: ServerPlayerMethod
+ pending?: boolean
+}
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
index d96c14d4ae8..f610e61cd8f 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
@@ -351,6 +351,7 @@ import {
LinkIcon,
LoaderCircleIcon,
LockIcon,
+ PlayIcon,
MoreVerticalIcon,
ServerIcon as ServerAssetIcon,
SettingsIcon,
@@ -432,6 +433,7 @@ const props = withDefaults(
showCopyIdAction?: boolean
showAdvancedDebugInfo?: boolean
showUptime?: boolean
+ showPlayTab?: boolean
additionalTabs?: Tab[]
stripePublishableKey?: string
siteUrl?: string
@@ -456,6 +458,7 @@ const props = withDefaults(
showCopyIdAction: false,
showAdvancedDebugInfo: false,
showUptime: true,
+ showPlayTab: false,
additionalTabs: () => [],
stripePublishableKey: undefined,
siteUrl: undefined,
@@ -834,6 +837,16 @@ watch(serverData, (data) => {
})
const navLinks = computed(() => [
+ ...(props.showPlayTab
+ ? [
+ {
+ label: 'Play',
+ href: `/hosting/manage/${props.serverId}/play`,
+ icon: PlayIcon,
+ subpages: [],
+ },
+ ]
+ : []),
{
label: 'Overview',
href: `/hosting/manage/${props.serverId}`,
diff --git a/packages/ui/src/layouts/wrapped/index.ts b/packages/ui/src/layouts/wrapped/index.ts
index 59ba8d71097..c4ddd4de573 100644
--- a/packages/ui/src/layouts/wrapped/index.ts
+++ b/packages/ui/src/layouts/wrapped/index.ts
@@ -5,5 +5,6 @@ export { default as ServersManageContentPage } from './hosting/manage/content.vu
export { default as ServersManageFilesPage } from './hosting/manage/files.vue'
export { default as ServersManagePageIndex } from './hosting/manage/index.vue'
export { default as ServersManageOverviewPage } from './hosting/manage/overview.vue'
+export { default as ServersManagePlayPage } from './hosting/manage/[id]/play/play.vue'
export { default as ServersManageRootLayout } from './hosting/manage/root.vue'
export * from './settings'
From a72ebce7f186f5dd98901c5d3216e4f040b484dd Mon Sep 17 00:00:00 2001
From: "Calum H. (IMB11)"
Date: Fri, 4 Sep 2026 15:30:06 +0100
Subject: [PATCH 02/67] fix: build error
---
.../manage/[id]/play/ServerPlayCard.vue | 95 +++++++++----------
.../manage/[id]/play/ServerPlayersTable.vue | 26 ++---
2 files changed, 57 insertions(+), 64 deletions(-)
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
index b5159719929..3fa2fb5bc0c 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
@@ -12,18 +12,14 @@
-
-
-
-
-
-
+
+
@@ -38,44 +34,41 @@
-
+
+
+
+ {{ formatMessage(messages.downloadModpackButton) }}
+
+
+
+
+
+
+
+
+
@@ -107,7 +100,7 @@ import {
} from '@modrinth/assets'
import { ref } from 'vue'
-import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
+import { Button, ButtonGroup, ButtonLink, IconButton } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
const props = defineProps<{
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue
index f2359226557..56730841e2d 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue
@@ -75,18 +75,18 @@
-
-
-
+
+
+
+
@@ -105,7 +105,7 @@ import {
import { computed, ref } from 'vue'
import Avatar from '#ui/components/base/Avatar.vue'
-import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
+import { IconButton } from '#ui/components/base/buttons'
import Combobox, { type ComboboxOption } from '#ui/components/base/Combobox.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Table, { type SortDirection, type TableColumn } from '#ui/components/base/Table.vue'
From 96fe2a00f0dce757a2169223d33b3524252151a4 Mon Sep 17 00:00:00 2001
From: "Calum H."
Date: Fri, 4 Sep 2026 15:33:02 +0100
Subject: [PATCH 03/67] feat: start on content tab env focused design (#7386)
---
apps/app-frontend/src/pages/Browse.vue | 16 +-
.../src/pages/discover/[type]/index.vue | 16 +-
.../src/modules/archon/content/v1.ts | 51 ++
.../api-client/src/modules/archon/types.ts | 32 ++
.../src/modules/kyros/content/v1.ts | 18 +
packages/ui/src/components/base/Toggle.vue | 28 +-
.../ui/src/composables/server-panel-sync.ts | 51 +-
packages/ui/src/composables/virtual-scroll.ts | 13 +-
.../components/ContentCardItem.vue | 59 ++-
.../components/ContentCardItemIcon.vue | 91 ++++
.../components/ContentCardTable.vue | 66 ++-
.../components/ContentEnabledFor.vue | 112 +++++
.../managed-content-modal/index.vue | 97 +++-
.../src/layouts/shared/content-tab/layout.vue | 104 ++--
.../content-tab/providers/content-manager.ts | 4 +-
.../src/layouts/shared/content-tab/types.ts | 18 +
.../server-settings/pages/installation.vue | 43 +-
.../wrapped/hosting/manage/content.vue | 465 ++++++++++++------
packages/ui/src/locales/cs-CZ/index.json | 9 -
packages/ui/src/locales/de-CH/index.json | 9 -
packages/ui/src/locales/de-DE/index.json | 9 -
packages/ui/src/locales/en-US/index.json | 49 +-
packages/ui/src/locales/es-419/index.json | 9 -
packages/ui/src/locales/es-ES/index.json | 9 -
packages/ui/src/locales/fil-PH/index.json | 7 -
packages/ui/src/locales/fr-FR/index.json | 9 -
packages/ui/src/locales/hu-HU/index.json | 6 -
packages/ui/src/locales/it-IT/index.json | 9 -
packages/ui/src/locales/ja-JP/index.json | 9 -
packages/ui/src/locales/ko-KR/index.json | 9 -
packages/ui/src/locales/ms-MY/index.json | 9 -
packages/ui/src/locales/nl-NL/index.json | 9 -
packages/ui/src/locales/pl-PL/index.json | 9 -
packages/ui/src/locales/pt-BR/index.json | 9 -
packages/ui/src/locales/ro-RO/index.json | 9 -
packages/ui/src/locales/ru-RU/index.json | 9 -
packages/ui/src/locales/sr-CS/index.json | 9 -
packages/ui/src/locales/sv-SE/index.json | 9 -
packages/ui/src/locales/th-TH/index.json | 4 -
packages/ui/src/locales/tr-TR/index.json | 9 -
packages/ui/src/locales/uk-UA/index.json | 9 -
packages/ui/src/locales/vi-VN/index.json | 9 -
packages/ui/src/locales/zh-CN/index.json | 9 -
packages/ui/src/locales/zh-TW/index.json | 9 -
.../ui/src/stories/base/Toggle.stories.ts | 16 +-
packages/ui/src/utils/common-messages.ts | 2 +-
46 files changed, 1065 insertions(+), 501 deletions(-)
create mode 100644 packages/ui/src/layouts/shared/content-tab/components/ContentCardItemIcon.vue
create mode 100644 packages/ui/src/layouts/shared/content-tab/components/ContentEnabledFor.vue
diff --git a/apps/app-frontend/src/pages/Browse.vue b/apps/app-frontend/src/pages/Browse.vue
index d859d2b88c5..5954c0236cc 100644
--- a/apps/app-frontend/src/pages/Browse.vue
+++ b/apps/app-frontend/src/pages/Browse.vue
@@ -909,17 +909,18 @@ function getCardActions(
const isQueued = queuedServerInstallProjectIds.value.has(projectResult.project_id)
const isQueuedRoot = queuedServerInstallRootProjectIds.value.has(projectResult.project_id)
const isInstallingSelection = isInstallingQueuedServerInstalls.value
+ const showAsInstalling = isInstalling || (isInstallingSelection && isQueuedRoot)
const validatingInstall =
isInstalling && currentProjectType !== 'modpack' && !isInstallingSelection
const installLabel = showAsInstalled
? commonMessages.installedLabel
: isQueued
- ? isInstalling || isInstallingSelection
+ ? showAsInstalling
? validatingInstall
? commonMessages.validatingLabel
: messages.installingToServer
: commonMessages.selectedLabel
- : isInstalling || isInstallingSelection
+ : showAsInstalling
? validatingInstall
? commonMessages.validatingLabel
: messages.installingToServer
@@ -928,16 +929,11 @@ function getCardActions(
{
key: 'install',
label: formatMessage(installLabel),
- icon:
- isInstalling || isInstallingSelection
- ? SpinnerIcon
- : isQueued || showAsInstalled
- ? CheckIcon
- : PlusIcon,
- iconClass: isInstalling || isInstallingSelection ? 'animate-spin' : undefined,
+ icon: showAsInstalling ? SpinnerIcon : isQueued || showAsInstalled ? CheckIcon : PlusIcon,
+ iconClass: showAsInstalling ? 'animate-spin' : undefined,
disabled:
showAsInstalled || isInstalling || isInstallingSelection || (isQueued && !isQueuedRoot),
- color: isQueued && !isInstalling && !isInstallingSelection ? 'green' : 'brand',
+ color: isQueued && !showAsInstalling ? 'green' : 'brand',
type: 'outlined',
onClick: async () => {
if (isQueuedRoot) {
diff --git a/apps/frontend/src/pages/discover/[type]/index.vue b/apps/frontend/src/pages/discover/[type]/index.vue
index 09213818fdb..74877228d4c 100644
--- a/apps/frontend/src/pages/discover/[type]/index.vue
+++ b/apps/frontend/src/pages/discover/[type]/index.vue
@@ -361,17 +361,18 @@ function getCardActions(
serverData.value.upstream?.project_id === result.project_id
const isInstalling = installingProjectIds.value.has(result.project_id)
const isInstallingSelection = isInstallingQueuedServerInstalls.value
+ const showAsInstalling = isInstalling || (isInstallingSelection && isQueuedRoot)
const validatingInstall =
isInstalling && currentProjectType !== 'modpack' && !isInstallingSelection
const installLabel = isInstalled
? formatMessage(commonMessages.installedLabel)
: isQueued
- ? isInstalling || isInstallingSelection
+ ? showAsInstalling
? validatingInstall
? formatMessage(commonMessages.validatingLabel)
: formatMessage(commonMessages.installingLabel)
: formatMessage(commonMessages.selectedLabel)
- : isInstalling || isInstallingSelection
+ : showAsInstalling
? validatingInstall
? formatMessage(commonMessages.validatingLabel)
: formatMessage(commonMessages.installingLabel)
@@ -381,16 +382,11 @@ function getCardActions(
{
key: 'install',
label: installLabel,
- icon:
- isInstalling || isInstallingSelection
- ? SpinnerIcon
- : isQueued || isInstalled
- ? CheckIcon
- : DownloadIcon,
- iconClass: isInstalling || isInstallingSelection ? 'animate-spin' : undefined,
+ icon: showAsInstalling ? SpinnerIcon : isQueued || isInstalled ? CheckIcon : DownloadIcon,
+ iconClass: showAsInstalling ? 'animate-spin' : undefined,
disabled:
!!isInstalled || isInstalling || isInstallingSelection || (isQueued && !isQueuedRoot),
- color: isQueued && !isInstalling && !isInstallingSelection ? 'green' : 'brand',
+ color: isQueued && !showAsInstalling ? 'green' : 'brand',
type: 'outlined',
onClick: () => serverInstall(projectResult),
},
diff --git a/packages/api-client/src/modules/archon/content/v1.ts b/packages/api-client/src/modules/archon/content/v1.ts
index fa0c951d6f2..71aedc9bd1f 100644
--- a/packages/api-client/src/modules/archon/content/v1.ts
+++ b/packages/api-client/src/modules/archon/content/v1.ts
@@ -105,6 +105,57 @@ export class ArchonContentV1Module extends AbstractModule {
})
}
+ /** POST /v1/:server_id/worlds/:world_id/addons/set-enabled-server */
+ public async setAddonEnabledServer(
+ serverId: string,
+ worldId: string,
+ request: Archon.Content.v1.SetAddonEnabledRequest,
+ ): Promise {
+ await this.client.request(
+ `/servers/${serverId}/worlds/${worldId}/addons/set-enabled-server`,
+ {
+ api: 'archon',
+ version: 1,
+ method: 'POST',
+ body: request,
+ },
+ )
+ }
+
+ /** POST /v1/:server_id/worlds/:world_id/addons/set-enabled-player */
+ public async setAddonEnabledPlayer(
+ serverId: string,
+ worldId: string,
+ request: Archon.Content.v1.SetAddonEnabledRequest,
+ ): Promise {
+ await this.client.request(
+ `/servers/${serverId}/worlds/${worldId}/addons/set-enabled-player`,
+ {
+ api: 'archon',
+ version: 1,
+ method: 'POST',
+ body: request,
+ },
+ )
+ }
+
+ /** POST /v1/:server_id/worlds/:world_id/addons/set-locked-side-toggle */
+ public async setAddonSideToggleLocked(
+ serverId: string,
+ worldId: string,
+ request: Archon.Content.v1.SetAddonSideToggleLockedRequest,
+ ): Promise {
+ await this.client.request(
+ `/servers/${serverId}/worlds/${worldId}/addons/set-locked-side-toggle`,
+ {
+ api: 'archon',
+ version: 1,
+ method: 'POST',
+ body: request,
+ },
+ )
+ }
+
/** POST /v1/:server_id/worlds/:world_id/addons/delete-many */
public async deleteAddons(
serverId: string,
diff --git a/packages/api-client/src/modules/archon/types.ts b/packages/api-client/src/modules/archon/types.ts
index a6c916a5b61..0c04de3304e 100644
--- a/packages/api-client/src/modules/archon/types.ts
+++ b/packages/api-client/src/modules/archon/types.ts
@@ -302,6 +302,25 @@ export namespace Archon {
environment?: Labrinth.Projects.v3.Environment | null
}
+ export type AddonManifestEnvironment =
+ | 'client_and_server'
+ | 'client_only'
+ | 'dedicated_server_only'
+
+ export type AddonManifestWarnings = {
+ multiple_mod_entries: number | null
+ malformed: boolean
+ }
+
+ export type AddonManifest = {
+ platform: Modloader
+ name: string | null
+ version: string | null
+ environment: AddonManifestEnvironment | null
+ icon_embedded: boolean
+ warnings: AddonManifestWarnings
+ }
+
export type AddonStatus =
| 'pending'
| 'installed'
@@ -317,6 +336,10 @@ export namespace Archon {
filesize: number
btime?: string
disabled: boolean
+ disabled_server: boolean
+ disabled_player: boolean
+ side_toggle_unlocked: boolean
+ manifest: AddonManifest | null
kind: AddonKind
from_modpack: boolean
status: AddonStatus
@@ -355,6 +378,14 @@ export namespace Archon {
filename: string
}
+ export type SetAddonEnabledRequest = RemoveAddonRequest & {
+ enabled: boolean
+ }
+
+ export type SetAddonSideToggleLockedRequest = RemoveAddonRequest & {
+ locked: boolean
+ }
+
export type UpdateAddonRequest = {
filename: string
version_id?: string | null
@@ -1100,6 +1131,7 @@ export namespace Archon {
project_id: string | null
pack_client_retained: boolean
pack_client_depends: boolean
+ manifest?: Archon.Content.v1.AddonManifest | null
status: Archon.Content.v1.AddonStatus
filesize: number | null
name: string | null
diff --git a/packages/api-client/src/modules/kyros/content/v1.ts b/packages/api-client/src/modules/kyros/content/v1.ts
index 9f5f4f8b160..02d32927a88 100644
--- a/packages/api-client/src/modules/kyros/content/v1.ts
+++ b/packages/api-client/src/modules/kyros/content/v1.ts
@@ -7,6 +7,24 @@ export class KyrosContentV1Module extends AbstractModule {
return 'kyros_content_v1'
}
+ /** GET /v1/worlds/:world_id/content/embedded-icon */
+ public async getEmbeddedAddonIcon(
+ worldId: string,
+ parentDirectory: 'mods' | 'plugins',
+ filename: string,
+ ): Promise {
+ return this.client.request(`/worlds/${worldId}/content/embedded-icon`, {
+ api: '',
+ version: 'v1',
+ method: 'GET',
+ params: {
+ parent_directory: parentDirectory,
+ filename,
+ },
+ useNodeAuth: true,
+ })
+ }
+
/**
* Upload addon files to a world via multipart form data
*
diff --git a/packages/ui/src/components/base/Toggle.vue b/packages/ui/src/components/base/Toggle.vue
index 4ffa62d33ca..ddc0153102f 100644
--- a/packages/ui/src/components/base/Toggle.vue
+++ b/packages/ui/src/components/base/Toggle.vue
@@ -3,12 +3,12 @@
:id="id"
type="button"
role="switch"
- :aria-checked="modelValue"
+ :aria-checked="indeterminate ? 'mixed' : modelValue"
:disabled="disabled"
class="group inline-flex shrink-0 touch-manipulation items-center rounded-full m-0 p-1 transition-all duration-200 cursor-pointer border border-solid border-surface-5"
:class="[
small ? 'h-5 !w-[40px]' : 'h-6 !w-[48px]',
- modelValue ? 'bg-brand' : 'bg-button-bg',
+ indeterminate || modelValue ? 'bg-brand' : 'bg-button-bg',
disabled ? 'opacity-50 cursor-not-allowed' : '',
]"
@click="toggle"
@@ -16,17 +16,24 @@
@@ -37,6 +44,7 @@ const props = defineProps<{
id?: string
disabled?: boolean
small?: boolean
+ indeterminate?: boolean
}>()
const modelValue = defineModel()
diff --git a/packages/ui/src/composables/server-panel-sync.ts b/packages/ui/src/composables/server-panel-sync.ts
index 30541a240ef..400cc150705 100644
--- a/packages/ui/src/composables/server-panel-sync.ts
+++ b/packages/ui/src/composables/server-panel-sync.ts
@@ -254,17 +254,48 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
}
const content = worldContentUpdateToAddons(event)
- queryClient.setQueryData(contentListKey(serverId), {
+ queryClient.setQueryData(contentListKey(serverId), (current) => ({
...content,
- addons: content.addons?.filter((addon) => !addon.from_modpack) ?? null,
- })
- queryClient.setQueryData(modpackContentListKey(serverId), {
- ...content,
- addons: content.addons?.filter((addon) => addon.from_modpack) ?? null,
- })
+ addons: mergeWorldContentSideState(
+ current?.addons ?? [],
+ content.addons?.filter((addon) => !addon.from_modpack) ?? [],
+ ),
+ }))
+ queryClient.setQueryData(
+ modpackContentListKey(serverId),
+ (current) => ({
+ ...content,
+ addons: mergeWorldContentSideState(
+ current?.addons ?? [],
+ content.addons?.filter((addon) => addon.from_modpack) ?? [],
+ ),
+ }),
+ )
+ void queryClient.invalidateQueries({ queryKey: contentListKey(serverId) })
+ void queryClient.invalidateQueries({ queryKey: modpackContentListKey(serverId) })
void queryClient.invalidateQueries({ queryKey: serverV1DetailKey(serverId) })
}
+ function mergeWorldContentSideState(
+ currentAddons: Archon.Content.v1.Addon[],
+ incomingAddons: Archon.Content.v1.Addon[],
+ ) {
+ const currentByFilename = new Map(
+ currentAddons.map((addon) => [normalizeAddonFilename(addon.filename), addon] as const),
+ )
+ return incomingAddons.map((incoming) => {
+ const current = currentByFilename.get(normalizeAddonFilename(incoming.filename))
+ return current
+ ? {
+ ...incoming,
+ disabled_server: current.disabled_server,
+ disabled_player: current.disabled_player,
+ side_toggle_unlocked: current.side_toggle_unlocked,
+ }
+ : incoming
+ })
+ }
+
function worldContentUpdateToAddons(
event: Archon.Sync.v1.WorldContentUpdateEvent,
): Archon.Content.v1.Addons {
@@ -318,7 +349,11 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
filename: item.filename,
filesize: item.filesize ?? 0,
btime: item.btime,
- disabled: item.filename.endsWith('.disabled'),
+ disabled: false,
+ disabled_server: false,
+ disabled_player: false,
+ side_toggle_unlocked: false,
+ manifest: item.manifest ?? null,
kind: parentDirectoryToAddonKind(item.parent_directory),
from_modpack: item.from_modpack,
status: item.status,
diff --git a/packages/ui/src/composables/virtual-scroll.ts b/packages/ui/src/composables/virtual-scroll.ts
index 9bfbfc1456c..54bc6d54bb3 100644
--- a/packages/ui/src/composables/virtual-scroll.ts
+++ b/packages/ui/src/composables/virtual-scroll.ts
@@ -7,7 +7,7 @@ export interface ScrollViewportOptions {
}
export interface VirtualScrollOptions {
- itemHeight: number
+ itemHeight: number | Ref
bufferSize?: number
initialItemCount?: number
enabled?: Ref
@@ -140,6 +140,9 @@ export function useVirtualScroll(items: Ref, options: VirtualScrollOptio
onNearEnd,
nearEndThreshold = 0.2,
} = options
+ const resolvedItemHeight = computed(() =>
+ typeof itemHeight === 'number' ? itemHeight : itemHeight.value,
+ )
const {
containerOffset,
@@ -153,7 +156,7 @@ export function useVirtualScroll(items: Ref, options: VirtualScrollOptio
onScroll: checkNearEnd,
})
- const totalHeight = computed(() => items.value.length * itemHeight)
+ const totalHeight = computed(() => items.value.length * resolvedItemHeight.value)
const visibleRange = computed(() => {
if (enabled && !enabled.value) {
@@ -164,8 +167,8 @@ export function useVirtualScroll(items: Ref, options: VirtualScrollOptio
return { start: 0, end: Math.min(items.value.length, initialItemCount) }
}
- const start = Math.floor(relativeScrollTop.value / itemHeight)
- const visibleCount = Math.ceil(viewportHeight.value / itemHeight)
+ const start = Math.floor(relativeScrollTop.value / resolvedItemHeight.value)
+ const visibleCount = Math.ceil(viewportHeight.value / resolvedItemHeight.value)
const rangeSize = visibleCount + bufferSize * 2
const rangeStart = Math.min(
@@ -181,7 +184,7 @@ export function useVirtualScroll(items: Ref, options: VirtualScrollOptio
})
const visibleTop = computed(() =>
- enabled && !enabled.value ? 0 : visibleRange.value.start * itemHeight,
+ enabled && !enabled.value ? 0 : visibleRange.value.start * resolvedItemHeight.value,
)
const visibleItems = computed(() =>
diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
index feefeefd210..bb71a6e8cba 100644
--- a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
+++ b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
@@ -29,11 +29,16 @@ import { truncatedTooltip } from '#ui/utils/truncate'
import type {
ClientWarningType,
+ ContentCardEmbeddedIcon,
ContentCardProject,
ContentCardVersion,
+ ContentEnabledForState,
ContentOwner,
+ ContentSide,
ContentSource,
} from '../types'
+import ContentCardItemIcon from './ContentCardItemIcon.vue'
+import ContentEnabledFor from './ContentEnabledFor.vue'
const { formatMessage } = useVIntl()
@@ -90,6 +95,8 @@ interface Props {
hideDelete?: boolean
hideActions?: boolean
inline?: boolean
+ enabledFor?: ContentEnabledForState
+ embeddedIcon?: ContentCardEmbeddedIcon
}
const props = withDefaults(defineProps(), {
@@ -120,6 +127,8 @@ const props = withDefaults(defineProps(), {
hideDelete: false,
hideActions: false,
inline: false,
+ enabledFor: undefined,
+ embeddedIcon: undefined,
})
const selected = defineModel('selected')
@@ -128,6 +137,7 @@ const projectTitle = computed(() => props.project.title.replace(/§[0-9a-fk-orx]
const emit = defineEmits<{
'update:enabled': [value: boolean]
+ 'update:enabled-for': [side: ContentSide, value: boolean]
select: [value: boolean, event?: MouseEvent]
delete: [event: MouseEvent]
update: []
@@ -149,6 +159,11 @@ const isToggleDisabled = computed(() => isDisabled.value || props.toggleDisabled
const syncStatusLabel = computed(() =>
formatMessage(props.syncUpdatePending ? messages.syncUpdatePending : messages.synced),
)
+const toggleTooltip = computed(() => {
+ if (!isToggleDisabled.value) return undefined
+ return props.toggleDisabledTooltip ?? props.disabledTooltip ?? undefined
+})
+const isEnabledForDisabled = computed(() => !props.enabledFor?.server && !props.enabledFor?.player)
const clientWarningMessage = computed(() => {
switch (props.clientWarning) {
@@ -173,12 +188,15 @@ const installTooltip = computed(() => {
{
:class="
hideActions || !showVersion
? 'flex-1'
- : 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
+ : enabledFor
+ ? 'flex-1 @[800px]:w-[340px] @[800px]:shrink-0 @[800px]:flex-none'
+ : 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
"
>
{
+
+ emit('update:enabled-for', side, value)"
+ />
+
+
@@ -368,7 +396,8 @@ const installTooltip = computed(() => {
{
+import { useQuery } from '@tanstack/vue-query'
+import { computed, onScopeDispose, ref, watch } from 'vue'
+
+import Avatar from '#ui/components/base/Avatar.vue'
+
+import type { ContentCardEmbeddedIcon } from '../types'
+
+const props = defineProps<{
+ src?: string | null
+ fallbackUrl?: string | null
+ embeddedIcon?: ContentCardEmbeddedIcon
+ alt: string
+ tintBy?: string
+}>()
+
+const queryKey = computed(
+ () => props.embeddedIcon?.queryKey ?? (['content', 'embedded-icon', 'disabled'] as const),
+)
+
+const embeddedIconQuery = useQuery({
+ queryKey,
+ queryFn: () => {
+ if (!props.embeddedIcon) throw new Error('Missing embedded icon request')
+ return props.embeddedIcon.queryFn()
+ },
+ enabled: computed(
+ () => typeof window !== 'undefined' && !props.src && props.embeddedIcon !== undefined,
+ ),
+ staleTime: Infinity,
+})
+
+const embeddedIconUrl = ref()
+let pendingIconUrl: string | undefined
+let validationId = 0
+
+watch(
+ () => embeddedIconQuery.data.value,
+ (blob) => {
+ validationId += 1
+ const currentValidationId = validationId
+ if (embeddedIconUrl.value) URL.revokeObjectURL(embeddedIconUrl.value)
+ if (pendingIconUrl) URL.revokeObjectURL(pendingIconUrl)
+ embeddedIconUrl.value = undefined
+ pendingIconUrl = undefined
+ if (!blob) return
+
+ const candidateUrl = URL.createObjectURL(blob)
+ pendingIconUrl = candidateUrl
+ const image = new Image()
+ image.onload = () => {
+ if (currentValidationId !== validationId) return
+ pendingIconUrl = undefined
+ embeddedIconUrl.value = candidateUrl
+ }
+ image.onerror = () => {
+ if (currentValidationId !== validationId) return
+ pendingIconUrl = undefined
+ URL.revokeObjectURL(candidateUrl)
+ }
+ image.src = candidateUrl
+ },
+ { immediate: true },
+)
+
+onScopeDispose(() => {
+ validationId += 1
+ if (embeddedIconUrl.value) URL.revokeObjectURL(embeddedIconUrl.value)
+ if (pendingIconUrl) URL.revokeObjectURL(pendingIconUrl)
+})
+
+const resolvedSrc = computed(
+ () =>
+ props.src ??
+ embeddedIconUrl.value ??
+ props.embeddedIcon?.fallbackUrl ??
+ props.fallbackUrl ??
+ undefined,
+)
+
+
+
+
+
diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue
index 02a606d4dc2..0ae7d96f63c 100644
--- a/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue
+++ b/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue
@@ -1,9 +1,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui/src/layouts/shared/content-tab/components/managed-content-modal/index.vue b/packages/ui/src/layouts/shared/content-tab/components/managed-content-modal/index.vue
index 91a322254fa..12617e0dccd 100644
--- a/packages/ui/src/layouts/shared/content-tab/components/managed-content-modal/index.vue
+++ b/packages/ui/src/layouts/shared/content-tab/components/managed-content-modal/index.vue
@@ -8,6 +8,7 @@ import {
PaintbrushIcon,
SearchIcon,
SpinnerIcon,
+ UnknownIcon,
} from '@modrinth/assets'
import Fuse from 'fuse.js'
import { computed, nextTick, ref, watchSyncEffect } from 'vue'
@@ -30,7 +31,12 @@ import {
} from '#ui/utils/common-messages'
import { getClientWarningType } from '../../composables/content-filtering'
-import type { ContentCardProject, ContentCardTableItem, ContentItem } from '../../types'
+import type {
+ ContentCardProject,
+ ContentCardTableItem,
+ ContentItem,
+ ContentSide,
+} from '../../types'
import ContentCardTable from '../ContentCardTable.vue'
import ContentSelectionBar from '../ContentSelectionBar.vue'
@@ -44,6 +50,7 @@ interface Props {
sourceName?: string
sourceIconUrl?: string
enableToggle?: boolean
+ enableEnabledFor?: boolean
actionDisabled?: boolean
actionDisabledTooltip?: string | null
getOverflowOptions?: (item: ContentItem) => ButtonMenuOption[]
@@ -60,6 +67,7 @@ const props = withDefaults(defineProps(), {
sourceName: undefined,
sourceIconUrl: undefined,
enableToggle: false,
+ enableEnabledFor: false,
actionDisabled: false,
actionDisabledTooltip: undefined,
getOverflowOptions: undefined,
@@ -71,6 +79,7 @@ const props = withDefaults(defineProps(), {
const emit = defineEmits<{
'update:enabled': [item: ContentItem, value: boolean]
+ 'update:enabled-for': [item: ContentItem, side: ContentSide, value: boolean]
'bulk:enable': [items: ContentItem[]]
'bulk:disable': [items: ContentItem[]]
hide: []
@@ -129,6 +138,18 @@ const messages = defineMessages({
id: 'instances.managed-content-modal.disabled',
defaultMessage: 'Disabled',
},
+ enabledFor: {
+ id: 'content.enabled-for.label',
+ defaultMessage: 'Enabled for',
+ },
+ enabledForDescription: {
+ id: 'content.enabled-for.description',
+ defaultMessage: 'Choose where this content is enabled. Turn both off to disable it.',
+ },
+ pleaseWait: {
+ id: 'content.enabled-for.please-wait',
+ defaultMessage: 'Please wait',
+ },
})
export interface ManagedContentModalState {
@@ -285,6 +306,7 @@ const tableItems = computed(() =>
icon_url: item.embedded_metadata?.icon_url ?? null,
},
projectLink: !item.external && item.project?.id ? `/project/${item.project.id}` : undefined,
+ embeddedIcon: item.embeddedIcon,
version: props.showVersion
? (item.version ?? {
id: item.id,
@@ -307,18 +329,26 @@ const tableItems = computed(() =>
link: item.source.link ?? sourceProjectLink(item.source.project),
}
: undefined,
- ...(props.enableToggle ? { enabled: item.enabled } : {}),
+ ...(props.enableToggle || props.enableEnabledFor ? { enabled: item.enabled } : {}),
+ ...(props.enableEnabledFor ? { enabledFor: item.enabledFor } : {}),
synced: !!item.synced_pack,
syncUpdatePending: item.synced_pack?.update_pending,
locked: item.locked,
installing: item.installing === true,
toggleDisabled: props.actionDisabled,
toggleDisabledTooltip: props.actionDisabled ? props.actionDisabledTooltip : undefined,
- isClientOnly: getClientWarningType(item, props.showEnvironmentWarnings) !== null,
- clientWarning: getClientWarningType(item, props.showEnvironmentWarnings),
+ isClientOnly:
+ !props.enableEnabledFor && getClientWarningType(item, props.showEnvironmentWarnings) !== null,
+ clientWarning: props.enableEnabledFor
+ ? null
+ : getClientWarningType(item, props.showEnvironmentWarnings),
disabled:
props.actionDisabled || disabledIds.value.has(item.file_name) || item.installing === true,
- disabledTooltip: props.actionDisabled ? props.actionDisabledTooltip : undefined,
+ disabledTooltip: props.actionDisabled
+ ? props.actionDisabledTooltip
+ : disabledIds.value.has(item.file_name)
+ ? formatMessage(messages.pleaseWait)
+ : undefined,
overflowOptions: [
...(props.switchVersion && !item.locked && item.project?.id && item.version?.id
? [
@@ -356,7 +386,9 @@ const externalUrls = computed(() => {
return urls
})
const hasExternalSlicerUrls = computed(() => Object.keys(externalSlicerUrls.value).length > 0)
-const showTableActions = computed(() => props.enableToggle || hasExternalSlicerUrls.value)
+const showTableActions = computed(
+ () => props.enableToggle || props.enableEnabledFor || hasExternalSlicerUrls.value,
+)
function getTypeIcon(type: string) {
switch (type) {
@@ -399,6 +431,13 @@ function handleEnabledChange(id: string, value: boolean) {
emit('update:enabled', item, value)
}
+function handleEnabledForChange(id: string, side: ContentSide, value: boolean) {
+ if (props.actionDisabled || !props.enableEnabledFor) return
+ const item = items.value.find((item) => item.id === id)
+ if (!item) return
+ emit('update:enabled-for', item, side, value)
+}
+
function bulkEnable() {
if (props.actionDisabled) return
emit('bulk:enable', [...toggleableSelectedItems.value])
@@ -504,8 +543,12 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
@@ -579,13 +622,16 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
@@ -601,16 +647,39 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
formatMessage(commonMessages.projectLabel)
}}
+
+ {{
+ formatMessage(messages.enabledFor)
+ }}
+
+
{{
formatMessage(commonMessages.versionLabel)
}}
-
+
{{
formatMessage(commonMessages.actionsLabel)
}}
@@ -624,14 +693,16 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
:show-selection="props.enableToggle"
:show-item-actions="showTableActions"
:show-version="showVersion"
+ :show-enabled-for-column="props.enableEnabledFor"
hide-delete
hide-header
flat
v-on="
- props.enableToggle
+ props.enableToggle || props.enableEnabledFor
? { 'update:enabled': (id: string, val: boolean) => handleEnabledChange(id, val) }
: {}
"
+ @update:enabled-for="handleEnabledForChange"
>
(`content-sort:${ctx.filterPersistKey}`, 'alphabetical-asc')
- : ref('alphabetical-asc')
+ ? useSessionStorage(`content-sort:${ctx.filterPersistKey}`, defaultSortMode)
+ : ref(defaultSortMode)
+
+const validSortModes: readonly string[] = [
+ 'alphabetical-asc',
+ 'alphabetical-desc',
+ 'date-added-newest',
+ 'date-added-oldest',
+]
+if (!validSortModes.includes(sortMode.value)) sortMode.value = defaultSortMode
const sortLabels: Record string> = {
'alphabetical-asc': () => formatMessage(messages.sortAlphabeticalAscending),
@@ -205,32 +214,36 @@ const sortLabels: Record string> = {
'date-added-oldest': () => formatMessage(messages.sortDateAddedOldest),
}
-const sortOptions = computed(() => [
- {
- id: 'alphabetical-asc',
- label: formatMessage(messages.sortAlphabeticalAscending),
- icon: ArrowDownAZIcon,
- action: () => (sortMode.value = 'alphabetical-asc'),
- },
- {
- id: 'alphabetical-desc',
- label: formatMessage(messages.sortAlphabeticalDescending),
- icon: ArrowUpZAIcon,
- action: () => (sortMode.value = 'alphabetical-desc'),
- },
- {
- id: 'date-added-newest',
- label: formatMessage(messages.sortDateAddedNewest),
- icon: ClockArrowDownIcon,
- action: () => (sortMode.value = 'date-added-newest'),
- },
- {
- id: 'date-added-oldest',
- label: formatMessage(messages.sortDateAddedOldest),
- icon: ClockArrowUpIcon,
- action: () => (sortMode.value = 'date-added-oldest'),
- },
-])
+const sortOptions = computed(() => {
+ const options: ButtonMenuOption[] = [
+ {
+ id: 'alphabetical-asc',
+ label: formatMessage(messages.sortAlphabeticalAscending),
+ icon: ArrowDownAZIcon,
+ action: () => (sortMode.value = 'alphabetical-asc'),
+ },
+ {
+ id: 'alphabetical-desc',
+ label: formatMessage(messages.sortAlphabeticalDescending),
+ icon: ArrowUpZAIcon,
+ action: () => (sortMode.value = 'alphabetical-desc'),
+ },
+ {
+ id: 'date-added-newest',
+ label: formatMessage(messages.sortDateAddedNewest),
+ icon: ClockArrowDownIcon,
+ action: () => (sortMode.value = 'date-added-newest'),
+ },
+ {
+ id: 'date-added-oldest',
+ label: formatMessage(messages.sortDateAddedOldest),
+ icon: ClockArrowUpIcon,
+ action: () => (sortMode.value = 'date-added-oldest'),
+ },
+ ]
+
+ return options
+})
const sortedItems = computed(() => {
const items = [...ctx.items.value]
@@ -450,14 +463,22 @@ const tableItems = computed(() => {
const base = ctx.mapToTableItem(item)
const id = getItemId(item)
const locked = base.locked ?? item.locked ?? false
- const clientWarning = getClientWarningType(item, ctx.showEnvironmentWarnings)
+ const clientWarning = base.enabledFor
+ ? null
+ : getClientWarningType(item, ctx.showEnvironmentWarnings)
return {
...base,
id,
locked,
disabled:
isChanging(id) || ctx.isBusy.value || isBulkOperating.value || item.installing === true,
- disabledTooltip: ctx.isBusy.value ? (ctx.busyMessage?.value ?? null) : null,
+ disabledTooltip: ctx.isBusy.value
+ ? (ctx.busyMessage?.value ?? null)
+ : isChanging(id)
+ ? formatMessage(messages.pleaseWait)
+ : item.installing
+ ? formatMessage(commonMessages.installingLabel)
+ : null,
toggleDisabled: ctx.isBusy.value || base.toggleDisabled,
toggleDisabledTooltip: ctx.isBusy.value
? (ctx.busyMessage?.value ?? null)
@@ -531,7 +552,10 @@ function canDeleteItem(item: ContentItem) {
}
function canToggleItem(item: ContentItem) {
- return ctx.canToggleItem?.(item) ?? true
+ return (
+ ctx.canToggleItem?.(item) ??
+ !!(ctx.toggleEnabled || ctx.bulkEnableItems || ctx.bulkDisableItems)
+ )
}
const deletableSelectedItems = computed(() => selectedItems.value.filter(canDeleteItem))
@@ -654,6 +678,7 @@ async function disableItemsWithoutWarning(items: ContentItem[]) {
await ctx.bulkDisableItems(items)
return
}
+ if (!ctx.toggleEnabled) return
for (const item of items) {
const id = getItemId(item)
@@ -768,7 +793,7 @@ async function confirmDisable() {
if (ctx.bulkDisableItems) {
await ctx.bulkDisableItems(itemsToDisable)
} else {
- await ctx.toggleEnabled(item)
+ await ctx.toggleEnabled?.(item)
}
} finally {
unmarkChanging(id)
@@ -782,7 +807,7 @@ async function confirmDisable() {
}
async function handleToggleEnabledById(id: string, _value: boolean) {
- if (ctx.isBusy.value) return
+ if (ctx.isBusy.value || !ctx.toggleEnabled) return
const item = ctx.items.value.find((i) => getItemId(i) === id)
if (!item) return
if (!canToggleItem(item)) return
@@ -822,7 +847,8 @@ async function bulkEnable() {
}
return
}
- await runBulk('enable', items, (item) => ctx.toggleEnabled(item), { onComplete: clearSelection })
+ if (!ctx.toggleEnabled) return
+ await runBulk('enable', items, (item) => ctx.toggleEnabled!(item), { onComplete: clearSelection })
}
async function bulkDisable() {
@@ -845,6 +871,14 @@ function handleSwitchVersionById(id: string) {
}
}
+async function handleSetEnabledForById(id: string, side: 'server' | 'player', enabled: boolean) {
+ if (ctx.isBusy.value || !ctx.setEnabledFor) return
+ const item = ctx.items.value.find((candidate) => getItemId(candidate) === id)
+ if (!item) return
+
+ await ctx.setEnabledFor(item, side, enabled)
+}
+
const queryClient = useQueryClient()
const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()
@@ -1401,7 +1435,9 @@ const confirmUnlinkModal = ref>()
:items="tableItems"
:highlighted-item-id="highlightedItemId"
:show-selection="true"
+ :show-enabled-for-column="!!ctx.setEnabledFor"
@update:enabled="handleToggleEnabledById"
+ @update:enabled-for="handleSetEnabledForById"
@delete="handleDeleteById"
@update="handleUpdateById"
@switch-version="handleSwitchVersionById"
diff --git a/packages/ui/src/layouts/shared/content-tab/providers/content-manager.ts b/packages/ui/src/layouts/shared/content-tab/providers/content-manager.ts
index 2edf9327414..435ac15d3c1 100644
--- a/packages/ui/src/layouts/shared/content-tab/providers/content-manager.ts
+++ b/packages/ui/src/layouts/shared/content-tab/providers/content-manager.ts
@@ -8,6 +8,7 @@ import type {
ContentActionWarning,
ContentCardTableItem,
ContentItem,
+ ContentSide,
ManagedContentCardData,
} from '../types'
@@ -48,7 +49,8 @@ export interface ContentManagerContext {
currentLoader?: Ref | ComputedRef
// Core actions
- toggleEnabled: (item: ContentItem) => Promise
+ toggleEnabled?: (item: ContentItem) => Promise
+ setEnabledFor?: (item: ContentItem, side: ContentSide, enabled: boolean) => Promise
deleteItem: (item: ContentItem) => Promise
refresh: () => Promise
browse: () => void
diff --git a/packages/ui/src/layouts/shared/content-tab/types.ts b/packages/ui/src/layouts/shared/content-tab/types.ts
index f5cb9e76a50..16e1ea6b704 100644
--- a/packages/ui/src/layouts/shared/content-tab/types.ts
+++ b/packages/ui/src/layouts/shared/content-tab/types.ts
@@ -50,6 +50,22 @@ export interface EmbeddedContentMetadata {
icon_url?: string | null
}
+export type ContentSide = 'server' | 'player'
+
+export interface ContentEnabledForState {
+ server: boolean
+ player: boolean
+ locked: boolean
+ disabledSides?: ContentSide[]
+ warningTooltip?: string | null
+}
+
+export interface ContentCardEmbeddedIcon {
+ queryKey: readonly unknown[]
+ queryFn: () => Promise
+ fallbackUrl?: string | null
+}
+
export interface ContentCardTableItem {
id: string
project: ContentCardProject
@@ -76,6 +92,8 @@ export interface ContentCardTableItem {
hideDelete?: boolean
hideSwitchVersion?: boolean
overflowOptions?: ButtonMenuOption[]
+ enabledFor?: ContentEnabledForState
+ embeddedIcon?: ContentCardEmbeddedIcon
}
export type ContentCardTableSortColumn = 'project' | 'version'
diff --git a/packages/ui/src/layouts/shared/server-settings/pages/installation.vue b/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
index b15621d212b..798988a1c8e 100644
--- a/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
+++ b/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
@@ -425,6 +425,26 @@ async function uploadLocalModpackWithSoftOverride() {
return true
}
+async function disableAddonsEverywhere(addons: Archon.Content.v1.Addon[]) {
+ await Promise.all(
+ addons.flatMap((addon) => {
+ const request: Archon.Content.v1.SetAddonEnabledRequest = {
+ kind: addon.kind,
+ filename: addon.filename,
+ enabled: false,
+ }
+ return [
+ ...(!addon.disabled_server
+ ? [client.archon.content_v1.setAddonEnabledServer(serverId, worldId.value!, request)]
+ : []),
+ ...(!addon.disabled_player
+ ? [client.archon.content_v1.setAddonEnabledPlayer(serverId, worldId.value!, request)]
+ : []),
+ ]
+ }),
+ )
+}
+
provideInstallationSettings({
closeSettings: serverSettings.closeModal,
afterSave: async () => {
@@ -824,12 +844,12 @@ provideInstallationSettings({
if (setupActionDisabled.value) return
debug('disableAllContent: fetching all addons')
const addons = await client.archon.content_v1.getAddons(serverId, worldId.value!)
- const items = (addons.addons ?? [])
- .filter((a) => !a.disabled)
- .map((a) => ({ kind: a.kind, filename: a.filename }))
- if (items.length > 0) {
- debug('disableAllContent: disabling', items.length, 'addons')
- await client.archon.content_v1.disableAddons(serverId, worldId.value!, items)
+ const activeAddons = (addons.addons ?? []).filter(
+ (addon) => !addon.disabled_server || !addon.disabled_player,
+ )
+ if (activeAddons.length > 0) {
+ debug('disableAllContent: disabling', activeAddons.length, 'addons')
+ await disableAddonsEverywhere(activeAddons)
}
debug('disableAllContent: done')
},
@@ -838,7 +858,9 @@ provideInstallationSettings({
if (setupActionDisabled.value) return
debug('disableIncompatibleContent: fetching addons')
const addons = await client.archon.content_v1.getAddons(serverId, worldId.value!)
- const activeAddons = (addons.addons ?? []).filter((a) => !a.disabled)
+ const activeAddons = (addons.addons ?? []).filter(
+ (addon) => !addon.disabled_server || !addon.disabled_player,
+ )
const modrinthAddons = activeAddons.filter((a) => a.version?.id)
const customAddons = activeAddons.filter((a) => !a.version?.id)
@@ -861,7 +883,12 @@ provideInstallationSettings({
if (incompatibleItems.length > 0) {
debug('disableIncompatibleContent: disabling', incompatibleItems.length, 'addons')
- await client.archon.content_v1.disableAddons(serverId, worldId.value!, incompatibleItems)
+ const incompatibleKeys = new Set(
+ incompatibleItems.map((item) => `${item.kind}:${item.filename}`),
+ )
+ await disableAddonsEverywhere(
+ activeAddons.filter((addon) => incompatibleKeys.has(`${addon.kind}:${addon.filename}`)),
+ )
}
debug('disableIncompatibleContent: done')
},
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
index 13ac74aec89..6ec05af81ef 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
@@ -1,10 +1,11 @@
diff --git a/apps/frontend/src/composables/featureFlags.ts b/apps/frontend/src/composables/featureFlags.ts
index 0e30be1e8a2..d1d3d3b610b 100644
--- a/apps/frontend/src/composables/featureFlags.ts
+++ b/apps/frontend/src/composables/featureFlags.ts
@@ -59,7 +59,6 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
alwaysShowVersionDevInfo: false,
advancedFiltersCollapsed: true,
dismissedPhotosensitivityFilterWarning: false,
- ServerPlayFrontend: false,
} as const)
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
diff --git a/apps/frontend/src/pages/hosting/manage/[id].vue b/apps/frontend/src/pages/hosting/manage/[id].vue
index 9e218386abf..5f6d5245f42 100644
--- a/apps/frontend/src/pages/hosting/manage/[id].vue
+++ b/apps/frontend/src/pages/hosting/manage/[id].vue
@@ -5,7 +5,6 @@
:resolve-viewer="resolveViewer"
:show-copy-id-action="flags.developerMode"
:show-advanced-debug-info="flags.advancedDebugInfo"
- :show-play-tab="flags.ServerPlayFrontend"
:stripe-publishable-key="config.public.stripePublishableKey as string"
:site-url="config.public.siteUrl as string"
:products="products"
diff --git a/apps/frontend/src/pages/hosting/manage/[id]/play.vue b/apps/frontend/src/pages/hosting/manage/[id]/play.vue
index b4866d98f76..0c23d78a03e 100644
--- a/apps/frontend/src/pages/hosting/manage/[id]/play.vue
+++ b/apps/frontend/src/pages/hosting/manage/[id]/play.vue
@@ -2,12 +2,6 @@
import { injectModrinthServerContext, ServersManagePlayPage } from '@modrinth/ui'
const { server } = injectModrinthServerContext()
-const flags = useFeatureFlags()
-const route = useNativeRoute()
-
-if (!flags.value.ServerPlayFrontend) {
- await navigateTo(`/hosting/manage/${String(route.params.id)}`, { replace: true })
-}
useHead({
title: computed(() => `Play - ${server.value?.name ?? 'Server'} - Modrinth`),
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
index f610e61cd8f..d1f7a8a433d 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
@@ -433,7 +433,6 @@ const props = withDefaults(
showCopyIdAction?: boolean
showAdvancedDebugInfo?: boolean
showUptime?: boolean
- showPlayTab?: boolean
additionalTabs?: Tab[]
stripePublishableKey?: string
siteUrl?: string
@@ -458,7 +457,6 @@ const props = withDefaults(
showCopyIdAction: false,
showAdvancedDebugInfo: false,
showUptime: true,
- showPlayTab: false,
additionalTabs: () => [],
stripePublishableKey: undefined,
siteUrl: undefined,
@@ -837,16 +835,12 @@ watch(serverData, (data) => {
})
const navLinks = computed(() => [
- ...(props.showPlayTab
- ? [
- {
- label: 'Play',
- href: `/hosting/manage/${props.serverId}/play`,
- icon: PlayIcon,
- subpages: [],
- },
- ]
- : []),
+ {
+ label: 'Play',
+ href: `/hosting/manage/${props.serverId}/play`,
+ icon: PlayIcon,
+ subpages: [],
+ },
{
label: 'Overview',
href: `/hosting/manage/${props.serverId}`,
From c9d7e3ee73e9bb59f2bcebf0746adfb8169e38cf Mon Sep 17 00:00:00 2001
From: "Calum H. (IMB11)"
Date: Fri, 4 Sep 2026 16:03:22 +0100
Subject: [PATCH 05/67] fix: build
---
.../wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue
index 56730841e2d..1426bdb6113 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue
@@ -1,12 +1,12 @@
-
Date: Fri, 4 Sep 2026 16:55:24 +0100
Subject: [PATCH 06/67] dev: notes
---
SERVER_PLAY_NOTES.md | 10 ++++++++++
1 file changed, 10 insertions(+)
create mode 100644 SERVER_PLAY_NOTES.md
diff --git a/SERVER_PLAY_NOTES.md b/SERVER_PLAY_NOTES.md
new file mode 100644
index 00000000000..80a156db144
--- /dev/null
+++ b/SERVER_PLAY_NOTES.md
@@ -0,0 +1,10 @@
+Flow:
+1. Shared instance wont exist until they:
+ - Click "Play server"
+ - Need special handling in-app
+ - Click "Invite players"
+ - Click "Download mrpack"
+ - disable btns until share response is done, then continue
+2. When content changes, watch shared_instance_needs_update: boolean on WorldsFull, show "Push update" admonition like app instances
+ 1. Push update flow like usual, show content changes diff]
+ 2. tbd
From 8ddbdeafc2fbbc38c168a3448814cd60e2eabf38 Mon Sep 17 00:00:00 2001
From: "Calum H. (IMB11)"
Date: Thu, 10 Sep 2026 14:10:28 +0100
Subject: [PATCH 07/67] feat: impl endpoints into panel
---
SERVER_PLAY_NOTES.md | 10 -
apps/app-frontend/src/App.vue | 13 +
.../ui/hosting/HostingPlayHandler.vue | 185 ++++++++
.../generated/app-events/CommandPayload.ts | 2 +-
.../generated/app-events/postcard/index.d.ts | 2 +-
.../generated/app-events/postcard/index.js | 8 +
.../src/pages/hosting/manage/Play.vue | 14 +-
apps/app-frontend/src/routes.js | 2 +-
apps/app/tauri.conf.json | 2 +-
.../src/pages/hosting/manage/[id]/play.vue | 23 +-
.../src/modules/archon/content/v1.ts | 12 +
.../api-client/src/modules/archon/types.ts | 34 ++
.../modules/shared-instances/instances/v1.ts | 18 +
.../modules/shared-instances/invites/v1.ts | 18 +
.../src/modules/shared-instances/types.ts | 7 +
packages/api-client/src/platform/generic.ts | 5 +-
packages/api-client/src/platform/nuxt.ts | 1 +
packages/api-client/src/platform/tauri.ts | 2 +-
packages/api-client/src/types/request.ts | 3 +
packages/app-lib/src/api/handler.rs | 15 +
packages/app-lib/src/event/mod.rs | 4 +
.../ui/src/composables/server-panel-sync.ts | 11 +
.../shared/server-settings/pages/general.vue | 8 +
.../manage/[id]/play/ServerPlayCard.vue | 60 +--
.../manage/[id]/play/ServerPlayersTable.vue | 4 +-
.../wrapped/hosting/manage/[id]/play/play.vue | 415 +++++++++---------
.../hosting/manage/[id]/play/share-diff.ts | 35 ++
.../manage/[id]/play/use-server-players.ts | 116 +++++
packages/ui/src/providers/index.ts | 1 +
packages/ui/src/providers/server-play.ts | 20 +
30 files changed, 774 insertions(+), 276 deletions(-)
delete mode 100644 SERVER_PLAY_NOTES.md
create mode 100644 apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue
create mode 100644 packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/share-diff.ts
create mode 100644 packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
create mode 100644 packages/ui/src/providers/server-play.ts
diff --git a/SERVER_PLAY_NOTES.md b/SERVER_PLAY_NOTES.md
deleted file mode 100644
index 80a156db144..00000000000
--- a/SERVER_PLAY_NOTES.md
+++ /dev/null
@@ -1,10 +0,0 @@
-Flow:
-1. Shared instance wont exist until they:
- - Click "Play server"
- - Need special handling in-app
- - Click "Invite players"
- - Click "Download mrpack"
- - disable btns until share response is done, then continue
-2. When content changes, watch shared_instance_needs_update: boolean on WorldsFull, show "Push update" admonition like app instances
- 1. Push update flow like usual, show content changes diff]
- 2. tbd
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index e9b449e7253..cbf1b6f9379 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -77,6 +77,9 @@ import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountR
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavButton from '@/components/ui/NavButton.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
+import HostingPlayHandler from '@/components/ui/hosting/HostingPlayHandler.vue'
+import { provideServerPlay } from '@modrinth/ui'
+
import SharedInstanceInviteHandler from '@/components/ui/shared-instances/shared-instance-invite-handler/index.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import SurveyPopup from '@/components/ui/SurveyPopup.vue'
@@ -954,6 +957,13 @@ const contentInstallModpackAlreadyInstalledModal = ref()
const addServerToInstanceModal = ref()
const incompatibilityWarningModal = ref()
const installToPlayModal = ref()
+const hostingPlayHandler = ref()
+provideServerPlay({
+ async play(target) {
+ if (!hostingPlayHandler.value) throw new Error('Server play handler is not ready.')
+ await hostingPlayHandler.value.play(target)
+ },
+})
const sharedInstanceInviteHandler = ref()
const updateToPlayModal = ref()
@@ -1539,6 +1549,8 @@ async function handleCommand(e) {
} else {
await run(e.id).catch(handleError)
}
+ } else if (e.event === 'PlayHostingServer') {
+ await hostingPlayHandler.value?.play({ serverId: e.server_id, worldId: e.world_id })
} else if (e.event === 'InstallSharedInstanceInvite') {
await sharedInstanceInviteHandler.value?.installFromInviteId(e.invite_id)
} else if (e.event === 'InstallServer') {
@@ -2274,6 +2286,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@create-anyway="handleContentInstallModpackDuplicateCreateAnyway"
@go-to-instance="handleContentInstallModpackDuplicateGoToInstance"
/>
+
diff --git a/apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue b/apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue
new file mode 100644
index 00000000000..b952435e5e0
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/generated/app-events/CommandPayload.ts b/apps/app-frontend/src/generated/app-events/CommandPayload.ts
index 1183c97876c..d844877140a 100644
--- a/apps/app-frontend/src/generated/app-events/CommandPayload.ts
+++ b/apps/app-frontend/src/generated/app-events/CommandPayload.ts
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-export type CommandPayload = { "event": "InstallMod", id: string, } | { "event": "InstallVersion", id: string, } | { "event": "InstallModpack", id: string, } | { "event": "InstallServer", id: string, } | { "event": "LaunchInstance", id: string, server: string | null, singleplayer_world: string | null, } | { "event": "InstallSharedInstanceInvite", invite_id: string, } | { "event": "RunMRPack", path: string, };
+export type CommandPayload = { "event": "InstallMod", id: string, } | { "event": "InstallVersion", id: string, } | { "event": "InstallModpack", id: string, } | { "event": "InstallServer", id: string, } | { "event": "LaunchInstance", id: string, server: string | null, singleplayer_world: string | null, } | { "event": "InstallSharedInstanceInvite", invite_id: string, } | { "event": "RunMRPack", path: string, } | { "event": "PlayHostingServer", server_id: string, world_id: string, };
diff --git a/apps/app-frontend/src/generated/app-events/postcard/index.d.ts b/apps/app-frontend/src/generated/app-events/postcard/index.d.ts
index 9fd49624608..af600623e02 100644
--- a/apps/app-frontend/src/generated/app-events/postcard/index.d.ts
+++ b/apps/app-frontend/src/generated/app-events/postcard/index.d.ts
@@ -38,7 +38,7 @@ export type AppEvent = { tag: "loading", value: LoadingPayload } | { tag: "proce
export type LoadingBarType = { tag: "legacy_data_migration" } | { tag: "directory_move", value: { old: string, new: string } } | { tag: "java_download", value: { version: u32 } } | { tag: "pack_file_download", value: { instance_id: string, pack_name: string, icon: string | undefined, pack_version: string } } | { tag: "pack_download", value: { instance_id: string, pack_name: string, icon: string | undefined, pack_id: string | undefined, pack_version: string | undefined } } | { tag: "pack_import", value: { pack_name: string } } | { tag: "minecraft_download", value: { instance_id: string, instance_name: string } } | { tag: "zip_extract", value: { instance_id: string, instance_name: string } } | { tag: "pack_export", value: { instance_id: string, instance_name: string } } | { tag: "config_change", value: { new_path: string } } | { tag: "copy_instance", value: { import_location: string, instance_name: string } } | { tag: "launcher_update", value: { version: string, current_version: string } }
export type LoadingPayload = { event: LoadingBarType, loader_uuid: string, fraction: f64 | undefined, message: string }
export type WarningPayload = { message: string }
-export type CommandPayload = { tag: "InstallMod", value: { id: string } } | { tag: "InstallVersion", value: { id: string } } | { tag: "InstallModpack", value: { id: string } } | { tag: "InstallServer", value: { id: string } } | { tag: "LaunchInstance", value: { id: string, server: string | undefined, singleplayer_world: string | undefined } } | { tag: "InstallSharedInstanceInvite", value: { invite_id: string } } | { tag: "RunMRPack", value: { path: string } }
+export type CommandPayload = { tag: "InstallMod", value: { id: string } } | { tag: "InstallVersion", value: { id: string } } | { tag: "InstallModpack", value: { id: string } } | { tag: "InstallServer", value: { id: string } } | { tag: "LaunchInstance", value: { id: string, server: string | undefined, singleplayer_world: string | undefined } } | { tag: "InstallSharedInstanceInvite", value: { invite_id: string } } | { tag: "RunMRPack", value: { path: string } } | { tag: "PlayHostingServer", value: { server_id: string, world_id: string } }
export type ProcessPayload = { instance_id: string, uuid: string, event: ProcessPayloadType, message: string }
export type ProcessPayloadType = { tag: "launched" } | { tag: "finished" }
export type InstancePayload = { instance_id: string, event: InstancePayloadType }
diff --git a/apps/app-frontend/src/generated/app-events/postcard/index.js b/apps/app-frontend/src/generated/app-events/postcard/index.js
index 1393f047999..db3fb5c6b79 100644
--- a/apps/app-frontend/src/generated/app-events/postcard/index.js
+++ b/apps/app-frontend/src/generated/app-events/postcard/index.js
@@ -259,6 +259,14 @@ function deserialize_COMMAND_PAYLOAD(d) {
path: d.deserialize_string()
}
};
+ case 7:
+ return {
+ tag: "PlayHostingServer",
+ value: {
+ server_id: d.deserialize_string(),
+ world_id: d.deserialize_string()
+ }
+ };
default:
throw "variant not implemented"
}
diff --git a/apps/app-frontend/src/pages/hosting/manage/Play.vue b/apps/app-frontend/src/pages/hosting/manage/Play.vue
index 170357365d3..e9147c188e3 100644
--- a/apps/app-frontend/src/pages/hosting/manage/Play.vue
+++ b/apps/app-frontend/src/pages/hosting/manage/Play.vue
@@ -1,7 +1,17 @@
-
+
diff --git a/apps/app-frontend/src/routes.js b/apps/app-frontend/src/routes.js
index 5e2b388d3f0..10da081b623 100644
--- a/apps/app-frontend/src/routes.js
+++ b/apps/app-frontend/src/routes.js
@@ -26,7 +26,7 @@ export default new createRouter({
{
path: 'play',
name: 'ServerManagePlay',
- component: Hosting.Play,
+ component: () => import('@/pages/hosting/manage/Play.vue'),
},
{
path: '',
diff --git a/apps/app/tauri.conf.json b/apps/app/tauri.conf.json
index db4ef3624bf..9552e0c77c1 100644
--- a/apps/app/tauri.conf.json
+++ b/apps/app/tauri.conf.json
@@ -102,7 +102,7 @@
"capabilities": ["ads", "core", "plugins"],
"csp": {
"default-src": "'self' customprotocol: asset:",
- "connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.nodes.modrinth.com https://*.posthog.com https://posthog.modrinth.com https://*.sentry.io https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://js.stripe.com https://*.stripe.com wss://*.stripe.com https://*.intercom.io wss://*.intercom.io https://*.intercomcdn.com https://www.intercom-reporting.com https://app.getsentry.com wss://*.nodes.modrinth.com https://*.taila228c5.ts.net https://*.taila228c5.ts.net wss://*.taila228c5.ts.net https://fill.papermc.io https://api.purpurmc.org 'self' data: blob:",
+ "connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.nodes.modrinth.com https://*.posthog.com https://posthog.modrinth.com https://*.sentry.io https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://js.stripe.com https://*.stripe.com wss://*.stripe.com https://*.intercom.io wss://*.intercom.io https://*.intercomcdn.com https://www.intercom-reporting.com https://app.getsentry.com wss://*.nodes.modrinth.com https://kyros-test2.tail029726.ts.net wss://kyros-test2.tail029726.ts.net https://fill.papermc.io https://api.purpurmc.org 'self' data: blob:",
"font-src": ["https://cdn.modrinth.com/fonts/", "https://js.intercomcdn.com"],
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
"style-src": "'unsafe-inline' 'self'",
diff --git a/apps/frontend/src/pages/hosting/manage/[id]/play.vue b/apps/frontend/src/pages/hosting/manage/[id]/play.vue
index 0c23d78a03e..630bc537b84 100644
--- a/apps/frontend/src/pages/hosting/manage/[id]/play.vue
+++ b/apps/frontend/src/pages/hosting/manage/[id]/play.vue
@@ -1,13 +1,26 @@
-
+
diff --git a/packages/api-client/src/modules/archon/content/v1.ts b/packages/api-client/src/modules/archon/content/v1.ts
index 71aedc9bd1f..1720d758b78 100644
--- a/packages/api-client/src/modules/archon/content/v1.ts
+++ b/packages/api-client/src/modules/archon/content/v1.ts
@@ -6,6 +6,18 @@ export class ArchonContentV1Module extends AbstractModule {
return 'archon_content_v1'
}
+ public async share(serverId: string, worldId: string): Promise {
+ return this.client.request(`/servers/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId)}/content/share`, {
+ api: 'archon', version: 1, method: 'POST', timeout: 600_000, retry: false,
+ })
+ }
+
+ public async getShareDiff(serverId: string, worldId: string): Promise {
+ return this.client.request(`/servers/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId)}/content/share/diff`, {
+ api: 'archon', version: 1, method: 'GET',
+ })
+ }
+
/** GET /v1/:server_id/worlds/:world_id/addons */
public async getAddons(
serverId: string,
diff --git a/packages/api-client/src/modules/archon/types.ts b/packages/api-client/src/modules/archon/types.ts
index 0c04de3304e..7038a65e6c8 100644
--- a/packages/api-client/src/modules/archon/types.ts
+++ b/packages/api-client/src/modules/archon/types.ts
@@ -285,6 +285,30 @@ export namespace Archon {
export namespace Content {
export namespace v1 {
+ export type ShareWorldContentResponse = {
+ shared_instance_id: string
+ version: number | null
+ }
+
+ export type SharedContentChange =
+ | { kind: 'added'; after: T }
+ | { kind: 'removed'; before: T }
+ | { kind: 'updated'; before: T; after: T }
+
+ export type SharedContentDiffEntry =
+ | { type: 'project'; project_id: string; change: SharedContentChange }
+ | { type: 'external_file'; file_type: string; file_name: string; kind: 'added' | 'removed' | 'updated' }
+ | { type: 'modpack' | 'game_version'; change: SharedContentChange }
+ | { type: 'loader'; change: SharedContentChange<{ name: string; version: string | null }> }
+
+ export type SharedInstancePublishDiff = {
+ shared_instance_id: string
+ latest_version: number
+ local_updated_at: string
+ has_changes: boolean
+ diffs: SharedContentDiffEntry[]
+ }
+
export type AddonKind = 'mod' | 'plugin' | 'datapack' | 'shader' | 'resourcepack'
export type ContentOwnerType = 'user' | 'organization'
@@ -848,6 +872,8 @@ export namespace Archon {
}
export type WorldContentInfo = {
+ shared_instance_id: string | null
+ shared_instance_needs_update: boolean
modloader: string
modloader_version: string
game_version: string
@@ -1152,6 +1178,13 @@ export namespace Archon {
content: WorldContentItem[]
}
+ export type WorldSharedInstanceUpdateEvent = {
+ type: 'world.shared_instance.update'
+ world_id: string
+ shared_instance_id: string
+ needs_update: boolean
+ }
+
export type SyncEvent =
| ProtocolResetEvent
| ProtocolInvalidEvent
@@ -1171,6 +1204,7 @@ export namespace Archon {
| WorldContentAddonPatchEvent
| WorldContentBaseUpdateEvent
| WorldContentUpdateEvent
+ | WorldSharedInstanceUpdateEvent
}
}
diff --git a/packages/api-client/src/modules/shared-instances/instances/v1.ts b/packages/api-client/src/modules/shared-instances/instances/v1.ts
index 7b958104639..727568413c0 100644
--- a/packages/api-client/src/modules/shared-instances/instances/v1.ts
+++ b/packages/api-client/src/modules/shared-instances/instances/v1.ts
@@ -63,4 +63,22 @@ export class SharedInstancesInstancesV1Module extends AbstractModule {
},
)
}
+
+ public async inviteUsers(instanceId: string, userIds: string[]): Promise<{ failed: string[] }> {
+ return this.client.request(`/instances/${encodeURIComponent(instanceId)}/users`, {
+ api: 'sharedinstances', version: 1, method: 'POST', body: { user_ids: userIds }, retry: false,
+ })
+ }
+
+ public async removeUsers(instanceId: string, userIds: string[]): Promise {
+ return this.client.request(`/instances/${encodeURIComponent(instanceId)}/users`, {
+ api: 'sharedinstances', version: 1, method: 'DELETE', body: { user_ids: userIds },
+ })
+ }
+
+ public async downloadMrpack(instanceId: string, version: number): Promise {
+ return this.client.request(`/instances/${encodeURIComponent(instanceId)}/versions/${version}/mrpack`, {
+ api: 'sharedinstances', version: 1, method: 'GET', responseType: 'blob', timeout: 600_000,
+ })
+ }
}
diff --git a/packages/api-client/src/modules/shared-instances/invites/v1.ts b/packages/api-client/src/modules/shared-instances/invites/v1.ts
index 72536cd0a0e..8ff2f2f92ac 100644
--- a/packages/api-client/src/modules/shared-instances/invites/v1.ts
+++ b/packages/api-client/src/modules/shared-instances/invites/v1.ts
@@ -18,4 +18,22 @@ export class SharedInstancesInvitesV1Module extends AbstractModule {
},
)
}
+
+ public async list(instanceId: string): Promise {
+ return this.client.request(`/instances/${encodeURIComponent(instanceId)}/invites`, {
+ api: 'sharedinstances', version: 1, method: 'GET',
+ })
+ }
+
+ public async create(instanceId: string, options: { max_age?: number; max_uses: number }): Promise<{ id: string }> {
+ return this.client.request(`/instances/${encodeURIComponent(instanceId)}/invites`, {
+ api: 'sharedinstances', version: 1, method: 'POST', body: options, retry: false,
+ })
+ }
+
+ public async delete(instanceId: string, inviteId: string): Promise {
+ return this.client.request(`/instances/${encodeURIComponent(instanceId)}/invites/${encodeURIComponent(inviteId)}`, {
+ api: 'sharedinstances', version: 1, method: 'DELETE',
+ })
+ }
}
diff --git a/packages/api-client/src/modules/shared-instances/types.ts b/packages/api-client/src/modules/shared-instances/types.ts
index 4ad1e3afbf7..c41d30a55e9 100644
--- a/packages/api-client/src/modules/shared-instances/types.ts
+++ b/packages/api-client/src/modules/shared-instances/types.ts
@@ -23,6 +23,13 @@ export namespace SharedInstances {
joined_at: string | null
}
+ export type InviteLink = {
+ id: string
+ expiration: string
+ max_uses: number
+ uses: number
+ }
+
export type Invite = {
instance_id: string
instance_name: string
diff --git a/packages/api-client/src/platform/generic.ts b/packages/api-client/src/platform/generic.ts
index 7ec63b68249..bc2f1972286 100644
--- a/packages/api-client/src/platform/generic.ts
+++ b/packages/api-client/src/platform/generic.ts
@@ -46,8 +46,9 @@ export class GenericModrinthClient extends XHRUploadClient {
protected async executeRequest(url: string, options: RequestOptions): Promise {
try {
- const response = await $fetch(url, {
+ const response = await $fetch(url, {
method: options.method ?? 'GET',
+ responseType: options.responseType,
headers: options.headers,
body: options.body as BodyInit,
params: options.params as Record,
@@ -55,7 +56,7 @@ export class GenericModrinthClient extends XHRUploadClient {
signal: options.signal,
})
- return response
+ return response as T
} catch (error) {
// ofetch throws FetchError for HTTP errors
throw this.normalizeError(error)
diff --git a/packages/api-client/src/platform/nuxt.ts b/packages/api-client/src/platform/nuxt.ts
index a5308535ba3..b09f5077fca 100644
--- a/packages/api-client/src/platform/nuxt.ts
+++ b/packages/api-client/src/platform/nuxt.ts
@@ -160,6 +160,7 @@ export class NuxtModrinthClient extends XHRUploadClient {
// @ts-expect-error - $fetch is provided by Nuxt
const response = await $fetch(url, {
method: options.method ?? 'GET',
+ responseType: options.responseType,
headers: options.headers,
body: options.body,
params: options.params,
diff --git a/packages/api-client/src/platform/tauri.ts b/packages/api-client/src/platform/tauri.ts
index ac82b53edba..78fd17787c2 100644
--- a/packages/api-client/src/platform/tauri.ts
+++ b/packages/api-client/src/platform/tauri.ts
@@ -92,7 +92,7 @@ export class TauriModrinthClient extends XHRUploadClient {
// Handle binary downloads (e.g. kyros fs files) before JSON parsing.
const contentType = response.headers.get('content-type')?.toLowerCase() ?? ''
- if (fullUrl.includes('/fs/download')) {
+ if (options.responseType === 'blob' || fullUrl.includes('/fs/download')) {
return (await response.blob()) as T
}
if (
diff --git a/packages/api-client/src/types/request.ts b/packages/api-client/src/types/request.ts
index d1a57d8845a..ca7b9836530 100644
--- a/packages/api-client/src/types/request.ts
+++ b/packages/api-client/src/types/request.ts
@@ -29,6 +29,9 @@ export type RequestOptions = {
*/
method?: HttpMethod
+ /** Explicit response decoding for binary downloads. */
+ responseType?: 'blob'
+
/**
* Request headers
*/
diff --git a/packages/app-lib/src/api/handler.rs b/packages/app-lib/src/api/handler.rs
index 1f16218dc8b..f53f465c719 100644
--- a/packages/app-lib/src/api/handler.rs
+++ b/packages/app-lib/src/api/handler.rs
@@ -30,6 +30,21 @@ pub async fn handle_url(sublink: &str) -> crate::Result {
Some(("server", id)) => {
CommandPayload::InstallServer { id: id.to_string() }
}
+ Some(("hosting", path)) => {
+ let (server_id, world_id) = path.split_once('/').ok_or_else(|| {
+ crate::ErrorKind::InputError("Missing hosting world ID".to_string())
+ })?;
+ let server_id = uuid::Uuid::parse_str(server_id).map_err(|_| {
+ crate::ErrorKind::InputError("Invalid hosting server ID".to_string())
+ })?;
+ let world_id = uuid::Uuid::parse_str(world_id).map_err(|_| {
+ crate::ErrorKind::InputError("Invalid hosting world ID".to_string())
+ })?;
+ CommandPayload::PlayHostingServer {
+ server_id: server_id.to_string(),
+ world_id: world_id.to_string(),
+ }
+ }
// /share/{invite_id}
Some(("share", raw)) => {
let (raw, _) = raw.split_once('?').unwrap_or((raw, ""));
diff --git a/packages/app-lib/src/event/mod.rs b/packages/app-lib/src/event/mod.rs
index 04fadf0d6b3..243d41b5dd6 100644
--- a/packages/app-lib/src/event/mod.rs
+++ b/packages/app-lib/src/event/mod.rs
@@ -386,6 +386,10 @@ pub enum CommandPayload {
// run or install .mrpack
path: String,
},
+ PlayHostingServer {
+ server_id: String,
+ world_id: String,
+ },
}
#[derive(Serialize, Deserialize, Clone)]
diff --git a/packages/ui/src/composables/server-panel-sync.ts b/packages/ui/src/composables/server-panel-sync.ts
index 400cc150705..2758664ade2 100644
--- a/packages/ui/src/composables/server-panel-sync.ts
+++ b/packages/ui/src/composables/server-panel-sync.ts
@@ -118,6 +118,17 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
case 'world.content.base.update':
handleWorldContentBaseUpdate(serverId, event)
break
+ case 'world.shared_instance.update':
+ patchServerFullWorld(serverId, event.world_id, (world) => world.content ? {
+ ...world,
+ content: {
+ ...world.content,
+ shared_instance_id: event.shared_instance_id,
+ shared_instance_needs_update: event.needs_update,
+ },
+ } : world)
+ void queryClient.invalidateQueries({ queryKey: ['servers', 'share-diff', serverId, event.world_id] })
+ break
case 'world.content.update':
handleWorldContentUpdate(serverId, event)
break
diff --git a/packages/ui/src/layouts/shared/server-settings/pages/general.vue b/packages/ui/src/layouts/shared/server-settings/pages/general.vue
index 1e2c9b0b887..8a85f207779 100644
--- a/packages/ui/src/layouts/shared/server-settings/pages/general.vue
+++ b/packages/ui/src/layouts/shared/server-settings/pages/general.vue
@@ -210,6 +210,11 @@ const preferences = {
description: 'Show RAM usage in bytes instead of a percentage.',
implemented: true,
},
+ reviewChangesBeforePlaying: {
+ displayName: 'Review changes before playing',
+ description: 'Show pending shared content changes before Play server publishes them. Saved on this device.',
+ implemented: true,
+ },
} as const
type PreferenceKeys = keyof typeof preferences
@@ -222,11 +227,14 @@ const defaultPreferences: UserPreferences = {
hideSubdomainLabel: false,
// autoRestart: false,
ramAsNumber: false,
+ reviewChangesBeforePlaying: false,
}
const userPreferences = useStorage(
`pyro-server-${serverId}-preferences`,
defaultPreferences,
+ undefined,
+ { mergeDefaults: true },
)
const newUserPreferences = ref(JSON.parse(JSON.stringify(userPreferences.value)))
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
index 3fa2fb5bc0c..5e662bb1e9d 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
@@ -12,12 +12,14 @@
-
@@ -34,41 +36,11 @@
-
-
-
- {{ formatMessage(messages.downloadModpackButton) }}
-
-
-
- {{ formatMessage(messages.downloadModpackButton) }}
-
-
-
-
-
-
-
-
+
+
+
+ {{ formatMessage(messages.downloadModpackButton) }}
+
@@ -92,26 +64,28 @@
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/share-diff.ts b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/share-diff.ts
new file mode 100644
index 00000000000..c66472cd508
--- /dev/null
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/share-diff.ts
@@ -0,0 +1,35 @@
+import type { AbstractModrinthClient, Archon } from '@modrinth/api-client'
+
+import type { ContentDiffItem } from '#ui/layouts/shared/installation-settings/types'
+
+type Change
= Archon.Content.v1.SharedContentChange
+const before = (change: Change) => 'before' in change ? change.before : undefined
+const after = (change: Change) => 'after' in change ? change.after : undefined
+
+export async function resolveServerShareDiff(client: AbstractModrinthClient, diff: Archon.Content.v1.SharedInstancePublishDiff): Promise {
+ const versionIds = new Set()
+ for (const entry of diff.diffs) {
+ if (entry.type !== 'project' && entry.type !== 'modpack') continue
+ const values = [before(entry.change), after(entry.change)]
+ for (const id of values) if (id) versionIds.add(id)
+ }
+ const versions = await Promise.all([...versionIds].map((id) => client.labrinth.versions_v3.getVersion(id)))
+ const projectIds = [...new Set(versions.map((version) => version.project_id))]
+ const projects = projectIds.length ? await client.labrinth.projects_v3.getMultiple(projectIds) : []
+ const versionName = (id?: string) => versions.find((version) => version.id === id)?.version_number ?? id
+ return diff.diffs.map((entry): ContentDiffItem => {
+ if (entry.type === 'external_file') return { type: entry.kind, fileName: encodeURIComponent(entry.file_name) }
+ if (entry.type === 'loader') {
+ const label = (value?: { name: string; version: string | null }) => value ? [value.name, value.version].filter(Boolean).join(' ') : undefined
+ return { type: 'loader_updated', currentVersionName: label(before(entry.change)), newVersionName: label(after(entry.change)) }
+ }
+ if (entry.type === 'game_version') return { type: 'game_version_updated', currentVersionName: before(entry.change), newVersionName: after(entry.change) }
+ const projectId = entry.type === 'project' ? entry.project_id : versions.find((version) => version.id === (after(entry.change) ?? before(entry.change)))?.project_id
+ return {
+ type: entry.type === 'project' ? entry.change.kind : entry.change.kind === 'added' ? 'modpack_linked' : entry.change.kind === 'removed' ? 'modpack_unlinked' : 'modpack_updated',
+ projectName: projects.find((project) => project.id === projectId)?.title ?? projectId,
+ currentVersionName: versionName(before(entry.change)),
+ newVersionName: versionName(after(entry.change)),
+ }
+ })
+}
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
new file mode 100644
index 00000000000..911836080be
--- /dev/null
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
@@ -0,0 +1,116 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
+import { computed, type Ref } from 'vue'
+
+import type { InviteLinkSettings, InvitePlayersUser } from '#ui/components/sharing'
+import { injectAuth, injectModrinthClient } from '#ui/providers'
+
+import type { ServerPlayerRow } from './types'
+
+export function useServerPlayers(instanceId: Ref, canManage: Ref) {
+ const client = injectModrinthClient()
+ const auth = injectAuth()
+ const queryClient = useQueryClient()
+ const userId = computed(() => auth.user.value?.id)
+ const memberKey = (id: string) => ['shared-instances', id, 'players', userId.value] as const
+ const inviteKey = (id: string) => ['shared-instances', id, 'invites', userId.value] as const
+ const members = useQuery({
+ queryKey: computed(() => memberKey(instanceId.value ?? '')),
+ enabled: computed(() => !!instanceId.value && !!userId.value),
+ queryFn: async () => {
+ const response = await client.sharedinstances.instances_v1.getUsers(instanceId.value!)
+ const users = response.users.length
+ ? await client.labrinth.users_v2.getMultiple(response.users.map((user) => user.id))
+ : []
+ const rows: ServerPlayerRow[] = response.users.filter((user) => user.join_type !== 'owner').map((member) => {
+ const user = users.find((user) => user.id === member.id)
+ return {
+ id: member.id,
+ username: user?.username ?? member.id,
+ avatarUrl: user?.avatar_url ?? undefined,
+ joinedAt: member.joined_at ? new Date(member.joined_at) : null,
+ lastPlayedAt: member.last_played ? new Date(member.last_played) : null,
+ pending: !member.joined_at,
+ method: member.join_type === 'link' ? 'link' : 'direct',
+ }
+ })
+ return { rows, remaining: Math.max(0, 50 - response.users.length - response.tokens) }
+ },
+ refetchInterval: 30_000,
+ })
+ const rows = computed(() => members.data.value?.rows ?? [])
+ const remaining = computed(() => members.data.value?.remaining ?? 0)
+ const friends = useQuery({
+ queryKey: computed(() => ['shared-instances', 'friends', userId.value]),
+ enabled: computed(() => !!userId.value && canManage.value),
+ queryFn: async () => {
+ const currentUserId = userId.value
+ const relationships = await client.labrinth.friends_v3.list()
+ const ids = relationships.filter((friend) => friend.accepted)
+ .map((friend) => friend.id === currentUserId ? friend.friend_id : friend.id)
+ return ids.length ? client.labrinth.users_v2.getMultiple(ids) : []
+ },
+ })
+ const candidates = computed(() => {
+ const candidates = new Map()
+ for (const friend of friends.data.value ?? []) {
+ candidates.set(friend.id, { id: friend.id, username: friend.username, avatarUrl: friend.avatar_url, status: 'available' })
+ }
+ for (const row of rows.value) {
+ candidates.set(row.id, { ...row, status: row.pending ? 'pending' : 'added' })
+ }
+ return [...candidates.values()]
+ })
+ const links = useQuery({
+ queryKey: computed(() => inviteKey(instanceId.value ?? '')),
+ enabled: computed(() => !!instanceId.value && canManage.value),
+ queryFn: () => client.sharedinstances.invites_v1.list(instanceId.value!),
+ })
+ const link = computed(() => links.data.value?.find((link) => new Date(link.expiration).getTime() > Date.now() && link.uses < link.max_uses))
+
+ const membershipMutation = useMutation({
+ mutationFn: async ({ id, userId, remove }: { id: string; userId: string; remove: boolean }) => {
+ if (!canManage.value) throw new Error('You do not have permission to manage players.')
+ if (remove) return client.sharedinstances.instances_v1.removeUsers(id, [userId])
+ const result = await client.sharedinstances.instances_v1.inviteUsers(id, [userId])
+ if (result.failed.includes(userId)) throw new Error('This player’s privacy settings do not allow this invitation.')
+ },
+ onSettled: (_data, _error, { id }) => queryClient.invalidateQueries({ queryKey: ['shared-instances', id, 'players'] }),
+ })
+ const linkMutation = useMutation({
+ mutationFn: async ({ id, settings, replaceId }: { id: string; settings: InviteLinkSettings; replaceId?: string }) => {
+ if (!canManage.value) throw new Error('You do not have permission to manage invite links.')
+ const created = await client.sharedinstances.invites_v1.create(id, {
+ max_age: Math.max(1, Math.min(604800, Math.floor((settings.expiresAt.getTime() - Date.now()) / 1000))),
+ max_uses: Math.max(1, Math.min(settings.maxUses, remaining.value)),
+ })
+ if (replaceId) {
+ try {
+ await client.sharedinstances.invites_v1.delete(id, replaceId)
+ } catch (error) {
+ await client.sharedinstances.invites_v1.delete(id, created.id)
+ throw error
+ }
+ }
+ },
+ onSettled: (_data, _error, { id }) => queryClient.invalidateQueries({ queryKey: ['shared-instances', id, 'invites'] }),
+ })
+
+ async function ensureLink(id: string) {
+ const available = await queryClient.fetchQuery({ queryKey: inviteKey(id), queryFn: () => client.sharedinstances.invites_v1.list(id), staleTime: 0 })
+ if (available.some((link) => new Date(link.expiration).getTime() > Date.now() && link.uses < link.max_uses)) return
+ if (remaining.value <= 0) return
+ await linkMutation.mutateAsync({ id, settings: { maxUses: Math.min(10, remaining.value), expiresAt: new Date(Date.now() + 86400_000) } })
+ }
+
+ async function search(query: string) {
+ const users = await queryClient.fetchQuery({
+ queryKey: ['users', 'search', query],
+ queryFn: () => client.labrinth.users_v3.search(query),
+ staleTime: 30_000,
+ })
+ return users.filter((user) => user.id !== userId.value && !candidates.value.some((candidate) => candidate.id === user.id))
+ .map((user) => ({ id: user.id, username: user.username, avatarUrl: user.avatar_url }))
+ }
+
+ return { members, rows, remaining, candidates, link, links, friends, membershipMutation, linkMutation, ensureLink, search }
+}
diff --git a/packages/ui/src/providers/index.ts b/packages/ui/src/providers/index.ts
index a14625a924a..e802488c384 100644
--- a/packages/ui/src/providers/index.ts
+++ b/packages/ui/src/providers/index.ts
@@ -29,3 +29,4 @@ export {
export * from './user-country'
export * from './user-preferences'
export * from './web-notifications'
+export * from './server-play'
diff --git a/packages/ui/src/providers/server-play.ts b/packages/ui/src/providers/server-play.ts
new file mode 100644
index 00000000000..ae5b8d10254
--- /dev/null
+++ b/packages/ui/src/providers/server-play.ts
@@ -0,0 +1,20 @@
+import type { Archon } from '@modrinth/api-client'
+
+import { createContext } from './create-context'
+
+export type ServerPlayTarget = {
+ serverId: string
+ worldId: string
+}
+
+export const [injectServerPlay, provideServerPlay] = createContext<{
+ play: (target: ServerPlayTarget) => Promise
+}>('root', 'serverPlay')
+
+export function getHostingServerAddress(net: Archon.Servers.v0.Net, subdomain?: string) {
+ const domain = net.domain || subdomain
+ if (domain) return domain.includes('.') ? domain : `${domain}.modrinth.gg`
+ if (!net.ip) return ''
+ const host = net.ip.includes(':') && !net.ip.startsWith('[') ? `[${net.ip}]` : net.ip
+ return net.port && net.port !== 25565 ? `${host}:${net.port}` : host
+}
From 9a2470197b8aafb5e082112e583d32ac876f3ca3 Mon Sep 17 00:00:00 2001
From: "Calum H. (IMB11)"
Date: Thu, 10 Sep 2026 15:31:31 +0100
Subject: [PATCH 08/67] qa: 1
---
.../ui/hosting/HostingPlayHandler.vue | 3 +
.../instances/use-hosting-instance.ts | 89 +++++++++++++++++
.../instance-page-header-server-metadata.vue | 13 ++-
.../src/pages/instance/content/index.vue | 8 +-
.../src/pages/instance/layout.vue | 96 +++++++------------
.../components/project/server/ServerPing.vue | 2 +-
.../project/server/ServerRegion.vue | 16 +++-
.../admonitions/ServerPanelAdmonitions.vue | 75 ++++++++++++++-
.../wrapped/hosting/manage/[id]/play/play.vue | 37 ++++---
9 files changed, 252 insertions(+), 87 deletions(-)
create mode 100644 apps/app-frontend/src/composables/instances/use-hosting-instance.ts
diff --git a/apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue b/apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue
index b952435e5e0..fd230fda5e2 100644
--- a/apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue
+++ b/apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue
@@ -23,6 +23,7 @@ import { useRouter } from 'vue-router'
import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountRequiredModal.vue'
import SharedInstanceInstallModal from '@/components/ui/shared-instances/shared-instance-install-modal/index.vue'
+import { hostingInstanceMetadata, useHostingInstanceCache } from '@/composables/instances/use-hosting-instance'
import { toError } from '@/helpers/errors'
import { install_job_list, install_get_shared_instance_preview, install_get_shared_instance_update_preview, install_shared_instance, install_update_shared_instance, installJobInstanceId, wait_for_install_job } from '@/helpers/install'
import { get, list } from '@/helpers/instance'
@@ -36,6 +37,7 @@ const client = injectModrinthClient()
const appEvents = injectAppEvents()
const queryClient = useQueryClient()
const router = useRouter()
+const hostingInstances = useHostingInstanceCache()
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const accountModal = ref>()
@@ -73,6 +75,7 @@ async function join(target: LaunchTarget, instanceId: string) {
if (!address) throw new Error(formatMessage(messages.noAddress))
await assertAccount(target)
await ensureManagedServerWorldExists(instanceId, target.name, address)
+ hostingInstances.value[instanceId] = hostingInstanceMetadata(server, target.worldId, target.sharedInstanceId, address)
await router.push(`/instance/${encodeURIComponent(instanceId)}`)
await start_join_server(instanceId, address)
}
diff --git a/apps/app-frontend/src/composables/instances/use-hosting-instance.ts b/apps/app-frontend/src/composables/instances/use-hosting-instance.ts
new file mode 100644
index 00000000000..1034b212c74
--- /dev/null
+++ b/apps/app-frontend/src/composables/instances/use-hosting-instance.ts
@@ -0,0 +1,89 @@
+import type { Archon } from '@modrinth/api-client'
+import { getHostingServerAddress, injectAuth, injectModrinthClient } from '@modrinth/ui'
+import { useQuery } from '@tanstack/vue-query'
+import { useStorage } from '@vueuse/core'
+import { computed, type Ref, watch } from 'vue'
+
+import type { GameInstance } from '@/helpers/types'
+import { get_instance_worlds } from '@/helpers/worlds'
+
+type HostingInstanceMetadata = {
+ sharedInstanceId: string
+ serverId: string
+ worldId: string
+ address: string
+ region?: string
+}
+
+export function hostingInstanceMetadata(
+ server: Archon.Servers.v1.ServerFull,
+ worldId: string,
+ sharedInstanceId: string,
+ address: string,
+): HostingInstanceMetadata {
+ const location = server.location
+ return {
+ sharedInstanceId,
+ serverId: server.id,
+ worldId,
+ address,
+ region: location.status === 'assigned' && location.location_metadata.region_should_be_user_displayed
+ ? location.location_metadata.region
+ : undefined,
+ }
+}
+
+export function useHostingInstanceCache() {
+ return useStorage>('hosting-instance-metadata', {})
+}
+
+export function useHostingInstance(instance: Ref, offline: Ref) {
+ const client = injectModrinthClient()
+ const auth = injectAuth()
+ const cache = useHostingInstanceCache()
+ const isHostingInstance = computed(() => !!instance.value?.shared_instance?.server_manager_name)
+ const saved = computed(() => {
+ const current = instance.value
+ const metadata = current && cache.value[current.id]
+ return metadata?.sharedInstanceId === current?.shared_instance?.id ? metadata : undefined
+ })
+ const serverQuery = useQuery({
+ queryKey: computed(() => ['instances', instance.value?.id, 'hosting', auth.user.value?.id]),
+ enabled: computed(() => isHostingInstance.value && !offline.value && !!auth.user.value?.id && auth.user.value.id === instance.value?.shared_instance?.linked_user_id),
+ queryFn: async () => {
+ const current = instance.value!
+ const sharedId = current.shared_instance!.id
+ const known = saved.value
+ const servers = known
+ ? [await client.archon.servers_v1.get(known.serverId)]
+ : await client.archon.servers_v1.list()
+ const server = servers.find((server) => server.worlds.some((world) => world.content?.shared_instance_id === sharedId))
+ const world = server?.worlds.find((world) => world.content?.shared_instance_id === sharedId)
+ if (!server || !world) return null
+ const legacy = await client.archon.servers_v0.get(server.id)
+ return { instanceId: current.id, metadata: hostingInstanceMetadata(server, world.id, sharedId, getHostingServerAddress(legacy.net, server.subdomain)) }
+ },
+ staleTime: 60_000,
+ retry: false,
+ })
+ watch(serverQuery.data, (result) => {
+ if (result) cache.value[result.instanceId] = result.metadata
+ })
+ const worldsQuery = useQuery({
+ queryKey: computed(() => ['instances', instance.value?.id, 'hosting-worlds']),
+ enabled: computed(() => isHostingInstance.value && !saved.value && instance.value?.install_stage === 'installed'),
+ queryFn: () => get_instance_worlds(instance.value!.id),
+ })
+ const address = computed(() => {
+ if (saved.value?.address) return saved.value.address
+ const world = worldsQuery.data.value?.find((world) =>
+ world.type === 'server' && world.name === instance.value?.shared_instance?.server_manager_name,
+ )
+ return world?.type === 'server' ? world.address : undefined
+ })
+ return {
+ isHostingInstance,
+ region: computed(() => saved.value?.region),
+ address,
+ }
+}
diff --git a/apps/app-frontend/src/pages/instance/components/page-header/instance-page-header-server-metadata.vue b/apps/app-frontend/src/pages/instance/components/page-header/instance-page-header-server-metadata.vue
index 04e0db3bfdd..915188e13e7 100644
--- a/apps/app-frontend/src/pages/instance/components/page-header/instance-page-header-server-metadata.vue
+++ b/apps/app-frontend/src/pages/instance/components/page-header/instance-page-header-server-metadata.vue
@@ -4,14 +4,15 @@
-
+
+
import type { Labrinth } from '@modrinth/api-client'
-import { TimerIcon } from '@modrinth/assets'
+import { SpinnerIcon, TimerIcon } from '@modrinth/assets'
import {
+ commonMessages,
PageHeaderMetadata,
PageHeaderMetadataItem,
ServerOnlinePlayers,
ServerPing,
ServerRegion,
+ useVIntl,
} from '@modrinth/ui'
+const { formatMessage } = useVIntl()
+
defineProps<{
loadingServerPing?: boolean
playersOnline?: number
diff --git a/apps/app-frontend/src/pages/instance/content/index.vue b/apps/app-frontend/src/pages/instance/content/index.vue
index 2af97905dfe..d275402e22d 100644
--- a/apps/app-frontend/src/pages/instance/content/index.vue
+++ b/apps/app-frontend/src/pages/instance/content/index.vue
@@ -269,11 +269,13 @@ watch(projects, (newProjects) => {
})
const mergedProjects = computed(() => {
+ const managedFiles = new Set(managedContentItems.value.map((item) => item.file_name))
+ const additionalProjects = projects.value.filter((item) => !managedFiles.has(item.file_name))
const active = installingItems.value.get(instance.value.id)
- const pending = active ?? installingBuffer.value
- if (pending.length === 0) return projects.value
+ const pending = (active ?? installingBuffer.value).filter((item) => !managedFiles.has(item.file_name))
+ if (pending.length === 0) return additionalProjects
const pendingProjectIds = new Set(pending.map((p) => p.project?.id).filter(Boolean))
- const displayProjects = projects.value.map((project) =>
+ const displayProjects = additionalProjects.map((project) =>
project.project?.id && pendingProjectIds.has(project.project.id)
? { ...project, installing: true }
: project,
diff --git a/apps/app-frontend/src/pages/instance/layout.vue b/apps/app-frontend/src/pages/instance/layout.vue
index f28f8ce207d..742c31350a7 100644
--- a/apps/app-frontend/src/pages/instance/layout.vue
+++ b/apps/app-frontend/src/pages/instance/layout.vue
@@ -116,13 +116,13 @@ import { computed, type ComputedRef, onUnmounted, ref, shallowRef, watch } from
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
import ExportModal from '@/components/ui/ExportModal.vue'
+import { useHostingInstance } from '@/composables/instances/use-hosting-instance'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import SharedInstanceInstallModal from '@/components/ui/shared-instances/shared-instance-install-modal/index.vue'
import SharedInstanceUpdateModal from '@/components/ui/shared-instances/SharedInstanceUpdateModal.vue'
import {
- fetchCachedServerStatus,
- getFreshCachedServerStatus,
+ getServerStatusQueryKey,
} from '@/composables/instances/use-server-status-query'
import { useAppEvent } from '@/composables/use-app-event'
import { useAppSettings } from '@/composables/use-app-settings.ts'
@@ -151,7 +151,7 @@ import {
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
import type { GameInstance } from '@/helpers/types'
import { createInstanceShortcut, showInstanceInFolder } from '@/helpers/utils.js'
-import type { ServerStatus } from '@/helpers/worlds'
+import { get_server_status, start_join_server } from '@/helpers/worlds'
import { provideInstanceBackup } from '@/providers/instance-backup'
import { injectServerInstall } from '@/providers/server-install'
import { useRootBreadcrumb } from '@/shared/breadcrumbs'
@@ -296,7 +296,8 @@ const linkedProjectQuery = useQuery(
})),
)
const linkedProjectV3 = computed(() => linkedProjectQuery.data.value ?? undefined)
-const isServerInstance = computed(() => linkedProjectV3.value?.minecraft_server != null)
+const hosting = useHostingInstance(instance, offline)
+const isServerInstance = computed(() => hosting.isHostingInstance.value || linkedProjectV3.value?.minecraft_server != null)
const processesQuery = useQuery(
computed(() => ({
...instanceProcessesQueryOptions(instanceId.value),
@@ -375,13 +376,11 @@ const { notifySharedInstanceError, notifySharedInstanceUnavailable } = useShared
useLoadingBarToken(subpagePending)
useLoadingBarToken(computed(() => instanceQuery.isPending.value && !instance.value))
-const minecraftServer = computed(() => linkedProjectV3.value?.minecraft_server)
+const minecraftServer = computed(() => hosting.isHostingInstance.value
+ ? { region: hosting.region.value }
+ : 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 playersOnline = ref(undefined)
-const ping = ref(undefined)
-const loadingServerPing = ref(false)
const sharedInstanceState = createSharedInstanceContext(
instance,
offline,
@@ -410,53 +409,19 @@ const sharedInstanceUpdateAvailable = computed(
sharedInstanceUpdateKey.value !== hiddenSharedInstanceUpdateKey.value,
)
-function applyServerStatus(status: ServerStatus) {
- playersOnline.value = status.players?.online
- ping.value = status.ping
- liveServerStatusOnline.value = true
- loadingServerPing.value = true
-}
-
-function resetServerStatus() {
- ping.value = undefined
- playersOnline.value = undefined
- liveServerStatusOnline.value = false
- loadingServerPing.value = false
-}
-
-const serverAddress = computed(() => linkedProjectV3.value?.minecraft_java_server?.address)
-watch(
- [instanceId, serverAddress, isServerInstance],
- ([requestedInstanceId, address, serverInstance]) => {
- resetServerStatus()
- if (serverInstance && address) {
- const cachedStatus = getFreshCachedServerStatus(queryClient, address)
- if (cachedStatus) {
- applyServerStatus(cachedStatus)
- } else {
- playersOnline.value = undefined
- ping.value = undefined
- loadingServerPing.value = false
- }
-
- fetchCachedServerStatus(queryClient, address)
- .then((status) => {
- if (instanceId.value !== requestedInstanceId || serverAddress.value !== address) return
- applyServerStatus(status)
- })
- .catch((error) => {
- console.error(`Failed to fetch server status for ${address}:`, error)
- })
- .finally(() => {
- if (instanceId.value !== requestedInstanceId) return
- loadingServerPing.value = true
- })
- } else {
- loadingServerPing.value = true
- }
- },
- { immediate: true },
-)
+const serverAddress = computed(() => hosting.isHostingInstance.value ? hosting.address.value : linkedProjectV3.value?.minecraft_java_server?.address)
+const serverStatusQuery = useQuery({
+ queryKey: computed(() => getServerStatusQueryKey(serverAddress.value ?? '')),
+ queryFn: () => get_server_status(serverAddress.value!),
+ enabled: computed(() => isServerInstance.value && !!serverAddress.value && !offline.value),
+ staleTime: 30_000,
+ refetchInterval: 30_000,
+ retry: false,
+})
+const statusOnline = computed(() => !serverStatusQuery.isError.value && (!!serverStatusQuery.data.value || !!javaServerPingData.value))
+const playersOnline = computed(() => serverStatusQuery.isError.value ? undefined : serverStatusQuery.data.value?.players?.online)
+const ping = computed(() => serverStatusQuery.isError.value ? undefined : serverStatusQuery.data.value?.ping)
+const loadingServerPing = computed(() => serverStatusQuery.isFetched.value || offline.value)
async function refreshInstance() {
await Promise.all([instanceQuery.refetch(), sharedInstanceState.refreshAvailability()])
@@ -575,12 +540,13 @@ watch(
const options = ref | null>(null)
-const launchInstance = async (context: string) => {
+const launchInstance = async (context: string, address?: string) => {
if (!instance.value || instance.value.quarantined) return
const currentInstance = instance.value
loading.value = true
try {
- await run(currentInstance.id)
+ if (address) await start_join_server(currentInstance.id, address)
+ else await run(currentInstance.id)
queryClient.setQueryData(instanceKeys.processes(currentInstance.id), [true])
} catch (err) {
handleSevereError(err, { instanceId: currentInstance.id })
@@ -634,7 +600,7 @@ function handleSharedInstanceUpdateComplete(successful: boolean) {
}
}
-const startInstance = async (context: string) => {
+const startInstance = async (context: string, address?: string) => {
if (!instance.value || instance.value.quarantined) return
if (checkingSharedInstanceLaunch.value || loading.value || playing.value) return
@@ -663,7 +629,7 @@ const startInstance = async (context: string) => {
if (preview?.updateAvailable && sharedInstanceUpdateModal.value) {
sharedInstanceUpdateModal.value.show(instance.value, preview, async () => {
await refreshInstance()
- await launchInstance(context)
+ await launchInstance(context, address)
})
return
}
@@ -673,7 +639,7 @@ const startInstance = async (context: string) => {
if (isSharedInstanceMember) {
updateToPlayModal.value.show(instance.value, null, async () => {
await refreshInstance()
- await launchInstance(context)
+ await launchInstance(context, address)
})
} else {
updateToPlayModal.value.show(instance.value)
@@ -681,7 +647,7 @@ const startInstance = async (context: string) => {
return
}
- await launchInstance(context)
+ await launchInstance(context, address)
}
const stopInstance = async (context: string) => {
@@ -700,6 +666,10 @@ const stopInstance = async (context: string) => {
}
const handlePlayServer = async () => {
+ if (hosting.isHostingInstance.value) {
+ if (serverAddress.value) await startInstance('InstancePage', serverAddress.value)
+ return
+ }
if (!instance.value?.link?.project_id || instance.value.quarantined) return
loading.value = true
try {
diff --git a/packages/ui/src/components/project/server/ServerPing.vue b/packages/ui/src/components/project/server/ServerPing.vue
index 1d2198b3aa5..706a7d1f4b3 100644
--- a/packages/ui/src/components/project/server/ServerPing.vue
+++ b/packages/ui/src/components/project/server/ServerPing.vue
@@ -46,7 +46,7 @@ const pingClass = computed(() => {
diff --git a/packages/ui/src/components/project/server/ServerRegion.vue b/packages/ui/src/components/project/server/ServerRegion.vue
index 1bf67167421..8ccfb45c168 100644
--- a/packages/ui/src/components/project/server/ServerRegion.vue
+++ b/packages/ui/src/components/project/server/ServerRegion.vue
@@ -3,10 +3,12 @@ import { computed } from 'vue'
import { defineMessage, useVIntl } from '../../../composables'
import { SERVER_REGIONS } from '../../../utils'
+import { regionOverrides } from '../../../utils/regions'
import { TagItem } from '../../base'
-const { region } = defineProps<{
+const { region, flagOnly = false } = defineProps<{
region: string
+ flagOnly?: boolean
}>()
const { formatMessage } = useVIntl()
@@ -17,12 +19,22 @@ const tooltip = defineMessage({
})
const regionName = computed(() => {
+ const hostingRegion = regionOverrides[region as keyof typeof regionOverrides]
+ if (hostingRegion) return formatMessage(hostingRegion.name)
const name = SERVER_REGIONS[region]
if (name) return formatMessage(name)
return region
})
+const regionFlag = computed(() => regionOverrides[region as keyof typeof regionOverrides]?.flag)
- {{ regionName }}
+
+ {{ regionName }}
diff --git a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
index a7938c79a9b..7a09eeb172f 100644
--- a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
+++ b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
@@ -1,8 +1,11 @@
diff --git a/apps/app-frontend/src/features/instances/settings-modal/sharing-settings/index.vue b/apps/app-frontend/src/features/instances/settings-modal/sharing-settings/index.vue
new file mode 100644
index 00000000000..e65bd8f71c0
--- /dev/null
+++ b/apps/app-frontend/src/features/instances/settings-modal/sharing-settings/index.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
diff --git a/apps/app-frontend/src/features/instances/settings-modal/sharing-settings/use-sharing-settings.ts b/apps/app-frontend/src/features/instances/settings-modal/sharing-settings/use-sharing-settings.ts
new file mode 100644
index 00000000000..6f6d5dbf3cf
--- /dev/null
+++ b/apps/app-frontend/src/features/instances/settings-modal/sharing-settings/use-sharing-settings.ts
@@ -0,0 +1,98 @@
+import { injectAuth, type SharingSettingsContext } from '@modrinth/ui'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
+import { computed } from 'vue'
+
+import {
+ get_shared_instance_invites,
+ revoke_shared_instance_invite,
+ type SharedInstanceInvite,
+ unpublish_shared_instance,
+} from '@/helpers/instance'
+import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
+import { instanceKeys } from '@/pages/instance/query-options'
+
+import { injectInstanceSettings } from '../instance-settings-context.ts'
+
+export function useInstanceSharingSettings(): SharingSettingsContext {
+ const { instance, offline, onUnlinked } = injectInstanceSettings()
+ const auth = injectAuth()
+ const { notifySharedInstanceError } = useSharedInstanceErrors()
+ const queryClient = useQueryClient()
+ const targetKey = computed(() => {
+ const sharedInstance = instance.value.shared_instance
+ return sharedInstance?.role === 'owner' && !instance.value.quarantined && auth.user.value
+ ? JSON.stringify([instance.value.id, sharedInstance.id, auth.user.value.id])
+ : null
+ })
+ const invitesKey = computed(
+ () => ['sharedInstanceInvites', instance.value.id, auth.user.value?.id] as const,
+ )
+ const invites = useQuery({
+ queryKey: invitesKey,
+ queryFn: ({ queryKey }) => get_shared_instance_invites(queryKey[1]),
+ enabled: () => !!targetKey.value && !offline,
+ retry: false,
+ staleTime: Infinity,
+ refetchOnMount: 'always',
+ refetchOnReconnect: false,
+ refetchOnWindowFocus: false,
+ })
+
+ type ShareTarget = {
+ instanceId: string
+ invitesKey: typeof invitesKey.value
+ }
+ const revokeMutation = useMutation({
+ mutationFn: (target: ShareTarget & { inviteId: string }) =>
+ revoke_shared_instance_invite(target.instanceId, target.inviteId),
+ onSuccess: (_data, target) => {
+ queryClient.setQueryData(target.invitesKey, (current = []) =>
+ current.filter((invite) => invite.id !== target.inviteId),
+ )
+ },
+ })
+ const unpublishMutation = useMutation({
+ mutationFn: (target: ShareTarget) => unpublish_shared_instance(target.instanceId),
+ onSuccess: async (_data, target) => {
+ queryClient.setQueryData(instanceKeys.sharedMembers(target.instanceId), [])
+ queryClient.setQueryData(target.invitesKey, [])
+ await queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', target.instanceId] })
+ if (target.instanceId === instance.value.id && target.invitesKey[2] === auth.user.value?.id) {
+ onUnlinked()
+ }
+ },
+ })
+ const busy = computed(
+ () =>
+ !targetKey.value ||
+ !!offline ||
+ instance.value.install_stage !== 'installed' ||
+ revokeMutation.isPending.value ||
+ unpublishMutation.isPending.value,
+ )
+
+ function currentTarget(expectedKey: string): ShareTarget | undefined {
+ if (busy.value || expectedKey !== targetKey.value) return
+ return { instanceId: instance.value.id, invitesKey: invitesKey.value }
+ }
+
+ return {
+ targetKey,
+ invites: computed(() => invites.data.value ?? []),
+ loading: invites.isLoading,
+ error: invites.error,
+ busy,
+ refresh: async () => {
+ if (targetKey.value && !offline) await invites.refetch()
+ },
+ revokeInvite: async (inviteId, expectedKey) => {
+ const target = currentTarget(expectedKey)
+ if (target) await revokeMutation.mutateAsync({ ...target, inviteId })
+ },
+ unpublish: async (expectedKey) => {
+ const target = currentTarget(expectedKey)
+ if (target) await unpublishMutation.mutateAsync(target)
+ },
+ onError: notifySharedInstanceError,
+ }
+}
diff --git a/apps/app-frontend/src/helpers/shared-instance-errors.ts b/apps/app-frontend/src/helpers/shared-instance-errors.ts
index b0ce5d0f74d..64126e94fe5 100644
--- a/apps/app-frontend/src/helpers/shared-instance-errors.ts
+++ b/apps/app-frontend/src/helpers/shared-instance-errors.ts
@@ -11,6 +11,10 @@ export const sharedInstanceErrorMessages = defineMessages({
id: 'instance.shared-instance.unavailable.title',
defaultMessage: 'Shared instance no longer available',
},
+ notFoundTitle: {
+ id: 'instance.shared-instance.unavailable.not-found-title',
+ defaultMessage: 'Shared instance unavailable',
+ },
lockedTitle: {
id: 'instance.shared-instance.unavailable.locked-title',
defaultMessage: 'Instance locked',
@@ -21,9 +25,9 @@ export const sharedInstanceErrorMessages = defineMessages({
"Your local instance is still available, but it is no longer linked and won't receive updates.",
},
deletedText: {
- id: 'instance.shared-instance.unavailable.deleted-text',
+ id: 'instance.shared-instance.unavailable.not-found-text',
defaultMessage:
- 'The primary instance was deleted. This instance is still available, but it is no longer linked and will no longer receive updates.',
+ 'We couldn’t find this shared instance. Your installed content is still on this device.',
},
accessRevokedText: {
id: 'instance.shared-instance.unavailable.access-revoked-text',
@@ -65,6 +69,7 @@ export function sharedInstanceUnavailableTextMessage(
export function sharedInstanceUnavailableTitleMessage(
reason: SharedInstanceUnavailableReason | null,
) {
+ if (reason === 'deleted') return sharedInstanceErrorMessages.notFoundTitle
return reason === 'quarantined'
? sharedInstanceErrorMessages.lockedTitle
: sharedInstanceErrorMessages.unavailableTitle
diff --git a/apps/app-frontend/src/pages/instance/share/index.vue b/apps/app-frontend/src/pages/instance/share/index.vue
index d88eed0d3ce..d889a75de13 100644
--- a/apps/app-frontend/src/pages/instance/share/index.vue
+++ b/apps/app-frontend/src/pages/instance/share/index.vue
@@ -57,16 +57,19 @@
-
+ type="warning"
+ :header="formatMessage(sharedInstanceUnavailableTitleMessage(sharedInstanceUnavailableReason))"
+ >
+ {{ formatSharedInstanceUnavailable(sharedInstanceUnavailableReason, sharedInstanceUnavailableManager) }}
+
+
+
+ {{ formatMessage(commonMessages.retryButton) }}
+
+
+
import { LogInIcon, SpinnerIcon, UserPlusIcon } from '@modrinth/assets'
import {
+ Admonition,
Avatar,
Button,
+ commonMessages,
ConfirmUnlinkModal,
defineMessages,
injectAuth,
@@ -154,7 +159,7 @@ import {
import { edit } from '@/helpers/instance'
import type { ModrinthAuthFlow } from '@/helpers/mr_auth.ts'
import {
- sharedInstanceErrorMessages,
+ sharedInstanceUnavailableTitleMessage,
useSharedInstanceErrors,
} from '@/helpers/shared-instance-errors'
@@ -185,6 +190,16 @@ const actionsLocked = sharedInstanceState.shareActionsLocked
const sharedInstanceActionsLocked = actionsLocked
const currentUserId = computed(() => auth.user.value?.id ?? null)
const isSignedIn = computed(() => !!auth.session_token.value)
+const retryingAvailability = ref(false)
+async function retryAvailability() {
+ if (retryingAvailability.value) return
+ retryingAvailability.value = true
+ try {
+ await sharedInstanceState.refreshAvailability()
+ } finally {
+ retryingAvailability.value = false
+ }
+}
const sharedInstancesApiUnavailable = ref(false)
const accountRequiredModal = ref>()
const invitePlayersModal = ref>()
diff --git a/packages/api-client/src/modules/archon/content/v1.ts b/packages/api-client/src/modules/archon/content/v1.ts
index 1720d758b78..ccb7a03fe68 100644
--- a/packages/api-client/src/modules/archon/content/v1.ts
+++ b/packages/api-client/src/modules/archon/content/v1.ts
@@ -18,6 +18,15 @@ export class ArchonContentV1Module extends AbstractModule {
})
}
+ public async unshare(serverId: string, worldId: string): Promise {
+ return this.client.request(`/servers/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId)}/content/share`, {
+ api: 'archon',
+ version: 1,
+ method: 'DELETE',
+ retry: false,
+ })
+ }
+
/** GET /v1/:server_id/worlds/:world_id/addons */
public async getAddons(
serverId: string,
diff --git a/packages/api-client/src/modules/archon/types.ts b/packages/api-client/src/modules/archon/types.ts
index 7038a65e6c8..16c534afa05 100644
--- a/packages/api-client/src/modules/archon/types.ts
+++ b/packages/api-client/src/modules/archon/types.ts
@@ -307,6 +307,12 @@ export namespace Archon {
local_updated_at: string
has_changes: boolean
diffs: SharedContentDiffEntry[]
+ projects: Record
+ versions: Record
}
export type AddonKind = 'mod' | 'plugin' | 'datapack' | 'shader' | 'resourcepack'
@@ -1185,6 +1191,12 @@ export namespace Archon {
needs_update: boolean
}
+ export type WorldSharedInstanceDeleteEvent = {
+ type: 'world.shared_instance.delete'
+ world_id: string
+ shared_instance_id: string
+ }
+
export type SyncEvent =
| ProtocolResetEvent
| ProtocolInvalidEvent
@@ -1205,6 +1217,7 @@ export namespace Archon {
| WorldContentBaseUpdateEvent
| WorldContentUpdateEvent
| WorldSharedInstanceUpdateEvent
+ | WorldSharedInstanceDeleteEvent
}
}
diff --git a/packages/app-lib/src/api/instance/shared/install.rs b/packages/app-lib/src/api/instance/shared/install.rs
index f2de99b2c4f..ea422033e28 100644
--- a/packages/app-lib/src/api/instance/shared/install.rs
+++ b/packages/app-lib/src/api/instance/shared/install.rs
@@ -439,6 +439,12 @@ pub(super) async fn handle_unavailable_shared_instance_if_current_user(
reason: SharedInstanceUnavailableReason,
state: &State,
) -> crate::Result<()> {
+ // A missing ID can belong to another API environment. Keep the attachment
+ // so opening a production instance in a staging build cannot unlink it.
+ if reason == SharedInstanceUnavailableReason::Deleted {
+ return Ok(());
+ }
+
if reason != SharedInstanceUnavailableReason::Quarantined
&& !shared_attachment_matches_current_user(attachment, state).await?
{
diff --git a/packages/ui/src/components/base/NavTabs.vue b/packages/ui/src/components/base/NavTabs.vue
index 1958a4498b8..b74ecd34e6a 100644
--- a/packages/ui/src/components/base/NavTabs.vue
+++ b/packages/ui/src/components/base/NavTabs.vue
@@ -182,6 +182,8 @@ function computeActiveIndex(): { index: number; isSubpage: boolean } {
}
}
+ let subpageIndex = -1
+
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
const link = filteredLinks.value[i]
const decodedPath = decodeURIComponent(route.path)
@@ -204,12 +206,12 @@ function computeActiveIndex(): { index: number; isSubpage: boolean } {
(decodedPath.length === decodedHref.length || decodedPath[decodedHref.length] === '/')) ||
link.subpages?.some((subpage) => decodedPath.includes(subpage))
- if (isSubpageMatch) {
- return { index: i, isSubpage: true }
+ if (isSubpageMatch && subpageIndex === -1) {
+ subpageIndex = i
}
}
- return { index: -1, isSubpage: false }
+ return { index: subpageIndex, isSubpage: subpageIndex !== -1 }
}
function getTabElement(index: number): HTMLElement | null {
diff --git a/packages/ui/src/components/base/inputs/Input.vue b/packages/ui/src/components/base/inputs/Input.vue
index ab805a9aa0f..9f1b5d5cf71 100644
--- a/packages/ui/src/components/base/inputs/Input.vue
+++ b/packages/ui/src/components/base/inputs/Input.vue
@@ -28,35 +28,52 @@
-
+
+ {{ hasValue ? model : placeholder || ' ' }}
+
+
+
+
+
diff --git a/packages/ui/src/components/servers/ServerSettingsModal.vue b/packages/ui/src/components/servers/ServerSettingsModal.vue
index 983c34cb55a..b4c1f2f53aa 100644
--- a/packages/ui/src/components/servers/ServerSettingsModal.vue
+++ b/packages/ui/src/components/servers/ServerSettingsModal.vue
@@ -2,7 +2,7 @@
import type { Archon } from '@modrinth/api-client'
import { ChevronRightIcon } from '@modrinth/assets'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
-import { computed, nextTick, ref } from 'vue'
+import { computed, nextTick, ref, watch } from 'vue'
import type { TabbedModalTab } from '#ui/components'
import { TabbedModal } from '#ui/components'
@@ -14,6 +14,7 @@ import {
ServerSettingsNetworkPage,
ServerSettingsPropertiesPage,
ServerSettingsSupportPage,
+ ServerSettingsSharingPage,
serverSettingsTabDefinitions,
type ServerSettingsTabId,
} from '#ui/layouts/shared/server-settings'
@@ -32,6 +33,7 @@ type ShowOptions = {
}
const props = defineProps<{
+ siteUrl?: string
resolveViewer: () => Promise<{ userId: string | null; userRole: string | null }>
browseModpacks?: (args: {
serverId: string
@@ -54,7 +56,7 @@ const messages = defineMessages({
const modal = ref | null>(null)
-const { serverId: currentServerId, worldId, server } = injectModrinthServerContext()
+const { serverId: currentServerId, worldId, server, serverFull } = injectModrinthServerContext()
const currentUserId = ref(null)
const currentUserRole = ref(null)
@@ -75,6 +77,7 @@ useQuery({
const serverSettingsTabComponentMap = {
general: ServerSettingsGeneralPage,
installation: ServerSettingsInstallationPage,
+ sharing: ServerSettingsSharingPage,
network: ServerSettingsNetworkPage,
properties: ServerSettingsPropertiesPage,
advanced: ServerSettingsAdvancedPage,
@@ -85,6 +88,7 @@ const saveBannerTarget = ref(null)
const saveBannerShown = ref(false)
provideServerSettings({
+ siteUrl: computed(() => (props.siteUrl ?? 'https://modrinth.com').replace(/\/$/, '')),
isApp,
currentUserId,
currentUserRole,
@@ -105,6 +109,8 @@ const tabs = computed(() =>
serverStatus: server.value?.status,
isOwner: isOwner.value,
isAdmin: isAdmin.value,
+ isShared: !!serverFull.value?.worlds.find((world) => world.id === worldId.value)?.content
+ ?.shared_instance_id,
}
const name = defineMessage({
id: `server.settings.tabs.${tab.id}`,
@@ -130,6 +136,15 @@ const tabs = computed(() =>
}),
)
+watch(tabs, (currentTabs, previousTabs) => {
+ if (!modal.value) return
+ const selectedTab = previousTabs.filter((tab) => tab.shown !== false)[modal.value.selectedTab]
+ const selectedIndex = currentTabs
+ .filter((tab) => tab.shown !== false)
+ .findIndex((tab) => tab.content === selectedTab?.content)
+ modal.value.setTab(Math.max(selectedIndex, 0))
+})
+
async function fetchViewer() {
currentUserId.value = null
currentUserRole.value = null
diff --git a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
index 7a09eeb172f..08a45cfab50 100644
--- a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
+++ b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
@@ -1,8 +1,8 @@
+
+
+ {{ permissionDeniedMessage }}
+ {{ formatMessage(messages.notShared) }}
+
+
diff --git a/packages/ui/src/layouts/shared/server-settings/providers/server-settings.ts b/packages/ui/src/layouts/shared/server-settings/providers/server-settings.ts
index 59abf87f198..fa2250c8f37 100644
--- a/packages/ui/src/layouts/shared/server-settings/providers/server-settings.ts
+++ b/packages/ui/src/layouts/shared/server-settings/providers/server-settings.ts
@@ -9,6 +9,7 @@ export interface ServerSettingsBrowseModpacksArgs {
}
export interface ServerSettingsContext {
+ siteUrl: Ref
isApp: Ref
currentUserId: Ref
currentUserRole: Ref
diff --git a/packages/ui/src/layouts/shared/server-settings/tabs.ts b/packages/ui/src/layouts/shared/server-settings/tabs.ts
index 518ceb40d6c..fc141756276 100644
--- a/packages/ui/src/layouts/shared/server-settings/tabs.ts
+++ b/packages/ui/src/layouts/shared/server-settings/tabs.ts
@@ -5,6 +5,7 @@ import {
ModrinthIcon,
SettingsIcon,
ShieldIcon,
+ UsersIcon,
TextQuoteIcon,
VersionIcon,
WrenchIcon,
@@ -14,6 +15,7 @@ import type { Component } from 'vue'
export type ServerSettingsTabId =
| 'general'
| 'installation'
+ | 'sharing'
| 'network'
| 'properties'
| 'advanced'
@@ -27,6 +29,7 @@ export interface ServerSettingsTabContext {
serverStatus?: Archon.Servers.v0.Status | null
isOwner: boolean
isAdmin: boolean
+ isShared: boolean
}
export interface ServerSettingsTabDefinition {
@@ -49,6 +52,12 @@ export const serverSettingsTabDefinitions: ServerSettingsTabDefinition[] = [
label: 'Installation',
icon: WrenchIcon,
},
+ {
+ id: 'sharing',
+ label: 'Sharing',
+ icon: UsersIcon,
+ shown: ({ isShared }) => isShared,
+ },
{
id: 'network',
label: 'Network',
diff --git a/packages/ui/src/layouts/shared/server-sharing/cache.ts b/packages/ui/src/layouts/shared/server-sharing/cache.ts
new file mode 100644
index 00000000000..fab0913ff68
--- /dev/null
+++ b/packages/ui/src/layouts/shared/server-sharing/cache.ts
@@ -0,0 +1,39 @@
+import type { Archon } from '@modrinth/api-client'
+import type { QueryClient } from '@tanstack/vue-query'
+
+export async function clearServerSharedInstance(
+ queryClient: QueryClient,
+ serverId: string,
+ worldId: string,
+ sharedInstanceId: string,
+) {
+ const detailKey = ['servers', 'v1', 'detail', serverId] as const
+ const instanceKey = ['shared-instances', sharedInstanceId] as const
+ const diffKey = ['servers', 'share-diff', serverId, worldId] as const
+ await Promise.all([
+ queryClient.cancelQueries({ queryKey: detailKey }),
+ queryClient.cancelQueries({ queryKey: instanceKey }),
+ queryClient.cancelQueries({ queryKey: diffKey }),
+ ])
+ queryClient.setQueryData(detailKey, (server) =>
+ server
+ ? {
+ ...server,
+ worlds: server.worlds.map((world) =>
+ world.id === worldId && world.content?.shared_instance_id === sharedInstanceId
+ ? {
+ ...world,
+ content: {
+ ...world.content,
+ shared_instance_id: null,
+ shared_instance_needs_update: false,
+ },
+ }
+ : world,
+ ),
+ }
+ : server,
+ )
+ queryClient.removeQueries({ queryKey: instanceKey })
+ queryClient.removeQueries({ queryKey: diffKey })
+}
diff --git a/packages/ui/src/layouts/shared/server-sharing/index.ts b/packages/ui/src/layouts/shared/server-sharing/index.ts
new file mode 100644
index 00000000000..e0c6049e1d9
--- /dev/null
+++ b/packages/ui/src/layouts/shared/server-sharing/index.ts
@@ -0,0 +1,3 @@
+export { clearServerSharedInstance } from './cache'
+export { sharedInstanceInvitesQueryOptions } from './query-options'
+export { useServerSharingSettings } from './use-server-sharing'
diff --git a/packages/ui/src/layouts/shared/server-sharing/query-options.ts b/packages/ui/src/layouts/shared/server-sharing/query-options.ts
new file mode 100644
index 00000000000..532f9dceee1
--- /dev/null
+++ b/packages/ui/src/layouts/shared/server-sharing/query-options.ts
@@ -0,0 +1,13 @@
+import type { AbstractModrinthClient } from '@modrinth/api-client'
+
+export function sharedInstanceInvitesQueryOptions(
+ client: AbstractModrinthClient,
+ instanceId: string,
+ userId: string | undefined,
+) {
+ return {
+ queryKey: ['shared-instances', instanceId, 'invites', userId] as const,
+ queryFn: () => client.sharedinstances.invites_v1.list(instanceId),
+ retry: false,
+ }
+}
diff --git a/packages/ui/src/layouts/shared/server-sharing/use-server-sharing.ts b/packages/ui/src/layouts/shared/server-sharing/use-server-sharing.ts
new file mode 100644
index 00000000000..57026f31338
--- /dev/null
+++ b/packages/ui/src/layouts/shared/server-sharing/use-server-sharing.ts
@@ -0,0 +1,120 @@
+import type { SharedInstances } from '@modrinth/api-client'
+import { useIsMutating, useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
+import { useStorage } from '@vueuse/core'
+import { computed } from 'vue'
+
+import { useServerPermissions } from '#ui/composables/server-permissions'
+import {
+ injectAuth,
+ injectModrinthClient,
+ injectModrinthServerContext,
+ injectNotificationManager,
+} from '#ui/providers'
+
+import type { SharingSettingsContext } from '../sharing-settings'
+import { clearServerSharedInstance } from './cache'
+import { sharedInstanceInvitesQueryOptions } from './query-options'
+
+export function useServerSharingSettings() {
+ const client = injectModrinthClient()
+ const auth = injectAuth()
+ const queryClient = useQueryClient()
+ const { handleError } = injectNotificationManager()
+ const { serverId, worldId, serverFull, busyReasons } = injectModrinthServerContext()
+ const { canSetup, permissionDeniedMessage } = useServerPermissions()
+ const preferences = useStorage(
+ `pyro-server-${serverId}-preferences`,
+ { reviewChangesBeforePlaying: false },
+ undefined,
+ { mergeDefaults: true },
+ )
+ const userId = computed(() => auth.user.value?.id)
+ const sharedInstanceId = computed(() =>
+ serverFull.value?.worlds.find((world) => world.id === worldId.value)?.content?.shared_instance_id,
+ )
+ const targetKey = computed(() =>
+ canSetup.value && worldId.value && sharedInstanceId.value && userId.value
+ ? JSON.stringify([serverId, worldId.value, sharedInstanceId.value, userId.value])
+ : null,
+ )
+ const invites = useQuery(
+ computed(() => ({
+ ...sharedInstanceInvitesQueryOptions(client, sharedInstanceId.value ?? '', userId.value),
+ enabled: !!targetKey.value,
+ refetchOnMount: 'always' as const,
+ refetchInterval: 30_000,
+ })),
+ )
+ const shareActions = useIsMutating({ mutationKey: ['servers', 'share-action', serverId] })
+
+ type ShareTarget = { worldId: string; instanceId: string }
+ const revokeMutation = useMutation({
+ mutationFn: (target: ShareTarget & { inviteId: string }) =>
+ client.sharedinstances.invites_v1.delete(target.instanceId, target.inviteId),
+ onSuccess: (_data, target) => {
+ queryClient.setQueriesData(
+ { queryKey: ['shared-instances', target.instanceId, 'invites'] },
+ (current) => current?.filter((invite) => invite.id !== target.inviteId),
+ )
+ },
+ onSettled: (_data, _error, target) =>
+ queryClient.invalidateQueries({ queryKey: ['shared-instances', target.instanceId, 'invites'] }),
+ })
+ const unpublishMutation = useMutation({
+ mutationKey: ['servers', 'share-action', serverId],
+ mutationFn: (target: ShareTarget) => client.archon.content_v1.unshare(serverId, target.worldId),
+ onSuccess: (_data, target) =>
+ clearServerSharedInstance(queryClient, serverId, target.worldId, target.instanceId),
+ onSettled: () => queryClient.invalidateQueries({ queryKey: ['servers', 'v1', 'detail', serverId] }),
+ })
+ const busy = computed(
+ () =>
+ !targetKey.value ||
+ busyReasons.value.length > 0 ||
+ shareActions.value > 0 ||
+ revokeMutation.isPending.value ||
+ unpublishMutation.isPending.value,
+ )
+
+ function currentTarget(expectedKey: string): ShareTarget | undefined {
+ if (busy.value || expectedKey !== targetKey.value || !worldId.value || !sharedInstanceId.value) {
+ return
+ }
+ return { worldId: worldId.value, instanceId: sharedInstanceId.value }
+ }
+
+ const settings: SharingSettingsContext = {
+ targetKey,
+ invites: computed(() =>
+ (invites.data.value ?? []).map((invite) => ({
+ id: invite.id,
+ expiration: invite.expiration,
+ maxUses: invite.max_uses,
+ uses: invite.uses,
+ })),
+ ),
+ loading: invites.isLoading,
+ error: invites.error,
+ busy,
+ refresh: async () => {
+ if (targetKey.value) await invites.refetch()
+ },
+ revokeInvite: async (inviteId, expectedKey) => {
+ const target = currentTarget(expectedKey)
+ if (target) await revokeMutation.mutateAsync({ ...target, inviteId })
+ },
+ unpublish: async (expectedKey) => {
+ const target = currentTarget(expectedKey)
+ if (target) await unpublishMutation.mutateAsync(target)
+ },
+ onError: (error) => handleError(error),
+ reviewChangesBeforePlaying: computed({
+ get: () => preferences.value.reviewChangesBeforePlaying,
+ set: (value) => {
+ preferences.value.reviewChangesBeforePlaying = value
+ },
+ }),
+ }
+
+ return { settings, canSetup, permissionDeniedMessage, sharedInstanceId }
+}
diff --git a/packages/ui/src/layouts/shared/sharing-settings/components/active-invites.vue b/packages/ui/src/layouts/shared/sharing-settings/components/active-invites.vue
new file mode 100644
index 00000000000..33515907236
--- /dev/null
+++ b/packages/ui/src/layouts/shared/sharing-settings/components/active-invites.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+ {{ formatMessage(messages.activeInvitesTitle) }}
+
+
{{ formatMessage(messages.activeInvitesDescription) }}
+
+
+ {{ formatMessage(messages.retry) }}
+
+
+
+
+
+ {{ formatMessage(messages.noInvites) }}
+
+
+
+
+
+
+ {{ row.uses }}
+ / {{ row.maxUses }}
+
+
+
+ {{ formatRelativeTime(row.expiration) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/features/instances/settings-modal/confirm-revoke-shared-instance-invite-modal.vue b/packages/ui/src/layouts/shared/sharing-settings/components/revoke-invite-modal.vue
similarity index 72%
rename from apps/app-frontend/src/features/instances/settings-modal/confirm-revoke-shared-instance-invite-modal.vue
rename to packages/ui/src/layouts/shared/sharing-settings/components/revoke-invite-modal.vue
index 89fd9f6cd8f..0db1f327c3a 100644
--- a/apps/app-frontend/src/features/instances/settings-modal/confirm-revoke-shared-instance-invite-modal.vue
+++ b/packages/ui/src/layouts/shared/sharing-settings/components/revoke-invite-modal.vue
@@ -1,7 +1,7 @@
-
+
@@ -14,7 +14,7 @@
{{ formatMessage(commonMessages.cancelButton) }}
-
+
{{ formatMessage(messages.revokeButton) }}
@@ -25,34 +25,19 @@
diff --git a/packages/ui/src/layouts/shared/sharing-settings/components/unpublish-modal.vue b/packages/ui/src/layouts/shared/sharing-settings/components/unpublish-modal.vue
new file mode 100644
index 00000000000..5356282094a
--- /dev/null
+++ b/packages/ui/src/layouts/shared/sharing-settings/components/unpublish-modal.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+ {{ formatMessage(description) }}
+
+
+
+
+ {{ formatMessage(commonMessages.cancelButton) }}
+
+
+
+
+ {{ formatMessage(unpublishing ? messages.unpublishingButton : messages.unpublishButton) }}
+
+
+
+
+
diff --git a/packages/ui/src/layouts/shared/sharing-settings/index.ts b/packages/ui/src/layouts/shared/sharing-settings/index.ts
new file mode 100644
index 00000000000..fa4e1445363
--- /dev/null
+++ b/packages/ui/src/layouts/shared/sharing-settings/index.ts
@@ -0,0 +1,3 @@
+export { default as SharingSettingsLayout } from './layout.vue'
+export * from './providers/sharing-settings'
+export * from './types'
diff --git a/packages/ui/src/layouts/shared/sharing-settings/layout.vue b/packages/ui/src/layouts/shared/sharing-settings/layout.vue
new file mode 100644
index 00000000000..5bdd71e98a5
--- /dev/null
+++ b/packages/ui/src/layouts/shared/sharing-settings/layout.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.unpublishTitle) }}
+
+
+
+
+
+ {{ formatMessage(unpublishing ? messages.unpublishing : messages.unpublish) }}
+
+
+ {{ formatMessage(unpublishDescription) }}
+
+
+
+
+
diff --git a/packages/ui/src/layouts/shared/sharing-settings/providers/sharing-settings.ts b/packages/ui/src/layouts/shared/sharing-settings/providers/sharing-settings.ts
new file mode 100644
index 00000000000..d8e6f27ea9f
--- /dev/null
+++ b/packages/ui/src/layouts/shared/sharing-settings/providers/sharing-settings.ts
@@ -0,0 +1,21 @@
+import type { Ref } from 'vue'
+
+import { createContext } from '#ui/providers/create-context'
+
+import type { SharingInvite } from '../types'
+
+export interface SharingSettingsContext {
+ targetKey: Readonly[>
+ invites: Readonly][>
+ loading: Readonly][>
+ error: Readonly][>
+ busy: Readonly][>
+ refresh: () => Promise
+ revokeInvite: (inviteId: string, targetKey: string) => Promise
+ unpublish: (targetKey: string) => Promise
+ onError: (error: unknown) => void
+ reviewChangesBeforePlaying?: Ref
+}
+
+export const [injectSharingSettings, provideSharingSettings] =
+ createContext('SharingSettingsLayout', 'sharingSettings')
diff --git a/packages/ui/src/layouts/shared/sharing-settings/types.ts b/packages/ui/src/layouts/shared/sharing-settings/types.ts
new file mode 100644
index 00000000000..288ea7ff398
--- /dev/null
+++ b/packages/ui/src/layouts/shared/sharing-settings/types.ts
@@ -0,0 +1,6 @@
+export interface SharingInvite {
+ id: string
+ expiration: string
+ maxUses: number
+ uses: number
+}
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
index 5e662bb1e9d..80abb4a6dc8 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayCard.vue
@@ -41,21 +41,18 @@
{{ formatMessage(messages.downloadModpackButton) }}
]
-
- {{ address }}
-
-
-
-
-
+ {{ address }}
+
+
+
@@ -64,7 +61,7 @@
-
+
diff --git a/apps/app-frontend/src/pages/instance/layout.vue b/apps/app-frontend/src/pages/instance/layout.vue
index 742c31350a7..22bea0e389b 100644
--- a/apps/app-frontend/src/pages/instance/layout.vue
+++ b/apps/app-frontend/src/pages/instance/layout.vue
@@ -36,7 +36,7 @@
:show-instance-play-time="showInstancePlayTime"
:time-played="timePlayed"
:playing="playing"
- :loading="loading"
+ :loading="loading || checkingSharedInstanceLaunch || instanceLaunch.isStarting(instance.id)"
:stopping="stopping"
:loading-server-ping="loadingServerPing"
:players-online="playersOnline"
@@ -125,6 +125,7 @@ import {
getServerStatusQueryKey,
} from '@/composables/instances/use-server-status-query'
import { useAppEvent } from '@/composables/use-app-event'
+import { useInstanceLaunchState } from '@/composables/instances/use-instance-launch-state'
import { useAppSettings } from '@/composables/use-app-settings.ts'
import { handleSevereError } from '@/composables/use-error.js'
import { useInstanceConsole } from '@/composables/useInstanceConsole'
@@ -359,6 +360,7 @@ useRootBreadcrumb({
})
const loading = ref(false)
+const instanceLaunch = useInstanceLaunchState()
const checkingSharedInstanceLaunch = ref(false)
const subpagePending = ref(false)
const stopping = ref(false)
@@ -545,9 +547,11 @@ const launchInstance = async (context: string, address?: string) => {
const currentInstance = instance.value
loading.value = true
try {
- if (address) await start_join_server(currentInstance.id, address)
- else await run(currentInstance.id)
- queryClient.setQueryData(instanceKeys.processes(currentInstance.id), [true])
+ await instanceLaunch.run(currentInstance.id, async () => {
+ if (address) await start_join_server(currentInstance.id, address)
+ else await run(currentInstance.id)
+ queryClient.setQueryData(instanceKeys.processes(currentInstance.id), [true])
+ })
} catch (err) {
handleSevereError(err, { instanceId: currentInstance.id })
}
@@ -602,7 +606,7 @@ function handleSharedInstanceUpdateComplete(successful: boolean) {
const startInstance = async (context: string, address?: string) => {
if (!instance.value || instance.value.quarantined) return
- if (checkingSharedInstanceLaunch.value || loading.value || playing.value) return
+ if (checkingSharedInstanceLaunch.value || loading.value || playing.value || instanceLaunch.isStarting(instance.value.id)) return
const instanceId = instance.value.id
const isSharedInstanceMember = instance.value.shared_instance?.role === 'member'
diff --git a/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue b/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue
index 35d81cabc0d..48fc6dac4f4 100644
--- a/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue
+++ b/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue
@@ -1,175 +1,42 @@
-
-
-
-
-
-
-
-
- {{ formatMessage(messages.pushUpdate) }}
-
-
-
-
- Invite friends
-
-
-
-
-
-
- All
-
-
- {{ option.label }}
-
-
-
-
-
-
- {{
- formatMessage(rows.length === 0 ? messages.noUsersJoined : messages.noUsersMatchFilters)
- }}
-
+
+
+
+
+ {{ formatMessage(messages.pushUpdate) }}
+
+
-
-
-
-
- {{ row.username }}
-
-
-
-
- {{
- formatRelativeTime(row.lastPlayedAt)
- }}
- Never
-
-
- Pending
- {{
- formatRelativeTime(row.joinedAt)
- }}
-
-
-
-
-
- {{ methodLabels[row.method] }}
-
-
-
-
-
-
-
-
-
-
+
+
+ Invite friends
+
+
+
diff --git a/apps/app-frontend/src/pages/instance/share/shared-instance-share-types.ts b/apps/app-frontend/src/pages/instance/share/shared-instance-share-types.ts
index a9bebd01c25..9938247ea0f 100644
--- a/apps/app-frontend/src/pages/instance/share/shared-instance-share-types.ts
+++ b/apps/app-frontend/src/pages/instance/share/shared-instance-share-types.ts
@@ -1,22 +1,8 @@
-export type ShareMethod = 'direct' | 'link'
-export type MethodFilter = ShareMethod | 'all'
-export type ShareTableColumn = 'username' | 'lastPlayed' | 'joined' | 'method' | 'actions'
+export {
+ invitedPlayerMethodLabels as methodLabels,
+ normalizeInviteKey,
+ type InvitedPlayerMethod as ShareMethod,
+ type InvitedPlayerRow as ShareRow,
+} from '@modrinth/ui'
export const SHARED_INSTANCE_USER_LIMIT = 50
-
-export type ShareRow = {
- id: string
- username: string
- avatarUrl?: string
- lastPlayedAt: Date | null
- joinedAt: Date | null
- method: ShareMethod
- pending?: boolean
-}
-
-export const methodLabels: Record
= {
- direct: 'Direct invite',
- link: 'Share link',
-}
-
-export { normalizeInviteKey } from '@modrinth/ui'
diff --git a/apps/app/capabilities/plugins.json b/apps/app/capabilities/plugins.json
index b08053e33eb..bc72394dcfa 100644
--- a/apps/app/capabilities/plugins.json
+++ b/apps/app/capabilities/plugins.json
@@ -2,7 +2,9 @@
"identifier": "plugins",
"description": "",
"local": true,
- "windows": ["main"],
+ "windows": [
+ "main"
+ ],
"permissions": [
"dialog:allow-open",
"dialog:allow-confirm",
@@ -31,6 +33,9 @@
{
"url": "https://*.nodes.modrinth.com/*"
},
+ {
+ "url": "https://*.tail029726.ts.net/*"
+ },
{
"url": "https://api.mclo.gs/*"
},
diff --git a/packages/api-client/src/modules/archon/content/v1.ts b/packages/api-client/src/modules/archon/content/v1.ts
index ccb7a03fe68..a4a20939449 100644
--- a/packages/api-client/src/modules/archon/content/v1.ts
+++ b/packages/api-client/src/modules/archon/content/v1.ts
@@ -6,9 +6,10 @@ export class ArchonContentV1Module extends AbstractModule {
return 'archon_content_v1'
}
- public async share(serverId: string, worldId: string): Promise {
+ public async share(serverId: string, worldId: string, configPaths: string[] = []): Promise {
return this.client.request(`/servers/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId)}/content/share`, {
api: 'archon', version: 1, method: 'POST', timeout: 600_000, retry: false,
+ body: { config_paths: configPaths },
})
}
diff --git a/packages/api-client/src/modules/shared-instances/types.ts b/packages/api-client/src/modules/shared-instances/types.ts
index c41d30a55e9..773039d06fe 100644
--- a/packages/api-client/src/modules/shared-instances/types.ts
+++ b/packages/api-client/src/modules/shared-instances/types.ts
@@ -48,6 +48,7 @@ export namespace SharedInstances {
name: string
icon: string | null
quarantine: boolean
+ linked_server: { domain: string; region: string } | null
}
export type JoinType = 'owner' | 'invite' | 'link'
diff --git a/packages/app-lib/src/api/instance.rs b/packages/app-lib/src/api/instance.rs
index 9f7b21562b6..8ef1e53c905 100644
--- a/packages/app-lib/src/api/instance.rs
+++ b/packages/app-lib/src/api/instance.rs
@@ -75,6 +75,7 @@ pub use self::screenshots::{
};
pub(crate) use self::shared::{
CONFIG_BUNDLE_FILE_TYPE, CONFIG_DIRECTORY, CONFIG_FILE_EXTENSIONS,
+ CONFIG_FILE_TYPE, MAX_CONFIG_BUNDLE_FILE_SIZE,
CONFIG_SYNC_ENABLED, MAX_CONFIG_BUNDLE_ENTRIES,
read_bounded_config_bundle_entry,
};
diff --git a/packages/app-lib/src/api/instance/shared/client.rs b/packages/app-lib/src/api/instance/shared/client.rs
index bedd567b3fc..8982213a86f 100644
--- a/packages/app-lib/src/api/instance/shared/client.rs
+++ b/packages/app-lib/src/api/instance/shared/client.rs
@@ -34,6 +34,11 @@ pub(super) enum SharedInstanceRemoteResponse {
#[derive(Clone, Debug, Deserialize)]
pub(super) struct RemoteInstanceResponse {
+ pub(super) name: String,
+ pub(super) icon: Option,
+ #[serde(default)]
+ pub(super) linked_server:
+ Option,
#[serde(default)]
pub(super) quarantine: bool,
}
@@ -242,7 +247,7 @@ pub(super) async fn update_remote_instance(
pub(super) async fn get_remote_instance_access(
shared_instance_id: &str,
state: &State,
-) -> crate::Result> {
+) -> crate::Result> {
let operation = "get_instance";
let method = Method::GET;
let path = format!("/instances/{shared_instance_id}");
@@ -278,7 +283,7 @@ pub(super) async fn get_remote_instance_access(
));
}
- Ok(SharedInstanceRemoteResponse::Available(()))
+ Ok(SharedInstanceRemoteResponse::Available(instance))
}
pub(super) async fn update_remote_instance_icon(
diff --git a/packages/app-lib/src/api/instance/shared/diff.rs b/packages/app-lib/src/api/instance/shared/diff.rs
index 84483bce32a..23dbdef3320 100644
--- a/packages/app-lib/src/api/instance/shared/diff.rs
+++ b/packages/app-lib/src/api/instance/shared/diff.rs
@@ -33,7 +33,7 @@ pub(super) async fn shared_instance_update_diffs(
configuration: after_configuration,
};
- shared_content_diffs(
+ let mut diffs = shared_content_diffs(
&before,
&after,
&HashSet::new(),
@@ -42,7 +42,24 @@ pub(super) async fn shared_instance_update_diffs(
CommonExternalFilePolicy::AssumeUpdated,
state,
)
- .await
+ .await?;
+ let config_file_count = version.external_files
+ .iter()
+ .filter(|file| file.file_type == CONFIG_FILE_TYPE)
+ .count();
+ if config_file_count > 0 {
+ diffs.push(SharedInstanceUpdateDiff {
+ type_: SharedInstanceUpdateDiffType::ConfigFilesUpdated,
+ project_id: None,
+ project_name: None,
+ file_name: None,
+ current_version_name: None,
+ new_version_name: None,
+ config_file_count: Some(config_file_count),
+ disabled: false,
+ });
+ }
+ Ok(diffs)
}
pub(super) async fn shared_instance_publish_diffs(
@@ -484,7 +501,7 @@ fn remote_shared_content(
let external_files = version
.external_files
.iter()
- .filter(|file| file.file_type != CONFIG_BUNDLE_FILE_TYPE)
+ .filter(|file| !matches!(file.file_type.as_str(), CONFIG_BUNDLE_FILE_TYPE | CONFIG_FILE_TYPE))
.map(|file| shared_external_file_key(&file.file_type, &file.file_name))
.collect::>()?;
Ok((version_ids, external_files))
diff --git a/packages/app-lib/src/api/instance/shared/install.rs b/packages/app-lib/src/api/instance/shared/install.rs
index ea422033e28..9bb71f77a4b 100644
--- a/packages/app-lib/src/api/instance/shared/install.rs
+++ b/packages/app-lib/src/api/instance/shared/install.rs
@@ -142,7 +142,7 @@ pub(super) async fn shared_instance_install_preview_from_version(
let external_files = version
.external_files
.iter()
- .filter(|file| file.file_type != CONFIG_BUNDLE_FILE_TYPE)
+ .filter(|file| !matches!(file.file_type.as_str(), CONFIG_BUNDLE_FILE_TYPE | CONFIG_FILE_TYPE))
.map(|file| SharedInstanceExternalFilePreview {
file_name: file.file_name.clone(),
file_type: file.file_type.clone(),
@@ -529,6 +529,22 @@ pub(super) async fn shared_instance_install_data(
.into());
}
+ let remote = match get_remote_instance_access(shared_instance_id, state)
+ .await?
+ {
+ SharedInstanceRemoteResponse::Available(remote) => remote,
+ SharedInstanceRemoteResponse::Unavailable(reason) => {
+ return Err(shared_instance_unavailable_error(reason));
+ }
+ };
+ let (manager_id, server_manager_name, server_manager_icon_url) =
+ if remote.linked_server.is_some() {
+ (None, Some(remote.name), remote.icon.clone())
+ } else {
+ (manager_id, server_manager_name, server_manager_icon_url)
+ };
+ let instance_icon_url = remote.icon.or(instance_icon_url);
+
let name = shared_instance_name(name);
let linked_user_id = linked_modrinth_user_id(state).await?;
let modpack = shared_instance_install_modpack(&version, state).await?;
@@ -540,7 +556,29 @@ pub(super) async fn shared_instance_install_data(
.filter(|id| Some(id.as_str()) != modpack_version_id)
.collect();
+ let mut config_count = 0;
+ let mut config_size = 0_u64;
+ for file in &version.external_files {
+ if file.file_type == CONFIG_FILE_TYPE {
+ config_count += 1;
+ config_size = config_size.saturating_add(
+ file.file_size
+ .and_then(|size| u64::try_from(size).ok())
+ .unwrap_or(u64::MAX),
+ );
+ }
+ }
+ if config_count > MAX_CONFIG_BUNDLE_ENTRIES
+ || config_size > MAX_CONFIG_BUNDLE_TOTAL_SIZE
+ {
+ return Err(crate::ErrorKind::InputError(
+ "Shared instance config files exceed the size or file count limit"
+ .to_string(),
+ ).into());
+ }
+
Ok(SharedInstanceInstallData {
+ linked_server: remote.linked_server,
shared_instance_id: shared_instance_id.to_string(),
manager_id,
server_manager_name,
diff --git a/packages/app-lib/src/api/instance/shared/mod.rs b/packages/app-lib/src/api/instance/shared/mod.rs
index 4fdce76f1c8..730736cddf0 100644
--- a/packages/app-lib/src/api/instance/shared/mod.rs
+++ b/packages/app-lib/src/api/instance/shared/mod.rs
@@ -32,6 +32,7 @@ use std::io::Read;
pub(crate) const CONFIG_BUNDLE_FILE_NAME: &str = "configs.zip";
pub(crate) const CONFIG_BUNDLE_FILE_TYPE: &str = "configs";
+pub(crate) const CONFIG_FILE_TYPE: &str = "config";
pub(crate) const CONFIG_SYNC_ENABLED: bool = true;
pub(crate) const CONFIG_DIRECTORY: &str = "config";
pub(crate) const MAX_CONFIG_BUNDLE_ENTRIES: usize = 4096;
diff --git a/packages/app-lib/src/api/instance/shared/publish.rs b/packages/app-lib/src/api/instance/shared/publish.rs
index 26d4d0d12af..53aec53cfc5 100644
--- a/packages/app-lib/src/api/instance/shared/publish.rs
+++ b/packages/app-lib/src/api/instance/shared/publish.rs
@@ -195,7 +195,7 @@ pub(super) async fn remote_publish_content(
version
.external_files
.iter()
- .filter(|file| file.file_type != CONFIG_BUNDLE_FILE_TYPE)
+ .filter(|file| !matches!(file.file_type.as_str(), CONFIG_BUNDLE_FILE_TYPE | CONFIG_FILE_TYPE))
.map(|file| {
shared_external_file_key(&file.file_type, &file.file_name)
})
diff --git a/packages/app-lib/src/api/pack/install_from.rs b/packages/app-lib/src/api/pack/install_from.rs
index 0dfa24941b4..9baca6e6a6e 100644
--- a/packages/app-lib/src/api/pack/install_from.rs
+++ b/packages/app-lib/src/api/pack/install_from.rs
@@ -630,12 +630,16 @@ pub async fn set_instance_information(
&instance_id,
EditInstance {
install_stage: Some(InstanceInstallStage::PackInstalling),
- name: Some(
- description
- .override_title
- .clone()
- .unwrap_or_else(|| backup_name.to_string()),
- ),
+ name: if matches!(&link, Some(InstanceLink::SharedInstance { .. })) {
+ None
+ } else {
+ Some(
+ description
+ .override_title
+ .clone()
+ .unwrap_or_else(|| backup_name.to_string()),
+ )
+ },
icon_path: description
.icon
.as_ref()
diff --git a/packages/app-lib/src/install/model.rs b/packages/app-lib/src/install/model.rs
index 0746774d610..2216826adf9 100644
--- a/packages/app-lib/src/install/model.rs
+++ b/packages/app-lib/src/install/model.rs
@@ -222,8 +222,16 @@ pub struct InstallPostInstallEdit {
pub link: Option,
}
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct SharedInstanceLinkedServer {
+ pub domain: String,
+ pub region: String,
+}
+
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SharedInstanceInstallData {
+ #[serde(default)]
+ pub linked_server: Option,
pub shared_instance_id: String,
pub manager_id: Option,
#[serde(default)]
diff --git a/packages/app-lib/src/install/shared_instance.rs b/packages/app-lib/src/install/shared_instance.rs
index b8ff987ae60..b91e882ae88 100644
--- a/packages/app-lib/src/install/shared_instance.rs
+++ b/packages/app-lib/src/install/shared_instance.rs
@@ -9,6 +9,7 @@ use super::runner::{
};
use crate::api::instance::{
CONFIG_BUNDLE_FILE_TYPE, CONFIG_DIRECTORY, CONFIG_FILE_EXTENSIONS,
+ CONFIG_FILE_TYPE, MAX_CONFIG_BUNDLE_FILE_SIZE,
CONFIG_SYNC_ENABLED, MAX_CONFIG_BUNDLE_ENTRIES,
read_bounded_config_bundle_entry,
};
@@ -57,6 +58,15 @@ pub(super) async fn finalize_shared_instance_attachment(
data: &SharedInstanceInstallData,
state: &State,
) -> crate::Result<()> {
+ if let Some(server) = &data.linked_server {
+ crate::api::worlds::ensure_managed_server_in_instance(
+ instance_id,
+ data.server_manager_name.clone()
+ .unwrap_or_else(|| data.name.clone()),
+ server.domain.clone(),
+ )
+ .await?;
+ }
crate::state::attach_shared_instance(
instance_id,
SharedInstanceAttachmentInput {
@@ -118,6 +128,7 @@ struct DesiredSharedInstanceContent {
DesiredSharedInstanceExternalFile,
>,
config_bundle: Option,
+ config_files: Vec,
}
struct SharedInstanceProjectUpdate {
@@ -135,6 +146,7 @@ struct SharedInstanceApplyPlan {
external_updates: Vec,
external_additions: Vec,
config_bundle: Option,
+ config_files: Vec,
}
impl SharedInstanceApplyPlan {
@@ -170,6 +182,7 @@ impl SharedInstanceApplyPlan {
project_removals,
external_removals,
config_bundle: desired.config_bundle,
+ config_files: desired.config_files,
..Default::default()
};
@@ -223,7 +236,8 @@ impl SharedInstanceApplyPlan {
+ self.project_additions.len()
+ self.external_updates.len()
+ self.external_additions.len()
- + usize::from(self.config_bundle.is_some())) as u64
+ + usize::from(self.config_bundle.is_some())
+ + self.config_files.len()) as u64
}
}
@@ -405,10 +419,10 @@ async fn apply_shared_instance_update_inner(
.await?;
}
- if let Some(config_bundle) = plan.config_bundle {
+ for config_file in plan.config_bundle.into_iter().chain(plan.config_files) {
install_shared_instance_external_file(
instance_id,
- &config_bundle,
+ &config_file,
state,
)
.await?;
@@ -556,6 +570,12 @@ async fn desired_shared_instance_content(
}
for file in &data.external_files {
+ if file.file_type == CONFIG_FILE_TYPE {
+ if CONFIG_SYNC_ENABLED {
+ content.config_files.push(file.clone());
+ }
+ continue;
+ }
if file.file_type == CONFIG_BUNDLE_FILE_TYPE {
if CONFIG_SYNC_ENABLED {
content.config_bundle = Some(file.clone());
@@ -842,11 +862,27 @@ async fn install_shared_instance_external_file(
file: &SharedInstanceExternalFileData,
state: &State,
) -> crate::Result<()> {
- if file.file_type == CONFIG_BUNDLE_FILE_TYPE && !CONFIG_SYNC_ENABLED {
+ if matches!(
+ file.file_type.as_str(),
+ CONFIG_BUNDLE_FILE_TYPE | CONFIG_FILE_TYPE
+ ) && !CONFIG_SYNC_ENABLED {
return Ok(());
}
- validate_shared_instance_external_file_name(&file.file_name)?;
+ if file.file_type == CONFIG_FILE_TYPE {
+ crate::state::content_store::validate_relative(&file.file_name)?;
+ if file.file_name.split('/').any(|part| part.starts_with('.'))
+ || !is_supported_config_file(std::path::Path::new(&file.file_name))
+ || file.file_size > MAX_CONFIG_BUNDLE_FILE_SIZE
+ {
+ return Err(crate::ErrorKind::InputError(format!(
+ "Shared instance config file {} is unsupported or too large",
+ file.file_name
+ )).into());
+ }
+ } else {
+ validate_shared_instance_external_file_name(&file.file_name)?;
+ }
if file.file_size > MAX_SHARED_INSTANCE_EXTERNAL_FILE_SIZE {
return Err(crate::ErrorKind::InputError(format!(
@@ -917,6 +953,18 @@ async fn install_shared_instance_external_file(
}
let bytes = bytes::Bytes::from(bytes);
+ if file.file_type == CONFIG_FILE_TYPE {
+ let path = crate::api::instance::validate_instance_file_write(
+ instance_id,
+ &file.file_name,
+ ).await?;
+ if let Some(parent) = path.parent() {
+ crate::util::io::create_dir_all(parent).await?;
+ }
+ crate::util::io::write(path, bytes).await?;
+ return Ok(());
+ }
+
if file.file_type == CONFIG_BUNDLE_FILE_TYPE {
return install_shared_instance_config_bundle(
instance_id,
diff --git a/packages/ui/src/components/base/BaseTerminal.vue b/packages/ui/src/components/base/BaseTerminal.vue
index b169c1528fb..3a3399efcf2 100644
--- a/packages/ui/src/components/base/BaseTerminal.vue
+++ b/packages/ui/src/components/base/BaseTerminal.vue
@@ -1,9 +1,14 @@
-
-
+
+
(),
{
scrollback: Infinity,
+ minLogHeight: undefined,
showInput: false,
disableInput: false,
disableInputTooltip: undefined,
diff --git a/packages/ui/src/components/base/IntlFormatted.vue b/packages/ui/src/components/base/IntlFormatted.vue
index 7edebb2064c..a6a944bcb27 100644
--- a/packages/ui/src/components/base/IntlFormatted.vue
+++ b/packages/ui/src/components/base/IntlFormatted.vue
@@ -55,7 +55,9 @@ const formattedParts = computed(() => {
for (const slotName of slotNames) {
const normalizedName = slotName.startsWith('~') ? slotName.slice(1) : slotName
- slotHandlers[normalizedName] = (chunks) => {
+ let handlerName = `intlSlot${Object.keys(slotHandlers).length}`
+ while (Object.prototype.hasOwnProperty.call(props.values ?? {}, handlerName)) handlerName += '_'
+ slotHandlers[handlerName] = (chunks) => {
const slot = slots[slotName]
if (slot) {
const nodes = slot({
@@ -69,10 +71,12 @@ const formattedParts = computed(() => {
return markRaw(chunks) as VNode[]
}
- msg = msg.replace(
- new RegExp(`\\{${normalizedName}\\}`, 'g'),
- `<${normalizedName}>${normalizedName}>`,
- )
+ msg = msg
+ .replaceAll(`<${normalizedName}>`, `<${handlerName}>`)
+ .replaceAll(`${normalizedName}>`, `${handlerName}>`)
+ if (!Object.prototype.hasOwnProperty.call(props.values ?? {}, normalizedName)) {
+ msg = msg.replaceAll(`{${normalizedName}}`, `<${handlerName}>${handlerName}>`)
+ }
}
try {
diff --git a/packages/ui/src/components/servers/ServerConfigFilePicker.vue b/packages/ui/src/components/servers/ServerConfigFilePicker.vue
new file mode 100644
index 00000000000..d0061c15727
--- /dev/null
+++ b/packages/ui/src/components/servers/ServerConfigFilePicker.vue
@@ -0,0 +1,146 @@
+
+
+
+
+ {{ formatMessage(messages.title) }}
+
+
+
+ {{ formatMessage(messages.retry) }}
+
+
+
+
+
+
+
diff --git a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
index 08a45cfab50..be5e722a73b 100644
--- a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
+++ b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
@@ -10,6 +10,7 @@ import StackedAdmonitions, {
type StackedAdmonitionItem,
} from '#ui/components/base/StackedAdmonitions.vue'
import InstallingBanner from '#ui/components/servers/InstallingBanner.vue'
+import ServerConfigFilePicker from '#ui/components/servers/ServerConfigFilePicker.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { useServerBackupsQueue } from '#ui/composables/server-backups-queue'
import { useServerPermissions } from '#ui/composables/server-permissions'
@@ -42,7 +43,9 @@ const needsShareUpdate = computed(() =>
)
const shareActions = useIsMutating({ mutationKey: ['servers', 'share-action', ctx.serverId] })
const sharePreviews = useIsFetching({ queryKey: ['servers', 'share-diff', ctx.serverId] })
-const sharePending = computed(() => shareActions.value > 0 || sharePreviews.value > 0)
+const resolvingConfigs = ref(false)
+const sharePending = computed(() => resolvingConfigs.value || shareActions.value > 0 || sharePreviews.value > 0)
+const configPicker = ref
>()
const diffModal = ref>()
const previewOpen = ref(false)
@@ -57,12 +60,11 @@ const previewQuery = useQuery({
})
const pushMutation = useMutation({
mutationKey: ['servers', 'share-action', ctx.serverId],
- mutationFn: async (worldId: string) => {
- await client.archon.content_v1.share(ctx.serverId, worldId)
+ mutationFn: async ({ worldId, configPaths }: { worldId: string; configPaths: string[] }) => {
+ await client.archon.content_v1.share(ctx.serverId, worldId, configPaths)
await queryClient.invalidateQueries({ queryKey: ['servers', 'v1', 'detail', ctx.serverId] })
await queryClient.invalidateQueries({ queryKey: ['servers', 'share-diff', ctx.serverId, worldId] })
},
- onError: (error) => handleError(error),
})
async function reviewShareUpdate() {
@@ -80,10 +82,22 @@ async function reviewShareUpdate() {
}
}
-function pushShareUpdate() {
+async function pushShareUpdate() {
if (!ctx.worldId.value || !canSetup.value || sharePending.value || ctx.busyReasons.value.length) return
- previewOpen.value = false
- pushMutation.mutate(ctx.worldId.value)
+ const worldId = ctx.worldId.value
+ const userId = auth.user.value?.id
+ resolvingConfigs.value = true
+ try {
+ const configPaths = await configPicker.value?.resolvePaths() ?? []
+ if (!previewOpen.value || ctx.worldId.value !== worldId || auth.user.value?.id !== userId || !canSetup.value || ctx.busyReasons.value.length) return
+ await pushMutation.mutateAsync({ worldId, configPaths })
+ previewOpen.value = false
+ } catch (error) {
+ handleError(error)
+ if (previewOpen.value && ctx.worldId.value === worldId && auth.user.value?.id === userId) diffModal.value?.show()
+ } finally {
+ resolvingConfigs.value = false
+ }
}
watch([ctx.worldId, () => auth.user.value?.id], () => {
@@ -554,5 +568,9 @@ function onInstallationDismiss() {
:removed-label="formatMessage(messages.removed)"
@confirm="pushShareUpdate"
@cancel="previewOpen = false"
- />
+ >
+
+
+
+
diff --git a/packages/ui/src/composables/hosting-intercom.ts b/packages/ui/src/composables/hosting-intercom.ts
index 6c986399891..ceb8d15a492 100644
--- a/packages/ui/src/composables/hosting-intercom.ts
+++ b/packages/ui/src/composables/hosting-intercom.ts
@@ -45,6 +45,9 @@ const LAUNCHER_SELECTOR =
const RIGHT_VAR = '--modrinth-hosting-intercom-right'
const BOTTOM_VAR = '--modrinth-hosting-intercom-bottom'
const POINTER_EVENTS_VAR = '--modrinth-hosting-intercom-pointer-events'
+const LAUNCHER_OPACITY_VAR = '--modrinth-hosting-intercom-launcher-opacity'
+const LAUNCHER_VISIBILITY_VAR = '--modrinth-hosting-intercom-launcher-visibility'
+const LAUNCHER_POINTER_EVENTS_VAR = '--modrinth-hosting-intercom-launcher-pointer-events'
function sanitizePixels(value: number | undefined, fallback = DEFAULT_PADDING) {
if (typeof value !== 'number' || !Number.isFinite(value)) return fallback
@@ -75,7 +78,12 @@ iframe[name='intercom-messenger-frame'] {
iframe[name='intercom-launcher-frame'] {
right: var(${RIGHT_VAR}, ${DEFAULT_PADDING}px) !important;
bottom: var(${BOTTOM_VAR}, ${DEFAULT_PADDING}px) !important;
+ opacity: var(${LAUNCHER_OPACITY_VAR}, 1) !important;
+ visibility: var(${LAUNCHER_VISIBILITY_VAR}, visible) !important;
+ pointer-events: var(${LAUNCHER_POINTER_EVENTS_VAR}, auto) !important;
transition:
+ opacity 0.15s ease-out,
+ visibility 0.15s,
right 0.12s ease-out,
bottom 0.12s ease-out !important;
}
@@ -95,6 +103,8 @@ export function useHostingIntercom(options: UseHostingIntercomOptions) {
const { stackCount } = useModalStack()
const horizontalPaddingRequests = new Map()
const verticalClearanceRequests = new Map()
+ const hiddenRequests = new Set()
+ const launcherHidden = ref(false)
const requestedHorizontalPadding = ref(null)
const requestedVerticalClearance = ref(null)
const launcherWidth = ref(DEFAULT_LAUNCHER_WIDTH)
@@ -105,7 +115,7 @@ export function useHostingIntercom(options: UseHostingIntercomOptions) {
let syncAfterBoot = false
let stopSync: (() => void) | null = null
let stopPositionSync: (() => void) | null = null
- let stopModalSync: (() => void) | null = null
+ let stopVisibilitySync: (() => void) | null = null
let launcherObserver: ResizeObserver | null = null
let documentObserver: MutationObserver | null = null
let observedLauncher: Element | null = null
@@ -145,6 +155,15 @@ export function useHostingIntercom(options: UseHostingIntercomOptions) {
POINTER_EVENTS_VAR,
stackCount.value > 0 ? 'none' : 'auto',
)
+ document.documentElement.style.setProperty(LAUNCHER_OPACITY_VAR, launcherHidden.value ? '0' : '1')
+ document.documentElement.style.setProperty(
+ LAUNCHER_VISIBILITY_VAR,
+ launcherHidden.value ? 'hidden' : 'visible',
+ )
+ document.documentElement.style.setProperty(
+ LAUNCHER_POINTER_EVENTS_VAR,
+ launcherHidden.value || stackCount.value > 0 ? 'none' : 'auto',
+ )
if (updateSdk && booted) {
updateIntercom({
@@ -180,6 +199,9 @@ export function useHostingIntercom(options: UseHostingIntercomOptions) {
document.documentElement.style.removeProperty(RIGHT_VAR)
document.documentElement.style.removeProperty(BOTTOM_VAR)
document.documentElement.style.removeProperty(POINTER_EVENTS_VAR)
+ document.documentElement.style.removeProperty(LAUNCHER_OPACITY_VAR)
+ document.documentElement.style.removeProperty(LAUNCHER_VISIBILITY_VAR)
+ document.documentElement.style.removeProperty(LAUNCHER_POINTER_EVENTS_VAR)
}
function stop() {
@@ -262,13 +284,13 @@ export function useHostingIntercom(options: UseHostingIntercomOptions) {
immediate: true,
})
stopPositionSync = watch([horizontalPadding, verticalPadding], () => applyPosition(true))
- stopModalSync = watch(stackCount, () => applyPosition())
+ stopVisibilitySync = watch([stackCount, launcherHidden], () => applyPosition())
})
onBeforeUnmount(() => {
stopSync?.()
stopPositionSync?.()
- stopModalSync?.()
+ stopVisibilitySync?.()
launcherObserver?.disconnect()
documentObserver?.disconnect()
stop()
@@ -279,6 +301,11 @@ export function useHostingIntercom(options: UseHostingIntercomOptions) {
intercomBubble: {
width: launcherWidth,
horizontalPadding,
+ requestHidden: (id: symbol, hidden: boolean) => {
+ if (hidden) hiddenRequests.add(id)
+ else hiddenRequests.delete(id)
+ launcherHidden.value = hiddenRequests.size > 0
+ },
requestHorizontalPadding: (id: symbol, value: number | null) =>
requestFromMap(horizontalPaddingRequests, requestedHorizontalPadding, id, value),
requestVerticalClearance: (id: symbol, value: number | null) =>
diff --git a/packages/ui/src/layouts/index.ts b/packages/ui/src/layouts/index.ts
index d828ac37554..64257c0a9de 100644
--- a/packages/ui/src/layouts/index.ts
+++ b/packages/ui/src/layouts/index.ts
@@ -4,6 +4,7 @@ export * from './shared/console'
export * from './shared/content-tab'
export * from './shared/files-tab'
export * from './shared/installation-settings'
+export * from './shared/invited-players'
export * from './shared/server-settings'
export * from './shared/sharing-settings'
export * from './shared/user-profile'
diff --git a/packages/ui/src/layouts/shared/console/layout.vue b/packages/ui/src/layouts/shared/console/layout.vue
index 19c71045092..ed1040ac477 100644
--- a/packages/ui/src/layouts/shared/console/layout.vue
+++ b/packages/ui/src/layouts/shared/console/layout.vue
@@ -1,9 +1,10 @@
-
+
()
+
const ctx = injectConsoleManager()
const client = injectModrinthClient()
const modalBehavior = injectModalBehavior()
diff --git a/packages/ui/src/layouts/shared/invited-players/composables/use-invited-players-table.ts b/packages/ui/src/layouts/shared/invited-players/composables/use-invited-players-table.ts
new file mode 100644
index 00000000000..863cbf903ed
--- /dev/null
+++ b/packages/ui/src/layouts/shared/invited-players/composables/use-invited-players-table.ts
@@ -0,0 +1,77 @@
+import { computed, ref, type Ref, watch } from 'vue'
+
+import type { SortDirection } from '#ui/components/base/Table.vue'
+
+import {
+ invitedPlayerMethodLabels as methodLabels,
+ type InvitedPlayerMethod,
+ type InvitedPlayerRow,
+} from '../types'
+
+type MethodFilter = InvitedPlayerMethod | 'all'
+
+export function useInvitedPlayersTable(
+ rows: Ref
,
+ formatRelativeTime: (date: Date) => string,
+) {
+ const search = ref('')
+ const methodFilter = ref('all')
+ const sortColumn = ref('joined')
+ const sortDirection = ref('desc')
+ const methodFilterOptions: Array<{ id: InvitedPlayerMethod; label: string }> = [
+ { id: 'direct', label: methodLabels.direct },
+ { id: 'link', label: methodLabels.link },
+ ]
+ const hasMultipleMethods = computed(() => new Set(rows.value.map((row) => row.method)).size > 1)
+ const filteredRows = computed(() => {
+ const query = search.value.trim().toLowerCase()
+ return rows.value.filter((row) => {
+ if (methodFilter.value !== 'all' && row.method !== methodFilter.value) return false
+ if (!query) return true
+ return [
+ row.username,
+ row.lastPlayedAt ? formatRelativeTime(row.lastPlayedAt) : 'Never',
+ row.pending ? 'Pending' : row.joinedAt ? formatRelativeTime(row.joinedAt) : '',
+ methodLabels[row.method],
+ ].some((value) => value.toLowerCase().includes(query))
+ })
+ })
+ const sortedRows = computed(() => [...filteredRows.value].sort(compareRows))
+
+ function compareRows(a: InvitedPlayerRow, b: InvitedPlayerRow) {
+ let compared: number
+ if (sortColumn.value === 'username') compared = a.username.localeCompare(b.username)
+ else if (sortColumn.value === 'lastPlayed')
+ compared =
+ (a.lastPlayedAt?.getTime() ?? Number.NEGATIVE_INFINITY) -
+ (b.lastPlayedAt?.getTime() ?? Number.NEGATIVE_INFINITY)
+ else if (sortColumn.value === 'method')
+ compared = methodLabels[a.method].localeCompare(methodLabels[b.method])
+ else
+ compared =
+ (a.pending ? Number.MAX_SAFE_INTEGER : (a.joinedAt?.getTime() ?? Number.NEGATIVE_INFINITY)) -
+ (b.pending
+ ? Number.MAX_SAFE_INTEGER
+ : (b.joinedAt?.getTime() ?? Number.NEGATIVE_INFINITY)) ||
+ a.username.localeCompare(b.username)
+ return sortDirection.value === 'asc' ? compared : -compared
+ }
+ function toggleMethodFilter(filter: InvitedPlayerMethod) {
+ methodFilter.value = methodFilter.value === filter ? 'all' : filter
+ }
+
+ watch(hasMultipleMethods, (multiple) => {
+ if (!multiple) methodFilter.value = 'all'
+ })
+
+ return {
+ search,
+ methodFilter,
+ sortColumn,
+ sortDirection,
+ methodFilterOptions,
+ hasMultipleMethods,
+ sortedRows,
+ toggleMethodFilter,
+ }
+}
diff --git a/packages/ui/src/layouts/shared/invited-players/index.ts b/packages/ui/src/layouts/shared/invited-players/index.ts
new file mode 100644
index 00000000000..e8eadeeefe3
--- /dev/null
+++ b/packages/ui/src/layouts/shared/invited-players/index.ts
@@ -0,0 +1,2 @@
+export { default as InvitedPlayersTableLayout } from './layout.vue'
+export * from './types'
diff --git a/packages/ui/src/layouts/shared/invited-players/layout.vue b/packages/ui/src/layouts/shared/invited-players/layout.vue
new file mode 100644
index 00000000000..f1c23b8a8cf
--- /dev/null
+++ b/packages/ui/src/layouts/shared/invited-players/layout.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+
+
+ All
+
+
+ {{ option.label }}
+
+
+
+
+
+
+
+ {{
+ formatMessage(rows.length === 0 ? messages.noUsersJoined : messages.noUsersMatchFilters)
+ }}
+
+
+
+
+
+
+ {{ row.username }}
+
+
+
+
+ {{
+ formatRelativeTime(row.lastPlayedAt)
+ }}
+ Never
+
+
+ Pending
+ {{
+ formatRelativeTime(row.joinedAt)
+ }}
+
+
+
+
+
+ {{ methodLabels[row.method] }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui/src/layouts/shared/invited-players/types.ts b/packages/ui/src/layouts/shared/invited-players/types.ts
new file mode 100644
index 00000000000..8e559575f6e
--- /dev/null
+++ b/packages/ui/src/layouts/shared/invited-players/types.ts
@@ -0,0 +1,16 @@
+export type InvitedPlayerMethod = 'direct' | 'link'
+
+export type InvitedPlayerRow = {
+ id: string
+ username: string
+ avatarUrl?: string
+ lastPlayedAt: Date | null
+ joinedAt: Date | null
+ method: InvitedPlayerMethod
+ pending?: boolean
+}
+
+export const invitedPlayerMethodLabels: Record = {
+ direct: 'Direct invite',
+ link: 'Share link',
+}
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue
deleted file mode 100644
index 2ba3def2038..00000000000
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/ServerPlayersTable.vue
+++ /dev/null
@@ -1,201 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- No users match your filters.
-
-
-
-
-
-
-
- {{ formatPlayerDate(row.lastPlayedAt) }}
-
- Never
-
-
-
- Pending
-
-
- {{ formatPlayerDate(row.joinedAt) }}
-
-
-
-
-
-
- {{ methodLabel(row.method) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
index 862f46b3b9d..ad58837a112 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
@@ -1,5 +1,5 @@
-
+
{{ formatMessage(messages.retry) }}
-
+
+
+
+
+
+ {{ formatMessage(messages.pushUpdate) }}
+
+
+
+
+
{{ formatMessage(messages.refreshingPreview) }}
{{ formatMessage(messages.retry) }}
@@ -60,20 +77,22 @@
import type { Archon } from '@modrinth/api-client'
import { SpinnerIcon, UploadIcon } from '@modrinth/assets'
import { useIsMutating, useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
-import { useStorage } from '@vueuse/core'
-import { computed, nextTick, ref, watch } from 'vue'
+import { useIntersectionObserver, useStorage } from '@vueuse/core'
+import { computed, nextTick, onScopeDispose, ref, watch } from 'vue'
import Admonition from '#ui/components/base/Admonition.vue'
import { Button } from '#ui/components/base/buttons'
import ConfirmModal from '#ui/components/modal/ConfirmModal.vue'
+import ServerConfigFilePicker from '#ui/components/servers/ServerConfigFilePicker.vue'
import { type InviteLinkSettings, type InvitePlayersInvitePayload, InvitePlayersModal, type InvitePlayersUser } from '#ui/components/sharing'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { useServerPermissions } from '#ui/composables/server-permissions'
import ContentDiffModal from '#ui/layouts/shared/installation-settings/components/ContentDiffModal.vue'
+import InvitedPlayersTableLayout from '#ui/layouts/shared/invited-players/layout.vue'
import { getHostingServerAddress, injectAuth, injectModrinthClient, injectModrinthServerContext, injectNotificationManager, type ServerPlayTarget } from '#ui/providers'
+import { injectPageContext } from '#ui/providers/page-context'
import ServerPlayCard from './ServerPlayCard.vue'
-import ServerPlayersTable from './ServerPlayersTable.vue'
import { resolveServerShareDiff } from './share-diff'
import type { ServerPlayerRow } from './types'
import { useServerPlayers } from './use-server-players'
@@ -85,6 +104,15 @@ const props = defineProps<{
siteUrl: string
}>()
const { formatMessage } = useVIntl()
+const pageContext = injectPageContext(null)
+const pageBottom = ref(null)
+const intercomHiddenRequestId = Symbol('server-play-bottom')
+useIntersectionObserver(pageBottom, ([entry]) => {
+ pageContext?.intercomBubble?.requestHidden?.(intercomHiddenRequestId, entry?.isIntersecting ?? false)
+})
+onScopeDispose(() => {
+ pageContext?.intercomBubble?.requestHidden?.(intercomHiddenRequestId, false)
+})
const { handleError } = injectNotificationManager()
const client = injectModrinthClient()
const auth = injectAuth()
@@ -97,6 +125,8 @@ const needsUpdate = computed(() => world.value?.content?.shared_instance_needs_u
const players = useServerPlayers(sharedInstanceId, canSetup)
const invitePlayersModal = ref>()
const diffModal = ref>()
+const configPicker = ref>()
+const resolvingConfigs = ref(false)
const removeModal = ref>()
const playerToRemove = ref()
const previewOpen = ref(false)
@@ -116,13 +146,13 @@ const previewQuery = useQuery({
})
const actionMutation = useMutation({
mutationKey: ['servers', 'share-action', serverId],
- mutationFn: async ({ action, targetWorldId, userId }: { action: Action; targetWorldId: string; userId: string | undefined }) => {
+ mutationFn: async ({ action, targetWorldId, userId, configPaths }: { action: Action; targetWorldId: string; userId: string | undefined; configPaths: string[] }) => {
if (busyReasons.value.length) throw new Error(formatMessage(messages.busy))
if (auth.user.value?.id !== userId) return
const sameContext = () => worldId.value === targetWorldId && auth.user.value?.id === userId
let id = serverFull.value?.worlds.find((world) => world.id === targetWorldId)?.content?.shared_instance_id
- if (canSetup.value) {
- const shared = await client.archon.content_v1.share(serverId, targetWorldId)
+ if (canSetup.value && (action !== 'play' || !id || needsUpdate.value || configPaths.length > 0)) {
+ const shared = await client.archon.content_v1.share(serverId, targetWorldId, configPaths)
id = shared.shared_instance_id
queryClient.setQueryData(['servers', 'v1', 'detail', serverId], (current) => current ? {
...current,
@@ -130,7 +160,9 @@ const actionMutation = useMutation({
...world, content: { ...world.content, shared_instance_id: shared.shared_instance_id },
} : world),
} : current)
- await queryClient.invalidateQueries({ queryKey: ['servers', 'v1', 'detail', serverId] })
+ const refresh = queryClient.invalidateQueries({ queryKey: ['servers', 'v1', 'detail', serverId] })
+ if (action === 'play') void refresh
+ else await refresh
} else if (action === 'invite' || action === 'push') {
throw new Error(formatMessage(messages.permission))
}
@@ -158,15 +190,27 @@ const actionMutation = useMutation({
})
const pendingAction = computed(() => actionMutation.isPending.value ? actionMutation.variables.value?.action : undefined)
const shareActions = useIsMutating({ mutationKey: ['servers', 'share-action', serverId] })
-const actionsLocked = computed(() => shareActions.value > 0 || previewQuery.isFetching.value || players.linkMutation.isPending.value || busyReasons.value.length > 0)
-function perform(action: Action, reviewed = false) {
+const actionsLocked = computed(() => resolvingConfigs.value || shareActions.value > 0 || previewQuery.isFetching.value || players.linkMutation.isPending.value || busyReasons.value.length > 0)
+async function perform(action: Action, reviewed = false) {
if (!worldId.value || actionsLocked.value) return
if (action === 'play' && !reviewed && canSetup.value && needsUpdate.value && preferences.value.reviewChangesBeforePlaying) {
void showPreview(true)
return
}
- previewOpen.value = false
- actionMutation.mutate({ action, targetWorldId: worldId.value, userId: auth.user.value?.id })
+ const targetWorldId = worldId.value
+ const userId = auth.user.value?.id
+ resolvingConfigs.value = true
+ try {
+ const configPaths = reviewed ? await configPicker.value?.resolvePaths() ?? [] : []
+ if (worldId.value !== targetWorldId || auth.user.value?.id !== userId || (reviewed && !previewOpen.value)) return
+ previewOpen.value = false
+ actionMutation.mutate({ action, targetWorldId, userId, configPaths })
+ } catch (error) {
+ handleError(error)
+ if (previewOpen.value && worldId.value === targetWorldId && auth.user.value?.id === userId) diffModal.value?.show()
+ } finally {
+ resolvingConfigs.value = false
+ }
}
async function showPreview(playAfter = false) {
if (actionsLocked.value) return
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/types.ts b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/types.ts
index 63f2dbb0550..d69cf7cad06 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/types.ts
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/types.ts
@@ -1,11 +1,4 @@
-export type ServerPlayerMethod = 'direct' | 'link'
-
-export type ServerPlayerRow = {
- id: string
- username: string
- avatarUrl?: string
- lastPlayedAt: Date | null
- joinedAt: Date | null
- method: ServerPlayerMethod
- pending?: boolean
-}
+export type {
+ InvitedPlayerMethod as ServerPlayerMethod,
+ InvitedPlayerRow as ServerPlayerRow,
+} from '#ui/layouts/shared/invited-players/types'
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/overview.vue b/packages/ui/src/layouts/wrapped/hosting/manage/overview.vue
index f5608bb195f..95b607e48ce 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/overview.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/overview.vue
@@ -1,13 +1,10 @@
-
@@ -59,11 +56,11 @@ import { injectModrinthClient, injectModrinthServerContext } from '#ui/providers
const props = withDefaults(
defineProps<{
showAdvancedDebugInfo?: boolean
- containedConsole?: boolean
+ fillConsole?: boolean
}>(),
{
showAdvancedDebugInfo: false,
- containedConsole: false,
+ fillConsole: false,
},
)
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
index 148154e264c..2d1a7f2660d 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
@@ -107,8 +107,8 @@
}"
:class="[
'server-panel-' + revealState,
- containedLayout
- ? 'h-full min-h-0 overflow-hidden pb-6'
+ fillLayout
+ ? 'flex-1 pb-6'
: constrainWidth
? 'min-h-[100svh] max-w-[1280px] pb-16'
: 'min-h-[calc(100svh-100px)] pb-6',
@@ -118,7 +118,7 @@
@@ -241,13 +241,12 @@
-
+
@@ -255,7 +254,7 @@
@@ -452,7 +451,7 @@ const props = withDefaults(
type: 'mod' | 'plugin' | 'datapack'
}) => void | Promise
constrainWidth?: boolean
- layoutMode?: 'page' | 'contained'
+ layoutMode?: 'page' | 'fill'
}>(),
{
showCopyIdAction: false,
@@ -506,7 +505,7 @@ const DISABLE_LOADING_ANIM = true
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const constrainWidth = computed(() => props.constrainWidth)
-const containedLayout = computed(() => props.layoutMode === 'contained')
+const fillLayout = computed(() => props.layoutMode === 'fill')
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
const queryClient = useQueryClient()
const route = useRoute()
diff --git a/packages/ui/src/providers/page-context.ts b/packages/ui/src/providers/page-context.ts
index 99585948aaa..b8eb0d444a1 100644
--- a/packages/ui/src/providers/page-context.ts
+++ b/packages/ui/src/providers/page-context.ts
@@ -16,6 +16,7 @@ export interface PageContext {
horizontalPadding: Ref | ComputedRef
requestHorizontalPadding?: (id: symbol, padding: number | null) => void
requestVerticalClearance: (id: symbol, clearance: number | null) => void
+ requestHidden?: (id: symbol, hidden: boolean) => void
}
featureFlags?: {
serverRamAsBytesAlwaysOn?: Ref
From 812b333f1f161db60ebac5c11a232cdad7d07895 Mon Sep 17 00:00:00 2001
From: "Calum H. (IMB11)"
Date: Sun, 20 Sep 2026 14:52:19 +0100
Subject: [PATCH 13/67] fix: qa
---
apps/app-frontend/src/App.vue | 5 +-
.../ui/hosting/HostingPlayHandler.vue | 185 +++++++++---
.../ui/modal/InstallToPlayModal.vue | 9 -
.../instances/use-hosting-instance.ts | 30 +-
.../app-frontend/src/locales/ar-SA/index.json | 63 ----
.../app-frontend/src/locales/cs-CZ/index.json | 33 ---
.../app-frontend/src/locales/de-CH/index.json | 63 ----
.../app-frontend/src/locales/de-DE/index.json | 63 ----
.../app-frontend/src/locales/en-US/index.json | 102 +++----
.../src/locales/es-419/index.json | 63 ----
.../app-frontend/src/locales/es-ES/index.json | 63 ----
.../app-frontend/src/locales/fr-FR/index.json | 63 ----
.../app-frontend/src/locales/he-IL/index.json | 6 -
.../app-frontend/src/locales/hu-HU/index.json | 63 ----
.../app-frontend/src/locales/it-IT/index.json | 63 ----
.../app-frontend/src/locales/ja-JP/index.json | 63 ----
.../app-frontend/src/locales/ko-KR/index.json | 90 ------
.../app-frontend/src/locales/ms-MY/index.json | 3 -
.../app-frontend/src/locales/nl-NL/index.json | 63 ----
.../app-frontend/src/locales/pl-PL/index.json | 90 ------
.../app-frontend/src/locales/pt-BR/index.json | 63 ----
.../app-frontend/src/locales/pt-PT/index.json | 6 -
.../app-frontend/src/locales/ru-RU/index.json | 63 ----
.../app-frontend/src/locales/sv-SE/index.json | 90 ------
.../app-frontend/src/locales/tr-TR/index.json | 63 ----
.../app-frontend/src/locales/uk-UA/index.json | 63 ----
.../app-frontend/src/locales/vi-VN/index.json | 72 -----
.../app-frontend/src/locales/zh-CN/index.json | 63 ----
.../app-frontend/src/locales/zh-TW/index.json | 63 ----
apps/app-frontend/src/pages/Browse.vue | 107 +++++--
.../src/pages/hosting/manage/Index.vue | 22 +-
.../src/pages/hosting/manage/Play.vue | 11 +-
.../instance-page-header-server-metadata.vue | 15 +-
.../src/pages/instance/content/index.vue | 4 +-
.../src/pages/instance/layout.vue | 47 ++-
.../src/pages/instance/share/index.vue | 11 +-
.../share/shared-instance-members-table.vue | 45 +--
.../providers/setup/server-install-content.ts | 76 +++--
.../composables/use-server-install-content.ts | 97 +++---
apps/frontend/src/locales/en-US/index.json | 2 +-
apps/frontend/src/pages/discover.vue | 27 +-
.../src/pages/discover/[type]/index.vue | 20 +-
.../src/pages/hosting/manage/[id]/play.vue | 16 +-
.../src/modules/archon/content/v1.ts | 52 +++-
.../api-client/src/modules/archon/types.ts | 20 +-
.../modules/shared-instances/instances/v1.ts | 24 +-
.../modules/shared-instances/invites/v1.ts | 26 +-
.../ui/src/components/base/inputs/Input.vue | 2 +-
.../components/CustomSetupStage.vue | 5 +-
.../components/SetupTypeStage.vue | 8 +-
.../creation-flow-context.ts | 26 +-
.../creation-flow-modal/server-content.ts | 173 +++++++++++
packages/ui/src/components/modal/NewModal.vue | 4 +-
.../servers/ServerConfigFilePicker.vue | 78 ++++-
.../components/servers/ServerSetupModal.vue | 15 +-
.../admonitions/ServerPanelAdmonitions.vue | 85 +++++-
.../server-header/PanelServerActionButton.vue | 14 +-
.../server-header/use-server-power-action.ts | 10 +-
.../ui/src/composables/hosting-intercom.ts | 5 +-
packages/ui/src/composables/index.ts | 1 +
.../ui/src/composables/server-onboarding.ts | 38 +++
.../ui/src/composables/server-panel-sync.ts | 31 +-
.../ui/src/composables/server-preferences.ts | 14 +
packages/ui/src/composables/virtual-scroll.ts | 3 +-
.../src/layouts/shared/browse-tab/sidebar.vue | 15 +-
.../ui/src/layouts/shared/console/layout.vue | 4 +-
.../components/ContentCardItem.vue | 47 ++-
.../components/ContentCardTable.vue | 61 +++-
.../components/ContentEnabledFor.vue | 24 +-
.../components/managed-content-card/index.vue | 13 +-
.../managed-content-modal/index.vue | 24 +-
.../modals/ConfirmEnvironmentModal.vue | 112 +++++++
.../composables/content-filtering.ts | 17 +-
.../use-content-metadata-filters.ts | 10 +-
.../src/layouts/shared/content-tab/types.ts | 1 +
.../composables/use-invited-players-table.ts | 8 +-
.../layouts/shared/invited-players/layout.vue | 72 ++++-
.../shared/server-settings/pages/general.vue | 73 ++---
.../shared/server-settings/pages/sharing.vue | 4 +-
.../layouts/shared/server-settings/tabs.ts | 2 +-
.../server-sharing/use-server-sharing.ts | 20 +-
.../components/active-invites.vue | 16 +-
.../shared/sharing-settings/layout.vue | 10 +-
.../hosting/manage/[id]/onboarding.vue | 46 +--
.../manage/[id]/play/ServerPlayCard.vue | 35 +--
.../wrapped/hosting/manage/[id]/play/play.vue | 277 ++++++++++++++----
.../hosting/manage/[id]/play/share-diff.ts | 45 ++-
.../manage/[id]/play/use-server-players.ts | 140 ++++++---
.../wrapped/hosting/manage/content.vue | 165 ++++-------
.../wrapped/hosting/manage/overview.vue | 5 +-
.../layouts/wrapped/hosting/manage/root.vue | 105 ++-----
packages/ui/src/layouts/wrapped/index.ts | 2 +-
packages/ui/src/locales/cs-CZ/index.json | 3 -
packages/ui/src/locales/de-CH/index.json | 9 -
packages/ui/src/locales/de-DE/index.json | 9 -
packages/ui/src/locales/en-US/index.json | 256 ++++++++++++++--
packages/ui/src/locales/es-419/index.json | 9 -
packages/ui/src/locales/es-ES/index.json | 9 -
packages/ui/src/locales/fr-FR/index.json | 9 -
packages/ui/src/locales/hu-HU/index.json | 9 -
packages/ui/src/locales/it-IT/index.json | 9 -
packages/ui/src/locales/ja-JP/index.json | 9 -
packages/ui/src/locales/ko-KR/index.json | 9 -
packages/ui/src/locales/ms-MY/index.json | 9 -
packages/ui/src/locales/nl-NL/index.json | 9 -
packages/ui/src/locales/pl-PL/index.json | 9 -
packages/ui/src/locales/pt-BR/index.json | 9 -
packages/ui/src/locales/ru-RU/index.json | 9 -
packages/ui/src/locales/sr-CS/index.json | 9 -
packages/ui/src/locales/sv-SE/index.json | 9 -
packages/ui/src/locales/tr-TR/index.json | 9 -
packages/ui/src/locales/uk-UA/index.json | 9 -
packages/ui/src/locales/zh-CN/index.json | 9 -
packages/ui/src/locales/zh-TW/index.json | 9 -
packages/ui/src/providers/index.ts | 2 +-
packages/ui/src/utils/search.ts | 18 ++
116 files changed, 2211 insertions(+), 2464 deletions(-)
create mode 100644 packages/ui/src/components/flows/creation-flow-modal/server-content.ts
create mode 100644 packages/ui/src/composables/server-onboarding.ts
create mode 100644 packages/ui/src/composables/server-preferences.ts
create mode 100644 packages/ui/src/layouts/shared/content-tab/components/modals/ConfirmEnvironmentModal.vue
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index cbf1b6f9379..3b5a2abf2a6 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -40,6 +40,7 @@ import {
provideNotificationManager,
providePageContext,
providePopupNotificationManager,
+ provideServerPlay,
TeleportOverflowMenu,
TooltipDirective,
useDebugLogger,
@@ -65,6 +66,7 @@ import { useRouteLoading } from '@/app/runtime/use-route-loading'
import AppSidebar from '@/app/shell/sidebar/index.vue'
import AppTitleBar from '@/app/shell/title-bar/index.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
+import HostingPlayHandler from '@/components/ui/hosting/HostingPlayHandler.vue'
import HostingUpdateRequired from '@/components/ui/HostingUpdateRequired.vue'
import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue'
import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
@@ -77,9 +79,6 @@ import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountR
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavButton from '@/components/ui/NavButton.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
-import HostingPlayHandler from '@/components/ui/hosting/HostingPlayHandler.vue'
-import { provideServerPlay } from '@modrinth/ui'
-
import SharedInstanceInviteHandler from '@/components/ui/shared-instances/shared-instance-invite-handler/index.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import SurveyPopup from '@/components/ui/SurveyPopup.vue'
diff --git a/apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue b/apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue
index 17498a9531f..a8f55a00919 100644
--- a/apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue
+++ b/apps/app-frontend/src/components/ui/hosting/HostingPlayHandler.vue
@@ -17,17 +17,39 @@
diff --git a/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue b/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue
index 068e33338be..19c8fd955a3 100644
--- a/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue
+++ b/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue
@@ -7,10 +7,6 @@
width="544px"
>
-
- {{ formatMessage(messages.inviteWarning) }}
-
-
@@ -401,11 +397,6 @@ const messages = defineMessages({
id: 'app.modal.install-to-play.header',
defaultMessage: 'Install to play',
},
- inviteWarning: {
- id: 'app.modal.install-to-play.invite-warning',
- defaultMessage:
- 'This invite was created by another Modrinth user, not Modrinth. Only accept invites from people you trust.',
- },
sharedInstance: {
id: 'app.modal.install-to-play.shared-instance',
defaultMessage: 'Shared instance',
diff --git a/apps/app-frontend/src/composables/instances/use-hosting-instance.ts b/apps/app-frontend/src/composables/instances/use-hosting-instance.ts
index 516b06c3c0d..8624a64dc6f 100644
--- a/apps/app-frontend/src/composables/instances/use-hosting-instance.ts
+++ b/apps/app-frontend/src/composables/instances/use-hosting-instance.ts
@@ -27,9 +27,10 @@ export function hostingInstanceMetadata(
serverId: server.id,
worldId,
address,
- region: location.status === 'assigned' && location.location_metadata.region_should_be_user_displayed
- ? location.location_metadata.region
- : undefined,
+ region:
+ location.status === 'assigned' && location.location_metadata.region_should_be_user_displayed
+ ? location.location_metadata.region
+ : undefined,
}
}
@@ -48,7 +49,13 @@ export function useHostingInstance(instance: Ref, offl
})
const serverQuery = useQuery({
queryKey: computed(() => ['instances', instance.value?.id, 'hosting', auth.user.value?.id]),
- enabled: computed(() => !!instance.value?.shared_instance && !offline.value && !!auth.user.value?.id && auth.user.value.id === instance.value.shared_instance.linked_user_id),
+ enabled: computed(
+ () =>
+ !!instance.value?.shared_instance &&
+ !offline.value &&
+ !!auth.user.value?.id &&
+ auth.user.value.id === instance.value.shared_instance.linked_user_id,
+ ),
queryFn: async () => {
const current = instance.value!
const sharedId = current.shared_instance!.id
@@ -71,16 +78,23 @@ export function useHostingInstance(instance: Ref, offl
watch(serverQuery.data, (result) => {
if (result) cache.value[result.instanceId] = result.metadata
})
- const isHostingInstance = computed(() => !!saved.value || !!instance.value?.shared_instance?.server_manager_name)
+ const isHostingInstance = computed(
+ () => !!saved.value || !!instance.value?.shared_instance?.server_manager_name,
+ )
const worldsQuery = useQuery({
queryKey: computed(() => ['instances', instance.value?.id, 'hosting-worlds']),
- enabled: computed(() => isHostingInstance.value && !saved.value && instance.value?.install_stage === 'installed'),
+ enabled: computed(
+ () =>
+ isHostingInstance.value && !saved.value && instance.value?.install_stage === 'installed',
+ ),
queryFn: () => get_instance_worlds(instance.value!.id),
})
const address = computed(() => {
if (saved.value?.address) return saved.value.address
- const world = worldsQuery.data.value?.find((world) =>
- world.type === 'server' && world.name === instance.value?.shared_instance?.server_manager_name,
+ const world = worldsQuery.data.value?.find(
+ (world) =>
+ world.type === 'server' &&
+ world.name === instance.value?.shared_instance?.server_manager_name,
)
return world?.type === 'server' ? world.address : undefined
})
diff --git a/apps/app-frontend/src/locales/ar-SA/index.json b/apps/app-frontend/src/locales/ar-SA/index.json
index 5948dd4859b..062dbd2911b 100644
--- a/apps/app-frontend/src/locales/ar-SA/index.json
+++ b/apps/app-frontend/src/locales/ar-SA/index.json
@@ -680,12 +680,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "حساب خاطئ"
},
- "app.instance.share.members.empty": {
- "message": "لم ينضم أي مستخدمين حتى الآن"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "لا يوجد مستخدمون يطابقون الفلاتر الخاصة بك."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "لن يتلقوا أي تحديثات لهذه النسخة المشتركة بعد الآن."
},
@@ -2201,9 +2195,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "النسخة المشتركة المرتبطة"
},
- "installation-settings.shared-instance.title": {
- "message": "إلغاء نشر النسخة"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "إلغاء ربط النسخة المشتركة"
},
@@ -2213,15 +2204,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "جاري إلغاء الربط..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "إلغاء نشر النسخة المُشاركة"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "إزالة هذه النسخة المُشاركة من Modrinth والتوقف عن إرسال التحديثات لأي شخص يستخدمها. لن تتأثر نسختك المحلية."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "جاري إلغاء النشر..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "هذا يؤثر فقط على نسختك المحلية. سيبقى المحتوى المثبت على هذا الجهاز، ولن تتأثر النسخة المُشاركة أو الأشخاص الآخرون الذين يستخدمونها."
},
@@ -2234,12 +2219,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "سيؤدي هذا إلى حذف النسخة المُشاركة من خوادم Modrinth. سيتوقف الأشخاص الذين يستخدمونها على تطبيق Modrinth عن تلقي التحديثات، ولكن ستظل نسختك المحلية ومحتواها على هذا الجهاز."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "جاري إلغاء نشر النسخة المُشاركة"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "إلغاء نشر النسخة المُشاركة"
- },
"instance.action.create-shortcut": {
"message": "إنشاء اختصار"
},
@@ -2546,45 +2525,6 @@
"instance.server-modal.resource-pack": {
"message": "حزمة الموارد"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "الإجراءات"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "رابط الدعوة"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "يمكن لأي شخص لديه أحد روابط الدعوة هذه الانضمام طالما ظلت نشطة."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "لا توجد أي دعوات نشطة."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "تنتهي الصلاحية في"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "إلغاء الدعوة"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "إلغاء الدعوة {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "الدعوات النشطة"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "الاستخدامات"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "سيتوقف رابط الدعوة {code} عن العمل فورًا. وسيظل الأشخاص الذين انضموا فعلًا قادرين على الوصول."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "لا يمكن التراجع عن هذا الإجراء"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "إلغاء الدعوة"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "إلغاء الدعوة"
- },
"instance.settings.tabs.behavior.description": {
"message": "اختر علامات التبويب الذي يظهر في هذه النسخة."
},
@@ -2876,9 +2816,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "تم إلغاء وصولك إلى النسخة المشتركة. هذه النسخة لا تزال متاحة، ولكنها لم تعد مرتبطة ولن تتلقى التحديثات بعد الآن."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "تم حذف النسخة الرئيسية. هذه النسخة لا تزال متاحة، ولكنها لم تعد مرتبطة ولن تتلقى التحديثات بعد الآن."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "تم قفل هذه النسخة المُشارَكة بواسطة فريق الإشراف على المحتوى. لن تتلقى هذه النسخة أي تحديثات أخرى من النسخة الرئيسية ولا يمكن لعبها."
},
diff --git a/apps/app-frontend/src/locales/cs-CZ/index.json b/apps/app-frontend/src/locales/cs-CZ/index.json
index 613b881b33b..d8720462b91 100644
--- a/apps/app-frontend/src/locales/cs-CZ/index.json
+++ b/apps/app-frontend/src/locales/cs-CZ/index.json
@@ -710,12 +710,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Špatný účet"
},
- "app.instance.share.members.empty": {
- "message": "Zatím se nepřipojili žádní uživatelé"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Žádný uživatelé odpovídají vašemu filtru."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Pro tuto sdílenou instanci již nebudou dostávat aktualizace"
},
@@ -2270,33 +2264,6 @@
"instance.server-modal.resource-pack": {
"message": "Balíček textur"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Akce"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Odkaz s pozvánkou"
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Vyprší"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Zrušit pozvánku"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Zrušit pozvánku pro {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Aktivní pozvánky"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Využití"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Zrušit pozvánku"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Zrušit pozvánku"
- },
"instance.settings.tabs.general": {
"message": "Obecné"
},
diff --git a/apps/app-frontend/src/locales/de-CH/index.json b/apps/app-frontend/src/locales/de-CH/index.json
index 42b3fd4a704..99a3dde3806 100644
--- a/apps/app-frontend/src/locales/de-CH/index.json
+++ b/apps/app-frontend/src/locales/de-CH/index.json
@@ -773,12 +773,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Falsches Konto"
},
- "app.instance.share.members.empty": {
- "message": "Es sind noch keine Benutzer beigetreten"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Keine Benutzer passen zu deinen Filtern."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Diese Person wird für diese geteilte Instanz keine Updates mehr erhalten"
},
@@ -3611,9 +3605,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Verknüpfte geteilte Instanz"
},
- "installation-settings.shared-instance.title": {
- "message": "Instanz zurückziehen"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Geteilte Instanz trennen"
},
@@ -3623,15 +3614,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Wird getrennt..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Geteilte Instanz zurückziehen"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Entferne diese geteilte Instanz von Modrinth und sende keine Updates mehr an Personen, die sie nutzen. Deine lokale Instanz ist davon nicht betroffen."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Wird zurückgezogen..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Dies betrifft nur deine lokale Instanz. Deine installierten Inhalte bleiben auf diesem Gerät, und die freigegebene Instanz sowie andere Benutzer, die diese verwenden, sind davon nicht betroffen."
},
@@ -3644,12 +3629,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Dadurch wird die geteilte Instanz von den Servern von Modrinth gelöscht. Personen, die diese in der Modrinth-App nutzen, erhalten keine Updates mehr, aber deine lokale Instanz und dessen Inhalte bleiben auf diesem Gerät erhalten."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Geteilte Instanz zurückziehen"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Geteilte Instanz zurückziehen"
- },
"instance.action.create-shortcut": {
"message": "Verknüpfung erstellen"
},
@@ -3959,45 +3938,6 @@
"instance.server-modal.resource-pack": {
"message": "Ressourcenpaket"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Aktionen"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Einladungslink"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Jeder, der einen dieser Einladungslinks hat, kann beitreten, solange der Link aktiv ist."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Es gibt keine aktiven Einladungen."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Läuft ab"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Einladung zurückziehen"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Einladung {code} zurückziehen"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Aktive Einladungen"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Nutzungen"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "Der Einladungslink {code} wird sofort aufhören zu funktionieren. Nutzer, die bereits beigetreten sind, bleibt der Zugang gewährt."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Diese Aktion kann nicht rückgängig gemacht werden"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Einladung zurückziehen"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Einladung zurückziehen"
- },
"instance.settings.synced-options.data-override": {
"message": "Deaktiviere diese Einstellung, um synchronisierte Daten bearbeiten zu können."
},
@@ -4325,9 +4265,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Dein Zugriff auf die geteilte Instanz wurde widerrufen. Diese Instanz ist weiterhin verfügbar, ist jedoch nicht mehr verknüpft und erhält keine Updates mehr."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "Die primäre Instanz wurde gelöscht. Diese Instanz ist weiterhin verfügbar, ist jedoch nicht mehr verknüpft und erhält keine Updates mehr."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Diese geteilte Instanz wurde vom Inhaltsmoderationsteam gesperrt. Sie erhält keine Updates von der primären Instanz mehr und kann nicht mehr gespielt werden."
},
diff --git a/apps/app-frontend/src/locales/de-DE/index.json b/apps/app-frontend/src/locales/de-DE/index.json
index b9397171092..0c48725a9e6 100644
--- a/apps/app-frontend/src/locales/de-DE/index.json
+++ b/apps/app-frontend/src/locales/de-DE/index.json
@@ -773,12 +773,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Falsches Konto"
},
- "app.instance.share.members.empty": {
- "message": "Es sind noch keine Benutzer beigetreten"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Keine Benutzer passen zu deinen Filtern."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Diese Person wird für diese geteilte Instanz keine Updates mehr erhalten"
},
@@ -3611,9 +3605,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Verknüpfte geteilte Instanz"
},
- "installation-settings.shared-instance.title": {
- "message": "Instanz zurückziehen"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Geteilte Instanz trennen"
},
@@ -3623,15 +3614,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Wird getrennt..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Geteilte Instanz zurückziehen"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Entferne diese geteilte Instanz von Modrinth und sende keine Updates mehr an Personen, die sie nutzen. Deine lokale Instanz ist davon nicht betroffen."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Wird zurückgezogen..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Dies betrifft nur deine lokale Instanz. Deine installierten Inhalte bleiben auf diesem Gerät, und die freigegebene Instanz sowie andere Benutzer, die diese verwenden, sind davon nicht betroffen."
},
@@ -3644,12 +3629,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Dadurch wird die geteilte Instanz von den Servern von Modrinth gelöscht. Personen, die diese in der Modrinth-App nutzen, erhalten keine Updates mehr, aber deine lokale Instanz und dessen Inhalte bleiben auf diesem Gerät erhalten."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Geteilte Instanz zurückziehen"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Geteilte Instanz zurückziehen"
- },
"instance.action.create-shortcut": {
"message": "Verknüpfung erstellen"
},
@@ -3959,45 +3938,6 @@
"instance.server-modal.resource-pack": {
"message": "Ressourcenpaket"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Aktionen"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Einladungslink"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Jeder, der einen dieser Einladungslinks hat, kann beitreten, solange der Link aktiv ist."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Es gibt keine aktiven Einladungen."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Läuft ab"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Einladung zurückziehen"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Einladung {code} zurückziehen"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Aktive Einladungen"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Nutzungen"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "Der Einladungslink {code} wird sofort aufhören zu funktionieren. Nutzer, die bereits beigetreten sind, bleibt der Zugang gewährt."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Diese Aktion kann nicht rückgängig gemacht werden"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Einladung zurückziehen"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Einladung zurückziehen"
- },
"instance.settings.synced-options.data-override": {
"message": "Deaktiviere diese Einstellung, um synchronisierte Daten bearbeiten zu können."
},
@@ -4325,9 +4265,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Dein Zugriff auf die geteilte Instanz wurde widerrufen. Diese Instanz ist weiterhin verfügbar, ist jedoch nicht mehr verknüpft und erhält keine Updates mehr."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "Die primäre Instanz wurde gelöscht. Diese Instanz ist weiterhin verfügbar, ist jedoch nicht mehr verknüpft und erhält keine Updates mehr."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Diese geteilte Instanz wurde vom Inhaltsmoderationsteam gesperrt. Sie erhält keine Updates von der primären Instanz mehr und kann nicht mehr gespielt werden."
},
diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json
index 2db540e8e20..93fa301b802 100644
--- a/apps/app-frontend/src/locales/en-US/index.json
+++ b/apps/app-frontend/src/locales/en-US/index.json
@@ -269,6 +269,9 @@
"app.browse.project-type.mods": {
"message": "Mods"
},
+ "app.browse.project-type.plugins": {
+ "message": "Plugins"
+ },
"app.browse.project-type.resource-packs": {
"message": "Resource Packs"
},
@@ -782,12 +785,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Wrong account"
},
- "app.instance.share.members.empty": {
- "message": "No users have joined yet"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "No users match your filters."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "They will no longer receive updates for this shared instance"
},
@@ -3653,12 +3650,27 @@
"friends.sign-in-to-add-friends": {
"message": "Sign in to a Modrinth account to add friends and see what they're playing!"
},
+ "hosting.play.account-changed": {
+ "message": "Your Modrinth account changed. Press Play server again to continue."
+ },
+ "hosting.play.no-address": {
+ "message": "This server does not have a connection address yet."
+ },
+ "hosting.play.not-ready": {
+ "message": "This instance is not available to launch. Check its installation status in your library."
+ },
+ "hosting.play.update-description": {
+ "message": "Update this instance to the server’s latest shared content before joining."
+ },
+ "hosting.play.update-to-play": {
+ "message": "Update to play"
+ },
+ "hosting.play.world-changed": {
+ "message": "This world is no longer active or has not been shared. Open the server panel and press Play server again."
+ },
"installation-settings.shared-instance.linked-title": {
"message": "Linked shared instance"
},
- "installation-settings.shared-instance.title": {
- "message": "Unpublish instance"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Unlink shared instance"
},
@@ -3668,15 +3680,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Unlinking..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Unpublish shared instance"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Remove this shared instance from Modrinth and stop sending updates to anyone using it. Your local instance will not be affected."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Unpublishing..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "This only affects your local instance. Your installed content will stay on this device, and the shared instance and other people using it will not be affected."
},
@@ -3689,12 +3695,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "This deletes the shared instance from Modrinth's servers. People using it in the Modrinth App will stop receiving updates, but your local instance and its content will stay on this device."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Unpublishing shared instance"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Unpublish shared instance"
- },
"instance.action.create-shortcut": {
"message": "Create shortcut"
},
@@ -4007,45 +4007,6 @@
"instance.settings.defaults.open-app-settings": {
"message": "Manage default settings"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Actions"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Invite link"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Anyone with one of these invite links can join while it remains active."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "There are no active invites."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Expires"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Revoke invite"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Revoke invite {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Active invites"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Uses"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "The invite link {code} will stop working immediately. People who already joined will keep access."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "This action cannot be undone"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Revoke invite"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Revoke invite"
- },
"instance.settings.sidebar.label.game": {
"message": "Game"
},
@@ -4400,9 +4361,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Your access to the shared instance was revoked. This instance is still available, but it is no longer linked and will no longer receive updates."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "The primary instance was deleted. This instance is still available, but it is no longer linked and will no longer receive updates."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "This shared instance was locked by the Content Moderation team. It will no longer receive updates from the primary instance and cannot be played."
},
@@ -4412,6 +4370,12 @@
"instance.shared-instance.unavailable.manager-fallback": {
"message": "the instance manager"
},
+ "instance.shared-instance.unavailable.not-found-text": {
+ "message": "We couldn’t find this shared instance. Your installed content is still on this device."
+ },
+ "instance.shared-instance.unavailable.not-found-title": {
+ "message": "Shared instance unavailable"
+ },
"instance.shared-instance.unavailable.text": {
"message": "Your local instance is still available, but it is no longer linked and won't receive updates."
},
@@ -4595,9 +4559,21 @@
"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"
+ },
+ "search.filter.locked.server.sync": {
+ "message": "Sync with server"
+ },
"settings.sidebar.label.account": {
"message": "Account"
},
diff --git a/apps/app-frontend/src/locales/es-419/index.json b/apps/app-frontend/src/locales/es-419/index.json
index e9ca0902173..bc8598fd97a 100644
--- a/apps/app-frontend/src/locales/es-419/index.json
+++ b/apps/app-frontend/src/locales/es-419/index.json
@@ -773,12 +773,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Cuenta equivocada"
},
- "app.instance.share.members.empty": {
- "message": "Nadie se ha unido todavía"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Ningún usuario coincide con tus filtros."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Ya no recibirán actualizaciones para esta instancia"
},
@@ -3611,9 +3605,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Vincular instancia compartida"
},
- "installation-settings.shared-instance.title": {
- "message": "Despublicar instancia"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Desvincular instancia compartida"
},
@@ -3623,15 +3614,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Desvinculando..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Despublicar instancia compartida"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Borra esta instancia compartida de Modrinth y deja de enviar actualizaciones a cualquiera que la tenga. Tu instancia local no se verá afectada."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Despublicando..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Esto solo afecta a tu instancia local. Tu contenido instalado se quedará en este dispositivo, y la instancia compartida, junto a todos los que la estén usando, no se verán afectados."
},
@@ -3644,12 +3629,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Esto borrará la instancia compartida de los servidores de Modrinth. La gente que esté usando esta instancia dejará de recibir actualizaciones, pero tu instancia local y sus contenidos se quedarán en este dispositivo."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Despublicando instancia compartida"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Despublicar instancia compartida"
- },
"instance.action.create-shortcut": {
"message": "Crear acceso directo"
},
@@ -3959,45 +3938,6 @@
"instance.server-modal.resource-pack": {
"message": "Paquete de recursos"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Acciones"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Link de invitación"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Cualquiera con alguno de estos links se puede unir mientras el link esté activo."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "No hay invitaciones activas."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Expira"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Revocar invitación"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Revocar invitación {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Invitaciones activas"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Usos"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "El link de invitación {code} dejará de funcionar inmediatamente. Cualquiera que ya se haya unido mantendrá su acceso."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Esta acción no se puede deshacer"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Revocar invitación"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Revocar invitación"
- },
"instance.settings.synced-options.data-override": {
"message": "Desactivar este sobreescrito para editar los datos sincronizados."
},
@@ -4325,9 +4265,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Tu acceso a la instancia compartida fue revocado. Esta instancia seguirá disponible, pero ya no está vinculada, y no recibirás más actualizaciones."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "La instancia principal fue borrada. Esta instancia seguirá disponible, pero ya no está vinculada, y no recibirás más actualizaciones."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Esta instancia compartida fue bloqueada por el equipo de Moderación de Contenido. Ya no recibirá más actualizaciones de la instancia principal y tampoco se puede jugar."
},
diff --git a/apps/app-frontend/src/locales/es-ES/index.json b/apps/app-frontend/src/locales/es-ES/index.json
index dc862a5d1af..fa8b21cde15 100644
--- a/apps/app-frontend/src/locales/es-ES/index.json
+++ b/apps/app-frontend/src/locales/es-ES/index.json
@@ -761,12 +761,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Cuenta incorrecta"
},
- "app.instance.share.members.empty": {
- "message": "Ningún usuario se ha unido aún"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "No se han encontrado usuarios que coincidan con tus filtros."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Ya no recibirán actualizaciones para esta instancia compartida"
},
@@ -3581,9 +3575,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Instancia compartida enlazada"
},
- "installation-settings.shared-instance.title": {
- "message": "Dejar de compartir instancia"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Dejar de compartir instancia"
},
@@ -3593,15 +3584,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Desvinculando..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Dejar de compartir instancia"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Elimina esta instancia compartida de Modrinth y deja de enviar actualizaciones a cualquiera que la use. Tu instancia local no se verá afectada."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Dejando de compartir..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Esto solo afecta a tu instancia local. El contenido instalado permanecerá en este dispositivo y la instancia compartida y otras personas que lo usen no se verán afectadas."
},
@@ -3614,12 +3599,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Esto elimina la instancia compartida de los servidores de Modrinth. La gente que lo usa en la aplicación Modrinth dejará de recibir actualizaciones, pero tu instancia local y su contenido permanecerán en este dispositivo."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Dejar de compartir instancia"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Dejar de compartir instancia"
- },
"instance.action.create-shortcut": {
"message": "Crear acceso directo"
},
@@ -3929,45 +3908,6 @@
"instance.server-modal.resource-pack": {
"message": "Paquete de recursos"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Acciones"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Enlace de invitación"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Cualquiera con uno de estos enlaces de invitación puede unirse mientras permanece activo."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "No hay invitaciones activas."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Caduca"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Retirar invitación"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Retirar invitación {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Invitaciones activas"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Usos"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "El enlace de invitación {code} dejará de funcionar inmediatamente. Las personas que ya se unieron seguirán teniendo acceso."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Esta acción no se puede deshacer"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Revocar invitación"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Revocar invitación"
- },
"instance.settings.synced-options.data-override": {
"message": "Desactivar este sobreescrito para editar los datos sincronizados."
},
@@ -4295,9 +4235,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Se revocó tu acceso a la instancia compartida. Esta instancia todavía está disponible, pero ya no está vinculada y ya no recibirá actualizaciones."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "La instancia principal fue eliminada. Esta instancia todavía está disponible, pero ya no está vinculada y ya no recibirá actualizaciones."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Esta instancia compartida ha sido bloqueada por el equipo de Moderación de Contenido. Ya no recibirá actualizaciones de la instancia principal y no se podrá usar."
},
diff --git a/apps/app-frontend/src/locales/fr-FR/index.json b/apps/app-frontend/src/locales/fr-FR/index.json
index b903764760b..9d9efb1ff6d 100644
--- a/apps/app-frontend/src/locales/fr-FR/index.json
+++ b/apps/app-frontend/src/locales/fr-FR/index.json
@@ -773,12 +773,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Mauvais compte"
},
- "app.instance.share.members.empty": {
- "message": "Aucun utilisateur n'a encore rejoint"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Aucun utilisateur ne correspond à vos filtres."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Ils vont désormais cesser de recevoir des mises à jour pour cette instance partagée"
},
@@ -3590,9 +3584,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Instance partagée liée"
},
- "installation-settings.shared-instance.title": {
- "message": "Dépublier l'instance"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Dissocier l'instance partagée"
},
@@ -3602,15 +3593,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Dissociation..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Dépublier l'instance partagée"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Supprime cette instance partagée de Modrinth et arrête d'envoyer des mises à jour à toute personne l'utilisant. Votre copie locale ne sera pas affectée."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Dépublication..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Cela affecte seulement votre instance locale. Votre contenu installé restera sur cet appareil, et l'instance partagée et les autres personnes l'utilisant ne seront pas affectés."
},
@@ -3623,12 +3608,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Cela supprime l'instance partagée des serveurs de Modrinth. Les personnes l'utilisant dans Modrinth App vont cesser de recevoir des mises à jour, mais votre instance locale et son contenu resteront sur cet appareil."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Dépublication de l'instance partagée"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Dépublier l'instance partagée"
- },
"instance.action.create-shortcut": {
"message": "Créer un raccourci"
},
@@ -3935,45 +3914,6 @@
"instance.server-modal.resource-pack": {
"message": "Pack de ressources"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Actions"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Lien d'invitation"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "N'importe qui avec l'un de ces liens d'invitation peut rejoindre pendant qu'il reste actif."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Il n'y a aucune invitation active."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Expire"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Révoquer l'invitation"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Révoquer l'invitation {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Invitations actives"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Utilisations"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "Le lien d'invitation {code} cessera de fonctionner immédiatement. Les personnes qui ont déjà rejoint conserveront leur accès."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Cette action ne peut pas être annulée"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Révoquer l'invitation"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Révoquer l'invitation"
- },
"instance.settings.synced-options.data-packs": {
"message": "Désynchroniser les packs de données"
},
@@ -4262,9 +4202,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Votre accès à l'instance partagée a été révoqué. Cette instance reste disponible, mais n'est plus liée et ne recevra plus de mises à jour."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "L'instance principale a été supprimée. Cette instance est encore disponible, mais n'est plus liée et va cesser de recevoir des mises à jour."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Cette instance partagée a été verrouillée par la Team de Modération de Contenu. Elle ne recevra plus de mises à jour de l'instance primaire et ne peut plus être jouée."
},
diff --git a/apps/app-frontend/src/locales/he-IL/index.json b/apps/app-frontend/src/locales/he-IL/index.json
index 1416acf045d..2c3c8e85ad4 100644
--- a/apps/app-frontend/src/locales/he-IL/index.json
+++ b/apps/app-frontend/src/locales/he-IL/index.json
@@ -677,12 +677,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "משתמש לא נכון"
},
- "app.instance.share.members.empty": {
- "message": "עדיין שום משתמש נכנס"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "שום משתמש לא תואם לסינון שלך."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "ההתקנה המשותפת הזו לא תקבל יותר עדכונים"
},
diff --git a/apps/app-frontend/src/locales/hu-HU/index.json b/apps/app-frontend/src/locales/hu-HU/index.json
index ec464c1f7ed..69d68e5a1cd 100644
--- a/apps/app-frontend/src/locales/hu-HU/index.json
+++ b/apps/app-frontend/src/locales/hu-HU/index.json
@@ -761,12 +761,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Helytelen fiók"
},
- "app.instance.share.members.empty": {
- "message": "Még senki sem csatlakozott"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Nem található a keresésnek megfelelő felhasználó"
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Többé nem fog frissítéseket kapni ehhez a megosztott példányhoz"
},
@@ -3590,9 +3584,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Összekapcsolt megosztott példány"
},
- "installation-settings.shared-instance.title": {
- "message": "Példány megosztásának visszavonása"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Megosztott példány leválasztása"
},
@@ -3602,15 +3593,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Leválasztás..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Pédány megosztásának visszavonása"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Eltávolítja ezt a megosztott példányt a Modrinthból, és nem küld többé frissítéseket azoknak, akik használják. A helyi példányodat ez nem érinti."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Közzététel visszavonása..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Ez kizárólag a helyi példányodra vonatkozik. A telepített tartalmaid ezen az eszközön maradnak, és ez nem érinti a megosztott példányt, illetve az azt használó többi felhasználót."
},
@@ -3623,12 +3608,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Ezzel a megosztott példány törlődik a Modrinth szervereiről. A Modrinth Appban az ezt használók többé nem kapnak frissítéseket, de a helyipéldány és annak tartalma továbbra is ezen az eszközön marad."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Visszavonod a megosztott példány megosztását"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Megosztott példány megosztásának visszavonása"
- },
"instance.action.create-shortcut": {
"message": "Parancsikon létrehozása"
},
@@ -3938,45 +3917,6 @@
"instance.server-modal.resource-pack": {
"message": "Forráscsomag"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Műveletek"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Meghívó link"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Bárki, akinek van ilyen meghívó linkje, csatlakozhat, amíg az aktív marad."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Nincsenek aktív meghívások."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Lejár"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Meghívó törlése"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "A(z) {code} meghívó törlése"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Aktív meghívások"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Használatok"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "A(z) {code} meghívó link azonnal érvényét veszti. Azok, akik már csatlakoztak, továbbra is hozzáférhetnek."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Ezt a műveletet nem lehet visszavonni"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Meghívó törlése"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Meghívó törlése"
- },
"instance.settings.synced-options.data-override": {
"message": "Kapcsold ki ezt a felülírást hogy szerkeszthető legyen a szinkronizált adat."
},
@@ -4304,9 +4244,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "A megosztott példányhoz való hozzáférésedet megvonták. Ez a példány továbbra is elérhető, de már nincs összekapcsolva, és többé nem kap frissítéseket."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "Az elsődleges példányt törölték. Ez a példány továbbra is elérhető, de már nincs összekapcsolva, és többé nem kap frissítéseket."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Ezt a megosztott példányt a Tartalommoderációs csapat zárolta. Többé nem kap frissítéseket az elsődleges példánytól, és nem lehet játszani vele."
},
diff --git a/apps/app-frontend/src/locales/it-IT/index.json b/apps/app-frontend/src/locales/it-IT/index.json
index d46609e21e8..7c3db76efbf 100644
--- a/apps/app-frontend/src/locales/it-IT/index.json
+++ b/apps/app-frontend/src/locales/it-IT/index.json
@@ -773,12 +773,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Account errato"
},
- "app.instance.share.members.empty": {
- "message": "Ancora nessun partecipante"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Nessun utente trovato."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Non riceveranno più aggiornamenti"
},
@@ -3611,9 +3605,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Istanza condivisa collegata"
},
- "installation-settings.shared-instance.title": {
- "message": "Rimuovi l'istanza dai server"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Scollega l'istanza condivisa"
},
@@ -3623,15 +3614,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Scollegamento..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Rimuovi l'istanza condivisa dai server"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Rimuovi l'istanza condivisa da Modrinth e non inviare più aggiornamenti a chi la usa. L'istanza locale non sarà toccata."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Rimozione..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Ciò influirà solo sull'istanza locale. I contenuti installati rimarranno sul dispositivo e l'istanza condivisa e gli utenti che la usano non verranno toccati."
},
@@ -3644,12 +3629,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Ciò eliminerà l'istanza condivisa dai server di Modrinth. Gli utenti che la usavano non riceveranno più aggiornamenti, ma l'istanza locale e i contenuti rimarranno su questo dispositivo."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Rimuovere l'istanza condivisa dai server"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Rimuovi l'istanza condivisa dai server"
- },
"instance.action.create-shortcut": {
"message": "Crea collegamento"
},
@@ -3959,45 +3938,6 @@
"instance.server-modal.resource-pack": {
"message": "Pacchetto di risorse"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Azioni"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Link d'invito"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Chiunque abbia uno di questi inviti potrà entrare finché siano attivi."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Non ci sono inviti attivi."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Scadenza"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Revoca invito"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Revoca invito {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Inviti attivi"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Usi"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "L'invito {code} smetterà di funzionare immediatamente. Le persone già entrate manterranno l'accesso."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Questa azione è irreversibile"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Revoca invito"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Revoca invito"
- },
"instance.settings.synced-options.data-override": {
"message": "Disattiva la sovrascrittura per modificare i dati sincronizzati."
},
@@ -4325,9 +4265,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "L'accesso all'istanza condivisa è stato revocato. È ancora disponibile localmente, ma non è più collegata e non riceverà aggiornamenti."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "L'istanza principale è stata eliminata. È ancora disponibile localmente, ma non è più collegata e non riceverà aggiornamenti."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "L'istanza condivisa è stata bloccata dal team di moderazione dei contenuti. Non riceverà più aggiornamenti dall'istanza principale e non potrà essere avviata."
},
diff --git a/apps/app-frontend/src/locales/ja-JP/index.json b/apps/app-frontend/src/locales/ja-JP/index.json
index 7b83c823030..03b8b30c67a 100644
--- a/apps/app-frontend/src/locales/ja-JP/index.json
+++ b/apps/app-frontend/src/locales/ja-JP/index.json
@@ -770,12 +770,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "アカウントが違います"
},
- "app.instance.share.members.empty": {
- "message": "まだ誰も参加していません"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "フィルターに一致するユーザーが見つかりません。"
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "このインスタンスのアップデートは受信できなくなります"
},
@@ -2744,9 +2738,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "リンク済の共有インスタンス"
},
- "installation-settings.shared-instance.title": {
- "message": "インスタンスを非公開化"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "インスタンスの共有を解除"
},
@@ -2756,15 +2747,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "共有を解除中…"
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "共有インスタンスを非公開にする"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "この共有インスタンスをModrinthから削除し、使用者へのアップデート送信を停止します。ローカルのインスタンスには影響しません。"
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "非公開に設定中…"
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "これはローカルインスタンスにのみ影響します。インストール済みのコンテンツはこのデバイスに残り、共有インスタンスやそれを使用している他のユーザーには影響しません。"
},
@@ -2777,12 +2762,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Modrinthのサーバーからこの共有インスタンスを削除します。Modrinthアプリでこれを使用しているユーザーへのアップデート配信は停止しますが、ローカルのインスタンスとそのコンテンツはこのデバイスに残ります。"
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "共有インスタンスの非公開化"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "共有インスタンスを非公開にする"
- },
"instance.action.create-shortcut": {
"message": "ショートカットを作成"
},
@@ -3089,45 +3068,6 @@
"instance.server-modal.resource-pack": {
"message": "リソースパック"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "アクション"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "招待リンク"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "アクティブな招待リンクをお持ちの方なら誰でも参加できます。"
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "有効な招待はありません。"
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "有効期限"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "招待を取り消す"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "招待コード {code} を取り消す"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "有効な招待"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "ユーザー"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "招待リンク {code} は即座に無効化されます。すでに参加しているユーザーのアクセス権はそのまま保持されます。"
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "この操作は取り消せません"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "招待を取り消す"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "招待を取り消す"
- },
"instance.settings.tabs.behavior.description": {
"message": "このインスタンスに表示するタブを選択してください。"
},
@@ -3419,9 +3359,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "共有インスタンスへのアクセス権が取り消されました。このインスタンスは引き続き利用可能ですが、リンクは解除され、今後のアップデートは受信されなくなります。"
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "プライマリインスタンスが削除されました。このインスタンスは引き続き利用可能ですが、リンクは解除され、今後のアップデートは受信されなくなります。"
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "この共有インスタンスはコンテンツモデレーションチームによってロックされました。プライマリインスタンスからのアップデートの受信は停止し、プレイすることもできません。"
},
diff --git a/apps/app-frontend/src/locales/ko-KR/index.json b/apps/app-frontend/src/locales/ko-KR/index.json
index 9f6220e24f1..d7fdd1c5e75 100644
--- a/apps/app-frontend/src/locales/ko-KR/index.json
+++ b/apps/app-frontend/src/locales/ko-KR/index.json
@@ -761,39 +761,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "잘못된 계정"
},
- "app.instance.share.members.empty": {
- "message": "아직 참여한 사용자가 없습니다"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "필터에 일치하는 사용자가 없습니다."
- },
- "app.instance.share.remove-user-modal.effect-access": {
- "message": "이들은 더 이상 해당 공유 인스턴스의 업데이트를 받지 않습니다"
- },
- "app.instance.share.remove-user-modal.effect-installed-copy": {
- "message": "이미 설치한 사본은 해당 기기에 그대로 남아 있습니다"
- },
- "app.instance.share.remove-user-modal.effect-invite-again": {
- "message": "나중에 다시 초대할 수 있습니다"
- },
- "app.instance.share.remove-user-modal.effect-last-user": {
- "message": "마지막 사용자이며, 이 인스턴스의 공유가 비활성화됩니다"
- },
- "app.instance.share.remove-user-modal.effects-label": {
- "message": "어떻게 되나요?"
- },
- "app.instance.share.remove-user-modal.header": {
- "message": "접근 권한 제거"
- },
- "app.instance.share.remove-user-modal.remove-button": {
- "message": "접근 권한 제거"
- },
- "app.instance.share.remove-user-modal.user-avatar-alt": {
- "message": "{username}의 아바타"
- },
- "app.instance.share.remove-user-modal.warning-body": {
- "message": "이 공유 인스턴스에서 {username} 사용자의 접근 권한을 제거하면, 해당 사용자가 업데이트를 받기 전에 다시 초대해야 합니다."
- },
"app.instance.share.sign-in.button": {
"message": "로그인"
},
@@ -3590,9 +3557,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "연동된 공유 인스턴스"
},
- "installation-settings.shared-instance.title": {
- "message": "인스턴스 게시 취소"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "공유 인스턴스 연동 해제"
},
@@ -3602,15 +3566,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "연동 해제 중..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "공유 인스턴스 게시 취소"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Modrinth에서 이 공유 인스턴스를 삭제하고 이용자에게 업데이트 전송을 중단합니다. 본인의 로컬 인스턴스는 영향을 받지 않습니다."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "게시 취소 중..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "이 작업은 본인의 로컬 인스턴스에만 영향을 미칩니다. 설치된 콘텐츠는 이 기기에 유지되며, 공유 인스턴스 및 이를 이용하는 다른 사용자에게는 영향을 미치지 않습니다."
},
@@ -3623,12 +3581,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "이 작업은 Modrinth 서버에서 공유 인스턴스를 삭제합니다. Modrinth 앱에서 이를 이용 중인 사용자는 더 이상 업데이트를 받지 않지만, 본인의 로컬 인스턴스 및 콘텐츠는 이 기기에 유지됩니다."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "공유 인스턴스 게시 취소 중"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "공유 인스턴스 게시 취소"
- },
"instance.action.create-shortcut": {
"message": "바로가기 만들기"
},
@@ -3938,45 +3890,6 @@
"instance.server-modal.resource-pack": {
"message": "리소스 팩"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "작업"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "초대 링크"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "초대 링크가 유효한 동안 링크를 보유한 사람은 누구나 참여할 수 있습니다."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "활성화된 초대가 없습니다."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "만료일"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "초대 철회"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "초대 {code} 철회"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "활성화된 초대"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "사용 횟수"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "초대 링크 {code} 사용이 즉시 중단됩니다. 이미 참여한 사용자는 계속 접근 권한을 유지합니다."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "이 작업은 되돌릴 수 없습니다"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "초대 철회"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "초대 철회"
- },
"instance.settings.synced-options.data-override": {
"message": "이 재정의를 꺼서 동기화된 데이터를 편집합니다."
},
@@ -4304,9 +4217,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "공유 인스턴스 접근 권한이 취소되었습니다. 이 인스턴스는 계속 사용 가능하지만, 더 이상 연동되지 않으며 업데이트를 받지 않습니다."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "기본 인스턴스가 삭제되었습니다. 이 인스턴스는 계속 사용 가능하지만, 더 이상 연동되지 않으며 업데이트를 받지 않습니다."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "이 공유 인스턴스는 콘텐츠 조정 팀에 의해 잠겼습니다. 기본 인스턴스 업데이트를 받지 않으며 플레이할 수 없습니다."
},
diff --git a/apps/app-frontend/src/locales/ms-MY/index.json b/apps/app-frontend/src/locales/ms-MY/index.json
index 53a5651b0c6..77bcfea6294 100644
--- a/apps/app-frontend/src/locales/ms-MY/index.json
+++ b/apps/app-frontend/src/locales/ms-MY/index.json
@@ -1805,9 +1805,6 @@
"instance.server-modal.resource-pack": {
"message": "Pek sumber"
},
- "instance.settings.sharing.active-invites.uses": {
- "message": "Penggunaan"
- },
"instance.settings.tabs.general": {
"message": "Umum"
},
diff --git a/apps/app-frontend/src/locales/nl-NL/index.json b/apps/app-frontend/src/locales/nl-NL/index.json
index f4dfc335a25..2b33b184c72 100644
--- a/apps/app-frontend/src/locales/nl-NL/index.json
+++ b/apps/app-frontend/src/locales/nl-NL/index.json
@@ -761,12 +761,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Verkeerd account"
},
- "app.instance.share.members.empty": {
- "message": "Nog geen gebruikers zijn lid geworden"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Geen gebruikers voldoen aan je filters."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Ze zullen geen updates meer ontvangen voor deze gedeelde instantie"
},
@@ -3236,9 +3230,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Gekoppelde gedeelde instantie"
},
- "installation-settings.shared-instance.title": {
- "message": "Instantie terugtrekken"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Gedeelde instantie ontkoppelen"
},
@@ -3248,15 +3239,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Ontkoppelen..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Gedeelde instantie terugtrekken"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Verwijder deze gedeelde instantie uit Modrinth en stuur geen updates meer naar gebruikers ervan. Je lokale instantie merkt hier geen gevolgen van."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Terugtrekken..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Dit heeft alleen gevolgen voor je lokale instantie. Je geïnstalleerde inhoud blijft op dit apparaat staan, en de gedeelde instantie en andere gebruikers ervan ondervinden hier geen hinder van."
},
@@ -3269,12 +3254,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Hiermee wordt de gedeelde instantie van de servers van Modrinth verwijderd. Gebruikers die deze instantie in de Modrinth-app gebruiken, ontvangen geen updates meer, maar je lokale instantie en de inhoud daarvan blijven op dit apparaat staan."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Gedeelde instantie terugtrekken"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Gedeelde instantie terugtrekken"
- },
"instance.action.create-shortcut": {
"message": "Snelkoppeling maken"
},
@@ -3581,45 +3560,6 @@
"instance.server-modal.resource-pack": {
"message": "Bronpakket"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Acties"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Uitnodigingslink"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Iedereen die zo'n uitnodigingslink heeft, kan meedoen zolang deze actief is."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Er zijn geen actieve uitnodigingen."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Vervalt"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Uitnodiging intrekken"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Uitnodiging {code} intrekken"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Actieve uitnodigingen"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Keren gebruikt"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "De uitnodigingslink {code} zal gelijk niet meer werken. Mensen die zich al hebben aangemeld, behouden hun toegang."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Deze actie kan niet ongedaan worden gemaakt"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Uitnodiging intrekken"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Uitnodiging intrekken"
- },
"instance.settings.tabs.behavior.description": {
"message": "Kies welke tabbladen worden weergegeven op deze instantie."
},
@@ -3911,9 +3851,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Je toegang tot de gedeelde instantie is ingetrokken. Deze instantie is nog steeds beschikbaar, maar is niet langer gekoppeld en ontvangt geen updates meer."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "De hoofdinstantie is verwijderd. Deze instantie is nog steeds beschikbaar, maar is niet meer gekoppeld en ontvangt geen updates meer."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Deze gedeelde instantie is vergrendeld door het inhoudsmoderatieteam. De instantie ontvangt geen updates meer van de hoofdinstantie en je kunt er niet meer in spelen."
},
diff --git a/apps/app-frontend/src/locales/pl-PL/index.json b/apps/app-frontend/src/locales/pl-PL/index.json
index c5bff2eff29..8a87296245e 100644
--- a/apps/app-frontend/src/locales/pl-PL/index.json
+++ b/apps/app-frontend/src/locales/pl-PL/index.json
@@ -761,39 +761,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Złe konto"
},
- "app.instance.share.members.empty": {
- "message": "Żaden użytkownik jeszcze nie dołączył"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Nie znaleziono użytkowników pasujących do tych filtrów."
- },
- "app.instance.share.remove-user-modal.effect-access": {
- "message": "Nie będzie w stanie otrzymywać aktualizacji dla tej udostępnionej instancji"
- },
- "app.instance.share.remove-user-modal.effect-installed-copy": {
- "message": "Zainstalowana kopia pozostanie na urządzeniu tego użytkownika"
- },
- "app.instance.share.remove-user-modal.effect-invite-again": {
- "message": "Możesz zaprosić tego użytkownika ponownie później"
- },
- "app.instance.share.remove-user-modal.effect-last-user": {
- "message": "To jest ostatni użytkownik, udostępnianie zostanie wyłączone dla tej instancji"
- },
- "app.instance.share.remove-user-modal.effects-label": {
- "message": "Co się stanie?"
- },
- "app.instance.share.remove-user-modal.header": {
- "message": "Odwołaj dostęp"
- },
- "app.instance.share.remove-user-modal.remove-button": {
- "message": "Odwołaj dostęp"
- },
- "app.instance.share.remove-user-modal.user-avatar-alt": {
- "message": "Awatar użytkownika {username}"
- },
- "app.instance.share.remove-user-modal.warning-body": {
- "message": "Jeśli pozbawisz użytkownika {username} dostępu do tej współdzielonej instancji, musisz zaprosić go ponownie, aby mógł otrzymywać aktualizacje."
- },
"app.instance.share.sign-in.button": {
"message": "Zaloguj się"
},
@@ -3599,9 +3566,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Powiązana udostępniona instancja"
},
- "installation-settings.shared-instance.title": {
- "message": "Wycofaj instancję"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Odłącz udostępnioną instancję"
},
@@ -3611,15 +3575,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Odłączanie..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Wycofaj udostępnioną instancję"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Usuń tę udostępnioną instancję z Modrinth i przestań wysyłać aktualizacje do użytkowników, którym jest udostępniona. Nie wpłynie to na twoją lokalną instancję."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Anulowanie publikacji..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Dotyczy to tylko Twojej instancji lokalnej. Twoja zainstalowana zawartość pozostanie na tym urządzeniu, a współdzielona instancja i inne osoby korzystające z niej nie zostaną dotknięte."
},
@@ -3632,12 +3590,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Spowoduje to usunięcie tej udostępnionej instancji z serwerów Modrinth. Użytkownicy korzystający z niej w Modrinth App przestaną otrzymywać aktualizacje, ale twoja lokalna instalacja i jej zawartość pozostanie na tym urządzeniu."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Wycofywanie udostępnionej instancji"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Wycofaj udostępnioną instancję"
- },
"instance.action.create-shortcut": {
"message": "Utwórz skrót"
},
@@ -3947,45 +3899,6 @@
"instance.server-modal.resource-pack": {
"message": "Paczka zasobów"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Akcje"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Link zaproszenia"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Każdy, kto ma jeden z tych linków może dołączyć, póki jest on aktywny."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Brak aktywnych zaproszeń."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Wygasa"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Cofnij zaproszenie"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Cofnij zaproszenie {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Aktywne zaproszenia"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Użyć"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "Link zaproszenia {code} przestanie natychmiastowo działać. Osoby, które już dołączyły, będą mieć do niego dostęp."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Tej akcji nie można cofnąć"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Cofnij zaproszenie"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Cofnij zaproszenie"
- },
"instance.settings.synced-options.data-override": {
"message": "Wyłącz ten wyjątek synchronizacji, aby móc edytować synchronizowane dane."
},
@@ -4313,9 +4226,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Twój dostęp do udostępnionej instancji został odwołany. Instancja jest nadal dostępna, ale nie jest już połączona i nie będzie otrzymywać aktualizacji."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "Instancja źródłowa została usunięta. Instancja jest nadal dostępna, ale nie jest już połączona i nie będzie otrzymywać aktualizacji."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Ta udostępniona instancja została zablokowana przez zespół moderacyjny Modrinth. Nie będzie otrzymywać dalszych aktualizacji z instancji źródłowej."
},
diff --git a/apps/app-frontend/src/locales/pt-BR/index.json b/apps/app-frontend/src/locales/pt-BR/index.json
index 5e9eaa224ba..f313b0fa1fa 100644
--- a/apps/app-frontend/src/locales/pt-BR/index.json
+++ b/apps/app-frontend/src/locales/pt-BR/index.json
@@ -773,12 +773,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Conta errada"
},
- "app.instance.share.members.empty": {
- "message": "Nenhum usuário entrou ainda"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Nenhum usuário corresponde seus filtros."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Eles não receberão mais atualizações para esta instância compartilhada"
},
@@ -3611,9 +3605,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Instância compartilhada vinculada"
},
- "installation-settings.shared-instance.title": {
- "message": "Remover instância"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Desvincular instância compartilhada"
},
@@ -3623,15 +3614,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Desvinculando..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Remover instância compartilhada"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Remova esta instância compartilhada do Modrinth e pare de enviar atualizações para todos que a utilizam. Sua instância local não será afetada."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Removendo..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Isso afeta apenas a sua instância local. O conteúdo instalado permanecerá neste dispositivo, e a instância compartilhada e outras pessoas que a utilizam não serão afetadas."
},
@@ -3644,12 +3629,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Isso excluirá a instância compartilhada dos servidores do Modrinth. As pessoas usando-o no Modrinth App não receberá mais atualizações, mas sua instância local e seu conteúdo ainda permanecerá no dispositivo."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Removendo instância compartilhada"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Remover instância compartilhada"
- },
"instance.action.create-shortcut": {
"message": "Criar atalho"
},
@@ -3959,45 +3938,6 @@
"instance.server-modal.resource-pack": {
"message": "Pacote de recursos"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Ações"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Link de convite"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Qualquer um com algum desses links de convite pode entrar enquanto permanecem ativos."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Não há nenhum convite ativo."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Expira"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Revogar convite"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Revogar convite {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Convites ativos"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Usos"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "Este link de convite {code} deixará de funcionar imediatamente. As pessoas que já entraram ainda permanecerão com acesso."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Esta ação não pode ser desfeita"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Revogar convite"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Revogar convite"
- },
"instance.settings.synced-options.data-override": {
"message": "Desligar sobreposição para editar dados sincronizados"
},
@@ -4325,9 +4265,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Seu acesso a esta instância compartilhada foi revogado. Ela ainda está disponível, mas não está mais vinculada e não receberá mais atualizações."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "A instância principal foi excluída. Esta instância ainda está disponível, mas não está mais vinculada e não receberá mais atualizações."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Esta instância compartilhada foi bloqueada pela equipe de Moderação de Conteúdo. Ela não receberá mais atualizações da instância principal e não poderá ser jogada."
},
diff --git a/apps/app-frontend/src/locales/pt-PT/index.json b/apps/app-frontend/src/locales/pt-PT/index.json
index f60f0c3e98e..5bb59335622 100644
--- a/apps/app-frontend/src/locales/pt-PT/index.json
+++ b/apps/app-frontend/src/locales/pt-PT/index.json
@@ -1586,12 +1586,6 @@
"instance.server-modal.resource-pack": {
"message": "Pacote de recursos"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Ações"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Usos"
- },
"instance.settings.tabs.general": {
"message": "Geral"
},
diff --git a/apps/app-frontend/src/locales/ru-RU/index.json b/apps/app-frontend/src/locales/ru-RU/index.json
index 365189392b6..8ea8cefe729 100644
--- a/apps/app-frontend/src/locales/ru-RU/index.json
+++ b/apps/app-frontend/src/locales/ru-RU/index.json
@@ -761,12 +761,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Неправильная учётная запись"
},
- "app.instance.share.members.empty": {
- "message": "Ни один пользователь ещё не присоединился"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "По заданным фильтрам пользователи не найдены."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Пользователь больше не будет получать обновления совместной сборки"
},
@@ -3590,9 +3584,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Отвязка совместной сборки"
},
- "installation-settings.shared-instance.title": {
- "message": "Закрытие доступа"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Отвязать совместную сборку"
},
@@ -3602,15 +3593,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Отвязка..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Закрыть доступ к сборке"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Удаляет сборку с серверов Modrinth и прекращает отправку обновлений. Локальная сборка останется нетронутой."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Закрытие доступа..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Будет затронута только локальная сборка. Весь контент останется на устройстве, а совместная сборка у других участников не изменится."
},
@@ -3623,12 +3608,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Сборка будет удалена с серверов Modrinth. Локальная копия и её контент останутся на устройстве, а другие участники перестанут получать обновления."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Закрытие доступа к сборке"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Закрытие доступа к сборке"
- },
"instance.action.create-shortcut": {
"message": "Создать ярлык"
},
@@ -3935,45 +3914,6 @@
"instance.server-modal.resource-pack": {
"message": "Набор ресурсов"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Действия"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Ссылка"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Любой, у кого есть одна из этих ссылок, может присоединиться, пока она активна."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Нет действующих приглашений."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Истекает"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Отозвать приглашение"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Отозвать приглашение {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Действующие приглашения"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Использований"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "Ссылка {code} сразу перестанет работать. Уже присоединившиеся пользователи сохранят доступ."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Это действие необратимо"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Отозвать приглашение"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Отзыв приглашения"
- },
"instance.settings.synced-options.data-override": {
"message": "Отключить это переопределение для редактирования синхронизированных данных."
},
@@ -4301,9 +4241,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Доступ к сборке отозван. Сборка всё ещё доступна, но она не будет связана и не будет обновляться."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "Основная сборка была удалена. Эта сборка всё ещё доступна, но она теперь не связана и не будет получать обновления."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Совместная сборка заблокирована модераторами контента. Она больше не будет получать обновления от основной сборки и недоступна для игры."
},
diff --git a/apps/app-frontend/src/locales/sv-SE/index.json b/apps/app-frontend/src/locales/sv-SE/index.json
index 14f33c5a572..72d8c761093 100644
--- a/apps/app-frontend/src/locales/sv-SE/index.json
+++ b/apps/app-frontend/src/locales/sv-SE/index.json
@@ -773,39 +773,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Fel konto"
},
- "app.instance.share.members.empty": {
- "message": "Inga användare har anslutit än"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Ingen användare matchade dina filter."
- },
- "app.instance.share.remove-user-modal.effect-access": {
- "message": "De kommer inte längre att få uppdateringar för den här delade instansen"
- },
- "app.instance.share.remove-user-modal.effect-installed-copy": {
- "message": "Alla kopior de redan har installerat kommer vara kvar på deras enhet"
- },
- "app.instance.share.remove-user-modal.effect-invite-again": {
- "message": "Du kan bjuda in dem igen senare"
- },
- "app.instance.share.remove-user-modal.effect-last-user": {
- "message": "Detta är den sista användaren, delning kommer avaktiveras för den här instansen"
- },
- "app.instance.share.remove-user-modal.effects-label": {
- "message": "Vad händer?"
- },
- "app.instance.share.remove-user-modal.header": {
- "message": "Återkalla åtkomst"
- },
- "app.instance.share.remove-user-modal.remove-button": {
- "message": "Återkalla åtkomst"
- },
- "app.instance.share.remove-user-modal.user-avatar-alt": {
- "message": "{username}s avatar"
- },
- "app.instance.share.remove-user-modal.warning-body": {
- "message": "Återkallar du {username}s åtkomst till den här delade instansen kommer du behöva bjuda in hen igen innan hen kan få uppdateringar."
- },
"app.instance.share.sign-in.button": {
"message": "Logga in"
},
@@ -3158,24 +3125,15 @@
"installation-settings.shared-instance.linked-title": {
"message": "Länkad delad instans"
},
- "installation-settings.shared-instance.title": {
- "message": "Avpublicera instans"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Avlänka delad instans"
},
"installation-settings.shared-instance.unlinking-button": {
"message": "Avlänkar..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Avpublicera delad instans"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Ta bort den delade instansen från Modrinth och sluta skicka uppdateringar till alla som använder den. Din lokala instans kommer inte att påverkas."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Avpublicerar..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Avlänkar delad instans"
},
@@ -3185,12 +3143,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Det här raderar den delade instansen från Modrinths servrar. De som använder den i Modrinth App kommer inte längre få uppdateringar, men din lokala instans och dess innehåll är kvar på den här enheten."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Avpublicerar delad instans"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Avpublicera delad instans"
- },
"instance.action.create-shortcut": {
"message": "Skapa genväg"
},
@@ -3476,45 +3428,6 @@
"instance.server-modal.resource-pack": {
"message": "Resurs pack"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Åtgärder"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Inbjudningslänk"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Vem som helst med en av de här inbjudningslänkarna kan ansluta medan den är aktiv."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Det finns inga aktiva inbjudningar."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Går ut"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Återkalla inbjudan"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Återkalla inbjudan {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Aktiva inbjudningar"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Användningar"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "Inbjudningslänken {code} kommer att sluta fungera omedelbart. De som redan gått med kommer att behålla sin åtkomst."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Den här åtgärden kan inte ångras"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Återkalla inbjudan"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Återkalla inbjudan"
- },
"instance.settings.synced-options.data-packs.description": {
"message": "Håll den här instansens datapaket separata från synkroniserade paket."
},
@@ -3782,9 +3695,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Din åtkomst till den delade instansen återkallades. Den här instansen är fortfarande tillgänglig, men är inte längre länkad och kommer inte längre få uppdateringar."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "Primärinstansen raderades. Den här instansen är fortfarande tillgänglig, men är inte längre länkad och kommer inte längre få uppdateringar."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Den här instansen låstes av innehållsmodereringsteamet. Den kommer inte längre få uppdateringar från primärinstansen och kan inte spelas."
},
diff --git a/apps/app-frontend/src/locales/tr-TR/index.json b/apps/app-frontend/src/locales/tr-TR/index.json
index c7192221d25..4a4a97cf635 100644
--- a/apps/app-frontend/src/locales/tr-TR/index.json
+++ b/apps/app-frontend/src/locales/tr-TR/index.json
@@ -773,12 +773,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Hatalı hesap"
},
- "app.instance.share.members.empty": {
- "message": "Henüz katılan kullanıcı yok"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Filtrelerinizle eşleşen kullanıcı yok."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Artık bu paylaşılan kurulum için güncelleme almayacaklar"
},
@@ -2933,9 +2927,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Bağlantılı paylaşımlı örnek"
},
- "installation-settings.shared-instance.title": {
- "message": "Örneği yayından kaldır"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Paylaşımlı örneğin bağlantısını kaldır"
},
@@ -2945,15 +2936,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Bağlantı kesiliyor..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Paylaşımlı örneği yayından kaldır"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Bu paylaşımlı örneği Modrinth'ten kaldırın ve onu kullanan kişilere güncelleme göndermeyi durdurun. Yerel örneğiniz bundan etkilenmeyecektir."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Yayından kaldırılıyor..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Bu yalnızca yerel örneğinizi etkiler. Yüklediğiniz içerik bu cihazda kalacak, paylaşımlı örnek ve kullanan diğer kişiler etkilenmeyecek."
},
@@ -2966,12 +2951,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Bu paylaşımlı örneği Modrinth'in sunucularından kaldırır. Modrinth App'i kullanan kişiler güncelleme almayı bırakır fakat yerel örneğiniz ve içeriği bu cihazda kalacak."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Paylaşımlı örnek yayından kaldırılıyor"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Paylaşımlı örneği yayından kaldır"
- },
"instance.action.create-shortcut": {
"message": "Kısayol oluştur"
},
@@ -3278,45 +3257,6 @@
"instance.server-modal.resource-pack": {
"message": "Kaynak paketi"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Eylemler"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Davet bağlantısı"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Bu davet bağlantısına sahip olan herhangi birisi aktif olduğu sürece katılabilecektir."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Aktif davet bulunmuyor."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Süresi doluyor"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Daveti geri çek"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "{code} davetini geri çek"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Aktif davetler"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Kullanımlar"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "Davet linki {code}, hemen çalışmayı durduracaktır. Zaten katılmış kişiler erişimi sürdürecektir."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Bu eylem geri alınamaz"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Daveti geri çek"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Daveti geri çek"
- },
"instance.settings.tabs.behavior.description": {
"message": "Bu örnekte hangi sekmelerin görüneceğini seçin."
},
@@ -3608,9 +3548,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Bu paylaşımlı örneğe olan erişiminiz geri çekildi. Örnek hala erişilebilir fakat artık bağlantılı değil ve güncelleme almayacak."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "Ana örnek silindi. Bu örnek hala erişilebilir fakat artık bağlantılı değil ve güncelleme almayacak."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Bu paylaşımlı örnek İçerik Moderasyonu ekibi tarafından kilitlendi. Artık ana örnekten güncelleme almayacak ve oynanılamaz."
},
diff --git a/apps/app-frontend/src/locales/uk-UA/index.json b/apps/app-frontend/src/locales/uk-UA/index.json
index 296070c5dd3..d5eb1319334 100644
--- a/apps/app-frontend/src/locales/uk-UA/index.json
+++ b/apps/app-frontend/src/locales/uk-UA/index.json
@@ -764,12 +764,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Неправильний обліковий запис"
},
- "app.instance.share.members.empty": {
- "message": "Жоден користувач ще не приєднався"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Жоден користувач не відповідає вашим фільтрам."
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "Він більше не зможе отримувати оновлення для цього спільного примірника"
},
@@ -3602,9 +3596,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "Пов'язаний спільний примірник"
},
- "installation-settings.shared-instance.title": {
- "message": "Скасувати публікацію примірника"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "Відв'язати спільний примірник"
},
@@ -3614,15 +3605,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "Відв'язування..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "Скасувати публікацію спільного примірника"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "Видалити цей спільний примірник із Modrinth і припинити надсилати оновлення всім, хто ним користується. Це не вплине на ваш локальний примірник."
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "Скасування публікації..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Це вплине лише на ваш локальний примірник. Установлений уміст залишиться на цьому пристрої, а спільний примірник й інші користувачі не зазнають змін."
},
@@ -3635,12 +3620,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Це видалить спільний примірник із серверів Modrinth. Користувачі Modrinth App припинять отримувати оновлення, але ваш локальний примірник і його вміст залишаться на цьому пристрої."
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "Скасування публікації спільного примірника"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "Скасувати публікацію спільного примірника"
- },
"instance.action.create-shortcut": {
"message": "Створити ярлик"
},
@@ -3950,45 +3929,6 @@
"instance.server-modal.resource-pack": {
"message": "Пакет ресурсів"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Дії"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Посилання"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Будь-хто з цим посиланням-запрошенням може приєднатися, поки воно залишається активним."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Немає активних запрошень."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Чинний до"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Відкликати запрошення"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Відкликати запрошення {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Активні запрошення"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Використання"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "Посилання для запрошення {code} негайно припинить діяти. Люди, які вже приєдналися, збережуть доступ."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Цю дію неможливо скасувати"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Відкликати запрошення"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Відкликати запрошення"
- },
"instance.settings.synced-options.data-override": {
"message": "Вимкніть це перевизначення для зміни синхронізованих даних."
},
@@ -4316,9 +4256,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Ваш доступ до цього спільного примірника відкликано. Примірник досі доступний, але вже не прив'язаний та не отримуватиме оновлення."
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "Основний примірник видалено. Цей примірник досі доступний, але вже не прив'язаний та не отримуватиме оновлення."
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "Цей спільний примірник заблокували модератори вмісту. Він більше не отримуватиме оновлень від основного примірника і не може бути запущений."
},
diff --git a/apps/app-frontend/src/locales/vi-VN/index.json b/apps/app-frontend/src/locales/vi-VN/index.json
index 5e382158db2..ff2fd205db8 100644
--- a/apps/app-frontend/src/locales/vi-VN/index.json
+++ b/apps/app-frontend/src/locales/vi-VN/index.json
@@ -773,39 +773,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Sai tài khoản"
},
- "app.instance.share.members.empty": {
- "message": "Chưa có người dùng nào tham gia"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "Không có người dùng nào khớp với bộ lọc."
- },
- "app.instance.share.remove-user-modal.effect-access": {
- "message": "Họ sẽ không còn nhận được cập nhật cho bản cài đặt được chia sẻ này"
- },
- "app.instance.share.remove-user-modal.effect-installed-copy": {
- "message": "Các bản sao họ đã cài vẫn sẽ ở trên thiết bị của họ"
- },
- "app.instance.share.remove-user-modal.effect-invite-again": {
- "message": "Bạn có thể mời lại họ sau"
- },
- "app.instance.share.remove-user-modal.effect-last-user": {
- "message": "Đây là người dùng cuối cùng; tính năng chia sẻ sẽ bị tắt cho bản cài đặt này"
- },
- "app.instance.share.remove-user-modal.effects-label": {
- "message": "Điều gì sẽ xảy ra?"
- },
- "app.instance.share.remove-user-modal.header": {
- "message": "Thu hồi quyền truy cập"
- },
- "app.instance.share.remove-user-modal.remove-button": {
- "message": "Thu hồi quyền truy cập"
- },
- "app.instance.share.remove-user-modal.user-avatar-alt": {
- "message": "Ảnh đại diện của {username}"
- },
- "app.instance.share.remove-user-modal.warning-body": {
- "message": "Nếu bạn thu hồi quyền truy cập của {username} vào bản cài đặt được chia sẻ này, bạn sẽ phải mời lại họ trước khi họ có thể nhận cập nhật."
- },
"app.instance.share.sign-in.button": {
"message": "Đăng nhập"
},
@@ -3959,45 +3926,6 @@
"instance.server-modal.resource-pack": {
"message": "Gói tài nguyên"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "Thao tác"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "Liên kết mời"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "Bất kỳ ai có một trong các liên kết mời này đều có thể tham gia khi liên kết còn hiệu lực."
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "Không có lời mời nào đang hoạt động."
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "Hết hạn"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "Thu hồi lời mời"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "Thu hồi lời mời {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "Lời mời đang hoạt động"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "Lượt dùng"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "Liên kết mời {code} sẽ ngừng hoạt động ngay lập tức. Những người đã tham gia vẫn giữ quyền truy cập."
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "Không thể hoàn tác thao tác này"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "Thu hồi lời mời"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "Thu hồi lời mời"
- },
"instance.settings.synced-options.data-override": {
"message": "Tắt ghi đè này để chỉnh sửa dữ liệu đã đồng bộ."
},
diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json
index 523bdaae502..b8508d93d4c 100644
--- a/apps/app-frontend/src/locales/zh-CN/index.json
+++ b/apps/app-frontend/src/locales/zh-CN/index.json
@@ -773,12 +773,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "账号错误"
},
- "app.instance.share.members.empty": {
- "message": "暂无用户加入"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "没有符合您筛选条件的用户。"
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "对方将不再收到该共享实例的后续更新"
},
@@ -3611,9 +3605,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "已关联的共享实例"
},
- "installation-settings.shared-instance.title": {
- "message": "取消发布实例"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "解除共享实例关联"
},
@@ -3623,15 +3614,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "正在解除关联……"
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "取消发布共享实例"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "从Modrinth移除这个分享实例,同时停止发送新的更新到正在使用它的人,你的本地实例不会被影响。"
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "取消发布中……"
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "这只会影响到你的本地实例,你安装过的内容依然会在你的设备上保留,被其他人使用的分享实例也不会被影响。"
},
@@ -3644,12 +3629,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "这将从 Modrinth 服务器上删除该共享实例。在 Modrinth App 中使用它的用户将停止接收更新,但你的本地实例及其内容仍会保留在此设备上。"
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "取消发布共享实例"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "取消发布共享实例"
- },
"instance.action.create-shortcut": {
"message": "创建快捷方式"
},
@@ -3959,45 +3938,6 @@
"instance.server-modal.resource-pack": {
"message": "资源包"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "操作"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "邀请链接"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "只要邀请链接有效,任何拥有该链接的人均可加入。"
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "暂无有效邀请。"
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "有效期"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "撤销邀请码"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "撤销邀请码 {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "有效邀请"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "使用次数"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "邀请链接 {code} 将立即失效。已加入的成员将保留访问权限。"
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "这一操作无法撤销"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "撤销邀请"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "撤销邀请"
- },
"instance.settings.synced-options.data-override": {
"message": "关闭此覆盖以编辑同步数据。"
},
@@ -4325,9 +4265,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "你对此共享实例的访问权限已被撤销。该实例仍可正常使用,但已解除关联且不再接收后续更新。"
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "源实例已被删除。该实例仍可正常使用,但已解除关联且不再接收后续更新。"
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "此共享实例已被内容审核团队锁定。它将不再接收主实例的更新,且无法继续游玩。"
},
diff --git a/apps/app-frontend/src/locales/zh-TW/index.json b/apps/app-frontend/src/locales/zh-TW/index.json
index 33f30eee9a7..6a79155a77e 100644
--- a/apps/app-frontend/src/locales/zh-TW/index.json
+++ b/apps/app-frontend/src/locales/zh-TW/index.json
@@ -773,12 +773,6 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "帳號錯誤"
},
- "app.instance.share.members.empty": {
- "message": "還沒有使用者加入"
- },
- "app.instance.share.members.no-filter-results": {
- "message": "沒有找到使用者。"
- },
"app.instance.share.remove-user-modal.effect-access": {
"message": "他不會再收到這個共用實例的更新"
},
@@ -3611,9 +3605,6 @@
"installation-settings.shared-instance.linked-title": {
"message": "已連結的共用實例"
},
- "installation-settings.shared-instance.title": {
- "message": "取消發布實例"
- },
"installation-settings.shared-instance.unlink-button": {
"message": "取消連結共用實例"
},
@@ -3623,15 +3614,9 @@
"installation-settings.shared-instance.unlinking-button": {
"message": "正在取消連結..."
},
- "installation-settings.shared-instance.unpublish-button": {
- "message": "取消發布共用實例"
- },
"installation-settings.shared-instance.unpublish-description": {
"message": "在 Modrinth 上移除這個共用實例,並停止向任何使用者推送更新。你的本機實例不會受到影響。"
},
- "installation-settings.shared-instance.unpublishing-button": {
- "message": "正在取消發布..."
- },
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "這只會影響你的本機實例。你已安裝的內容仍會保留在此裝置上,共用實例及其他使用者都不會受到影響。"
},
@@ -3644,12 +3629,6 @@
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "這將從 Modrinth 的伺服器刪除共用實例。在 Modrinth App 使用該實例的使用者將不再收到更新,但本機實例及其內容仍會保留在此裝置上。"
},
- "installation-settings.unpublish-shared-instance.modal.admonition-header": {
- "message": "即將取消發布共用實例"
- },
- "installation-settings.unpublish-shared-instance.modal.header": {
- "message": "取消發布共用實例"
- },
"instance.action.create-shortcut": {
"message": "建立捷徑"
},
@@ -3959,45 +3938,6 @@
"instance.server-modal.resource-pack": {
"message": "資源包"
},
- "instance.settings.sharing.active-invites.actions": {
- "message": "動作"
- },
- "instance.settings.sharing.active-invites.code": {
- "message": "邀請連結"
- },
- "instance.settings.sharing.active-invites.description": {
- "message": "任何使用者都可透過下列任何一個有效邀請連結加入。"
- },
- "instance.settings.sharing.active-invites.empty": {
- "message": "目前沒有有效的邀請。"
- },
- "instance.settings.sharing.active-invites.expires": {
- "message": "有效期限"
- },
- "instance.settings.sharing.active-invites.revoke": {
- "message": "撤銷邀請"
- },
- "instance.settings.sharing.active-invites.revoke-with-code": {
- "message": "撤銷邀請 {code}"
- },
- "instance.settings.sharing.active-invites.title": {
- "message": "有效邀請"
- },
- "instance.settings.sharing.active-invites.uses": {
- "message": "使用次數"
- },
- "instance.settings.sharing.revoke-invite.admonition-body": {
- "message": "邀請連結 {code} 將立即失效。已加入的使用者不受影響。"
- },
- "instance.settings.sharing.revoke-invite.admonition-header": {
- "message": "這項動作無法復原"
- },
- "instance.settings.sharing.revoke-invite.confirm": {
- "message": "撤銷邀請"
- },
- "instance.settings.sharing.revoke-invite.header": {
- "message": "撤銷邀請"
- },
"instance.settings.synced-options.data-override": {
"message": "關閉這項覆寫設定即可編輯已同步資料。"
},
@@ -4325,9 +4265,6 @@
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "你對共用實例的存取權已被撤銷。這個實例仍可正常使用,但已不再與共用實例連結,且不再接收更新。"
},
- "instance.shared-instance.unavailable.deleted-text": {
- "message": "主要實例已被刪除。這個實例仍可正常使用,但已不再與共用實例連結,且不再接收更新。"
- },
"instance.shared-instance.unavailable.locked-text": {
"message": "這個共用實例已被內容管理團隊鎖定,不會再接收來自主要實例的更新,且無法遊玩。"
},
diff --git a/apps/app-frontend/src/pages/Browse.vue b/apps/app-frontend/src/pages/Browse.vue
index 5954c0236cc..236eedf6dea 100644
--- a/apps/app-frontend/src/pages/Browse.vue
+++ b/apps/app-frontend/src/pages/Browse.vue
@@ -18,6 +18,7 @@ import {
CreationFlowModal,
defineMessages,
formatProjectTypeSentence,
+ getHostingModEnvironmentOverride,
getLatestMatchingInstallVersion,
getSelectedInstallPreferences,
getTargetInstallPreferences,
@@ -138,9 +139,8 @@ const {
installQueuedServerInstallsAndBack,
initServerContext,
watchServerContextChanges,
- searchServerModpacks,
getServerProjectVersions,
- enforceSetupModpackRoute,
+ enforceServerSetupRoute,
getQueuedServerInstallPlans,
setQueuedServerInstallPlans,
resolveQueuedServerInstallPlan,
@@ -274,7 +274,7 @@ if (isFromWorlds.value && route.params.projectType !== 'server') {
})
}
-enforceSetupModpackRoute(route.params.projectType as string | undefined)
+enforceServerSetupRoute(route.params.projectType as string | undefined)
const allInstalledIds = computed(
() => new Set([...newlyInstalled.value, ...(installedProjectIds.value ?? [])]),
@@ -435,6 +435,15 @@ const instanceFilters = computed(() => {
return filters
})
+const serverOnly = ref(route.query.so === 'true')
+const showServerOnly = computed(() => isServerContext.value && projectType.value === 'mod')
+const serverEnvironmentOverride = computed(() =>
+ showServerOnly.value
+ ? getHostingModEnvironmentOverride(serverOnly.value)
+ : isSetupServerContext.value
+ ? getHostingModEnvironmentOverride(false)
+ : undefined,
+)
const serverHideInstalled = ref(false)
const hideSelectedServerInstalls = ref(false)
if (route.query.shi) {
@@ -463,7 +472,7 @@ const serverContextFilters = computed(() => {
if (!serverContextServerData.value) return filters
const pt = projectType.value
- if (pt !== 'modpack') {
+ if (pt !== 'modpack' && !isSetupServerContext.value) {
const gameVersion = serverContextServerData.value.mc_version
if (gameVersion) filters.push({ type: 'game_version', option: gameVersion })
@@ -473,8 +482,6 @@ const serverContextFilters = computed(() => {
if (platform && ['paper', 'purpur'].includes(platform))
filters.push({ type: 'plugin_loader', option: platform })
- if (pt === 'mod') filters.push({ type: 'environment', option: 'server' })
-
if (hideSelectedServerInstalls.value && queuedServerInstallProjectIds.value.size > 0) {
for (const id of queuedServerInstallProjectIds.value) {
filters.push({ type: 'project_id', option: `project_id:${id}`, negative: true })
@@ -483,10 +490,7 @@ const serverContextFilters = computed(() => {
}
if (pt === 'modpack') {
- filters.push(
- { type: 'environment', option: 'client' },
- { type: 'environment', option: 'server' },
- )
+ filters.push({ type: 'environment', option: 'server' })
if (hideInstalledModpacks.value && hiddenInstanceProjectIds.value.size > 0) {
for (const id of hiddenInstanceProjectIds.value) {
@@ -495,7 +499,11 @@ const serverContextFilters = computed(() => {
}
}
- if (serverHideInstalled.value && hiddenServerContentProjectIds.value.size > 0) {
+ if (
+ !isSetupServerContext.value &&
+ serverHideInstalled.value &&
+ hiddenServerContentProjectIds.value.size > 0
+ ) {
for (const id of hiddenServerContentProjectIds.value) {
filters.push({ type: 'project_id', option: `project_id:${id}`, negative: true })
}
@@ -561,6 +569,22 @@ const messages = defineMessages({
id: 'search.filter.locked.server-environment.title',
defaultMessage: 'Only client-side mods can be added to the server instance',
},
+ gameVersionProvidedByServer: {
+ id: 'search.filter.locked.server-game-version.title',
+ defaultMessage: 'Game version is provided by the server',
+ },
+ modLoaderProvidedByServer: {
+ id: 'search.filter.locked.server-loader.title',
+ defaultMessage: 'Loader is provided by the server',
+ },
+ providedByServer: {
+ id: 'search.filter.locked.server',
+ defaultMessage: 'Provided by the server',
+ },
+ syncWithServer: {
+ id: 'search.filter.locked.server.sync',
+ defaultMessage: 'Sync with server',
+ },
gameVersionProvidedByInstance: {
id: 'search.filter.locked.instance-game-version.title',
defaultMessage: 'Game version is provided by the instance',
@@ -594,6 +618,10 @@ const messages = defineMessages({
id: 'app.browse.project-type.modpacks',
defaultMessage: 'Modpacks',
},
+ pluginsProjectType: {
+ id: 'app.browse.project-type.plugins',
+ defaultMessage: 'Plugins',
+ },
modsProjectType: { id: 'app.browse.project-type.mods', defaultMessage: 'Mods' },
resourcePacksProjectType: {
id: 'app.browse.project-type.resource-packs',
@@ -636,8 +664,8 @@ watch(
return
}
if (isSetupServerContext.value) {
- enforceSetupModpackRoute(newType)
- if (newType !== 'modpack') return
+ enforceServerSetupRoute(newType)
+ if (!['modpack', 'mod', 'plugin', 'datapack'].includes(newType)) return
}
if (!newType || newType === projectType.value) return
@@ -703,6 +731,9 @@ const selectableProjectTypes = computed(() => {
if (isSetupServerContext.value) {
return [
{ label: formatMessage(messages.modpacksProjectType), href: `/browse/modpack${suffix}` },
+ { label: formatMessage(messages.modsProjectType), href: `/browse/mod${suffix}` },
+ { label: formatMessage(messages.pluginsProjectType), href: `/browse/plugin${suffix}` },
+ { label: formatMessage(messages.dataPacksProjectType), href: `/browse/datapack${suffix}` },
]
}
@@ -807,6 +838,7 @@ function getCurrentSelectedInstallPreferences(projectTypeValue: string) {
}
function getServerInstallTargetPreferences(contentType: BrowseInstallContentType) {
+ if (isSetupServerContext.value) return {}
return getTargetInstallPreferences(
{
gameVersion: serverContextServerData.value?.mc_version,
@@ -900,14 +932,19 @@ function getCardActions(
serverContentProjectIds.value.has(projectResult.project_id || '') ||
serverContextServerData.value?.upstream?.project_id === projectResult.project_id
const isInstalling = installingProjectIds.value.has(projectResult.project_id)
- const showAsInstalled = isInstalled && currentProjectType !== 'modpack'
+ const showAsInstalled =
+ isInstalled && currentProjectType !== 'modpack' && !isSetupServerContext.value
if (
isServerContext.value &&
['modpack', 'mod', 'plugin', 'datapack'].includes(currentProjectType)
) {
- const isQueued = queuedServerInstallProjectIds.value.has(projectResult.project_id)
- const isQueuedRoot = queuedServerInstallRootProjectIds.value.has(projectResult.project_id)
+ const isQueued =
+ !isSetupServerContext.value &&
+ queuedServerInstallProjectIds.value.has(projectResult.project_id)
+ const isQueuedRoot =
+ !isSetupServerContext.value &&
+ queuedServerInstallRootProjectIds.value.has(projectResult.project_id)
const isInstallingSelection = isInstallingQueuedServerInstalls.value
const showAsInstalling = isInstalling || (isInstallingSelection && isQueuedRoot)
const validatingInstall =
@@ -952,7 +989,7 @@ function getCardActions(
const plan = await requestInstall({
project: projectResult,
contentType,
- mode: isModpack ? 'immediate' : 'queue',
+ mode: isModpack || isSetupServerContext.value ? 'immediate' : 'queue',
selectedFilters: isModpack
? []
: stripServerRuntimeInstallFilters(searchState.currentFilters.value),
@@ -968,12 +1005,14 @@ function getCardActions(
install: (plan) =>
openServerModpackInstallFlow({
projectId: plan.projectId,
+ contentType: plan.contentType,
versionId: plan.versionId,
name: plan.project.name,
iconUrl: plan.project.icon_url ?? undefined,
}),
})
- if (!isModpack) await resolveQueuedServerInstallPlan(plan)
+ if (!isModpack && !isSetupServerContext.value)
+ await resolveQueuedServerInstallPlan(plan)
} catch (err) {
if (!isModpack) removeQueuedServerInstall(projectResult.project_id)
handleError(err as Error)
@@ -1137,11 +1176,23 @@ async function search(requestParams: string) {
}
const lockedFilterMessages = computed(() => ({
- gameVersion: formatMessage(messages.gameVersionProvidedByInstance),
- modLoader: formatMessage(messages.modLoaderProvidedByInstance),
+ gameVersion: formatMessage(
+ isServerContext.value
+ ? messages.gameVersionProvidedByServer
+ : messages.gameVersionProvidedByInstance,
+ ),
+ modLoader: formatMessage(
+ isServerContext.value
+ ? messages.modLoaderProvidedByServer
+ : messages.modLoaderProvidedByInstance,
+ ),
environment: formatMessage(messages.environmentProvidedByServer),
- syncButton: formatMessage(messages.syncFilterButton),
- providedBy: formatMessage(messages.providedByInstance),
+ syncButton: formatMessage(
+ isServerContext.value ? messages.syncWithServer : messages.syncFilterButton,
+ ),
+ providedBy: formatMessage(
+ isServerContext.value ? messages.providedByServer : messages.providedByInstance,
+ ),
}))
const searchState = useBrowseSearch({
@@ -1149,13 +1200,15 @@ const searchState = useBrowseSearch({
tags,
active: browseRouteActive,
providedFilters: combinedProvidedFilters,
+ environmentOverride: serverEnvironmentOverride,
search,
- persistentQueryParams: ['i', 'ai', 'shi', 'sid', 'wid', 'from'],
+ persistentQueryParams: ['i', 'ai', 'shi', 'so', 'sid', 'wid', 'from'],
getExtraQueryParams: () => ({
sid: serverIdQuery.value || undefined,
wid: effectiveServerWorldId.value || undefined,
ai: instanceHideInstalled.value ? 'true' : undefined,
shi: serverHideInstalled.value ? 'true' : undefined,
+ so: showServerOnly.value && serverOnly.value ? 'true' : undefined,
}),
})
@@ -1267,11 +1320,14 @@ provideBrowseManager({
query: getProjectBrowseQuery(),
}),
selectableProjectTypes,
- showProjectTypeTabs: computed(() => !isServerContext.value),
+ showProjectTypeTabs: computed(() => !isServerContext.value || isSetupServerContext.value),
variant: 'app',
getCardActions,
installContext,
providedFilters: combinedProvidedFilters,
+ serverOnly,
+ showServerOnly,
+ hiddenFilterTypes: computed(() => (showServerOnly.value ? ['environment'] : [])),
hideInstalled: computed({
get: () => {
if (projectType.value === 'modpack') return hideInstalledModpacks.value
@@ -1337,13 +1393,12 @@ provideBrowseManager({
{}"
diff --git a/apps/app-frontend/src/pages/hosting/manage/Index.vue b/apps/app-frontend/src/pages/hosting/manage/Index.vue
index f117e312efa..295de403ba2 100644
--- a/apps/app-frontend/src/pages/hosting/manage/Index.vue
+++ b/apps/app-frontend/src/pages/hosting/manage/Index.vue
@@ -1,8 +1,5 @@
-
+
route.name === 'ServerManageOverview')
+const serverRoute = shallowRef(router.currentRoute.value)
+watch(
+ router.currentRoute,
+ (nextRoute) => {
+ if (nextRoute.matched.some((record) => record.name === 'ServerManage')) {
+ serverRoute.value = nextRoute
+ }
+ },
+ { flush: 'sync' },
+)
+
+const isOverviewRoute = computed(() => serverRoute.value.name === 'ServerManageOverview')
const serverId = computed(() => {
- const rawId = route.params.id
+ const rawId = serverRoute.value.params.id
return Array.isArray(rawId) ? (rawId[0] ?? '') : (rawId ?? '')
})
diff --git a/apps/app-frontend/src/pages/hosting/manage/Play.vue b/apps/app-frontend/src/pages/hosting/manage/Play.vue
index e9147c188e3..7732da08aea 100644
--- a/apps/app-frontend/src/pages/hosting/manage/Play.vue
+++ b/apps/app-frontend/src/pages/hosting/manage/Play.vue
@@ -7,11 +7,18 @@ import { config } from '@/config'
const { play } = injectServerPlay()
async function downloadMrpack(blob: Blob, filename: string) {
- const path = await save({ defaultPath: filename, filters: [{ name: 'Modrinth modpack', extensions: ['mrpack'] }] })
+ const path = await save({
+ defaultPath: filename,
+ filters: [{ name: 'Modrinth modpack', extensions: ['mrpack'] }],
+ })
if (path) await writeFile(path, new Uint8Array(await blob.arrayBuffer()))
}
-
+
diff --git a/apps/app-frontend/src/pages/instance/components/page-header/instance-page-header-server-metadata.vue b/apps/app-frontend/src/pages/instance/components/page-header/instance-page-header-server-metadata.vue
index 915188e13e7..ddd835320fd 100644
--- a/apps/app-frontend/src/pages/instance/components/page-header/instance-page-header-server-metadata.vue
+++ b/apps/app-frontend/src/pages/instance/components/page-header/instance-page-header-server-metadata.vue
@@ -3,16 +3,15 @@
-
+
-
+
-
(() => {
const managedFiles = new Set(managedContentItems.value.map((item) => item.file_name))
const additionalProjects = projects.value.filter((item) => !managedFiles.has(item.file_name))
const active = installingItems.value.get(instance.value.id)
- const pending = (active ?? installingBuffer.value).filter((item) => !managedFiles.has(item.file_name))
+ const pending = (active ?? installingBuffer.value).filter(
+ (item) => !managedFiles.has(item.file_name),
+ )
if (pending.length === 0) return additionalProjects
const pendingProjectIds = new Set(pending.map((p) => p.project?.id).filter(Boolean))
const displayProjects = additionalProjects.map((project) =>
diff --git a/apps/app-frontend/src/pages/instance/layout.vue b/apps/app-frontend/src/pages/instance/layout.vue
index 22bea0e389b..30292723d74 100644
--- a/apps/app-frontend/src/pages/instance/layout.vue
+++ b/apps/app-frontend/src/pages/instance/layout.vue
@@ -116,16 +116,14 @@ import { computed, type ComputedRef, onUnmounted, ref, shallowRef, watch } from
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
import ExportModal from '@/components/ui/ExportModal.vue'
-import { useHostingInstance } from '@/composables/instances/use-hosting-instance'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import SharedInstanceInstallModal from '@/components/ui/shared-instances/shared-instance-install-modal/index.vue'
import SharedInstanceUpdateModal from '@/components/ui/shared-instances/SharedInstanceUpdateModal.vue'
-import {
- getServerStatusQueryKey,
-} from '@/composables/instances/use-server-status-query'
-import { useAppEvent } from '@/composables/use-app-event'
+import { useHostingInstance } from '@/composables/instances/use-hosting-instance'
import { useInstanceLaunchState } from '@/composables/instances/use-instance-launch-state'
+import { getServerStatusQueryKey } from '@/composables/instances/use-server-status-query'
+import { useAppEvent } from '@/composables/use-app-event'
import { useAppSettings } from '@/composables/use-app-settings.ts'
import { handleSevereError } from '@/composables/use-error.js'
import { useInstanceConsole } from '@/composables/useInstanceConsole'
@@ -298,7 +296,9 @@ const linkedProjectQuery = useQuery(
)
const linkedProjectV3 = computed(() => linkedProjectQuery.data.value ?? undefined)
const hosting = useHostingInstance(instance, offline)
-const isServerInstance = computed(() => hosting.isHostingInstance.value || linkedProjectV3.value?.minecraft_server != null)
+const isServerInstance = computed(
+ () => hosting.isHostingInstance.value || linkedProjectV3.value?.minecraft_server != null,
+)
const processesQuery = useQuery(
computed(() => ({
...instanceProcessesQueryOptions(instanceId.value),
@@ -378,9 +378,10 @@ const { notifySharedInstanceError, notifySharedInstanceUnavailable } = useShared
useLoadingBarToken(subpagePending)
useLoadingBarToken(computed(() => instanceQuery.isPending.value && !instance.value))
-const minecraftServer = computed(() => hosting.isHostingInstance.value
- ? { region: hosting.region.value }
- : linkedProjectV3.value?.minecraft_server,
+const minecraftServer = computed(() =>
+ hosting.isHostingInstance.value
+ ? { region: hosting.region.value }
+ : linkedProjectV3.value?.minecraft_server,
)
const javaServerPingData = computed(() => linkedProjectV3.value?.minecraft_java_server?.ping?.data)
const sharedInstanceState = createSharedInstanceContext(
@@ -411,7 +412,11 @@ const sharedInstanceUpdateAvailable = computed(
sharedInstanceUpdateKey.value !== hiddenSharedInstanceUpdateKey.value,
)
-const serverAddress = computed(() => hosting.isHostingInstance.value ? hosting.address.value : linkedProjectV3.value?.minecraft_java_server?.address)
+const serverAddress = computed(() =>
+ hosting.isHostingInstance.value
+ ? hosting.address.value
+ : linkedProjectV3.value?.minecraft_java_server?.address,
+)
const serverStatusQuery = useQuery({
queryKey: computed(() => getServerStatusQueryKey(serverAddress.value ?? '')),
queryFn: () => get_server_status(serverAddress.value!),
@@ -420,9 +425,17 @@ const serverStatusQuery = useQuery({
refetchInterval: 30_000,
retry: false,
})
-const statusOnline = computed(() => !serverStatusQuery.isError.value && (!!serverStatusQuery.data.value || !!javaServerPingData.value))
-const playersOnline = computed(() => serverStatusQuery.isError.value ? undefined : serverStatusQuery.data.value?.players?.online)
-const ping = computed(() => serverStatusQuery.isError.value ? undefined : serverStatusQuery.data.value?.ping)
+const statusOnline = computed(
+ () =>
+ !serverStatusQuery.isError.value &&
+ (!!serverStatusQuery.data.value || !!javaServerPingData.value),
+)
+const playersOnline = computed(() =>
+ serverStatusQuery.isError.value ? undefined : serverStatusQuery.data.value?.players?.online,
+)
+const ping = computed(() =>
+ serverStatusQuery.isError.value ? undefined : serverStatusQuery.data.value?.ping,
+)
const loadingServerPing = computed(() => serverStatusQuery.isFetched.value || offline.value)
async function refreshInstance() {
@@ -606,7 +619,13 @@ function handleSharedInstanceUpdateComplete(successful: boolean) {
const startInstance = async (context: string, address?: string) => {
if (!instance.value || instance.value.quarantined) return
- if (checkingSharedInstanceLaunch.value || loading.value || playing.value || instanceLaunch.isStarting(instance.value.id)) return
+ if (
+ checkingSharedInstanceLaunch.value ||
+ loading.value ||
+ playing.value ||
+ instanceLaunch.isStarting(instance.value.id)
+ )
+ return
const instanceId = instance.value.id
const isSharedInstanceMember = instance.value.shared_instance?.role === 'member'
diff --git a/apps/app-frontend/src/pages/instance/share/index.vue b/apps/app-frontend/src/pages/instance/share/index.vue
index d889a75de13..0b61d18f419 100644
--- a/apps/app-frontend/src/pages/instance/share/index.vue
+++ b/apps/app-frontend/src/pages/instance/share/index.vue
@@ -60,9 +60,16 @@
- {{ formatSharedInstanceUnavailable(sharedInstanceUnavailableReason, sharedInstanceUnavailableManager) }}
+ {{
+ formatSharedInstanceUnavailable(
+ sharedInstanceUnavailableReason,
+ sharedInstanceUnavailableManager,
+ )
+ }}
diff --git a/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue b/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue
index 48fc6dac4f4..39464f3c9a4 100644
--- a/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue
+++ b/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue
@@ -2,39 +2,19 @@
-
-
-
-
- {{ formatMessage(messages.pushUpdate) }}
-
-
-
-
- Invite friends
-
-
-
+ />
diff --git a/apps/app-frontend/src/providers/setup/server-install-content.ts b/apps/app-frontend/src/providers/setup/server-install-content.ts
index 33d6714e4a9..e72008b936e 100644
--- a/apps/app-frontend/src/providers/setup/server-install-content.ts
+++ b/apps/app-frontend/src/providers/setup/server-install-content.ts
@@ -10,9 +10,9 @@ import {
getTargetInstallPreferences,
injectModrinthClient,
injectNotificationManager,
- type ProjectSearchResult,
readStoredServerInstallQueue,
resolveServerAddonInstallPlans,
+ useDismissServerIntro,
useServerContextRuntime,
useServerPanelSync,
waitForServerContextRuntimeReady,
@@ -30,6 +30,7 @@ type InstallableSearchResult = Labrinth.Search.v3.ResultSearchProject & {
}
export interface ServerModpackSelectionRequest {
+ contentType?: string
projectId: string
versionId: string
name: string
@@ -69,9 +70,8 @@ export interface ServerInstallContentContext {
installQueuedServerInstallsAndBack: () => Promise
initServerContext: () => Promise
watchServerContextChanges: () => void
- searchServerModpacks: (query: string, limit?: number) => Promise
getServerProjectVersions: (projectId: string) => Promise<{ id: string }[]>
- enforceSetupModpackRoute: (currentProjectType: string | undefined) => void
+ enforceServerSetupRoute: (currentProjectType: string | undefined) => void
getQueuedServerInstallPlans: () => Map>
setQueuedServerInstallPlans: (
plans: Map>,
@@ -102,6 +102,7 @@ export function createServerInstallContent(opts: {
const client = injectModrinthClient()
const { handleError } = injectNotificationManager()
const queryClient = useQueryClient()
+ const dismissServerIntro = useDismissServerIntro()
const serverIdQuery = computed(() => readQueryString(route.query.sid))
const worldIdQuery = computed(() => readQueryString(route.query.wid))
@@ -184,7 +185,7 @@ export function createServerInstallContent(opts: {
})
const serverBrowseHeading = computed(() => {
if (serverFlowFrom.value === 'reset-server') {
- return 'Selecting modpack to install after reset'
+ return 'Selecting content to install after reset'
}
return 'Installing content'
})
@@ -281,35 +282,18 @@ export function createServerInstallContent(opts: {
})
}
- function enforceSetupModpackRoute(currentProjectType: string | undefined) {
- if (!isSetupServerContext.value || currentProjectType === 'modpack') return
+ function enforceServerSetupRoute(currentProjectType: string | undefined) {
+ if (
+ !isSetupServerContext.value ||
+ ['modpack', 'mod', 'plugin', 'datapack'].includes(currentProjectType ?? '')
+ )
+ return
router.replace({
path: '/browse/modpack',
query: route.query,
})
}
- async function searchServerModpacks(query: string, limit: number = 10) {
- const results = await client.labrinth.projects_v3.search({
- query: query || undefined,
- new_filters:
- 'project_types = "modpack" AND environment IN ["client_and_server", "server_only_client_optional"]',
- limit,
- })
-
- return {
- hits: results.hits.map((hit) => ({
- project_id: hit.project_id,
- title: hit.name,
- icon_url: hit.icon_url ?? '',
- latest_version: hit.version_id,
- })),
- total_hits: results.total_hits,
- offset: (results.page - 1) * results.hits_per_page,
- limit: results.hits_per_page,
- }
- }
-
async function getServerProjectVersions(projectId: string) {
const versions = await client.labrinth.versions_v3.getProjectVersions(projectId)
return versions.map((version) => ({ id: version.id }))
@@ -323,12 +307,17 @@ export function createServerInstallContent(opts: {
const modalInstance = serverSetupModalRef.value
if (!modalInstance) return
- modalInstance.show()
+ await modalInstance.show()
await nextTick()
const ctx = modalInstance.ctx
if (!ctx) return
+ if (request.contentType && request.contentType !== 'modpack') {
+ await ctx.selectProject(request.projectId, request.contentType, request.versionId)
+ return
+ }
+
ctx.setupType.value = 'modpack'
ctx.modpackSelection.value = {
projectId: request.projectId,
@@ -516,26 +505,30 @@ export function createServerInstallContent(opts: {
async function handleServerModpackFlowCreate(config: CreationFlowContextValue) {
const sid = serverIdQuery.value
const wid = effectiveServerWorldId.value
- if (!sid || !wid || !config.modpackSelection.value) {
+ if (!sid || !wid || (!config.modpackSelection.value && !config.projectInstall.value)) {
config.loading.value = false
return
}
try {
- await client.archon.content_v1.installContent(sid, wid, {
- content_variant: 'modpack',
- spec: {
- platform: 'modrinth',
- project_id: config.modpackSelection.value.projectId,
- version_id: config.modpackSelection.value.versionId,
- },
- soft_override: false,
- properties: config.buildProperties(),
- } satisfies Archon.Content.v1.InstallWorldContent)
+ if (config.projectInstall.value) {
+ await config.installServerContent(sid, wid)
+ } else {
+ await client.archon.content_v1.installContent(sid, wid, {
+ content_variant: 'modpack',
+ spec: {
+ platform: 'modrinth',
+ project_id: config.modpackSelection.value!.projectId,
+ version_id: config.modpackSelection.value!.versionId,
+ },
+ soft_override: false,
+ properties: config.buildProperties(),
+ } satisfies Archon.Content.v1.InstallWorldContent)
+ }
serverSetupModalRef.value?.hide()
if (serverFlowFrom.value === 'onboarding') {
- await client.archon.servers_v1.endIntro(sid)
+ await dismissServerIntro.mutateAsync(sid)
await router.push(`/hosting/manage/${sid}/content`)
return
}
@@ -578,9 +571,8 @@ export function createServerInstallContent(opts: {
installQueuedServerInstallsAndBack,
initServerContext,
watchServerContextChanges,
- searchServerModpacks,
getServerProjectVersions,
- enforceSetupModpackRoute,
+ enforceServerSetupRoute,
getQueuedServerInstallPlans,
setQueuedServerInstallPlans,
resolveQueuedServerInstallPlan,
diff --git a/apps/frontend/src/composables/use-server-install-content.ts b/apps/frontend/src/composables/use-server-install-content.ts
index c60cc6929cc..3ae7fbbba3c 100644
--- a/apps/frontend/src/composables/use-server-install-content.ts
+++ b/apps/frontend/src/composables/use-server-install-content.ts
@@ -11,6 +11,7 @@ import {
commonMessages,
defineMessages,
flushStoredServerAddonInstallQueue,
+ getHostingModEnvironmentOverride,
getServerAddonInstallPlanProjectIds,
getStoredServerAddonInstallQueue,
getTargetInstallPreferences,
@@ -21,6 +22,7 @@ import {
resolveServerAddonInstallPlans,
stripServerRuntimeInstallFilters,
stripServerRuntimeInstallOverrides,
+ useDismissServerIntro,
useServerContextRuntime,
useServerPanelSync,
useVIntl,
@@ -82,7 +84,7 @@ const messages = defineMessages({
},
resetModpackHeading: {
id: 'discover.install.heading.reset-modpack',
- defaultMessage: 'Selecting modpack to install after reset',
+ defaultMessage: 'Selecting content to install after reset',
},
})
@@ -94,6 +96,7 @@ export function useServerInstallContent({
const { formatMessage } = useVIntl()
const client = injectModrinthClient()
const queryClient = useQueryClient()
+ const dismissServerIntro = useDismissServerIntro()
const route = useRoute()
const { handleError } = injectNotificationManager()
let browseSearchState: ServerInstallBrowseSearchState | null = null
@@ -105,6 +108,10 @@ export function useServerInstallContent({
const currentServerId = computed(() => queryAsString(route.query.sid) || null)
const fromContext = computed(() => queryAsString(route.query.from) || null)
+ const isSetupServerContext = computed(
+ () =>
+ !!currentServerId.value && ['onboarding', 'reset-server'].includes(fromContext.value ?? ''),
+ )
const currentWorldId = computed(() => queryAsString(route.query.wid) || null)
useServerContextRuntime(currentServerId)
useServerPanelSync({
@@ -240,7 +247,7 @@ export function useServerInstallContent({
projectType.value?.id,
)
const filters: FilterValue[] = []
- if (serverData.value && projectType.value?.id !== 'modpack') {
+ if (serverData.value && projectType.value?.id !== 'modpack' && !isSetupServerContext.value) {
const gameVersion = serverData.value.mc_version
if (gameVersion) {
filters.push({ type: 'game_version', option: gameVersion })
@@ -280,10 +287,7 @@ export function useServerInstallContent({
}
if (currentServerId.value && projectType.value?.id === 'modpack') {
- filters.push(
- { type: 'environment', option: 'client' },
- { type: 'environment', option: 'server' },
- )
+ filters.push({ type: 'environment', option: 'server' })
}
debug('serverFilters result:', filters)
return filters
@@ -292,23 +296,10 @@ export function useServerInstallContent({
const showServerOnlyToggle = computed(() => !!serverData.value && projectType.value?.id === 'mod')
const serverEnvironmentOverride = computed(() => {
- if (!showServerOnlyToggle.value) return undefined
- if (serverContentServerOnly.value) {
- return {
- mode: 'include',
- values: [
- 'server_only',
- 'dedicated_server_only',
- 'server_only_client_optional',
- 'client_or_server_prefers_both',
- 'client_or_server',
- ],
- }
- }
- return {
- mode: 'exclude',
- values: ['client_only', 'singleplayer_only'],
+ if (!showServerOnlyToggle.value) {
+ return isSetupServerContext.value ? getHostingModEnvironmentOverride(false) : undefined
}
+ return getHostingModEnvironmentOverride(serverContentServerOnly.value)
})
function getCurrentServerInstallType(): BrowseInstallContentType {
@@ -320,6 +311,7 @@ export function useServerInstallContent({
}
function getServerInstallTargetPreferences(contentType: BrowseInstallContentType) {
+ if (isSetupServerContext.value) return {}
return getTargetInstallPreferences(
{
gameVersion: serverData.value?.mc_version,
@@ -495,11 +487,20 @@ export function useServerInstallContent({
const isModpack = contentType === 'modpack'
try {
- if (!isModpack && queuedServerInstallRootProjectIds.value.has(project.project_id)) {
+ if (
+ !isModpack &&
+ !isSetupServerContext.value &&
+ queuedServerInstallRootProjectIds.value.has(project.project_id)
+ ) {
removeQueuedServerInstall(project.project_id)
return
}
- if (!isModpack && queuedServerInstallProjectIds.value.has(project.project_id)) return
+ if (
+ !isModpack &&
+ !isSetupServerContext.value &&
+ queuedServerInstallProjectIds.value.has(project.project_id)
+ )
+ return
if (isModpack || !queuedServerInstallProjectIds.value.has(project.project_id)) {
setProjectInstalling(project.project_id, true)
@@ -508,7 +509,7 @@ export function useServerInstallContent({
const plan = await requestInstall({
project,
contentType,
- mode: isModpack ? 'immediate' : 'queue',
+ mode: isModpack || isSetupServerContext.value ? 'immediate' : 'queue',
selectedFilters: isModpack
? []
: stripServerRuntimeInstallFilters(browseSearchState.currentFilters.value),
@@ -529,11 +530,16 @@ export function useServerInstallContent({
}
onboardingInstallingProject.value = plan.project
- modalInstance.show()
+ await modalInstance.show()
await nextTick()
const ctx = modalInstance.ctx
if (!ctx) return
+ if (!isModpack) {
+ await ctx.selectProject(plan.projectId, plan.contentType, plan.versionId)
+ return
+ }
+
ctx.setupType.value = 'modpack'
ctx.modpackSelection.value = {
projectId: plan.projectId,
@@ -544,7 +550,7 @@ export function useServerInstallContent({
ctx.modal.value?.setStage('final-config')
},
})
- if (!isModpack) await resolveAndStoreQueuedAddonPlan(plan)
+ if (!isModpack && !isSetupServerContext.value) await resolveAndStoreQueuedAddonPlan(plan)
} catch (e) {
console.error(e)
if (isModpack) {
@@ -574,29 +580,37 @@ export function useServerInstallContent({
}
async function onModpackFlowCreate(config: CreationFlowContextValue) {
- if (!currentServerId.value || !currentWorldId.value || !config.modpackSelection.value) return
+ if (
+ !currentServerId.value ||
+ !currentWorldId.value ||
+ (!config.modpackSelection.value && !config.projectInstall.value)
+ )
+ return
try {
- await client.archon.content_v1.installContent(currentServerId.value, currentWorldId.value, {
- content_variant: 'modpack',
- spec: {
- platform: 'modrinth',
- project_id: config.modpackSelection.value.projectId,
- version_id: config.modpackSelection.value.versionId,
- },
- soft_override: false,
- properties: config.buildProperties(),
- } satisfies Archon.Content.v1.InstallWorldContent)
+ if (config.projectInstall.value) {
+ await config.installServerContent(currentServerId.value, currentWorldId.value)
+ } else {
+ await client.archon.content_v1.installContent(currentServerId.value, currentWorldId.value, {
+ content_variant: 'modpack',
+ spec: {
+ platform: 'modrinth',
+ project_id: config.modpackSelection.value!.projectId,
+ version_id: config.modpackSelection.value!.versionId,
+ },
+ soft_override: false,
+ properties: config.buildProperties(),
+ } satisfies Archon.Content.v1.InstallWorldContent)
+ }
if (fromContext.value === 'onboarding') {
- await client.archon.servers_v1.endIntro(currentServerId.value)
- queryClient.invalidateQueries({ queryKey: ['servers', 'detail', currentServerId.value] })
+ await dismissServerIntro.mutateAsync(currentServerId.value)
navigateTo(`/hosting/manage/${currentServerId.value}/content`)
} else {
navigateTo(`/hosting/manage/${currentServerId.value}?openSettings=installation`)
}
} catch (e) {
- handleError(new Error(`Error installing modpack: ${e}`))
+ handleError(new Error(`Error installing content: ${e}`))
config.loading.value = false
}
}
@@ -694,6 +708,7 @@ export function useServerInstallContent({
return {
currentServerId,
fromContext,
+ isSetupServerContext,
currentWorldId,
serverData,
serverContentData,
diff --git a/apps/frontend/src/locales/en-US/index.json b/apps/frontend/src/locales/en-US/index.json
index 3a868648cac..40b2b090b61 100644
--- a/apps/frontend/src/locales/en-US/index.json
+++ b/apps/frontend/src/locales/en-US/index.json
@@ -2214,7 +2214,7 @@
"message": "This content type cannot be installed to a server from Discover."
},
"discover.install.heading.reset-modpack": {
- "message": "Selecting modpack to install after reset"
+ "message": "Selecting content to install after reset"
},
"discover.seo.description": {
"message": "Search and browse thousands of Minecraft {projectType} on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft {projectType}."
diff --git a/apps/frontend/src/pages/discover.vue b/apps/frontend/src/pages/discover.vue
index 74f3699db89..38c24b4cf2f 100644
--- a/apps/frontend/src/pages/discover.vue
+++ b/apps/frontend/src/pages/discover.vue
@@ -8,7 +8,10 @@ const flags = useFeatureFlags()
const route = useRoute()
const modSettings = useModerationSettings()
-const allowTabChanging = computed(() => !route.query.sid)
+const isServerSetup = computed(
+ () => !!route.query.sid && ['onboarding', 'reset-server'].includes(String(route.query.from)),
+)
+const allowTabChanging = computed(() => !route.query.sid || isServerSetup.value)
const marginTarget = computed(() => getMarginTarget(modSettings.value))
const selectableProjectTypes = [
@@ -48,6 +51,22 @@ const selectableProjectTypes = [
type: 'servers',
},
]
+const projectTypeLinks = computed(() => {
+ const query = new URLSearchParams()
+ for (const key of ['sid', 'wid', 'from']) {
+ const value = route.query[key]
+ if (typeof value === 'string') query.set(key, value)
+ }
+ return selectableProjectTypes
+ .filter(
+ (type) =>
+ !isServerSetup.value || ['mods', 'plugins', 'modpacks', 'datapacks'].includes(type.type),
+ )
+ .map((type) => ({
+ ...type,
+ href: isServerSetup.value ? `${type.href}?${query.toString()}` : type.href,
+ }))
+})
diff --git a/apps/frontend/src/pages/discover/[type]/index.vue b/apps/frontend/src/pages/discover/[type]/index.vue
index 74877228d4c..1817ac12a60 100644
--- a/apps/frontend/src/pages/discover/[type]/index.vue
+++ b/apps/frontend/src/pages/discover/[type]/index.vue
@@ -177,6 +177,7 @@ const onboardingModalRef = ref(null)
const {
currentServerId,
fromContext,
+ isSetupServerContext,
serverData,
serverContentData,
serverFilters,
@@ -351,14 +352,17 @@ function getCardActions(
}
if (serverData.value) {
- const isQueued = queuedServerInstallProjectIds.value.has(result.project_id)
- const isQueuedRoot = queuedServerInstallRootProjectIds.value.has(result.project_id)
+ const isQueued =
+ !isSetupServerContext.value && queuedServerInstallProjectIds.value.has(result.project_id)
+ const isQueuedRoot =
+ !isSetupServerContext.value && queuedServerInstallRootProjectIds.value.has(result.project_id)
const isInstalled =
- projectResult.installed ||
- optimisticallyInstalledProjectIds.value.has(result.project_id) ||
- (serverContentData.value &&
- (serverContentData.value.addons ?? []).find((x) => x.project_id === result.project_id)) ||
- serverData.value.upstream?.project_id === result.project_id
+ !isSetupServerContext.value &&
+ (projectResult.installed ||
+ optimisticallyInstalledProjectIds.value.has(result.project_id) ||
+ (serverContentData.value &&
+ (serverContentData.value.addons ?? []).find((x) => x.project_id === result.project_id)) ||
+ serverData.value.upstream?.project_id === result.project_id)
const isInstalling = installingProjectIds.value.has(result.project_id)
const isInstallingSelection = isInstallingQueuedServerInstalls.value
const showAsInstalling = isInstalling || (isInstallingSelection && isQueuedRoot)
@@ -646,7 +650,7 @@ const { isStuck: isInstallHeaderStuck } = useStickyObserver(
-import { injectModrinthServerContext, type ServerPlayTarget, ServersManagePlayPage } from '@modrinth/ui'
+import {
+ injectModrinthServerContext,
+ type ServerPlayTarget,
+ ServersManagePlayPage,
+} from '@modrinth/ui'
const { server } = injectModrinthServerContext()
const config = useRuntimeConfig()
useHead({ title: computed(() => `Play - ${server.value?.name ?? 'Server'} - Modrinth`) })
function playServer({ serverId, worldId }: ServerPlayTarget) {
- window.location.assign(`modrinth://hosting/${encodeURIComponent(serverId)}/${encodeURIComponent(worldId)}`)
+ window.location.assign(
+ `modrinth://hosting/${encodeURIComponent(serverId)}/${encodeURIComponent(worldId)}`,
+ )
}
async function downloadMrpack(blob: Blob, filename: string) {
@@ -22,5 +28,9 @@ async function downloadMrpack(blob: Blob, filename: string) {
-
+
diff --git a/packages/api-client/src/modules/archon/content/v1.ts b/packages/api-client/src/modules/archon/content/v1.ts
index a4a20939449..413c1292e18 100644
--- a/packages/api-client/src/modules/archon/content/v1.ts
+++ b/packages/api-client/src/modules/archon/content/v1.ts
@@ -6,26 +6,48 @@ export class ArchonContentV1Module extends AbstractModule {
return 'archon_content_v1'
}
- public async share(serverId: string, worldId: string, configPaths: string[] = []): Promise {
- return this.client.request(`/servers/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId)}/content/share`, {
- api: 'archon', version: 1, method: 'POST', timeout: 600_000, retry: false,
- body: { config_paths: configPaths },
- })
+ public async share(
+ serverId: string,
+ worldId: string,
+ configPaths: string[] = [],
+ ): Promise {
+ return this.client.request(
+ `/servers/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId)}/content/share`,
+ {
+ api: 'archon',
+ version: 1,
+ method: 'POST',
+ timeout: 600_000,
+ retry: false,
+ body: { config_paths: configPaths },
+ },
+ )
}
- public async getShareDiff(serverId: string, worldId: string): Promise {
- return this.client.request(`/servers/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId)}/content/share/diff`, {
- api: 'archon', version: 1, method: 'GET',
- })
+ public async getShareDiff(
+ serverId: string,
+ worldId: string,
+ ): Promise {
+ return this.client.request(
+ `/servers/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId)}/content/share/diff`,
+ {
+ api: 'archon',
+ version: 1,
+ method: 'GET',
+ },
+ )
}
public async unshare(serverId: string, worldId: string): Promise {
- return this.client.request(`/servers/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId)}/content/share`, {
- api: 'archon',
- version: 1,
- method: 'DELETE',
- retry: false,
- })
+ return this.client.request(
+ `/servers/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId)}/content/share`,
+ {
+ api: 'archon',
+ version: 1,
+ method: 'DELETE',
+ retry: false,
+ },
+ )
}
/** GET /v1/:server_id/worlds/:world_id/addons */
diff --git a/packages/api-client/src/modules/archon/types.ts b/packages/api-client/src/modules/archon/types.ts
index 16c534afa05..c8127f3ab76 100644
--- a/packages/api-client/src/modules/archon/types.ts
+++ b/packages/api-client/src/modules/archon/types.ts
@@ -297,7 +297,12 @@ export namespace Archon {
export type SharedContentDiffEntry =
| { type: 'project'; project_id: string; change: SharedContentChange }
- | { type: 'external_file'; file_type: string; file_name: string; kind: 'added' | 'removed' | 'updated' }
+ | {
+ type: 'external_file'
+ file_type: string
+ file_name: string
+ kind: 'added' | 'removed' | 'updated'
+ }
| { type: 'modpack' | 'game_version'; change: SharedContentChange }
| { type: 'loader'; change: SharedContentChange<{ name: string; version: string | null }> }
@@ -308,11 +313,14 @@ export namespace Archon {
has_changes: boolean
diffs: SharedContentDiffEntry[]
projects: Record
- versions: Record
+ versions: Record<
+ string,
+ {
+ project_id: string
+ name: string
+ version_number: string | null
+ }
+ >
}
export type AddonKind = 'mod' | 'plugin' | 'datapack' | 'shader' | 'resourcepack'
diff --git a/packages/api-client/src/modules/shared-instances/instances/v1.ts b/packages/api-client/src/modules/shared-instances/instances/v1.ts
index 727568413c0..fff35b9fa36 100644
--- a/packages/api-client/src/modules/shared-instances/instances/v1.ts
+++ b/packages/api-client/src/modules/shared-instances/instances/v1.ts
@@ -66,19 +66,33 @@ export class SharedInstancesInstancesV1Module extends AbstractModule {
public async inviteUsers(instanceId: string, userIds: string[]): Promise<{ failed: string[] }> {
return this.client.request(`/instances/${encodeURIComponent(instanceId)}/users`, {
- api: 'sharedinstances', version: 1, method: 'POST', body: { user_ids: userIds }, retry: false,
+ api: 'sharedinstances',
+ version: 1,
+ method: 'POST',
+ body: { user_ids: userIds },
+ retry: false,
})
}
public async removeUsers(instanceId: string, userIds: string[]): Promise {
return this.client.request(`/instances/${encodeURIComponent(instanceId)}/users`, {
- api: 'sharedinstances', version: 1, method: 'DELETE', body: { user_ids: userIds },
+ api: 'sharedinstances',
+ version: 1,
+ method: 'DELETE',
+ body: { user_ids: userIds },
})
}
public async downloadMrpack(instanceId: string, version: number): Promise {
- return this.client.request(`/instances/${encodeURIComponent(instanceId)}/versions/${version}/mrpack`, {
- api: 'sharedinstances', version: 1, method: 'GET', responseType: 'blob', timeout: 600_000,
- })
+ return this.client.request(
+ `/instances/${encodeURIComponent(instanceId)}/versions/${version}/mrpack`,
+ {
+ api: 'sharedinstances',
+ version: 1,
+ method: 'GET',
+ responseType: 'blob',
+ timeout: 600_000,
+ },
+ )
}
}
diff --git a/packages/api-client/src/modules/shared-instances/invites/v1.ts b/packages/api-client/src/modules/shared-instances/invites/v1.ts
index 8ff2f2f92ac..01a42edb52e 100644
--- a/packages/api-client/src/modules/shared-instances/invites/v1.ts
+++ b/packages/api-client/src/modules/shared-instances/invites/v1.ts
@@ -21,19 +21,33 @@ export class SharedInstancesInvitesV1Module extends AbstractModule {
public async list(instanceId: string): Promise {
return this.client.request(`/instances/${encodeURIComponent(instanceId)}/invites`, {
- api: 'sharedinstances', version: 1, method: 'GET',
+ api: 'sharedinstances',
+ version: 1,
+ method: 'GET',
})
}
- public async create(instanceId: string, options: { max_age?: number; max_uses: number }): Promise<{ id: string }> {
+ public async create(
+ instanceId: string,
+ options: { max_age?: number; max_uses: number },
+ ): Promise<{ id: string }> {
return this.client.request(`/instances/${encodeURIComponent(instanceId)}/invites`, {
- api: 'sharedinstances', version: 1, method: 'POST', body: options, retry: false,
+ api: 'sharedinstances',
+ version: 1,
+ method: 'POST',
+ body: options,
+ retry: false,
})
}
public async delete(instanceId: string, inviteId: string): Promise {
- return this.client.request(`/instances/${encodeURIComponent(instanceId)}/invites/${encodeURIComponent(inviteId)}`, {
- api: 'sharedinstances', version: 1, method: 'DELETE',
- })
+ return this.client.request(
+ `/instances/${encodeURIComponent(instanceId)}/invites/${encodeURIComponent(inviteId)}`,
+ {
+ api: 'sharedinstances',
+ version: 1,
+ method: 'DELETE',
+ },
+ )
}
}
diff --git a/packages/ui/src/components/base/inputs/Input.vue b/packages/ui/src/components/base/inputs/Input.vue
index 9f1b5d5cf71..8e9d669e15d 100644
--- a/packages/ui/src/components/base/inputs/Input.vue
+++ b/packages/ui/src/components/base/inputs/Input.vue
@@ -34,7 +34,7 @@
aria-hidden="true"
class="pointer-events-none invisible col-start-1 row-start-1 whitespace-pre font-medium"
:class="[size === 'small' ? 'text-sm' : 'text-base', inputClass, controlClass]"
- >{{ hasValue ? model : placeholder || ' ' }}{{ hasValue ? model : placeholder || ' ' }}
selectedLoader.value,
async (loader) => {
- if (ctx.projectInstall.value) return
+ if (ctx.projectInstall.value && ctx.flowType === 'instance') return
await fetchLoaderMetadata(loader)
},
{ immediate: true },
@@ -638,7 +638,7 @@ watch(
loaderVersionsData.value = []
selectedLoaderVersion.value = null
- if (ctx.projectInstall.value) return
+ if (ctx.projectInstall.value && ctx.flowType === 'instance') return
if (!loader || !gameVersion || loader === 'vanilla') return
loaderVersionsLoading.value = true
@@ -683,6 +683,7 @@ watch(
// Auto-select based on loaderVersionType
autoSelectLoaderVersion()
},
+ { immediate: true },
)
watch(
diff --git a/packages/ui/src/components/flows/creation-flow-modal/components/SetupTypeStage.vue b/packages/ui/src/components/flows/creation-flow-modal/components/SetupTypeStage.vue
index cbaccf7e194..e13c36196d0 100644
--- a/packages/ui/src/components/flows/creation-flow-modal/components/SetupTypeStage.vue
+++ b/packages/ui/src/components/flows/creation-flow-modal/components/SetupTypeStage.vue
@@ -28,7 +28,11 @@
{{
formatMessage(
- isModpackOption(item.value) ? messages.installModpack : messages.createInstance,
+ isModpackOption(item.value)
+ ? messages.installModpack
+ : ctx.flowType === 'instance'
+ ? messages.createInstance
+ : commonMessages.continueButton,
)
}}
@@ -328,7 +332,7 @@ watch(
if (!projectId) return
const hit = ctx.projectSearchHits.value[projectId]
- if (ctx.flowType === 'instance') {
+ if (ctx.flowType === 'instance' || hit?.projectType !== 'modpack') {
void ctx.selectProject(projectId, hit?.projectType ?? 'mod')
return
}
diff --git a/packages/ui/src/components/flows/creation-flow-modal/creation-flow-context.ts b/packages/ui/src/components/flows/creation-flow-modal/creation-flow-context.ts
index 52205cd758e..ca22f19c02c 100644
--- a/packages/ui/src/components/flows/creation-flow-modal/creation-flow-context.ts
+++ b/packages/ui/src/components/flows/creation-flow-modal/creation-flow-context.ts
@@ -16,6 +16,7 @@ import { createContext, injectModrinthClient, injectNotificationManager } from '
import type { ImportableLauncher } from '../../../providers/instance-import'
import type { MultiStageModal, StageConfigInput } from '../../base'
import type { ComboboxOption } from '../../base/Combobox.vue'
+import { installServerContent, prepareServerContent, searchServerContent } from './server-content'
import { stageConfigs } from './stages'
export type FlowType = 'world' | 'server-onboarding' | 'reset-server' | 'instance'
@@ -118,6 +119,8 @@ export interface ProjectSearchResult {
}
export interface ProjectInstallSelection {
+ versionId?: string
+ contentType?: 'mod' | 'plugin' | 'datapack'
projectId: string
title: string
iconUrl?: string | null
@@ -234,7 +237,8 @@ export interface CreationFlowContextValue {
setSetupType: (type: SetupType) => void
setImportMode: () => void
browseModpacks: () => void
- selectProject: (projectId: string, projectType: string) => Promise
+ selectProject: (projectId: string, projectType: string, versionId?: string) => Promise
+ installServerContent: (serverId: string, worldId: string) => Promise
finish: () => void
buildProperties: () => Archon.Content.v1.PropertiesFields
fetchLoaderMetadata: (loader?: string | null) => Promise
@@ -298,10 +302,14 @@ export function createCreationFlowContext(
const onBack = options.onBack ?? null
const randomizeInstanceIcon = options.randomizeInstanceIcon ?? null
const customizeInstanceIcon = options.customizeInstanceIcon ?? null
- const searchProjects = options.searchProjects!
+ const searchProjects =
+ options.searchProjects ??
+ ((query: string, limit?: number) => searchServerContent(client, query, limit))
const prepareProjectInstall = options.prepareProjectInstall
const createProjectInstall = options.createProjectInstall
- const getProjectVersions = options.getProjectVersions!
+ const getProjectVersions =
+ options.getProjectVersions ??
+ ((projectId: string) => client.labrinth.versions_v3.getProjectVersions(projectId))
const getLoaderManifest = options.getLoaderManifest ?? null
const finishDisabled = options.finishDisabled ?? computed(() => false)
const finishDisabledTooltip = options.finishDisabledTooltip ?? computed(() => undefined)
@@ -387,7 +395,7 @@ export function createCreationFlowContext(
() =>
setupType.value === 'vanilla' ||
selectedLoader.value === 'vanilla' ||
- projectInstall.value !== null,
+ (flowType === 'instance' && projectInstall.value !== null),
)
function toApiLoaderName(loader: string): string {
@@ -548,11 +556,13 @@ export function createCreationFlowContext(
emit.browseModpacks()
}
- async function selectProject(projectId: string, projectType: string) {
- if (!prepareProjectInstall) return
+ async function selectProject(projectId: string, projectType: string, versionId?: string) {
+ if (!prepareProjectInstall && flowType === 'instance') return
try {
- const selection = await prepareProjectInstall(projectId, projectType)
+ const selection = prepareProjectInstall
+ ? await prepareProjectInstall(projectId, projectType)
+ : await prepareServerContent(client, projectId, projectType, availableLoaders, versionId)
if (selection) {
setProjectInstall(selection)
} else {
@@ -694,6 +704,8 @@ export function createCreationFlowContext(
setImportMode,
browseModpacks,
selectProject,
+ installServerContent: (serverId, worldId) =>
+ installServerContent(client, contextValue, serverId, worldId),
finish,
buildProperties,
fetchLoaderMetadata,
diff --git a/packages/ui/src/components/flows/creation-flow-modal/server-content.ts b/packages/ui/src/components/flows/creation-flow-modal/server-content.ts
new file mode 100644
index 00000000000..be5eaebb8e7
--- /dev/null
+++ b/packages/ui/src/components/flows/creation-flow-modal/server-content.ts
@@ -0,0 +1,173 @@
+import type { AbstractModrinthClient, Archon, Labrinth } from '@modrinth/api-client'
+
+import { retainServerContextRuntime } from '../../../composables/server-context-runtime'
+import type { CreationFlowContextValue, ProjectInstallSelection } from './creation-flow-context'
+
+const serverEnvironments = [
+ 'client_and_server',
+ 'server_only',
+ 'server_only_client_optional',
+ 'dedicated_server_only',
+ 'client_or_server',
+ 'client_or_server_prefers_both',
+ 'client_only_server_optional',
+]
+
+export async function searchServerContent(
+ client: AbstractModrinthClient,
+ query: string,
+ limit = 10,
+) {
+ const results = await client.labrinth.projects_v3.search({
+ query: query || undefined,
+ new_filters: `project_types IN ["modpack", "mod", "plugin", "datapack"] AND environment IN ${JSON.stringify(serverEnvironments)}`,
+ limit,
+ })
+ return {
+ hits: results.hits.map((hit) => ({
+ project_id: hit.project_id,
+ project_type: ['modpack', 'plugin', 'datapack', 'mod'].find((type) =>
+ hit.project_types.includes(type as Labrinth.Projects.v3.ProjectType),
+ ),
+ title: hit.name,
+ icon_url: hit.icon_url ?? '',
+ })),
+ total_hits: results.total_hits,
+ offset: (results.page - 1) * results.hits_per_page,
+ limit: results.hits_per_page,
+ }
+}
+
+export async function prepareServerContent(
+ client: AbstractModrinthClient,
+ projectId: string,
+ projectType: string,
+ availableLoaders: string[],
+ versionId?: string,
+): Promise {
+ if (!['mod', 'plugin', 'datapack'].includes(projectType)) {
+ throw new Error('This content cannot be installed on a server.')
+ }
+ const [project, versions] = await Promise.all([
+ client.labrinth.projects_v3.get(projectId),
+ versionId
+ ? client.labrinth.versions_v3.getVersion(versionId).then((version) => [version])
+ : client.labrinth.versions_v3.getProjectVersions(projectId),
+ ])
+ const getLoaders = (version: Labrinth.Versions.v3.Version) =>
+ availableLoaders.filter((loader) => {
+ if (projectType === 'datapack') {
+ return loader === 'vanilla' && version.loaders.includes('datapack')
+ }
+ if (version.loaders.includes(loader)) return true
+ return (
+ ['paper', 'purpur'].includes(loader) &&
+ version.loaders.some((value) => ['bukkit', 'spigot', 'paper'].includes(value))
+ )
+ })
+ const version = [...versions]
+ .sort((a, b) => Date.parse(b.date_published) - Date.parse(a.date_published))
+ .find(
+ (version) =>
+ version.project_id === projectId &&
+ (!version.environment || serverEnvironments.includes(version.environment)) &&
+ getLoaders(version).length > 0 &&
+ version.game_versions.length > 0,
+ )
+ if (!version) throw new Error('No compatible server version is available for this project.')
+ const gameVersions = [...version.game_versions].sort((a, b) =>
+ b.localeCompare(a, undefined, { numeric: true }),
+ )
+ return {
+ projectId,
+ versionId: version.id,
+ contentType: projectType as 'mod' | 'plugin' | 'datapack',
+ title: project.name,
+ iconUrl: project.icon_url,
+ link: `/${projectType}/${project.slug ?? projectId}`,
+ compatibleLoaders: getLoaders(version),
+ gameVersions,
+ releaseGameVersions: new Set(
+ gameVersions.filter((version) => /^\d+\.\d+(\.\d+)?$/.test(version)),
+ ),
+ }
+}
+
+export async function installServerContent(
+ client: AbstractModrinthClient,
+ ctx: CreationFlowContextValue,
+ serverId: string,
+ worldId: string,
+) {
+ const selection = ctx.projectInstall.value
+ const loader = ctx.selectedLoader.value
+ const gameVersion = ctx.selectedGameVersion.value
+ const loaderVersion = ctx.selectedLoaderVersion.value ?? ''
+ if (!selection?.contentType || !selection.versionId || !loader || !gameVersion) {
+ throw new Error('Select content, a loader, and a Minecraft version before installing.')
+ }
+ if (loader !== 'vanilla' && !loaderVersion) {
+ throw new Error('Select a loader version before installing.')
+ }
+ const properties = ctx.buildProperties()
+ const resolved = await client.labrinth.content_v3.resolve({
+ project_id: selection.projectId,
+ version_id: selection.versionId,
+ content_type: selection.contentType,
+ selected: {
+ game_versions: [gameVersion],
+ loaders: selection.contentType === 'datapack' ? ['datapack'] : [loader],
+ },
+ })
+ const runtime = retainServerContextRuntime(client, serverId)
+ let unsubscribe = () => {}
+ let timeout: ReturnType | undefined
+ try {
+ await runtime.waitUntilReady()
+ const previousIds = new Set(runtime.installProgressItems.value.map((item) => item.id))
+ let started = false
+ const completed = new Promise((resolve, reject) => {
+ unsubscribe = client.archon.sockets.on(serverId, 'install-progress', ({ items }) => {
+ const item = items.find(
+ (item) =>
+ item.world_id === worldId &&
+ item.key.type === 'platform' &&
+ item.key.platform === loader &&
+ item.key.game_version === gameVersion &&
+ item.key.platform_version === loaderVersion,
+ )
+ if (!item) return
+ if (!previousIds.has(item.id) || (item.progress != null && item.progress < 100)) {
+ started = true
+ }
+ if (!started) return
+ if (item?.error) reject(new Error(item.error))
+ else if (item?.progress === 100) resolve()
+ })
+ timeout = setTimeout(() => reject(new Error('Timed out waiting for server setup.')), 600_000)
+ })
+ await Promise.all([
+ completed,
+ client.archon.content_v1.installContent(serverId, worldId, {
+ content_variant: 'bare',
+ loader: loader === 'neoforge' ? 'neo_forge' : (loader as Archon.Content.v1.Modloader),
+ version: loaderVersion,
+ game_version: gameVersion,
+ soft_override: false,
+ properties,
+ }),
+ ])
+ await client.archon.content_v1.addAddons(
+ serverId,
+ worldId,
+ [resolved.primary, ...resolved.dependencies].map((item) => ({
+ project_id: item.project_id,
+ version_id: item.version_id,
+ })),
+ )
+ } finally {
+ unsubscribe()
+ clearTimeout(timeout)
+ runtime.release()
+ }
+}
diff --git a/packages/ui/src/components/modal/NewModal.vue b/packages/ui/src/components/modal/NewModal.vue
index 1040e39ab43..de3633af1c3 100644
--- a/packages/ui/src/components/modal/NewModal.vue
+++ b/packages/ui/src/components/modal/NewModal.vue
@@ -94,7 +94,8 @@
data-modal-content
:class="[
'flex-1 min-h-0 overflow-y-auto',
- props.noPadding ? '' : 'p-6 !pb-1 sm:pb-6',
+ props.noPadding ? '' : 'p-6',
+ { '!pb-0': $slots.actions && !actionsDivider && !props.noPadding },
{ 'pt-12': props.mergeHeader && closable && !props.noPadding },
]"
:style="{ maxHeight: maxContentHeight }"
@@ -127,6 +128,7 @@
:class="[
'min-h-0',
props.noPadding ? '' : 'overflow-y-auto p-6',
+ { '!pb-0': $slots.actions && !actionsDivider && !props.noPadding },
{ 'pt-12': props.mergeHeader && closable && !props.noPadding },
]"
>
diff --git a/packages/ui/src/components/servers/ServerConfigFilePicker.vue b/packages/ui/src/components/servers/ServerConfigFilePicker.vue
index d0061c15727..f63ed5efef2 100644
--- a/packages/ui/src/components/servers/ServerConfigFilePicker.vue
+++ b/packages/ui/src/components/servers/ServerConfigFilePicker.vue
@@ -1,14 +1,26 @@
-
+
{{ formatMessage(messages.title) }}
-
+
{{ formatMessage(messages.retry) }}
-
+
([])
const excludedPaths = ref([])
const container = ref(null)
const { showTopFade, showBottomFade, forceCheck } = useScrollIndicator(container)
-const extensions = new Set(['json', 'json5', 'jsonc', 'yml', 'yaml', 'css', 'toml', 'txt', 'ini', 'cfg', 'conf', 'properties', 'xml', 'nbt'])
+const extensions = new Set([
+ 'json',
+ 'json5',
+ 'jsonc',
+ 'yml',
+ 'yaml',
+ 'css',
+ 'toml',
+ 'txt',
+ 'ini',
+ 'cfg',
+ 'conf',
+ 'properties',
+ 'xml',
+ 'nbt',
+])
function directoryOptions(path: string) {
const worldId = props.worldId
const directoryPath = path ? `/config/${path}` : '/config'
return {
- queryKey: ['servers', 'share-config-files', props.serverId, worldId, auth.user.value?.id, directoryPath],
+ queryKey: [
+ 'servers',
+ 'share-config-files',
+ props.serverId,
+ worldId,
+ auth.user.value?.id,
+ directoryPath,
+ ],
queryFn: async () => {
const items: FileTreeSelectItem[] = []
let pages = 1
@@ -72,9 +106,14 @@ function directoryOptions(path: string) {
const result = await client.kyros.files_v0.listDirectory(directoryPath, page, 2000)
pages = result.total
for (const item of result.items) {
- if (item.name.startsWith('.') || /[/\\:]/.test(item.name) || /[. ]$/.test(item.name)) continue
+ if (item.name.startsWith('.') || /[/\\:]/.test(item.name) || /[. ]$/.test(item.name))
+ continue
if (item.type !== 'directory' && item.type !== 'file') continue
- if (item.type === 'file' && !extensions.has(item.name.split('.').pop()?.toLowerCase() ?? '')) continue
+ if (
+ item.type === 'file' &&
+ !extensions.has(item.name.split('.').pop()?.toLowerCase() ?? '')
+ )
+ continue
items.push({
path: path ? `${path}/${item.name}` : item.name,
type: item.type,
@@ -115,11 +154,16 @@ async function resolvePaths() {
return selected
}
async function visit(path: string) {
- if (props.worldId !== worldId || auth.user.value?.id !== userId) throw new Error(formatMessage(messages.contextChanged))
+ if (props.worldId !== worldId || auth.user.value?.id !== userId)
+ throw new Error(formatMessage(messages.contextChanged))
const items = await queryClient.fetchQuery(directoryOptions(path))
for (const item of items) {
if (item.type === 'directory') {
- if (isSelected(item.path) || [...included].some((included) => included.startsWith(`${item.path}/`))) await visit(item.path)
+ if (
+ isSelected(item.path) ||
+ [...included].some((included) => included.startsWith(`${item.path}/`))
+ )
+ await visit(item.path)
} else if (isSelected(item.path)) {
const size = item.size ?? 0
totalSize += size
@@ -135,11 +179,21 @@ async function resolvePaths() {
}
const messages = defineMessages({
- title: { id: 'instance.shared-instance.publish-review.config-title-v2', defaultMessage: 'Select config files' },
+ title: {
+ id: 'instance.shared-instance.publish-review.config-title-v2',
+ defaultMessage: 'Select config files',
+ },
error: { id: 'servers.play.config-files.error', defaultMessage: 'Could not load config files' },
retry: { id: 'servers.play.retry', defaultMessage: 'Retry' },
- selectionTooLarge: { id: 'servers.play.config-files.selection-too-large', defaultMessage: 'Select up to 4,096 config files, with a maximum of 16 MiB per file and 128 MiB in total.' },
- contextChanged: { id: 'servers.play.config-files.context-changed', defaultMessage: 'The world or account changed. Open Push update again.' },
+ selectionTooLarge: {
+ id: 'servers.play.config-files.selection-too-large',
+ defaultMessage:
+ 'Select up to 4,096 config files, with a maximum of 16 MiB per file and 128 MiB in total.',
+ },
+ contextChanged: {
+ id: 'servers.play.config-files.context-changed',
+ defaultMessage: 'The world or account changed. Open Push update again.',
+ },
})
defineExpose({ resolvePaths })
diff --git a/packages/ui/src/components/servers/ServerSetupModal.vue b/packages/ui/src/components/servers/ServerSetupModal.vue
index 64f6449431d..cd1a8608985 100644
--- a/packages/ui/src/components/servers/ServerSetupModal.vue
+++ b/packages/ui/src/components/servers/ServerSetupModal.vue
@@ -9,7 +9,6 @@
:initial-loader="initialLoader"
:initial-game-version="initialGameVersion"
:fade="props.initialSetup ? undefined : 'danger'"
- :search-projects="searchModpacks"
:get-project-versions="getProjectVersions"
:finish-disabled="!canCompleteSetup"
:finish-disabled-tooltip="!canCompleteSetup ? permissionDeniedMessage : undefined"
@@ -65,15 +64,6 @@ const { addNotification } = injectNotificationManager()
const serverLoaders = ['vanilla', 'fabric', 'neoforge', 'forge', 'quilt', 'paper', 'purpur']
const { canSetup, canResetServer, permissionDeniedMessage } = useServerPermissions()
-async function searchModpacks(query: string, limit: number = 10) {
- return client.labrinth.projects_v2.search({
- query: query || undefined,
- new_filters:
- 'project_types = "modpack" AND (client_side = "optional" OR client_side = "required") AND server_side = "required"',
- limit,
- })
-}
-
async function getProjectVersions(projectId: string) {
const versions = await client.labrinth.versions_v3.getProjectVersions(projectId)
return versions.map((v) => ({ id: v.id }))
@@ -126,7 +116,10 @@ async function onFlowComplete(ctx: CreationFlowContextValue) {
})
try {
- if (ctx.setupType.value === 'modpack' && ctx.modpackFile.value) {
+ if (ctx.projectInstall.value) {
+ await ctx.installServerContent(serverContext.serverId, serverContext.worldId.value!)
+ emitReinstall()
+ } else if (ctx.setupType.value === 'modpack' && ctx.modpackFile.value) {
debug('onFlowComplete: mrpack upload path')
await handleMrpackUpload(ctx.modpackFile.value, ctx.buildProperties())
} else if (ctx.setupType.value === 'modpack' && ctx.modpackSelection.value) {
diff --git a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
index be5e722a73b..df95c0dc0fb 100644
--- a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
+++ b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue
@@ -1,6 +1,12 @@
-
+
0"
- v-tooltip="formatMessage(messages.locked)"
+ v-tooltip="modelValue.lockedTooltip ?? formatMessage(messages.locked)"
class="inline-flex size-5 shrink-0 cursor-help items-center justify-center text-secondary"
tabindex="0"
>
diff --git a/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/index.vue b/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/index.vue
index cb76de13a26..0a399209c6d 100644
--- a/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/index.vue
+++ b/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/index.vue
@@ -67,9 +67,9 @@ const title = computed(() =>
:aria-busy="disabled || undefined"
>
-
-
+
([], {
watchSyncEffect(() => fuse.setCollection(items.value))
+function getItemWarningType(item: ContentItem) {
+ return props.enableEnabledFor
+ ? getContentWarningType(item, props.showEnvironmentWarnings)
+ : getClientWarningType(item, props.showEnvironmentWarnings)
+}
+
const filterOptions = computed(() => {
if (props.filterMode === 'status') {
return [
@@ -234,9 +240,7 @@ const filterOptions = computed(() => {
}
})
- if (
- items.value.some((item) => getClientWarningType(item, props.showEnvironmentWarnings) !== null)
- ) {
+ if (items.value.some((item) => getItemWarningType(item) !== null)) {
options.push({ id: 'warnings', label: formatMessage(messages.warnings) })
}
@@ -247,6 +251,13 @@ const filterOptions = computed(() => {
return options
})
+watchSyncEffect(() => {
+ if (items.value.length === 0) return
+ const availableFilters = new Set(filterOptions.value.map((option) => option.id))
+ const validFilters = selectedFilters.value.filter((filter) => availableFilters.has(filter))
+ if (validFilters.length !== selectedFilters.value.length) selectedFilters.value = validFilters
+})
+
const stats = computed(() => {
const counts: Record = {}
for (const item of items.value) {
@@ -267,8 +278,7 @@ function matchesSelectedFilters(item: ContentItem) {
return false
if (hasEnabledFilter !== hasDisabledFilter && Boolean(item.enabled) !== hasEnabledFilter)
return false
- if (hasWarningsFilter && getClientWarningType(item, props.showEnvironmentWarnings) === null)
- return false
+ if (hasWarningsFilter && getItemWarningType(item) === null) return false
return true
}
diff --git a/packages/ui/src/layouts/shared/content-tab/components/modals/ConfirmEnvironmentModal.vue b/packages/ui/src/layouts/shared/content-tab/components/modals/ConfirmEnvironmentModal.vue
new file mode 100644
index 00000000000..a1a06cbbbf4
--- /dev/null
+++ b/packages/ui/src/layouts/shared/content-tab/components/modals/ConfirmEnvironmentModal.vue
@@ -0,0 +1,112 @@
+
+
+
+
+ {{ formatMessage(bodyMessage, { project }) }}
+
+
+
+
+
+
+ {{ formatMessage(commonMessages.cancelButton) }}
+
+
+
+ {{ formatMessage(commonMessages.enableButton) }}
+
+
+
+
+
+
+
diff --git a/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts b/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts
index 218f8d32c01..4c5d3ffb8de 100644
--- a/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts
+++ b/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts
@@ -23,6 +23,19 @@ export function getClientWarningType(
return null
}
+export function getContentWarningType(
+ item: ContentItem,
+ showEnvironmentWarnings = false,
+): ClientWarningType | 'unknown-environment' | null {
+ if (!item.enabledFor) return getClientWarningType(item, showEnvironmentWarnings)
+ if (!item.enabledFor.warningTooltip) return null
+ if (item.enabledFor.server) {
+ if (item.pack_client_retained) return 'retained'
+ if (item.pack_client_depends) return 'depends'
+ }
+ return 'unknown-environment'
+}
+
export interface ContentFilterOption {
id: string
label: string
@@ -96,7 +109,7 @@ export function useContentFilters(items: Ref, config?: ContentFil
if (
config?.showWarningsFilter &&
items.value.some(
- (item) => getClientWarningType(item, config.showEnvironmentWarnings) !== null,
+ (item) => getContentWarningType(item, config.showEnvironmentWarnings) !== null,
)
) {
options.push({ id: 'warnings', label: formatMessage(messages.warnings) })
@@ -168,7 +181,7 @@ export function useContentFilters(items: Ref, config?: ContentFil
if (filter === 'disabled' && item.enabled) return false
if (
filter === 'warnings' &&
- getClientWarningType(item, config?.showEnvironmentWarnings) === null
+ getContentWarningType(item, config?.showEnvironmentWarnings) === null
)
return false
}
diff --git a/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts b/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts
index d05eb079b80..edcea50ea84 100644
--- a/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts
+++ b/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts
@@ -13,7 +13,7 @@ import {
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import type { ContentItem } from '../types'
-import { getClientWarningType } from './content-filtering'
+import { getContentWarningType } from './content-filtering'
export type ContentMetadataFilterValue = Record
@@ -127,6 +127,10 @@ const messages = defineMessages({
id: 'content.metadata-filter.warning.none',
defaultMessage: 'No warnings',
},
+ unknownEnvironment: {
+ id: 'content.metadata-filter.warning.unknown-environment',
+ defaultMessage: 'Unknown environment',
+ },
external: {
id: 'content.metadata-filter.source.external',
defaultMessage: 'External',
@@ -213,7 +217,7 @@ export function useContentMetadataFilters(
key: 'warnings',
label: formatMessage(messages.warnings),
values: (item) => {
- const warning = getClientWarningType(item, config?.showEnvironmentWarnings)
+ const warning = getContentWarningType(item, config?.showEnvironmentWarnings)
switch (warning) {
case 'retained':
return [option(warning, formatMessage(messages.clientRetained))]
@@ -221,6 +225,8 @@ export function useContentMetadataFilters(
return [option(warning, formatMessage(messages.clientDepends))]
case 'environment':
return [option(warning, formatMessage(messages.clientOnly))]
+ case 'unknown-environment':
+ return [option(warning, formatMessage(messages.unknownEnvironment))]
default:
return [option('none', formatMessage(messages.noWarnings))]
}
diff --git a/packages/ui/src/layouts/shared/content-tab/types.ts b/packages/ui/src/layouts/shared/content-tab/types.ts
index 16e1ea6b704..7821cfcbb25 100644
--- a/packages/ui/src/layouts/shared/content-tab/types.ts
+++ b/packages/ui/src/layouts/shared/content-tab/types.ts
@@ -56,6 +56,7 @@ export interface ContentEnabledForState {
server: boolean
player: boolean
locked: boolean
+ lockedTooltip?: string
disabledSides?: ContentSide[]
warningTooltip?: string | null
}
diff --git a/packages/ui/src/layouts/shared/invited-players/composables/use-invited-players-table.ts b/packages/ui/src/layouts/shared/invited-players/composables/use-invited-players-table.ts
index 863cbf903ed..6f5bb500b3b 100644
--- a/packages/ui/src/layouts/shared/invited-players/composables/use-invited-players-table.ts
+++ b/packages/ui/src/layouts/shared/invited-players/composables/use-invited-players-table.ts
@@ -1,10 +1,10 @@
-import { computed, ref, type Ref, watch } from 'vue'
+import { computed, type Ref, ref, watch } from 'vue'
import type { SortDirection } from '#ui/components/base/Table.vue'
import {
- invitedPlayerMethodLabels as methodLabels,
type InvitedPlayerMethod,
+ invitedPlayerMethodLabels as methodLabels,
type InvitedPlayerRow,
} from '../types'
@@ -49,7 +49,9 @@ export function useInvitedPlayersTable(
compared = methodLabels[a.method].localeCompare(methodLabels[b.method])
else
compared =
- (a.pending ? Number.MAX_SAFE_INTEGER : (a.joinedAt?.getTime() ?? Number.NEGATIVE_INFINITY)) -
+ (a.pending
+ ? Number.MAX_SAFE_INTEGER
+ : (a.joinedAt?.getTime() ?? Number.NEGATIVE_INFINITY)) -
(b.pending
? Number.MAX_SAFE_INTEGER
: (b.joinedAt?.getTime() ?? Number.NEGATIVE_INFINITY)) ||
diff --git a/packages/ui/src/layouts/shared/invited-players/layout.vue b/packages/ui/src/layouts/shared/invited-players/layout.vue
index f1c23b8a8cf..675980711a2 100644
--- a/packages/ui/src/layouts/shared/invited-players/layout.vue
+++ b/packages/ui/src/layouts/shared/invited-players/layout.vue
@@ -10,7 +10,32 @@
size="medium"
clearable
/>
-
+
+
+
+
+ {{ formatMessage(messages.pushUpdate) }}
+
+
+
+
+ {{ inviteLabel ?? formatMessage(messages.invitePlayers) }}
+
+
@@ -97,8 +122,8 @@
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/share-diff.ts b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/share-diff.ts
index 6b0847ec711..41c5ea465d8 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/share-diff.ts
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/share-diff.ts
@@ -3,27 +3,52 @@ import type { Archon } from '@modrinth/api-client'
import type { ContentDiffItem } from '#ui/layouts/shared/installation-settings/types'
type Change = Archon.Content.v1.SharedContentChange
-const before = (change: Change) => 'before' in change ? change.before : undefined
-const after = (change: Change) => 'after' in change ? change.after : undefined
+const before = (change: Change) => ('before' in change ? change.before : undefined)
+const after = (change: Change) => ('after' in change ? change.after : undefined)
-export function resolveServerShareDiff(diff: Archon.Content.v1.SharedInstancePublishDiff): ContentDiffItem[] {
+export function resolveServerShareDiff(
+ diff: Archon.Content.v1.SharedInstancePublishDiff,
+): ContentDiffItem[] {
const versionName = (id?: string) => {
if (!id) return undefined
const version = diff.versions?.[id]
return version?.version_number ?? version?.name ?? id
}
return diff.diffs.map((entry): ContentDiffItem => {
- if (entry.type === 'external_file') return { type: entry.kind, fileName: encodeURIComponent(entry.file_name) }
+ if (entry.type === 'external_file')
+ return { type: entry.kind, fileName: encodeURIComponent(entry.file_name) }
if (entry.type === 'loader') {
- const label = (value?: { name: string; version: string | null }) => value ? [value.name, value.version].filter(Boolean).join(' ') : undefined
- return { type: 'loader_updated', currentVersionName: label(before(entry.change)), newVersionName: label(after(entry.change)) }
+ const label = (value?: { name: string; version: string | null }) =>
+ value ? [value.name, value.version].filter(Boolean).join(' ') : undefined
+ return {
+ type: 'loader_updated',
+ currentVersionName: label(before(entry.change)),
+ newVersionName: label(after(entry.change)),
+ }
}
- if (entry.type === 'game_version') return { type: 'game_version_updated', currentVersionName: before(entry.change), newVersionName: after(entry.change) }
+ if (entry.type === 'game_version')
+ return {
+ type: 'game_version_updated',
+ currentVersionName: before(entry.change),
+ newVersionName: after(entry.change),
+ }
const versionId = after(entry.change) ?? before(entry.change)
- const projectId = entry.type === 'project' ? entry.project_id : versionId ? diff.versions?.[versionId]?.project_id : undefined
+ const projectId =
+ entry.type === 'project'
+ ? entry.project_id
+ : versionId
+ ? diff.versions?.[versionId]?.project_id
+ : undefined
return {
- type: entry.type === 'project' ? entry.change.kind : entry.change.kind === 'added' ? 'modpack_linked' : entry.change.kind === 'removed' ? 'modpack_unlinked' : 'modpack_updated',
- projectName: (projectId ? diff.projects?.[projectId]?.name ?? projectId : versionId),
+ type:
+ entry.type === 'project'
+ ? entry.change.kind
+ : entry.change.kind === 'added'
+ ? 'modpack_linked'
+ : entry.change.kind === 'removed'
+ ? 'modpack_unlinked'
+ : 'modpack_updated',
+ projectName: projectId ? (diff.projects?.[projectId]?.name ?? projectId) : versionId,
currentVersionName: versionName(before(entry.change)),
newVersionName: versionName(after(entry.change)),
}
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
index 00d0300fdef..6cc82ba83eb 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
@@ -2,8 +2,8 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, type Ref } from 'vue'
import type { InviteLinkSettings, InvitePlayersUser } from '#ui/components/sharing'
-import { injectAuth, injectModrinthClient } from '#ui/providers'
import { sharedInstanceInvitesQueryOptions } from '#ui/layouts/shared/server-sharing'
+import { injectAuth, injectModrinthClient } from '#ui/providers'
import type { ServerPlayerRow } from './types'
@@ -21,18 +21,20 @@ export function useServerPlayers(instanceId: Ref, canManage: Ref<
const users = response.users.length
? await client.labrinth.users_v2.getMultiple(response.users.map((user) => user.id))
: []
- const rows: ServerPlayerRow[] = response.users.filter((user) => user.join_type !== 'owner').map((member) => {
- const user = users.find((user) => user.id === member.id)
- return {
- id: member.id,
- username: user?.username ?? member.id,
- avatarUrl: user?.avatar_url ?? undefined,
- joinedAt: member.joined_at ? new Date(member.joined_at) : null,
- lastPlayedAt: member.last_played ? new Date(member.last_played) : null,
- pending: !member.joined_at,
- method: member.join_type === 'link' ? 'link' : 'direct',
- }
- })
+ const rows: ServerPlayerRow[] = response.users
+ .filter((user) => user.join_type !== 'owner')
+ .map((member) => {
+ const user = users.find((user) => user.id === member.id)
+ return {
+ id: member.id,
+ username: user?.username ?? member.id,
+ avatarUrl: user?.avatar_url ?? undefined,
+ joinedAt: member.joined_at ? new Date(member.joined_at) : null,
+ lastPlayedAt: member.last_played ? new Date(member.last_played) : null,
+ pending: !member.joined_at,
+ method: member.join_type === 'link' ? 'link' : 'direct',
+ }
+ })
return { rows, remaining: Math.max(0, 50 - response.users.length - response.tokens) }
},
refetchInterval: 30_000,
@@ -45,38 +47,62 @@ export function useServerPlayers(instanceId: Ref, canManage: Ref<
queryFn: async () => {
const currentUserId = userId.value
const relationships = await client.labrinth.friends_v3.list()
- const ids = relationships.filter((friend) => friend.accepted)
- .map((friend) => friend.id === currentUserId ? friend.friend_id : friend.id)
+ const ids = relationships
+ .filter((friend) => friend.accepted)
+ .map((friend) => (friend.id === currentUserId ? friend.friend_id : friend.id))
return ids.length ? client.labrinth.users_v2.getMultiple(ids) : []
},
})
const candidates = computed(() => {
const candidates = new Map()
for (const friend of friends.data.value ?? []) {
- candidates.set(friend.id, { id: friend.id, username: friend.username, avatarUrl: friend.avatar_url, status: 'available' })
+ candidates.set(friend.id, {
+ id: friend.id,
+ username: friend.username,
+ avatarUrl: friend.avatar_url,
+ status: 'available',
+ })
}
for (const row of rows.value) {
candidates.set(row.id, { ...row, status: row.pending ? 'pending' : 'added' })
}
return [...candidates.values()]
})
- const links = useQuery(computed(() => ({
- ...sharedInstanceInvitesQueryOptions(client, instanceId.value ?? '', userId.value),
- enabled: !!instanceId.value && !!userId.value && canManage.value,
- })))
- const link = computed(() => links.data.value?.find((link) => new Date(link.expiration).getTime() > Date.now() && link.uses < link.max_uses))
+ const links = useQuery(
+ computed(() => ({
+ ...sharedInstanceInvitesQueryOptions(client, instanceId.value ?? '', userId.value),
+ enabled: !!instanceId.value && !!userId.value && canManage.value,
+ })),
+ )
+ const link = computed(() =>
+ links.data.value?.find(
+ (link) => new Date(link.expiration).getTime() > Date.now() && link.uses < link.max_uses,
+ ),
+ )
const membershipMutation = useMutation({
- mutationFn: async ({ id, userId, remove }: { id: string; userId: string; remove: boolean; user?: InvitePlayersUser }) => {
+ mutationFn: async ({
+ id,
+ userId,
+ remove,
+ }: {
+ id: string
+ userId: string
+ remove: boolean
+ user?: InvitePlayersUser
+ }) => {
if (!canManage.value) throw new Error('You do not have permission to manage players.')
if (remove) return client.sharedinstances.instances_v1.removeUsers(id, [userId])
const result = await client.sharedinstances.instances_v1.inviteUsers(id, [userId])
- if (result.failed.includes(userId)) throw new Error('This player’s privacy settings do not allow this invitation.')
+ if (result.failed.includes(userId))
+ throw new Error('This player’s privacy settings do not allow this invitation.')
},
onMutate: async ({ id, userId, remove, user }) => {
const queryKey = memberKey(id)
await queryClient.cancelQueries({ queryKey, exact: true })
- const previous = queryClient.getQueryData<{ rows: ServerPlayerRow[]; remaining: number }>(queryKey)
+ const previous = queryClient.getQueryData<{ rows: ServerPlayerRow[]; remaining: number }>(
+ queryKey,
+ )
if (previous) {
const exists = previous.rows.some((row) => row.id === userId)
const addedRow: ServerPlayerRow = {
@@ -91,7 +117,9 @@ export function useServerPlayers(instanceId: Ref, canManage: Ref<
queryClient.setQueryData(queryKey, {
rows: remove
? previous.rows.filter((row) => row.id !== userId)
- : exists ? previous.rows : [...previous.rows, addedRow],
+ : exists
+ ? previous.rows
+ : [...previous.rows, addedRow],
remaining: Math.max(0, previous.remaining + (remove ? Number(exists) : -Number(!exists))),
})
}
@@ -100,13 +128,25 @@ export function useServerPlayers(instanceId: Ref, canManage: Ref<
onError: (_error, _variables, context) => {
if (context?.previous) queryClient.setQueryData(context.queryKey, context.previous)
},
- onSettled: (_data, _error, { id }) => queryClient.invalidateQueries({ queryKey: ['shared-instances', id, 'players'] }),
+ onSettled: (_data, _error, { id }) =>
+ queryClient.invalidateQueries({ queryKey: ['shared-instances', id, 'players'] }),
})
const linkMutation = useMutation({
- mutationFn: async ({ id, settings, replaceId }: { id: string; settings: InviteLinkSettings; replaceId?: string }) => {
+ mutationFn: async ({
+ id,
+ settings,
+ replaceId,
+ }: {
+ id: string
+ settings: InviteLinkSettings
+ replaceId?: string
+ }) => {
if (!canManage.value) throw new Error('You do not have permission to manage invite links.')
const created = await client.sharedinstances.invites_v1.create(id, {
- max_age: Math.max(1, Math.min(604800, Math.floor((settings.expiresAt.getTime() - Date.now()) / 1000))),
+ max_age: Math.max(
+ 1,
+ Math.min(604800, Math.floor((settings.expiresAt.getTime() - Date.now()) / 1000)),
+ ),
max_uses: Math.max(1, Math.min(settings.maxUses, remaining.value)),
})
if (replaceId) {
@@ -118,14 +158,29 @@ export function useServerPlayers(instanceId: Ref, canManage: Ref<
}
}
},
- onSettled: (_data, _error, { id }) => queryClient.invalidateQueries({ queryKey: ['shared-instances', id, 'invites'] }),
+ onSettled: (_data, _error, { id }) =>
+ queryClient.invalidateQueries({ queryKey: ['shared-instances', id, 'invites'] }),
})
async function ensureLink(id: string) {
- const available = await queryClient.fetchQuery({ ...sharedInstanceInvitesQueryOptions(client, id, userId.value), staleTime: 0 })
- if (available.some((link) => new Date(link.expiration).getTime() > Date.now() && link.uses < link.max_uses)) return
+ const available = await queryClient.fetchQuery({
+ ...sharedInstanceInvitesQueryOptions(client, id, userId.value),
+ staleTime: 0,
+ })
+ if (
+ available.some(
+ (link) => new Date(link.expiration).getTime() > Date.now() && link.uses < link.max_uses,
+ )
+ )
+ return
if (remaining.value <= 0) return
- await linkMutation.mutateAsync({ id, settings: { maxUses: Math.min(10, remaining.value), expiresAt: new Date(Date.now() + 86400_000) } })
+ await linkMutation.mutateAsync({
+ id,
+ settings: {
+ maxUses: Math.min(10, remaining.value),
+ expiresAt: new Date(Date.now() + 86400_000),
+ },
+ })
}
async function search(query: string) {
@@ -134,9 +189,26 @@ export function useServerPlayers(instanceId: Ref, canManage: Ref<
queryFn: () => client.labrinth.users_v3.search(query),
staleTime: 30_000,
})
- return users.filter((user) => user.id !== userId.value && !candidates.value.some((candidate) => candidate.id === user.id))
+ return users
+ .filter(
+ (user) =>
+ user.id !== userId.value &&
+ !candidates.value.some((candidate) => candidate.id === user.id),
+ )
.map((user) => ({ id: user.id, username: user.username, avatarUrl: user.avatar_url }))
}
- return { members, rows, remaining, candidates, link, links, friends, membershipMutation, linkMutation, ensureLink, search }
+ return {
+ members,
+ rows,
+ remaining,
+ candidates,
+ link,
+ links,
+ friends,
+ membershipMutation,
+ linkMutation,
+ ensureLink,
+ search,
+ }
}
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
index 6ec05af81ef..c86019b76cd 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
@@ -1,6 +1,6 @@
diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
index 08a38104494..356f92457a5 100644
--- a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
+++ b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
@@ -95,6 +95,8 @@ interface Props {
hideDelete?: boolean
hideActions?: boolean
inline?: boolean
+ tableLayout?: 'wide' | 'compact' | 'stacked' | 'narrow'
+ enabledForColumn?: boolean
enabledFor?: ContentEnabledForState
embeddedIcon?: ContentCardEmbeddedIcon
}
@@ -127,10 +129,15 @@ const props = withDefaults(defineProps(), {
hideDelete: false,
hideActions: false,
inline: false,
+ tableLayout: undefined,
+ enabledForColumn: false,
enabledFor: undefined,
embeddedIcon: undefined,
})
+const stacked = computed(() => props.tableLayout === 'stacked' || props.tableLayout === 'narrow')
+const separateVersion = computed(() => props.tableLayout === 'wide')
+
const selected = defineModel('selected')
const projectTitle = computed(() => props.project.title.replace(/§[0-9a-fk-orx]/gi, ''))
@@ -193,26 +200,20 @@ const installTooltip = computed(() => {
{
: undefined
"
:to="owner.link"
- class="flex shrink-0 items-center gap-1 !decoration-secondary"
+ class="flex min-w-0 items-center gap-1 !decoration-secondary"
:class="{ 'hover:underline': owner.link }"
>
{
no-shadow
class="shrink-0"
/>
- {{ owner.name }}
+ {{ owner.name }}
@@ -334,7 +335,7 @@ const installTooltip = computed(() => {
@@ -363,11 +364,10 @@ const installTooltip = computed(() => {
-
+
{
@@ -393,7 +393,7 @@ const installTooltip = computed(() => {
class="inline-flex min-w-0 font-semibold leading-6 text-contrast !decoration-contrast"
:class="{ 'hover:underline': versionLink, 'cursor-pointer': versionLink }"
>
-
{{
+ {{
version.version_number.slice(0, Math.ceil(version.version_number.length / 2))
}}{{
@@ -404,7 +404,7 @@ const installTooltip = computed(() => {
v-tooltip="truncatedTooltip(fileNameRef, version.file_name)"
class="flex min-w-0 leading-6 text-secondary"
>
- {{
+ {{
version.file_name.slice(0, Math.ceil(version.file_name.length / 2))
}}{{
@@ -417,7 +417,11 @@ const installTooltip = computed(() => {
{
(hasUpdateListener && hasUpdate) ||
(hasSwitchVersionListener && version && !hideSwitchVersion)
"
- class="flex w-8 items-center justify-center"
+ class="flex w-9 shrink-0 items-center justify-center"
>
{
number[]
}
const props = withDefaults(defineProps(), {
@@ -58,6 +60,7 @@ const props = withDefaults(defineProps(), {
showItemActions: false,
showVersion: true,
showEnabledForColumn: false,
+ getAdditionalActionWidths: () => [],
})
const stickyHeaderRef = ref(null)
@@ -88,16 +91,10 @@ const hasEnabledForColumn = computed(
() => props.showEnabledForColumn || props.items.some((item) => item.enabledFor !== undefined),
)
-const enabledForColumnWidth = computed(() => {
- const iconCount = props.items.reduce((maximum, item) => {
- const state = item.enabledFor
- const count =
- Number(Boolean(state?.locked && state.disabledSides?.length)) +
- Number(Boolean(state?.warningTooltip))
- return Math.max(maximum, count)
- }, 0)
- return `${172 + iconCount * 26}px`
-})
+const enabledForMeasureRef = ref(null)
+const actionsLabelRef = ref(null)
+const { width: enabledForWidth } = useElementSize(enabledForMeasureRef)
+const { width: actionsLabelWidth } = useElementSize(actionsLabelRef)
const hasAnyActions = computed(() => {
// Check if there are listeners for actions
@@ -113,34 +110,86 @@ const hasAnyActions = computed(() => {
(item) =>
(item.overflowOptions && item.overflowOptions.length > 0) ||
item.hasUpdate ||
+ item.synced ||
+ item.locked ||
+ props.getAdditionalActionWidths(item).length > 0 ||
(item.enabled !== undefined && !item.hideToggle),
)
return hasListeners || hasItemActions || props.showItemActions
})
-const contentColumnStyles = computed(() => {
- const columns = ['minmax(0, 1.2fr)', enabledForColumnWidth.value]
- const actions = hasAnyActions.value ? ['168px'] : []
+const actionsWidth = computed(() =>
+ props.items.reduce((maximum, item) => {
+ const widths = props.getAdditionalActionWidths(item).slice()
+ if (item.synced) widths.push(42)
+ if (
+ item.locked ||
+ (hasUpdateListener.value && item.hasUpdate) ||
+ (hasSwitchVersionListener.value && item.version && !item.hideSwitchVersion)
+ ) {
+ widths.push(36)
+ }
+ if (item.enabled !== undefined && !item.hideToggle) widths.push(48)
+ if (hasDeleteListener.value && !props.hideDelete && !item.hideDelete) widths.push(36)
+ if (item.overflowOptions?.length) widths.push(36)
+ const width = widths.reduce((sum, width) => sum + width, 0)
+ return Math.max(maximum, width + Math.max(0, widths.length - 1) * 8)
+ }, actionsLabelWidth.value),
+)
+
+function getItemListeners(id: string) {
return {
- '--enabled-for-column-width': enabledForColumnWidth.value,
- '--content-columns': [...columns, ...actions].join(' '),
- '--content-columns-wide': [
- 'minmax(0, 1fr)',
- `minmax(${enabledForColumnWidth.value}, 1fr)`,
- ...(props.showVersion ? ['minmax(0, 1fr)'] : []),
- ...actions,
- ].join(' '),
+ ...(hasDeleteListener.value ? { delete: (event: MouseEvent) => emit('delete', id, event) } : {}),
+ ...(hasUpdateListener.value ? { update: () => emit('update', id) } : {}),
+ ...(hasSwitchVersionListener.value ? { switchVersion: () => emit('switchVersion', id) } : {}),
}
-})
+}
-// Virtualization
const tableRef = ref(null)
const { width: tableWidth } = useElementSize(tableRef)
+const controlColumns = computed(() => [
+ ...(hasEnabledForColumn.value ? [Math.ceil(enabledForWidth.value)] : []),
+ ...(hasAnyActions.value ? [Math.ceil(actionsWidth.value)] : []),
+])
+const layout = computed(() => {
+ const controls = controlColumns.value
+ const controlBudget = controls.reduce((sum, width) => sum + width, 0)
+ const compactBudget = 24 + controlBudget + controls.length * 16 + 240
+ const wideBudget = compactBudget + (props.showVersion ? 288 + 16 : 0)
+ if (tableWidth.value >= wideBudget) return 'wide'
+ if (tableWidth.value >= compactBudget) return 'compact'
+ if (tableWidth.value >= 24 + controlBudget + Math.max(0, controls.length - 1) * 16) {
+ return 'stacked'
+ }
+ return 'narrow'
+})
+const separateVersion = computed(() => props.showVersion && layout.value === 'wide')
+const stacked = computed(() => layout.value === 'stacked' || layout.value === 'narrow')
+const contentColumnStyles = computed(() => {
+ const controls = controlColumns.value.map((width) => `${width}px`)
+ let columns: string[]
+ if (layout.value === 'narrow') {
+ columns = ['minmax(0, 1fr)']
+ } else if (stacked.value) {
+ columns = controls.length > 1 ? [controls[0], 'minmax(0, 1fr)'] : ['minmax(0, 1fr)']
+ } else {
+ columns = [
+ 'minmax(0, 1.2fr)',
+ ...(hasEnabledForColumn.value ? [controls[0]] : []),
+ ...(separateVersion.value ? ['minmax(0, 1fr)'] : []),
+ ...(hasAnyActions.value ? [`${Math.ceil(actionsWidth.value)}px`] : []),
+ ]
+ }
+ return {
+ '--content-columns': columns.join(' '),
+ '--content-row-height': `${itemHeight.value}px`,
+ }
+})
const itemHeight = computed(() => {
- if (!hasEnabledForColumn.value) return 74
- if (tableWidth.value < 450) return 152
- return tableWidth.value < 600 ? 112 : 72
+ if (!stacked.value) return 74
+ const controlRows = layout.value === 'narrow' ? controlColumns.value.length : Math.min(1, controlColumns.value.length)
+ return 25 + 48 + controlRows * 44
})
const { listContainer, totalHeight, visibleRange, visibleTop, visibleItems, scrollToIndex } =
useVirtualScroll(toRef(props, 'items'), {
@@ -236,19 +285,36 @@ function handleSort(column: ContentCardTableSortColumn) {
+
+
+
+ {{ formatMessage(messages.enabledFor) }}
+
+
+
+
+
+ {{ formatMessage(commonMessages.actionsLabel) }}
+
+
{{ formatMessage(messages.enabledFor) }}
-
+
{{
formatMessage(commonMessages.actionsLabel)
@@ -369,6 +423,8 @@ function handleSort(column: ContentCardTableSortColumn) {
:project-link="item.projectLink"
:version="item.version"
:show-version="showVersion"
+ :table-layout="layout"
+ :enabled-for-column="hasEnabledForColumn"
:version-link="item.versionLink"
:owner="item.owner"
:source="item.source"
@@ -413,11 +469,7 @@ function handleSort(column: ContentCardTableSortColumn) {
"
@update:enabled="(val) => emit('update:enabled', item.id, val)"
@update:enabled-for="(side, val) => emit('update:enabled-for', item.id, side, val)"
- @delete="(e: MouseEvent) => emit('delete', item.id, e)"
- @update="emit('update', item.id)"
- v-on="
- hasSwitchVersionListener ? { switchVersion: () => emit('switchVersion', item.id) } : {}
- "
+ v-on="getItemListeners(item.id)"
>
@@ -446,6 +498,8 @@ function handleSort(column: ContentCardTableSortColumn) {
:project-link="item.projectLink"
:version="item.version"
:show-version="showVersion"
+ :table-layout="layout"
+ :enabled-for-column="hasEnabledForColumn"
:version-link="item.versionLink"
:owner="item.owner"
:source="item.source"
@@ -485,9 +539,7 @@ function handleSort(column: ContentCardTableSortColumn) {
@select="(val, event) => toggleItemSelection(item.id, val ?? false, index, event)"
@update:enabled="(val) => emit('update:enabled', item.id, val)"
@update:enabled-for="(side, val) => emit('update:enabled-for', item.id, side, val)"
- @delete="(e: MouseEvent) => emit('delete', item.id, e)"
- @update="emit('update', item.id)"
- @switch-version="emit('switchVersion', item.id)"
+ v-on="getItemListeners(item.id)"
>
diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentEnabledFor.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentEnabledFor.vue
index 7ee9cfbb5f4..a511d35f17d 100644
--- a/packages/ui/src/layouts/shared/content-tab/components/ContentEnabledFor.vue
+++ b/packages/ui/src/layouts/shared/content-tab/components/ContentEnabledFor.vue
@@ -40,10 +40,12 @@ const messages = defineMessages({
const props = withDefaults(
defineProps<{
modelValue: ContentEnabledForState
+ reserveStatusSpace?: boolean
disabled?: boolean
disabledTooltip?: string | null
}>(),
{
+ reserveStatusSpace: false,
disabled: false,
disabledTooltip: undefined,
},
@@ -106,7 +108,8 @@ function toggle(side: ContentSide) {
-
+
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
index 32d88437725..b8acc8a483e 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
@@ -111,7 +111,6 @@
-
-
-
-
-
-
-
- {{ formatMessage(messages.background) }}
-
-
-
-
-
-
- {{ formatMessage(messages.symbol) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ formatMessage(messages.description) }}
-
-
-
-
- {{ formatMessage(commonMessages.cancelButton) }}
-
-
-
-
- {{ formatMessage(messages.saveIcon) }}
-
-
-
-
-
+
-
-
diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts
index a17a0acadc2..8e44342631f 100644
--- a/apps/app-frontend/src/helpers/types.d.ts
+++ b/apps/app-frontend/src/helpers/types.d.ts
@@ -1,5 +1,8 @@
+import type { IconConfig } from '@modrinth/ui'
import type { ModrinthId } from '@modrinth/utils'
+export type { IconBackground } from '@modrinth/ui'
+
export type GameInstance = {
id: string
path: string
@@ -53,21 +56,7 @@ export type GameInstance = {
}
}
-export type IconBackground =
- | {
- type: 'color'
- value: string
- }
- | {
- type: 'linear-top-down-gradient'
- top_color: string
- bottom_color: string
- }
-
-export type InstanceIconConfig = {
- background: IconBackground
- symbol: string
-}
+export type InstanceIconConfig = IconConfig
type InstallStage =
| 'installed'
diff --git a/apps/app-frontend/src/assets/instance-icons/backpack.png b/packages/assets/illustrations/instance-icons/backpack.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/backpack.png
rename to packages/assets/illustrations/instance-icons/backpack.png
diff --git a/apps/app-frontend/src/assets/instance-icons/beacon.png b/packages/assets/illustrations/instance-icons/beacon.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/beacon.png
rename to packages/assets/illustrations/instance-icons/beacon.png
diff --git a/apps/app-frontend/src/assets/instance-icons/blue-shark.png b/packages/assets/illustrations/instance-icons/blue-shark.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/blue-shark.png
rename to packages/assets/illustrations/instance-icons/blue-shark.png
diff --git a/apps/app-frontend/src/assets/instance-icons/bookshelf.png b/packages/assets/illustrations/instance-icons/bookshelf.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/bookshelf.png
rename to packages/assets/illustrations/instance-icons/bookshelf.png
diff --git a/apps/app-frontend/src/assets/instance-icons/brown-bear.png b/packages/assets/illustrations/instance-icons/brown-bear.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/brown-bear.png
rename to packages/assets/illustrations/instance-icons/brown-bear.png
diff --git a/apps/app-frontend/src/assets/instance-icons/cake.png b/packages/assets/illustrations/instance-icons/cake.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/cake.png
rename to packages/assets/illustrations/instance-icons/cake.png
diff --git a/apps/app-frontend/src/assets/instance-icons/campfire.png b/packages/assets/illustrations/instance-icons/campfire.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/campfire.png
rename to packages/assets/illustrations/instance-icons/campfire.png
diff --git a/apps/app-frontend/src/assets/instance-icons/chest.png b/packages/assets/illustrations/instance-icons/chest.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/chest.png
rename to packages/assets/illustrations/instance-icons/chest.png
diff --git a/apps/app-frontend/src/assets/instance-icons/cogwheel.png b/packages/assets/illustrations/instance-icons/cogwheel.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/cogwheel.png
rename to packages/assets/illustrations/instance-icons/cogwheel.png
diff --git a/apps/app-frontend/src/assets/instance-icons/command-block.png b/packages/assets/illustrations/instance-icons/command-block.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/command-block.png
rename to packages/assets/illustrations/instance-icons/command-block.png
diff --git a/apps/app-frontend/src/assets/instance-icons/cooking-pot.png b/packages/assets/illustrations/instance-icons/cooking-pot.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/cooking-pot.png
rename to packages/assets/illustrations/instance-icons/cooking-pot.png
diff --git a/apps/app-frontend/src/assets/instance-icons/couch.png b/packages/assets/illustrations/instance-icons/couch.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/couch.png
rename to packages/assets/illustrations/instance-icons/couch.png
diff --git a/apps/app-frontend/src/assets/instance-icons/crafting-table.png b/packages/assets/illustrations/instance-icons/crafting-table.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/crafting-table.png
rename to packages/assets/illustrations/instance-icons/crafting-table.png
diff --git a/apps/app-frontend/src/assets/instance-icons/creeper.png b/packages/assets/illustrations/instance-icons/creeper.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/creeper.png
rename to packages/assets/illustrations/instance-icons/creeper.png
diff --git a/apps/app-frontend/src/assets/instance-icons/enchanting-table.png b/packages/assets/illustrations/instance-icons/enchanting-table.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/enchanting-table.png
rename to packages/assets/illustrations/instance-icons/enchanting-table.png
diff --git a/apps/app-frontend/src/assets/instance-icons/ender-chest.png b/packages/assets/illustrations/instance-icons/ender-chest.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/ender-chest.png
rename to packages/assets/illustrations/instance-icons/ender-chest.png
diff --git a/apps/app-frontend/src/assets/instance-icons/ender-dragon.png b/packages/assets/illustrations/instance-icons/ender-dragon.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/ender-dragon.png
rename to packages/assets/illustrations/instance-icons/ender-dragon.png
diff --git a/apps/app-frontend/src/assets/instance-icons/engine.png b/packages/assets/illustrations/instance-icons/engine.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/engine.png
rename to packages/assets/illustrations/instance-icons/engine.png
diff --git a/apps/app-frontend/src/assets/instance-icons/fabric.png b/packages/assets/illustrations/instance-icons/fabric.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/fabric.png
rename to packages/assets/illustrations/instance-icons/fabric.png
diff --git a/apps/app-frontend/src/assets/instance-icons/forge.png b/packages/assets/illustrations/instance-icons/forge.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/forge.png
rename to packages/assets/illustrations/instance-icons/forge.png
diff --git a/apps/app-frontend/src/assets/instance-icons/furnace.png b/packages/assets/illustrations/instance-icons/furnace.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/furnace.png
rename to packages/assets/illustrations/instance-icons/furnace.png
diff --git a/apps/app-frontend/src/assets/instance-icons/gizmo.png b/packages/assets/illustrations/instance-icons/gizmo.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/gizmo.png
rename to packages/assets/illustrations/instance-icons/gizmo.png
diff --git a/apps/app-frontend/src/assets/instance-icons/globe.png b/packages/assets/illustrations/instance-icons/globe.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/globe.png
rename to packages/assets/illustrations/instance-icons/globe.png
diff --git a/apps/app-frontend/src/assets/instance-icons/grass-block.png b/packages/assets/illustrations/instance-icons/grass-block.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/grass-block.png
rename to packages/assets/illustrations/instance-icons/grass-block.png
diff --git a/apps/app-frontend/src/assets/instance-icons/lantern.png b/packages/assets/illustrations/instance-icons/lantern.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/lantern.png
rename to packages/assets/illustrations/instance-icons/lantern.png
diff --git a/apps/app-frontend/src/assets/instance-icons/moobloom.png b/packages/assets/illustrations/instance-icons/moobloom.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/moobloom.png
rename to packages/assets/illustrations/instance-icons/moobloom.png
diff --git a/apps/app-frontend/src/assets/instance-icons/mr-pack.png b/packages/assets/illustrations/instance-icons/mr-pack.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/mr-pack.png
rename to packages/assets/illustrations/instance-icons/mr-pack.png
diff --git a/apps/app-frontend/src/assets/instance-icons/neoforge.png b/packages/assets/illustrations/instance-icons/neoforge.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/neoforge.png
rename to packages/assets/illustrations/instance-icons/neoforge.png
diff --git a/apps/app-frontend/src/assets/instance-icons/orb.png b/packages/assets/illustrations/instance-icons/orb.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/orb.png
rename to packages/assets/illustrations/instance-icons/orb.png
diff --git a/apps/app-frontend/src/assets/instance-icons/oxygen-distributor.png b/packages/assets/illustrations/instance-icons/oxygen-distributor.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/oxygen-distributor.png
rename to packages/assets/illustrations/instance-icons/oxygen-distributor.png
diff --git a/apps/app-frontend/src/assets/instance-icons/pancakes.png b/packages/assets/illustrations/instance-icons/pancakes.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/pancakes.png
rename to packages/assets/illustrations/instance-icons/pancakes.png
diff --git a/apps/app-frontend/src/assets/instance-icons/pickaxe.png b/packages/assets/illustrations/instance-icons/pickaxe.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/pickaxe.png
rename to packages/assets/illustrations/instance-icons/pickaxe.png
diff --git a/apps/app-frontend/src/assets/instance-icons/poke-ball.png b/packages/assets/illustrations/instance-icons/poke-ball.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/poke-ball.png
rename to packages/assets/illustrations/instance-icons/poke-ball.png
diff --git a/apps/app-frontend/src/assets/instance-icons/quilt.png b/packages/assets/illustrations/instance-icons/quilt.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/quilt.png
rename to packages/assets/illustrations/instance-icons/quilt.png
diff --git a/apps/app-frontend/src/assets/instance-icons/redstone-block.png b/packages/assets/illustrations/instance-icons/redstone-block.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/redstone-block.png
rename to packages/assets/illustrations/instance-icons/redstone-block.png
diff --git a/apps/app-frontend/src/assets/instance-icons/sculk-sensor.png b/packages/assets/illustrations/instance-icons/sculk-sensor.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/sculk-sensor.png
rename to packages/assets/illustrations/instance-icons/sculk-sensor.png
diff --git a/apps/app-frontend/src/assets/instance-icons/skeleton.png b/packages/assets/illustrations/instance-icons/skeleton.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/skeleton.png
rename to packages/assets/illustrations/instance-icons/skeleton.png
diff --git a/apps/app-frontend/src/assets/instance-icons/skillet.png b/packages/assets/illustrations/instance-icons/skillet.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/skillet.png
rename to packages/assets/illustrations/instance-icons/skillet.png
diff --git a/apps/app-frontend/src/assets/instance-icons/slime-block.png b/packages/assets/illustrations/instance-icons/slime-block.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/slime-block.png
rename to packages/assets/illustrations/instance-icons/slime-block.png
diff --git a/apps/app-frontend/src/assets/instance-icons/space-helmet.png b/packages/assets/illustrations/instance-icons/space-helmet.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/space-helmet.png
rename to packages/assets/illustrations/instance-icons/space-helmet.png
diff --git a/apps/app-frontend/src/assets/instance-icons/sticky-piston.png b/packages/assets/illustrations/instance-icons/sticky-piston.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/sticky-piston.png
rename to packages/assets/illustrations/instance-icons/sticky-piston.png
diff --git a/apps/app-frontend/src/assets/instance-icons/sword.png b/packages/assets/illustrations/instance-icons/sword.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/sword.png
rename to packages/assets/illustrations/instance-icons/sword.png
diff --git a/apps/app-frontend/src/assets/instance-icons/terminal.png b/packages/assets/illustrations/instance-icons/terminal.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/terminal.png
rename to packages/assets/illustrations/instance-icons/terminal.png
diff --git a/apps/app-frontend/src/assets/instance-icons/tiny-potato.png b/packages/assets/illustrations/instance-icons/tiny-potato.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/tiny-potato.png
rename to packages/assets/illustrations/instance-icons/tiny-potato.png
diff --git a/apps/app-frontend/src/assets/instance-icons/tire.png b/packages/assets/illustrations/instance-icons/tire.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/tire.png
rename to packages/assets/illustrations/instance-icons/tire.png
diff --git a/apps/app-frontend/src/assets/instance-icons/tnt.png b/packages/assets/illustrations/instance-icons/tnt.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/tnt.png
rename to packages/assets/illustrations/instance-icons/tnt.png
diff --git a/apps/app-frontend/src/assets/instance-icons/wrench-rinth.png b/packages/assets/illustrations/instance-icons/wrench-rinth.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/wrench-rinth.png
rename to packages/assets/illustrations/instance-icons/wrench-rinth.png
diff --git a/apps/app-frontend/src/assets/instance-icons/wrench.png b/packages/assets/illustrations/instance-icons/wrench.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/wrench.png
rename to packages/assets/illustrations/instance-icons/wrench.png
diff --git a/apps/app-frontend/src/assets/instance-icons/zombie.png b/packages/assets/illustrations/instance-icons/zombie.png
similarity index 100%
rename from apps/app-frontend/src/assets/instance-icons/zombie.png
rename to packages/assets/illustrations/instance-icons/zombie.png
diff --git a/apps/app-frontend/src/components/ui/instance_settings/icon-editor-modal/editor-catalog.ts b/packages/ui/src/components/base/icon-editor-modal/editor-catalog.ts
similarity index 77%
rename from apps/app-frontend/src/components/ui/instance_settings/icon-editor-modal/editor-catalog.ts
rename to packages/ui/src/components/base/icon-editor-modal/editor-catalog.ts
index 7fa45b51a56..6552853ff16 100644
--- a/apps/app-frontend/src/components/ui/instance_settings/icon-editor-modal/editor-catalog.ts
+++ b/packages/ui/src/components/base/icon-editor-modal/editor-catalog.ts
@@ -1,54 +1,54 @@
-import { defineMessages, type MessageDescriptor } from '@modrinth/ui'
+import { defineMessages, type MessageDescriptor } from '#ui/composables/i18n'
-import backpack from '@/assets/instance-icons/backpack.png'
-import beacon from '@/assets/instance-icons/beacon.png'
-import blueShark from '@/assets/instance-icons/blue-shark.png'
-import bookshelf from '@/assets/instance-icons/bookshelf.png'
-import brownBear from '@/assets/instance-icons/brown-bear.png'
-import cake from '@/assets/instance-icons/cake.png'
-import campfire from '@/assets/instance-icons/campfire.png'
-import chest from '@/assets/instance-icons/chest.png'
-import cogwheel from '@/assets/instance-icons/cogwheel.png'
-import commandBlock from '@/assets/instance-icons/command-block.png'
-import cookingPot from '@/assets/instance-icons/cooking-pot.png'
-import couch from '@/assets/instance-icons/couch.png'
-import craftingTable from '@/assets/instance-icons/crafting-table.png'
-import creeper from '@/assets/instance-icons/creeper.png'
-import enchantingTable from '@/assets/instance-icons/enchanting-table.png'
-import enderChest from '@/assets/instance-icons/ender-chest.png'
-import enderDragon from '@/assets/instance-icons/ender-dragon.png'
-import engine from '@/assets/instance-icons/engine.png'
-import fabric from '@/assets/instance-icons/fabric.png'
-import forge from '@/assets/instance-icons/forge.png'
-import furnace from '@/assets/instance-icons/furnace.png'
-import gizmo from '@/assets/instance-icons/gizmo.png'
-import globe from '@/assets/instance-icons/globe.png'
-import grassBlock from '@/assets/instance-icons/grass-block.png'
-import lantern from '@/assets/instance-icons/lantern.png'
-import moobloom from '@/assets/instance-icons/moobloom.png'
-import mrPack from '@/assets/instance-icons/mr-pack.png'
-import neoForge from '@/assets/instance-icons/neoforge.png'
-import orb from '@/assets/instance-icons/orb.png'
-import oxygenDistributor from '@/assets/instance-icons/oxygen-distributor.png'
-import pancakes from '@/assets/instance-icons/pancakes.png'
-import pickaxe from '@/assets/instance-icons/pickaxe.png'
-import pokeBall from '@/assets/instance-icons/poke-ball.png'
-import quilt from '@/assets/instance-icons/quilt.png'
-import redstoneBlock from '@/assets/instance-icons/redstone-block.png'
-import sculkSensor from '@/assets/instance-icons/sculk-sensor.png'
-import skeleton from '@/assets/instance-icons/skeleton.png'
-import skillet from '@/assets/instance-icons/skillet.png'
-import slimeBlock from '@/assets/instance-icons/slime-block.png'
-import spaceHelmet from '@/assets/instance-icons/space-helmet.png'
-import stickyPiston from '@/assets/instance-icons/sticky-piston.png'
-import sword from '@/assets/instance-icons/sword.png'
-import terminal from '@/assets/instance-icons/terminal.png'
-import tinyPotato from '@/assets/instance-icons/tiny-potato.png'
-import tire from '@/assets/instance-icons/tire.png'
-import tnt from '@/assets/instance-icons/tnt.png'
-import wrench from '@/assets/instance-icons/wrench.png'
-import wrenchRinth from '@/assets/instance-icons/wrench-rinth.png'
-import zombie from '@/assets/instance-icons/zombie.png'
+import backpack from '@modrinth/assets/illustrations/instance-icons/backpack.png'
+import beacon from '@modrinth/assets/illustrations/instance-icons/beacon.png'
+import blueShark from '@modrinth/assets/illustrations/instance-icons/blue-shark.png'
+import bookshelf from '@modrinth/assets/illustrations/instance-icons/bookshelf.png'
+import brownBear from '@modrinth/assets/illustrations/instance-icons/brown-bear.png'
+import cake from '@modrinth/assets/illustrations/instance-icons/cake.png'
+import campfire from '@modrinth/assets/illustrations/instance-icons/campfire.png'
+import chest from '@modrinth/assets/illustrations/instance-icons/chest.png'
+import cogwheel from '@modrinth/assets/illustrations/instance-icons/cogwheel.png'
+import commandBlock from '@modrinth/assets/illustrations/instance-icons/command-block.png'
+import cookingPot from '@modrinth/assets/illustrations/instance-icons/cooking-pot.png'
+import couch from '@modrinth/assets/illustrations/instance-icons/couch.png'
+import craftingTable from '@modrinth/assets/illustrations/instance-icons/crafting-table.png'
+import creeper from '@modrinth/assets/illustrations/instance-icons/creeper.png'
+import enchantingTable from '@modrinth/assets/illustrations/instance-icons/enchanting-table.png'
+import enderChest from '@modrinth/assets/illustrations/instance-icons/ender-chest.png'
+import enderDragon from '@modrinth/assets/illustrations/instance-icons/ender-dragon.png'
+import engine from '@modrinth/assets/illustrations/instance-icons/engine.png'
+import fabric from '@modrinth/assets/illustrations/instance-icons/fabric.png'
+import forge from '@modrinth/assets/illustrations/instance-icons/forge.png'
+import furnace from '@modrinth/assets/illustrations/instance-icons/furnace.png'
+import gizmo from '@modrinth/assets/illustrations/instance-icons/gizmo.png'
+import globe from '@modrinth/assets/illustrations/instance-icons/globe.png'
+import grassBlock from '@modrinth/assets/illustrations/instance-icons/grass-block.png'
+import lantern from '@modrinth/assets/illustrations/instance-icons/lantern.png'
+import moobloom from '@modrinth/assets/illustrations/instance-icons/moobloom.png'
+import mrPack from '@modrinth/assets/illustrations/instance-icons/mr-pack.png'
+import neoForge from '@modrinth/assets/illustrations/instance-icons/neoforge.png'
+import orb from '@modrinth/assets/illustrations/instance-icons/orb.png'
+import oxygenDistributor from '@modrinth/assets/illustrations/instance-icons/oxygen-distributor.png'
+import pancakes from '@modrinth/assets/illustrations/instance-icons/pancakes.png'
+import pickaxe from '@modrinth/assets/illustrations/instance-icons/pickaxe.png'
+import pokeBall from '@modrinth/assets/illustrations/instance-icons/poke-ball.png'
+import quilt from '@modrinth/assets/illustrations/instance-icons/quilt.png'
+import redstoneBlock from '@modrinth/assets/illustrations/instance-icons/redstone-block.png'
+import sculkSensor from '@modrinth/assets/illustrations/instance-icons/sculk-sensor.png'
+import skeleton from '@modrinth/assets/illustrations/instance-icons/skeleton.png'
+import skillet from '@modrinth/assets/illustrations/instance-icons/skillet.png'
+import slimeBlock from '@modrinth/assets/illustrations/instance-icons/slime-block.png'
+import spaceHelmet from '@modrinth/assets/illustrations/instance-icons/space-helmet.png'
+import stickyPiston from '@modrinth/assets/illustrations/instance-icons/sticky-piston.png'
+import sword from '@modrinth/assets/illustrations/instance-icons/sword.png'
+import terminal from '@modrinth/assets/illustrations/instance-icons/terminal.png'
+import tinyPotato from '@modrinth/assets/illustrations/instance-icons/tiny-potato.png'
+import tire from '@modrinth/assets/illustrations/instance-icons/tire.png'
+import tnt from '@modrinth/assets/illustrations/instance-icons/tnt.png'
+import wrench from '@modrinth/assets/illustrations/instance-icons/wrench.png'
+import wrenchRinth from '@modrinth/assets/illustrations/instance-icons/wrench-rinth.png'
+import zombie from '@modrinth/assets/illustrations/instance-icons/zombie.png'
const names = defineMessages({
yellow: {
@@ -513,3 +513,7 @@ export const RANDOM_CONFIG_BLACKLIST = [
export const DEFAULT_BACKGROUND_ID = 'purple' satisfies BackgroundId
export const DEFAULT_SYMBOL_ID = 'grass_block' satisfies SymbolId
+
+export function getIconSymbolAsset(symbol: string): string | undefined {
+ return symbolOptions.find((option) => option.id === symbol)?.asset
+}
diff --git a/packages/ui/src/components/base/icon-editor-modal/index.ts b/packages/ui/src/components/base/icon-editor-modal/index.ts
new file mode 100644
index 00000000000..c9f82ccea6e
--- /dev/null
+++ b/packages/ui/src/components/base/icon-editor-modal/index.ts
@@ -0,0 +1,4 @@
+export { default as IconEditorModal } from './index.vue'
+export type { IconBackground, IconConfig } from './types'
+export { getIconSymbolAsset } from './editor-catalog'
+export { renderIcon } from './render-icon'
diff --git a/packages/ui/src/components/base/icon-editor-modal/index.vue b/packages/ui/src/components/base/icon-editor-modal/index.vue
new file mode 100644
index 00000000000..5f8fb49a23f
--- /dev/null
+++ b/packages/ui/src/components/base/icon-editor-modal/index.vue
@@ -0,0 +1,498 @@
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.background) }}
+
+
+
+
+
+
+ {{ formatMessage(messages.symbol) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.description) }}
+
+
+
+
+ {{ formatMessage(commonMessages.cancelButton) }}
+
+
+
+
+ {{ formatMessage(messages.saveIcon) }}
+
+
+
+
+
+
+
+
diff --git a/packages/ui/src/components/base/icon-editor-modal/render-icon.ts b/packages/ui/src/components/base/icon-editor-modal/render-icon.ts
new file mode 100644
index 00000000000..18d83289e28
--- /dev/null
+++ b/packages/ui/src/components/base/icon-editor-modal/render-icon.ts
@@ -0,0 +1,33 @@
+import type { IconConfig } from './types'
+
+export async function renderIcon(config: IconConfig, symbolAsset: string): Promise {
+ const size = 256
+ const canvas = document.createElement('canvas')
+ canvas.width = size
+ canvas.height = size
+ const context = canvas.getContext('2d')
+ if (!context) throw new Error('Could not create an icon image.')
+
+ if (config.background.type === 'color') {
+ context.fillStyle = config.background.value
+ } else {
+ const gradient = context.createLinearGradient(0, 0, 0, size - 1)
+ gradient.addColorStop(0, config.background.top_color)
+ gradient.addColorStop(1, config.background.bottom_color)
+ context.fillStyle = gradient
+ }
+ context.fillRect(0, 0, size, size)
+
+ const symbol = new Image()
+ symbol.src = symbolAsset
+ await symbol.decode()
+ context.drawImage(symbol, 0, 0, size, size)
+
+ const blob = await new Promise((resolve, reject) => {
+ canvas.toBlob((result) => {
+ if (result) resolve(result)
+ else reject(new Error('Could not encode the icon image.'))
+ }, 'image/png')
+ })
+ return new File([blob], 'server-icon-original.png', { type: 'image/png' })
+}
diff --git a/packages/ui/src/components/base/icon-editor-modal/types.ts b/packages/ui/src/components/base/icon-editor-modal/types.ts
new file mode 100644
index 00000000000..a22c06a362f
--- /dev/null
+++ b/packages/ui/src/components/base/icon-editor-modal/types.ts
@@ -0,0 +1,8 @@
+export type IconBackground =
+ | { type: 'color'; value: string }
+ | { type: 'linear-top-down-gradient'; top_color: string; bottom_color: string }
+
+export type IconConfig = {
+ background: IconBackground
+ symbol: string
+}
diff --git a/packages/ui/src/components/base/index.ts b/packages/ui/src/components/base/index.ts
index b77bf88582a..96a80a584f4 100644
--- a/packages/ui/src/components/base/index.ts
+++ b/packages/ui/src/components/base/index.ts
@@ -72,6 +72,7 @@ export { default as HeadingLink } from './HeadingLink.vue'
export { default as HorizontalRule } from './HorizontalRule.vue'
export { default as I18nDebugPanel } from './I18nDebugPanel.vue'
export { default as IconSelect } from './IconSelect.vue'
+export * from './icon-editor-modal'
export { default as InlineEditableText } from './InlineEditableText.vue'
export { default as InlineValidationMessage } from './InlineValidationMessage.vue'
export type { InputAppearance, InputSize } from './inputs'
diff --git a/packages/ui/src/components/servers/edit-server-icon/EditServerIcon.vue b/packages/ui/src/components/servers/edit-server-icon/EditServerIcon.vue
index 30967a7440c..ade06ecf987 100644
--- a/packages/ui/src/components/servers/edit-server-icon/EditServerIcon.vue
+++ b/packages/ui/src/components/servers/edit-server-icon/EditServerIcon.vue
@@ -1,4 +1,10 @@
+
Icon
@@ -18,9 +24,16 @@
disabled: !props.canEdit,
tooltip: !props.canEdit ? editIconTooltip : undefined,
},
+ {
+ id: 'create',
+ label: generatedConfig ? 'Edit created icon' : 'Create an icon',
+ action: () => openIconEditor(),
+ disabled: !props.canEdit,
+ tooltip: !props.canEdit ? editIconTooltip : undefined,
+ },
{
id: 'sync',
- label: 'Sync icon',
+ label: 'Reset icon',
action: () => resetIcon(),
disabled: !props.canEdit,
tooltip: !props.canEdit ? editIconTooltip : undefined,
@@ -44,19 +57,23 @@
Upload icon
-
Sync icon
+
{{ generatedConfig ? 'Edit created icon' : 'Create an icon' }}
+
Reset icon
diff --git a/packages/ui/src/components/flows/creation-flow-modal/creation-flow-context.ts b/packages/ui/src/components/flows/creation-flow-modal/creation-flow-context.ts
index ca22f19c02c..fe438344fa1 100644
--- a/packages/ui/src/components/flows/creation-flow-modal/creation-flow-context.ts
+++ b/packages/ui/src/components/flows/creation-flow-modal/creation-flow-context.ts
@@ -17,6 +17,7 @@ import type { ImportableLauncher } from '../../../providers/instance-import'
import type { MultiStageModal, StageConfigInput } from '../../base'
import type { ComboboxOption } from '../../base/Combobox.vue'
import { installServerContent, prepareServerContent, searchServerContent } from './server-content'
+import { createServerOnboardingInvite } from './server-onboarding-invite'
import { stageConfigs } from './stages'
export type FlowType = 'world' | 'server-onboarding' | 'reset-server' | 'instance'
@@ -220,6 +221,11 @@ export interface CreationFlowContextValue {
loading: Ref
finishDisabled: ComputedRef
finishDisabledTooltip: ComputedRef
+ inviteLink: Ref
+ inviteLoading: Ref
+ inviteError: Ref
+ inviteSubmitted: Ref
+ inviteCompleted: Ref
// Backup state (set by InlineBackupCreator in reset-server flow)
isBackingUp: Ref
@@ -240,6 +246,14 @@ export interface CreationFlowContextValue {
selectProject: (projectId: string, projectType: string, versionId?: string) => Promise
installServerContent: (serverId: string, worldId: string) => Promise
finish: () => void
+ showInvite: (
+ serverId: string,
+ worldId: string,
+ siteUrl: string,
+ onDone: () => void | Promise,
+ ) => void
+ retryInvite: () => Promise
+ completeInvite: () => void
buildProperties: () => Archon.Content.v1.PropertiesFields
fetchLoaderMetadata: (loader?: string | null) => Promise
prefetchLoaderMetadata: () => Promise
@@ -384,6 +398,14 @@ export function createCreationFlowContext(
const hardReset = ref(isInitialSetup)
const loading = ref(false)
+ const inviteLink = ref(null)
+ const inviteLoading = ref(false)
+ const inviteError = ref(null)
+ const inviteSubmitted = ref(false)
+ const inviteCompleted = ref(false)
+ let inviteRun = 0
+ let inviteTarget: { serverId: string; worldId: string; siteUrl: string } | null = null
+ let inviteOnDone: (() => void | Promise) | null = null
const isBackingUp = ref(false)
const cancelBackup = ref<(() => void) | null>(null)
@@ -476,6 +498,14 @@ export function createCreationFlowContext(
}
async function reset() {
+ inviteRun++
+ inviteLink.value = null
+ inviteLoading.value = false
+ inviteError.value = null
+ inviteSubmitted.value = false
+ inviteCompleted.value = false
+ inviteTarget = null
+ inviteOnDone = null
if (fetchExistingInstanceNames) {
existingInstanceNames.value = await fetchExistingInstanceNames()
}
@@ -620,6 +650,55 @@ export function createCreationFlowContext(
emit.create(contextValue)
}
+ async function retryInvite() {
+ if (!inviteTarget || inviteLoading.value) return
+ const run = ++inviteRun
+ inviteLoading.value = true
+ inviteError.value = null
+ try {
+ const link = await createServerOnboardingInvite(
+ client,
+ inviteTarget.serverId,
+ inviteTarget.worldId,
+ inviteTarget.siteUrl,
+ () => run === inviteRun,
+ )
+ if (run === inviteRun) inviteLink.value = link
+ } catch (error) {
+ if (run === inviteRun) {
+ inviteError.value = error instanceof Error ? error.message : String(error)
+ }
+ } finally {
+ if (run === inviteRun) inviteLoading.value = false
+ }
+ }
+
+ function showInvite(
+ serverId: string,
+ worldId: string,
+ siteUrl: string,
+ onDone: () => void | Promise,
+ ) {
+ if (flowType !== 'server-onboarding') return
+ inviteTarget = { serverId, worldId, siteUrl }
+ inviteOnDone = onDone
+ inviteSubmitted.value = true
+ loading.value = false
+ modal.value?.setStage('invite-friends')
+ void retryInvite()
+ }
+
+ function completeInvite() {
+ if (!inviteSubmitted.value || inviteCompleted.value) return
+ inviteCompleted.value = true
+ inviteRun++
+ inviteLoading.value = false
+ modal.value?.hide()
+ void Promise.resolve()
+ .then(() => inviteOnDone?.())
+ .catch((error) => handleError(error as Error))
+ }
+
function buildProperties(): Archon.Content.v1.PropertiesFields {
const isHardcore = gamemode.value === 'hardcore'
const known: Archon.Content.v1.KnownPropertiesFields = {
@@ -694,6 +773,11 @@ export function createCreationFlowContext(
loading,
finishDisabled,
finishDisabledTooltip,
+ inviteLink,
+ inviteLoading,
+ inviteError,
+ inviteSubmitted,
+ inviteCompleted,
isBackingUp,
cancelBackup,
modal,
@@ -707,6 +791,9 @@ export function createCreationFlowContext(
installServerContent: (serverId, worldId) =>
installServerContent(client, contextValue, serverId, worldId),
finish,
+ showInvite,
+ retryInvite,
+ completeInvite,
buildProperties,
fetchLoaderMetadata,
prefetchLoaderMetadata,
diff --git a/packages/ui/src/components/flows/creation-flow-modal/index.vue b/packages/ui/src/components/flows/creation-flow-modal/index.vue
index eaf7790ac17..fce044e7aa3 100644
--- a/packages/ui/src/components/flows/creation-flow-modal/index.vue
+++ b/packages/ui/src/components/flows/creation-flow-modal/index.vue
@@ -6,6 +6,8 @@
:fade="fade"
disable-progress
@hide="handleHide"
+ @after-hide="emit('after-hide')"
+ @after-show="emit('after-show')"
/>
@@ -66,7 +68,7 @@ const props = withDefaults(
)
const emit = defineEmits<{
- (e: 'hide' | 'browse-modpacks'): void
+ (e: 'hide' | 'after-hide' | 'after-show' | 'browse-modpacks'): void
(e: 'create', config: CreationFlowContextValue): void
}>()
diff --git a/packages/ui/src/components/flows/creation-flow-modal/server-onboarding-invite.ts b/packages/ui/src/components/flows/creation-flow-modal/server-onboarding-invite.ts
new file mode 100644
index 00000000000..3dd43a656fa
--- /dev/null
+++ b/packages/ui/src/components/flows/creation-flow-modal/server-onboarding-invite.ts
@@ -0,0 +1,22 @@
+import type { AbstractModrinthClient } from '@modrinth/api-client'
+
+import { ensureServerInviteLink } from '#ui/layouts/shared/server-sharing'
+
+export async function createServerOnboardingInvite(
+ client: AbstractModrinthClient,
+ serverId: string,
+ worldId: string,
+ siteUrl: string,
+ isCurrent: () => boolean,
+): Promise {
+ const details = await client.archon.servers_v1.get(serverId)
+ if (!isCurrent()) return null
+ const existingId = details.worlds.find((world) => world.id === worldId)?.content?.shared_instance_id
+ const instanceId =
+ existingId ??
+ (await client.archon.content_v1.share(serverId, worldId, [], { timeout: 20_000 }))
+ .shared_instance_id
+ if (!isCurrent()) return null
+ const inviteId = await ensureServerInviteLink(client, instanceId)
+ return `${siteUrl.replace(/\/$/, '')}/share/${encodeURIComponent(inviteId)}`
+}
diff --git a/packages/ui/src/components/flows/creation-flow-modal/stages/final-config-stage.ts b/packages/ui/src/components/flows/creation-flow-modal/stages/final-config-stage.ts
index cae3e361012..dd6848f6305 100644
--- a/packages/ui/src/components/flows/creation-flow-modal/stages/final-config-stage.ts
+++ b/packages/ui/src/components/flows/creation-flow-modal/stages/final-config-stage.ts
@@ -22,10 +22,12 @@ export const stageConfig: StageConfigInput = {
title: (ctx) => ctx.formatMessage(flowTypeHeadingMessages[ctx.flowType]),
stageContent: markRaw(FinalConfigStage),
skip: (ctx) => ctx.flowType === 'instance' || ctx.isImportMode.value,
+ disableClose: (ctx) => ctx.flowType === 'server-onboarding' && ctx.loading.value,
cannotNavigateForward: isForwardBlocked,
leftButtonConfig: (ctx) => ({
label: ctx.formatMessage(commonMessages.backButton),
icon: LeftArrowIcon,
+ disabled: ctx.loading.value,
onClick: () => {
if (ctx.onBack) {
ctx.onBack()
diff --git a/packages/ui/src/components/flows/creation-flow-modal/stages/index.ts b/packages/ui/src/components/flows/creation-flow-modal/stages/index.ts
index be1203fa783..c990106dffc 100644
--- a/packages/ui/src/components/flows/creation-flow-modal/stages/index.ts
+++ b/packages/ui/src/components/flows/creation-flow-modal/stages/index.ts
@@ -3,6 +3,7 @@ import type { CreationFlowContextValue } from '../creation-flow-context'
import { stageConfig as customSetupStageConfig } from './custom-setup-stage'
import { stageConfig as finalConfigStageConfig } from './final-config-stage'
import { stageConfig as importInstanceStageConfig } from './import-instance-stage'
+import { stageConfig as inviteFriendsStageConfig } from './invite-friends-stage'
import { stageConfig as setupTypeStageConfig } from './setup-type-stage'
export const stageConfigs: StageConfigInput[] = [
@@ -10,4 +11,5 @@ export const stageConfigs: StageConfigInput[] = [
importInstanceStageConfig,
customSetupStageConfig,
finalConfigStageConfig,
+ inviteFriendsStageConfig,
]
diff --git a/packages/ui/src/components/flows/creation-flow-modal/stages/invite-friends-stage.ts b/packages/ui/src/components/flows/creation-flow-modal/stages/invite-friends-stage.ts
new file mode 100644
index 00000000000..1359aa1378b
--- /dev/null
+++ b/packages/ui/src/components/flows/creation-flow-modal/stages/invite-friends-stage.ts
@@ -0,0 +1,34 @@
+import { CheckIcon } from '@modrinth/assets'
+import { markRaw } from 'vue'
+
+import type { StageConfigInput } from '../../../base'
+import InviteFriendsStage from '../components/InviteFriendsStage.vue'
+import type { CreationFlowContextValue } from '../creation-flow-context'
+
+export const stageConfig: StageConfigInput = {
+ id: 'invite-friends',
+ title: (ctx) =>
+ ctx.formatMessage({
+ id: 'servers.setup.onboarding.invite.title',
+ defaultMessage: 'Invite players to join',
+ }),
+ stageContent: markRaw(InviteFriendsStage),
+ skip: (ctx) => ctx.flowType !== 'server-onboarding',
+ nonProgressStage: true,
+ hideHeader: true,
+ mergeHeader: true,
+ beforeHide: (ctx) => {
+ if (ctx.inviteCompleted.value) return true
+ ctx.completeInvite()
+ return false
+ },
+ leftButtonConfig: null,
+ rightButtonConfig: (ctx) => ({
+ label: ctx.formatMessage({ id: 'servers.setup.onboarding.invite.done', defaultMessage: 'Done' }),
+ icon: CheckIcon,
+ iconPosition: 'before',
+ onClick: ctx.completeInvite,
+ }),
+ maxWidth: '448px',
+ actionsTopMargin: 'sm',
+}
diff --git a/packages/ui/src/components/modal/NewModal.vue b/packages/ui/src/components/modal/NewModal.vue
index de3633af1c3..e697950e212 100644
--- a/packages/ui/src/components/modal/NewModal.vue
+++ b/packages/ui/src/components/modal/NewModal.vue
@@ -33,7 +33,8 @@
role="dialog"
aria-modal="true"
tabindex="-1"
- :aria-labelledby="headerId"
+ :aria-labelledby="hideHeader ? undefined : headerId"
+ :aria-label="hideHeader ? header : undefined"
class="modal-body flex flex-col bg-bg-raised rounded-2xl border border-solid border-surface-5 outline-none"
v-bind="$attrs"
>
@@ -63,16 +64,17 @@
-
-
-
+
+
+
+
+
void
onAfterHide?: () => void
onShow?: () => void
+ onAfterShow?: () => void
beforeHide?: () => boolean
initialFocus?: () => HTMLElement | null
mergeHeader?: boolean
@@ -222,6 +225,7 @@ const props = withDefaults(
onHide: () => {},
onAfterHide: () => {},
onShow: () => {},
+ onAfterShow: () => {},
beforeHide: undefined,
initialFocus: undefined,
mergeHeader: false,
@@ -455,6 +459,7 @@ function show(event?: MouseEvent) {
visible.value = true
nextTick(() => {
scheduleFocus()
+ props.onAfterShow?.()
})
}, 50)
}
diff --git a/packages/ui/src/components/servers/ServerListing.vue b/packages/ui/src/components/servers/ServerListing.vue
index 6b27cd022bf..00bed2e4c6a 100644
--- a/packages/ui/src/components/servers/ServerListing.vue
+++ b/packages/ui/src/components/servers/ServerListing.vue
@@ -558,7 +558,10 @@ const { data: image } = useQuery({
const fsAuth = await archon.servers_v0.getFilesystemAuth(props.server_id)
try {
- const blob = await kyros.files_v0.downloadFileWithAuth(fsAuth, '/server-icon.png')
+ const blob = await kyros.files_v0.downloadFileWithAuth(
+ fsAuth,
+ '/server-icon-original.png',
+ )
return await processImageBlob(blob, 64)
} catch (error) {
const statusCode = (error as { statusCode?: number })?.statusCode
@@ -567,15 +570,15 @@ const { data: image } = useQuery({
}
try {
- const originalBlob = await kyros.files_v0.downloadFileWithAuth(
+ const gameIconBlob = await kyros.files_v0.downloadFileWithAuth(
fsAuth,
- '/server-icon-original.png',
+ '/server-icon.png',
)
- return await processImageBlob(originalBlob, 64)
- } catch (originalError) {
- const originalStatusCode = (originalError as { statusCode?: number })?.statusCode
- if (originalStatusCode != null && originalStatusCode !== 404) {
- throw originalError
+ return await processImageBlob(gameIconBlob, 64)
+ } catch (gameIconError) {
+ const gameIconStatusCode = (gameIconError as { statusCode?: number })?.statusCode
+ if (gameIconStatusCode != null && gameIconStatusCode !== 404) {
+ throw gameIconError
}
}
diff --git a/packages/ui/src/components/servers/ServerOnboardingInviteModal.vue b/packages/ui/src/components/servers/ServerOnboardingInviteModal.vue
new file mode 100644
index 00000000000..b380897a376
--- /dev/null
+++ b/packages/ui/src/components/servers/ServerOnboardingInviteModal.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
diff --git a/packages/ui/src/components/servers/index.ts b/packages/ui/src/components/servers/index.ts
index 2b3dd1cd54d..9854cddbdc3 100644
--- a/packages/ui/src/components/servers/index.ts
+++ b/packages/ui/src/components/servers/index.ts
@@ -11,6 +11,7 @@ export { default as ModrinthServersIcon } from './ModrinthServersIcon.vue'
export { default as SaveBanner } from './SaveBanner.vue'
export * from './server-header'
export { default as ServerListEmpty } from './server-list-empty/ServerListEmpty.vue'
+export { default as ServerOnboardingInviteModal } from './ServerOnboardingInviteModal.vue'
export { type PendingChange, default as ServerListing } from './ServerListing.vue'
export { default as ServerSettingsModal } from './ServerSettingsModal.vue'
export { default as ServerSetupModal } from './ServerSetupModal.vue'
diff --git a/packages/ui/src/composables/use-server-image.ts b/packages/ui/src/composables/use-server-image.ts
index c33018ff5e7..c977870d9e8 100644
--- a/packages/ui/src/composables/use-server-image.ts
+++ b/packages/ui/src/composables/use-server-image.ts
@@ -67,17 +67,17 @@ export function useServerImage(
const fsAuth = await client.archon.servers_v0.getFilesystemAuth(serverId)
try {
- const blob = await client.kyros.files_v0.downloadFileWithAuth(fsAuth, '/server-icon.png')
+ const blob = await client.kyros.files_v0.downloadFileWithAuth(
+ fsAuth,
+ '/server-icon-original.png',
+ )
return await processImageBlob(blob, iconSize)
} catch (error) {
if (!isNotFound(error)) throw error
}
try {
- const blob = await client.kyros.files_v0.downloadFileWithAuth(
- fsAuth,
- '/server-icon-original.png',
- )
+ const blob = await client.kyros.files_v0.downloadFileWithAuth(fsAuth, '/server-icon.png')
return await processImageBlob(blob, iconSize)
} catch (error) {
if (!isNotFound(error)) throw error
diff --git a/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts b/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts
index 765e2789fae..b9f2d4590bd 100644
--- a/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts
+++ b/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts
@@ -76,13 +76,17 @@ const messages = defineMessages({
id: 'content.enabled-for.label',
defaultMessage: 'Enabled for',
},
- server: {
- id: 'content.enabled-for.server',
- defaultMessage: 'Server',
+ serverOnly: {
+ id: 'content.enabled-for.server-only',
+ defaultMessage: 'Server only',
},
- player: {
- id: 'content.enabled-for.player',
- defaultMessage: 'Player',
+ playerOnly: {
+ id: 'content.enabled-for.player-only',
+ defaultMessage: 'Player only',
+ },
+ serverAndPlayer: {
+ id: 'content.enabled-for.server-and-player',
+ defaultMessage: 'Server and player',
},
clientSideOnly: {
id: 'project.settings.environment.client_only.title',
@@ -155,6 +159,17 @@ export function useContentMetadataFilters(
const selectedMetadataFilters = persistKey
? useSessionStorage(`content-metadata-filters:${persistKey}`, {})
: ref({})
+ const savedEnabledFor = selectedMetadataFilters.value.enabled_for
+ if (savedEnabledFor?.some((value) => value === 'server' || value === 'player')) {
+ selectedMetadataFilters.value = {
+ ...selectedMetadataFilters.value,
+ enabled_for: [
+ ...(savedEnabledFor.includes('server') ? ['server_only'] : []),
+ ...(savedEnabledFor.includes('player') ? ['player_only'] : []),
+ 'server_and_player',
+ ],
+ }
+ }
function option(value: string, label: string, searchTerms?: string[]): DropdownFilterBarOption {
return { value, label, searchTerms }
@@ -188,13 +203,22 @@ export function useContentMetadataFilters(
key: 'enabled_for',
label: formatMessage(messages.enabledFor),
options: [
- option('server', formatMessage(messages.server)),
- option('player', formatMessage(messages.player)),
- ],
- values: (item) => [
- ...(item.enabledFor?.server ? [option('server', formatMessage(messages.server))] : []),
- ...(item.enabledFor?.player ? [option('player', formatMessage(messages.player))] : []),
+ option('server_only', formatMessage(messages.serverOnly)),
+ option('player_only', formatMessage(messages.playerOnly)),
+ option('server_and_player', formatMessage(messages.serverAndPlayer)),
],
+ values: (item) => {
+ if (item.enabledFor?.server && item.enabledFor.player) {
+ return [option('server_and_player', formatMessage(messages.serverAndPlayer))]
+ }
+ if (item.enabledFor?.server) {
+ return [option('server_only', formatMessage(messages.serverOnly))]
+ }
+ if (item.enabledFor?.player) {
+ return [option('player_only', formatMessage(messages.playerOnly))]
+ }
+ return []
+ },
}
: {
key: 'environment',
diff --git a/packages/ui/src/layouts/shared/server-sharing/index.ts b/packages/ui/src/layouts/shared/server-sharing/index.ts
index e0c6049e1d9..bb76d4643c5 100644
--- a/packages/ui/src/layouts/shared/server-sharing/index.ts
+++ b/packages/ui/src/layouts/shared/server-sharing/index.ts
@@ -1,3 +1,4 @@
export { clearServerSharedInstance } from './cache'
export { sharedInstanceInvitesQueryOptions } from './query-options'
+export { ensureServerInviteLink } from './invite-link'
export { useServerSharingSettings } from './use-server-sharing'
diff --git a/packages/ui/src/layouts/shared/server-sharing/invite-link.ts b/packages/ui/src/layouts/shared/server-sharing/invite-link.ts
new file mode 100644
index 00000000000..6ce0e4dba5d
--- /dev/null
+++ b/packages/ui/src/layouts/shared/server-sharing/invite-link.ts
@@ -0,0 +1,26 @@
+import type { AbstractModrinthClient } from '@modrinth/api-client'
+
+const INVITE_MAX_AGE = 7 * 24 * 60 * 60
+
+export async function ensureServerInviteLink(
+ client: AbstractModrinthClient,
+ instanceId: string,
+): Promise {
+ const [links, players] = await Promise.all([
+ client.sharedinstances.invites_v1.list(instanceId),
+ client.sharedinstances.instances_v1.getUsers(instanceId),
+ ])
+ const active = links.find(
+ (link) => new Date(link.expiration).getTime() > Date.now() && link.uses < link.max_uses,
+ )
+ if (active) return active.id
+
+ const remaining = Math.max(0, 50 - players.users.length - players.tokens)
+ if (remaining === 0) throw new Error('This shared instance has no available player slots.')
+
+ const invite = await client.sharedinstances.invites_v1.create(instanceId, {
+ max_age: INVITE_MAX_AGE,
+ max_uses: Math.min(10, remaining),
+ })
+ return invite.id
+}
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue
index 7e2839f7299..8c435575e54 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue
@@ -16,7 +16,11 @@
-
+
-
+
{{ i + 1 }}. {{ step.title }}
@@ -68,7 +72,7 @@
:get-project-versions="getProjectVersions"
:finish-disabled="!canSetup"
:finish-disabled-tooltip="!canSetup ? permissionDeniedMessage : undefined"
- @hide="() => {}"
+ @after-hide="onModalAfterHide"
@browse-modpacks="onBrowseModpacks"
@create="onCreate"
/>
@@ -87,7 +91,7 @@ import {
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
-import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
+import { computed, nextTick, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import type { CreationFlowContextValue } from '#ui/components'
@@ -162,7 +166,7 @@ const messages = defineMessages({
inviteFriendsDescription: {
id: 'servers.setup.onboarding.step.invite-friends.description',
defaultMessage:
- "Share your server with friends by copying the address and letting them know which mods they'll need to join.",
+ 'Invite friends to your server with a link and let them join in one click from the Modrinth App.',
},
})
@@ -178,6 +182,7 @@ const dismissServerIntro = useDismissServerIntro()
const props = withDefaults(
defineProps<{
+ siteUrl: string
browseModpacks?: (args: {
serverId: string
worldId: string | null
@@ -189,7 +194,11 @@ const props = withDefaults(
},
)
+const emit = defineEmits<{ 'setup-started': []; 'invite-started': []; 'setup-finished': [] }>()
+
const modalRef = ref
| null>(null)
+let inviteModalHidden = false
+let introFinished = false
const uploading = ref(false)
const uploadedBytes = ref(0)
@@ -203,8 +212,6 @@ const openModal = () => {
modalRef.value?.show()
}
-onBeforeUnmount(() => modalRef.value?.hide())
-
function onBrowseModpacks() {
if (!canSetup.value) return
@@ -260,9 +267,32 @@ onMounted(async () => {
})
async function finalizeSetup() {
- modalRef.value?.hide()
- await dismissServerIntro.mutateAsync(serverId)
- await router.push(`/hosting/manage/${serverId}/`)
+ try {
+ await dismissServerIntro.mutateAsync(serverId)
+ } finally {
+ introFinished = true
+ finishInviteIfReady()
+ }
+}
+
+function onModalAfterHide() {
+ if (!modalRef.value?.ctx?.inviteSubmitted.value) {
+ emit('setup-finished')
+ return
+ }
+ inviteModalHidden = true
+ finishInviteIfReady()
+}
+
+function finishInviteIfReady() {
+ if (inviteModalHidden && introFinished) emit('setup-finished')
+}
+
+function showInvite(config: CreationFlowContextValue) {
+ inviteModalHidden = false
+ introFinished = false
+ emit('invite-started')
+ config.showInvite(serverId, worldId.value!, props.siteUrl, finalizeSetup)
}
function markInstalling() {
@@ -282,11 +312,12 @@ const onCreate = async (config: CreationFlowContextValue) => {
config.loading.value = false
return
}
+ emit('setup-started')
if (config.projectInstall.value) {
try {
await config.installServerContent(serverId, worldId.value!)
- await finalizeSetup()
+ showInvite(config)
} catch (error) {
addNotification({
title: formatMessage(messages.installationFailedTitle),
@@ -301,7 +332,6 @@ const onCreate = async (config: CreationFlowContextValue) => {
// Handle mrpack file upload
if (config.setupType.value === 'modpack' && config.modpackFile.value) {
- modalRef.value?.hide()
uploading.value = true
uploadedBytes.value = 0
totalBytes.value = config.modpackFile.value.size
@@ -321,7 +351,7 @@ const onCreate = async (config: CreationFlowContextValue) => {
)
await handle.promise
markInstalling()
- await finalizeSetup()
+ showInvite(config)
} catch {
addNotification({
title: formatMessage(messages.modpackUploadFailedTitle),
@@ -364,7 +394,7 @@ const onCreate = async (config: CreationFlowContextValue) => {
try {
await client.archon.content_v1.installContent(serverId, worldId.value!, request)
markInstalling()
- await finalizeSetup()
+ showInvite(config)
} catch {
addNotification({
title: formatMessage(messages.installationFailedTitle),
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
index 6cc82ba83eb..892329b4280 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, type Ref } from 'vue'
import type { InviteLinkSettings, InvitePlayersUser } from '#ui/components/sharing'
-import { sharedInstanceInvitesQueryOptions } from '#ui/layouts/shared/server-sharing'
+import { ensureServerInviteLink, sharedInstanceInvitesQueryOptions } from '#ui/layouts/shared/server-sharing'
import { injectAuth, injectModrinthClient } from '#ui/providers'
import type { ServerPlayerRow } from './types'
@@ -163,24 +163,8 @@ export function useServerPlayers(instanceId: Ref, canManage: Ref<
})
async function ensureLink(id: string) {
- const available = await queryClient.fetchQuery({
- ...sharedInstanceInvitesQueryOptions(client, id, userId.value),
- staleTime: 0,
- })
- if (
- available.some(
- (link) => new Date(link.expiration).getTime() > Date.now() && link.uses < link.max_uses,
- )
- )
- return
- if (remaining.value <= 0) return
- await linkMutation.mutateAsync({
- id,
- settings: {
- maxUses: Math.min(10, remaining.value),
- expiresAt: new Date(Date.now() + 86400_000),
- },
- })
+ await ensureServerInviteLink(client, id)
+ await queryClient.invalidateQueries({ queryKey: ['shared-instances', id, 'invites'] })
}
async function search(query: string) {
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/files.vue b/packages/ui/src/layouts/wrapped/hosting/manage/files.vue
index debed3a0733..66a93b7ca8e 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/files.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/files.vue
@@ -163,7 +163,10 @@ const {
})
function isVisibleFileItem(item: Kyros.Files.v0.DirectoryItem) {
- return !item.path.split('/').includes('.modrinth-staged')
+ return (
+ !item.path.split('/').includes('.modrinth-staged') &&
+ item.path.replace(/^\/+/, '') !== 'server-icon-config.json'
+ )
}
const items = computed(() =>
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
index c1f5a20d049..ec40e9bf080 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
@@ -114,7 +114,7 @@
: 'min-h-[calc(100svh-100px)] pb-6',
]"
>
-
+
-
+
@@ -379,6 +384,7 @@ import type { ServerSettingsTabId } from '#ui/layouts/shared/server-settings'
import {
injectModrinthClient,
injectNotificationManager,
+ injectServerOnboardingInviteFlow,
provideServerSettingsModal,
} from '#ui/providers'
import type { ServerStats } from '#ui/providers/server-context'
@@ -464,6 +470,7 @@ const isNuxt = computed(() => client instanceof NuxtModrinthClient)
const queryClient = useQueryClient()
const route = useRoute()
const router = useRouter()
+const globalInviteFlow = injectServerOnboardingInviteFlow(null)
const debug = useDebugLogger('ServerManage')
const isReconnecting = ref(false)
@@ -596,7 +603,24 @@ const {
onStateEvent,
})
-const isOnboarding = computed(() => serverData.value?.flows?.intro && !installation.value)
+const onboardingFlowActive = ref(false)
+const inviteFlowActive = ref(false)
+const globalInviteForServer = computed(
+ () => globalInviteFlow?.request.value?.serverId === props.serverId,
+)
+const isOnboarding = computed(
+ () =>
+ !inviteFlowActive.value &&
+ !globalInviteForServer.value &&
+ !!serverData.value?.flows?.intro &&
+ !installation.value,
+)
+const keepOnboardingMounted = computed(() => onboardingFlowActive.value || isOnboarding.value)
+
+function finishOnboardingFlow() {
+ onboardingFlowActive.value = false
+ inviteFlowActive.value = false
+}
const serverHeaderImage = computed(() =>
serverData.value?.is_medal ? 'https://cdn.modrinth.com/medal_icon.webp' : serverImage.value,
diff --git a/packages/ui/src/providers/index.ts b/packages/ui/src/providers/index.ts
index 191d017cc99..2587093c3c3 100644
--- a/packages/ui/src/providers/index.ts
+++ b/packages/ui/src/providers/index.ts
@@ -18,6 +18,7 @@ export * from './popup-notifications'
export * from './project-page'
export * from './project-page-new'
export * from './server-context'
+export * from './server-onboarding-invite'
export * from './server-play'
export * from './server-settings-modal'
export * from './tags'
diff --git a/packages/ui/src/providers/server-onboarding-invite.ts b/packages/ui/src/providers/server-onboarding-invite.ts
new file mode 100644
index 00000000000..d7214e6a1c6
--- /dev/null
+++ b/packages/ui/src/providers/server-onboarding-invite.ts
@@ -0,0 +1,55 @@
+import { shallowRef, type ShallowRef } from 'vue'
+
+import { createContext } from './create-context'
+
+export interface ServerOnboardingInviteRequest {
+ serverId: string
+ worldId: string
+ siteUrl: string
+}
+
+export interface ServerOnboardingInviteFlow {
+ request: ShallowRef
+ open: (request: ServerOnboardingInviteRequest) => Promise
+ markShown: () => void
+ fail: (error: unknown) => void
+ clear: () => void
+}
+
+export const [injectServerOnboardingInviteFlow, provideServerOnboardingInviteFlow] =
+ createContext('root', 'serverOnboardingInviteFlow')
+
+export function createServerOnboardingInviteFlow(): ServerOnboardingInviteFlow {
+ const request = shallowRef(null)
+ let resolveShown: (() => void) | null = null
+ let rejectShown: ((error: unknown) => void) | null = null
+
+ function open(nextRequest: ServerOnboardingInviteRequest) {
+ return new Promise((resolve, reject) => {
+ resolveShown = resolve
+ rejectShown = reject
+ request.value = nextRequest
+ })
+ }
+
+ function markShown() {
+ resolveShown?.()
+ resolveShown = null
+ rejectShown = null
+ }
+
+ function fail(error: unknown) {
+ request.value = null
+ rejectShown?.(error)
+ resolveShown = null
+ rejectShown = null
+ }
+
+ function clear() {
+ request.value = null
+ resolveShown = null
+ rejectShown = null
+ }
+
+ return { request, open, markShown, fail, clear }
+}
diff --git a/packages/ui/src/stories/servers/CreationFlowModal.stories.ts b/packages/ui/src/stories/servers/CreationFlowModal.stories.ts
index c70be62f22a..0c9d28645aa 100644
--- a/packages/ui/src/stories/servers/CreationFlowModal.stories.ts
+++ b/packages/ui/src/stories/servers/CreationFlowModal.stories.ts
@@ -59,7 +59,7 @@ export const CreateWorld: Story = {
// ============================================
export const ServerOnboarding: Story = {
- name: 'Server Setup (Legacy) (Hosting)',
+ name: 'Server onboarding (Hosting)',
render: () => ({
components: { CreationFlowModal, Button },
setup() {
@@ -69,6 +69,10 @@ export const ServerOnboarding: Story = {
const onCreate = (config: CreationFlowContextValue) => {
lastEvent.value = `create emitted — loader: ${config.selectedLoader.value}, version: ${config.selectedGameVersion.value}`
+ config.loading.value = false
+ config.inviteLink.value = 'https://modrinth.com/share/example-invite'
+ config.inviteSubmitted.value = true
+ config.modal.value?.setStage('invite-friends')
}
return { modalRef, openModal, lastEvent, onCreate }
},
From 2e10be0ad3de2e98c4e9f7e4060705c8e32fd3d8 Mon Sep 17 00:00:00 2001
From: "Calum H. (IMB11)"
Date: Tue, 22 Sep 2026 22:58:21 +0100
Subject: [PATCH 21/67] fix: prepr + lint
---
apps/app-frontend/src/App.vue | 4 +-
apps/app-frontend/src/helpers/types.d.ts | 12 +-
.../app-frontend/src/locales/ar-SA/index.json | 213 -------------
.../app-frontend/src/locales/cs-CZ/index.json | 207 ------------
.../app-frontend/src/locales/de-CH/index.json | 213 -------------
.../app-frontend/src/locales/de-DE/index.json | 213 -------------
.../app-frontend/src/locales/en-US/index.json | 231 ++------------
.../src/locales/es-419/index.json | 213 -------------
.../app-frontend/src/locales/es-ES/index.json | 213 -------------
.../app-frontend/src/locales/fr-FR/index.json | 99 ------
.../app-frontend/src/locales/he-IL/index.json | 108 -------
.../app-frontend/src/locales/hu-HU/index.json | 213 -------------
.../app-frontend/src/locales/it-IT/index.json | 213 -------------
.../app-frontend/src/locales/ja-JP/index.json | 213 -------------
.../app-frontend/src/locales/ko-KR/index.json | 213 -------------
.../app-frontend/src/locales/ms-MY/index.json | 48 ---
.../app-frontend/src/locales/nl-NL/index.json | 213 -------------
.../app-frontend/src/locales/pl-PL/index.json | 213 -------------
.../app-frontend/src/locales/pt-BR/index.json | 213 -------------
.../app-frontend/src/locales/ru-RU/index.json | 213 -------------
.../app-frontend/src/locales/sv-SE/index.json | 192 ------------
.../app-frontend/src/locales/tr-TR/index.json | 213 -------------
.../app-frontend/src/locales/uk-UA/index.json | 213 -------------
.../app-frontend/src/locales/zh-CN/index.json | 213 -------------
.../app-frontend/src/locales/zh-TW/index.json | 213 -------------
.../src/pages/instance/layout.vue | 15 +-
.../providers/setup/server-install-content.ts | 1 -
apps/frontend/src/app.vue | 4 +-
packages/app-lib/src/api/handler.rs | 37 ++-
packages/app-lib/src/api/instance.rs | 5 +-
.../app-lib/src/api/instance/shared/client.rs | 10 +-
.../app-lib/src/api/instance/shared/diff.rs | 42 +--
.../src/api/instance/shared/install.rs | 89 +++---
.../src/api/instance/shared/publish.rs | 7 +-
packages/app-lib/src/api/pack/install_from.rs | 21 +-
packages/app-lib/src/event/mod.rs | 8 +-
packages/app-lib/src/install/model.rs | 8 +-
.../app-lib/src/install/shared_instance.rs | 113 ++++---
.../base/icon-editor-modal/editor-catalog.ts | 4 +-
.../base/icon-editor-modal/index.ts | 4 +-
.../base/icon-editor-modal/index.vue | 10 +-
packages/ui/src/components/base/index.ts | 2 +-
.../components/InviteFriendsStage.vue | 6 +-
.../server-onboarding-invite.ts | 3 +-
.../stages/invite-friends-stage.ts | 5 +-
.../src/components/servers/ServerListing.vue | 10 +-
.../edit-server-icon/EditServerIcon.vue | 32 +-
packages/ui/src/components/servers/index.ts | 2 +-
.../ui/src/composables/server-panel-sync.ts | 4 +-
.../components/ContentCardItem.vue | 4 +-
.../components/ContentCardTable.vue | 19 +-
.../components/ContentEnabledFor.vue | 4 +-
.../managed-content-modal/index.vue | 10 +-
.../layouts/shared/server-sharing/index.ts | 2 +-
.../hosting/manage/[id]/onboarding.vue | 6 +-
.../manage/[id]/play/ServerPlayCard.vue | 8 +-
.../wrapped/hosting/manage/[id]/play/play.vue | 6 +-
.../manage/[id]/play/use-server-players.ts | 5 +-
.../wrapped/hosting/manage/content.vue | 11 +-
.../layouts/wrapped/hosting/manage/index.vue | 6 +-
packages/ui/src/locales/ar-SA/index.json | 10 -
packages/ui/src/locales/cs-CZ/index.json | 15 -
packages/ui/src/locales/de-CH/index.json | 21 --
packages/ui/src/locales/de-DE/index.json | 21 --
packages/ui/src/locales/en-US/index.json | 294 ++++++++++++++++--
packages/ui/src/locales/es-419/index.json | 21 --
packages/ui/src/locales/es-ES/index.json | 21 --
packages/ui/src/locales/fr-FR/index.json | 21 --
packages/ui/src/locales/hu-HU/index.json | 21 --
packages/ui/src/locales/it-IT/index.json | 21 --
packages/ui/src/locales/ja-JP/index.json | 15 -
packages/ui/src/locales/ko-KR/index.json | 18 --
packages/ui/src/locales/nl-NL/index.json | 21 --
packages/ui/src/locales/pl-PL/index.json | 21 --
packages/ui/src/locales/pt-BR/index.json | 21 --
packages/ui/src/locales/ru-RU/index.json | 21 --
packages/ui/src/locales/sr-CS/index.json | 12 -
packages/ui/src/locales/sv-SE/index.json | 21 --
packages/ui/src/locales/tr-TR/index.json | 21 --
packages/ui/src/locales/uk-UA/index.json | 21 --
packages/ui/src/locales/zh-CN/index.json | 21 --
packages/ui/src/locales/zh-TW/index.json | 21 --
.../src/providers/server-onboarding-invite.ts | 2 +-
83 files changed, 575 insertions(+), 5172 deletions(-)
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index bfe10d11240..ce286b19051 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -27,8 +27,8 @@ import {
commonSettingsMessages,
ContentInstallModal,
ContentUpdaterModal,
- CreationFlowModal,
createServerOnboardingInviteFlow,
+ CreationFlowModal,
defineMessages,
I18nDebugPanel,
IconButton,
@@ -41,8 +41,8 @@ import {
provideNotificationManager,
providePageContext,
providePopupNotificationManager,
- provideServerPlay,
provideServerOnboardingInviteFlow,
+ provideServerPlay,
ServerOnboardingInviteModal,
TeleportOverflowMenu,
TooltipDirective,
diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts
index 8e44342631f..c1b4055e186 100644
--- a/apps/app-frontend/src/helpers/types.d.ts
+++ b/apps/app-frontend/src/helpers/types.d.ts
@@ -146,7 +146,7 @@ export type ContentSourceKind =
| 'imported_modpack'
| 'shared_instance'
-type ContentFile = {
+export type ContentFile = {
enabled: boolean
locked: boolean
source_kind?: ContentSourceKind | null
@@ -156,9 +156,9 @@ type ContentFile = {
}
}
-type ContentFileProjectType = 'mod' | 'datapack' | 'resourcepack' | 'shaderpack'
+export type ContentFileProjectType = 'mod' | 'datapack' | 'resourcepack' | 'shaderpack'
-type CacheBehaviour =
+export type CacheBehaviour =
// Serve expired data. If fetch fails / launcher is offline, errors are ignored
| 'stale_while_revalidate_skip_offline'
// Serve expired data, revalidate in background
@@ -172,7 +172,7 @@ type MemorySettings = {
maximum: number
}
-type WindowSize = [number, number]
+export type WindowSize = [number, number]
type Hooks = {
pre_launch?: string
@@ -180,7 +180,7 @@ type Hooks = {
post_exit?: string
}
-type Manifest = {
+export type Manifest = {
gameVersions: ManifestGameVersion[]
versionGroups?: ManifestVersionGroup[]
}
@@ -203,7 +203,7 @@ type ManifestLoaderVersion = {
stable: boolean
}
-type AppSettings = {
+export type AppSettings = {
max_concurrent_downloads: number
max_concurrent_writes: number
diff --git a/apps/app-frontend/src/locales/ar-SA/index.json b/apps/app-frontend/src/locales/ar-SA/index.json
index 062dbd2911b..017f905757d 100644
--- a/apps/app-frontend/src/locales/ar-SA/index.json
+++ b/apps/app-frontend/src/locales/ar-SA/index.json
@@ -2288,219 +2288,6 @@
"instance.files.save-as": {
"message": "حفظ ك..."
},
- "instance.icon-editor.background": {
- "message": "الخلفية"
- },
- "instance.icon-editor.background.blue": {
- "message": "أزرق"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "رمادي داكن"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "أخضر داكن"
- },
- "instance.icon-editor.background.gray": {
- "message": "رمادي"
- },
- "instance.icon-editor.background.green": {
- "message": "أخضر"
- },
- "instance.icon-editor.background.indigo": {
- "message": "نيلي"
- },
- "instance.icon-editor.background.lavender": {
- "message": "أرجواني"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "رمادي فاتح"
- },
- "instance.icon-editor.background.lime": {
- "message": "ليموني "
- },
- "instance.icon-editor.background.orange": {
- "message": "برتقالي"
- },
- "instance.icon-editor.background.pink": {
- "message": "وردي"
- },
- "instance.icon-editor.background.purple": {
- "message": "بنفسجي"
- },
- "instance.icon-editor.background.red": {
- "message": "أحمر"
- },
- "instance.icon-editor.background.rose": {
- "message": "وردي"
- },
- "instance.icon-editor.background.yellow": {
- "message": "أصفر"
- },
- "instance.icon-editor.description": {
- "message": "خلط العناصر ومطابقتها لإنشاء أيقونة مخصصة."
- },
- "instance.icon-editor.recents": {
- "message": "التطبيقات الحديثة"
- },
- "instance.icon-editor.save": {
- "message": "حفظ الأيقونة"
- },
- "instance.icon-editor.surprise-me": {
- "message": "عشوائي"
- },
- "instance.icon-editor.symbol": {
- "message": "رمز"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "حقيب"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "بيكن"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "قرش أزرق"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "مكتبة كتب"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "دب بني"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "كعكة"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "نار المخيم"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "صندوق"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "عجلة مسننة"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "كمند بلك"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "قدر طهي"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "كنبة"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "طاولة الصنع"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "مفك"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "كريبر"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "الة التطور"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "صندوق الاند"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "تنين الاند"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "محرك"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "فابرك"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "فورج"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "فرن"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Gizmo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "الكرة الأرضية"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "كتلة عشب"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "فانوس"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "البقرة الصفراء :)"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "سيد ضفدع"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "ثعلب احمر"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Orb"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "موزع الأكسجين"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "بنكايك"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "فأس"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "كرة البكمن"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "كولة"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "مكعب ردستون"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "مستشعر"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "هيكل عظمي"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "مقلاة"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "مكعب سليم"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "خوذة الفضاء"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "مكبس لاصق"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "سيف"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "حاسوب"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "بطاط"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "عجل"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "متفجراة"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "مفتاح Modrinth"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "زومبي"
- },
- "instance.icon-editor.title": {
- "message": "تغير الايقون"
- },
"instance.last-played": {
"message": "آخر لعب"
},
diff --git a/apps/app-frontend/src/locales/cs-CZ/index.json b/apps/app-frontend/src/locales/cs-CZ/index.json
index d8720462b91..b4a8bb1a52b 100644
--- a/apps/app-frontend/src/locales/cs-CZ/index.json
+++ b/apps/app-frontend/src/locales/cs-CZ/index.json
@@ -2033,213 +2033,6 @@
"instance.files.save-as": {
"message": "Uložit jako..."
},
- "instance.icon-editor.background": {
- "message": "Pozadí"
- },
- "instance.icon-editor.background.blue": {
- "message": "Modrá"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Tmavě šedá"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Tmavě zelená"
- },
- "instance.icon-editor.background.gray": {
- "message": "Šedá"
- },
- "instance.icon-editor.background.green": {
- "message": "Zelená"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Indigová"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Levandulová"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Světle šedá"
- },
- "instance.icon-editor.background.lime": {
- "message": "Limetková"
- },
- "instance.icon-editor.background.orange": {
- "message": "Oranžová"
- },
- "instance.icon-editor.background.pink": {
- "message": "Růžová"
- },
- "instance.icon-editor.background.purple": {
- "message": "Fialová"
- },
- "instance.icon-editor.background.red": {
- "message": "Červená"
- },
- "instance.icon-editor.background.rose": {
- "message": "Růžová"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Žlutá"
- },
- "instance.icon-editor.description": {
- "message": "Promíchejte prvky pro vytvoření vlastní ikony."
- },
- "instance.icon-editor.recents": {
- "message": "Nedávné"
- },
- "instance.icon-editor.save": {
- "message": "Ikona uložení"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Náhodné"
- },
- "instance.icon-editor.symbol": {
- "message": "Symbol"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Batoh"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Beacon"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Modrý Žralok"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Knihovna"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Hnědý medvěd"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Dort"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Ohniště"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Truhla"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Kolečko"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Příkazový Blok"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Hrnec na vaření"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Gauč"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Pracovní stůl"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Montážní klíč"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Zaklínací stůl"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Ender truhla"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Ender Drak"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Motor"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Pec"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Gizmo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Glóbus"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Blok Trávy"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Lucerna"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Moobloom"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Mr Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Orb"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Distributor kyslíku"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Palačinky"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Krumpáč"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Poké Ball"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Redstone Blok"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Kostlivec"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Slimový blok"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Vesmírná helma"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Lepivý píst"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Meč"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Terminál"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Brambůrek"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Pneumatika"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "Dynamit (TNT)"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinth klíč"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombie"
- },
- "instance.icon-editor.title": {
- "message": "Editor ikon"
- },
"instance.last-played": {
"message": "Naposledy hráno"
},
diff --git a/apps/app-frontend/src/locales/de-CH/index.json b/apps/app-frontend/src/locales/de-CH/index.json
index 99a3dde3806..4af4b0bc1a6 100644
--- a/apps/app-frontend/src/locales/de-CH/index.json
+++ b/apps/app-frontend/src/locales/de-CH/index.json
@@ -3701,219 +3701,6 @@
"instance.files.save-as": {
"message": "Speichern unter..."
},
- "instance.icon-editor.background": {
- "message": "Hintergrund"
- },
- "instance.icon-editor.background.blue": {
- "message": "Blau"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Dunkelgrau"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Dunkelgrün"
- },
- "instance.icon-editor.background.gray": {
- "message": "Grau"
- },
- "instance.icon-editor.background.green": {
- "message": "Grün"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Indigo"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Lavendel"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Hellgrau"
- },
- "instance.icon-editor.background.lime": {
- "message": "Hellgrün"
- },
- "instance.icon-editor.background.orange": {
- "message": "Orange"
- },
- "instance.icon-editor.background.pink": {
- "message": "Rosa"
- },
- "instance.icon-editor.background.purple": {
- "message": "Violett"
- },
- "instance.icon-editor.background.red": {
- "message": "Rot"
- },
- "instance.icon-editor.background.rose": {
- "message": "Rose"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Gelb"
- },
- "instance.icon-editor.description": {
- "message": "Kombiniere verschiedene Elemente, um ein benutzerdefiniertes Icon zu erstellen."
- },
- "instance.icon-editor.recents": {
- "message": "Letzte"
- },
- "instance.icon-editor.save": {
- "message": "Icon speichern"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Randomisieren"
- },
- "instance.icon-editor.symbol": {
- "message": "Symbol"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Rucksack"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Leuchtfeuer"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Blauer Hai"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Bücherregal"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Brauner Bär"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Kuchen"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Lagerfeuer"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Truhe"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Zahnrad"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Befehlsblock"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Kochtopf"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Sofa"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Werkbank"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Schlüssel"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Zaubertisch"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Endertruhe"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Enderdrache"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Motor"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Ofen"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Gizmo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Globus"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Grasblock"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Laterne"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Blumenkuh"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Herr Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Orb"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Sauerstoffverteiler"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Pfannkuchen"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Spitzhacke"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Pokeball"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Redstone-Block"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Sculk-Sensor"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Skelett"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Bratpfanne"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Schleimblock"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Astronautenhelm"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Klebriger Kolben"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Schwert"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Konsole"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Winzige Kartoffel"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Reifen"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinth-Schlüssel"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombie"
- },
- "instance.icon-editor.title": {
- "message": "Icon-Editor"
- },
"instance.last-played": {
"message": "Zuletzt gespielt"
},
diff --git a/apps/app-frontend/src/locales/de-DE/index.json b/apps/app-frontend/src/locales/de-DE/index.json
index 0c48725a9e6..47816464908 100644
--- a/apps/app-frontend/src/locales/de-DE/index.json
+++ b/apps/app-frontend/src/locales/de-DE/index.json
@@ -3701,219 +3701,6 @@
"instance.files.save-as": {
"message": "Speichern unter..."
},
- "instance.icon-editor.background": {
- "message": "Hintergrund"
- },
- "instance.icon-editor.background.blue": {
- "message": "Blau"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Dunkelgrau"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Dunkelgrün"
- },
- "instance.icon-editor.background.gray": {
- "message": "Grau"
- },
- "instance.icon-editor.background.green": {
- "message": "Grün"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Indigo"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Lavendel"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Hellgrau"
- },
- "instance.icon-editor.background.lime": {
- "message": "Hellgrün"
- },
- "instance.icon-editor.background.orange": {
- "message": "Orange"
- },
- "instance.icon-editor.background.pink": {
- "message": "Rosa"
- },
- "instance.icon-editor.background.purple": {
- "message": "Violett"
- },
- "instance.icon-editor.background.red": {
- "message": "Rot"
- },
- "instance.icon-editor.background.rose": {
- "message": "Rose"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Gelb"
- },
- "instance.icon-editor.description": {
- "message": "Kombiniere verschiedene Elemente, um ein benutzerdefiniertes Icon zu erstellen."
- },
- "instance.icon-editor.recents": {
- "message": "Letzte"
- },
- "instance.icon-editor.save": {
- "message": "Icon speichern"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Randomisieren"
- },
- "instance.icon-editor.symbol": {
- "message": "Symbol"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Rucksack"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Leuchtfeuer"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Blauer Hai"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Bücherregal"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Brauner Bär"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Kuchen"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Lagerfeuer"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Truhe"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Zahnrad"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Befehlsblock"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Kochtopf"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Sofa"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Werkbank"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Schlüssel"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Zaubertisch"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Endertruhe"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Enderdrache"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Motor"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Ofen"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Gizmo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Globus"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Grasblock"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Laterne"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Blumenkuh"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Herr Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Orb"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Sauerstoffverteiler"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Pfannkuchen"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Spitzhacke"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Pokeball"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Redstone-Block"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Sculk-Sensor"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Skelett"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Bratpfanne"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Schleimblock"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Astronautenhelm"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Klebriger Kolben"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Schwert"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Konsole"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Winzige Kartoffel"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Reifen"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinth-Schlüssel"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombie"
- },
- "instance.icon-editor.title": {
- "message": "Icon-Editor"
- },
"instance.last-played": {
"message": "Zuletzt gespielt"
},
diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json
index 93fa301b802..e8f811203a6 100644
--- a/apps/app-frontend/src/locales/en-US/index.json
+++ b/apps/app-frontend/src/locales/en-US/index.json
@@ -752,6 +752,18 @@
"app.instance.navigation.back-to-instance": {
"message": "Back to instance"
},
+ "app.instance.server.offline.description": {
+ "message": "This server is currently offline. You can still launch Minecraft, but you may not be able to connect."
+ },
+ "app.instance.server.offline.launch-anyway": {
+ "message": "Launch anyway"
+ },
+ "app.instance.server.offline.title": {
+ "message": "Server is offline"
+ },
+ "app.instance.server.status.unknown": {
+ "message": "Status unknown"
+ },
"app.instance.share.empty.description": {
"message": "You can share this instance with your friends!"
},
@@ -3767,219 +3779,6 @@
"instance.files.save-as": {
"message": "Save as..."
},
- "instance.icon-editor.background": {
- "message": "Background"
- },
- "instance.icon-editor.background.blue": {
- "message": "Blue"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Dark gray"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Dark green"
- },
- "instance.icon-editor.background.gray": {
- "message": "Gray"
- },
- "instance.icon-editor.background.green": {
- "message": "Green"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Indigo"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Lavender"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Light gray"
- },
- "instance.icon-editor.background.lime": {
- "message": "Lime"
- },
- "instance.icon-editor.background.orange": {
- "message": "Orange"
- },
- "instance.icon-editor.background.pink": {
- "message": "Pink"
- },
- "instance.icon-editor.background.purple": {
- "message": "Purple"
- },
- "instance.icon-editor.background.red": {
- "message": "Red"
- },
- "instance.icon-editor.background.rose": {
- "message": "Rose"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Yellow"
- },
- "instance.icon-editor.description": {
- "message": "Mix and match elements to create a custom icon."
- },
- "instance.icon-editor.recents": {
- "message": "Recents"
- },
- "instance.icon-editor.save": {
- "message": "Save icon"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Randomize"
- },
- "instance.icon-editor.symbol": {
- "message": "Symbol"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Backpack"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Beacon"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Blue Shark"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Bookshelf"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Brown Bear"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Cake"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Campfire"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Chest"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Cogwheel"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Command Block"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Cooking Pot"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Couch"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Crafting Table"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Wrench"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Enchanting Table"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Ender Chest"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Ender Dragon"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Engine"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Furnace"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Gizmo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Globe"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Grass Block"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Lantern"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Moobloom"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Mr Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Orb"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Oxygen Distributor"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Pancakes"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Pickaxe"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Poke Ball"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Redstone Block"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Sculk Sensor"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Skeleton"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Skillet"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Slime Block"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Space Helmet"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Sticky Piston"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Sword"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Terminal"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Tiny Potato"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Tire"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinth Wrench"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombie"
- },
- "instance.icon-editor.title": {
- "message": "Icon editor"
- },
"instance.last-played": {
"message": "Last played"
},
@@ -4547,6 +4346,12 @@
"onboarding-checklist.title": {
"message": "Getting started"
},
+ "project.server.status.offline": {
+ "message": "Offline"
+ },
+ "project.server.status.online": {
+ "message": "Online"
+ },
"search.filter.locked.instance": {
"message": "Provided by the instance"
},
diff --git a/apps/app-frontend/src/locales/es-419/index.json b/apps/app-frontend/src/locales/es-419/index.json
index bc8598fd97a..f66f55876ee 100644
--- a/apps/app-frontend/src/locales/es-419/index.json
+++ b/apps/app-frontend/src/locales/es-419/index.json
@@ -3701,219 +3701,6 @@
"instance.files.save-as": {
"message": "Guardar como..."
},
- "instance.icon-editor.background": {
- "message": "Fondo"
- },
- "instance.icon-editor.background.blue": {
- "message": "Azul"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Gris oscuro"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Verde oscuro"
- },
- "instance.icon-editor.background.gray": {
- "message": "Gris"
- },
- "instance.icon-editor.background.green": {
- "message": "Verde"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Indigo"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Lavanda"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Gris claro"
- },
- "instance.icon-editor.background.lime": {
- "message": "Lima"
- },
- "instance.icon-editor.background.orange": {
- "message": "Naranja"
- },
- "instance.icon-editor.background.pink": {
- "message": "Rosado"
- },
- "instance.icon-editor.background.purple": {
- "message": "Morado"
- },
- "instance.icon-editor.background.red": {
- "message": "Rojo"
- },
- "instance.icon-editor.background.rose": {
- "message": "Rosa"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Amarillo"
- },
- "instance.icon-editor.description": {
- "message": "Mezclar y combinar elementos para crear un ícono personalizado."
- },
- "instance.icon-editor.recents": {
- "message": "Recientes"
- },
- "instance.icon-editor.save": {
- "message": "Guardar ícono"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Aleatorizar"
- },
- "instance.icon-editor.symbol": {
- "message": "Símbolo"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Mochila"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Faro"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Tiburón Azul"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Estantería"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Oso pardo"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Torta"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Fogata"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Cofre"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Rueda dentada"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Bloque de comandos"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Olla de cocina"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Sofá"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Mesa de crafteo"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Llave inglesa"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Mesa de encantamientos"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Cofre de Ender"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Enderdragón"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Máquina"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Horno"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Gizmo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Globo"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Bloque de pasto"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Linterna"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Moobloom"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Mr Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Orbe"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Distribuidor de Oxígeno"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Panqueques"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Pico"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "PokéBola"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Bloque de Redstone"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Sensor de Sculk"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Esqueleto"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Sartén"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Bloque de Slime"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Casco espacial"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Pistón pegajoso"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Espada"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Terminal"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Patata pequeña"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Neumático"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "Dinamita"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Llave Inglesa de Modrinth"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombi"
- },
- "instance.icon-editor.title": {
- "message": "Editor de ícono"
- },
"instance.last-played": {
"message": "Última vez jugado"
},
diff --git a/apps/app-frontend/src/locales/es-ES/index.json b/apps/app-frontend/src/locales/es-ES/index.json
index fa8b21cde15..3840fbb528d 100644
--- a/apps/app-frontend/src/locales/es-ES/index.json
+++ b/apps/app-frontend/src/locales/es-ES/index.json
@@ -3671,219 +3671,6 @@
"instance.files.save-as": {
"message": "Guardar como..."
},
- "instance.icon-editor.background": {
- "message": "Fondo"
- },
- "instance.icon-editor.background.blue": {
- "message": "Azul"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Gris oscuro"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Verde oscuro"
- },
- "instance.icon-editor.background.gray": {
- "message": "Gris"
- },
- "instance.icon-editor.background.green": {
- "message": "Verde"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Añil"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Lavanda"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Gris claro"
- },
- "instance.icon-editor.background.lime": {
- "message": "Verde lima"
- },
- "instance.icon-editor.background.orange": {
- "message": "Naranja"
- },
- "instance.icon-editor.background.pink": {
- "message": "Rosa"
- },
- "instance.icon-editor.background.purple": {
- "message": "Morado"
- },
- "instance.icon-editor.background.red": {
- "message": "Rojo"
- },
- "instance.icon-editor.background.rose": {
- "message": "Rosáceo"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Amarillo"
- },
- "instance.icon-editor.description": {
- "message": "Mezclar y combinar elementos para crear un icono personalizado."
- },
- "instance.icon-editor.recents": {
- "message": "Recientes"
- },
- "instance.icon-editor.save": {
- "message": "Guardar icono"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Aleatorizar"
- },
- "instance.icon-editor.symbol": {
- "message": "Símbolo"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Mochila"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Faro"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Tiburón azul"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Estantería"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Oso pardo"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Pastel"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Fogata"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Cofre"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Engranaje"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Bloque de Comandos"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Olla de Cocina"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Sofá"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Mesa de Trabajo"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Llave inglesa"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Mesa de encantamientos"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Cofre de Ender"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Dragón del End"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Motor"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Horno"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Artilugio"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Globo Terráqueo"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Bloque de césped"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Linterna"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Moobloom"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Mr. Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Orbe"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Distribuidor de oxígeno"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Tortitas"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Pico"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Poke Ball"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Bloque de Redstone"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Sensor de Sculk"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Esqueleto"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Sartén"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Bloque de slime"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Casco espacial"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Pistón pegajoso"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Espada"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Terminal"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Patatita"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Neumático"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Llave de Modrinth"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombi"
- },
- "instance.icon-editor.title": {
- "message": "Editor de iconos"
- },
"instance.last-played": {
"message": "Jugado por última vez"
},
diff --git a/apps/app-frontend/src/locales/fr-FR/index.json b/apps/app-frontend/src/locales/fr-FR/index.json
index 9d9efb1ff6d..379e00742f0 100644
--- a/apps/app-frontend/src/locales/fr-FR/index.json
+++ b/apps/app-frontend/src/locales/fr-FR/index.json
@@ -3677,105 +3677,6 @@
"instance.files.save-as": {
"message": "Sauvegarder en tant que..."
},
- "instance.icon-editor.background": {
- "message": "Arrière plan"
- },
- "instance.icon-editor.background.blue": {
- "message": "Bleu"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Gris foncé"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Vert foncé"
- },
- "instance.icon-editor.background.gray": {
- "message": "Gris"
- },
- "instance.icon-editor.background.green": {
- "message": "Vert"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Indigo"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Lavande"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Gris clair"
- },
- "instance.icon-editor.background.lime": {
- "message": "Citron"
- },
- "instance.icon-editor.background.orange": {
- "message": "Orange"
- },
- "instance.icon-editor.background.pink": {
- "message": "Rose"
- },
- "instance.icon-editor.background.purple": {
- "message": "Violet"
- },
- "instance.icon-editor.background.red": {
- "message": "Rouge"
- },
- "instance.icon-editor.background.rose": {
- "message": "Rose"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Jaune"
- },
- "instance.icon-editor.description": {
- "message": "Mélanger et faites correspondre des éléments pour créer des icônes customisées."
- },
- "instance.icon-editor.recents": {
- "message": "Récents"
- },
- "instance.icon-editor.save": {
- "message": "Enregistrer l'Icône"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Aléatoire"
- },
- "instance.icon-editor.symbol": {
- "message": "Symbole"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Sac à dos"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Balise"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Requin Bleu"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Bibliothèque"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Ours Brun"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Gâteau"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Feu de camp"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Coffre"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Roue Crantée"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Bloc de Commande"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Marmite"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Canapé"
- },
"instance.icon-editor.symbol.crafting-table": {
"message": "Établi"
},
diff --git a/apps/app-frontend/src/locales/he-IL/index.json b/apps/app-frontend/src/locales/he-IL/index.json
index 2c3c8e85ad4..c59320d3243 100644
--- a/apps/app-frontend/src/locales/he-IL/index.json
+++ b/apps/app-frontend/src/locales/he-IL/index.json
@@ -1622,114 +1622,6 @@
"instance.files.save-as": {
"message": "שמור כ..."
},
- "instance.icon-editor.background": {
- "message": "רקע"
- },
- "instance.icon-editor.background.blue": {
- "message": "כחול"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "אפור כהה"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "ירוק כהה"
- },
- "instance.icon-editor.background.gray": {
- "message": "אפור"
- },
- "instance.icon-editor.background.green": {
- "message": "ירוק"
- },
- "instance.icon-editor.background.indigo": {
- "message": "אינדיגו"
- },
- "instance.icon-editor.background.lavender": {
- "message": "לבנדר"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "אפור בהיר"
- },
- "instance.icon-editor.background.lime": {
- "message": "ירוק ליים"
- },
- "instance.icon-editor.background.orange": {
- "message": "כתום"
- },
- "instance.icon-editor.background.pink": {
- "message": "ורוד"
- },
- "instance.icon-editor.background.purple": {
- "message": "סגול"
- },
- "instance.icon-editor.background.red": {
- "message": "אדום"
- },
- "instance.icon-editor.background.rose": {
- "message": "רוז"
- },
- "instance.icon-editor.background.yellow": {
- "message": "צהוב"
- },
- "instance.icon-editor.description": {
- "message": "בחר אלמנטים ליצירת סמל אישי."
- },
- "instance.icon-editor.recents": {
- "message": "אחרונים"
- },
- "instance.icon-editor.save": {
- "message": "שמור סמל"
- },
- "instance.icon-editor.surprise-me": {
- "message": "אקראי"
- },
- "instance.icon-editor.symbol": {
- "message": "סמל"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "תיק גב"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "כריש כחול"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "מדף ספרים"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "דוב חום"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "עוגה"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "מדורה"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "תיבה"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "בלוק פקודה"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "סיר בישול"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "ספה"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "שולחן מלאכה"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "קריפר"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "שולחן כישופים"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "תיבת אנדר"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "דרקון אנדר"
- },
"instance.server-modal.address": {
"message": "כתובת"
},
diff --git a/apps/app-frontend/src/locales/hu-HU/index.json b/apps/app-frontend/src/locales/hu-HU/index.json
index 69d68e5a1cd..6c7e01f0f75 100644
--- a/apps/app-frontend/src/locales/hu-HU/index.json
+++ b/apps/app-frontend/src/locales/hu-HU/index.json
@@ -3680,219 +3680,6 @@
"instance.files.save-as": {
"message": "Mentés másként..."
},
- "instance.icon-editor.background": {
- "message": "Háttér"
- },
- "instance.icon-editor.background.blue": {
- "message": "Kék"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Sötét szürke"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Sotét zöld"
- },
- "instance.icon-editor.background.gray": {
- "message": "Szürke"
- },
- "instance.icon-editor.background.green": {
- "message": "Zöld"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Indigókék"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Levendula"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Világosszürke"
- },
- "instance.icon-editor.background.lime": {
- "message": "Limezöld"
- },
- "instance.icon-editor.background.orange": {
- "message": "Narancssárga"
- },
- "instance.icon-editor.background.pink": {
- "message": "Rózsaszín"
- },
- "instance.icon-editor.background.purple": {
- "message": "Lila"
- },
- "instance.icon-editor.background.red": {
- "message": "Vörös"
- },
- "instance.icon-editor.background.rose": {
- "message": "Rózsa"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Sárga"
- },
- "instance.icon-editor.description": {
- "message": "Válogass részeket az egyéni ikon felépítéséhez."
- },
- "instance.icon-editor.recents": {
- "message": "Előzmények"
- },
- "instance.icon-editor.save": {
- "message": "Ikon mentése"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Véletlenszerű"
- },
- "instance.icon-editor.symbol": {
- "message": "Szimbólum"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Hátizsák"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Jelzőfény"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Kék cápa"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Könyvespolc"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Barna medve"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Torta"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Tábortűz"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Láda"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Fogaskerék"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Parancs block"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Kondér"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Kanapé"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Barkácsasztal"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Csavarkulcs"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Varázslóasztal"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Végzetláda"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Végzetsárkány"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Motor"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Kemence"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "3D-s vezérlő"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Földgömb"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Füves földblokk"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Lámpás"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Múúvirág"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Mr Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Eredetgömb "
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Oxigénelosztó"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Palacsinta"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Csákány"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Pokélabda"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Vöröskőblokk"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Rezgésérzékelő"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Csontváz"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Serpenyő"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Nyálkablokk"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Űrsisak"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Ragacsos dugattyú"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Kard"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Terminál"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Kicsi burgonya"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Kerék"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinth csavarkulcs"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombi"
- },
- "instance.icon-editor.title": {
- "message": "Ikonszerkesztő"
- },
"instance.last-played": {
"message": "Legutóbb játszva"
},
diff --git a/apps/app-frontend/src/locales/it-IT/index.json b/apps/app-frontend/src/locales/it-IT/index.json
index 7c3db76efbf..12d7ebc2f2f 100644
--- a/apps/app-frontend/src/locales/it-IT/index.json
+++ b/apps/app-frontend/src/locales/it-IT/index.json
@@ -3701,219 +3701,6 @@
"instance.files.save-as": {
"message": "Salva come..."
},
- "instance.icon-editor.background": {
- "message": "Sfondo"
- },
- "instance.icon-editor.background.blue": {
- "message": "Blu"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Grigio scuro"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Verde scuro"
- },
- "instance.icon-editor.background.gray": {
- "message": "Grigio"
- },
- "instance.icon-editor.background.green": {
- "message": "Verde"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Indaco"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Lavanda"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Grigio chiaro"
- },
- "instance.icon-editor.background.lime": {
- "message": "Lime"
- },
- "instance.icon-editor.background.orange": {
- "message": "Arancione"
- },
- "instance.icon-editor.background.pink": {
- "message": "Rosa"
- },
- "instance.icon-editor.background.purple": {
- "message": "Viola"
- },
- "instance.icon-editor.background.red": {
- "message": "Rosso"
- },
- "instance.icon-editor.background.rose": {
- "message": "Rosato"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Giallo"
- },
- "instance.icon-editor.description": {
- "message": "Abbina gli elementi per creare una icona personale."
- },
- "instance.icon-editor.recents": {
- "message": "Recenti"
- },
- "instance.icon-editor.save": {
- "message": "Salva"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Casuale"
- },
- "instance.icon-editor.symbol": {
- "message": "Simbolo"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Zaino"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Faro"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Squalo blu"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Libreria"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Orso bruno"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Torta"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Fuoco da campo"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Baule"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Ingranaggio"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Blocco comandi"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Pentola"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Divano"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Banco da lavoro"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Chiave a pappagallo"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Tavolo per incantesimi"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Baule di ender"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Enderdrago"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Motore"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Fornace"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Assi di controllo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Mappamondo"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Blocco d'erba"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Lanterna"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Fior di bovino"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Mr. Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Globo"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Distributore di ossigeno"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Pancake"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Piccone"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Poké Ball"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Blocco di redstone"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Sensore di sculk"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Scheletro"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Padella"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Blocco di gelatina"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Casco spaziale"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Pistone appiccicoso"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Spada"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Terminale"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Patatuccia"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Ruota"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Chiave di Modrinth"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombi"
- },
- "instance.icon-editor.title": {
- "message": "Editor di icone"
- },
"instance.last-played": {
"message": "Avviato"
},
diff --git a/apps/app-frontend/src/locales/ja-JP/index.json b/apps/app-frontend/src/locales/ja-JP/index.json
index 03b8b30c67a..9ec31794e14 100644
--- a/apps/app-frontend/src/locales/ja-JP/index.json
+++ b/apps/app-frontend/src/locales/ja-JP/index.json
@@ -2831,219 +2831,6 @@
"instance.files.save-as": {
"message": "名前をつけて保存…"
},
- "instance.icon-editor.background": {
- "message": "背景"
- },
- "instance.icon-editor.background.blue": {
- "message": "青色"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "濃い灰色"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "濃い緑色"
- },
- "instance.icon-editor.background.gray": {
- "message": "灰色"
- },
- "instance.icon-editor.background.green": {
- "message": "緑色"
- },
- "instance.icon-editor.background.indigo": {
- "message": "藍色"
- },
- "instance.icon-editor.background.lavender": {
- "message": "ラベンダー色"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "薄灰色"
- },
- "instance.icon-editor.background.lime": {
- "message": "黄緑色"
- },
- "instance.icon-editor.background.orange": {
- "message": "橙色"
- },
- "instance.icon-editor.background.pink": {
- "message": "桃色"
- },
- "instance.icon-editor.background.purple": {
- "message": "紫色"
- },
- "instance.icon-editor.background.red": {
- "message": "赤色"
- },
- "instance.icon-editor.background.rose": {
- "message": "薔薇色"
- },
- "instance.icon-editor.background.yellow": {
- "message": "黄色"
- },
- "instance.icon-editor.description": {
- "message": "要素を組み合わせてカスタムアイコンを作成しましょう。"
- },
- "instance.icon-editor.recents": {
- "message": "最近使用した項目"
- },
- "instance.icon-editor.save": {
- "message": "アイコンを保存"
- },
- "instance.icon-editor.surprise-me": {
- "message": "ランダム化"
- },
- "instance.icon-editor.symbol": {
- "message": "記号"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "バックパック"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "ビーコン"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "ブルーシャーク"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "本棚"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "ヒグマ"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "ケーキ"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "焚き火"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "チェスト"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "歯車"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "コマンドブロック"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "調理鍋"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "ソファー"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "作業台"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "レンチ"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "クリーパー"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "エンチャントテーブル"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "エンダーチェスト"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "エンダードラゴン"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "エンジン"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "かまど"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Gizmo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "地球"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "草ブロック"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "ランタン"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "ムーブルーム"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Mr Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "オーブ"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "酸素分配機"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "パンケーキ"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "ツルハシ"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "モンスターボール"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "レッドストーンブロック"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "スカルクセンサー"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "スケルトン"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "フライパン"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "スライムブロック"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "宇宙ヘルメット"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "粘着ピストン"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "剣"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "ターミナル"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "小さなジャガイモ"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "タイヤ"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinthのレンチ"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "ゾンビ"
- },
- "instance.icon-editor.title": {
- "message": "アイコンエディター"
- },
"instance.last-played": {
"message": "最終プレイ"
},
diff --git a/apps/app-frontend/src/locales/ko-KR/index.json b/apps/app-frontend/src/locales/ko-KR/index.json
index d7fdd1c5e75..8da6e775af7 100644
--- a/apps/app-frontend/src/locales/ko-KR/index.json
+++ b/apps/app-frontend/src/locales/ko-KR/index.json
@@ -3653,219 +3653,6 @@
"instance.files.save-as": {
"message": "다른 이름으로 저장..."
},
- "instance.icon-editor.background": {
- "message": "배경"
- },
- "instance.icon-editor.background.blue": {
- "message": "파랑"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "진한 회색"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "진한 초록"
- },
- "instance.icon-editor.background.gray": {
- "message": "회색"
- },
- "instance.icon-editor.background.green": {
- "message": "초록"
- },
- "instance.icon-editor.background.indigo": {
- "message": "남색"
- },
- "instance.icon-editor.background.lavender": {
- "message": "라벤더"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "연한 회색"
- },
- "instance.icon-editor.background.lime": {
- "message": "연두"
- },
- "instance.icon-editor.background.orange": {
- "message": "주황"
- },
- "instance.icon-editor.background.pink": {
- "message": "분홍"
- },
- "instance.icon-editor.background.purple": {
- "message": "보라"
- },
- "instance.icon-editor.background.red": {
- "message": "빨강"
- },
- "instance.icon-editor.background.rose": {
- "message": "로즈"
- },
- "instance.icon-editor.background.yellow": {
- "message": "노란색"
- },
- "instance.icon-editor.description": {
- "message": "요소를 섞고 조합하여 맞춤 아이콘을 만드세요."
- },
- "instance.icon-editor.recents": {
- "message": "최근"
- },
- "instance.icon-editor.save": {
- "message": "아이콘 저장"
- },
- "instance.icon-editor.surprise-me": {
- "message": "무작위"
- },
- "instance.icon-editor.symbol": {
- "message": "문양"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "배낭"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "신호기"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "파랑 상어"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "책장"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "갈색 곰"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "케이크"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "모닥불"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "상자"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "톱니바퀴"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "명령 블록"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "요리 냄비"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "소파"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "제작대"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "렌치"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "크리퍼"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "마법 부여대"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "엔더 상자"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "엔더 드래곤"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "엔진"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "화로"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "기즈모"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "지구본"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "잔디 블록"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "랜턴"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "꽃소"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "미스터 팩"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "구슬"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "산소 분사기"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "팬케이크"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "곡괭이"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "몬스터볼"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "레드스톤 블록"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "스컬크 감지체"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "스켈레톤"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "프라이팬"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "슬라임 블록"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "우주 헬멧"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "끈끈이 피스톤"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "검"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "터미널"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "작은 감자"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "타이어"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinth 렌치"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "좀비"
- },
- "instance.icon-editor.title": {
- "message": "아이콘 편집기"
- },
"instance.last-played": {
"message": "마지막 플레이: "
},
diff --git a/apps/app-frontend/src/locales/ms-MY/index.json b/apps/app-frontend/src/locales/ms-MY/index.json
index 77bcfea6294..40c0c9e02e3 100644
--- a/apps/app-frontend/src/locales/ms-MY/index.json
+++ b/apps/app-frontend/src/locales/ms-MY/index.json
@@ -1745,54 +1745,6 @@
"instance.files.save-as": {
"message": "Simpan sebagai..."
},
- "instance.icon-editor.background.blue": {
- "message": "Biru"
- },
- "instance.icon-editor.background.gray": {
- "message": "Kelabu"
- },
- "instance.icon-editor.background.green": {
- "message": "Hijau"
- },
- "instance.icon-editor.background.lime": {
- "message": "Hijau Muda"
- },
- "instance.icon-editor.background.pink": {
- "message": "Merah Jambu"
- },
- "instance.icon-editor.background.red": {
- "message": "Merah"
- },
- "instance.icon-editor.background.rose": {
- "message": "Mawar"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Kek"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Peti"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Glob"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Orb"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Pedang"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Tayar"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
"instance.server-modal.address": {
"message": "Alamat"
},
diff --git a/apps/app-frontend/src/locales/nl-NL/index.json b/apps/app-frontend/src/locales/nl-NL/index.json
index 2b33b184c72..2b195f31199 100644
--- a/apps/app-frontend/src/locales/nl-NL/index.json
+++ b/apps/app-frontend/src/locales/nl-NL/index.json
@@ -3323,219 +3323,6 @@
"instance.files.save-as": {
"message": "Opslaan als..."
},
- "instance.icon-editor.background": {
- "message": "Achtergrond"
- },
- "instance.icon-editor.background.blue": {
- "message": "Blauw"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Donkergrijs"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Donkergroen"
- },
- "instance.icon-editor.background.gray": {
- "message": "Grijs"
- },
- "instance.icon-editor.background.green": {
- "message": "Groen"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Indigo"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Lavendel"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Lichtgrijs"
- },
- "instance.icon-editor.background.lime": {
- "message": "Limoen"
- },
- "instance.icon-editor.background.orange": {
- "message": "Oranje"
- },
- "instance.icon-editor.background.pink": {
- "message": "Roze"
- },
- "instance.icon-editor.background.purple": {
- "message": "Paars"
- },
- "instance.icon-editor.background.red": {
- "message": "Rood"
- },
- "instance.icon-editor.background.rose": {
- "message": "Roos"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Geel"
- },
- "instance.icon-editor.description": {
- "message": "Mix en match elementen om een aangepast icoon te maken."
- },
- "instance.icon-editor.recents": {
- "message": "Recente"
- },
- "instance.icon-editor.save": {
- "message": "Pictogram opslaan"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Willekeurig"
- },
- "instance.icon-editor.symbol": {
- "message": "Symbool"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Rugzak"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Baken"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Blauwe haai"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Boekenkast"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Bruine beer"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Taart"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Kampvuur"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Kist"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Tandwiel"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Opdrachtblok"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Kookpot"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Bank"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Werkbank"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Moersleutel"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Betoveringstafel"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Enderkist"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Enderdraak"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Engine"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Oven"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Gizmo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Wereldbol"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Grasblok"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Lantaarn"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Moobloom"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Meneer Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Orb"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Zuurstofdistributeur"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Pannenkoeken"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Pikhouweel"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Poke bal"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Redstone-blok"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Sculk sensor"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Skelet"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Koekepan"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Slijmblok"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Ruimte helm"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Kleefzuiger"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Zwaard"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Terminal"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Kleine aardappel"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Band"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "Dynamiet"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinth moersleutel"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombie"
- },
- "instance.icon-editor.title": {
- "message": "Pictogram bewerker"
- },
"instance.last-played": {
"message": "Laatst gespeeld"
},
diff --git a/apps/app-frontend/src/locales/pl-PL/index.json b/apps/app-frontend/src/locales/pl-PL/index.json
index 8a87296245e..f2611956ceb 100644
--- a/apps/app-frontend/src/locales/pl-PL/index.json
+++ b/apps/app-frontend/src/locales/pl-PL/index.json
@@ -3662,219 +3662,6 @@
"instance.files.save-as": {
"message": "Zapisz jako..."
},
- "instance.icon-editor.background": {
- "message": "Tło"
- },
- "instance.icon-editor.background.blue": {
- "message": "Niebieskie"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Ciemnoszare"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Ciemnozielone"
- },
- "instance.icon-editor.background.gray": {
- "message": "Szare"
- },
- "instance.icon-editor.background.green": {
- "message": "Zielone"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Indygo"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Lawendowe"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Jasnoszare"
- },
- "instance.icon-editor.background.lime": {
- "message": "Jasnozielone"
- },
- "instance.icon-editor.background.orange": {
- "message": "Pomarańczowe"
- },
- "instance.icon-editor.background.pink": {
- "message": "Różowe"
- },
- "instance.icon-editor.background.purple": {
- "message": "Fioletowe"
- },
- "instance.icon-editor.background.red": {
- "message": "Czerwone"
- },
- "instance.icon-editor.background.rose": {
- "message": "Różane"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Żółte"
- },
- "instance.icon-editor.description": {
- "message": "Wybierz elementy, by utworzyć własną ikonę."
- },
- "instance.icon-editor.recents": {
- "message": "Ostatnio używane"
- },
- "instance.icon-editor.save": {
- "message": "Zapisz ikonę"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Losuj"
- },
- "instance.icon-editor.symbol": {
- "message": "Symbol"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Plecak"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Magiczna latarnia"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Niebieski rekin"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Biblioteczka"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Brązowy miś"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Ciasto"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Ognisko"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Skrzynia"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Koło zębate"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Blok poleceń"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Garnek"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Kanapa"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Stół rzemieślniczy"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Klucz"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Stół do zaklinania"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Skrzynia Endu"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Smok Endu"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Silnik"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Piec"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Gizmo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Globus"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Blok trawy"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Latarnia"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Moobloom"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Mr Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Kula"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Dystrybutor tlenu"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Naleśniki"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Kilof"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Poke Ball"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Blok redstone"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Sculkowy czujnik"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Szkielet"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Patelnia"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Blok szlamu"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Hełm Kosmiczny"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Lepki tłok"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Miecz"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Terminal"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Ziemniaczek"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Opona"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Klucz Modrinth"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombie"
- },
- "instance.icon-editor.title": {
- "message": "Edytor ikon"
- },
"instance.last-played": {
"message": "Ostatnio grane"
},
diff --git a/apps/app-frontend/src/locales/pt-BR/index.json b/apps/app-frontend/src/locales/pt-BR/index.json
index f313b0fa1fa..6a84b3cc8af 100644
--- a/apps/app-frontend/src/locales/pt-BR/index.json
+++ b/apps/app-frontend/src/locales/pt-BR/index.json
@@ -3701,219 +3701,6 @@
"instance.files.save-as": {
"message": "Salvar como..."
},
- "instance.icon-editor.background": {
- "message": "Plano de fundo"
- },
- "instance.icon-editor.background.blue": {
- "message": "Azul"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Cinza-escuro"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Verde-escuro"
- },
- "instance.icon-editor.background.gray": {
- "message": "Cinza"
- },
- "instance.icon-editor.background.green": {
- "message": "Verde"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Índigo"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Lavanda"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Cinza-claro"
- },
- "instance.icon-editor.background.lime": {
- "message": "Limão"
- },
- "instance.icon-editor.background.orange": {
- "message": "Laranja"
- },
- "instance.icon-editor.background.pink": {
- "message": "Rosa"
- },
- "instance.icon-editor.background.purple": {
- "message": "Roxo"
- },
- "instance.icon-editor.background.red": {
- "message": "Vermelho"
- },
- "instance.icon-editor.background.rose": {
- "message": "Rosê"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Amarelo"
- },
- "instance.icon-editor.description": {
- "message": "Misture e combine elementos para criar um ícone personalizado."
- },
- "instance.icon-editor.recents": {
- "message": "Recentes"
- },
- "instance.icon-editor.save": {
- "message": "Salvar ícone"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Aleatorizar"
- },
- "instance.icon-editor.symbol": {
- "message": "Símbolo"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Mochila"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Sinalizador"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Tubarão Azul"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Estante de livros"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Urso Marrom"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Bolo"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Fogueira"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Baú"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Engrenagem"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Bloco de Comando"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Panela de Cozinhar"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Poltrona"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Bancada de Trabalho"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Chave Inglesa"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Mesa de Encantamento"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Baú do Fim"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Dragão do Fim"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Motor"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Fornalha"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Bugiganga"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Globo"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Bloco de Grama"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Lampião"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Vaca Cogumelo Amarela"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Sr. Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Orbe"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Distribuidor de Oxigênio"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Panquecas"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Picareta"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Pokébola"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Bloco de Redstone"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Sensor de Sculk"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Esqueleto"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Frigideira"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Bloco de Slime"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Capacete Espacial"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Pistão Grudento"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Espada"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Terminal"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Pequena Batata"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Pneu"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "Dinamite"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Chave de Fenda Modrinth"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zumbi"
- },
- "instance.icon-editor.title": {
- "message": "Editor de ícone"
- },
"instance.last-played": {
"message": "Jogado pela última vez"
},
diff --git a/apps/app-frontend/src/locales/ru-RU/index.json b/apps/app-frontend/src/locales/ru-RU/index.json
index 8ea8cefe729..2d14818c493 100644
--- a/apps/app-frontend/src/locales/ru-RU/index.json
+++ b/apps/app-frontend/src/locales/ru-RU/index.json
@@ -3677,219 +3677,6 @@
"instance.files.save-as": {
"message": "Сохранить как..."
},
- "instance.icon-editor.background": {
- "message": "Фон"
- },
- "instance.icon-editor.background.blue": {
- "message": "Голубой"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Тёмно-серый"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Тёмно-зелёный"
- },
- "instance.icon-editor.background.gray": {
- "message": "Серый"
- },
- "instance.icon-editor.background.green": {
- "message": "Зелёный"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Индиго"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Лиловый"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Светло-серый"
- },
- "instance.icon-editor.background.lime": {
- "message": "Лаймовый"
- },
- "instance.icon-editor.background.orange": {
- "message": "Оранжевый"
- },
- "instance.icon-editor.background.pink": {
- "message": "Розовый"
- },
- "instance.icon-editor.background.purple": {
- "message": "Синий"
- },
- "instance.icon-editor.background.red": {
- "message": "Красный"
- },
- "instance.icon-editor.background.rose": {
- "message": "Коралловый"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Жёлтый"
- },
- "instance.icon-editor.description": {
- "message": "Сочетайте элементы, чтобы создать иконку."
- },
- "instance.icon-editor.recents": {
- "message": "Недавние"
- },
- "instance.icon-editor.save": {
- "message": "Сохранить иконку"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Рандомизировать"
- },
- "instance.icon-editor.symbol": {
- "message": "Значок"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Рюкзак"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Маяк"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Синяя акула"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Книжная полка"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Бурый медведь"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Торт"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Костёр"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Сундук"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Шестерня"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Командный блок"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Кухонный котёл"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Диван"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Верстак"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Гаечный ключ"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Крипер"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Чародейский стол"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Эндер-сундук"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Эндер-дракон"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Двигатель"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Печь"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Гизмо"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Глобус"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Дёрн"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Фонарь"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Коровоцвет"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "М‑р Пак"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Сфера"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Распылитель кислорода"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Блинчики"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Кирка"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Покебол"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Редстоуновый блок"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Скалковый сенсор"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Скелет"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Сковорода"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Блок слизи"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Шлем скафандра"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Липкий поршень"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Меч"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Терминал"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Крошка-картошка"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Шина"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "Динамит"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Гаечный ключ Modrinth"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Зомби"
- },
- "instance.icon-editor.title": {
- "message": "Редактор иконок"
- },
"instance.last-played": {
"message": "Последний запуск"
},
diff --git a/apps/app-frontend/src/locales/sv-SE/index.json b/apps/app-frontend/src/locales/sv-SE/index.json
index 72d8c761093..fe80570d332 100644
--- a/apps/app-frontend/src/locales/sv-SE/index.json
+++ b/apps/app-frontend/src/locales/sv-SE/index.json
@@ -3212,198 +3212,6 @@
"instance.files.save-as": {
"message": "Spara som..."
},
- "instance.icon-editor.background": {
- "message": "Bakgrund"
- },
- "instance.icon-editor.background.blue": {
- "message": "Blå"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Mörkgrå"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Mörkgrön"
- },
- "instance.icon-editor.background.gray": {
- "message": "Grå"
- },
- "instance.icon-editor.background.green": {
- "message": "Grön"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Indigo"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Lavendel"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Ljusgrå"
- },
- "instance.icon-editor.background.lime": {
- "message": "Limegrön"
- },
- "instance.icon-editor.background.orange": {
- "message": "Orange"
- },
- "instance.icon-editor.background.pink": {
- "message": "Rosa"
- },
- "instance.icon-editor.background.purple": {
- "message": "Lila"
- },
- "instance.icon-editor.background.red": {
- "message": "Röd"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Gul"
- },
- "instance.icon-editor.recents": {
- "message": "Senaste"
- },
- "instance.icon-editor.save": {
- "message": "Spara ikon"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Slumpa"
- },
- "instance.icon-editor.symbol": {
- "message": "Symbol"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Ryggsäck"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Fyr"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Blåhaj"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Bokhylla"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Brunbjörn"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Tårta"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Lägereld"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Kista"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Kugghjul"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Kommandoblock"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Gryta"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Soffa"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Arbetsbänk"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Skiftnyckel"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Förtrollningsbord"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Enderkista"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Enderdrake"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Motor"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Ugn"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Glob"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Gräsblock"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Lykta"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Moobloom"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Mr Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Syrefördelare"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Pannkakor"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Hacka"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Skelett"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Stekpanna"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Slemblock"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Rymdhjälm"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Klibbig kolv"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Svärd"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Terminal"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Liten potatis"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Däck"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "Dynamit"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinth skiftnyckel"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombie"
- },
- "instance.icon-editor.title": {
- "message": "Ikonredigerare"
- },
"instance.last-played": {
"message": "Senast spelad"
},
diff --git a/apps/app-frontend/src/locales/tr-TR/index.json b/apps/app-frontend/src/locales/tr-TR/index.json
index 4a4a97cf635..47b76156148 100644
--- a/apps/app-frontend/src/locales/tr-TR/index.json
+++ b/apps/app-frontend/src/locales/tr-TR/index.json
@@ -3020,219 +3020,6 @@
"instance.files.save-as": {
"message": "Farklı kaydet..."
},
- "instance.icon-editor.background": {
- "message": "Arkaplan"
- },
- "instance.icon-editor.background.blue": {
- "message": "Mavi"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Koyu gri"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Koyu yeşil"
- },
- "instance.icon-editor.background.gray": {
- "message": "Gri"
- },
- "instance.icon-editor.background.green": {
- "message": "Yeşil"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Çivit mavisi"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Eflatun"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Açık gri"
- },
- "instance.icon-editor.background.lime": {
- "message": "Açık Yeşil"
- },
- "instance.icon-editor.background.orange": {
- "message": "Turuncu"
- },
- "instance.icon-editor.background.pink": {
- "message": "Pembe"
- },
- "instance.icon-editor.background.purple": {
- "message": "Mor"
- },
- "instance.icon-editor.background.red": {
- "message": "Kırmızı"
- },
- "instance.icon-editor.background.rose": {
- "message": "Gül Pembesi"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Sarı"
- },
- "instance.icon-editor.description": {
- "message": "Özel bir simge oluşturmak için öğeleri karıştırıp eşleştirin."
- },
- "instance.icon-editor.recents": {
- "message": "En Son"
- },
- "instance.icon-editor.save": {
- "message": "İkonu kaydet"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Rastgele"
- },
- "instance.icon-editor.symbol": {
- "message": "Sembol"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Sırt Çantası"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Fener"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Mavi Köpekbalığı"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Kitaplık"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Boz Ayı"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Pasta"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Kamp Ateşi"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Sandık"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Dişli Çark"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Komut bloğu"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Pişirme Kazanı"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Kanepe"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Çalışma Masası"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "İngiliz Anahtarı"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Creeper"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Büyü Masası"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Ender Sandığı"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Ender Ejderhası"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Motor"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Fırın"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Gizmo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Küre"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Çimen Bloğu"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Fener"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Moobloom İneği"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Mr Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Küre"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Oksijen Dağıtıcısı"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Pankekler"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Kazma"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Poke Topu"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Redstone Bloğu"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Sculk Sensörü"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "İskelet"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Tava"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Balçık Bloğu"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Uzay Kaskı"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Yapışkan Piston"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Kılıç"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Terminal"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Minik Patates"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Tekerlek"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinth Anahtarı"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Zombi"
- },
- "instance.icon-editor.title": {
- "message": "Simge düzenleyicisi"
- },
"instance.last-played": {
"message": "Son oynanış:"
},
diff --git a/apps/app-frontend/src/locales/uk-UA/index.json b/apps/app-frontend/src/locales/uk-UA/index.json
index d5eb1319334..52d9328ba68 100644
--- a/apps/app-frontend/src/locales/uk-UA/index.json
+++ b/apps/app-frontend/src/locales/uk-UA/index.json
@@ -3692,219 +3692,6 @@
"instance.files.save-as": {
"message": "Зберегти як..."
},
- "instance.icon-editor.background": {
- "message": "Тло"
- },
- "instance.icon-editor.background.blue": {
- "message": "Синій"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "Темно-сірий"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "Темно-зелений"
- },
- "instance.icon-editor.background.gray": {
- "message": "Сірий"
- },
- "instance.icon-editor.background.green": {
- "message": "Зелений"
- },
- "instance.icon-editor.background.indigo": {
- "message": "Індиго"
- },
- "instance.icon-editor.background.lavender": {
- "message": "Лавандовий"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "Світло-сірий"
- },
- "instance.icon-editor.background.lime": {
- "message": "Світло-зелений"
- },
- "instance.icon-editor.background.orange": {
- "message": "Жовтогарячий"
- },
- "instance.icon-editor.background.pink": {
- "message": "Рожевий"
- },
- "instance.icon-editor.background.purple": {
- "message": "Фіолетовий"
- },
- "instance.icon-editor.background.red": {
- "message": "Червоний"
- },
- "instance.icon-editor.background.rose": {
- "message": "Трояндовий"
- },
- "instance.icon-editor.background.yellow": {
- "message": "Жовтий"
- },
- "instance.icon-editor.description": {
- "message": "Поєднуйте елементи й створіть свій власний значок."
- },
- "instance.icon-editor.recents": {
- "message": "Недавні"
- },
- "instance.icon-editor.save": {
- "message": "Зберегти значок"
- },
- "instance.icon-editor.surprise-me": {
- "message": "Випадковий"
- },
- "instance.icon-editor.symbol": {
- "message": "Піктограма"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "Наплічник"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "Маяк"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "Блакитна акула"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "Книжкова полиця"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "Бурий ведмідь"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "Торт"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "Багаття"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "Скриня"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "Шестірня"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "Командний блок"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "Баняк"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "Диван"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "Верстак"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "Гайковий ключ"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "Кріпер"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "Стіл зачарування"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "Скриня Енду"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "Дракон Енду"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "Двигун"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "Піч"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Штуковина"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "Глобус"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "Трав’яний блок"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "Ліхтар"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "Квіткорова"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Пан Пакет"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "Гало"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "Кисневий розповсюджувач"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "Млинці"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "Кайло"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "Покебол"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "Блок редстоуну"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "Скалковий сенсор"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "Скелет"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "Сковорода"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "Блок слизу"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "Скафандовий шолом"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "Липкий поршень"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "Меч"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "Термінал"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "Бараболик"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "Колесо"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "Динаміт"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Гайковий ключ Modrinth"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "Зомбі"
- },
- "instance.icon-editor.title": {
- "message": "Редактор значків"
- },
"instance.last-played": {
"message": "Востаннє зіграно:"
},
diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json
index b8508d93d4c..90bad4f25f8 100644
--- a/apps/app-frontend/src/locales/zh-CN/index.json
+++ b/apps/app-frontend/src/locales/zh-CN/index.json
@@ -3701,219 +3701,6 @@
"instance.files.save-as": {
"message": "保存为……"
},
- "instance.icon-editor.background": {
- "message": "背景"
- },
- "instance.icon-editor.background.blue": {
- "message": "蓝"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "深灰"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "深绿"
- },
- "instance.icon-editor.background.gray": {
- "message": "灰"
- },
- "instance.icon-editor.background.green": {
- "message": "绿"
- },
- "instance.icon-editor.background.indigo": {
- "message": "靛青"
- },
- "instance.icon-editor.background.lavender": {
- "message": "浅紫"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "浅灰"
- },
- "instance.icon-editor.background.lime": {
- "message": "青绿"
- },
- "instance.icon-editor.background.orange": {
- "message": "橙"
- },
- "instance.icon-editor.background.pink": {
- "message": "粉"
- },
- "instance.icon-editor.background.purple": {
- "message": "紫"
- },
- "instance.icon-editor.background.red": {
- "message": "红"
- },
- "instance.icon-editor.background.rose": {
- "message": "玫瑰红"
- },
- "instance.icon-editor.background.yellow": {
- "message": "黄"
- },
- "instance.icon-editor.description": {
- "message": "挑选并混合各种元素来创建个性化的图标。"
- },
- "instance.icon-editor.recents": {
- "message": "历史记录"
- },
- "instance.icon-editor.save": {
- "message": "保存图标"
- },
- "instance.icon-editor.surprise-me": {
- "message": "随机"
- },
- "instance.icon-editor.symbol": {
- "message": "图案"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "背包"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "信标"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "蓝鲨"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "书架"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "棕熊"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "蛋糕"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "营火"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "箱子"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "齿轮"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "命令方块"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "厨锅"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "沙发"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "工作台"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "扳手"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "苦力怕"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "附魔台"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "末影箱"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "末影龙"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "便携式引擎"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "熔炉"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "坐标系"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "地球仪"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "草方块"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "灯笼"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "哞花"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "整合包先生"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "起源之球"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "氧气分配器"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "煎饼"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "镐"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "精灵球"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "红石块"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "幽匿感测体"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "骷髅"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "平底锅"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "粘液快"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "航天头盔"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "粘性活塞"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "剑"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "终端"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "小土豆"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "车轮"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinth 扳手"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "僵尸"
- },
- "instance.icon-editor.title": {
- "message": "图标编辑器"
- },
"instance.last-played": {
"message": "上次游玩"
},
diff --git a/apps/app-frontend/src/locales/zh-TW/index.json b/apps/app-frontend/src/locales/zh-TW/index.json
index 6a79155a77e..63424f81869 100644
--- a/apps/app-frontend/src/locales/zh-TW/index.json
+++ b/apps/app-frontend/src/locales/zh-TW/index.json
@@ -3701,219 +3701,6 @@
"instance.files.save-as": {
"message": "另存新檔..."
},
- "instance.icon-editor.background": {
- "message": "背景"
- },
- "instance.icon-editor.background.blue": {
- "message": "藍色"
- },
- "instance.icon-editor.background.dark-gray": {
- "message": "深灰色"
- },
- "instance.icon-editor.background.dark-green": {
- "message": "深綠色"
- },
- "instance.icon-editor.background.gray": {
- "message": "灰色"
- },
- "instance.icon-editor.background.green": {
- "message": "綠色"
- },
- "instance.icon-editor.background.indigo": {
- "message": "靛藍色"
- },
- "instance.icon-editor.background.lavender": {
- "message": "淡紫色"
- },
- "instance.icon-editor.background.light-gray": {
- "message": "淺灰色"
- },
- "instance.icon-editor.background.lime": {
- "message": "淺綠色"
- },
- "instance.icon-editor.background.orange": {
- "message": "橙色"
- },
- "instance.icon-editor.background.pink": {
- "message": "粉紅色"
- },
- "instance.icon-editor.background.purple": {
- "message": "紫色"
- },
- "instance.icon-editor.background.red": {
- "message": "紅色"
- },
- "instance.icon-editor.background.rose": {
- "message": "玫瑰色"
- },
- "instance.icon-editor.background.yellow": {
- "message": "黃色"
- },
- "instance.icon-editor.description": {
- "message": "自由搭配各種元素,打造專屬圖示。"
- },
- "instance.icon-editor.recents": {
- "message": "最近"
- },
- "instance.icon-editor.save": {
- "message": "儲存圖示"
- },
- "instance.icon-editor.surprise-me": {
- "message": "隨機產生"
- },
- "instance.icon-editor.symbol": {
- "message": "圖案"
- },
- "instance.icon-editor.symbol.backpack": {
- "message": "背包"
- },
- "instance.icon-editor.symbol.beacon": {
- "message": "烽火台"
- },
- "instance.icon-editor.symbol.blue-shark": {
- "message": "藍鯊"
- },
- "instance.icon-editor.symbol.bookshelf": {
- "message": "書櫃"
- },
- "instance.icon-editor.symbol.brown-bear": {
- "message": "棕熊"
- },
- "instance.icon-editor.symbol.cake": {
- "message": "蛋糕"
- },
- "instance.icon-editor.symbol.campfire": {
- "message": "營火"
- },
- "instance.icon-editor.symbol.chest": {
- "message": "儲物箱"
- },
- "instance.icon-editor.symbol.cogwheel": {
- "message": "齒輪"
- },
- "instance.icon-editor.symbol.command-block": {
- "message": "指令方塊"
- },
- "instance.icon-editor.symbol.cooking-pot": {
- "message": "烹飪鍋"
- },
- "instance.icon-editor.symbol.couch": {
- "message": "沙發"
- },
- "instance.icon-editor.symbol.crafting-table": {
- "message": "工作台"
- },
- "instance.icon-editor.symbol.create-wrench": {
- "message": "扳手"
- },
- "instance.icon-editor.symbol.creeper": {
- "message": "苦力怕"
- },
- "instance.icon-editor.symbol.enchanting-table": {
- "message": "附魔台"
- },
- "instance.icon-editor.symbol.ender-chest": {
- "message": "終界箱"
- },
- "instance.icon-editor.symbol.ender-dragon": {
- "message": "終界龍"
- },
- "instance.icon-editor.symbol.engine": {
- "message": "引擎"
- },
- "instance.icon-editor.symbol.fabric": {
- "message": "Fabric"
- },
- "instance.icon-editor.symbol.forge": {
- "message": "Forge"
- },
- "instance.icon-editor.symbol.furnace": {
- "message": "熔爐"
- },
- "instance.icon-editor.symbol.gizmo": {
- "message": "Gizmo"
- },
- "instance.icon-editor.symbol.globe": {
- "message": "地球儀"
- },
- "instance.icon-editor.symbol.grass-block": {
- "message": "草地"
- },
- "instance.icon-editor.symbol.lantern": {
- "message": "燈籠"
- },
- "instance.icon-editor.symbol.moobloom": {
- "message": "哞花"
- },
- "instance.icon-editor.symbol.mr-pack": {
- "message": "Mr Pack"
- },
- "instance.icon-editor.symbol.neoforge": {
- "message": "NeoForge"
- },
- "instance.icon-editor.symbol.orb": {
- "message": "起源之球"
- },
- "instance.icon-editor.symbol.oxygen-distributor": {
- "message": "氧氣分配器"
- },
- "instance.icon-editor.symbol.pancakes": {
- "message": "鬆餅"
- },
- "instance.icon-editor.symbol.pickaxe": {
- "message": "鎬子"
- },
- "instance.icon-editor.symbol.poke-ball": {
- "message": "精靈球"
- },
- "instance.icon-editor.symbol.quilt": {
- "message": "Quilt"
- },
- "instance.icon-editor.symbol.redstone-block": {
- "message": "紅石方塊"
- },
- "instance.icon-editor.symbol.sculk-sensor": {
- "message": "伏聆振測器"
- },
- "instance.icon-editor.symbol.skeleton": {
- "message": "骷髏"
- },
- "instance.icon-editor.symbol.skillet": {
- "message": "平底鍋"
- },
- "instance.icon-editor.symbol.slime-block": {
- "message": "史萊姆方塊"
- },
- "instance.icon-editor.symbol.space-helmet": {
- "message": "太空頭盔"
- },
- "instance.icon-editor.symbol.sticky-piston": {
- "message": "黏性活塞"
- },
- "instance.icon-editor.symbol.sword": {
- "message": "劍"
- },
- "instance.icon-editor.symbol.terminal": {
- "message": "終端機"
- },
- "instance.icon-editor.symbol.tiny-potato": {
- "message": "小小馬鈴薯"
- },
- "instance.icon-editor.symbol.tire": {
- "message": "車輪"
- },
- "instance.icon-editor.symbol.tnt": {
- "message": "TNT"
- },
- "instance.icon-editor.symbol.wrenth-rinth": {
- "message": "Modrinth 扳手"
- },
- "instance.icon-editor.symbol.zombie": {
- "message": "殭屍"
- },
- "instance.icon-editor.title": {
- "message": "圖示編輯器"
- },
"instance.last-played": {
"message": "最後遊玩時間:"
},
diff --git a/apps/app-frontend/src/pages/instance/layout.vue b/apps/app-frontend/src/pages/instance/layout.vue
index fa64e6cd35e..f49269a5a5b 100644
--- a/apps/app-frontend/src/pages/instance/layout.vue
+++ b/apps/app-frontend/src/pages/instance/layout.vue
@@ -390,7 +390,9 @@ const checkingSharedInstanceLaunch = ref(false)
const subpagePending = ref(false)
const stopping = ref(false)
const serverOfflineModal = ref>()
-const pendingOfflineLaunch = ref<{ instanceId: string; context: string; address?: string } | null>(null)
+const pendingOfflineLaunch = ref<{ instanceId: string; context: string; address?: string } | null>(
+ null,
+)
const exportModal = ref>()
const updateToPlayModal = ref>()
const sharedInstanceUpdateModal = ref>()
@@ -452,12 +454,11 @@ const serverStatusQuery = useQuery({
refetchInterval: 30_000,
retry: false,
})
-const statusOnline = computed(
- () =>
- hosting.isHostingInstance.value
- ? hosting.onlineStatus.value === 'running'
- : !serverStatusQuery.isError.value &&
- (!!serverStatusQuery.data.value || !!javaServerPingData.value),
+const statusOnline = computed(() =>
+ hosting.isHostingInstance.value
+ ? hosting.onlineStatus.value === 'running'
+ : !serverStatusQuery.isError.value &&
+ (!!serverStatusQuery.data.value || !!javaServerPingData.value),
)
const playersOnline = computed(() =>
serverStatusQuery.isError.value ? undefined : serverStatusQuery.data.value?.players?.online,
diff --git a/apps/app-frontend/src/providers/setup/server-install-content.ts b/apps/app-frontend/src/providers/setup/server-install-content.ts
index 299473356da..dda998657bc 100644
--- a/apps/app-frontend/src/providers/setup/server-install-content.ts
+++ b/apps/app-frontend/src/providers/setup/server-install-content.ts
@@ -24,7 +24,6 @@ import { useRoute, useRouter } from 'vue-router'
import { config as appConfig } from '@/config'
-
type ServerFlowFrom = 'onboarding' | 'reset-server'
type InstallableSearchResult = Labrinth.Search.v3.ResultSearchProject & {
diff --git a/apps/frontend/src/app.vue b/apps/frontend/src/app.vue
index 5917d752196..c5694b6f9b8 100644
--- a/apps/frontend/src/app.vue
+++ b/apps/frontend/src/app.vue
@@ -16,13 +16,13 @@
+
+
+
+ {{ formatMessage(messages.unpublishedBody) }}
+
+
+
+
+ {{
+ formatMessage(
+ publishing
+ ? messages.publishing
+ : reviewing
+ ? messages.reviewing
+ : messages.pushUpdate,
+ )
+ }}
+
+
+
+
diff --git a/packages/ui/src/components/servers/edit-server-icon/EditServerIcon.vue b/packages/ui/src/components/servers/edit-server-icon/EditServerIcon.vue
index b072714c6d4..527947f45d8 100644
--- a/packages/ui/src/components/servers/edit-server-icon/EditServerIcon.vue
+++ b/packages/ui/src/components/servers/edit-server-icon/EditServerIcon.vue
@@ -245,15 +245,20 @@ async function uploadIcon(file: File, config: IconConfig | null) {
} catch (error) {
console.debug('Server icon original upload failed:', error)
}
- await deleteFile(fsAuth, configPath)
- if (config) {
- const configFile = new File([JSON.stringify(config)], 'server-icon-config.json', {
- type: 'application/json',
- })
- await client.kyros.files_v0.uploadFileWithAuth(fsAuth, configPath, configFile).promise
+ let configFailed = false
+ try {
+ await deleteFile(fsAuth, configPath)
+ if (config) {
+ const configFile = new File([JSON.stringify(config)], 'server-icon-config.json', {
+ type: 'application/json',
+ })
+ await client.kyros.files_v0.uploadFileWithAuth(fsAuth, configPath, configFile).promise
+ }
+ generatedConfig.value = config
+ if (config) await saveRecentConfig(config)
+ } catch {
+ configFailed = true
}
- generatedConfig.value = config
- if (config) await saveRecentConfig(config)
const dataURL = await processImageBlob(file, 512)
setImage(dataURL)
@@ -261,11 +266,14 @@ async function uploadIcon(file: File, config: IconConfig | null) {
const remoteIcon = await refetchRemoteIcon()
if (remoteIcon.data) resetLocalOverride()
await queryClient.invalidateQueries({ queryKey: ['server-icon', serverId] })
+ if (configFailed) await loadGeneratedConfig()
addNotification({
- type: 'success',
- title: 'Server icon updated',
- text: 'Your server icon was successfully changed.',
+ type: configFailed ? 'error' : 'success',
+ title: configFailed ? 'Icon editor settings not saved' : 'Server icon updated',
+ text: configFailed
+ ? 'The server icon was updated, but its editor settings could not be saved.'
+ : 'Your server icon was successfully changed.',
})
} finally {
isUploadingIcon.value = false
diff --git a/packages/ui/src/components/servers/labels/ServerSubdomainLabel.vue b/packages/ui/src/components/servers/labels/ServerSubdomainLabel.vue
index 7381e0204c0..61872214b66 100644
--- a/packages/ui/src/components/servers/labels/ServerSubdomainLabel.vue
+++ b/packages/ui/src/components/servers/labels/ServerSubdomainLabel.vue
@@ -24,10 +24,11 @@
diff --git a/packages/ui/src/composables/server-panel-sync.ts b/packages/ui/src/composables/server-panel-sync.ts
index 65b79231105..9f9cfebcc46 100644
--- a/packages/ui/src/composables/server-panel-sync.ts
+++ b/packages/ui/src/composables/server-panel-sync.ts
@@ -391,7 +391,7 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
disabled_server: item.disabled_server ?? current?.disabled_server ?? false,
disabled_player: item.disabled_player ?? current?.disabled_player ?? false,
side_toggle_unlocked: item.side_toggle_unlocked ?? current?.side_toggle_unlocked ?? false,
- manifest: item.manifest ?? null,
+ manifest: item.manifest === undefined ? (current?.manifest ?? null) : item.manifest,
kind: parentDirectoryToAddonKind(item.parent_directory),
from_modpack: item.from_modpack,
status: item.status,
diff --git a/packages/ui/src/composables/server-preferences.ts b/packages/ui/src/composables/server-preferences.ts
index 51a681aaa05..0b7d837b6d2 100644
--- a/packages/ui/src/composables/server-preferences.ts
+++ b/packages/ui/src/composables/server-preferences.ts
@@ -7,6 +7,7 @@ export function useServerPreferences(serverId: string) {
hideSubdomainLabel: false,
ramAsNumber: false,
warnOnIncompatibleContent: true,
+ reviewChangesBeforePlaying: false,
},
undefined,
{ mergeDefaults: true },
diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
index ac5e62c7b2e..a2eba164121 100644
--- a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
+++ b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
@@ -177,11 +177,7 @@ const toggleTooltip = computed(() => {
if (!isToggleDisabled.value) return undefined
return props.toggleDisabledTooltip ?? props.disabledTooltip ?? undefined
})
-const isContentDisabled = computed(
- () =>
- props.enabled === false ||
- (props.enabledFor && !props.enabledFor.server && !props.enabledFor.player),
-)
+const isContentDisabled = computed(() => props.enabled === false)
const clientWarningMessage = computed(() => {
switch (props.clientWarning) {
diff --git a/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts b/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts
index 4c5d3ffb8de..75cae3d9cba 100644
--- a/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts
+++ b/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts
@@ -5,7 +5,7 @@ import { computed, ref, watch } from 'vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonProjectTypeCategoryMessages, normalizeProjectType } from '#ui/utils/common-messages'
-import type { ClientWarningType, ContentItem } from '../types'
+import type { ClientWarningType, ContentItem, ContentWarningType } from '../types'
const CLIENT_ONLY_ENVIRONMENTS = new Set(['client_only', 'singleplayer_only'])
@@ -26,14 +26,9 @@ export function getClientWarningType(
export function getContentWarningType(
item: ContentItem,
showEnvironmentWarnings = false,
-): ClientWarningType | 'unknown-environment' | null {
+): ContentWarningType | null {
if (!item.enabledFor) return getClientWarningType(item, showEnvironmentWarnings)
- if (!item.enabledFor.warningTooltip) return null
- if (item.enabledFor.server) {
- if (item.pack_client_retained) return 'retained'
- if (item.pack_client_depends) return 'depends'
- }
- return 'unknown-environment'
+ return item.enabledFor.warningKind ?? null
}
export interface ContentFilterOption {
diff --git a/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts b/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts
index b9f2d4590bd..02fd02d3760 100644
--- a/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts
+++ b/packages/ui/src/layouts/shared/content-tab/composables/use-content-metadata-filters.ts
@@ -132,10 +132,14 @@ const messages = defineMessages({
id: 'content.metadata-filter.warning.client-depends',
defaultMessage: 'Client depends on file',
},
- clientOnly: {
- id: 'content.metadata-filter.warning.client-only',
- defaultMessage: 'Client-only content',
- },
+ clientOnly: {
+ id: 'content.metadata-filter.warning.client-only',
+ defaultMessage: 'Client-only content',
+ },
+ serverOnly: {
+ id: 'content.metadata-filter.warning.server-only',
+ defaultMessage: 'Server-only content',
+ },
unknownEnvironment: {
id: 'content.metadata-filter.warning.unknown-environment',
defaultMessage: 'Unknown environment',
@@ -258,6 +262,8 @@ export function useContentMetadataFilters(
return [option(warning, formatMessage(messages.clientDepends))]
case 'environment':
return [option(warning, formatMessage(messages.clientOnly))]
+ case 'server-only':
+ return [option(warning, formatMessage(messages.serverOnly))]
case 'unknown-environment':
return [option(warning, formatMessage(messages.unknownEnvironment))]
default:
diff --git a/packages/ui/src/layouts/shared/content-tab/types.ts b/packages/ui/src/layouts/shared/content-tab/types.ts
index b9a9b7734e9..03921f4e2f2 100644
--- a/packages/ui/src/layouts/shared/content-tab/types.ts
+++ b/packages/ui/src/layouts/shared/content-tab/types.ts
@@ -28,6 +28,7 @@ export interface ContentSource {
}
export type ClientWarningType = 'retained' | 'depends' | 'environment'
+export type ContentWarningType = ClientWarningType | 'server-only' | 'unknown-environment'
export type ContentSourceKind =
| 'local'
@@ -59,6 +60,7 @@ export interface ContentEnabledForState {
lockedTooltip?: string
disabledSides?: ContentSide[]
warningTooltip?: string | null
+ warningKind?: ContentWarningType | null
}
export interface ContentCardEmbeddedIcon {
diff --git a/packages/ui/src/layouts/shared/invited-players/composables/use-invited-players-table.ts b/packages/ui/src/layouts/shared/invited-players/composables/use-invited-players-table.ts
index 6f5bb500b3b..e7ef7db16ef 100644
--- a/packages/ui/src/layouts/shared/invited-players/composables/use-invited-players-table.ts
+++ b/packages/ui/src/layouts/shared/invited-players/composables/use-invited-players-table.ts
@@ -4,7 +4,6 @@ import type { SortDirection } from '#ui/components/base/Table.vue'
import {
type InvitedPlayerMethod,
- invitedPlayerMethodLabels as methodLabels,
type InvitedPlayerRow,
} from '../types'
@@ -13,15 +12,16 @@ type MethodFilter = InvitedPlayerMethod | 'all'
export function useInvitedPlayersTable(
rows: Ref,
formatRelativeTime: (date: Date) => string,
+ labels: Ref<{ methods: Record; never: string; pending: string }>,
) {
const search = ref('')
const methodFilter = ref('all')
const sortColumn = ref('joined')
const sortDirection = ref('desc')
- const methodFilterOptions: Array<{ id: InvitedPlayerMethod; label: string }> = [
- { id: 'direct', label: methodLabels.direct },
- { id: 'link', label: methodLabels.link },
- ]
+ const methodFilterOptions = computed>(() => [
+ { id: 'direct', label: labels.value.methods.direct },
+ { id: 'link', label: labels.value.methods.link },
+ ])
const hasMultipleMethods = computed(() => new Set(rows.value.map((row) => row.method)).size > 1)
const filteredRows = computed(() => {
const query = search.value.trim().toLowerCase()
@@ -30,9 +30,9 @@ export function useInvitedPlayersTable(
if (!query) return true
return [
row.username,
- row.lastPlayedAt ? formatRelativeTime(row.lastPlayedAt) : 'Never',
- row.pending ? 'Pending' : row.joinedAt ? formatRelativeTime(row.joinedAt) : '',
- methodLabels[row.method],
+ row.lastPlayedAt ? formatRelativeTime(row.lastPlayedAt) : labels.value.never,
+ row.pending ? labels.value.pending : row.joinedAt ? formatRelativeTime(row.joinedAt) : '',
+ labels.value.methods[row.method],
].some((value) => value.toLowerCase().includes(query))
})
})
@@ -46,7 +46,7 @@ export function useInvitedPlayersTable(
(a.lastPlayedAt?.getTime() ?? Number.NEGATIVE_INFINITY) -
(b.lastPlayedAt?.getTime() ?? Number.NEGATIVE_INFINITY)
else if (sortColumn.value === 'method')
- compared = methodLabels[a.method].localeCompare(methodLabels[b.method])
+ compared = labels.value.methods[a.method].localeCompare(labels.value.methods[b.method])
else
compared =
(a.pending
diff --git a/packages/ui/src/layouts/shared/invited-players/layout.vue b/packages/ui/src/layouts/shared/invited-players/layout.vue
index c6156273b44..0f99e8a15d5 100644
--- a/packages/ui/src/layouts/shared/invited-players/layout.vue
+++ b/packages/ui/src/layouts/shared/invited-players/layout.vue
@@ -5,7 +5,7 @@
- All
+ {{ formatMessage(messages.all) }}
+
{{
- formatMessage(rows.length === 0 ? messages.noUsersJoined : messages.noUsersMatchFilters)
+ formatMessage(
+ loading
+ ? messages.loadingUsers
+ : rows.length === 0
+ ? messages.noUsersJoined
+ : messages.noUsersMatchFilters,
+ )
}}
@@ -82,7 +89,7 @@
>
Pending{{ formatMessage(messages.pending) }}
{{
formatRelativeTime(row.joinedAt)
@@ -110,22 +117,22 @@
{{
formatRelativeTime(row.lastPlayedAt)
}}
- Never
+ {{ formatMessage(messages.never) }}
- {{ methodLabels[row.method] }}
+ {{ labels.methods[row.method] }}
@@ -159,13 +166,14 @@ import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { truncatedTooltip } from '#ui/utils/truncate'
import { useInvitedPlayersTable } from './composables/use-invited-players-table'
-import { invitedPlayerMethodLabels as methodLabels, type InvitedPlayerRow } from './types'
+import { invitedPlayerMethodMessages, type InvitedPlayerRow } from './types'
type InvitedPlayerColumn = 'username' | 'lastPlayed' | 'joined' | 'method' | 'actions'
const props = withDefaults(
defineProps<{
rows: InvitedPlayerRow[]
+ loading?: boolean
canManage?: boolean
disabled?: boolean
showPushUpdate?: boolean
@@ -188,6 +196,51 @@ const usernameRefs = ref>({})
const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime({ style: 'narrow' })
const formatDateTime = useFormatDateTime({ dateStyle: 'medium', timeStyle: 'short' })
+const messages = defineMessages({
+ pushUpdate: {
+ id: 'app.instance.admonitions.shared-instance.publish-button',
+ defaultMessage: 'Push update',
+ },
+ invitePlayers: {
+ id: 'servers.play.card.app.invite-button',
+ defaultMessage: 'Invite players',
+ },
+ searchUsers: {
+ id: 'servers.play.players.search-users',
+ defaultMessage: 'Search {count} users...',
+ },
+ all: { id: 'servers.play.players.all', defaultMessage: 'All' },
+ username: { id: 'servers.play.players.username', defaultMessage: 'Username' },
+ joined: { id: 'servers.play.players.joined', defaultMessage: 'Joined' },
+ lastPlayed: { id: 'servers.play.players.last-played', defaultMessage: 'Last played' },
+ method: { id: 'servers.play.players.method', defaultMessage: 'Method' },
+ actions: { id: 'servers.play.players.actions', defaultMessage: 'Actions' },
+ pending: { id: 'servers.play.players.pending', defaultMessage: 'Pending' },
+ never: { id: 'servers.play.players.never', defaultMessage: 'Never' },
+ avatarAlt: { id: 'servers.play.players.avatar-alt', defaultMessage: "{username}'s avatar" },
+ revokeAccess: { id: 'servers.play.players.revoke-access', defaultMessage: 'Revoke access' },
+ revokeAccessFor: {
+ id: 'servers.play.players.revoke-access-for',
+ defaultMessage: 'Revoke access for {username}',
+ },
+ loadingUsers: { id: 'servers.play.players.loading', defaultMessage: 'Loading users...' },
+ noUsersJoined: {
+ id: 'app.instance.share.members.empty',
+ defaultMessage: 'No users have joined yet',
+ },
+ noUsersMatchFilters: {
+ id: 'app.instance.share.members.no-filter-results',
+ defaultMessage: 'No users match your filters.',
+ },
+})
+const labels = computed(() => ({
+ methods: {
+ direct: formatMessage(invitedPlayerMethodMessages.direct),
+ link: formatMessage(invitedPlayerMethodMessages.link),
+ },
+ never: formatMessage(messages.never),
+ pending: formatMessage(messages.pending),
+}))
const {
search,
methodFilter,
@@ -197,13 +250,13 @@ const {
hasMultipleMethods,
sortedRows,
toggleMethodFilter,
-} = useInvitedPlayersTable(toRef(props, 'rows'), formatRelativeTime)
+} = useInvitedPlayersTable(toRef(props, 'rows'), formatRelativeTime, labels)
const columns = computed[]>(() => {
const result: TableColumn[] = [
{
key: 'username',
- label: 'Username',
+ label: formatMessage(messages.username),
width: 'clamp(14rem, 30%, 26rem)',
enableSorting: true,
headerClass: '!pr-3',
@@ -211,7 +264,7 @@ const columns = computed[]>(() => {
},
{
key: 'joined',
- label: 'Joined',
+ label: formatMessage(messages.joined),
width: 'clamp(7rem, 14%, 12rem)',
enableSorting: true,
defaultSortDirection: 'desc',
@@ -220,7 +273,7 @@ const columns = computed[]>(() => {
},
{
key: 'lastPlayed',
- label: 'Last played',
+ label: formatMessage(messages.lastPlayed),
width: 'clamp(7rem, 15%, 13rem)',
enableSorting: true,
headerClass: 'whitespace-nowrap !px-2',
@@ -228,7 +281,7 @@ const columns = computed[]>(() => {
},
{
key: 'method',
- label: 'Method',
+ label: formatMessage(messages.method),
enableSorting: true,
headerClass: 'whitespace-nowrap !px-2',
cellClass: 'whitespace-nowrap !px-2',
@@ -237,7 +290,7 @@ const columns = computed[]>(() => {
if (props.canManage)
result.push({
key: 'actions',
- label: 'Actions',
+ label: formatMessage(messages.actions),
align: 'right',
width: 'clamp(5.5rem, 7%, 7rem)',
headerClass: 'whitespace-nowrap !pl-2 !pr-4',
@@ -255,24 +308,6 @@ function filterClass(active: boolean) {
]
}
-const messages = defineMessages({
- pushUpdate: {
- id: 'app.instance.admonitions.shared-instance.publish-button',
- defaultMessage: 'Push update',
- },
- invitePlayers: {
- id: 'servers.play.card.app.invite-button',
- defaultMessage: 'Invite players',
- },
- noUsersJoined: {
- id: 'app.instance.share.members.empty',
- defaultMessage: 'No users have joined yet',
- },
- noUsersMatchFilters: {
- id: 'app.instance.share.members.no-filter-results',
- defaultMessage: 'No users match your filters.',
- },
-})
function userProfileLink(username: string) {
return !username || username.includes('@') ? undefined : `/user/${encodeURIComponent(username)}`
}
diff --git a/packages/ui/src/layouts/shared/invited-players/types.ts b/packages/ui/src/layouts/shared/invited-players/types.ts
index 8e559575f6e..cad7f867c36 100644
--- a/packages/ui/src/layouts/shared/invited-players/types.ts
+++ b/packages/ui/src/layouts/shared/invited-players/types.ts
@@ -1,3 +1,5 @@
+import { defineMessages } from '#ui/composables/i18n'
+
export type InvitedPlayerMethod = 'direct' | 'link'
export type InvitedPlayerRow = {
@@ -10,7 +12,7 @@ export type InvitedPlayerRow = {
pending?: boolean
}
-export const invitedPlayerMethodLabels: Record = {
- direct: 'Direct invite',
- link: 'Share link',
-}
+export const invitedPlayerMethodMessages = defineMessages({
+ direct: { id: 'servers.play.players.method.direct', defaultMessage: 'Direct invite' },
+ link: { id: 'servers.play.players.method.link', defaultMessage: 'Share link' },
+})
diff --git a/packages/ui/src/layouts/shared/server-settings/pages/installation.vue b/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
index 8d48f9f60e9..59f51c8091f 100644
--- a/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
+++ b/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
@@ -426,23 +426,22 @@ async function uploadLocalModpackWithSoftOverride() {
}
async function disableAddonsEverywhere(addons: Archon.Content.v1.Addon[]) {
- await Promise.all(
- addons.flatMap((addon) => {
- const request: Archon.Content.v1.SetAddonEnabledRequest = {
- kind: addon.kind,
- filename: addon.filename,
- enabled: false,
- }
- return [
- ...(!addon.disabled_server
- ? [client.archon.content_v1.setAddonEnabledServer(serverId, worldId.value!, request)]
- : []),
- ...(!addon.disabled_player
- ? [client.archon.content_v1.setAddonEnabledPlayer(serverId, worldId.value!, request)]
- : []),
- ]
- }),
- )
+ const targetWorldId = worldId.value!
+ const requests: Array<() => Promise> = []
+ for (const addon of addons) {
+ const request: Archon.Content.v1.SetAddonEnabledRequest = {
+ kind: addon.kind,
+ filename: addon.filename,
+ enabled: false,
+ }
+ if (!addon.disabled_server)
+ requests.push(() => client.archon.content_v1.setAddonEnabledServer(serverId, targetWorldId, request))
+ if (!addon.disabled_player)
+ requests.push(() => client.archon.content_v1.setAddonEnabledPlayer(serverId, targetWorldId, request))
+ }
+ for (let index = 0; index < requests.length; index += 8) {
+ await Promise.all(requests.slice(index, index + 8).map((request) => request()))
+ }
}
provideInstallationSettings({
diff --git a/packages/ui/src/layouts/shared/server-sharing/invite-link.ts b/packages/ui/src/layouts/shared/server-sharing/invite-link.ts
index 6ce0e4dba5d..35d3162fbc3 100644
--- a/packages/ui/src/layouts/shared/server-sharing/invite-link.ts
+++ b/packages/ui/src/layouts/shared/server-sharing/invite-link.ts
@@ -5,22 +5,24 @@ const INVITE_MAX_AGE = 7 * 24 * 60 * 60
export async function ensureServerInviteLink(
client: AbstractModrinthClient,
instanceId: string,
+ remainingSlots?: number,
): Promise {
- const [links, players] = await Promise.all([
- client.sharedinstances.invites_v1.list(instanceId),
- client.sharedinstances.instances_v1.getUsers(instanceId),
- ])
+ const links = await client.sharedinstances.invites_v1.list(instanceId)
const active = links.find(
(link) => new Date(link.expiration).getTime() > Date.now() && link.uses < link.max_uses,
)
if (active) return active.id
- const remaining = Math.max(0, 50 - players.users.length - players.tokens)
- if (remaining === 0) throw new Error('This shared instance has no available player slots.')
+ let availableSlots = remainingSlots
+ if (availableSlots === undefined) {
+ const players = await client.sharedinstances.instances_v1.getUsers(instanceId)
+ availableSlots = Math.max(0, 50 - players.users.length - players.tokens)
+ }
+ if (availableSlots === 0) throw new Error('This shared instance has no available player slots.')
const invite = await client.sharedinstances.invites_v1.create(instanceId, {
max_age: INVITE_MAX_AGE,
- max_uses: Math.min(10, remaining),
+ max_uses: Math.min(10, availableSlots),
})
return invite.id
}
diff --git a/packages/ui/src/layouts/shared/server-sharing/query-options.ts b/packages/ui/src/layouts/shared/server-sharing/query-options.ts
index 532f9dceee1..f194b5c4dc0 100644
--- a/packages/ui/src/layouts/shared/server-sharing/query-options.ts
+++ b/packages/ui/src/layouts/shared/server-sharing/query-options.ts
@@ -1,5 +1,23 @@
import type { AbstractModrinthClient } from '@modrinth/api-client'
+import { resolveServerShareDiff } from './share-diff'
+
+export function serverShareDiffQueryOptions(
+ client: AbstractModrinthClient,
+ serverId: string,
+ worldId: string,
+ userId: string | undefined,
+) {
+ return {
+ queryKey: ['servers', 'share-diff', serverId, worldId, userId] as const,
+ queryFn: async () => {
+ const diff = await client.archon.content_v1.getShareDiff(serverId, worldId)
+ return { diff, items: resolveServerShareDiff(diff) }
+ },
+ retry: false,
+ }
+}
+
export function sharedInstanceInvitesQueryOptions(
client: AbstractModrinthClient,
instanceId: string,
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/share-diff.ts b/packages/ui/src/layouts/shared/server-sharing/share-diff.ts
similarity index 100%
rename from packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/share-diff.ts
rename to packages/ui/src/layouts/shared/server-sharing/share-diff.ts
diff --git a/packages/ui/src/layouts/shared/server-sharing/use-server-share-review.ts b/packages/ui/src/layouts/shared/server-sharing/use-server-share-review.ts
new file mode 100644
index 00000000000..c99d42154cf
--- /dev/null
+++ b/packages/ui/src/layouts/shared/server-sharing/use-server-share-review.ts
@@ -0,0 +1,166 @@
+import {
+ useIsFetching,
+ useIsMutating,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from '@tanstack/vue-query'
+import { computed, onScopeDispose, type Ref, ref, watch } from 'vue'
+
+import type ServerConfigFilePicker from '#ui/components/servers/ServerConfigFilePicker.vue'
+import { defineMessages, useVIntl } from '#ui/composables/i18n'
+import { useServerPermissions } from '#ui/composables/server-permissions'
+import type ContentDiffModal from '#ui/layouts/shared/installation-settings/components/ContentDiffModal.vue'
+import {
+ injectAuth,
+ injectModrinthClient,
+ injectModrinthServerContext,
+ injectNotificationManager,
+} from '#ui/providers'
+
+import { serverShareDiffQueryOptions } from './query-options'
+
+export type ServerShareActionTarget = {
+ worldId: string
+ userId: string | undefined
+ configPaths: string[]
+ isCurrent: () => boolean
+}
+
+export function useServerShareReview(options?: {
+ execute: (action: Action, target: ServerShareActionTarget) => Promise
+ disabled?: Ref
+}) {
+ const client = injectModrinthClient()
+ const auth = injectAuth()
+ const { serverId, worldId, serverFull, busyReasons } = injectModrinthServerContext()
+ const { canSetup } = useServerPermissions()
+ const { handleError } = injectNotificationManager()
+ const { formatMessage } = useVIntl()
+ const queryClient = useQueryClient()
+ const diffModal = ref>()
+ const configPicker = ref>()
+ const previewOpen = ref(false)
+ const sharedInstanceId = computed(
+ () =>
+ serverFull.value?.worlds.find((world) => world.id === worldId.value)?.content
+ ?.shared_instance_id,
+ )
+ let disposed = false
+ onScopeDispose(() => {
+ disposed = true
+ })
+ const previewQuery = useQuery(
+ computed(() => ({
+ ...serverShareDiffQueryOptions(client, serverId, worldId.value ?? '', auth.user.value?.id),
+ enabled: false,
+ })),
+ )
+ const shareActions = useIsMutating({ mutationKey: ['servers', 'share-action', serverId] })
+ const sharePreviews = useIsFetching({ queryKey: ['servers', 'share-diff', serverId] })
+ const actionMutation = useMutation({
+ mutationKey: ['servers', 'share-action', serverId],
+ mutationFn: async ({
+ action,
+ reviewed,
+ target,
+ }: {
+ action: Action
+ reviewed: boolean
+ target: ServerShareActionTarget
+ }) => {
+ if (reviewed) target.configPaths = (await configPicker.value?.resolvePaths()) ?? []
+ if (
+ !target.isCurrent() ||
+ busyReasons.value.length ||
+ (reviewed && (!previewOpen.value || !canSetup.value))
+ )
+ return
+ if (options) await options.execute(action, target)
+ else await publish(target)
+ },
+ })
+ const pending = computed(
+ () =>
+ actionMutation.isPending.value ||
+ shareActions.value > 0 ||
+ sharePreviews.value > 0 ||
+ busyReasons.value.length > 0 ||
+ !!options?.disabled?.value,
+ )
+
+ function currentTarget(): ServerShareActionTarget | undefined {
+ const targetWorldId = worldId.value
+ const userId = auth.user.value?.id
+ if (!targetWorldId || disposed) return
+ return {
+ worldId: targetWorldId,
+ userId,
+ configPaths: [],
+ isCurrent: () =>
+ !disposed && worldId.value === targetWorldId && auth.user.value?.id === userId,
+ }
+ }
+
+ async function showPreview() {
+ if (!canSetup.value || pending.value) return
+ const target = currentTarget()
+ if (!target) return
+ previewOpen.value = true
+ const result = await previewQuery.refetch()
+ if (!previewOpen.value || !target.isCurrent()) return
+ if (result.error) {
+ previewOpen.value = false
+ handleError(result.error)
+ } else diffModal.value?.show()
+ }
+
+ async function publish(target: ServerShareActionTarget) {
+ if (!canSetup.value) throw new Error(formatMessage(messages.permission))
+ const shared = await client.archon.content_v1.share(serverId, target.worldId, target.configPaths)
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: ['servers', 'v1', 'detail', serverId] }),
+ queryClient.invalidateQueries({ queryKey: ['servers', 'share-diff', serverId, target.worldId] }),
+ ])
+ return shared
+ }
+
+ async function runAction(action: Action, reviewed = false) {
+ if (pending.value) return
+ const target = currentTarget()
+ if (!target) return
+ try {
+ await actionMutation.mutateAsync({ action, reviewed, target })
+ if (target.isCurrent()) previewOpen.value = false
+ } catch (error) {
+ handleError(error)
+ if (reviewed && previewOpen.value && target.isCurrent()) diffModal.value?.show()
+ }
+ }
+
+ watch([worldId, sharedInstanceId, () => auth.user.value?.id], () => {
+ previewOpen.value = false
+ diffModal.value?.hide()
+ })
+
+ return {
+ diffModal,
+ configPicker,
+ previewOpen,
+ previewQuery,
+ shareActions,
+ sharePreviews,
+ actionMutation,
+ pending,
+ showPreview,
+ publish,
+ runAction,
+ }
+}
+
+const messages = defineMessages({
+ permission: {
+ id: 'servers.play.permission',
+ defaultMessage: 'You do not have permission to share this world.',
+ },
+})
diff --git a/packages/ui/src/layouts/shared/server-sharing/use-server-sharing.ts b/packages/ui/src/layouts/shared/server-sharing/use-server-sharing.ts
index ed6feea2c46..8f9af9d0365 100644
--- a/packages/ui/src/layouts/shared/server-sharing/use-server-sharing.ts
+++ b/packages/ui/src/layouts/shared/server-sharing/use-server-sharing.ts
@@ -1,9 +1,9 @@
import type { SharedInstances } from '@modrinth/api-client'
import { useIsMutating, useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
-import { useStorage } from '@vueuse/core'
import { computed } from 'vue'
import { useServerPermissions } from '#ui/composables/server-permissions'
+import { useServerPreferences } from '#ui/composables/server-preferences'
import {
injectAuth,
injectModrinthClient,
@@ -22,12 +22,7 @@ export function useServerSharingSettings() {
const { handleError } = injectNotificationManager()
const { serverId, worldId, serverFull, busyReasons } = injectModrinthServerContext()
const { canSetup, permissionDeniedMessage } = useServerPermissions()
- const preferences = useStorage(
- `pyro-server-${serverId}-preferences`,
- { reviewChangesBeforePlaying: false },
- undefined,
- { mergeDefaults: true },
- )
+ const preferences = useServerPreferences(serverId)
const userId = computed(() => auth.user.value?.id)
const sharedInstanceId = computed(
() =>
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
index bdf211b8098..2f03c4da0f4 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/play.vue
@@ -20,6 +20,7 @@
import { SpinnerIcon, UploadIcon } from '@modrinth/assets'
-import { useIsMutating, useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
-import { useIntersectionObserver, useStorage } from '@vueuse/core'
+import { useIntersectionObserver } from '@vueuse/core'
import { computed, nextTick, onScopeDispose, ref, watch } from 'vue'
import Admonition from '#ui/components/base/Admonition.vue'
@@ -128,8 +128,13 @@ import {
} from '#ui/components/sharing'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { useServerPermissions } from '#ui/composables/server-permissions'
+import { useServerPreferences } from '#ui/composables/server-preferences'
import ContentDiffModal from '#ui/layouts/shared/installation-settings/components/ContentDiffModal.vue'
import InvitedPlayersTableLayout from '#ui/layouts/shared/invited-players/layout.vue'
+import {
+ type ServerShareActionTarget,
+ useServerShareReview,
+} from '#ui/layouts/shared/server-sharing/use-server-share-review'
import {
getHostingServerAddress,
injectAuth,
@@ -141,7 +146,6 @@ import {
import { injectPageContext } from '#ui/providers/page-context'
import ServerPlayCard from './ServerPlayCard.vue'
-import { resolveServerShareDiff } from './share-diff'
import type { ServerPlayerRow } from './types'
import { useServerPlayers } from './use-server-players'
@@ -154,7 +158,6 @@ const props = defineProps<{
const { formatMessage } = useVIntl()
const pageContext = injectPageContext(null)
const pageBottom = ref(null)
-let disposed = false
const intercomHiddenRequestId = Symbol('server-play-bottom')
useIntersectionObserver(pageBottom, ([entry]) => {
pageContext?.intercomBubble?.requestHidden?.(
@@ -163,13 +166,11 @@ useIntersectionObserver(pageBottom, ([entry]) => {
)
})
onScopeDispose(() => {
- disposed = true
pageContext?.intercomBubble?.requestHidden?.(intercomHiddenRequestId, false)
})
const { handleError } = injectNotificationManager()
const client = injectModrinthClient()
const auth = injectAuth()
-const queryClient = useQueryClient()
const { serverId, worldId, server, serverFull, busyReasons, powerState, isConnected } =
injectModrinthServerContext()
const { canSetup, canUsePowerActions, permissionDeniedMessage } = useServerPermissions()
@@ -178,101 +179,68 @@ const sharedInstanceId = computed(() => world.value?.content?.shared_instance_id
const needsUpdate = computed(() => world.value?.content?.shared_instance_needs_update ?? false)
const players = useServerPlayers(sharedInstanceId, canSetup)
const invitePlayersModal = ref>()
-const diffModal = ref>()
-const configPicker = ref>()
-const resolvingConfigs = ref(false)
+const shareReview = useServerShareReview({
+ execute: performAction,
+ disabled: computed(() => players.linkMutation.isPending.value),
+})
+const {
+ diffModal,
+ configPicker,
+ previewOpen,
+ previewQuery,
+ actionMutation,
+ pending: actionsLocked,
+ runAction,
+} = shareReview
const removeModal = ref>()
const playerToRemove = ref()
-const previewOpen = ref(false)
const previewAction = ref<'play' | 'push'>('push')
-const preferences = useStorage(`pyro-server-${serverId}-preferences`, {
- reviewChangesBeforePlaying: false,
-})
+const preferences = useServerPreferences(serverId)
const serverAddress = computed(() =>
getHostingServerAddress(server.value.net, serverFull.value?.subdomain),
)
-const previewQuery = useQuery({
- queryKey: computed(() => ['servers', 'share-diff', serverId, worldId.value, auth.user.value?.id]),
- enabled: computed(() => previewOpen.value && !!worldId.value && !!sharedInstanceId.value),
- queryFn: async () => {
- const diff = await client.archon.content_v1.getShareDiff(serverId, worldId.value!)
- return { diff, items: resolveServerShareDiff(diff) }
- },
- retry: false,
-})
-const actionMutation = useMutation({
- mutationKey: ['servers', 'share-action', serverId],
- mutationFn: async ({
- action,
- targetWorldId,
- userId,
- configPaths,
- }: {
- action: Action
- targetWorldId: string
- userId: string | undefined
- configPaths: string[]
- }) => {
- if (busyReasons.value.length) throw new Error(formatMessage(messages.busy))
- if (auth.user.value?.id !== userId) return
- const sameContext = () =>
- !disposed && worldId.value === targetWorldId && auth.user.value?.id === userId
- let id = serverFull.value?.worlds.find((world) => world.id === targetWorldId)?.content
- ?.shared_instance_id
- if (
- canSetup.value &&
- (action !== 'play' || !id || needsUpdate.value || configPaths.length > 0)
- ) {
- const shared = await client.archon.content_v1.share(serverId, targetWorldId, configPaths)
- id = shared.shared_instance_id
- await queryClient.invalidateQueries({
- queryKey: ['servers', 'v1', 'detail', serverId],
- })
- } else if (action === 'invite' || action === 'push') {
- throw new Error(formatMessage(messages.permission))
- }
- if (!id) throw new Error(formatMessage(messages.notShared))
+async function performAction(action: Action, target: ServerShareActionTarget) {
+ const { worldId: targetWorldId, configPaths, isCurrent: sameContext } = target
+ let id = serverFull.value?.worlds.find((world) => world.id === targetWorldId)?.content
+ ?.shared_instance_id
+ const shouldShare =
+ canSetup.value &&
+ (!id ||
+ action === 'push' ||
+ ((action === 'play' || action === 'download') &&
+ (needsUpdate.value || configPaths.length > 0)))
+ if (shouldShare) {
+ const shared = await shareReview.publish(target)
+ id = shared.shared_instance_id
+ } else if (!canSetup.value && (action === 'invite' || action === 'push')) {
+ throw new Error(formatMessage(messages.permission))
+ }
+ if (!id) throw new Error(formatMessage(messages.notShared))
+ if (!sameContext()) return
+ if (action === 'play') {
+ await ensureServerRunning(sameContext)
if (!sameContext()) return
- if (action === 'play') {
- await ensureServerRunning(sameContext)
- if (!sameContext()) return
- await props.onPlayServer({ serverId, worldId: targetWorldId })
- } else if (action === 'invite') {
- await nextTick()
- await players.members.refetch({ throwOnError: true })
- if (!sameContext()) return
- await players.ensureLink(id)
- if (sameContext()) invitePlayersModal.value?.show()
- } else if (action === 'download') {
- const latest = await client.sharedinstances.instances_v1.getLatestVersion(id)
- if (!latest.ready) throw new Error(formatMessage(messages.notReady))
- const blob = await client.sharedinstances.instances_v1.downloadMrpack(id, latest.version)
- if (sameContext())
- await props.onDownloadMrpack(
- blob,
- `${server.value.name.replace(/[\\/:*?"<>|]/g, '_')}.mrpack`,
- )
- } else {
- previewOpen.value = false
- await queryClient.invalidateQueries({
- queryKey: ['servers', 'share-diff', serverId, targetWorldId],
- })
- }
- },
- onError: (error) => handleError(error),
-})
+ await props.onPlayServer({ serverId, worldId: targetWorldId })
+ } else if (action === 'invite') {
+ await nextTick()
+ const members = await players.members.refetch({ throwOnError: true })
+ if (!sameContext()) return
+ await players.ensureLink(id, members.data?.remaining)
+ if (sameContext()) invitePlayersModal.value?.show()
+ } else if (action === 'download') {
+ const latest = await client.sharedinstances.instances_v1.getLatestVersion(id)
+ if (!latest.ready) throw new Error(formatMessage(messages.notReady))
+ const blob = await client.sharedinstances.instances_v1.downloadMrpack(id, latest.version)
+ if (sameContext())
+ await props.onDownloadMrpack(
+ blob,
+ `${server.value.name.replace(/[\\/:*?"<>|]/g, '_')}.mrpack`,
+ )
+ }
+}
const pendingAction = computed(() =>
actionMutation.isPending.value ? actionMutation.variables.value?.action : undefined,
)
-const shareActions = useIsMutating({ mutationKey: ['servers', 'share-action', serverId] })
-const actionsLocked = computed(
- () =>
- resolvingConfigs.value ||
- shareActions.value > 0 ||
- previewQuery.isFetching.value ||
- players.linkMutation.isPending.value ||
- busyReasons.value.length > 0,
-)
async function ensureServerRunning(sameContext: () => boolean) {
const deadline = Date.now() + 180_000
let startRequested = false
@@ -308,39 +276,12 @@ async function perform(action: Action, reviewed = false) {
void showPreview(true)
return
}
- const targetWorldId = worldId.value
- const userId = auth.user.value?.id
- resolvingConfigs.value = true
- try {
- const configPaths = reviewed ? ((await configPicker.value?.resolvePaths()) ?? []) : []
- if (
- worldId.value !== targetWorldId ||
- auth.user.value?.id !== userId ||
- (reviewed && !previewOpen.value)
- )
- return
- previewOpen.value = false
- actionMutation.mutate({ action, targetWorldId, userId, configPaths })
- } catch (error) {
- handleError(error)
- if (previewOpen.value && worldId.value === targetWorldId && auth.user.value?.id === userId)
- diffModal.value?.show()
- } finally {
- resolvingConfigs.value = false
- }
+ await runAction(action, reviewed)
}
async function showPreview(playAfter = false) {
if (actionsLocked.value) return
- const target = worldId.value
previewAction.value = playAfter ? 'play' : 'push'
- previewOpen.value = true
- const result = await previewQuery.refetch()
- if (target !== worldId.value) return
- if (!previewOpen.value) return
- if (result.error) {
- previewOpen.value = false
- handleError(result.error)
- } else diffModal.value?.show()
+ await shareReview.showPreview()
}
function changeMember(userId: string, remove: boolean, user?: InvitePlayersUser) {
if (!sharedInstanceId.value || players.membershipMutation.isPending.value || !canSetup.value)
@@ -370,9 +311,7 @@ async function updateInviteLink(settings: InviteLinkSettings) {
}
watch([worldId, sharedInstanceId, () => auth.user.value?.id], () => {
invitePlayersModal.value?.hide()
- diffModal.value?.hide()
removeModal.value?.hide()
- previewOpen.value = false
playerToRemove.value = undefined
})
const messages = defineMessages({
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
index fabce9e7bc2..15eac607c00 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/play/use-server-players.ts
@@ -165,8 +165,8 @@ export function useServerPlayers(instanceId: Ref, canManage: Ref<
queryClient.invalidateQueries({ queryKey: ['shared-instances', id, 'invites'] }),
})
- async function ensureLink(id: string) {
- await ensureServerInviteLink(client, id)
+ async function ensureLink(id: string, remainingSlots?: number) {
+ await ensureServerInviteLink(client, id, remainingSlots)
await queryClient.invalidateQueries({ queryKey: ['shared-instances', id, 'invites'] })
}
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
index a1ae60c49f4..409ffeb7d62 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
@@ -38,7 +38,7 @@ import ContentUpdaterModal from '../../../shared/content-tab/components/modals/c
import ContentPageLayout from '../../../shared/content-tab/layout.vue'
import type { ManagedContentData } from '../../../shared/content-tab/providers/content-manager'
import { provideContentManager } from '../../../shared/content-tab/providers/content-manager'
-import type { ContentItem, ContentSide } from '../../../shared/content-tab/types'
+import type { ContentItem, ContentSide, ContentWarningType } from '../../../shared/content-tab/types'
import { summarizeManagedContent } from '../../../shared/content-tab/utils/managed-content'
type AddonWithUiState = Archon.Content.v1.Addon & { installing?: boolean }
@@ -354,23 +354,31 @@ function isIncompatibleEnvironment(addon: Archon.Content.v1.Addon, side: Content
: environment === 'server_only' || environment === 'dedicated_server_only'
}
-function getEnabledForWarning(addon: Archon.Content.v1.Addon) {
+function getEnabledForWarning(
+ addon: Archon.Content.v1.Addon,
+): { kind: ContentWarningType; tooltip: string } | null {
if (isPlayerOnlyContent(addon)) return null
if (!addon.disabled_server) {
- if (addon.pack_client_retained) return formatMessage(commonMessages.clientRetainedWarning)
- if (addon.pack_client_depends) return formatMessage(commonMessages.clientDependsWarning)
+ if (addon.pack_client_retained)
+ return { kind: 'retained', tooltip: formatMessage(commonMessages.clientRetainedWarning) }
+ if (addon.pack_client_depends)
+ return { kind: 'depends', tooltip: formatMessage(commonMessages.clientDependsWarning) }
if (isIncompatibleEnvironment(addon, 'server')) {
- return formatMessage(
- getAddonEnvironment(addon) === 'singleplayer_only'
- ? messages.singleplayerOnlyEnabledForServer
- : messages.clientOnlyEnabledForServer,
- )
+ return {
+ kind: 'environment',
+ tooltip: formatMessage(
+ getAddonEnvironment(addon) === 'singleplayer_only'
+ ? messages.singleplayerOnlyEnabledForServer
+ : messages.clientOnlyEnabledForServer,
+ ),
+ }
}
}
if (!addon.disabled_player && isIncompatibleEnvironment(addon, 'player')) {
- return formatMessage(messages.serverOnlyEnabledForPlayers)
+ return { kind: 'server-only', tooltip: formatMessage(messages.serverOnlyEnabledForPlayers) }
}
- if (!hasDetectedEnvironment(addon)) return formatMessage(messages.unknownEnvironment)
+ if (!hasDetectedEnvironment(addon))
+ return { kind: 'unknown-environment', tooltip: formatMessage(messages.unknownEnvironment) }
return null
}
@@ -1089,6 +1097,7 @@ function addonToContentItem(addon: AddonWithUiState): ContentItem {
const serverEnabled = !isPlayerOnlyContent(addon) && !addon.disabled_server
const playerEnabled = !addon.disabled_player
const lockedSides: ContentSide[] = isPlayerOnlyContent(addon) ? ['server'] : []
+ const warning = getEnabledForWarning(addon)
return {
project: {
...(projectMetadata ?? {}),
@@ -1129,7 +1138,8 @@ function addonToContentItem(addon: AddonWithUiState): ContentItem {
)
: undefined,
disabledSides: lockedSides,
- warningTooltip: getEnabledForWarning(addon),
+ warningTooltip: warning?.tooltip,
+ warningKind: warning?.kind,
},
embeddedIcon,
file_name: addon.filename,
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
index ec40e9bf080..253db884cd1 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
@@ -337,7 +337,6 @@ import {
UsersIcon,
} from '@modrinth/assets'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
-import { useStorage } from '@vueuse/core'
import DOMPurify from 'dompurify'
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
@@ -379,6 +378,7 @@ import type {
} from '#ui/composables/server-installation-tracker'
import { useServerManageCoreRuntime } from '#ui/composables/server-manage-core-runtime'
import { useServerPanelSync } from '#ui/composables/server-panel-sync'
+import { useServerPreferences } from '#ui/composables/server-preferences'
import type { LogLine } from '#ui/layouts/shared/console'
import type { ServerSettingsTabId } from '#ui/layouts/shared/server-settings'
import {
@@ -477,9 +477,7 @@ const isReconnecting = ref(false)
const isLoading = ref(true)
const isMounted = ref(true)
-const serverPreferences = useStorage(`pyro-server-${props.serverId}-preferences`, {
- hideSubdomainLabel: false,
-})
+const serverPreferences = useServerPreferences(props.serverId)
const serverSettingsModal = ref | null>(null)
const confirmLeaveModal = ref