mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 18:45:15 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a03ab1354a | ||
|
|
0748b8732e | ||
|
|
b0c77bb395 | ||
|
|
cba59f9848 | ||
|
|
08901e725f | ||
|
|
0fbd431169 |
@@ -907,17 +907,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
|
||||
@@ -926,16 +927,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) {
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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<void> {
|
||||
await this.client.request<void>(
|
||||
`/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<void> {
|
||||
await this.client.request<void>(
|
||||
`/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<void> {
|
||||
await this.client.request<void>(
|
||||
`/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,
|
||||
|
||||
@@ -301,6 +301,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'
|
||||
@@ -316,6 +335,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
|
||||
@@ -354,6 +377,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
|
||||
@@ -1063,6 +1094,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
|
||||
|
||||
@@ -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<Blob> {
|
||||
return this.client.request<Blob>(`/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
|
||||
*
|
||||
|
||||
@@ -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-none"
|
||||
: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 @@
|
||||
<span
|
||||
class="rounded-full transition-all duration-200"
|
||||
:class="[
|
||||
small ? 'w-3 h-3' : 'w-4 h-4',
|
||||
modelValue
|
||||
indeterminate
|
||||
? small
|
||||
? 'translate-x-[20px] bg-black/90'
|
||||
: 'translate-x-[24px] bg-black/90'
|
||||
: 'bg-gray',
|
||||
? 'h-0.5 w-2.5 translate-x-[11px] bg-black/90'
|
||||
: 'h-0.5 w-3 translate-x-[14px] bg-black/90'
|
||||
: modelValue
|
||||
? small
|
||||
? 'h-3 w-3 translate-x-[20px] bg-black/90'
|
||||
: 'h-4 w-4 translate-x-[24px] bg-black/90'
|
||||
: small
|
||||
? 'h-3 w-3 bg-gray'
|
||||
: 'h-4 w-4 bg-gray',
|
||||
disabled
|
||||
? ''
|
||||
: small
|
||||
? 'group-hover:w-[14px] group-hover:h-[14px] group-hover:m-[-1px] group-active:w-[10px] group-active:h-[10px] group-active:m-[1px]'
|
||||
: 'group-hover:w-[18px] group-hover:h-[18px] group-hover:m-[-1px] group-active:w-[14px] group-active:h-[14px] group-active:m-[1px]',
|
||||
: indeterminate
|
||||
? 'group-hover:scale-x-110 group-active:scale-x-75'
|
||||
: small
|
||||
? 'group-hover:w-[14px] group-hover:h-[14px] group-hover:m-[-1px] group-active:w-[10px] group-active:h-[10px] group-active:m-[1px]'
|
||||
: 'group-hover:w-[18px] group-hover:h-[18px] group-hover:m-[-1px] group-active:w-[14px] group-active:h-[14px] group-active:m-[1px]',
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
@@ -37,6 +44,7 @@ const props = defineProps<{
|
||||
id?: string
|
||||
disabled?: boolean
|
||||
small?: boolean
|
||||
indeterminate?: boolean
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<boolean>()
|
||||
|
||||
@@ -233,17 +233,48 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
}
|
||||
|
||||
const content = worldContentUpdateToAddons(event)
|
||||
queryClient.setQueryData<Archon.Content.v1.Addons>(contentListKey(serverId), {
|
||||
queryClient.setQueryData<Archon.Content.v1.Addons>(contentListKey(serverId), (current) => ({
|
||||
...content,
|
||||
addons: content.addons?.filter((addon) => !addon.from_modpack) ?? null,
|
||||
})
|
||||
queryClient.setQueryData<Archon.Content.v1.Addons>(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<Archon.Content.v1.Addons>(
|
||||
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 {
|
||||
@@ -297,7 +328,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,
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface ScrollViewportOptions {
|
||||
}
|
||||
|
||||
export interface VirtualScrollOptions {
|
||||
itemHeight: number
|
||||
itemHeight: number | Ref<number>
|
||||
bufferSize?: number
|
||||
initialItemCount?: number
|
||||
enabled?: Ref<boolean>
|
||||
@@ -140,6 +140,9 @@ export function useVirtualScroll<T>(items: Ref<T[]>, options: VirtualScrollOptio
|
||||
onNearEnd,
|
||||
nearEndThreshold = 0.2,
|
||||
} = options
|
||||
const resolvedItemHeight = computed(() =>
|
||||
typeof itemHeight === 'number' ? itemHeight : itemHeight.value,
|
||||
)
|
||||
|
||||
const {
|
||||
listContainer,
|
||||
@@ -152,7 +155,7 @@ export function useVirtualScroll<T>(items: Ref<T[]>, 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) {
|
||||
@@ -163,8 +166,8 @@ export function useVirtualScroll<T>(items: Ref<T[]>, 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(
|
||||
@@ -180,7 +183,7 @@ export function useVirtualScroll<T>(items: Ref<T[]>, 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(() =>
|
||||
|
||||
@@ -28,11 +28,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()
|
||||
|
||||
@@ -49,6 +54,10 @@ const messages = defineMessages({
|
||||
id: 'content.card.frozen',
|
||||
defaultMessage: 'This project is locked to its current version until unfrozen.',
|
||||
},
|
||||
useEnabledForControls: {
|
||||
id: 'content.card.use-enabled-for-controls',
|
||||
defaultMessage: 'Use the Enabled for controls to enable or disable this content.',
|
||||
},
|
||||
})
|
||||
|
||||
interface Props {
|
||||
@@ -77,6 +86,8 @@ interface Props {
|
||||
hideDelete?: boolean
|
||||
hideActions?: boolean
|
||||
inline?: boolean
|
||||
enabledFor?: ContentEnabledForState
|
||||
embeddedIcon?: ContentCardEmbeddedIcon
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -104,12 +115,15 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
hideDelete: false,
|
||||
hideActions: false,
|
||||
inline: false,
|
||||
enabledFor: undefined,
|
||||
embeddedIcon: undefined,
|
||||
})
|
||||
|
||||
const selected = defineModel<boolean>('selected')
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:enabled': [value: boolean]
|
||||
'update:enabled-for': [side: ContentSide, value: boolean]
|
||||
select: [value: boolean, event?: MouseEvent]
|
||||
delete: [event: MouseEvent]
|
||||
update: []
|
||||
@@ -127,7 +141,20 @@ const versionNumberRef = ref<HTMLElement | null>(null)
|
||||
const fileNameRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const isDisabled = computed(() => props.disabled || props.installing)
|
||||
const isToggleDisabled = computed(() => isDisabled.value || props.toggleDisabled)
|
||||
const isToggleIndeterminate = computed(
|
||||
() => props.enabledFor !== undefined && props.enabledFor.server !== props.enabledFor.player,
|
||||
)
|
||||
const isToggleDisabled = computed(
|
||||
() => isDisabled.value || props.toggleDisabled || isToggleIndeterminate.value,
|
||||
)
|
||||
const toggleTooltip = computed(() => {
|
||||
if (isToggleIndeterminate.value) {
|
||||
return formatMessage(messages.useEnabledForControls)
|
||||
}
|
||||
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) {
|
||||
@@ -152,18 +179,24 @@ const installTooltip = computed(() => {
|
||||
<template>
|
||||
<div
|
||||
role="row"
|
||||
class="flex items-center justify-between"
|
||||
class="flex items-center"
|
||||
:class="{
|
||||
'h-[74px] gap-4 px-3': !inline,
|
||||
'h-[74px] gap-4 px-3': !inline && !enabledFor,
|
||||
'h-[72px] gap-2 px-3': !inline && enabledFor,
|
||||
'gap-3': inline,
|
||||
'justify-between': !enabledFor,
|
||||
'opacity-50 grayscale': disabled && !installing,
|
||||
'opacity-50': installing,
|
||||
'opacity-50': installing || (enabledFor && isEnabledForDisabled && !disabled),
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="flex min-w-0 items-center gap-4"
|
||||
:class="
|
||||
hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
|
||||
hideActions
|
||||
? 'flex-1'
|
||||
: enabledFor
|
||||
? 'flex-1 @[800px]:w-[340px] @[800px]:shrink-0 @[800px]:flex-none'
|
||||
: 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
|
||||
"
|
||||
>
|
||||
<Checkbox
|
||||
@@ -177,15 +210,14 @@ const installTooltip = computed(() => {
|
||||
|
||||
<div
|
||||
class="flex min-w-0 items-center gap-3 transition-[filter,opacity] duration-200"
|
||||
:class="enabled === false && !disabled ? 'grayscale opacity-50' : ''"
|
||||
:class="!enabledFor && enabled === false && !disabled ? 'grayscale opacity-50' : ''"
|
||||
>
|
||||
<div v-tooltip="installTooltip" class="relative flex shrink-0 items-center">
|
||||
<Avatar
|
||||
<ContentCardItemIcon
|
||||
:src="project.icon_url"
|
||||
:alt="project.title"
|
||||
size="3rem"
|
||||
no-shadow
|
||||
class="rounded-2xl border border-surface-5"
|
||||
:tint-by="project.id"
|
||||
:embedded-icon="embeddedIcon"
|
||||
/>
|
||||
<div
|
||||
v-if="installing"
|
||||
@@ -295,11 +327,20 @@ const installTooltip = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="enabledFor" class="hidden w-[200px] shrink-0 @[800px]:block">
|
||||
<ContentEnabledFor
|
||||
:model-value="enabledFor"
|
||||
:disabled="isDisabled"
|
||||
:disabled-tooltip="isDisabled ? disabledTooltip : undefined"
|
||||
@update:model-value="(side, value) => emit('update:enabled-for', side, value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hidden flex-col gap-0.5 transition-[filter,opacity] duration-200 @[800px]:flex"
|
||||
:class="[
|
||||
hideActions ? 'flex-1' : 'flex-1 min-w-0',
|
||||
enabled === false && !disabled ? 'grayscale opacity-50' : '',
|
||||
hideActions ? 'flex-1' : enabledFor ? 'min-w-0 flex-1' : 'flex-1 min-w-0',
|
||||
!enabledFor && enabled === false && !disabled ? 'grayscale opacity-50' : '',
|
||||
]"
|
||||
>
|
||||
<template v-if="version">
|
||||
@@ -335,7 +376,8 @@ const installTooltip = computed(() => {
|
||||
|
||||
<div
|
||||
v-if="!hideActions"
|
||||
class="flex min-w-[160px] shrink-0 items-center justify-end gap-2 transition-colors duration-200"
|
||||
class="flex shrink-0 items-center justify-end gap-2 transition-colors duration-200"
|
||||
:class="enabledFor ? 'w-[168px]' : 'min-w-[160px]'"
|
||||
>
|
||||
<slot name="additionalButtonsLeft" />
|
||||
|
||||
@@ -399,13 +441,10 @@ const installTooltip = computed(() => {
|
||||
|
||||
<Toggle
|
||||
v-if="enabled !== undefined && !hideToggle"
|
||||
v-tooltip="
|
||||
isToggleDisabled && (toggleDisabledTooltip || disabledTooltip)
|
||||
? (toggleDisabledTooltip ?? disabledTooltip)
|
||||
: undefined
|
||||
"
|
||||
v-tooltip="toggleTooltip"
|
||||
:model-value="enabled"
|
||||
:disabled="isToggleDisabled"
|
||||
:indeterminate="isToggleIndeterminate"
|
||||
:aria-label="project.title"
|
||||
class="my-auto"
|
||||
@update:model-value="(val) => emit('update:enabled', val as boolean)"
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
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<string>()
|
||||
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,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Avatar
|
||||
:src="resolvedSrc"
|
||||
:alt="alt"
|
||||
:tint-by="tintBy"
|
||||
size="3rem"
|
||||
no-shadow
|
||||
class="rounded-2xl border border-surface-5"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@modrinth/assets'
|
||||
import { ChevronDownIcon, ChevronUpIcon, InfoIcon } from '@modrinth/assets'
|
||||
import { computed, getCurrentInstance, ref, toRef } from 'vue'
|
||||
|
||||
import Checkbox from '#ui/components/base/Checkbox.vue'
|
||||
import { useVIntl } from '#ui/composables/i18n'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { useStickyObserver } from '#ui/composables/sticky-observer'
|
||||
import { useVirtualScroll } from '#ui/composables/virtual-scroll'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
@@ -17,6 +17,17 @@ import ContentCardItem from './ContentCardItem.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
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.',
|
||||
},
|
||||
})
|
||||
|
||||
interface Props {
|
||||
items: ContentCardTableItem[]
|
||||
showSelection?: boolean
|
||||
@@ -28,6 +39,7 @@ interface Props {
|
||||
hideHeader?: boolean
|
||||
flat?: boolean
|
||||
showItemActions?: boolean
|
||||
showEnabledForColumn?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -40,6 +52,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
hideHeader: false,
|
||||
flat: false,
|
||||
showItemActions: false,
|
||||
showEnabledForColumn: false,
|
||||
})
|
||||
|
||||
const stickyHeaderRef = ref<HTMLElement | null>(null)
|
||||
@@ -49,6 +62,7 @@ const selectedIds = defineModel<string[]>('selectedIds', { default: () => [] })
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:enabled': [id: string, value: boolean]
|
||||
'update:enabled-for': [id: string, side: 'server' | 'player', value: boolean]
|
||||
delete: [id: string, event: MouseEvent]
|
||||
update: [id: string]
|
||||
switchVersion: [id: string]
|
||||
@@ -65,6 +79,9 @@ const hasSwitchVersionListener = computed(
|
||||
const hasEnabledListener = computed(
|
||||
() => typeof instance?.vnode.props?.['onUpdate:enabled'] === 'function',
|
||||
)
|
||||
const hasEnabledForColumn = computed(
|
||||
() => props.showEnabledForColumn || props.items.some((item) => item.enabledFor !== undefined),
|
||||
)
|
||||
|
||||
const hasAnyActions = computed(() => {
|
||||
// Check if there are listeners for actions
|
||||
@@ -80,17 +97,18 @@ const hasAnyActions = computed(() => {
|
||||
(item) =>
|
||||
(item.overflowOptions && item.overflowOptions.length > 0) ||
|
||||
item.hasUpdate ||
|
||||
item.enabled !== undefined,
|
||||
(item.enabled !== undefined && !item.hideToggle),
|
||||
)
|
||||
|
||||
return hasListeners || hasItemActions || props.showItemActions
|
||||
})
|
||||
|
||||
// Virtualization
|
||||
const itemHeight = computed(() => (hasEnabledForColumn.value ? 72 : 74))
|
||||
const { listContainer, totalHeight, visibleRange, visibleTop, visibleItems } = useVirtualScroll(
|
||||
toRef(props, 'items'),
|
||||
{
|
||||
itemHeight: 74,
|
||||
itemHeight,
|
||||
bufferSize: 5,
|
||||
initialItemCount: 20,
|
||||
enabled: toRef(props, 'virtualized'),
|
||||
@@ -179,8 +197,9 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
v-if="!hideHeader"
|
||||
ref="stickyHeaderRef"
|
||||
role="rowgroup"
|
||||
class="sticky top-0 z-10 flex h-12 items-center justify-between gap-4 bg-surface-3 px-3"
|
||||
class="sticky top-0 z-10 flex h-12 items-center bg-surface-3 px-3"
|
||||
:class="[
|
||||
hasEnabledForColumn ? 'gap-2' : 'justify-between gap-4',
|
||||
flat || isStuck ? 'rounded-none' : 'rounded-t-[20px]',
|
||||
isStuck
|
||||
? 'transition-[border-radius] duration-100 border-0 border-y border-solid border-surface-4 shadow-md before:pointer-events-none before:absolute before:inset-x-0 before:-top-4 before:h-5 before:bg-surface-3'
|
||||
@@ -191,7 +210,11 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
role="row"
|
||||
class="flex min-w-0 items-center gap-4"
|
||||
:class="
|
||||
hasAnyActions ? 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none' : 'flex-1'
|
||||
hasAnyActions
|
||||
? hasEnabledForColumn
|
||||
? 'flex-1 @[800px]:w-[340px] @[800px]:shrink-0 @[800px]:flex-none'
|
||||
: 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
|
||||
: 'flex-1'
|
||||
"
|
||||
>
|
||||
<Checkbox
|
||||
@@ -225,7 +248,27 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden @[800px]:flex" :class="hasAnyActions ? 'flex-1 min-w-0' : 'flex-1'">
|
||||
<div
|
||||
v-if="hasEnabledForColumn"
|
||||
role="columnheader"
|
||||
class="hidden w-[200px] shrink-0 items-center gap-1.5 font-semibold text-secondary @[800px]:flex"
|
||||
>
|
||||
<span>{{ formatMessage(messages.enabledFor) }}</span>
|
||||
<span
|
||||
v-tooltip="formatMessage(messages.enabledForDescription)"
|
||||
class="inline-flex size-4 cursor-help items-center justify-center"
|
||||
tabindex="0"
|
||||
>
|
||||
<InfoIcon class="size-4" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hidden @[800px]:flex"
|
||||
:class="
|
||||
hasAnyActions ? (hasEnabledForColumn ? 'min-w-0 flex-1' : 'flex-1 min-w-0') : 'flex-1'
|
||||
"
|
||||
>
|
||||
<button
|
||||
v-if="sortable"
|
||||
role="columnheader"
|
||||
@@ -247,7 +290,12 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="hasAnyActions" role="columnheader" class="min-w-[160px] shrink-0 text-right">
|
||||
<div
|
||||
v-if="hasAnyActions"
|
||||
role="columnheader"
|
||||
class="shrink-0 text-right"
|
||||
:class="hasEnabledForColumn ? 'w-[168px]' : 'min-w-[160px]'"
|
||||
>
|
||||
<span class="font-semibold text-secondary">{{
|
||||
formatMessage(commonMessages.actionsLabel)
|
||||
}}</span>
|
||||
@@ -288,6 +336,8 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
:toggle-disabled="item.toggleDisabled"
|
||||
:toggle-disabled-tooltip="item.toggleDisabledTooltip"
|
||||
:hide-toggle="item.hideToggle"
|
||||
:enabled-for="item.enabledFor"
|
||||
:embedded-icon="item.embeddedIcon"
|
||||
:show-checkbox="showSelection"
|
||||
:hide-delete="hideDelete || item.hideDelete"
|
||||
:hide-actions="!hasAnyActions"
|
||||
@@ -306,6 +356,7 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
toggleItemSelection(item.id, val ?? false, visibleRange.start + idx, 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)"
|
||||
v-on="
|
||||
@@ -356,6 +407,8 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
:toggle-disabled="item.toggleDisabled"
|
||||
:toggle-disabled-tooltip="item.toggleDisabledTooltip"
|
||||
:hide-toggle="item.hideToggle"
|
||||
:enabled-for="item.enabledFor"
|
||||
:embedded-icon="item.embeddedIcon"
|
||||
:show-checkbox="showSelection"
|
||||
:hide-delete="hideDelete || item.hideDelete"
|
||||
:hide-actions="!hasAnyActions"
|
||||
@@ -371,6 +424,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)"
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { LockIcon, TriangleAlertIcon } from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
|
||||
import type { ContentEnabledForState, ContentSide } from '../types'
|
||||
|
||||
const messages = defineMessages({
|
||||
server: {
|
||||
id: 'content.enabled-for.server',
|
||||
defaultMessage: 'Server',
|
||||
},
|
||||
player: {
|
||||
id: 'content.enabled-for.player',
|
||||
defaultMessage: 'Player',
|
||||
},
|
||||
locked: {
|
||||
id: 'content.enabled-for.locked',
|
||||
defaultMessage: 'These choices are locked. Select Unlock from the row menu to change them.',
|
||||
},
|
||||
requiredHere: {
|
||||
id: 'content.enabled-for.required-here',
|
||||
defaultMessage: 'This content needs to be enabled here to work correctly.',
|
||||
},
|
||||
notSupportedHere: {
|
||||
id: 'content.enabled-for.not-supported-here',
|
||||
defaultMessage: "This content isn't designed to be enabled here.",
|
||||
},
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: ContentEnabledForState
|
||||
disabled?: boolean
|
||||
disabledTooltip?: string | null
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
disabledTooltip: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:model-value': [side: ContentSide, value: boolean]
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const sides: ContentSide[] = ['server', 'player']
|
||||
const disabledSides = computed(() => new Set(props.modelValue.disabledSides ?? []))
|
||||
|
||||
function isSelected(side: ContentSide) {
|
||||
return props.modelValue[side]
|
||||
}
|
||||
|
||||
function isDisabled(side: ContentSide) {
|
||||
return props.disabled || disabledSides.value.has(side)
|
||||
}
|
||||
|
||||
function getDisabledTooltip(side: ContentSide) {
|
||||
if (props.disabled) return props.disabledTooltip
|
||||
if (!disabledSides.value.has(side)) return undefined
|
||||
return formatMessage(isSelected(side) ? messages.requiredHere : messages.notSupportedHere)
|
||||
}
|
||||
|
||||
function toggle(side: ContentSide) {
|
||||
if (isDisabled(side)) return
|
||||
emit('update:model-value', side, !isSelected(side))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-[200px] shrink-0 items-center gap-1.5">
|
||||
<button
|
||||
v-for="side in sides"
|
||||
:key="side"
|
||||
v-tooltip="getDisabledTooltip(side)"
|
||||
type="button"
|
||||
class="flex h-8 items-center rounded-xl border border-solid px-3 text-sm font-medium transition-[background-color,border-color,color,opacity,transform] duration-100 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-brand-shadow"
|
||||
:class="[
|
||||
isSelected(side)
|
||||
? 'border-brand bg-brand-highlight text-brand'
|
||||
: 'border-surface-5 bg-transparent text-primary hover:bg-surface-3',
|
||||
isDisabled(side) ? 'cursor-not-allowed opacity-50' : 'cursor-pointer active:scale-[0.97]',
|
||||
]"
|
||||
:aria-pressed="isSelected(side)"
|
||||
:aria-disabled="isDisabled(side)"
|
||||
@click="toggle(side)"
|
||||
>
|
||||
{{ formatMessage(messages[side]) }}
|
||||
</button>
|
||||
|
||||
<span
|
||||
v-if="modelValue.locked && disabledSides.size > 0"
|
||||
v-tooltip="formatMessage(messages.locked)"
|
||||
class="inline-flex size-5 shrink-0 cursor-help items-center justify-center text-secondary"
|
||||
tabindex="0"
|
||||
>
|
||||
<LockIcon class="size-5" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="modelValue.warningTooltip"
|
||||
v-tooltip="modelValue.warningTooltip"
|
||||
class="inline-flex size-5 shrink-0 cursor-help items-center justify-center"
|
||||
tabindex="0"
|
||||
>
|
||||
<TriangleAlertIcon class="size-5 text-orange" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
+85
-14
@@ -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'
|
||||
|
||||
@@ -42,6 +48,7 @@ interface Props {
|
||||
sourceName?: string
|
||||
sourceIconUrl?: string
|
||||
enableToggle?: boolean
|
||||
enableEnabledFor?: boolean
|
||||
actionDisabled?: boolean
|
||||
actionDisabledTooltip?: string | null
|
||||
getOverflowOptions?: (item: ContentItem) => OverflowMenuOption[]
|
||||
@@ -54,6 +61,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
sourceName: undefined,
|
||||
sourceIconUrl: undefined,
|
||||
enableToggle: false,
|
||||
enableEnabledFor: false,
|
||||
actionDisabled: false,
|
||||
actionDisabledTooltip: undefined,
|
||||
getOverflowOptions: undefined,
|
||||
@@ -63,6 +71,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
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: []
|
||||
@@ -109,6 +118,18 @@ const messages = defineMessages({
|
||||
id: 'instances.managed-content-modal.download-file',
|
||||
defaultMessage: 'Download File',
|
||||
},
|
||||
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 {
|
||||
@@ -262,6 +283,7 @@ const tableItems = computed<ContentCardTableItem[]>(() =>
|
||||
icon_url: item.embedded_metadata?.icon_url ?? null,
|
||||
},
|
||||
projectLink: !item.external && item.project?.id ? `/project/${item.project.id}` : undefined,
|
||||
embeddedIcon: item.embeddedIcon,
|
||||
version: item.version ?? {
|
||||
id: item.id,
|
||||
version_number: contentVersionLabel(item),
|
||||
@@ -282,16 +304,24 @@ const tableItems = computed<ContentCardTableItem[]>(() =>
|
||||
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 } : {}),
|
||||
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
|
||||
? [
|
||||
@@ -329,7 +359,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) {
|
||||
@@ -372,6 +404,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])
|
||||
@@ -474,8 +513,12 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
||||
<template>
|
||||
<NewModal
|
||||
ref="modal"
|
||||
:max-width="'min(928px, calc(95vw - 10rem))'"
|
||||
:width="'min(928px, calc(95vw - 10rem))'"
|
||||
:max-width="
|
||||
props.enableEnabledFor ? 'min(1080px, calc(95vw - 4rem))' : 'min(928px, calc(95vw - 10rem))'
|
||||
"
|
||||
:width="
|
||||
props.enableEnabledFor ? 'min(1080px, calc(95vw - 4rem))' : 'min(928px, calc(95vw - 10rem))'
|
||||
"
|
||||
:on-hide="handleHide"
|
||||
no-padding
|
||||
>
|
||||
@@ -539,13 +582,16 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
||||
|
||||
<div v-else class="@container flex-1 min-h-0 flex flex-col">
|
||||
<div
|
||||
class="flex h-12 shrink-0 items-center justify-between gap-4 border-0 border-b border-solid border-surface-4 bg-surface-3 px-3"
|
||||
class="flex h-12 shrink-0 items-center border-0 border-b border-solid border-surface-4 bg-surface-3 px-3"
|
||||
:class="props.enableEnabledFor ? 'gap-2' : 'justify-between gap-4'"
|
||||
>
|
||||
<div
|
||||
class="flex min-w-0 items-center gap-4"
|
||||
:class="
|
||||
showTableActions
|
||||
? 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
|
||||
? props.enableEnabledFor
|
||||
? 'flex-1 @[800px]:w-[340px] @[800px]:shrink-0 @[800px]:flex-none'
|
||||
: 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
|
||||
: 'flex-1'
|
||||
"
|
||||
>
|
||||
@@ -561,15 +607,38 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
||||
formatMessage(commonMessages.projectLabel)
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.enableEnabledFor"
|
||||
class="hidden w-[200px] shrink-0 items-center gap-1.5 @[800px]:flex"
|
||||
>
|
||||
<span class="font-semibold text-secondary">{{
|
||||
formatMessage(messages.enabledFor)
|
||||
}}</span>
|
||||
<UnknownIcon
|
||||
v-tooltip="formatMessage(messages.enabledForDescription)"
|
||||
class="size-4 cursor-help text-secondary"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="hidden @[800px]:flex"
|
||||
:class="showTableActions ? 'flex-1 min-w-0' : 'flex-1'"
|
||||
:class="
|
||||
showTableActions
|
||||
? props.enableEnabledFor
|
||||
? 'min-w-0 flex-1'
|
||||
: 'flex-1 min-w-0'
|
||||
: 'flex-1'
|
||||
"
|
||||
>
|
||||
<span class="font-semibold text-secondary">{{
|
||||
formatMessage(commonMessages.versionLabel)
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="showTableActions" class="min-w-[160px] shrink-0 text-right">
|
||||
<div
|
||||
v-if="showTableActions"
|
||||
class="shrink-0 text-right"
|
||||
:class="props.enableEnabledFor ? 'w-[168px]' : 'min-w-[160px]'"
|
||||
>
|
||||
<span class="font-semibold text-secondary">{{
|
||||
formatMessage(commonMessages.actionsLabel)
|
||||
}}</span>
|
||||
@@ -580,15 +649,17 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
||||
v-model:selected-ids="selectedIds"
|
||||
:items="tableItems"
|
||||
:show-selection="props.enableToggle"
|
||||
:show-item-actions="hasExternalSlicerUrls"
|
||||
:show-enabled-for-column="props.enableEnabledFor"
|
||||
:show-item-actions="props.enableEnabledFor || hasExternalSlicerUrls"
|
||||
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"
|
||||
>
|
||||
<template #itemTitleBadges="{ item }">
|
||||
<span
|
||||
|
||||
@@ -180,9 +180,18 @@ function getItemId(item: ContentItem) {
|
||||
}
|
||||
|
||||
type SortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'date-added-newest' | 'date-added-oldest'
|
||||
const defaultSortMode: SortMode = 'alphabetical-asc'
|
||||
const sortMode = ctx.filterPersistKey
|
||||
? useSessionStorage<SortMode>(`content-sort:${ctx.filterPersistKey}`, 'alphabetical-asc')
|
||||
: ref<SortMode>('alphabetical-asc')
|
||||
? useSessionStorage<SortMode>(`content-sort:${ctx.filterPersistKey}`, defaultSortMode)
|
||||
: ref<SortMode>(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<SortMode, () => string> = {
|
||||
'alphabetical-asc': () => formatMessage(messages.sortAlphabeticalAscending),
|
||||
@@ -191,32 +200,36 @@ const sortLabels: Record<SortMode, () => string> = {
|
||||
'date-added-oldest': () => formatMessage(messages.sortDateAddedOldest),
|
||||
}
|
||||
|
||||
const sortOptions = computed<OverflowMenuOption[]>(() => [
|
||||
{
|
||||
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<OverflowMenuOption[]>(() => {
|
||||
const options: OverflowMenuOption[] = [
|
||||
{
|
||||
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]
|
||||
@@ -424,14 +437,22 @@ const tableItems = computed<ContentCardTableItem[]>(() => {
|
||||
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)
|
||||
@@ -505,7 +526,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))
|
||||
@@ -617,6 +641,7 @@ async function disableItemsWithoutWarning(items: ContentItem[]) {
|
||||
await ctx.bulkDisableItems(items)
|
||||
return
|
||||
}
|
||||
if (!ctx.toggleEnabled) return
|
||||
|
||||
for (const item of items) {
|
||||
const id = getItemId(item)
|
||||
@@ -725,15 +750,15 @@ async function confirmDisable() {
|
||||
if (itemsToDisable.length === 1) {
|
||||
const item = itemsToDisable[0]
|
||||
const id = getItemId(item)
|
||||
markChanging(id)
|
||||
if (!item.enabledFor) markChanging(id)
|
||||
try {
|
||||
if (ctx.bulkDisableItems) {
|
||||
await ctx.bulkDisableItems(itemsToDisable)
|
||||
} else {
|
||||
await ctx.toggleEnabled(item)
|
||||
await ctx.toggleEnabled?.(item)
|
||||
}
|
||||
} finally {
|
||||
unmarkChanging(id)
|
||||
if (!item.enabledFor) unmarkChanging(id)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -744,7 +769,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
|
||||
@@ -752,11 +777,11 @@ async function handleToggleEnabledById(id: string, _value: boolean) {
|
||||
await promptDisableItems([item])
|
||||
return
|
||||
}
|
||||
markChanging(id)
|
||||
if (!item.enabledFor) markChanging(id)
|
||||
try {
|
||||
await ctx.toggleEnabled(item)
|
||||
} finally {
|
||||
unmarkChanging(id)
|
||||
if (!item.enabledFor) unmarkChanging(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,7 +807,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() {
|
||||
@@ -805,6 +831,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)
|
||||
}
|
||||
|
||||
// Bulk updating
|
||||
const confirmBulkUpdateModal = ref<InstanceType<typeof ConfirmBulkUpdateModal>>()
|
||||
const pendingBulkUpdateItems = ref<ContentItem[]>([])
|
||||
@@ -1219,7 +1253,9 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
v-model:selected-ids="selectedIds"
|
||||
:items="tableItems"
|
||||
:show-selection="true"
|
||||
:show-enabled-for-column="!!ctx.setEnabledFor"
|
||||
@update:enabled="handleToggleEnabledById"
|
||||
@update:enabled-for="handleSetEnabledForById"
|
||||
@delete="handleDeleteById"
|
||||
@update="handleUpdateById"
|
||||
@switch-version="handleSwitchVersionById"
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
ContentActionWarning,
|
||||
ContentCardTableItem,
|
||||
ContentItem,
|
||||
ContentSide,
|
||||
ManagedContentCardData,
|
||||
} from '../types'
|
||||
|
||||
@@ -46,7 +47,8 @@ export interface ContentManagerContext {
|
||||
contentTypeLabel: Ref<string> | ComputedRef<string>
|
||||
|
||||
// Core actions
|
||||
toggleEnabled: (item: ContentItem) => Promise<void>
|
||||
toggleEnabled?: (item: ContentItem) => Promise<void>
|
||||
setEnabledFor?: (item: ContentItem, side: ContentSide, enabled: boolean) => Promise<void>
|
||||
deleteItem: (item: ContentItem) => Promise<void>
|
||||
refresh: () => Promise<void>
|
||||
browse: () => void
|
||||
|
||||
@@ -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<Blob>
|
||||
fallbackUrl?: string | null
|
||||
}
|
||||
|
||||
export interface ContentCardTableItem {
|
||||
id: string
|
||||
project: ContentCardProject
|
||||
@@ -74,6 +90,8 @@ export interface ContentCardTableItem {
|
||||
hideDelete?: boolean
|
||||
hideSwitchVersion?: boolean
|
||||
overflowOptions?: OverflowMenuOption[]
|
||||
enabledFor?: ContentEnabledForState
|
||||
embeddedIcon?: ContentCardEmbeddedIcon
|
||||
}
|
||||
|
||||
export type ContentCardTableSortColumn = 'project' | 'version'
|
||||
|
||||
@@ -500,6 +500,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 () => {
|
||||
@@ -899,12 +919,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')
|
||||
},
|
||||
@@ -913,7 +933,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)
|
||||
@@ -936,7 +958,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')
|
||||
},
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { type Archon, type Labrinth, ModrinthApiError } from '@modrinth/api-client'
|
||||
import { ClipboardCopyIcon } from '@modrinth/assets'
|
||||
import { ClipboardCopyIcon, LockIcon, LockOpenIcon } from '@modrinth/assets'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import type { OverflowMenuOption } from '#ui/components/base/buttons'
|
||||
import ReadyTransition from '#ui/components/base/ReadyTransition.vue'
|
||||
import UnknownFileWarningModal from '#ui/components/modal/UnknownFileWarningModal.vue'
|
||||
import { useUploadSessionUpload } from '#ui/composables/hosting/kyros-session-upload'
|
||||
@@ -34,7 +35,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 } from '../../../shared/content-tab/types'
|
||||
import type { ContentItem, ContentSide } from '../../../shared/content-tab/types'
|
||||
import { summarizeManagedContent } from '../../../shared/content-tab/utils/managed-content'
|
||||
|
||||
type AddonWithUiState = Archon.Content.v1.Addon & { installing?: boolean }
|
||||
@@ -63,9 +64,13 @@ const messages = defineMessages({
|
||||
id: 'hosting.content.failed-to-remove',
|
||||
defaultMessage: 'Failed to remove content',
|
||||
},
|
||||
failedToToggle: {
|
||||
id: 'hosting.content.failed-to-toggle',
|
||||
defaultMessage: 'Failed to toggle {name}',
|
||||
failedToSetEnabledFor: {
|
||||
id: 'hosting.content.failed-to-set-enabled-for',
|
||||
defaultMessage: 'Failed to change where {name} is enabled',
|
||||
},
|
||||
failedToSetSideLock: {
|
||||
id: 'hosting.content.failed-to-set-side-lock',
|
||||
defaultMessage: 'Failed to change the environment lock for {name}',
|
||||
},
|
||||
failedToUpload: {
|
||||
id: 'hosting.content.failed-to-upload',
|
||||
@@ -91,14 +96,6 @@ const messages = defineMessages({
|
||||
id: 'hosting.content.failed-to-bulk-delete',
|
||||
defaultMessage: 'Failed to delete content',
|
||||
},
|
||||
failedToBulkEnable: {
|
||||
id: 'hosting.content.failed-to-bulk-enable',
|
||||
defaultMessage: 'Failed to enable content',
|
||||
},
|
||||
failedToBulkDisable: {
|
||||
id: 'hosting.content.failed-to-bulk-disable',
|
||||
defaultMessage: 'Failed to disable content',
|
||||
},
|
||||
failedToBulkUpdate: {
|
||||
id: 'hosting.content.failed-to-bulk-update',
|
||||
defaultMessage: 'Failed to update content',
|
||||
@@ -107,6 +104,19 @@ const messages = defineMessages({
|
||||
id: 'hosting.content.failed-to-install',
|
||||
defaultMessage: 'Failed to install content',
|
||||
},
|
||||
unknownEnvironment: {
|
||||
id: 'hosting.content.enabled-for.unknown-environment',
|
||||
defaultMessage:
|
||||
"We couldn't tell where this content should be enabled. Review the Server and Player choices.",
|
||||
},
|
||||
lockEnvironment: {
|
||||
id: 'hosting.content.enabled-for.lock-environment',
|
||||
defaultMessage: 'Lock',
|
||||
},
|
||||
unlockEnvironment: {
|
||||
id: 'hosting.content.enabled-for.unlock-environment',
|
||||
defaultMessage: 'Unlock',
|
||||
},
|
||||
})
|
||||
|
||||
const client = injectModrinthClient()
|
||||
@@ -299,12 +309,56 @@ const managedContent = computed<ManagedContentData | null>(() => {
|
||||
|
||||
function friendlyAddonName(addon: Archon.Content.v1.Addon): string {
|
||||
if (addon.name) return addon.name
|
||||
if (addon.manifest?.name) return addon.manifest.name
|
||||
let cleanName = addon.filename
|
||||
const lastDotIndex = cleanName.lastIndexOf('.')
|
||||
if (lastDotIndex !== -1) cleanName = cleanName.substring(0, lastDotIndex)
|
||||
return cleanName
|
||||
}
|
||||
|
||||
function getAddonEnvironment(
|
||||
addon: Archon.Content.v1.Addon,
|
||||
): Labrinth.Projects.v3.Environment | undefined {
|
||||
return addon.version?.environment ?? addon.manifest?.environment ?? undefined
|
||||
}
|
||||
|
||||
function hasDetectedEnvironment(addon: Archon.Content.v1.Addon) {
|
||||
const environment = getAddonEnvironment(addon)
|
||||
return environment !== undefined && environment !== 'unknown'
|
||||
}
|
||||
|
||||
function getEnvironmentLockedSides(addon: Archon.Content.v1.Addon): ContentSide[] {
|
||||
switch (getAddonEnvironment(addon)) {
|
||||
case 'client_and_server':
|
||||
case 'client_only':
|
||||
case 'singleplayer_only':
|
||||
case 'server_only':
|
||||
case 'dedicated_server_only':
|
||||
return ['server', 'player']
|
||||
case 'client_only_server_optional':
|
||||
return ['player']
|
||||
case 'server_only_client_optional':
|
||||
return ['server']
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function getLockedSides(addon: Archon.Content.v1.Addon): ContentSide[] {
|
||||
return addon.side_toggle_unlocked ? [] : getEnvironmentLockedSides(addon)
|
||||
}
|
||||
|
||||
function hasEnvironmentLock(addon: Archon.Content.v1.Addon) {
|
||||
return getEnvironmentLockedSides(addon).length > 0
|
||||
}
|
||||
|
||||
function getEnabledForWarning(addon: Archon.Content.v1.Addon) {
|
||||
if (addon.pack_client_retained) return formatMessage(commonMessages.clientRetainedWarning)
|
||||
if (addon.pack_client_depends) return formatMessage(commonMessages.clientDependsWarning)
|
||||
if (!hasDetectedEnvironment(addon)) return formatMessage(messages.unknownEnvironment)
|
||||
return null
|
||||
}
|
||||
|
||||
const modpackAddons = ref<Archon.Content.v1.Addon[]>([])
|
||||
|
||||
const addonLookup = computed(() => {
|
||||
@@ -312,12 +366,23 @@ const addonLookup = computed(() => {
|
||||
for (const addon of contentQuery.data.value?.addons ?? []) {
|
||||
map.set(addon.filename, addon)
|
||||
}
|
||||
for (const addon of modpackAddons.value) {
|
||||
map.set(addon.filename, addon)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const modpackAddonLookup = computed(
|
||||
() => new Map(modpackAddons.value.map((addon) => [addon.filename, addon])),
|
||||
)
|
||||
|
||||
function getAddonForItem(item: ContentItem) {
|
||||
return item.source_kind === 'modrinth_modpack'
|
||||
? modpackAddonLookup.value.get(item.file_name)
|
||||
: addonLookup.value.get(item.file_name)
|
||||
}
|
||||
|
||||
function getAddonQueryKey(addon: Archon.Content.v1.Addon) {
|
||||
return addon.from_modpack ? modpackContentQueryKey.value : queryKey.value
|
||||
}
|
||||
|
||||
const projectMetadataBatchSize = 800
|
||||
const contentProjectIds = computed(() =>
|
||||
[...(contentQuery.data.value?.addons ?? []), ...modpackAddons.value]
|
||||
@@ -557,44 +622,142 @@ const deleteMutation = useMutation({
|
||||
},
|
||||
})
|
||||
|
||||
const toggleMutation = useMutation({
|
||||
mutationFn: async ({ addon }: { addon: Archon.Content.v1.Addon }) => {
|
||||
const request: Archon.Content.v1.RemoveAddonRequest = {
|
||||
type SetEnabledForVariables = {
|
||||
addon: Archon.Content.v1.Addon
|
||||
sides: ContentSide[]
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const setEnabledForMutation = useMutation({
|
||||
mutationFn: async ({ addon, sides, enabled }: SetEnabledForVariables) => {
|
||||
const request: Archon.Content.v1.SetAddonEnabledRequest = {
|
||||
filename: addon.filename,
|
||||
kind: addon.kind,
|
||||
enabled,
|
||||
}
|
||||
if (addon.disabled) {
|
||||
await client.archon.content_v1.enableAddon(serverId, worldId.value!, request)
|
||||
} else {
|
||||
await client.archon.content_v1.disableAddon(serverId, worldId.value!, request)
|
||||
}
|
||||
return { filename: addon.filename, newDisabled: !addon.disabled }
|
||||
await Promise.all(
|
||||
sides.map((side) =>
|
||||
side === 'server'
|
||||
? client.archon.content_v1.setAddonEnabledServer(serverId, worldId.value!, request)
|
||||
: client.archon.content_v1.setAddonEnabledPlayer(serverId, worldId.value!, request),
|
||||
),
|
||||
)
|
||||
},
|
||||
onSuccess: ({ filename, newDisabled }) => {
|
||||
queryClient.setQueryData(queryKey.value, (oldData: Archon.Content.v1.Addons | undefined) => {
|
||||
onMutate: async ({ addon, sides, enabled }) => {
|
||||
const targetQueryKey = getAddonQueryKey(addon)
|
||||
await queryClient.cancelQueries({ queryKey: targetQueryKey })
|
||||
const previousData = queryClient.getQueryData<Archon.Content.v1.Addons>(targetQueryKey)
|
||||
queryClient.setQueryData(targetQueryKey, (oldData: Archon.Content.v1.Addons | undefined) => {
|
||||
if (!oldData) return oldData
|
||||
return {
|
||||
...oldData,
|
||||
addons: (oldData.addons ?? []).map((a) =>
|
||||
a.filename === filename ? { ...a, disabled: newDisabled } : a,
|
||||
),
|
||||
addons: (oldData.addons ?? []).map((candidate) => {
|
||||
if (candidate.filename !== addon.filename || candidate.kind !== addon.kind)
|
||||
return candidate
|
||||
return {
|
||||
...candidate,
|
||||
...(sides.includes('server') ? { disabled_server: !enabled } : {}),
|
||||
...(sides.includes('player') ? { disabled_player: !enabled } : {}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
return { previousData, targetQueryKey }
|
||||
},
|
||||
onError: (_err, { addon }) => {
|
||||
onError: (error, { addon }, context) => {
|
||||
if (context?.previousData) {
|
||||
queryClient.setQueryData(context.targetQueryKey, context.previousData)
|
||||
}
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(messages.failedToToggle, { name: friendlyAddonName(addon) }),
|
||||
title: formatMessage(messages.failedToSetEnabledFor, {
|
||||
name: friendlyAddonName(addon),
|
||||
}),
|
||||
text: error instanceof Error ? error.message : undefined,
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
void queryClient.invalidateQueries({ queryKey: modpackContentQueryKey.value })
|
||||
},
|
||||
})
|
||||
|
||||
async function handleToggleEnabled(item: ContentItem) {
|
||||
const setSideLockMutation = useMutation({
|
||||
mutationFn: async ({ addon, locked }: { addon: Archon.Content.v1.Addon; locked: boolean }) => {
|
||||
await client.archon.content_v1.setAddonSideToggleLocked(serverId, worldId.value!, {
|
||||
filename: addon.filename,
|
||||
kind: addon.kind,
|
||||
locked,
|
||||
})
|
||||
},
|
||||
onMutate: async ({ addon, locked }) => {
|
||||
const targetQueryKey = getAddonQueryKey(addon)
|
||||
await queryClient.cancelQueries({ queryKey: targetQueryKey })
|
||||
const previousData = queryClient.getQueryData<Archon.Content.v1.Addons>(targetQueryKey)
|
||||
queryClient.setQueryData(targetQueryKey, (oldData: Archon.Content.v1.Addons | undefined) => {
|
||||
if (!oldData) return oldData
|
||||
return {
|
||||
...oldData,
|
||||
addons: (oldData.addons ?? []).map((candidate) =>
|
||||
candidate.filename === addon.filename && candidate.kind === addon.kind
|
||||
? { ...candidate, side_toggle_unlocked: !locked }
|
||||
: candidate,
|
||||
),
|
||||
}
|
||||
})
|
||||
return { previousData, targetQueryKey }
|
||||
},
|
||||
onError: (error, { addon }, context) => {
|
||||
if (context?.previousData) {
|
||||
queryClient.setQueryData(context.targetQueryKey, context.previousData)
|
||||
}
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(messages.failedToSetSideLock, {
|
||||
name: friendlyAddonName(addon),
|
||||
}),
|
||||
text: error instanceof Error ? error.message : undefined,
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
void queryClient.invalidateQueries({ queryKey: modpackContentQueryKey.value })
|
||||
},
|
||||
})
|
||||
|
||||
async function handleSetEnabledFor(item: ContentItem, side: ContentSide, enabled: boolean) {
|
||||
if (contentActionDisabled.value) return
|
||||
const addon = addonLookup.value.get(item.file_name)
|
||||
const addon = getAddonForItem(item)
|
||||
if (!addon) return
|
||||
await toggleMutation.mutateAsync({ addon })
|
||||
await setEnabledForMutation.mutateAsync({ addon, sides: [side], enabled })
|
||||
}
|
||||
|
||||
async function handleToggleEnabled(item: ContentItem) {
|
||||
if (contentActionDisabled.value || !item.enabledFor) return
|
||||
if (item.enabledFor.server !== item.enabledFor.player) return
|
||||
const addon = getAddonForItem(item)
|
||||
if (!addon) return
|
||||
await setEnabledForMutation.mutateAsync({
|
||||
addon,
|
||||
sides: ['server', 'player'],
|
||||
enabled: !item.enabledFor.server,
|
||||
})
|
||||
}
|
||||
|
||||
async function handleModpackSetEnabledFor(item: ContentItem, side: ContentSide, enabled: boolean) {
|
||||
try {
|
||||
await handleSetEnabledFor(item, side, enabled)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async function handleModpackToggleEnabled(item: ContentItem) {
|
||||
try {
|
||||
await handleToggleEnabled(item)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteItem(item: ContentItem) {
|
||||
@@ -629,38 +792,6 @@ async function handleBulkDelete(items: ContentItem[]) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBulkEnable(items: ContentItem[]) {
|
||||
if (contentActionDisabled.value) return
|
||||
const requests = itemsToAddonRequests(items)
|
||||
if (requests.length === 0) return
|
||||
try {
|
||||
await client.archon.content_v1.enableAddons(serverId, worldId.value!, requests)
|
||||
await queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(messages.failedToBulkEnable),
|
||||
text: err instanceof Error ? err.message : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBulkDisable(items: ContentItem[]) {
|
||||
if (contentActionDisabled.value) return
|
||||
const requests = itemsToAddonRequests(items)
|
||||
if (requests.length === 0) return
|
||||
try {
|
||||
await client.archon.content_v1.disableAddons(serverId, worldId.value!, requests)
|
||||
await queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(messages.failedToBulkDisable),
|
||||
text: err instanceof Error ? err.message : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const modpackUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
const modpackContentModal = ref<InstanceType<typeof ManagedContentModal>>()
|
||||
const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal>>()
|
||||
@@ -813,17 +944,48 @@ function addonToContentItem(addon: AddonWithUiState): ContentItem {
|
||||
const projectMetadata = addon.project_id
|
||||
? contentProjectsById.value.get(addon.project_id)
|
||||
: undefined
|
||||
const environment = getAddonEnvironment(addon)
|
||||
const embeddedIcon =
|
||||
!addon.icon_url &&
|
||||
addon.manifest?.icon_embedded &&
|
||||
worldId.value &&
|
||||
(addon.kind === 'mod' || addon.kind === 'plugin')
|
||||
? {
|
||||
queryKey: [
|
||||
'kyros',
|
||||
'content',
|
||||
'embedded-icon',
|
||||
worldId.value,
|
||||
addon.kind,
|
||||
addon.filename,
|
||||
] as const,
|
||||
queryFn: () =>
|
||||
client.kyros.content_v1.getEmbeddedAddonIcon(
|
||||
worldId.value!,
|
||||
addon.kind === 'mod' ? 'mods' : 'plugins',
|
||||
addon.filename,
|
||||
),
|
||||
fallbackUrl: projectMetadata?.icon_url,
|
||||
}
|
||||
: undefined
|
||||
const serverEnabled = !addon.disabled_server
|
||||
const playerEnabled = !addon.disabled_player
|
||||
const lockedSides = getLockedSides(addon)
|
||||
return {
|
||||
project: {
|
||||
...(projectMetadata ?? {}),
|
||||
id: addon.project_id ?? addon.filename,
|
||||
slug: projectMetadata?.slug ?? addon.project_id ?? addon.filename,
|
||||
title: projectMetadata?.title ?? friendlyAddonName(addon),
|
||||
icon_url: addon.icon_url ?? projectMetadata?.icon_url ?? undefined,
|
||||
icon_url:
|
||||
addon.icon_url ?? (embeddedIcon ? undefined : projectMetadata?.icon_url) ?? undefined,
|
||||
},
|
||||
version: {
|
||||
id: addon.version?.id ?? addon.filename,
|
||||
version_number: addon.version?.name ?? formatMessage(commonMessages.unknownLabel),
|
||||
version_number:
|
||||
addon.version?.name ??
|
||||
addon.manifest?.version ??
|
||||
formatMessage(commonMessages.unknownLabel),
|
||||
file_name: addon.filename,
|
||||
},
|
||||
owner: addon.owner
|
||||
@@ -836,13 +998,23 @@ function addonToContentItem(addon: AddonWithUiState): ContentItem {
|
||||
}
|
||||
: undefined,
|
||||
id: addon.id ?? addon.filename,
|
||||
enabled: !addon.disabled,
|
||||
external: !addon.project_id,
|
||||
source_kind: addon.from_modpack ? 'modrinth_modpack' : undefined,
|
||||
enabled: serverEnabled || playerEnabled,
|
||||
enabledFor: {
|
||||
server: serverEnabled,
|
||||
player: playerEnabled,
|
||||
locked: lockedSides.length > 0,
|
||||
disabledSides: lockedSides,
|
||||
warningTooltip: getEnabledForWarning(addon),
|
||||
},
|
||||
embeddedIcon,
|
||||
file_name: addon.filename,
|
||||
date_added: addon.btime,
|
||||
project_type: addon.kind,
|
||||
has_update: !!addon.has_update,
|
||||
update_version_id: addon.has_update,
|
||||
environment: addon.version?.environment ?? undefined,
|
||||
environment,
|
||||
pack_client_retained: addon.pack_client_retained,
|
||||
pack_client_depends: addon.pack_client_depends,
|
||||
installing: addon.installing ?? addon.status === 'pending',
|
||||
@@ -877,86 +1049,6 @@ async function handleViewModpackContent() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleModpackContentToggle(item: ContentItem) {
|
||||
if (contentActionDisabled.value) return
|
||||
const addon = addonLookup.value.get(item.file_name)
|
||||
if (!addon) return
|
||||
modpackContentModal.value?.updateItem(item.file_name, { disabled: true })
|
||||
try {
|
||||
await toggleMutation.mutateAsync({ addon })
|
||||
modpackAddons.value = modpackAddons.value.map((a) =>
|
||||
a.filename === addon.filename ? { ...a, disabled: !addon.disabled } : a,
|
||||
)
|
||||
queryClient.setQueryData(
|
||||
modpackContentQueryKey.value,
|
||||
(oldData: Archon.Content.v1.Addons | undefined) =>
|
||||
oldData
|
||||
? {
|
||||
...oldData,
|
||||
addons: (oldData.addons ?? []).map((a) =>
|
||||
a.filename === addon.filename ? { ...a, disabled: !addon.disabled } : a,
|
||||
),
|
||||
}
|
||||
: oldData,
|
||||
)
|
||||
modpackContentModal.value?.updateItem(item.file_name, {
|
||||
enabled: !item.enabled,
|
||||
disabled: false,
|
||||
})
|
||||
} catch {
|
||||
modpackContentModal.value?.updateItem(item.file_name, { disabled: false })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleModpackBulkToggle(items: ContentItem[], enable: boolean) {
|
||||
if (contentActionDisabled.value) return
|
||||
const requests = itemsToAddonRequests(items)
|
||||
if (requests.length === 0) return
|
||||
|
||||
// Optimistic update
|
||||
for (const item of items) {
|
||||
modpackAddons.value = modpackAddons.value.map((a) =>
|
||||
a.filename === item.file_name ? { ...a, disabled: !enable } : a,
|
||||
)
|
||||
modpackContentModal.value?.updateItem(item.file_name, { enabled: enable })
|
||||
}
|
||||
|
||||
try {
|
||||
if (enable) {
|
||||
await client.archon.content_v1.enableAddons(serverId, worldId.value!, requests)
|
||||
} else {
|
||||
await client.archon.content_v1.disableAddons(serverId, worldId.value!, requests)
|
||||
}
|
||||
queryClient.setQueryData(
|
||||
modpackContentQueryKey.value,
|
||||
(oldData: Archon.Content.v1.Addons | undefined) =>
|
||||
oldData
|
||||
? {
|
||||
...oldData,
|
||||
addons: (oldData.addons ?? []).map((addon) =>
|
||||
items.some((item) => item.file_name === addon.filename)
|
||||
? { ...addon, disabled: !enable }
|
||||
: addon,
|
||||
),
|
||||
}
|
||||
: oldData,
|
||||
)
|
||||
await queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
} catch (err) {
|
||||
for (const item of items) {
|
||||
modpackAddons.value = modpackAddons.value.map((a) =>
|
||||
a.filename === item.file_name ? { ...a, disabled: enable } : a,
|
||||
)
|
||||
modpackContentModal.value?.updateItem(item.file_name, { enabled: !enable })
|
||||
}
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(enable ? messages.failedToBulkEnable : messages.failedToBulkDisable),
|
||||
text: err instanceof Error ? err.message : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleModpackUnlink() {
|
||||
modpackUnlinkModal.value?.show()
|
||||
}
|
||||
@@ -1175,12 +1267,22 @@ function handleModpackUpdateCancel() {
|
||||
pendingModpackUpdateVersion.value = null
|
||||
}
|
||||
|
||||
function getOverflowOptions(item: ContentItem) {
|
||||
const options: { id: string; icon?: typeof ClipboardCopyIcon; action: () => void }[] = []
|
||||
async function handleToggleSideLock(addon: Archon.Content.v1.Addon) {
|
||||
if (contentActionDisabled.value) return
|
||||
await setSideLockMutation.mutateAsync({
|
||||
addon,
|
||||
locked: addon.side_toggle_unlocked,
|
||||
})
|
||||
}
|
||||
|
||||
if (item.project?.slug) {
|
||||
function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
|
||||
const options: OverflowMenuOption[] = []
|
||||
const addon = getAddonForItem(item)
|
||||
|
||||
if (addon?.project_id && item.project?.slug) {
|
||||
options.push({
|
||||
id: formatMessage(commonMessages.copyLinkButton),
|
||||
id: 'copy-link',
|
||||
label: formatMessage(commonMessages.copyLinkButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: async () => {
|
||||
await navigator.clipboard.writeText(
|
||||
@@ -1190,6 +1292,18 @@ function getOverflowOptions(item: ContentItem) {
|
||||
})
|
||||
}
|
||||
|
||||
if (addon && hasEnvironmentLock(addon)) {
|
||||
if (options.length > 0) options.push({ type: 'divider' })
|
||||
options.push({
|
||||
id: 'toggle-side-lock',
|
||||
label: formatMessage(
|
||||
addon.side_toggle_unlocked ? messages.lockEnvironment : messages.unlockEnvironment,
|
||||
),
|
||||
icon: addon.side_toggle_unlocked ? LockIcon : LockOpenIcon,
|
||||
action: () => handleToggleSideLock(addon),
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -1205,10 +1319,11 @@ provideContentManager({
|
||||
disableAddContentTooltip: permissionDeniedMessage.value,
|
||||
contentTypeLabel: type,
|
||||
toggleEnabled: handleToggleEnabled,
|
||||
setEnabledFor: handleSetEnabledFor,
|
||||
canToggleItem: (item) =>
|
||||
item.enabledFor !== undefined && item.enabledFor.server === item.enabledFor.player,
|
||||
deleteItem: handleDeleteItem,
|
||||
bulkDeleteItems: handleBulkDelete,
|
||||
bulkEnableItems: handleBulkEnable,
|
||||
bulkDisableItems: handleBulkDisable,
|
||||
refresh: async () => {
|
||||
await contentQuery.refetch()
|
||||
},
|
||||
@@ -1245,6 +1360,8 @@ provideContentManager({
|
||||
: undefined,
|
||||
external: item.external ?? !hasModrinthProject,
|
||||
enabled: item.enabled,
|
||||
enabledFor: item.enabledFor,
|
||||
embeddedIcon: item.embeddedIcon,
|
||||
}
|
||||
},
|
||||
filterPersistKey: `server:${serverId}:${worldId.value}`,
|
||||
@@ -1274,13 +1391,13 @@ provideContentManager({
|
||||
:source-name="managedContent?.card.manager.name"
|
||||
:source-icon-url="managedContent?.card.manager.iconUrl"
|
||||
:header="formatMessage(messages.modpackContent)"
|
||||
enable-toggle
|
||||
enable-enabled-for
|
||||
show-environment-warnings
|
||||
:get-overflow-options="getOverflowOptions"
|
||||
:action-disabled="contentActionDisabled"
|
||||
:action-disabled-tooltip="contentActionBusyMessage ?? undefined"
|
||||
@update:enabled="handleModpackContentToggle"
|
||||
@bulk:enable="handleModpackBulkToggle($event, true)"
|
||||
@bulk:disable="handleModpackBulkToggle($event, false)"
|
||||
@update:enabled="handleModpackToggleEnabled"
|
||||
@update:enabled-for="handleModpackSetEnabledFor"
|
||||
@hide="isModpackContentModalOpen = false"
|
||||
/>
|
||||
<ContentUpdaterModal
|
||||
|
||||
@@ -1538,12 +1538,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Obsah se nepodařilo smazat"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Obsah se nepodařilo vypnout"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Obsah se nepodařilo zapnout"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Obsah se nepodařilo nahrát"
|
||||
},
|
||||
@@ -1559,9 +1553,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Obsah se nepodařilo odebrat"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Nepodařilo se přepnout {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Modpack se nepodařilo odpojit"
|
||||
},
|
||||
@@ -4470,4 +4461,3 @@
|
||||
"defaultMessage": "Typ"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1901,12 +1901,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Entfernen von Inhalten fehlgeschlagen"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Deaktivieren von Inhalten fehlgeschlagen"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Aktivieren von Inhalten fehltgeschlagen"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Aktualisieren von Inhalten fehlgeschlagen"
|
||||
},
|
||||
@@ -1922,9 +1916,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Entfernen von Inhalten fehlgeschlagen"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Umschalten von {name} fehlgeschlagen"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Entkoppeln von Modpack fehlgeschlagen"
|
||||
},
|
||||
@@ -6765,4 +6756,3 @@
|
||||
"defaultMessage": "Typ"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1901,12 +1901,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Inhalte konnten nicht gelöscht werden"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Inhalte konnten nicht deaktiviert werden"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Inhalte konnten nicht aktiviert werden"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Inhalte konnten nicht aktualisiert werden"
|
||||
},
|
||||
@@ -1922,9 +1916,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Entfernen von Inhalten fehlgeschlagen"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Umschalten von {name} fehlgeschlagen"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Modpack konnte nicht getrennt werden"
|
||||
},
|
||||
@@ -6765,4 +6756,3 @@
|
||||
"defaultMessage": "Typ"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -377,6 +377,9 @@
|
||||
"content.card.uploaded": {
|
||||
"defaultMessage": "Uploaded"
|
||||
},
|
||||
"content.card.use-enabled-for-controls": {
|
||||
"defaultMessage": "Use the Enabled for controls to enable or disable this content."
|
||||
},
|
||||
"content.confirm-bulk-update.admonition-body": {
|
||||
"defaultMessage": "Are you sure you want to update {count, plural, one {# project} other {# projects}} to their latest compatible version? It's recommended to update content one-by-one."
|
||||
},
|
||||
@@ -551,6 +554,30 @@
|
||||
"content.diff-modal.updated-count": {
|
||||
"defaultMessage": "{count} updated"
|
||||
},
|
||||
"content.enabled-for.description": {
|
||||
"defaultMessage": "Choose where this content is enabled. Turn both off to disable it."
|
||||
},
|
||||
"content.enabled-for.label": {
|
||||
"defaultMessage": "Enabled for"
|
||||
},
|
||||
"content.enabled-for.locked": {
|
||||
"defaultMessage": "These choices are locked. Select Unlock from the row menu to change them."
|
||||
},
|
||||
"content.enabled-for.not-supported-here": {
|
||||
"defaultMessage": "This content isn't designed to be enabled here."
|
||||
},
|
||||
"content.enabled-for.player": {
|
||||
"defaultMessage": "Player"
|
||||
},
|
||||
"content.enabled-for.please-wait": {
|
||||
"defaultMessage": "Please wait"
|
||||
},
|
||||
"content.enabled-for.required-here": {
|
||||
"defaultMessage": "This content needs to be enabled here to work correctly."
|
||||
},
|
||||
"content.enabled-for.server": {
|
||||
"defaultMessage": "Server"
|
||||
},
|
||||
"content.filter.disabled": {
|
||||
"defaultMessage": "Disabled"
|
||||
},
|
||||
@@ -1898,15 +1925,18 @@
|
||||
"header.category.resolutions": {
|
||||
"defaultMessage": "Resolution"
|
||||
},
|
||||
"hosting.content.enabled-for.lock-environment": {
|
||||
"defaultMessage": "Lock"
|
||||
},
|
||||
"hosting.content.enabled-for.unknown-environment": {
|
||||
"defaultMessage": "We couldn't tell where this content should be enabled. Review the Server and Player choices."
|
||||
},
|
||||
"hosting.content.enabled-for.unlock-environment": {
|
||||
"defaultMessage": "Unlock"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Failed to delete content"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Failed to disable content"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Failed to enable content"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Failed to update content"
|
||||
},
|
||||
@@ -1922,8 +1952,11 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Failed to remove content"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Failed to toggle {name}"
|
||||
"hosting.content.failed-to-set-enabled-for": {
|
||||
"defaultMessage": "Failed to change where {name} is enabled"
|
||||
},
|
||||
"hosting.content.failed-to-set-side-lock": {
|
||||
"defaultMessage": "Failed to change the environment lock for {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Failed to unlink modpack"
|
||||
|
||||
@@ -1901,12 +1901,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Error al eliminar el contenido"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Error al desactivar el contenido"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Error al activar el contenido"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Error al actualizar el contenido"
|
||||
},
|
||||
@@ -1922,9 +1916,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Error al eliminar el contenido"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Error al activar/desactivar {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Error al desvincular el modpack"
|
||||
},
|
||||
@@ -6765,4 +6756,3 @@
|
||||
"defaultMessage": "Tipo"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1871,12 +1871,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "No se ha podido eliminar el contenido"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "No se ha podido desactivar el contenido"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "No se ha podido habilitar el contenido"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "No se ha podido actualizar el contenido"
|
||||
},
|
||||
@@ -1892,9 +1886,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "No se ha podido eliminar el contenido"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "No se ha podido alternar {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "No se ha podido desvincular el modpack"
|
||||
},
|
||||
|
||||
@@ -542,12 +542,6 @@
|
||||
"header.category.resolutions": {
|
||||
"defaultMessage": "Resolusyon"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Bigong mapatay ang tumutunggaling kontento"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Bigong mabukas ang kontento"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Bigong ma-unlink ang modpack"
|
||||
},
|
||||
@@ -2736,4 +2730,3 @@
|
||||
"defaultMessage": "Uri"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1763,12 +1763,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Impossible de supprimer le contenu"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Impossible de désactiver le contenu"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Impossible d'activer le contenu"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Impossible de mettre à jour le contenu"
|
||||
},
|
||||
@@ -1784,9 +1778,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Impossible de retirer le contenu"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Impossible de basculer {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Impossible de dissocier le modpack"
|
||||
},
|
||||
|
||||
@@ -1595,12 +1595,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Nem sikerült törölni a tartalmat"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "A tartalom letiltása nem sikerült"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "A tartalom engedélyezése nem sikerült"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "A tartalom frissítése sikertelen"
|
||||
},
|
||||
|
||||
@@ -1847,12 +1847,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Errore nella rimozione dei contenuti"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Errore nella disabilitazione dei contenuti"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Errore nell'abilitazione dei contenuti"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Errore nell'aggiornamento dei contenuti"
|
||||
},
|
||||
@@ -1868,9 +1862,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Errore nella rimozione dei contenuti"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Impossibile cambiare lo stato di \"{name}\""
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Errore nello scollegamento del pacchetto"
|
||||
},
|
||||
|
||||
@@ -1718,12 +1718,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "コンテンツの削除に失敗しました"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "コンテンツの無効化に失敗しました"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "コンテンツの有効化に失敗しました"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "コンテンツの更新に失敗しました"
|
||||
},
|
||||
@@ -1739,9 +1733,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "コンテンツの削除に失敗しました"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "{name}の切り替えに失敗しました"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Modパックのリンク解除に失敗しました"
|
||||
},
|
||||
|
||||
@@ -1793,12 +1793,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "콘텐츠 삭제 실패"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "콘텐츠 비활성화 실패"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "콘텐츠 활성화 실패"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "콘텐츠 업데이트 실패"
|
||||
},
|
||||
@@ -1814,9 +1808,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "콘텐츠 제거 실패"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "{name} 전환 실패"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "모드팩 연결 해제 실패"
|
||||
},
|
||||
|
||||
@@ -1328,12 +1328,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Gagal memadamkan kandungan"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Gagal menyahdayakan kandungan"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Gagal mendayakan kandungan"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Gagal mengemas kini kandungan"
|
||||
},
|
||||
@@ -1349,9 +1343,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Gagal mengalih keluar kandungan"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Gagal menogol {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Gagal menyahpaut pek mod"
|
||||
},
|
||||
@@ -4959,4 +4950,3 @@
|
||||
"defaultMessage": "Jenis"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1712,12 +1712,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Het verwijderen van inhoud is mislukt"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Het uitschakelen van inhoud is mislukt"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Het inschakelen van inhoud is mislukt"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Het bijwerken van inhoud is mislukt"
|
||||
},
|
||||
@@ -1733,9 +1727,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Het verwijderen van inhoud is mislukt"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Het omschakelen van {name} is mislukt"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Het ontkoppelen van het modpack is mislukt"
|
||||
},
|
||||
|
||||
@@ -1865,12 +1865,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Nie udało się usunąć zawartości"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Nie udało się wyłączyć zawartości"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Nie udało się włączyć zawartości"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Nie udało się zaktualizować zawartości"
|
||||
},
|
||||
@@ -1886,9 +1880,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Nie udało się usunąć zawartości"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Nie udało się przełączyć {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Nie udało się rozłączyć od paczki modów"
|
||||
},
|
||||
|
||||
@@ -1829,12 +1829,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Falha ao excluir o conteúdo"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Falha ao desativar o conteúdo"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Falha ao ativar o conteúdo"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Falha ao atualizar o conteúdo"
|
||||
},
|
||||
@@ -1850,9 +1844,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Falha ao remover o conteúdo"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Falha ao alterar {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Falha ao desvincular o pacote de mods"
|
||||
},
|
||||
@@ -6429,4 +6420,3 @@
|
||||
"defaultMessage": "Tipo"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -827,12 +827,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Nu s-a putut șterge conținutul"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Nu s-a putut dezactiva conținutul"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Nu s-a putut activa conținutul"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Nu s-a putut actualiza conținutul"
|
||||
},
|
||||
@@ -848,9 +842,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Nu s-a putut înlătura conținutul"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Nu s-a putut comuta {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Nu s-a putut nelinka pachetul de moduri"
|
||||
},
|
||||
|
||||
@@ -1820,12 +1820,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Не удалось удалить контент"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Не удалось отключить контент"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Не удалось включить контент"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Не удалось обновить контент"
|
||||
},
|
||||
@@ -1841,9 +1835,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Не удалось удалить контент"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Не удалось переключить {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Не удалось отвязать сборку"
|
||||
},
|
||||
|
||||
@@ -1664,12 +1664,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Greška sa brisanjem sadržaja"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Nije uspelo onemogućavanje sadržaja"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Nije uspelo omogućavanje sadržaja"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Ažuriranje sadržaja nije uspelo"
|
||||
},
|
||||
@@ -1685,9 +1679,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Greška sa brisanjem sadržaja"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Greška sa preklapanjem {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Greška sa uklanjanjem veze od modpacka"
|
||||
},
|
||||
@@ -5658,4 +5649,3 @@
|
||||
"defaultMessage": "Tip"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1712,12 +1712,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Kunde inte radera innehåll"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Kunde inte inaktivera innehåll"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Kunde inte aktivera innehåll"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Kunde inte uppdatera innehåll"
|
||||
},
|
||||
@@ -1733,9 +1727,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Kunde inte radera innehållet"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Kunde inte växla {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Kunde inte avlänka modpaketet"
|
||||
},
|
||||
|
||||
@@ -242,9 +242,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "ไม่สามารถลบเนื้อหาได้"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "ไม่สามารถปิดใช้งานเนื้อหาได้"
|
||||
},
|
||||
"icon-select.edit": {
|
||||
"defaultMessage": "แก้ไขไอคอน"
|
||||
},
|
||||
@@ -645,4 +642,3 @@
|
||||
"defaultMessage": "ภาษา"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1841,12 +1841,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "İçeriği silmek başarısız"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "İçeriği kapatmak başarısız"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "İçeriği açmak başarısız"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "İçeriği güncelleme başarısız"
|
||||
},
|
||||
@@ -1862,9 +1856,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "İçerik çıkarılamadı"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "{name} durumu değiştirilemedi"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Mod paketinin bağlantısı koparılamadı"
|
||||
},
|
||||
|
||||
@@ -1787,12 +1787,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Не вдалося видалити вміст"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Не вдалося вимкнути вміст"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Не вдалося ввімкнути вміст"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Не вдалося оновити вміст"
|
||||
},
|
||||
@@ -1808,9 +1802,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Не вдалося видалити вміст"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Не вдалося перемкнути {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Не вдалося відв’язати збірку"
|
||||
},
|
||||
|
||||
@@ -1346,12 +1346,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "Không thể xóa nội dung"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "Không thể tắt nội dung"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "Không thể bật nội dung"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "Không thể cập nhật nội dung"
|
||||
},
|
||||
@@ -1367,9 +1361,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "Không thể xóa nội dung"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "Lỗi khi chuyển đổi {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "Hủy liên kết modpack thất bại"
|
||||
},
|
||||
@@ -4974,4 +4965,3 @@
|
||||
"defaultMessage": "Đội ngũ Modrinth"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1901,12 +1901,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "删除内容失败"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "禁用内容失败"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "启用内容失败"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "更新内容失败"
|
||||
},
|
||||
@@ -1922,9 +1916,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "移除内容失败"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "无法切换 {name}"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "链接整合包失败"
|
||||
},
|
||||
@@ -6765,4 +6756,3 @@
|
||||
"defaultMessage": "类型"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1901,12 +1901,6 @@
|
||||
"hosting.content.failed-to-bulk-delete": {
|
||||
"defaultMessage": "無法刪除內容"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-disable": {
|
||||
"defaultMessage": "無法停用內容"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-enable": {
|
||||
"defaultMessage": "無法啟用內容"
|
||||
},
|
||||
"hosting.content.failed-to-bulk-update": {
|
||||
"defaultMessage": "無法更新內容"
|
||||
},
|
||||
@@ -1922,9 +1916,6 @@
|
||||
"hosting.content.failed-to-remove": {
|
||||
"defaultMessage": "無法移除內容"
|
||||
},
|
||||
"hosting.content.failed-to-toggle": {
|
||||
"defaultMessage": "無法切換「{name}」"
|
||||
},
|
||||
"hosting.content.failed-to-unlink": {
|
||||
"defaultMessage": "無法取消連結模組包"
|
||||
},
|
||||
@@ -6765,4 +6756,3 @@
|
||||
"defaultMessage": "類型"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ const meta = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Toggle uses touch manipulation so double tapping on mobile does not zoom.',
|
||||
component:
|
||||
'Toggle uses touch manipulation so double tapping on mobile does not zoom. Set indeterminate to show a line between its on and off states.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -30,6 +31,13 @@ export const Checked: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Indeterminate: Story = {
|
||||
args: {
|
||||
modelValue: false,
|
||||
indeterminate: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
modelValue: false,
|
||||
@@ -63,6 +71,9 @@ export const AllStates: Story = {
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="true" /> On
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="false" :indeterminate="true" /> Indeterminate
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="false" :disabled="true" /> Disabled
|
||||
</div>
|
||||
@@ -73,6 +84,9 @@ export const AllStates: Story = {
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="true" :small="true" /> On
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="false" :indeterminate="true" :small="true" /> Indeterminate
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="false" :small="true" :disabled="true" /> Disabled
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user